Creating COM components with Visual C++ 6.0
Binary standard permitting software objects developed in different languages and operating on separate platforms to communicate.
Using COM Objects
Applications built out of distinct, cooperating components (implemented as dlls or exes). Relationships can be established dynamically at run-time.
For COM object to be used it must be registered with Windows operating system. Each component has specially formatted GUID called a Class Identifier (ClassID) stored in registry. When client wants to talk to object, it uses the ClassID to identify it. The COM run-time libraries (part of OS) will attempt to locate and instantiate requested object.
When COM libraries receive a ClassID they search HKEY_CLASSES_ROOT portion of registry to find location of requested object. If successful the object is created and a pointer returned to its interface, otherwise an error is reported.
The application will communicate with the object via the returned interface pointer, executing the member functions contained within or asking for access to another interface supported by the object.
The object can initiate interaction with the client application by calling on event handlers the client may have registered.
COM features location transparency - client applications do not need to be concerned where the COM objects execute, it may be in the same process, on the same machine or on another machine.
Access to object services through one or more interfaces (identified by GUID known as Interface Identifier IID) that group related methods. The interface references a table of function pointers (a vtable) through which access to individual methods may be gained.
Interface is logically separate entity to the component - i.e. the same interface may appear on different objects.
Interface analogous to C++ abstract class. Once specification published to world, it is guaranteed not to change, i.e. number and order of methods, data types of arguments, return types, etc. will remain fixed. If need to change / add methods or arguments a new interface must be defined.
This interface must be implemented by every COM object. Contains three methods:
- QueryInterface - mechanism for clients to access other interfaces supported by this object
- AddRef - to increment usage counter of object
- Release -to decrement usage counter
The IUnknown methods must always be the first three methods in the vtable in order presented above. For example, the vtbl below is used by an object implementing three interfaces, IUnknown, IEncode and ICommunicate.
pVtbl QueryInterface IUnknown
128-bit numeric identifier guaranteed to be unique. Distinguishes objects and their interfaces. Generated by tools UUIDGEN (command line) or GUIDGEN (window).
Two formats; string and numeric. String form stored in registry and numeric used within applications.
Variable representations in code typically proceeded by CLSID_ (if GUID represents a class) or IID_ (if GUID represents an interface).
To register object create entries under HKEY_CLASSES_ROOT under objects CLSID (in string format). Beneath this key specify where to find the object, for example the InprocServer32 value specifies the location of the dll that implements this component (exes use a different value).
Can also include representations of the component name held in the ProgID (programmatic identifier). Provide a corresponding entry under HKEY_CLASSES_ROOT%ProgID%CLSID (where %ProgID% is the ProgID for object) with the object CLSID - permits applications to map CLSIDs to ProgIDs and visa-versa using CLSIDFromProgID(), etc. This permits your application to be created without the caller having to issue your CLSID directly - can use more friendly ProgID. Required for VB clients - they can't use CLSID.
Use CoCreateInstance() to request COM library to create object from specified CLSID in appropriate context - in process, local or remote. Returns pointer to requested interface.
When component server initialised, class factory instantiated. Class factories create objects of specific class - implemented as part of COM component. Your implementation of the class factories CreateInstance() method on IClassFactory must instantiate class providing methods that may be accessed by clients - class creation initialises appropriate vtable.
Pointer to IUnknown returned to COM libraries. When received the IClass Factory is released and IUnknown returned to client, through which other methods may be executed via vtable.
This process carried out only for first access to object. If other clients attempt to gain access, a pointer to the IUnknown interface is returned and its reference count incremented. Client must release interface when finished - if not then component will never be destroyed -> memory leak!
Create COM Component
ATL = C++ templates to help create COM components. Special support for key features such as IUnknown, IDispatch and dual interfaces.
Wizards generate boilerplate code for common features - developer concentrate on real work of component. By using templates get fast, light-weight code.
- Create ATL project using ATL Wizard. Specify in-process DLL or out process executable / remote service. Chose whether to support MFC, MTS and have proxy/stub code generated.
- Insert ATL object using ATL Object Wizard. Several object types available, e.g. mmc snap-ins, etc. Usually choose Simple Object. Then choose name of object to create and what file names to use. Finally decide on threading model, support for dual interfaces, aggregation support, etc.
- Add methods to object using Interface Wizard. First provide name for interface then add methods to it. Note parameters to methods must be specified in IDL format (e.g. use of the in keyword to specify inbound parameters.
- Add properties to object using Interface Wizard. Get and Put methods automatically generated for each property defined.
- Implement methods. From ClassView tab double click on member functions appearing below interface to gain access to code.
Understanding ATL Code
Class defined using multiple inheritance. For example a class may derive from CComObjectRootEx (an ATL template), CComCoClass (an ATL class) and IEncoder - abstract base class defining interface methods to be implemented by this class.
CComObjectRootEx provides default IUnknown implementation. Provide with details on threading model used.
CComCoClass provides a class factory to create an instance of this COM object. Provide factory with name of class to create (you class name) and its CLSID.
Class declaration contains COM map - specifies list of interfaces support by component. Entries added within COM_INTERFACE_ENTRY macro. Used by default QueryInterface() implementation to match GUIDs to functions.
Interface methods use STDMETHODIMP as their return type.
Ensure return appropriate values, e.g. S_OK for success.
Global Entry Points
Global CComModule instance called _module. Implements server module allowing clients to access modules components. Supports both in-process and out of process modules. Maintains object map containing entry for each object comprising COM component used by framework to:
- Initiate objects through class factory
- Establish communications between client and root component of object
- Lifetime management of class objects
- Enter and remove entries from registry
Exe servers do not implement following Dll* functions.
DllGetClassObject () entry point executed in response to client requests. Provides access to class factory of specified object. Executables achieve equivalent by registering object class factories with COM at server start-up. COM maintains internal maps from which to satisfy class factory requests.
DllRegisterServer() and DllUnregisterServer() provide component self registering support. Executables achieve equivalent using command line switches RegServer and UnregServer. Both forms call CComModule::RegisterServer() and CComModule::UnregisterServer() to add/remove all objects in object map from the registry. Registry entries to add/remove held in registry script resource.
Registry Script Resource
Project resource that contains entries to add into registry - e.g. CLSID, ProgID, type library, etc.
Compiled by MIDL to create:
- proxy/stub marshalling code
- type library file
- C file to define component and interface GUIDs
- C++ header file declaring interface methods of component.
Very similar syntax to C, addition of parameter direction indicators.
Can use straight C++, MFC, or any other language to generate COM objects.
Pure C++ requires considerable amounts of coding, mostly concerning mechanics of COM which are provided as boilerplate code by ATL.
MFC can generate COM code. MFC oriented towards applications, thus its COM support is framed in context of OLE (now largely re-branded ActiveX) permitting apps to export aspects of their functionality to other programs. OLE generally implemented using dispatch interfaces, if want to expose custom interface must use raw C++. If implement using MFC must also include MFC DLLs - which are big and so not good for distribution over web.
COM Server Header Files
MIDL generates files making COM server interfaces and GUID definitions available to clients. The file xxx_i.c contains the GUID definitions (such as IID, LSID, LIBID) for the object, while the.h file describes the interface exposed in terms of an abstract C++ class
Client programs use these definitions to create pointers to the COM object that may be passed into CoCreateInstance() and thence used to manipulate the object created.
Visual C++ Support
Version 5 introduced a number of classes (defined in comdef.h) and extensions to simplify creation of COM client programs.
Encapsulates COM interface to help simplify reference counting. AddRef() called when object created as copy of existing interface pointer. Release() called when goes out of scope.
Simple way to instantiate is to create using COM_SMARTPTR_TYPEDEF macro that takes interface name and GUID and creates specialisation of _com_ptr_t with name of interface suffixed with Ptr, i.e.
results in the type IEncoderPtr becoming available for use.
Instances of IEncoderPtr can then call the com_ptr_t member variable CreateInstance() to obtain interface pointer to COM server. Members called using overloaded -> operator.
Encapsulates BSTR data type. Manages resource allocation / deallocation and reference counting for efficient use of memory. Provides operators to allow use as easily as a CString.
Thin wrapper to VARIANT. Manages creation and destruction of encapsulated variant via calls to VariantInit() and VariantClear().
Represents COM exception condition. Encapsulates HRESUT returned by COM functions. Can be thrown by com_raise_error().
Generates C++ header information about COM object and interfaces from type library. Useful if don't have access to MIDL generated header files. Generates set of smart pointers to access COM object interfaces.
Pre-processor creates 2 header files that reconstruct type library contents as C++ source code. Primary file (has same name as type library or dll with .tlh extension) similar to the one generated by MIDL - defines C++ functions to access COM object methods. Also declares additional functions to provide property and method access similar to those used by VB - access properties as class members, wrapper HRESULTS within _com_error, use _bstr_t and _variant_t as arguments.
Secondary file has same name as type library or dll with .tli extension. Contains implementation for compiler generated wrapper functions specified in .tlh file.
COM components don't have to load into same process or machine as client. Has impact on parameter passing between components (different requirements for various contexts). Transfer of data across boundaries = marshalling.
In-process implemented as DLL. Quicker response than other contexts. Quicker to develop + less code required. Must register object on every computer that may run client.
Out-of-process implemented as EXE. Arguments to methods must transfer correctly across process boundaries - interfaces must be marshalled. Slower responses, + longer development time + code overheads.
Can implement out-of-process to execute on other machines - called remote-server. Very slow responses and non-deterministic. Marshalling code same as for out-of-process. Only require single copy of component for entire network - cheap + easy to upgrade.
Client / server relationship dictates marshalling technology.
|Custom||Process||Special s/w, protocol|
object loads into client process. All its methods can be called directly and data passed freely.
Used for both out-of-process and remote. Interfaces specified using IDL. Implement marshalling using MIDL compiler to produce proxy (client side) and stub (server side) code compiled to produce DLL that implements marshalling between client and server.
Automation marshaler provided by COM. Allows clients written in languages not supporting function tables to use COM. Automation implemented by IDispatch interface. Supports standard set of data types packaged into VARIANT data structure. Not as efficient as standard marshalling.
Use where standard not appropriate. Component must support IMarshall. Some reasons to use custom:
- Shared memory - standard copies data back and forward between client and server. Custom can create shared memory to achieve this -> improve performance.
- Marshalling by value - Standard operates by reference. By value permits local copies of objects to be made -> subsequent local calls.
- Smart proxy - Some methods implemented by proxy - don't need call server. Make use of asynchronous calls, etc.
- Bypass native threading models - permits use of other thread models.
Some components do not require IDispatch - e.g. those t be used exclusively by C++. To make objects accessible to many, must implement the dispatch interface using standard params that can be wrapped by VARIANT. Good idea to restrict use of params on all interfaces to automation-compatible ones, ensures no problems with clients.
Client app calls IDispatch->GetIDsOfNames() with name of method to invoke. Function returns dispatch identifier (DISPID) for requested method. This identifier then passed into IDispatch->Invoke() together with any params required (packaged in VARIANT array).
Your Invoke() implementation calls requested method on behalf of client - must maintain DISPID to Function mapping + have ability to unpack params from VARIANT.
Contains 2 fields (excluding reserved ones). vt describes type of data stored. To allow multiple data type to appear in second a union is used -> second field name varies depending on type.
Permit less strongly typed languages (VBScript) to invoke methods in strongly typed ones (C++). Invoke method should check params supplied are of correct type before passing to relevant function - if not can coerce using VariantChangeType().
Dispatch Interfaces slow + cumbersome. High performance languages (C++) require access to interfaces directly -> need to know details of methods on interface. Type library supplies binary description of interface properties, methods and arguments - analogous to compiled, language independent C++ header file.
Type library described by IDL, generated by MIDL compiler and given .tlb extension. Often linked into .dll or.exe containing COM component.
For clients to access type library further registry entries required, the component key under CLSID requires a TypeLib value containing the LIBID (GUID assigned to type library).
A key of the same value is created under TypeLib. Beneath here a win32 value specifies the file containing the type library, e.g. the DLL containing COM component.
Should implement dispatch interfaces as dual interfaces. Make all IDispatch methods available in vtable in addition to interfaces custom methods. Enables all clients' access to functionality, those written in advanced languages being able to gain direct access.
Create ActiveX User Interface Controls
ActiveX control = COM object designed to be placed in ActiveX control container - application dialogue box, web pages, etc. - to perform self contained function.
Frequently have .OCX extension, though these are actually DLLs.
Controls & Containers
ActiveX control = any COM object implementing IUnknwon + hosted by self registering server (either DLL or EXE). Client of ActiveX control = container, into which controls are sited or located.
Controls usually have UI. Containers expect controls to participate as part of their UI.
Containers may have additional requirements, such as required interfaces, to those laid out above. Most expect controls to maintain state data, fire events and support specific interfaces - often quiet a few.
MFC and ATL provide wizards to generate basic framework (including support for frequently required interfaces).
Support properties, methods and events to make control programmable. Usually support dispatch interface to make widely available.
Common to many controls, examples include font used for display, foreground and background colours, etc. Distinguished from custom properties - those specific to control.
Provide information about appearance of container. Controls should appear integrated into container - e.g. background colour of container should be used by control.
Notifications from control to container. Interface defined by control and implemented by container - makes it an outgoing interface. Distinction made between stock, such as mouse clicks, and custom events.
UI to change control properties. Property page based on dialogue box resource and is separate COM object with own CLSID.
Allows property values to be serialised.
Create with MFC
MFC supplies ActiveX ControlWizard to help create controls. Can specify number of controls in project, support for licensing, help files, etc. Creates DLLs with .OCX extension (not requirement for ActiveX) - leftover from days of OLE controls. Type library source code in ODL (predecessor to IDL), which the MIDL compiler is capable of processing.
Produces classes to handle the controls DLL server, the control itself and its property page. The control class is derived from COleControl (a child of CWnd and CCmdTarget) -> provides many methods related to operations of ActiveX controls, such as event firing, stock properties, persistence, etc.
Defining Control Interface
ControlWizard defines two dispatch interfaces, _D and _DEvents.
Add properties and methods using ClassWizard. For example to add BackColor stock property - go to property tab, select external names and from list select BackColor. This will add two methods GetBackColor() and SetBackColor(). Custom properties handled similarly, enter name into external name list and ClassWizard will add accessor functions and member variable to hold value.
Dispatch functionality handled by maps, declared using DECLARE_DISPATCH_MAP table and implemented by BEGIN_DISPATCH_MAP... END_DISPATCH_MAP. Very similar to message maps, mapping automation requests to class implementation of requested functionality. Note, stock properties supported by own macros.
Dispatch map handles both properties and methods.
Event map (similar to dispatch map) used to manage events. Stock events handled by base classes, custom events mapped by table from event identifier to function.
Handled by COleControl::DoPropExchange() that serialises to storage medium, usually the controls container. Function overridden in controls implementation class. Passed CPropExchange - encapsulates property exchange context. Must add own code to serialise custom properties, using MFC provided functions starting with PX_. For example a control contains a short member variable containing number of symbols supported by control , serialise using:
void CMyCtrl::DoPropExchange(CPropExchange *pPX)
ExchangeVersion(pPX, MAKELONG(_wVerMinor, _wVerMajor);
PX_short(pPX, "NumberOfSymbols", m_numberOfSymbols, 3);
Last argument to PX_short specifies default value to use if value can not be retrieved from storage medium.
Provide access to controls properties. Framework uses PROPPAGEID macros to specify array of property page IDs for controls property sheet - have to maintain structure yourself (not managed by ClassWizard).
In addition to standard DDX and DDV functions, ClassWizard uses DDP_ functions to transfer data between property page and control properties.
Stock pages available for stock properties, for example adding PROPPAGEID(CLSID_CColorPropPage) to array ensures stock colour selection page is added to property sheet.
Controls created using ActiveX ControlWizard are by their nature user-interface controls. Consequently, some of the customisation takes place within OnDraw(). Avoid using fixed values in drawing code, scale all output to fit bounding rectangle provide. Ensures control always visible and internal proportions maintained.
Create with ATL
Start like any ATL control with ATL ObjectWizard. When adding control select from Controls category - several to choose from; full controls can be embedded in any ActiveX container, Lite controls for use with Internet Explorer, Composite controls such as dialogue boxes can contain other ActiveX controls, HTML controls use embedded web-browser control to display HTML pages.
Instruct ObjectWizard to support connection points for object being inserted. On Stock Properties tab select those that will be available for the control.
Wizard implements interface and class (derived from CComControl) implementation for control. Directly below COM map for interface mapping is a property map - eases implementation of property persistence and property page display.
From ClassView right click on interface and choose to add property. Property access stubs created - must add member variable to class manually.
ObjectWizard created .IDL file describing interfaces to object. One of these is an event interface, marked with "source" attribute to indicate this object is the source of events (an example of a connection point). COM object exposing connection points also support IConnectionPointContainer to manage connection of source interfaces to corresponding client (a sink). Sink objects implement methods defined by source - through connection point sink objects interface provided to source, providing source with access to sinks implementation. To fire event source calls corresponding method n sinks interface.
To define event
- First add method to source interface representing event.
- After adding method create a connection point. This requires a type library to be available, so the next step is to generate the type library.
- Implement connection point by right clicking on class in ClassView and selecting "Implement Connection Point". The wizard creates a proxy class that implements event methods added to event interface and a connection point map to the class implementation.
Form ClassView right click on interface and choose to add method.
Each page of property sheet = separate object, implemented by class derived from IPropertyPage.
Use ObjectWizard to insert new Property Page control object. Implementation provided for Apply / OK buttons - must modify provided code to carry out appropriate action.
The CComControl::OnDraw() function receives an ATL_DRAWINFO structure containing, amongst other things, the DC to draw into and bounding rectangle of control. DC provided as a HDC (handle to DC) - the raw data type (no wrappering of MFC CDC). As receive HDC must use GDI API functions, not CDC versions - very similar to each other.
COM Component reuse
Key benefit OO = ability reuse (inheritance) / combine (containment) existing objects to perform new tasks. COM supports containment but not implementation inheritance. COM treats interfaces as immutable, if inheritance used it is possible for an interface to change because of changes to a base class - not allowed!
COM does support interface inheritance as their definitions are implemented as abstract base classes with no implementation details. Deriving interfaces allows specification of vtable order to be defined, for instance
IEncoder:: public IUknown
causes the vtable to contain the three interfaces for IUnknown followed by those defined for IEncoder.
Similar to C++ containment. Outer object creates an inner one, storing reference to inner IUnknown as data member. Outer object implements methods of inner as stubs, forwarding method calls it receives to inner. Not all inner object methods need be exposed - only those deemed fit by the outer. Calls to IUnknown are never forwarded as inner has no knowledge of the interfaces exposed by the outer.
As with containment, outer stores reference to inner objects IUnknown. Unlike containment, outer object exposes interfaces of inner directly to clients - less overhead as no stub functions.
Inner object must be written to support aggregation - as it can not handle calls to IUnknown on behalf of outer any calls it receives to this interface must be delegated to the outer objects IUknown (the controlling unknown).
When outer creates inner, second param of CoCreateInstance() used to pass address of controlling unknown.
The inner object must be able to handle calls to IUnknown in some circumstances, for example requests from the outer object. To achieve this two IUnknowns are implemented. External clients call the delegeating IUnknown which redirects calls to the controlling unknown, while the outer component calls the non-delegating IUnknown.
Implementing with ATL
To create aggregateable (inner) object ensure Aggregation = yes (default) is selected in the ATL Object Wizard while creating the object.
To implement outer:
- Add IUnknown pointer to class object.
- In class definition add macro DECLARE_GET_CONTROLLING_UNKNOWN
- Override FinalConstruct() to call CoCreateInstance() with first param = CLSID of inner object and second = return value of GetControllingUnknown(), fifth param should be member variable used to store pointer to inner objects IUnknown.
- Override FinalRelease() to call IUnknown::Release() on inner object.
- In outer objects interface map add COM_INTERFACE_ENTRY_AGGREGATE together with pointer to inner objects IUnknown
- Add interface definition of inner object to that of the outer in the .IDL file.
Create and Use Active Document
ActiveX documents also known as Active Documents are files created by ActiveX document server applications. Can be viewed and edited by application creating them or any ActiveX document container.
Word documents are example. Internet Explorer (ActiveX document container) can view and edit them using full functionality of Word (ActiveX document server) within the browser window.
Powerful feature of web sites for users of MS Apps -> extend IE functionality, providing versatile, feature-rich alternative to HTML.
Use ActiveX Documents
When container app loads ActiveX document it also loads menu, toolbar and status bar from associated server application. These resources merged into container - in effect the server takes over the container when the document is displayed.
ActiveX Documents on the WWW
IE uses its ActiveX container capabilities when viewing documents stored on web server. Word documents, Excel spreadsheets, etc. made available for viewing and editing by anyone who has corresponding document server applications installed on their machine. Changes made cannot be saved back to Internet host, must be stored locally.
Common use = forms centre of intranet which contains Word / Excel versions of common forms (e.g. training, leave, sickness, etc.). Employee loads blank form into IE, completes form and faxes or e-mails to relevant department.
Requirement for server application on client machine can be viewed as limitation. Can produce lightweight versions for distribution to clients, whilst keeping heavyweight version centrally to update documents on web site. Example is Word document viewer application - allows clients to view word documents on web site without need for full version to be installed on their machine.
As ActiveX documents only work within compatible browsers, only really for use where platform and configuration of clients can be specified.
Create ActiveX Document Server
VC6 makes task easy. In step 3 of AppWizard ensure Full-server and Active document server options selected. With these selected the wizard creates additional toolbar and menu resources with the ID IDR_SRVR_INPLACE - these are merged with the clients menus and toolbars. Besides normal editing of resources required when creating application (i.e. IDR_MAINFRAME) must also change these new ones to contain the required functionality.
COM defines 4 models to simplify issues surrounding multithreaded development.
Describe type + degree of thread safety implemented by component. Before using COM component clients must initialise COM - at this point can specify threading model is uses. If threading model used by client not compatible with that of the component then COM will ensure the two communicate in a thread safe manner (may adversely affect performance).
In-process components must record their threading model in registry - allows COM to determine if marshalling required - valid indicators are none, apartment, free, both.
Each thread accessing a COM component must initialise COM with a call to CoInitializeEx() and un-initialise with a call to CoUninitialize().
Component requires all client requests to be issued from single thread. Very efficient access for thread that created component, any others attempting to access it must be marshalled by the proxy/stub mechanism -> access via component message queue (not direct) consequently slow. Any requests from creator thread take priority to those from others as they go direct, not via a queue.
All clients have direct access to component interface - no use of proxy / stub required. Apartment provides logical structure for thread concurrency. Created when COM initialised an associated with 1+ threads and 1+ COM object instances. All threads within apartment have direct access to interfaces on all objects in it. 2 types:
Process can contain any number of STAs but only one MTA.
1 thread creates + calls objects -> object synchronisation. Better performance than single threaded model. Components created by STA thread can only be accessed by it -> protection from multiple thread access.
Threads wanting to access object in another apartment must be marshalled - direct access not allowed as this would expose component to multiple threads (violates STA). Marshalling achieved through messages, each STA having a message loop to receive marshalled calls from other processes or apartments within same process.
Also called free-threaded. Multiple threads reside in same apartment. Highest performance. No marshalling required between threads. Components created by MTA threads must be thread safe - use made of synchronisation objects (events, mutexes, etc.)
Supports both apartment and free-threading.