PureBasic COM Object Framework

Contents


Introduction

With the introduction of interfaces in PureBasic, accessing external COM (Component Object Model) objects has become fairly simple (in terms of needed amounts code to do so). However, creating COM objects in PB to be used externally, or with API commands is still something that requires lots of knowledge about PB and COM and quite some work to do. This framework tries to change that. It provides a set of macros to easily define an object and also to automate most of the tasks common to all object implementations (like implementing the IUnknown interface). Additionally, it provides extensive debugging functionality to track down bugs in your implementation. There are also some macros that are useful for general COM development. Top

Feature list

General - No preprocessing needed. Evetything is pure PB code. - Everything contained in residents and includefiles, minimizing possible problems with future PB versions - Threadsafe and unicode ready - Works with the EnableExplicit compiler option Object implementation - Simple macros to define the class structure - Multiple interfaces supported in one class (up to 20) - Implementation of IUnknown is completly done by the framework - Constructor/Destructor support - For not-implemented methods, a default method is automatically inserted which returns #E_NOTIMPL - A VTable is created with a single macro call - Macros for easy definition and handling of GUID values (IID, CLSID, ...) Debugging - Complete tracking of all calls to the objects methods, including displaying return values - Tracking of calls to dead (allready freed) objects - Catching of method calls outside the VTable (to find calls to wrong interfaces) - The amout of debug output for the tracking can be customized with 'DebugLevel' - Conversion of GUID and HRESULT values to text for easier debugging - With Debugger off, the debug output is printed with OutputDebugString_() for easy dll debugging Top

License

Copyright (c) 2006 Timo Harter Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. Top

Installation

To install the framework, just extract the zip file directly into your PureBasic folder. The 'ComFramework' folder must be placed in the main PureBasic folder and the content of the 'Residents' folder must be copied to the PureBasic residents folder. The place and name of the 'ComFramework' folder is important, as this is where the includefiles will be automatically included from if macros from the framework are used. Files included in the package: \Residents\ - Resident files containing the base macros of the framework \ComFramework\ - Main includefiles. Included in the source if the framework is used \ComFramework\Interfaces\ - Information on each supported interface. Included in the source as needed \ComFramework\Examples\ - Example sources Top

General info

The COM framework uses names starting with '__COM' for all its internal uses, so to avoid any conflicts, you should not use any names in your code that start with '__COM'. NOTE: The purpose of this framework is to implement interfaces that are defined in the Microsoft Component Object Model. The intend is not to provide any general OOP functionality for PureBasic. To automate most of the work, the framework needs information on the given interfaces, this is why only predefined interfaces can be implemented, not any custom one. (currently 855 interfaces are known to the framework) The framework is implemented as macros in resident files and a set of includefiles. You do not need to include any of the includefiles manually. They will be included automatically as needed by the macros in the resident files. Top

GUID management macros

The GUID macros are independant from the rest of the framework (using them does not cause the baseinclude to be included), so they can be also used for easier accessing of external COM objects only. The macros come as 3 names, ending on ...GUID, ...IID and ...CLSID. They are exactly the same, they just exist separately to allow writing more readable code. DefineGUID(Name, long, word1, word2, byte1, byte2, byte3, byte4, byte5, byte6, byte7, byte8) DefineIID(Name, long, word1, word2, byte1, byte2, byte3, byte4, byte5, byte6, byte7, byte8) DefineCLSID(Name, long, word1, word2, byte1, byte2, byte3, byte4, byte5, byte6, byte7, byte8) These are the same as the corresponding macro in C. Used to define a GUID in one line. The macro actually creates a DataSection with a label called 'Name' and the data following, so the GUID can later be accessed as '?Name', where Name is the name used in the definition. The macro is safe for multiple use. Multiple definitions of the same GUID will be ignored, which allows to simply define a GUID where it is needed without worrying that it might be defined in another part of the project (other includefile for example) as well. Example:
; define the IUnknown IID ; DefineIID(IID_IUnknown, $00000000, $0000, $0000, $C0, $00, $00, $00, $00, $00, $00, $46) ; ; use the IID value ; SomeObject\QueryInterface(?IID_IUnknown, @NewPointer.IUnknown)
DefineKnownGUID(Name) DefineKnownIID(Name) DefineKnownCLSID(Name) The framework includes a resident file called 'GuidList.res', which contains a big macro containing a list of GUID values extracted from the Microsoft Platform SDK as well as the DirectX9 sdk. (Total number: 2705 GUIDs) These macros allow to easily define such a known value by its name. Most of the common COM related values should be known. As the list is contained within only one macro, the general compilation speed is not affected by this huge list. These macros are protected against multiple use in the same way as the Define...() macros, so defining twice a value with any of these macros will have no effect. Example:
; define the IUnknown IID, the easy way ; the result is the same as the above example ; DefineKnownIID(IID_IUnknown)
CompareGUID(guid1, guid2) CompareIID(iid1, iid2) CompareCLSID(clsid1, clsid2) Compares the two GUID values and returns 1 if they are equal and 0 otherwise. This is just a simple wrapper to CompareMemory() to be able to write more readable code. Example:
; returns 1 of course ; DefineKnownIID(IID_IUnknown) Debug CompareIID(?IID_IUnknown, ?IID_IUnknown)
Top

