How To Do COM In C++Builder 5
If you find this article useful then please consider making a donation. It will be appreciated however big or small it might be and will encourage Brian to continue researching and writing about interesting subjects in the future.
COM (the Component Object Model) is playing an increasing rôle in Windows development, and C++Builder offers full support for this technology.
It is becoming more and more common for developers to build COM objects in their chosen language, and to then make those objects available to systems potentially written using other languages. The beauty of a COM object is that, whilst currently they are limited to working on Windows platforms, they are language independent.
Because the COM specification defines how COM objects must be laid out at the implementation level, designers of all languages can make objects work with COM.
Whilst COM objects are currently restricted to working on the Windows platform, DCOM (Distributed COM) allows an object to be on a completely different machine to the application that talks to it.
This paper will look at the subject of building COM objects and then how to access those COM objects from client applications. Along the way we will encounter numerous pieces of terminology that will be explained as they arise.
The paper starts with an overview of the important aspects of COM applications before taking you through the process of building COM servers and clients in a tutorial style.
Click here to download the files associated with this paper.
Before embarking on our COM mission, we must first understand the concept of an interface, as this knowledge is vital for us to work successfully with COM.
An interface is a definition of a set of related methods, typically which all work together to perform some sort of job or service. An interface definition lists the method names, parameters and return types, but no implementation at all, and no data fields.
The purpose of an interface is to describe some functionality that might be implemented by a COM object (or several COM objects). A COM object can then be designed to implement any chosen interface, and may implement many different interfaces simultaneously. The COM object is said to support any interfaces that it implements.
When an application (which is called a client application) talks to a COM object, it is not able to talk directly to the object. Instead, it must talk to it through any one of the interfaces that the object implements (or supports).
The client application can talk to the object through any interface it knows about. The COM object may implement more interfaces than the client knows about, but the client is restricted to working with what it knows.
By restricting access to COM objects through interfaces, the COM object developer is at liberty to change other aspects of the object, such as the implementation of the interface methods, or other aspects not related to the interface, without affecting the object's relationship to its clients. So these formalised interfaces to the object provide flexibility to the COM object developer.
Interfaces by convention all have a prefix letter of I, for example IMalloc, IStream and IDispatch.
In C++ you can define an interface as an abstract class, or pure virtual class (or more typically a pure virtual struct). Since an interface describes available behaviour in an object, such a class will have the pure virtual methods declared in its public section and will have no other sections and no data fields defined.
In fact, the symbol interface is defined in terms of struct in basetyps.h, to make interface definitions more obvious.
You can make one interface be a superset of another by inheriting one such interface class from another. The new interface is said to be based on the original interface. Although the interface is defined using class inheritance, the lack of actual functionality in an interface leads many people to avoid using the term inheritance to describe building new interfaces from old ones.
Whilst it is possible to build classes that inherit nothing at all, an interface must always be based on another interface. At the root of this resultant hierarchy is the most basic interface of all, IUnknown.
This base interface defines three methods that accomplish two key services of any COM object, lifetime management and interface querying. The interface definition (in C++ syntax) is shown in Listing 1, where you can see the Add(), Release() and QueryInterface() methods that implement these services.
Listing 1: The IUnknown interface
MIDL_INTERFACE("00000000-0000-0000-C000-000000000046") IUnknown { public: BEGIN_INTERFACE virtual HRESULT STDMETHODCALLTYPE QueryInterface( /* [in] */ REFIID riid, /* [iid_is][out] */ void __RPC_FAR *__RPC_FAR *ppvObject ) = 0; virtual ULONG STDMETHODCALLTYPE AddRef( void ) = 0; virtual ULONG STDMETHODCALLTYPE Release( void ) = 0; END_INTERFACE };
Since all interfaces are based on IUnknown, all interfaces will have these methods within them. This means that all COM objects (which are accessed through interfaces) offer lifetime management and interface querying facilities.
Before looking at the purpose of these methods, it is useful to see the definition of IUnknown without any pre-processor directives masking things. Listing 2 serves this need.
Listing 2: An expanded version of IUnknown
struct __declspec(uuid("00000000-0000-0000-C000-000000000046")) __declspec(novtable) IUnknown { public: virtual HRESULT __stdcall QueryInterface( const IID &riid, void * *ppvObject ) = 0; virtual ULONG __stdcall AddRef( void ) = 0; virtual ULONG __stdcall Release( void ) = 0; };
You create normal objects either on the stack, or on the heap (using pointer notation). If an object is created on the stack, it will be automatically destroyed when its scope ends. An object created on the heap must be explicitly destroyed to reclaim its resources.
COM objects live on a heap (in some process address space), but the management of their lifetime is performed somewhat differently. Take the scenario where you cause a COM object to be created and you use it for a while. Before you finish, several other COM client applications connect to the same object.
When you finish with the object, destroying it would serve your own purpose, but not the other client applications who may still be using the object. If you destroy the object, the other clients will get Access Violation errors when they try to call methods of the object.
So in short, you never destroy COM objects. Instead, COM objects use a reference counting mechanism to manage their own lifetime. Each time you store a reference to a COM object's interface, you increment the reference count by calling AddRef(). Each time you finish with an interface, you decrement the reference count with Release().
When all clients are done with the object, they will have all called Release() and the reference count will fall back to 0. When this happens, the object can destroy itself.
AddRef()
and Release() work together to ensure that objects only exist for as long as they need to.Any given COM object can support several interfaces. To start with, a client application is given the object's IUnknown interface. In order to access the other interfaces (which may or may not be supported by any given implementation), the client must query the object as to whether it supports the required interface.
This interface querying is done using the QueryInterface() method. QueryInterface() is coded to understand all the implemented interfaces. When a client asks the object if it supports a given interface, QueryInterface() will return an appropriate interface reference if possible, otherwise it will indicate the interface is not supported.
In order for any given COM entity to be uniquely identified apart from any other, everything is given a different GUID. A GUID is Microsoft's Globally Unique Identifier, pronounced either gwid or goo-id.
A GUID is a 128-bit number, typically displayed as a specially formatted string of hexadecimal numbers (see the first line of Listing 1). Microsoft has implemented logic in COM to be able to fabricate unique GUIDs upon demand (the CoCreateGuid() API).
The values are concocted from an incrementing internal count, in addition to your network card identifier and other system details.
The GUID is a tailored version of Unix's Universally Unique Identifier, or UUID and is used to identify various COM elements. Interfaces are uniquely identified by a GUID called an IID (Interface Identifier). A COM object with an associated class factory is identified by a CLSID (Class Identifier). A type library is identified by a LIBID (Library Identifier).
Implementations of QueryInterface() rely on interfaces having associated IIDs in order to known which interface is being asked for. Listing 1 shows an IID being specified using the MIDL_INTERFACE macro, which as Listing 2 shows, expands to a struct definition followed by a __declspec(uuid()) declaration specifier.
In textual display form, a GUID is a string of hexadecimal digits with four hyphens placed within, surrounded by double quotes. The C++Builder editor can generate a GUID on demand simply by pressing Shift+Ctrl+G, saving you calling CoCreateGuid() in code. This produces a GUID, although instead of a pair of double quotes, it will be surrounded by a pair of apostrophes and a pair of square brackets, for example:
['{84DF4740-647A-11D4-96EC-0060978E1359}']
This keystroke is more often used in Delphi, where strings are delimited by single quotes.
Sometimes you need to represent a GUID in a non-textual form, as an initialised GUID struct. The definition of this struct can be seen in Listing 3, along with an initialised struct representation of the above GUID.
Listing 3: The GUID struct and a sample GUID
typedef struct _GUID { unsigned long Data1; unsigned short Data2; unsigned short Data3; unsigned char Data4[ 8 ]; } GUID; const GUID SomeGUID = {0x84DF4740, 0x647A, 0x11D4, {0x96, 0xEC, 0x00, 0x60, 0x97, 0x8E, 0x13, 0x59} };
Interface Definition Language (IDL)
Because interfaces are access points in COM objects, and COM objects are programming language independent, a dedicated Interface Definition Language (IDL) is used to describe them unambiguously and without bias to any given language.
IDL is a C-like language that has been developed to help represent all the attributes of an interface without any suggestion of an implementation language, or a language for the client application.
IDL was originally part of the Open Software Foundation's Distributed Computing Environment (the OSF's DCE) and was used to describe function signatures for Remote Procedure Calls (RPCs). IDL enabled compilers to generate proxy and stub code to allow function parameters to be translated from one process on one machine to another (marshaling), where the two processes reside on different machines.
Microsoft extended IDL to accommodate all the trappings of COM interfaces including vtable interfaces (early bound) and dispatch interfaces (late bound). Microsoft's tool, MIDL.EXE, compiles IDL code and generates various C++ source files and also type libraries.
Microsoft developers start their COM objects by defining the interfaces in IDL, then generating all the C++ code including the COM object with stub methods to implement the appropriate interfaces, as well as generating a type library.
C++Builder takes an alternative direction to building COM objects and defining interfaces, as we shall see as the paper progresses.
Listing 4 shows what IUnknown looks like in IDL syntax.
Listing 4: IUnknown in IDL
[ local, object, uuid(00000000-0000-0000-C000-000000000046), pointer_default(unique) ] interface IUnknown { typedef [unique] IUnknown *LPUNKNOWN; HRESULT QueryInterface( [in] REFIID riid, [out, iid_is(riid)] void **ppvObject); ULONG AddRef(); ULONG Release(); }
It is highly recommended that every method you define returns the same type of value, a HRESULT. As you can see in Listing 4, AddRef() and Release() do not follow this guideline, but they are an exception to the rule. Since practically all COM methods return a HRESULT, error detection becomes quite consistent.
A HRESULT, despite its name and the normal Windows type-naming convention, is not a handle to anything. It is just a 32-bit integer value used to return success, failure or warning codes.
The high bit of a HRESULT (bit 31, called the severity bit) indicates success (if clear) or failure (if set). The next four bits are reserved by Windows and must currently be zero. The other eleven bits of the high word represent the facility code, and specify which group of status codes the HRESULT belongs to, effectively identifying the system service that generated the error. The low word represents the error code and indicates what happened.
To explicitly examine the severity bit, which means to find out if the HRESULT-generating call succeeded or failed, you have several options. The preferred approach is to use the SUCCEEDED() or FAILED() macros which do the check for you.
Other options include calling HRESULT_SEVERITY() and comparing the result against SEVERITY_SUCCESS or SEVERITY_ERROR. Finally, there is an IS_ERROR() function which does the same as FAILED().
Routines also exist for getting the other sections out of a HRESULT, including HRESULT_CODE() and HRESULT_FACILITY(). More exist to manufacture HRESULT values, such as MAKE_HRESULT() (out of a severity, facility and status code) and HRESULT_FROM_WIN32().
Some common HRESULT values include S_OK, which is a simple success indicator and S_FALSE, which is a successful failure. E_UNEXPECTED is a common generic error code. For custom HRESULT values from your own interface methods, they should come from the FACILITY_ITF facility with codes greater than 0x1ff, for example:
#define E_MY_ERROR MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, 0x200)
All these macros and constants are declared in WinError.h. You can find all the facilities and many more error constants defined there as well.
Additionally, ComObj.hpp defines a helper routine called OleCheck(), which takes a HRESULT and raises a descriptive exception if it indicates a failure. The exception description is generated by passing the HRESULT to SysErrorMessage().
A COM object is an object that lives in a COM server and can be accessed in some way by a client application. A COM object has at least one interface implemented that can be accessed by the client application.
Client applications can make requests to create new COM objects from the appropriate server, but not all COM objects can be externally created. A COM object that can be created by a suitable COM call from a client application is referred to as a CoClass. There are various requirements for a COM object to be a CoClass, most of which are satisfied by having an appropriate class factory.
A class factory is a utility object in a COM server that takes responsibility for creating a COM object in the server when a client application requests it. Without a class factory, the COM object is not a CoClass as it cannot be created.
Typically, any COM server will have class factory objects for all potential COM objects created at startup. When a COM object is required, the corresponding class factory will be asked to create an instance of the COM object.
Class factories are all quite small objects. The COM objects, on the other hand, could be of a non-trivial size. Ensuring the COM objects are only created when needed prevents the COM server taking up more resources than it needs to.
One of the jobs of a class factory in a C++Builder COM server is to register all the COM objects in the Windows registry when requested. We will see how to register and unregister a COM server later, but it is useful to note that it is the class factories that ultimately do this job.
When you create a COM object in C++Builder, you use a wizard (from the ActiveX page of the File | New... dialog) that asks a few questions about your target object (see Figure 1).
Figure 1: The COM Object wizard
The first thing it wants is the CoClass name. A CoClass name of Foo will make the default interface to be implemented called IFoo. The class that will be generated to implement the interface will be called TFooImpl, but the type library will advertise the CoClass as Foo.
You can change the name of this interface, or choose an existing interface from a list compiled from all the registered interfaces (with the List button). It can often be very helpful to have C++Builder generate stub methods for an existing registered interface that you wish to implement.
The object can support event methods in a dedicated events interface that clients can respond to with the Generate Event support code checkbox. This causes extra code to be added to the implementing class to support client applications (called event sinks) connecting to your server's events interface. This can be useful but will not be explored in the paper.
The other checkbox ensures a flag is set in the COM server's type library. This flag enables out-of-process COM servers to take advantage of Automation marshaling, rather than requiring dedicated marshaling code to be written by the developer. Note that the tooltip for this checkbox erroneously suggests it is most useful for in-process servers, whereas in fact it is irrelevant to in-proc servers.
Apart from the Description entry, which is fairly self-explanatory, the only other option on the wizard is Threading Model, whose default value is Apartment.
When you write any COM object, you will either take the trouble to make it completely thread-safe (able to operate correctly in a multi-threaded application scenario) or you will not. Depending on which way you go will dictate how you want your object used.
If the object is thread-safe, you will be happy for multiple clients to simultaneously call methods in your COM object. If the object is not thread-safe, you will not want this to happen, as the code cannot handle it.
COM uses apartments to enforce exactly these types of rules. Any process that makes use of COM (be it a server or client) has one or more apartments in it. An apartment is used to define thread awareness of the COM objects found within it.
Any thread that makes COM calls belongs in an apartment (and stays in the same apartment). When a thread creates a COM object, that COM object belongs to the same apartment as the thread. COM ensures that only threads in an apartment can call objects in that apartment (this is important).
There are two types of apartment that exist (although COM+ on Windows 2000 adds another one). There can be at most one Multi-Threaded Apartment (MTA) in a process, but there can be one or more Single-Threaded Apartments (STAs).
An MTA can have as many threads in it at any time as it likes. Since the MTA's threads can call an MTA's objects, there is high potential for multi-threaded calls to be executed.
An STA can only have a single thread in it. If that thread creates an object, the object will be in the same STA. That STA's single thread is the only thread allowed to call the object's methods, so all method calls to the objects in the STA will be made on this one thread.
COM ensures that even if simultaneous STA method calls are made from threads in another process, or other apartments in the same process, they will execute one at a time. This guarantees thread safety for objects that cannot do it for themselves.
The new COM+/Windows 2000 apartment is a Thread-Neutral Apartment (TNA). When an object is in a TNA, multiple threads from other apartments can call the objects methods simultaneously, but again, COM ensures that there are no conflicts.
To indicate which type of apartment would be best, the COM object wizard (Figure 1) allows you to specify your chosen threading model. The available options are shown in Table 1.
Table 1: Threading models available in the wizard
Threading model |
Meaning |
Single |
The object can work in an STA, so thread safety will be enforced. But it is assumed there will only be one STA, so global data needs no protecting. |
Apartment |
The object can work in an STA, so thread safety will be enforced. There may be many STAs in the process, so global data must be protected. This is the default option. |
Free |
The object can work in an MTA, so is thread-safe. |
Both |
The object can work in an STA or an MTA, so is very flexible (and thread-safe). |
Neutral |
This model is specific to COM+. When COM+ is not available it maps to Apartment. Should not be used for controls with a UI, but is recommended for other objects. |
By specifying a threading model, the object advertises how thread-safe it is, and COM can work out how best to deal with situations before they arise.
As we will see, this option is only relevant for in-process COM servers.
In order to allow COM to enforce thread safety where needed between different apartments, COM creates proxy objects. When a client application (in, say, an MTA) asks COM to create an object that advertises an Apartment threading model, COM creates a proxy object to intercede.
To the client, the proxy object looks exactly like the real object, but it uses internal trickery (a hidden window and a Windows message pump) to ensure that calls to the STA occur one at a time.
These proxy objects are made automatically to cater with cross-apartment, or cross-process calls. The proxy object is also responsible for marshaling data between apartments and processes.
In order for COM to create proxy objects, the corresponding interface must appear in a type library registered on the system. The type library is read to find out all the details of the interface method parameters, so the object can look exactly like it should, and know what data it needs to marshal.
For more information on apartments and proxy objects, see Reference 2.
To cater for inter-apartment and inter-process COM method calls, COM must know how to transfer the data successfully from one apartment/process to another. Remember that the data transfer could potentially be from one machine to another. This data transfer process is called marshaling.
Developers using Microsoft tools get marshaling code generated for free as one of the results of running MIDL.EXE over their IDL. MIDL generates proxy/stub code automatically so that proxy objects are already defined, and COM has to do very little.
Borland developers do not use MIDL and so do not have the proxy/stub code generated on their behalf. This would normally suggest that Borland developers who need marshaling would need to write it themselves, which is done by making your object support the IMarshal interface.
Fortunately, the checkbox mentioned earlier, and shown in Figure 1, means this rarely needs to be the case.
There is a special type of COM server, where the COM object(s) support the IDispatch interface, called an Automation server. The IDispatch interface allows many scripting languages to control the COM object's methods and properties using run-time method calls to IDispatch (a method call dispatching interface).
Information on writing and controlling Automation servers can be found in Reference 1, if you need to review it. However, all we need to know right now is that Automation has its own marshaling mechanism that can successfully deal with a certain number of data types.
The permissible types are:
As long as we restrict ourselves to these supported data types, the COM object wizard's checkbox gives us access to Automation marshaling for free. If you neglect to keep the checkbox on the wizard checked, you can get the same result in the type library editor. For each interface you create, the Flags page of the type library editor has an Ole Automation checkbox (see Figure 2).
Figure 2: The flag that requests Automation marshaling
VARIANT Arrays For Custom Data
Automation marshaling may seem a little restrictive at first, since custom structs are not permitted. However, you should keep in mind that a VARIANT can hold an array of elements (called a safe array).
The type of each element can be any type supported by a VARIANT, including a VARIANT. So you could create a safe array of bytes of an appropriate size, which could easily be given the struct data.
The VCL supports safe arrays in the Variant class to simplify their management, calling them Variant arrays. Look up the term SafeArrays in the online help for more information, or look at the example project under C++Builder's Examples\Doc\VarArray directory for a project that uses Variant arrays.
As mentioned earlier, a type library is part of the result of running MIDL.EXE across some IDL, for Microsoft developers. This is not the pattern employed by C++Builder developers.
Instead, a type library enters the equation right at the start of COM development. However, before worrying about the development side of things, let's find out what purpose a type library fulfils.
A type library is a binary file that can accompany a COM server (either as a separate file, or bound into the executable as a resource). It defines all the public interfaces available from the COM server, as well as custom enumeration values and other type information used by them.
Any program that needs to know what is on offer from a particular COM server can do so by examining the type library, if there is one.
Development tools such as Delphi and C++Builder can use the information in a server's type library to generate local language declarations of all the interfaces available from the COM server and store them in a dedicated source file (called a type library import unit).
C++Builder generates C++ interface definitions, whilst Delphi generates Object Pascal interface types. This saves the developer having to locate (or write) the appropriate definitions for themselves.
C++Builder has a dedicated type library editor window, available through View | Type Library when a type library is in the active project (see Figure 3).
Figure 3: The type library editor
When developing COM servers with the original Microsoft tools, you would first define your interfaces in IDL, which would be run through MIDL.EXE. This generated the type library file and C++ source files. The source would define the interfaces, and also define the classes that implement those interfaces, with stub methods waiting to be filled in with the implementation.
C++Builder turns this process into a visual development cycle. Once you add a COM object (via a dedicated wizard) into a C++Builder project, it automatically generates a type library. It also adds the description of the interface and the CoClass into the type library and generates a type library import unit.
This unit has a name based on the library name (the top node in the tree on the left of the type library editor). If the project is called XXXX, the type library name will also be called XXXX by default (although this can be changed in the type library editor), and will be saved as XXXX.tlb. If the type library name is XXXX, the import unit will be called XXXX_TLB.CPP with an associated header file.
You can switch between the type library editor and the import unit by pressing F12 ( View | Toggle Form/Unit).
The wizard continues by defining an implementation unit, which contains the definition of a class that implements your interface (i.e. the COM class or CoClass), leaving stub methods waiting to be filled in with the implementation.
The type library editor then allows you to add properties and methods to the interface, as well as defining other interfaces and CoClasses. At any stage, the type library editor's Refresh button can be pressed to update the type library import unit, as well as the implementation unit. Any new interface methods will have a corresponding stub method added to the implementing class.
All this is done automatically and without resort to IDL. But if you want, you can always export an IDL version of the type library contents with the Export button on the type library editor toolbar (visible in Figure 3). You can also edit the type library content in IDL format on the various Text pages of the type library editor's page control.
A COM server is an executable or DLL that contains CoClasses and typically comes with a type library. There are various differences to how the executable and DLL servers manage themselves, so we should take a look at the details.
A DLL-based sever is called an in-process server (or in-proc server) because DLLs are loaded into the address space of the calling application. As a consequence, the COM object code and the client code are in the same process.
This means the client code can talk directly to the COM object methods (unless there is a clash between the client apartment and the threading model of the COM object, whereupon COM will provide proxy objects to manage the difference.
Either way, the code is in the same process and so the job of marshaling the data from client to object and back is trivial. Consequently, marshaling is not an issue with in-proc servers, and method calls to in-proc servers are more efficient. This explains why most COM servers are built as in-proc servers.
In order for COM to detect a clash, in-process server objects advertise their threading model in the Windows registry. When COM is creating the COM object it can check on the client apartment and compare it with the threading model and make proxy objects if necessary.
A DLL-based COM server is simply a normal DLL. In-proc servers typically have a .DLL or .OCX extension, although the extension is immaterial. To make it fulfil the requirements to be a COM server, four routines are exported in order for COM to liase with it correctly.
You can make an in-proc server by choosing the ActiveX Library icon on the ActiveX page of the File | New... dialog. If you look at the bottom of the project source (Project | View Source) you will see the four exported routines: DllCanUnloadNow(), DllGetClassObject(), DllRegisterServer() and DllUnregisterServer().
DllGetClassObject()
is called by COM to get access to a COM object's class factory, which is then asked to create an instance of its associated class.DllCanUnloadNow()
is called to see if any COM objects still exist in the server. If all objects have been destroyed, the DLL will be unloaded.The other two routines are used for registering and unregistering the in-proc server.
COM objects can now be added to the server using the supplied wizards on the ActiveX page of the File | New... dialog.
An executable based COM server is called an out-of-process server (or out-of-proc server) because the client will inherently be in a separate process address space from the server. Consequently, COM will always give proxy objects to the client application. The proxy objects deal with marshaling parameters from client to server and back again. Out-of-process servers will always be less efficient than in-process servers for this reason.
You can make an out-of-proc server by simply making a new C++Builder application. COM objects can be added to the server using the supplied wizards on the ActiveX page of the File | New... dialog.
When a COM object in an out-of-process server is made with the COM Object wizard, the threading model choice is not used for anything. It is primarily for in-proc servers. However, you must decide the default apartment type for the server. When the server starts up, COM is initialised and a flag determines if an STA or MTA is entered.
You can set that flag on the ATL page of the Project | Options... dialog. Figure 4 shows the options, which are all used to customise how ATL will work. ATL is Microsoft's Active Template Library, which is used to implement the functionality in C++Builder COM servers.
Figure 4: The ATL options
The various threading models of the COM objects in the server should be used to decide the best apartment type to enter.
If all objects are Apartment threaded, you should choose the APARTMENTTHREADED option, to get a value of COINIT_APARTMENTTHREADED passed to the COM initialisation routine.
If you have any objects with a threading model of Free or Both, you should choose the other radio button to get a value of COINIT_MULTITHREADED passed to the initialisation call.
Incidentally, you should only use the OLE Initialization COINIT_xxx Flag radio buttons to set the primary thread apartment type. The other radio buttons in the Threading Model group are for backward compatibility with C++Builder 3.
DCOM allows out-of-proc servers to be located on different machines to the client, with the client not necessarily being aware of the server's disparate location. DCOM uses RPC to transport the method calls around the network.
Proxy objects are always used by the client and so, as explained earlier, the server's type library must be registered on the client machine to permit this to work.
Another option you can set with the ATL options (Figure 4) is the instancing option of your server. This is only relevant to out-of-process servers as it controls how many COM object instances can be managed by a single instance of your server.
The Single Use option means that each server instance can only manage a single instance of a COM object. Each new created instance will be housed in additional instances of the COM server.
For COM servers that have a UI, this option corresponds to a Single Document Interface (SDI) application.
The other option, Multiple Use (the default), allows many separate instances of the COM object (created by potentially many client applications) to be housed in the same server executable instance.
For COM servers that have a UI, this option corresponds to a Multiple Document Interface (MDI) application. This option would allow all COM objects access to global server data, so you should make sure such data is protected with synchronisation objects.
In order for COM to be able to locate a given COM object (identified by its CLSID), the corresponding server must be registered.
This means that information about the server, its location and content must be added to the Windows registry in order for the server to be useable. The details that are stored will vary between in-proc servers and out-of-proc servers, but the server deals with it all when asked without any fuss or bother.
An in-process server exports four routines from the project source, as seen earlier. Two of these routines have already been described. The other two, DllRegisterServer() and DllUnregisterServer(), clearly do the job of registering and unregistering the server.
There are various ways to get the appropriate exported routine called.
If you are developing the server in the IDE, you can use the Run | Register ActiveX Server and Run | Unregister ActiveX Server menu items.
These menu items compile the server, then load it into the IDE's address space (with LoadLibrary). The GetProcAddress() API is then used to locate either DllRegisterServer() or DllUnregisterServer() and, assuming one is found, it is called.
TRegSvr.Exe (Borland Turbo Register Server) is a command-line tool supplied in C++Builder's Bin directory. It (by default) registers and (with a -u switch) unregisters in-proc servers and type libraries.
RegSrv32.Exe is a similar tool to TRegSvr.exe, but which comes with all Win32 platforms in the Windows System directory. It (by default) registers and (with a /u switch) unregisters in-proc servers and type libraries.
Out-of-proc servers are not DLLs, so they do not export special routines for registration and un-registration. Instead, being executables they take command-line parameters to do the job.
The /regserver command-line parameter registers a COM server. With this parameter, the server will start, register and then close without displaying any UI.
You can also register an out-of-proc server by simply running it. This will cause it to register before displaying its main form, but the application will stay running until you close it.
The /unregserver command-line parameter unregisters a COM server. With this parameter, the server will start, unregister and then close without displaying any UI.
Typically, a remote out-of-proc server will be registered just as described above. However, as discussed earlier , the server's type library must also be registered on the client machine to allow Automation marshalling to work.
This assumes that the client application knows which machine the server is on. If the client is unaware of the server being on a different machine, then additional registration must occur on the client machine to allow COM to know that the server is remote, and where to find it.
To test out the theory described thus far, we can now create a DLL server. This will be an in-proc server with a simple CoClass defined therein.
Select File | New... and choose ActiveX Library from the ActiveX page of the dialog. This project can be saved as COMServer.bpr (that is the name of the sample server project that accompanies this paper).
Now select File | New... and choose COM Object from the ActiveX page of the dialog. Specify a CoClass name of Foo and enter a description. Choose File | Save All and accept the suggested name of FooImpl.cpp for the Foo CoClass implementation unit.
Using the type library editor (View | Type Library) add in two read-only properties called UserName and MachineName. Each should be of type BSTR, which is the Automation-compatible wide character string type. Automation does not support any string types made of ANSI characters.
Press Refresh to update the implementation unit. If you now look at the FooImpl.cpp unit, you will see that two property reader methods have been constructed (see Listing 5).
Listing 5: Two new COM property reader methods
STDMETHODIMP TFooImpl::get_MachineName(BSTR* Value) { try { } catch(Exception &e) { return Error(e.Message.c_str(), IID_IFoo); } return S_OK; }; STDMETHODIMP TFooImpl::get_UserName(BSTR* Value) { try { } catch(Exception &e) { return Error(e.Message.c_str(), IID_IFoo); } return S_OK; };
The properties are of type BSTR, but the property reader methods must return a HRESULT (accomplished by the STDMETHODIMP macro).
Notice that all VCL exceptions are trapped (it is an error to let exceptions bubble out of a COM method). Any trapped exception has its message passed along to the ATL Error() helper routine. This sets up rich error information by setting up a COM error object to represent the exception and then returns an error-indicating HRESULT.
Any interested client code can spot the error HRESULT, then test to see if rich error information is supported. If it is, it can extract the information from the COM error object and if needed recreate an exception from the stored message.
All that is left for this simple server is to make the property reader methods return the appropriate data. Listing 6 shows the code that can do the job.
Listing 6: Implementing the two properties
STDMETHODIMP TFooImpl::get_MachineName(BSTR* Value) { try { char Name[MAX_COMPUTERNAME_LENGTH + 1]; unsigned long Len = sizeof(Name); Win32Check(GetComputerName(Name, &Len)); WideString WName = Name; *Value = WName.Copy(); } catch(Exception &e) { return Error(e.Message.c_str(), IID_IFoo); } return S_OK; }; STDMETHODIMP TFooImpl::get_UserName(BSTR* Value) { try { char Name[256]; unsigned long Len = sizeof(Name); Win32Check(GetUserName(Name, &Len)); WideString WName = Name; *Value = WName.Copy(); } catch(Exception &e) { return Error(e.Message.c_str(), IID_IFoo); } return S_OK; };
Both GetComputerName() and GetUserName() use normal char * string buffers for their work. They also both return 0 when they fail (you call GetLastError() to find an error code indicating what the failure was).
The code uses the VCL helper routine, Win32Check(), to take the API return values and raise appropriately descriptive exceptions upon failure.
The code also uses a VCL WideString object to translate the ANSI strings into wide strings. A BSTR version of a WideString object can be obtained using the c_bstr() method, but in this case the Copy() method is used to ensure we get a duplicate wide string, as the original version will be disposed of when the WideString object goes out of scope.
With the server complete, you can register it (choose Run | Register ActiveX Server).
With the server finished, we can build a client application to test it with. But before proceeding, we should have an understanding of what is placed in the type library import unit.
The unit itself (the .CPP file) has little in it except for some initialised GUID struct constants (interface IDs, class IDs and type library IDs). However, the header file is where all the stuff of interest lies (remember you can switch between a source file and header with Ctrl+F6, or by right-clicking and choosing Open Source/Header File).
A namespace is defined, based on the name of the unit. Within the namespace, the following items can be found.
External references to the GUID constants in the unit source itself. For each CoClass, XXX, there will be an interface ID for the primary interface IXXX, called IID_IXXX, a class ID for the CoClass called CLSID_XXX. There will also be a type library ID called LIBID_YYY, where YYY is the name of the type library.
For a project called COMServer.bpr with a CoClass called Foo, Listing 7 shows the constants.
Listing 7: GUID constants
extern __declspec (package) const GUID LIBID_COMServer; extern __declspec (package) const GUID IID_IFoo; extern __declspec (package) const GUID CLSID_Foo;
The next item is a forward declaration of the CoClass primary interface (IFoo) including its IID, which is accompanied by a type definition that can be used by clients to reference the interface ( IFooPtr), as shown in Listing 8. We will see TComInterface again shortly, as it acts as a base class for smart interface pointers which increment and decrement an interface's reference count automatically.
Listing 8: Type declarations
interface DECLSPEC_UUID("{01AD712E-66D8-11D4-96EC-0060978E1359}") IFoo; typedef TComInterface<IFoo, &IID_IFoo> IFooPtr;
Next are more type definitions, mapping each CoClass to its default interface (see Listing 9). You can also se a simple macro that returns the type library ID for the Foo CoClass.
Listing 9: More type declarations
typedef IFoo Foo; typedef IFooPtr FooPtr; #define LIBID_OF_Foo (&LIBID_COMServer)
At last we get to see the definition of the primary interface itself, IFoo (see Listing 10). The two property reader methods are declared as pure virtual methods that return a HRESULT and have a pointer parameter that returns the required BSTR.
Listing 10: The primary interface of the CoClass
interface IFoo : public IUnknown { public: virtual HRESULT STDMETHODCALLTYPE get_UserName(BSTR* Value/*[out,retval]*/) = 0; // [1] virtual HRESULT STDMETHODCALLTYPE get_MachineName(BSTR* Value/*[out,retval]*/) = 0; // [2] };
Following the interface definition (a pure virtual struct), another class is defined that acts as a smart interface. This class can be used by clients to access the interface, and will deal with incrementing and decrementing the interface reference count automatically, simplifying the COM programmer's task significantly.
The class inherits from the TComInterface template class, which is passed the appropriate interface that it will work with (see Listing 11). You can see a number of constructors defined to cater for different circumstances, and also overloaded versions of the property reader methods. For flexibility, there are two definitions of each method related to a property.
One definition returns a HRESULT, and gives access to the BSTR through a pointer parameter (as in the original interface in Listing 10). The other definition takes no parameters and simply returns a BSTR, rather like the original property definition in the type library.
However, C++ interfaces do not use properties, and so Listing 10 only shows methods. This new property-like method is used by the smart interface class in the definition of a property that corresponds to the one defined in the type library.
Listing 11: The smart interface class
template <class T /* IFoo */ > class TCOMIFooT : public TComInterface<IFoo>, public TComInterfaceBase<IUnknown> { public: TCOMIFooT() {} TCOMIFooT(IFoo *intf, bool addRef = false) : TComInterface<IFoo>(intf, addRef) {} TCOMIFooT(const TCOMIFooT& src) : TComInterface<IFoo>(src) {} TCOMIFooT& operator=(const TCOMIFooT& src) { Bind(src, true); return *this;} HRESULT __fastcall get_UserName(BSTR* Value/*[out,retval]*/); BSTR __fastcall get_UserName(void); HRESULT __fastcall get_MachineName(BSTR* Value/*[out,retval]*/); BSTR __fastcall get_MachineName(void); __property BSTR UserName = {read = get_UserName}; __property BSTR MachineName = {read = get_MachineName}; }; typedef TCOMIFooT<IFoo> TCOMIFoo;
You can see that for any CoClass XXX, the smart interface class for its primary interface is defined by a typedef to be TCOMIXXX.
Immediately following this template class definition are the implementations of the interface access methods (four in this case). As an example, Listing 12 shows the two get_UserName() methods.
Listing 12: The smart interface class methods
template <class T> HRESULT __fastcall TCOMIFooT<T>::get_UserName(BSTR* Value/*[out,retval]*/) { return (*this)->get_UserName(Value); } template <class T> BSTR __fastcall TCOMIFooT<T>::get_UserName(void) { BSTR Value = 0; OLECHECK(this->get_UserName((BSTR*)&Value)); return Value; }
The first one is straightforward. Since this method matches the signature of the interface method, a direct call is made, and the HRESULT from the interface method call is passed back to the caller.
The second one, on the other hand, is designed to be more convenient to call (it is called when you read from the smart interface class property UserName) and so has a different signature. Consequently, the implementation is a little more fiddly. A local BSTR is defined, passed as a parameter to the other method, and then returned to the caller. The HRESULT from the other method call is passed to OLECHECK() to see if an error is indicated.
Notice in the second method in Listing 12 that the HRESULT returned from the call to the first method is passed to OLECHECK(). OLECHECK() is a macro defined in UtilCls.h that calls DebugHlpr_HRCHECK() from the same unit. The job of DebugHlpr_HRCHECK() is to examine a HRESULT and take appropriate action if it indicates an error (otherwise do nothing).
Appropriate action means either raising an EOleException exception (if NO_PROMPT_ON_HRCHECK_FAILURE is defined) or to call DebugHlpr_PROMPT(). DebugHlpr_PROMPT() displays a message box, allowing the user to choose whether an exception should be raised, the problem should be ignored, or the debugger should break the program at the current point.
To avoid getting the message box, ensure that NO_PROMPT_ON_HRCHECK_FAILURE is defined before UtilCls.h is included in your unit. Alternatively, in your own code you can use the VCL routine OleCheck() from the ComObj.hpp header, which always throws an EOleSysError exception when it is passed an error-indicating HRESULT.
Using either of these helper routines is much easier than deciphering HRESULT values yourself.
Default Interface Creator Class
The final thing of note in the type library import unit header generated for the COM server is another class (Listing 13). This class is also usable by clients to help simplify the process of connecting to an instance of a server.
Listing 13: The default interface creator class
typedef TCoClassCreatorT<TCOMIFoo, IFoo, &CLSID_Foo, &IID_IFoo> CoFoo;
For a CoClass called XXX, the default interface creator class will be called CoXXX. It has two key static methods called Create() and CreateRemote(), which are designed to create a local or remote copy of the CoClass respectively, using standard COM/DCOM calls, and return a reference to the primary implemented interface.
This client helper class does not seem to have an official description, so you can call it a default interface creator class, or a client-side CoClass proxy class.
With knowledge of the type library import unit content under our metaphorical belt, we can now try and build a client application that can connect to the server. So start by making a new project. The example that accompanies this paper is called Client.bpr.
Since we will need to gain access to types defined in the type library import unit you will need to include the header for the unit in your form unit source. Assuming the client and server are in different directories, you will either need to include the relative path to the header file in the #include directive or add an appropriate search path in the Project | Options... dialog on the Directories/Conditionals page.
To avoid any COM method calls resulting in the aforementioned message box, define NO_PROMPT_ON_HRCHECK_FAILURE above the #include directive.
You should also add the COMServer_TLB.cpp file to the client project as this unit defines a number of aforementioned initialised GUID struct constants that the linker will need to make the final EXE. You can do this with Project | Add to Project..., or Shift+F11.
Note that versions of C++Builder earlier than 5 had a bug in the project manager. If you have the client and server project in a project group, you will not be able to add the type library import unit to the client project with the project group open. Instead you must close the project group, and open up the client project on its own in order to accomplish the task.
Next, we need a variable that we can use to access the interface of the COM server object. In the form constructor, declare a variable called Foo. To make your life easy, define it in terms of the smart interface class, TCOMIFoo, described earlier.
The variable can be initialised by assigning it the return value of a call to the static Create() method of the CoFoo class.
So far, the form unit (ClientMainForm.cpp in this example) looks like Listing 14. The added code is highlighted in bold.
Listing 14: Connecting to the server
#include <vcl.h> #pragma hdrstop #define NO_PROMPT_ON_HRCHECK_FAILURE #include "..\Server\COMServer_TLB.h" #include "ClientMainForm.h" //--------------------------------------------------------------------------- #pragma package(smart_init) #pragma resource "*.dfm" TClientForm *ClientForm; //--------------------------------------------------------------------------- __fastcall TClientForm::TClientForm(TComponent* Owner) : TForm(Owner) { //Connect to server TCOMIFoo Foo = CoFoo::Create(); }
Now to make use of the COM object's properties, add two labels on the form. The labels can be given names of lblMachineName and lblUserName, and will be assigned the values read from the UserName and MachineName COM object properties.
To finish the job off, we can add the property reading code to the form constructor (see Listing 15). The client application can be seen running in Figure 5.
Listing 15: Reading the COM properties
__fastcall TClientForm::TClientForm(TComponent* Owner) : TForm(Owner) { //Connect to server and read properties TCOMIFoo Foo = CoFoo::Create(); WideString UserName = Foo.UserName; lblUserName->Caption = "Logged in user: " + UserName; WideString MachineName = Foo.MachineName; lblMachineName->Caption = "Machine name: " + MachineName; }
Figure 5: The finished client running
COM is a language-independent distributed object model. This paper describes the steps required to build a COM server in C++Builder 5, and also how to connect to that same server from a client written in C++Builder 5. Any other COM-capable language, such as Delphi or Visual Basic, can also connect to this server using steps appropriate to that language.
Click here to download the files associated with this paper.
Brian Long used to work at Borland UK, performing a number of duties including Technical Support on all the programming tools. Since leaving in 1995, Brian has spent the intervening years as a trainer, trouble-shooter and mentor focusing on the use of the C#, Delphi and C++ languages, and of the Win32 and .NET platforms. In his spare time Brian actively researches and employs strategies for the convenient identification, isolation and removal of malware. If you need training in these areas or need solutions to problems you have with them, please get in touch or visit Brian's Web site.
Brian authored a Borland Pascal problem-solving book in 1994 and occasionally acts as a Technical Editor for Wiley (previously Sybex); he was the Technical Editor for Mastering Delphi 7 and Mastering Delphi 2005 and also contributed a chapter to Delphi for .NET Developer Guide. Brian is a regular columnist in The Delphi Magazine and has had numerous articles published in Developer's Review, Computing, Delphi Developer's Journal and EXE Magazine. He was nominated for the Spirit of Delphi award in 2000.