Jim Karabatsos - GUI Computing
OLE For VB Programmers
In this article, we are going to take a look at what constitutes an 'object', in the Windows environment, and how they are implemented. We will look at issues of polymorphism and how this term is used when referring to Windows, or more correctly 'COM', objects and the vital role played by Interfaces in this environment.
Object? What's an Object?|
OOP Under the Covers
The Pointer to a Pointer...
Early and Late Binding|
OLE Automation : IDispatch
The Great IUnknown...
The VB Angle
Holler for a Marshall|
Inheritance and Polymorphism
The COM Angle
Getting that first Interface ...
Object Creation and the Class Factory
Set ObjXXX = Nothing
Object? What's an Object?
Quite simply, an object represents a real-world 'thing'. It encapsulates our knowledge of that 'thing'. In many ways, it is similar to a record (or a User Defined Type) because it typically has fields that contain values for various attributes of that real-world thing. It differs from a record in that it also has procedures (that we call 'methods' in OOP terminology). The data and methods are bound together to form an 'object'.
There are several problems associated with the traditional (record-based) approach to modelling the real world, in which we would typically define a record type and a set of procedures that acted upon that record type (as in AddCustomer, FindCustomer, etc.). Most of the problems with this approach stem from the simple fact that any particular instance of a record type is owned by the user of that record type. By the simple reality that all that 'client' code (spread throughout an application) had access to the field data of the record, that code could act upon the fields directly. This is rather than calling upon the pre-defined set of record-manipulation procedures that knew how to enforce all the required business rules. If we needed to make any change in the way that data was represented in a record, or to enforce some additional logic dictated by changing business requirements, we would need to go through the whole application with a fine-tooth comb trying to identify all the code that was affected and hope that we were able to get it right. Maintenance (and natural evolution) of a system became every programmers nightmare.
In contrast, 'pure' OOP systems do not allow object users to directly access the data fields of an object. Instead, all access to object data must be gated through methods that provide read and/or write access to the data. By inserting this code layer between client code and the data, we are able to enforce a separation of the interface from the implementation, which in turn allows us to enhance and evolve the implementation without breaking client code, provided of course that interface does not change.
Of course, not all OOP systems are created equal. The more commonly-used OOP languages are really hybrids. They allow programmers the freedom to allow direct access to the data inside an object. This includes C++ and Delphi and, in so far as it can be considered an OOP language, VB as well.
OOP Under the Covers
It is only by looking at how OOP languages actually work behind the scenes that we can understand the way that OLE objects work. This might get a little esoteric, but stick with it. All you really need to remember is that a pointer is nothing more than a variable that holds the address of another variable.
Each instance of an object has an individual, separate area of memory set aside for the storage of its data members. However, all instances of the same object type share the one set of method code. Objects are (almost) always accessed via a pointer.
An object reference (or variable) is a pointer that points to a data structure. This data structure contains a pointer to a methods table, followed by the instance data members.
Note how the object variable itself is a pointer to a pointer to a method table, which itself contains pointers to the actual code for the methods.
Fun, isn't it? This will be important later.
Two object variables that contain the same pointer value are references to the same object instance.
As you can see, whether you use Object1 or Object2, you are referencing the same data area. Both these object variables are references to the one object instance.
Two object variables that point to different pointers but those pointers contain the same value are separate instances of the same object type.
These two object variables share the same set of methods (i.e. code), but that code operates on different data.
Early and Late Binding
If the compiler knows what type of object an object variable is pointing to, it can compile a call directly to the method code without going through the Virtual Method Table (VMT). Such methods are called 'Static' methods in more traditional OOP languages. Neither Visual Basic nor OLE support static methods.
If an object variable can refer to different object types at run-time, then the compiler must generate code to follow the pointer to a pointer to the VMT, then look up the address of the method code in the VMT in order to call it. These methods are called either 'Virtual' or 'Dynamic' methods (depending on the actual mechanism employed in resolving the reference). In traditional OOP environments (such as C++ or Delphi), virtual methods are considered to be late bound (as compared to static methods, which are early bound).
With VB and OLE, there is no earlier binding possible, so access to methods via the VMT is called early binding. Late binding in OLE refers to method invocation via the OLE Automation interface.
OLE Automation : IDispatch
The OLE Automation (or IDispatch interface) is a particular set of standard methods that can be called by a client to determine:
This is called 'Late Binding' in OLE (and therefore in VB).
Early binding is much more efficient than late binding and about five times faster. Early binding also allows the compiler to check for errors before they occur. It can do this because it uses a Type Library to obtain information about the capabilities of an object. However, late binding is more flexible than early binding; for example, it is possible for clients to use objects created after the client was written and compiled.
In Visual Basic, you use early binding (i.e. the VMT) if you define an object to be of a particular object type:
On the other hand, when you define a generic object variable ( 'As Object' ), then you are going through the OLE Automation interface:
OLE objects conform to the Component Object Model (COM). This is a Microsoft specification that allows objects to be shared and re-used at a binary level. Some of the COM specification is implemented by the OLE system software; the rest is implemented by each OLE object.
In defining the Component Object Model, Microsoft had some specific goals. Before COM (and related standards such as CORBA and SOM), objects could only really be used within the one program. Sure, we could also re-use object source code in other programs, but object instances could not be shared across program boundaries. COM defines a binary standard that allows objects to be used across process boundaries and, in the latest incarnation (DCOM, or Distributed COM), even across machine boundaries.
COM defines an object to be a pointer to a pointer to a table of function addresses. Now, where have we seen that data structure before? This was obviously chosen to match the object format used by popular C++ compilers (and Delphi <g>). The table of addresses (that we have called the VMT until now) is refered to as a VTable in COM.
The Great IUnknown ...
All COM objects define one or more interfaces. Each interface defines a set of methods and corresponds to a separate VTable. All VTables have three functions defined first and in a particular order:
Any VTable that defines these three methods (first, and in this order) can be treated as an IUnknown VTable, or interface. The fact that COM requires that all interfaces start with these three methods in this order means that we can treat any COM interface as an IUnknown interface. This is no accident, for the functions in IUnknown are those that allow a client of an object to get a pointer to any other VTable that the object makes available. If you have a pointer to any VTable, (i.e.if you have any Interface pointer) then you can get a pointer to any other interface that is supported by the object.
Recall that an interface is a set of (related) functions that are grouped together in a VTable in a particular order. COM defines a lot of interfaces that are used by OLE, and you can define as many new ones as you like. Each interface is given a unique IID, which is a unique 128-bit number. IIDs are generated by an algorithm that uses the date and time and (if present) a network adaptor's unique ID. It has been estimated that the chances of generating duplicate IIDs is about the same as the chance of two random atoms colliding to spontaneously create a ham sandwich – in other words, don't sweat it <g>.
All Interfaces must include the QueryInterface function first in their VTable. This means that you can call QueryInterface through any interface of a COM object. You pass QI the IID you are interested in and, if that interface is supported, you get back a valid interface pointer. If not, you get an error. The QI mechanism allows you to ask an object 'can you do this' in a safe way. If it says 'yes', you have a pointer you can use to access those facilities. If it says 'no', you don't have any pointer so you have no mechanism to make a call to an unsupported function.
COM essentially breaks an object into a set of interfaces, each one providing some related set of functions.
The VB Angle
For VB public classes, the compiler creates 'dual mode' interfaces. All methods of a class are lumped together into a single VTable and an interface is thereby created for it. An IDispatch interface is also created allowing late-bound access to objects through OLE Automation.
Note that COM does not support direct access to data members (i.e. 'public variables'). If these are used in VB classes, the compiler generates simple Get/Let property procedures and includes them in the VTable instead.
Holler for a Marshal
One of the principles of COM is location transparency. The client has a pointer that it can call in the context of the client process. The object references parameters and pointed-to data from its own context.
In order for this to work, OLE provides support for packing parameters and transferring them into the context of the object, then packing the results and returning them back to the caller. This process is called marshalling.
For local (i.e. In-Process) servers, there is no work involved in marshalling because both the object and the client are in the same process. This means that an early-bound (i.e. VTable) call to an in-process OLE server is no less efficient than a call to a virtual method of a C++ or Delphi object, within a single program.
For Out-of-Process servers, data must be marshalled across the process boundaries. With the remote OLE features in VB4EE (or the newer DCOM specification, which used a totally different mechanism), this could even involve transferring information across machine boundaries. The important thing to realise is that this is not a concern of either the object or the client; the OLE system software is responsible for marshalling. It has built-in support for most pre-defined interfaces, and you can use MIDL to define other interfaces and create custom marshalling for them.
Inheritance and Polymorphism
Inheritance allows a new object type to be defined as a descendent of an existing object type. This mechanism allows the new object type to start with all the capabilities of the object that it descends from and means that the code for the new object type needs to address only those parts of the behaviour and capabilities that are new or changed. This can save a lot of coding and can significantly aid in maintenance, if the object hierarchy thus formed is well planned (of course, as in most areas of programming, most aren't).
Inheritance is also the mechanism used to implement polymorphism. In 'classical' OOP, any object variable can safely be set to point to an instance of any descendent object type.
Another way of looking at this is that any object instance can safely be treated as if it was an instance of any of its ancestor object types. This is because all descendants must have all their parents' data members and methods and these always come before the new ones. A client can safely treat a child object instance as if it was an instance of its parent type. However, the reverse is not true: a parent instance cannot safely be treated as an instance of one of its descendent object types.
The COM Angle
Inheritance is not supported in COM. While it is useful within an application where you have access to parent source, it does not really lend itself to use across applications or in components. However, polymorphism is very much A Good Thing in COM, so we need a different mechanism to implement it.
Polymorphism means two (different) things in COM :
OLE objects are said to be polymorphic if they support the same interfaces or some equivalent subset. For example, Version 1 of an object supports the three interfaces IA, IB and IC, whereas Version 2 supports IA, IB, IC, ID and IE. A V2 object can safely be treated as a V1 object but not vice versa.
Note that if clients of a V2 object are written well, they can safely be passed a V1 object too. This is because they can use QueryInterface to determine whether the two new interfaces are supported and, if not, they can refrain from calling those new interfaces.
If an interface extends another interface, then those interfaces are considered to be polymorphic. Because all interfaces extend IUnknown, all interfaces can be treated as if they were an IUnknown interface (or, if you prefer, all interfaces are polymorphic with IUnknown). There are many examples in OLE where extended interfaces have been defined; often the newer ones end with '2' or 'Ex'.
Getting that first Inheritance ...
We have already seen that all interfaces implement the QueryInterface method, so given any interface pointer we can ask for any other. The question remains : "How do we get the first instance?"
Object Creation and the Class Factory
OLE provides a standard mechanism for servers to expose the creation process. An OLE Server also implements a Class Factory object, whose job it is to create objects of that class.
The OLE software uses the registry to determine which server (DLL, EXE) provides the requested object based in the CLSID (which is another of those 128-bit unique numbers, generically referred to as GUIDs – Globally Unique Ids – in COM and OLE). It then loads the module and gets a IClassFactory interface pointer, which it then uses to create the actual object. The details of object creation are different depending on whether the server is a DLL or an EXE (and whether it is remote). It is far beyond the scope of this article to go into the details, but you should get a hold of 'Inside OLE' by Craig Brockschmidt (Microsoft Press, Second Edition) if you are at all serious about OLE.
All objects maintain a count of how many active interface pointers have been given out. Whenever you successfully call QueryInterface, the reference count is incremented by one. If you duplicate the pointer, you should call AddRef to further increment the reference count. When you no longer need an interface pointer (or when it is about to go out of scope), you should call Release to decrement it. When the reference count drops to zero, the OLE object is able to destroy itself.
VB does a lot of the AddRef/Release calling for you, and with OLE Automation, there are usually verbs to free or 'close' an object that do the same thing.
Set objXXX = Nothing
VB will call Release for an object variable when it goes out of scope. If you want to release it yourself, you can assign Nothing to it, which will result in Release being called first.
In VB3, several buglets prevented VB from always correctly calling Release, so setting object variables to Nothing is strongly recommended.