COM in ASM - Part 2 ------------------------------------------------------------------------------ My previous atricle described how to use COM objects in your assembly language programs. It described only how to call COM methods, but not how to create your own COM objects. This article will describe how to do that. This article will describe implementing COM Objects, using MASM syntax. TASM or NASM assemblers will not be considered, however the methods can be easily applied to any assembler. This article will also not describe some of the more advanced features of COM such as reuse, threading, servers/clients, and so on. These will presented in future articles. COM Interfaces Review ------------------------------------------------------------------------------ An interface definition specifies the interface's methods, their return types, the number and types of their parameters, and what the methods must do. Here is a sample interface definition: IInterface struct lpVtbl dd ? IInterface ends IInterfaceVtbl struct ; IUnknown methods STDMETHOD QueryInterface, :DWORD, :DWORD, :DWORD STDMETHOD AddRef, :DWORD STDMETHOD Release, :DWORD ; IInterface methods STDMETHOD Method1, :DWORD STDMETHOD Method2, :DWORD IInterfaceVtbl ends STDMETHOD is used to simplify the interface declaration, and is defined as: STDMETHOD MACRO name, argl :VARARG LOCAL @tmp_a LOCAL @tmp_b @tmp_a TYPEDEF PROTO argl @tmp_b TYPEDEF PTR @tmp_a name @tmp_b ? ENDM This macro is used to greatly simplify interface declarations, and so that the MASM invoke syntax can be used. (Macro originally by Ewald :) Access to the interface's methods occurs through a pointer. This pointer points to a table of function pointers, called a vtable. Here is a sample method call: mov eax, [lpif] ; lpif is the interface pointer mov eax, [eax] ; get the address of the vtable invoke (IInterfaceVtbl [eax]).Method1, [lpif] ; indirect call to the function - or - invoke [eax][IInterfaceVtbl.Method2], [lpif] ; alternate notation Two different styles of addressing the members are shown. Both notations produce equivalent code, so the method used is a matter of personal preference. All interfaces must inherit from the IUnknown interface. This means that the first 3 methods of the vtable must be QueryInterface, AddRef, and Release. The purpose and implementation of these methods will be discussed later. GUIDS ------------------------------------------------------------------------------ A GUID is a Globally Unique ID. A GUID is a 16-byte number, that is unique to an interface. COM uses GUID's to identify different interfaces from one another. Using this method prevents name clashing as well as version clashing. To get a GUID, you use a generator utility that is included with most win32 development packages. A GUID is represented by the following structure: GUID STRUCT Data1 dd ? Data2 dw ? Data3 dw ? Data4 db 8 dup(?) GUID ENDS A GUID is then defined in the data section: MyGUID GUID <3F2504E0h, 4f89h, 11D3h, <9Ah, 0C3h, 0h, 0h, 0E8h, 2Ch, 3h, 1h>> Once a GUID is assigned to an interface and published, no furthur changes to the interface definition are allowed. Note, that this does mean that the interface implementation may not change, only the definition. For changes to the interface definition, a new GUID must be assigned. COM Objects ------------------------------------------------------------------------------ A COM object is simply an implementation of an interface. Implentation details are not covered by the COM standard, so we are free to implement our objects as we choose, so long as they satisfy all the requirements of the interface definition. A typical object will contain pointers to the various interfaces it supports, a reference count, and any other data that the object needs. Here is a sample object definition, implemented as a structure: Object struct interface IInterface ; pointer to an IInterface nRefCount dd ? ; reference count nValue dd ? ; private object data Object ends We also have to define the vtable's we are going to be using. These tables must be static, and cannot change during run-time. Each member of the vtable is a pointer to a method. Following is a method for defining the vtable. @@IInterface segment dword vtblIInterface: dd offset IInterface@QueryInterface dd offset IInterface@AddRef dd offset IInterface@Release dd offset IInterface@GetValue dd offset IInterface@SetValue @@IInterface ends Reference Counting ------------------------------------------------------------------------------ COM object manage their lifetimes through reference counting. Each object maintains a reference count that keeps track of how many instances of the interface pointer have been created. The object is required to keep a counter that supports 2^32 instances, meaning the reference count must be a DWORD. When the reference count drops to zero, the object is no longer in use, and it destroys itself. The 2 IUnknown methods AddRef and Release handle the reference counting for a COM object. QueryInterface ------------------------------------------------------------------------------ The QueryInterface method is used by a COM object to determine if the object supports a given interface, and then if supported, to get the interface pointer. There are 3 rules to implementing the QueryInterface method: 1. Objects must have an identity - a call to QueryInterface must always return the same pointer value. 2. The set of interfaces of an object must never change - for example, if a call to QueryInterface with on IID succeeds once, it must succeed always. Likewise, if it fails once, it must fail always. 3. It must be possible to successfully query an interface of an object from any other interface. QueryInterface returns a pointer to a specified interface on an object to which a client currently holds an interface pointer. This function must call the AddRef method on the pointer it returns. Following are the QueryInterface parameters: pif : [in] a pointer to the calling interface riid : [in] pointer to the IID of the interface being queried ppv : [out] pointer to the pointer of the interface that is to be set. If the interface is not supported, the pointed to value is set to 0 QueryInterface returns the following: S_OK if the interface is supported E_NOINTERFACE if not supported Here is a simple assembly implementation of QueryInterface: IInterface@QueryInterface proc uses ebx pif:DWORD, riid:DWORD, ppv:DWORD ; The following compares the requested IID with the available ones. ; In this case, because IInterface inherits from IUnknown, the IInterface ; interface is prefixed with the IUnknown methods, and these 2 interfaces ; share the same interface pointer. invoke IsEqualGUID, [riid], addr IID_IInterface or eax,eax jnz @1 invoke IsEqualGUID, [riid], addr IID_IUnknown or eax,eax jnz @1 jmp @NoInterface @1: ; GETOBJECTPOINTER is a macro that will put the object pointer into eax, ; when given the name of the object, the name of the interface, and the ; interface pointer. GETOBJECTPOINTER Object, interface, pif ; now get the pointer to the requested interface lea eax, (Object ptr [eax]).interface ; set *ppv with this interface pointer mov ebx, [ppv] mov dword ptr [ebx], eax ; increment the reference count by calling AddRef GETOBJECTPOINTER Object, interface, pif mov eax, (Object ptr [eax]).interface invoke (IInterfaceVtbl ptr [eax]).AddRef, pif ; return S_OK mov eax, S_OK jmp return @NoInterface: ; interface not supported, so set *ppv to zero mov eax, [ppv] mov dword ptr [eax], 0 ; return E_NOINTERFACE mov eax, E_NOINTERFACE return: ret IInterface@QueryInterface endp AddRef ------------------------------------------------------------------------------ The AddRef method is used to increment the reference count for an interface of an object. It should be called for every new copy of an interface pointer to an object. AddRef takes no parameters, other than the interface pointer required for all methods. AddRef should return the new reference count. However, this value is to be used by callers only for testing purposes, as it may be unstable in certain situations. Following is a simple implementation of the AddRef method: IInterface@AddRef proc pif:DWORD GETOBJECTPOINTER Object, interface, pif ; increment the reference count inc [(Object ptr [eax]).nRefCount] ; now return the count mov eax, [(Object ptr [eax]).nRefCount] ret IInterface@AddRef endp Release ------------------------------------------------------------------------------ Release decrements the reference count for the calling interface on a object. If the reference count on the object is decrememnted to 0, then the object is freed from memory. This function should be called when you no longer need to use an interface pointer Like AddRef, Release takes only one parameter - the interface pointer. It also returns the current value of the reference count, which, similarly, is to be used for testing purposess only Here is a simple implementation of Release: IInterface@Release proc pif:DWORD GETOBJECTPOINTER Object, interface, pif ; decrement the reference count dec [(Object ptr [eax]).nRefCount] ; check to see if the reference count is zero. If it is, then destroy ; the object. mov eax, [(Object ptr [eax]).nRefCount] or eax, eax jnz @1 ; free the object: here we have assumed the object was allocated with ; LocalAlloc and with LMEM_FIXED option GETOBJECTPOINTER Object, interface, pif invoke LocalFree, eax @1: ret IInterface@Release endp Creating a COM object ------------------------------------------------------------------------------ Creating an object consisits basically of allocating the memory for the object, and then initializeing its data members. Typically, the vtable pointer is initialized and the reference count is zeroed. QueryInterface could then be called to get the interface pointer. Other methods exist for creating objects, such as using CoCreateInstance, and using class factories. These methods will not be discussed, and may be a topic for a future article. COM implementatiion sample application ------------------------------------------------------------------------------ Here follows a sample implementation and usage of a COM object. It shows how to create the object, call its methods, then free it. It would probably be very educational to assemble this and run it through a debugger. .386 .model flat,stdcall include windows.inc include kernel32.inc include user32.inc includelib kernel32.lib includelib user32.lib includelib uuid.lib ;----------------------------------------------------------------------------- ; Macro to simply interface declarations ; Borrowed from Ewald, http://here.is/diamond/ STDMETHOD MACRO name, argl :VARARG LOCAL @tmp_a LOCAL @tmp_b @tmp_a TYPEDEF PROTO argl @tmp_b TYPEDEF PTR @tmp_a name @tmp_b ? ENDM ; Macro that takes an interface pointer and returns the implementation ; pointer in eax GETOBJECTPOINTER MACRO Object, Interface, pif mov eax, pif IF (Object.Interface) sub eax, Object.Interface ENDIF ENDM ;----------------------------------------------------------------------------- IInterface@QueryInterface proto :DWORD, :DWORD, :DWORD IInterface@AddRef proto :DWORD IInterface@Release proto :DWORD IInterface@Get proto :DWORD IInterface@Set proto :DWORD, :DWORD CreateObject proto :DWORD IsEqualGUID proto :DWORD, :DWORD externdef IID_IUnknown:GUID ;----------------------------------------------------------------------------- ; declare the interface prototype IInterface struct lpVtbl dd ? IInterface ends IInterfaceVtbl struct ; IUnknown methods STDMETHOD QueryInterface, pif:DWORD, riid:DWORD, ppv:DWORD STDMETHOD AddRef, pif:DWORD STDMETHOD Release, pif:DWORD ; IInterface methods STDMETHOD GetValue, pif:DWORD STDMETHOD SetValue, pif:DWORD, val:DWORD IInterfaceVtbl ends ; declare the object structure Object struct ; interface object interface IInterface ; object data nRefCount dd ? nValue dd ? Object ends ;----------------------------------------------------------------------------- .data ; define the vtable @@IInterface segment dword vtblIInterface: dd offset IInterface@QueryInterface dd offset IInterface@AddRef dd offset IInterface@Release dd offset IInterface@GetValue dd offset IInterface@SetValue @@IInterface ends ; define the interface's IID ; {CF2504E0-4F89-11d3-9AC3-0000E82C0301} IID_IInterface GUID <0cf2504e0h, 04f89h, 011d3h, <09ah, 0c3h, 00h, 00h, 0e8h, 02ch, 03h, 01h>> ;----------------------------------------------------------------------------- .code start: StartProc proc LOCAL pif:DWORD ; interface pointer ; call the SetValue method mov eax, [pif] mov eax, [eax] invoke (IInterfaceVtbl ptr [eax]).SetValue, [pif], 12345h ; call the GetValue method mov eax, [pif] mov eax, [eax] invoke (IInterfaceVtbl ptr [eax]).GetValue, [pif] ; release the object mov eax, [pif] mov eax, [eax] invoke (IInterfaceVtbl ptr [eax]).Release, [pif] ret StartProc endp ;----------------------------------------------------------------------------- IInterface@QueryInterface proc uses ebx pif:DWORD, riid:DWORD, ppv:DWORD invoke IsEqualGUID, [riid], addr IID_IInterface test eax,eax jnz @F invoke IsEqualGUID, [riid], addr IID_IUnknown test eax,eax jnz @F jmp @Error @@: GETOBJECTPOINTER Object, interface, pif lea eax, (Object ptr [eax]).interface ; set *ppv mov ebx, [ppv] mov dword ptr [ebx], eax ; increment the reference count GETOBJECTPOINTER Object, interface, pif mov eax, (Object ptr [eax]).interface invoke (IInterfaceVtbl ptr [eax]).AddRef, [pif] ; return S_OK mov eax, S_OK jmp return @Error: ; error, interface not supported mov eax, [ppv] mov dword ptr [eax], 0 mov eax, E_NOINTERFACE return: ret IInterface@QueryInterface endp IInterface@AddRef proc pif:DWORD GETOBJECTPOINTER Object, interface, pif inc [(Object ptr [eax]).nRefCount] mov eax, [(Object ptr [eax]).nRefCount] ret IInterface@AddRef endp IInterface@Release proc pif:DWORD GETOBJECTPOINTER Object, interface, pif dec [(Object ptr [eax]).nRefCount] mov eax, [(Object ptr [eax]).nRefCount] or eax, eax jnz @1 ; free object mov eax, [pif] mov eax, [eax] invoke LocalFree, eax @1: ret IInterface@Release endp IInterface@GetValue proc pif:DWORD GETOBJECTPOINTER Object, interface, pif mov eax, (Object ptr [eax]).nValue ret IInterface@GetValue endp IInterface@SetValue proc uses ebx pif:DWORD, val:DWORD GETOBJECTPOINTER Object, interface, pif mov ebx, eax mov eax, [val] mov (Object ptr [ebx]).nValue, eax ret IInterface@SetValue endp ;----------------------------------------------------------------------------- CreateObject proc uses ebx ecx pobj:DWORD ; set *ppv to 0 mov eax, pobj mov dword ptr [eax], 0 ; allocate object invoke LocalAlloc, LMEM_FIXED, sizeof Object or eax, eax jnz @1 ; alloc failed, so return mov eax, E_OUTOFMEMORY jmp return @1: mov ebx, eax mov (Object ptr [ebx]).interface.lpVtbl, offset vtblIInterface mov (Object ptr [ebx]).nRefCount, 0 mov (Object ptr [ebx]).nValue, 0 ; Query the interface lea ecx, (Object ptr [ebx]).interface mov eax, (Object ptr [ebx]).interface.lpVtbl invoke (IInterfaceVtbl ptr [eax]).QueryInterface, ecx, addr IID_IInterface, [pobj] cmp eax, S_OK je return ; error in QueryInterface, so free memory push eax invoke LocalFree, ebx pop eax return: ret CreateObject endp ;----------------------------------------------------------------------------- IsEqualGUID proc rguid1:DWORD, rguid2:DWORD cld mov esi, [rguid1] mov edi, [rguid2] mov ecx, sizeof GUID / 4 repe cmpsd xor eax, eax or ecx, ecx setz eax ret IsEqualGUID endp end start Conclusion ------------------------------------------------------------------------------ We have (hopefully) seen how to implement a COM object. We can see that it is a bit messy to do, and adds quite some overhead to our programs. However, it can also add great flexibility and power to our programs. For more information on this subject, i have set up a small COM page on my site: http://lordlucifer.cjb.net Remember that COM defines only interfaces, and implementation is left to the programmer. This article presents only one possible implementation. This is not the only method, nor is it the best one. The reader should feel free to experiment with other methods.