MyCom2 and the CoLib
Component Object Library
Copyright © Dec 28, 2000 by Ernest Murphy ernie@surfree.com
For educational use only. All commercial use only by written license.
Re-written Dec 28 2000 for the MASM32 package
Abstract: --------------------------------------------------------------------------------------------------------------------
A simple COM object is created through a library of reusable functions. The requirements for using this library are discussed.
As this is still a work in progress, there will be minimal explanation given for how the lib works. Release version 1.0. IDispatch is fully working. The requirements for using this library will be given.
--------------------------------------------------------------------------------------------------------------------
In an earlier article I discussed some code that created the fully functional, albeit very simple COM object MyCom. That may now be used as a guide as to what a server must do. The current project creates MyCom2, which while having an identical set of methods inherits from IDispatch and thus supports a dual interface for either early or late binding. One interesting aspect aspect of this addition is the file sizes. The dll grew from 7168 to 12,288 bytes, a total increase of only 5120 bytes for a significant addition of reusable functionality provided by standard library functions.
In writing this library, a simple thought was kept in mind: make it all things to all peoples. Here is a list of the goals of CoLib.lib:
By including CoLib.inc and CoLib.lib, not one line of code or byte of data shall be added to a project. Library functions are added by the linker.
Individual library functions may be excluded from use should you wish to customize particular functions without editing the library.
The 5 DLL exports shall be included and reusable.
Multiple objects may be incorporated in a single DLL.
Each object may contain multiple interfaces.
Aggregation is supported.
IDispatch is supported.
Localization for IDispatch is supported
Registrar scripting is supported using same syntax as the ATL library.
All type libraries included in the resource are automatically registered and unregistered.
However, CoLib.inc is a work in progress. The following is a list of known issues and "things yet to do."
The registry exports simply do not work under Windows 2000.
No method is included to allow a lib object to aggregate other objects. This is planned for a future addition.
Finally, the fun part of this is when we get to writing the server code itself. When originally defined the simple COM server MyCom the .asm contained some 632 lines and 23,539 bytes. MyCom2.asm is just 72 lines of assembly and a remarkable 30 lines total in the .code section. Obviously, much of the overhead has been shifted to the library. Let's start by examining this code in Part II
--------------------------------------------------------------------------------------------------------------------
Here is the complete asm source code for the MyCom2 object. This object is identical to MyCom, except it inherits from IDispatch and thus has a dual interface. Statements defined by the lib are highlighted in blue.
;-------------------------------------------------------------------------------
; MyCom2.dll copyright 7/15/00 Ernest J Murphy
;
; sample project to illustrate how to use CoLib, a Component Object Library
;
;-------------------------------------------------------------------------------
.NOLIST ; keep the list file small, don't add the std libs to it.
.386
.model flat, stdcall
option casemap:none ; case sensitive
;-------------------------------------------------------------------------------
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include \masm32\include\advapi32.inc
include \masm32\include\oleaut32.inc
include \masm32\include\masm32.inc
include \masm32\include\ole32.inc
include \masm32\com\include\oaidl.inc ; basic COM definitions
include \masm32\com\include\colib.inc ; the new library
include IMyCom2.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\advapi32.lib
includelib \masm32\lib\oleaut32.lib
includelib \masm32\lib\ole32.lib
includelib \masm32\lib\masm32.lib
includelib \masm32\com\colib\colib.lib ; the new library
PUBLIC ClassMap ; need to export ONLY the ClassMap
;-------------------------------------------------------------------------------
.LISTALL
.data
; describe the classes inside the DLL
ClassMap ClassItem { pCLSID_MyCom2, DISPINTERFACE + SUPPLY_TYPE_INFO, \
OFFSET MyCom2TypeLibInfo, OFFSET MyCom2IMap, \
CreateMyCom2, NULL, SIZEOF MyCom2Object }
END_CLASS_MAP
; describe the MyCom2 object's interfaces
MyCom2IMap InterfaceItem { pIID_IMyCom2, OFFSET vtableIMyCom2 }
END_INTERFACE_MAP
; describe the type libraries
MyCom2TypeLibInfo TypeLibInfo { pLIBID_MyCom2, 1, 0 }
; describe the MyCom2 object itself (takes 2 steps)
; step 1
MyCom2ObjData STRUCT ; MyCom2 object private data struct
m_Value DWORD 0 ; Value (private data member)
MyCom2ObjData ENDS
; step 2
MyCom2Object STRUCT
ObjectData0 ObjectData { } ; base values
MyCom2ObjData0 MyCom2ObjData { } ; custom object data
ObjectEntry0 ObjectEntry { } ; delegated Unknown
ObjectEntry1 ObjectEntry { } ; IMyCom2
MyCom2Object ENDS
; fill in the vtable
vtableIMyCom2 IMyCom2 { pvtIDispatch, GetValue, SetValue, RaiseValue }
; define the interface IID's
DeclareGuid IID_IMyCom2
DeclareGuid CLSID_MyCom2
DeclareGuid LIBID_MyCom2
;-------------------------------------------------------------------------------
.code
CreateMyCom2 PROC this_:DWORD
pObjectData this_, edx ; cast this_ to object data
xor eax, eax ; get variable
mov (MyCom2ObjData ptr [edx]).m_Value, eax ; store new value
ret ; return S_OK
CreateMyCom2 ENDP
;-------------------------------------------------------------------------------
GetValue PROC this_:DWORD, pval:DWORD ; GetValue for the IMyCom Interface
pObjectData this_, edx ; cast this_ to object data
mov eax, (MyCom2ObjData ptr [edx]).m_Value ; get object data value
mov ecx, pval ; get ptr to variable for return
mov [ecx], eax ; mov value to variable
xor eax, eax ; return S_OK
ret
GetValue ENDP
;-------------------------------------------------------------------------------
SetValue PROC this_:DWORD, val:DWORD ; SetValue for the IMyCom Interface
pObjectData this_, edx ; cast this_ to object data
mov eax, val ; get variable
mov (MyCom2ObjData ptr [edx]).m_Value, eax ; store new value
xor eax, eax ; return S_OK
ret
SetValue ENDP
;-------------------------------------------------------------------------------
RaiseValue PROC this_:DWORD, val:DWORD ; RaiseValue for the IMyCom Interface
pObjectData this_, edx ; cast this_ to object data
mov eax, val ; get variable
add (MyCom2ObjData ptr [edx]).m_Value, eax ; add new value to current value
xor eax, eax ; return S_OK
ret
RaiseValue ENDP
;-------------------------------------------------------------------------------
end DllMain
--------------------------------------------------------------------------------------------------------------------
Now let's explain what these new library functions do for us:
First, we need to include the lib itself:
include \masm32\colib\oaidl.inc
include \masm32\colib\colib.inc
includelib \masm32\colib\colib.lib ; new library
It is easier to create an include file to define your interface, as this gives you a running start towards creating the client program to use your new objects:
include IMyCom2.inc
Finally, we make PUBLIC the single ClassMap so the library modules can also use it:
PUBLIC ClassMap ; need to export ONLY the ClassMap
All the library code is included by the linker as it resolves all the symbols, that is all we need do to fully invoke the library functions.
Our class will be defined last (though it appears first) so we can define the class item member names.
When we create a COM object with this lib, it must be to a specific pattern as defined in the lib.
First, we need a place to put the custom data members for the object we create.
MyCom2ObjData STRUCT ; MyCom object private data struct Value DWORD 0 ; Value (private data member) MyCom2ObjData ENDS
This structure is inserted into an object definition. Objects MUST be defined to this pattern, with ClassData followed by the custom data, and finally an ObjectEntry list. This Each interface needs it's own ObjectEntry structure (this is needed to "cast" the "this_" value returned from QueryInterface)
MyCom2Object STRUCT ClassData0 ClassData { } ; base values MyCom2Data MyCom2ObjData { } ; custom object data ObjectEntry0 ObjectEntry { } ; delegated Unknown ObjectEntry1 ObjectEntry { } ; IMyCom2 MyCom2Object ENDS
The interface map defines how many interfaces are supported by an object, and links to other information the maps supply. Each interface gets it's own MapItem entry. The structure needs to be defined for each object since you may include any number of interfaces in a particular object.
MyCom2IMap InterfaceItem { pIID_IMyCom2, OFFSET vtableIMyCom2 } END_INTERFACE_MAP
END_INTERFACE_MAP is a macro to define a NULL interface to mark the end of the InterfaceMap.
Each object needs type library support. It needs a pointer to the library ID, and the major and minor versions of the type lib.
MyCom2TypeLibInfo TypeLibInfo { pLIBID_MyCom2, 1, 0 }
Now we can define the ClassMap. Each object gets a separate ClassItem structure.
ClassMap ClassItem { pCLSID_MyCom2, DISPINTERFACE + SUPPLY_TYPE_INFO, \ OFFSET MyCom2TypeLibInfo, OFFSET MyCom2IMap, \ CreateMyCom2, NULL, SIZEOF MyCom2Object }
END_CLASS_MAP
Similarly to the InterfaceMap, END_CLASS_MAP is a macro to define a NULL interface to mark the end of the ClassMap
The parameters of a ClassItem are defined as follows:
ClassItem1.m_refiid DWORD ; CLSID for the object ClassItem1.m_Flags DWORD ; flags (see below) ClassItem1.m_pTypeLibInfo DWORD ; ptr to the TypeLibInfo struct ClassItem1.m_pIMap DWORD ; ptr to the MC2InterfaceMap ClassItem1.m_pConstructor DWORD ; ptr to the object's constructor function ClassItem1.m_pDestructor DWORD ; ptr to the object's destructor function ClassItem1.m_ObjectSize DWORD ; SIZEOF the object
m_Flags is the sum of the following:
AGGREGATABLE Allows aggregation by other objectsDISPINTERFACE Implements IDispatch through a dual interface SUPPORT_ERROR_INFO Supports rich error information for vtable clients SUPPLY_TYPE_INFO Type information may be hid with this flag
We create the MyCom2 vtable. Note the use of pvtIDispatch, which is a lib macro to add the addresses of the IUnknown and IDispatch implementations.
vtableIMyCom2 IMyCom2 { pvtIDispatch, GetValue, SetValue, RaiseValue}
We instance the object's 3 required GUIDs here. We also make pointers to these to simplify referencing them (these equated are used above) by using the DeclareGuid macro
DeclareGuid IID_IMyCom2 DeclareGuid CLSID_MyCom2 DeclareGuid LIBID_MyCom2
Similarly to C++, our objects may need custom Constructor and Destructor functions for whatever resources they need. The MyCom constructor simply needs to set the data value to zero.
;------------------------------------------------------------------------------ .code CreateMyCom2 PROC this_:DWORD pObjectData this_, edx ; cast this_ to object data xor eax, eax ; get variable mov (MyCom2ObjData ptr [edx]).m_Value, eax ; store new value
xor eax, eax ; return S_OK ret CreateMyCom2 ENDP
;------------------------------------------------------------------------------
Only the GetValue method is shown for example, as each work in a similar manor. The pObjectData macro is provided from the lib to convert (cast) this_ into a pointer to the object data. The pointer is placed in the register specified to the macro (here it is placed in ecx)
;------------------------------------------------------------------------------ GetValue PROC this_:DWORD, pval:DWORD ; GetValue for the IMyCom Interface pObjectData this_, edx ; cast this_ to object data mov eax, (MyCom2ObjData ptr [edx]).m_Value ; get object data value mov ecx, pval ; get ptr to var for return mov [ecx], eax ; mov value to variable xor eax, eax ; return S_OK ret GetValue ENDP
;------------------------------------------------------------------------------
That code looks so simple it may be magic. Not really... just the special COM parts of the DLL are stuffed into the lib.
Now's the time to look at the other files required to build our DLL. These are:
IMyCom2.inc The IMyCom interface definition in asm form
rsrc.rc standard MASM32 resource file
MyCom2.IDL Interface Definition File. This is compiled into MyCom.tlb
MyCom2.def defines the exported functions for the Dynamic Link Library
As before, statements defined by the CoLib library are highlighted in blue.
--------------------------------------------------------------------------------------------------------------------
IMyCom2.inc, the interface definition
; IMyCom2 Interface
;--------------------------------------------------------------------------
;
sIID_IMyCom2 TEXTEQU <{0F8CE5E41H, 01135H, 011D4H, \
{0A3H, 024H, 000H, 040H, 0F6H, 0D4H, 087H, 0D9H}}>
sLIBID_MyCom2 TEXTEQU <{0F8CE5E42H, 01135H, 011D4H, \
{0A3H, 024H, 000H, 040H, 0F6H, 0D4H, 087H, 0D9H}}>
sCLSID_MyCom2 TEXTEQU <{0F8CE5E43H, 01135H, 011D4H, \
{0A3H, 024H, 000H, 040H, 0F6H, 0D4H, 087H, 0D9H}}>
_vtIMyCom2 MACRO CastName:REQ
; IDispatch methods
_vtIDispatch CastName
; IMyCom2 methods
&CastName&_GetValue comethod2 ?
&CastName&_SetValue comethod2 ?
&CastName&_RaiseValue comethod2 ?
ENDM
IMyCom2 STRUCT
_vtIMyCom2 IMyCom2
IMyCom2 ENDS
The interface is defined as per the previous discussions in Creating COM objects in ASM (the orgional MyCom project). It is kept separate from the DLL file so it may be included in other ASM projects.
--------------------------------------------------------------------------------------------------------------------
rsrc.rc, the resource file
// get standard resources
#include <c:\masm32\include\colib\coserver.rc>
// type libraries here,
IDD_TypeLib TypeLib MyCom2.tlb
// add subsequent type libraries as such:
// IDD_TypeLib + 1 TypeLib MyCom3.tlb
// IDD_TypeLib + 2 TypeLib MyCom4.tlb
// the one and only registrar script
IDD_RGS_SCRIPT REGISTRY C:\masm32\MyCom2\MyCom2.rgs
// add additional resources here starting at ID number 300
This should be simple to duplicate. All you are specifying is the type library file and the resistrar script file. Should you have multiple objects, just keep adding the type libraries as the comment suggests. CoLib will register every type lib in the list that appear at the IDD_TypeLib resource number and on. There can only be one registrar file, so if you have multiple objects, you must hand edit these scripts into a single script.
MyCom.IDL Interface Definition File
--------------------------------------------------------------------------------------------------------------------
MyCom2.idl, the interface definition file
I will not pretend that I understand Interface Definition Language or syntax. All I can state is the following code works. I have Jeremy of Collake Software (http://www.collakesoftware.com/main.stm) to thank for the outline of this file (cause I stole mine from him).
// MyCom2.idl : IDL source for MyCom2.ocx (or dll)
//
import "oaidl.idl";
import "ocidl.idl";
[
uuid(F8CE5E42-1135-11d4-A324-0040F6D487D9),
helpstring("MyCom2 1.0 Type Library"),
version(1.0)
]
library MyCom2Lib
{
importlib("stdole32.tlb");
[
uuid(F8CE5E41-1135-11d4-A324-0040F6D487D9),
dual,
helpstring("IMyCom2 Dispatch Interface")
]
interface IMyCom2 : IDispatch
{
[propget, id(0), helpstring("property Value")] HRESULT Value([out, retval] long *pVal);
[propput, id(0), helpstring("property Value")] HRESULT Value([in] long newVal);
[id(1), helpstring("method Raise")] HRESULT Raise([in] long AddVal);
}
[
uuid(F8CE5E43-1135-11d4-A324-0040F6D487D9),
helpstring("MyCom2 Class")
]
coclass MyCom2
{
[default] interface IMyCom2;
}
};
There isn't much to say here, basically you *MAY* use MSVC to write this file for you as described in Creating COM objects in ASM. However, do check that IDispatch is working correctly. The CoLib lib uses the type library here to perform the IDispatch functions, and a bad type lib means no IDispatch.
--------------------------------------------------------------------------------------------------------------------
MyCom.rgs Resource Script File
If you use MSVC to define your .idl file, you also get the MyCom2.rgs registrar file. The registrar file looks like so:
HKCR
{
MyCom.MyCom2.1 = s 'MyCom2 Class'
{
CLSID = s '{F8CE5E43-1135-11d4-A324-0040F6D487D9}'
}
MyCom.MyCom2 = s 'MyCom2 Class'
{
CLSID = s '{F8CE5E43-1135-11d4-A324-0040F6D487D9}'
CurVer = s 'MyCom.MyCom2.1'
}
NoRemove CLSID
{
ForceRemove {F8CE5E43-1135-11d4-A324-0040F6D487D9} = s 'MyCom2 Class'
{
ProgID = s 'MyCom.MyCom2.1'
VersionIndependentProgID = s 'MyCom.MyCom2'
ForceRemove 'Programmable'
InprocServer32 = s '%MODULE%'
{
val ThreadingModel = s 'Apartment'
}
'TypeLib' = s '{F8CE5E42-1135-11d4-A324-0040F6D487D9}'
}
}
}
This file follows the registrar script format as defined in MSDN. Keep in mind capitalization is important here.
--------------------------------------------------------------------------------------------------------------------
MyCom2.def exported dll functions
; MyCom2.def : Declares the module parameters.
LIBRARY "MyCom2.dll"
EXPORTS DllCanUnloadNow @1 PRIVATE DllGetClassObject @2 PRIVATE DllRegisterServer @3 PRIVATE DllUnregisterServer @4 PRIVATE
This file is mostly boilerplate. All you need define is the LIBRARY name, which is optional as you really do not need the .lib file for a COM dll. The export names are always the same. You do need to name the file itself so a standard .dll compile and link bat file may find it.
CONCLUSION
--------------------------------------------------------------------------------------------------------------------