Object implementation

The following steps are needed to implement a COM object using this framework

Defining the class

The following is a skeleton of a definition for a COM object class. The order is important. This whole block may not be put inside a procedure.
COMClass(<ObjectName>) ; define implemented interfaces. At least one such statement is needed COMInterface(<ObjectName>, <InterfaceName> [, <ExtendedInterface> [, <ExtendedInterface> ...]]) ; define class local variables (optional) COMClassData(<ObjectName>) Value1.l Value2$ EndComClassData ; define constructor (optional) COMConstructor(<ObjectName> [, (<Argumentlist>)]) ; return a different interface from the constructor than IUnknown (optional) COMConstructorReturn <InterfaceName> EndCOMConstructor ; define destructor (optional) COMDestructor(<ObjectName>) EndCOMDestructor EndCOMClass(<ObjectName>, <Interface1>, <Interface2>, ...)
Detais: COMClass(<ObjectName>) Starts the definition of a new class in the COM framework. A structure with the name will be created to represent the objects structure and private data. COMInterface(<ObjectName>, <InterfaceName> [, <ExtendedInterface> [, <ExtendedInterface> ...]]) Defines which interfaces will be implemented in this class. If an interface that will be implemented actually extends another one, the framework will not know that the extended base interface is supported as well. You can make it recognize these interfaces as well by specifying the interfaces that are contained within the implemented interface as the optional parameters. (Up to 5 optional parameters are possible) NOTE: IUnknown is always automatically implemented by the framework, so do not specify it here! A class can only support up to 20 interfaces (including the extended ones). For any interface that is implemented here (or specified as an extended interface), the corresponding IID value will be automatically defined with the DefineIID() macro, as it will be needed by the framework. DefineIID() is protected against multiple use, so a double definition is no problem. COMClassData(<ObjectName>) / EndComClassData This is optional and must be put after all COMInterface() definitions. It allows to add private data to the structure that is defined by the COMClass() macro. Since the values inside will be structure members, all values must include the type. Also static arrays like in structures are possible. COMConstructor(<ObjectName> [, (<Argumentlist>)]) / EndCOMConstructor This is optional and must be put after COMClassData(). It allows to define a custom constructor for the class. It allows to define an optional argument list for the constructor procedure. (it must be the second argument in the call and MUST include the (). To create a new object later in the code, 'New_ObjectName()' is used. If the constructor specifies an argument list, these arguments will be used in the New_ procedure like this: 'New_ObjectName(<arguments>)' Inside the constructor, a local structure pointer called *THIS.<ObjectName> is available to access the private data of the new object. COMConstructorReturn <InterfaceName> By default, the New_ObjectName() function will return a pointer to the IUnknown interface of the new object. If another interface pointer should be returned here, this can be done by calling COMConstructorReturn <InterfaceName> inside the constructor. This quits the constructor, just like ProcedureReturn. COMDestructor(<ObjectName>) / EndCOMDestructor Allows to define a custom destructor that is called once all references to the object have been released, and before the object is freed. Like the constructor, you can use the *THIS.<ObjectName> pointer to access the private data. EndCOMClass(<ObjectName>, <Interface1>, <Interface2>, ...) This is actually a big macro that will do all the main implementation work for the class. It will define the IUnknown methods that handle the lifetime of the objects. It will also define the New_<objectname>() procedure that is used to create a new instance of the class. For reasons of the implementation, you need to specify again all the interfaces that are implemented in this class in the macro call. This includes the interfaces that were specified as extended interfaces in the COMInterface() calls. Example:
; This is a basic example which defines a class that implements only the IDispatch interface ; COMClass(MyDispatch) COMInterface(MyDispatch, IDispatch) ; extends only IUnknown, which is handled automatically COMClassData(MyDispatch) ; define our entries in the object structure Value1.l Value2.l EndCOMClassData COMConstructor(MyDispatch, (Value1, Value2)) ; define a constructor which takes the 2 values as arguments *THIS\Value1 = Value1 *THIS\Value2 = Value2 COMConstructorReturn IDispatch; We want the IDispatch pointer to be returned: ; NOTE: IDispatch extends IUnknown, so one could think that the IUnknown and IDispatch pointers for this ; implementation would be identical. This is not the case though. The framework implements a separate ; IUnknown interface that is not extended by anything. This pointer is initially returned if ; COMConstructorReturn is not used. It is also returned anytime IUnknown is requested from the object, since ; it is a basic COM principle that a query for IUnknown on an object must always return the same pointer. EndCOMConstructor COMDestructor(MyDispatch) ; do any needed free stuff here. ; *THIS\Value1 and *THIS\Value2 can be accessed here as well EndCOMConstructor EndCOMClass(MyDispatch, IDispatch) ; after all other stuff is done, such an object is created with Dispatch.IDispatch = New_MyDispatch(1, 2) ; where 1,2 are the two arguments for the constructor ; NOTE: This part alone is useless. To create functional objects, you need to implement the ; methods and call the needed VTable creation macros.
Top

Implementing the methods

The methods are simply procedures, which must be named after a specific naming scheme, and have a pointer called *THIS.<objectname> as first argument. (a method with 0 arguments will have only the *THIS pointer in in the definition. Additionally, the 'COMMethodOf(ObjectName)' macro must be called first thing inside the procedure. For readable code, the macro can be directly on the procedure line, even without any separator. A method procedure looks like this: Procedure <ObjectName>_<InterfaceName>_<MethodName>(*THIS.<ObjectName>, <Argument list>) COMMethodOf(<ObjectName>) EndProcedure Example:
Procedure MyDispatch_IDispatch_Invoke(*THIS.MyDispatch, ...) COMMethodOf(MyDispatch) EndProcedure
It is important that the name is exactly like this, so the VTable creation macro can detect which methods were implemented and which not. It is important to specify the .<ObjectName> type for the *THIS pointer, otherwise the COMMethodOf() macro will not work correctly. The purpose of this macro is to adjust the *THIS pointer. If the macro is not used, *THIS will point to the base of the called Interface, not to the base of the object structure. These are two different pointers, so accessing the local values inside the Object structure (defined with COMClassData() above) will fail. The COMMethodOf() macro ensures that this pointer is set to the base structure, so the values can be accessed with it. So theoretically, this macro can be left out if nothing is accessed with the *THIS pointer inside the procedure, but it is recommended to add it everywhere for consistency, and to avoid introducing a hard to find bug if it is left out where it is actually needed. Notes: Never implement the Iunknown QueeyInterface(), AddRef(), Release() methods for any Interface. This is always done by the framework. If an interface method is not defined in the source, the VTable creation macro will detect this and automatically insert a default method that returns #E_NOTIMPL. So the VTable will never contain any NULL pointers that could lead to a crash. #E_NOTIMPL is accepted as a result for many optional parts of COM Interfaces, so you can just leave those methods that you do not want/need to implement for an interface out, and the default method will be inserted. If an interface extends another one, the methods must only be implemented for the big interface that extends the others. These methods will be reused for the extended interface. If a method should only return a certain value but do nothing else, the COMEmptyMethod() macro can be used to avoid the need to declare the method procedure fully: COMEmptyMethod(<ObjectName>, <InterfaceName>, <MethodName>, <ReturnValue>) <ReturnValue> must be a constant long expression. The macro will implement a method with the right number of arguments for this method that will do nothing but return the given returnvalue. If two interfaces in the Class have the same method. (for example if both extend the same base interface), the COMReuseMethod() macro can be used to tell the framework to use the method that is implemented for one interface for another one as well, so the code does not need to be dublicated. COMReuseMethod(<ObjectName>, <ImplementingInterface>, <MethodName>, <ReusingInterface>) <ImplementingInterface> is the interface that has the actual method procedure implemented, <ReusingInterface> is the one that will use it as well. A method can be reused by many interfaces, but only within the same class.
Top

Calling the macros to create the VTables

Due to limitations of the PB macros (well, they are quite powerfull as this framework prooves, but here i hit a wall), the VTables for the class cannot be automatically created. You need to call one macro per Interface in the class to build them. Hopefully there will be a more comfortable way to do this in the future, but for now, this is still better than actually building the VTables manually. The macros have this form: BuildCOMVTable_<InterfaceName>(<ObjectName>) The Interfacename is part of the macro name. The required macro for each interface is automatically included with the source once it is included in the class with the the COMInterface() macro. Important notes: These macros must be put in the code below ALL method implementations and other method related macros. This is because the macros will check which methods were actually implemented to select the proper default methods or reused methods in the VTable. These macros produce code that must be executed, unlike the Class definition macros which only produce declarations and procedures. The macros must be executed BEFORE any instance of the class is created. They can be placed inside procedures. For an application, this can be somewhere before the real application code starts. For a COM dll, this can be the AttachProcess() procedure. You could also place these macros into a procedure and call it from the constructor of the class. This will work as well, as then the VTables are filled before the first interface on the object is actually called. YOu cannot place them into the constructor directly, as they need to be below all method implementations. Example:
BuildCOMVTable_IDispatch(MyDispatch) ; build the VTable for the IDispatch interface on the MyDispatch class.
Top

Creating object instances

Once all the above steps are taken (Class definition, method implementation, VTable creation), the implementaion is complete and objects can be created. This is done through a procedure called 'New_<ObjectName>()' It returns the IUnknown pointer of the object unless COMConstructorReturn was used in the constructor to retun a different interface pointer. If the constructor was defined with additional parameters, these must be passed to the New_<ObjectName>() procedure as well. New objects are created with a referencecount of 1. An object is destroyed when its referencecount reaches 0. This means that once usage of the object is completed, you must release your own reference to it by calling the Release() method on the interface. Example:
Dispatch.IDispatch = New_MyDispatch(1, 2) ; using the class definition of the above examples ; do something with the object (pass to API function, etc) Dispatch\Release() ; release our reference to the object. if no other references remain, it will be freed.
Top

Debugging

To enable the frameworks debugging functionality, add the 'EnableCOMDebugging' keyword at the beginning of the source (or before the class definitions). Using this macro will cause the framework to add the needed debugging code the the objects to track the calls and other stuff as described below. NOTE: When debugging is enabled, the framework will include large amounts of data with the executable (like lists of all IID and HRESULT values), so it is not recommended to leave this call inside a final build.

Debug output

If the PureBasic debugger is enabled when compiling the source, the debug output will be displayed in the debugger. If the debugger is not enabled, but EnableCOMDebugging is set, the debug output will be displayed with the OutputDebugString_() api. This allows for debugging of COM dlls where the PB debugger cannot be used. This output can be read with debugger programs like DebugView (http://www.sysinternals.com/Utilities/DebugView.html) Top

Provided debugging information

There are several levels of debug information that can be provided. They can be set with the PB 'DebugLevel' command. Higher levels include the output of all levels below as well. The following levels are available (lowest first).
#COM_DEBUG_Critical #COM_DEBUG_UnknownInterface #COM_DEBUG_ObjectStats #COM_DEBUG_MethodEntry #COM_DEBUG_MethodLeave #COM_DEBUG_FormatResult #COM_DEBUG_All
#COM_DEBUG_Critical Only debug information about critical conditions. (Stuff that is usually followed by a crash) One such situation is if a call to an interface outside of its VTable is encountered. This happens when the caller expected a larger interface than it actually was. Also if tracking of dead objects is enabled (see below), a catched call to a dead object will be reported as critical. #COM_DEBUG_UnknownInterface Displays a message when somebody queried your object (with QueryInterface()) for an interface that the object does not support. The message includes the name of the IID that was queried. This condition is not neccesarily a bug, as the IUnknown implementation will correctly return #E_NOTIMPL here, and API functions commonly query for multiple interfaces to test what functionality is available. This debug information can still be very helpfull, as it shows which interfaces the caller actually expected. #COM_DEBUG_ObjectStats Displays information about the lifetime (referencecount) of the object. It tracks the AddRef()/Release() as well as QueryInterface() calls and displays the current count. It also displays a message when the object is finally released. #COM_DEBUG_MethodEntry Displays all the method calls on the object. Very helpfull to track down which sequence of calls leads to a crash certain crash for example. #COM_DEBUG_MethodLeave Displays the returned values from all method calls. #COM_DEBUG_FormatResult Displays the equivalent HRESULT constant that corresponds to the return value (if it is an error value), as well as a text representation of the value. #COM_DEBUG_All Highest level, includes all others. This is the default level if 'DebugLevel' is not used. Note: DebugLevel is a compiletime statement, so it has an effect on all the lines compiled after it, not the lines executed after it. Changing the debuglevel between the implementation of COM classes causes the two to have a different level of output. So the calls of one class can be tracked closely, while for the other, only critical conditions are catched for example. Since DebugLevel is only affecting the PureBasic debugger output, the amount of output for OutputDebugString_() cannot be changed with it. If the PB debugger is off, the DebugLevel statememnts are ignored, and the framework will simply output all debug data. (equal to using #COM_DEBUG_All) Top

Tracking dead objects

One common source of bugs are problems with the lifetime of objects. For example if Release() is called once too often, the last call will be on an object which was allready freed (a dead object). Usually such calls result in a crash, although sometimes they succeed because the memory of the object was not trashed by something else yet, which makes it even harder to find such bugs. This framework provides tracking of all released objects to identify such calls. Any call on a method of an object that was allready released will be reported with the #COM_DEBUG_Critical DebugLevel. To achieve this, the memory af all previously existing objects must be kept allocated until the program ends. This can lead to a lot of memory consumption of objects are created/freed frequently. This is why ithis option is not enabled by default. To enable it, use the 'EnableCOMDeadTracking' statement. Top

Other debugging functions

Additinally to this automatic tracking of the objects, the framework provides some extra functions for debugging purposes (only available if 'EnableCOMDebugging' is used. DisplayCOMObjectList() This function simply prints a list of all currently existing objects on the debug output. The output contains the type of the object, as well as its current referencecount. This function can be usefull for example at the end of the program to find any objects that did not get correctly released. DisplaySupportedInterfaces(*Object) This function prints all the interfaces that the given object supports (works only for interfaces that are known to the framework, so no custom ones. It is very useful to know the features supported by an object. Constant$ = GetCOMErrorConstant(ErrorCode) Returns the constant name for a HRESULT value. (returnvalue of methods) This covers only negative values, as values >=0 indicate success and can have various meanings depending on the method. Message$ = GetCOMErrorMessage(ErrorCode) Returns a text representation for the HRESULT error value. These messages were extracted from WinError.h GUIDFromName(Name$) IIDFromName(Name$) CLSIDFromName(Name$) Returns a GUID value for the given name. IIDFromName("IID_IUnknown") for example returns the IUnknown IID. Name$ = NameFromGUID(guid) Name$ = NameFromIID(iid) Name$ = NameFromCLSID(clsid) Returns the name for the given GUID. This is very helpfull when unknown values are encountered to know what they actually represent. NOTE: Unlike the other macros (where ...GUID(), ...IID() and ...CLSID() actually do the same), here each function is different. The reason is that IID and CLSID values exist with the same names, so NameFromGUID() only searches names starting with 'GUID_', NameFromIID() only searches 'IID_' and NameFromCLSID() only searches 'CLSID_' values.
Top