[Dr. Chris Oakley's home page] [Portuguese translation of the page] [Romanian translation of this page]
I posted this article originally in 1997. OLE Automation was for me fascinating because of its complexity and impenetrable documentation, but when, between jobs, I got the chance to try and crack it once and for all I made the welcome discovery that, although much more complicated than it needed to be, the principles were relatively simple. The technology actually includes some genuinely clever things, particularly in regard to inter-process communication.
It is now 2012. Since then the Microsoft C++ compiler has come a long way, and .NET has happened. That .NET owes so much to COM does, to a large extent, endorse the principles upon which COM is based.
COM, the "Component Object Model", is a cross-language standard developed by Microsoft for specifying functions and classes supplied by libraries or executables which may include a binary description of contents known as a "Type Library". Methods of COM objects can be executed across process or machine boundaries through a mechanism termed "OLE Automation" - although the terms COM and "OLE Automation" are often used interchangeably. This remoting machinery is provided seamlessly and often invisibly by Microsoft. The main reason for taking an interest nowadays is that it is a COM object model that underlies Visual Basic for Applications, and this is still the programming language in most widespread use.
Well-designed COM classes should be very easy to use: it is the way that non-programmers can do object-oriented programming.
Creating the classes in C++, though, is not so easy. Microsoft have created a variety of tools to assist the process, but although "#import" is a boon, most of the others I personally find unhelpful: a particularly annoying feature being the need to inherit an implementation of a class from an interface. This means that, to take a finance example, you are prevented from inheriting ConvertibleBond from Bond - which you would normally want to do - because each has to inherit from its corresponding interface class instead. There are ways around this, but they are complicated. To me, the class hierarchy should be the one most appropriate for the task in hand, and should not be dictated by the programming environment.
Visual basic/COM is a lot like a cut-down version of C++, but with a layer which includes automatic bounds checking and reference counting to prevent VB scripts from crashing the program. It has the additional facility of being able to execute methods as remote procedure calls. Unlike C++, this is built-in and requires no extra machinery.
The C++ declaration
corresponds to
Dim T As ObjectType, TArray(0 To 5) As ObjectType
in VB. "ObjectType" is a user-defined class, which in the VB case is a COM object. Internally, each object is a pointer to a C++ object derived from IDispatch, an abstract class that contains two principal services: (i) garbage collection and (ii) dynamic (or "late") binding. Let us examine these in turn:
(i) Garbage collection
Consider the following piece of C++:
ObjectType *T = new ObjectType, *U;
U = T;
T->DoSomething();
delete T;
U->DoSomething();
Of course this will probably crash the program, and of course you have never done this or the equivalent because you realise the danger. Good. Neither have I. Well, almost never... VB has much less trust in you. VB’s attitude is that you must not crash the program under any circumstances, and so it does not allow you to delete objects. Instead, the environment keeps track of every object being used in the program, and when an object is no longer in use it gets deleted automatically. The paradigm which has deletion of objects no longer in use controlled by the environment rather than the programmer is known as Garbage Collection. In a garbage-collected environment the code is shorter, but, more importantly, it is impossible to access a deleted or invalid object. The equivalent VB
Dim T As New ObjectType, U As ObjectType
Set U = T
T.DoSomething
Set T = Nothing
U.DoSomething
to the above will not give an error because the attempt to delete the object by Set T = Nothing removes a reference, but does not actually delete it. The object will not be deleted until the variable U goes out of scope. Garbage collection is a feature of all COM objects and is implemented by deriving all COM classes from the abstract class IUnknown:
struct IUnknown
{
virtual HRESULT QueryInterface(const GUID * const riid, void **ppvObject) = 0;
virtual unsigned long AddRef() = 0;
virtual unsigned long Release() = 0;
};
The last two methods are for garbage collection. Interest in an object is expressed by creating it or by calling AddRef; no further interest is expressed by calling Release. The object is responsible for deleting itself when Release is called one more time than there were previous AddRef calls. This is most commonly implemented by having an unsigned long reference count m_refs which is set to one when the object is constructed, with AddRef and Release implemented as follows:
unsigned long DerivedFromIUnknown::AddRef() {return m_refs++;}
unsigned long DerivedFromIUnknown::Release()
{
if (--m_refs > 0) return m_refs;
delete this; // destructor must be declared as virtual for this to work
return 0; // Note that we must not reference m_refs after the "delete this"
}
(the return values are just for debugging purposes). This scheme is much the simplest kind of garbage collection, but has an important caveat, and one that has contributed to the demise of COM. If object A has object B as a property, with object B simultaneously having object A as a property, a situation known as a cyclic reference, then these mutual references will remain when the external references from the program (known as "roots") are taken away. The individual reference counts thus never reach zero, and the objects never get deleted, giving us a memory leak! Microsoft deal with this problem by telling you, when creating COM classes in VB, just to make sure that you never have cyclic references! If you do, then the subsequent memory leaks are entirely your own fault, apparently. Personally, I find cyclic references to be useful, and resent the restriction. You can, of course, just manually break the cycles at strategic points to prevent leaks, but a better solution is to have more sophisticated garbage collection. Microsoft clearly think so: the .NET garbage collection is perfectly capable of dealing with cyclic references.
This brings us to one thing in favour of COM over .NET: in COM there is no requirement to implement AddRef and Release in the way shown here. More sophisticated schemes that properly deal with cyclic references are possible (something, indeed, that has been explored, if not actually documented, by the author). In .NET, on the other hand, a "managed" object is subject to Microsoft's garbage collector, which you may or may not like, and you cannot substitute your own. However, the asynchronous mark-sweep scheme they use has disadvantages, namely (i) it makes the processor work unnecessarily hard and (ii) one does not know when an object, marked as garbage, is going to be finally deleted, which discourages one from doing anything substantial in a destructor.
The first method in IUnknown is for the benefit of COM itself. The globally-unique-ID (GUID) is a sixteen-byte identifier that identifies the object to COM. The object name and the name of the program or library that services it are normally enough to identify the object, but the GUID is intended to be unique not merely to the object/library, but to a particular version of it. It comes into its own in, for example, structured storage. Here a file, known as a container, contains a COM object which has embedded within it further COM objects, a nesting which may continue to arbitrary depth. The GUID at the head of each of these streams identifies the bytes that follow. The matching of GUIDs to the program/library/version that services it is a function carried out by the System Registry, and enables an application to open and view OLE documents without needing to know what they are. This is the mechanism by which, for example, you may embed a portion of an Excel spreadsheet in a Word document. The danger of using names and not GUIDs is that a new version of the program might name the streams the same but use a different file format (although, having said that, they could just as easily forget to provide a new GUID, but let’s not dwell on that). QueryInterface is called in VB when it needs to verify that the object is indeed an OLE automation object (i.e. inherited from IDispatch, a sub-class of IUnknown - to be investigated next), which is when the object is created through VB CreateObject or its equivalent. An implementation might be as follows:
const GUID IID_DerivedFromIUnknown = {0x00000000, 0x0000, 0x0000, 0x00, 0x00, 0x0, 0x0, 0x00, 0x00, 0x00, 0x01};
(replace these numbers with a unique ID obtained from GUIDGEN.exe).
HRESULT DerivedFromIUnknown::QueryInterface(const GUID * const riid, void **ppv)
{
if (riid == IID_IUnknown || riid == IID_IDispatch || riid ==
IID_DerivedFromIUnknown)
{
*ppv = this;
AddRef();
return NOERROR;
}
*ppv = NULL;
return ResultFromScode(E_NOINTERFACE);
}
To verify that an IUnknown is also an IDispatch, COM passes in the GUID for IDispatch to this method after the object is created, giving "Run-time error ‘430’: Class doesn’t support OLE Automation" if a negative response is given (clearly, it will not here). QueryInterface is useful when IDispatch is only one of many interfaces that the object supplies. Pointers to the other interfaces, implemented in related, but separate objects, should also be obtainable by invoking this method with the relevant GUIDs.
(ii) Dynamic Binding
In COM, the type of an object need not be known until run time. The VB declaration
Dim T As Object
identifies T as an object that supports IUnknown (so it need not even be of type IDispatch, although if any processing is done, QueryInterface will be called on it to get an IDispatch interface). Before VB became aware of Type Libraries, this was the only kind of user-defined object that was possible. This meant that a statement like
x = T.Method(param1, param2)
had to be handled in a way that assumed no prior knowledge of the object T. The string "Method" would be passed to the object, with the question, "do you support this?", using the IDispatch method GetIDsOfNames. If the answer was "yes" an ID would be passed back, which could then be fed into method Invoke with the parameters, if any, to execute the required action. For maximum generality, the parameters would be passed as a variable length array of the polymorphic VARIANT types, and the Invoke method would return a VARIANT. If the method returned a value, then it was up to VB to coerce it into the type of the variable being assigned to. The assignment and querying of "data" members are special cases of this. The assignments
T.Prop = value
value = T.Prop
which set or get the property Prop of the object merely trigger property put and property get methods (since the access to the internal data is only though these methods, data hiding is thus built in, a necessity in any case as the data may reside in a different memory space. This makes statements like
List1.Left = 100
possible, which not only assigns the left hand edge property of the object List1 to 100, but moves the control as well).
Here is the definition of IDispatch:
struct IDispatch : IUnknown
{
virtual HRESULT GetTypeInfoCount(unsigned int *pctinfo) = 0;
virtual HRESULT GetTypeInfo(unsigned int itinfo, unsigned long lcid, ITypeInfo **pptinfo) = 0;
virtual HRESULT GetIDsOfNames(const GUID * const riid, char *rgszNames, unsigned int cNames, unsigned long lcid, long *rgdispid) = 0;
virtual HRESULT Invoke(long dispidMember, const GUID * const riid, unsigned long
lcid, unsigned short wFlags,
DISPPARAMS *pdispparams, VARIANT *pvarResult,
EXCEPINFO *pexcepinfo, unsigned int *puArgErr) = 0;
};
The first two methods provide the link to a type library, the equivalent of a header file, to be explored next, which contains information about the object (they are the equivalent of the .NET getType(), but much harder to use). The other two provide the dynamic method invocation functionality outlined before. These are most easily implemented using OLE functions DispGetIDsOfNames and DispInvoke which use the definition of the object in the type library to give the appropriate responses.
The type library is the mechanism by which VB, or any other container, gets to know about user-defined objects. It contains the information here that makes possible declarations of the form
Dim T As ObjectType
where ObjectType is a class defined in the type library.
The source file, with extension IDL (Interface Description Language) is an extended version of a C++ header file. This is compiled into a binary of type TLB (Type LiBrary) by a program called MIDL. An IDL file can be included into the project. Making VB or VBA aware of it is then a simple matter of locating and including the TLB from the Tools/References dialog. If this is successful, the objects will be visible in the VBA Object Browser.
To understand the syntax of an IDL file it is necessary to realise that remote procedure calls are an essential part of COM. The object may reside in a different memory space, or even a different machine. Pointers cannot therefore be sent. Pointers only appear as parameters in order to mark out chunks of memory which are to be sent across the process boundary, or to mark out memory for receiving the reply. Concessions are not made for COM servers (in-process or DLL servers) that reside in the same memory space. The keywords in and out are applied to each parameter in each method, to indicate whether it is to be sent, received, or both. There are restrictions on types. Arrays must be of type SAFEARRAY which contains bound information. Strings are of type BSTR, which contains a character count. The memory management of both these types must be done by COM, which provides APIs for the purpose. A pointer to another COM object, however, may be passed, in which case, if the object is an a separate process, COM sets up the machinery (a proxy for the object at the client end, and a stub at the server) to enable remote manipulation. More details in the appendix. A type library may also include help strings and context-sensitive references to a help file. It also provides an alternative to BAS files for flat function declarations in DLLs through the module declaration. The parameters of functions in a module section are however subject to the same stringencies as object methods (albeit unnecessarily as the DLL resides in the same memory space).
Here is the IDL file in the code sample:
[uuid(C7E9002B-9E7F-43b5-971D-E2539E6039C2), version(1.0), helpstring
("COMDemo: Demo of COM object defined in C++")]
library COMDemo
{
A uuid is needed for the library as whole to enable OLE to track it in the System Registry. The program GUIDGEN.exe generates these (supposedly) globally unique IDs. When the output TLB is referenced in VB/VBA, the Object Browser will show the library name and help string given here to assist identification. We use this uuid to get a pointer to the type library via LoadRegTypeLib.
importlib("stdole2.tlb");
To obtain the definitions of IDispatch, etc.
[uuid(2BB79939-EE89-4ae0-BF7D-E7FB175A87CF), oleautomation, dual, hidden]
interface SimpleDispatch : IDispatch
{
/* These two methods are just to occupy two slots in the virtual function table.
They will never be directly called by a container ... */
[hidden] HRESULT VirtualDestructor([in]IUnknown *stream); // spurious parameter hides
function from Object Browser
[hidden] IUnknown *IID_This(); // incorrect parameter hides function from Object
Browser
};
An interface section defines an early-bound interface - normally, but not necessarily, a C++ class being accessed through its virtual function table. SimpleDispatch has here been created as a convenient base class for other COM objects. As the destructor is declared virtual at this level it will occupy a space in the virtual function table after the IDispatch functions, and needs to be accommodated. The next function is also an internal one for obtaining the class IID. Both will be hidden from the client. HRESULT is a standardised OLE error code (and will not appear in the Object Browser). One can raise VB errors by returning something other than NOERROR. Unless "On Error" is set, this will interrupt VBA program execution and pop up an message box. If the final parameter has attribute retval, then this is the return value of the method, or property get, and appears in the Object Browser as such.
// Custom class definitions ...
[uuid(7C8721D6-3D22-48a1-A945-5FF9815C5807), odl, oleautomation, dual,
helpstring("Name/value pair")]
interface ITestObj : SimpleDispatch
{
[propget, helpstring("Name of quantity")] HRESULT Name([out,retval]BSTR *);
[propput] HRESULT Name([in]BSTR);
[propget, id(0), helpstring("Value (default property)")]
HRESULT Value([out, retval]double *);
[propput, id(0)]
HRESULT Value([in]double); [helpstring("square of value")]
HRESULT Square([out,retval]double *square);
};
Here, finally, is the definition of a simple COM class. The attribute oleautomation combined with the (albeit indirect) derivation from IDispatch makes the interface visible to VB. The attribute dual indicates the methods may be called either directly using early binding, or indirectly using late binding (using IDispatch::Invoke). The class here comprises simply of string property Name, a numerical property Value and a method Square for returning the square of Value. Although there are five functions here, the VBA Object Browser will show just two properties and one method (and one can make properties read-only by omitting the propput method). The attribute id(0) makes the value property the "default" property of the object which (apart from in a Set statement) is used when the object name is used without qualification.
[uuid(5FC711F1-B9C7-4dcc-8CCC-E39F9E0F7556)]
coclass TestObj
{
interface ITestObj;
};
};
A coclass section groups one or more interface and dispinterface definitions together (a dispinterface is similar to an interface, but only supports late binding). There is only one interface here, but if there is more than one the interfaces must be mutually accessible via QueryInterface. Also, a coclass must be creatable, which is to say one should be able to create an instance of the object using (in VB):
Dim T As Object
Set T = CreateObject("COMDemo.TestObj")
or, if one has a Type Library
Dim T As COMDemo.TestObj
Set T = New COMDemo.TestObj
or simply
Dim T As New COMDemo.TestObj
The knowledge of how to create the object is stored in the System Registry. The steps are as follows:
An MS Visual Studio DLL project can be created with the attached COMDemo.idl and COMDemo.cpp. Two usable classes are defined: TestObj and TestWorksheetFuncs, but other classes may be added following the same model. After the library is built, the classes and type library need be registered using "regsvr32 COMDemo.dll" in the appropriate directory in a command shell with administrator privileges. If one later needs to unregister, use "regsvr32 /u COMDemo.dll".
Once TLB and DLL registration is complete, and the type library is referenced in VBA, the following should work:
Dim T As New TestObj
T.Name = "Test 1"
T.Value = 15
Debug.Print T.Name; ": "; T.Value; "squared is"; T.Square
T.Name = "Test 2"
T = 16
Debug.Print T.name; ": "; T; "squared is"; T.Square
The same should work with late binding with no TLB if T is just of type Object created by CreateObject("COMDemo.TestObj").
In the last two lines we make use of the fact that Value is the default property, enabling T on its own to be used as a shorthand for T.Value (except in a Set statement).
In Microsoft Excel under Developer/Add-ins/Automation one may locate "COMDemo: test worksheet functions" which exposes the two functions AddTwoNumbers and JoinTwoStrings via the TestWorksheetFuncs class. These are directly callable from the worksheet.
If the parameter is designated as "in" in the IDL file, it may be read, but not modified. All memory management should be left to the caller, which means that arrays, strings and COM objects must not be freed after use. Also, it must not be assumed that the parameter will continue to exist once the method is exited. If the parameter does need to be retained, then it should be copied, using API functions such as SysAllocString for strings and SafeArrayCopy for arrays. AddRef should be called on COM objects that need to be retained. If the parameter is designated "out", then the recipient of the parameter takes over the memory management. It is responsible for freeing strings and arrays passed in this way, and calling Release on objects. A newly-created object should be passed with reference count set to one. Flout these rules at your peril. They are especially important in the case of passing object references across process boundaries, because the object received/passed is a proxy, an artefact of COM, with a reference count that is to some extent independent of the original object. COM uses the information in the type library to work out how to build the proxy, so if a method is not listed there, or is not declared as in the right way, then using it in a remote client will bring disaster. A proxy may also be built through the methods IClassFactory::CreateInstance and IUnknown::QueryInterface. It is important to note that this proxy is just for the IID passed, so if the IID is IID_IUnknown, then only the three IUnknown methods will be callable in the remote client. The IID next to the interface declaration must be used if access to all the methods is required.