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