COM & Assembly

A. Purpose

Some of the basic technics used in the COM samples for assembly (Masm syntax) are described here. It is not a must to understand these things to use COM with Masm or JWasm, but it might help. Please note that no introduction to COM is given here. There exist other places on the net for that purpose. The source samples which will demonstrate the things described here are:
  • SimpleServer: "Hello world" COM sample. Implements a dual interface with just one property. It is implemented as an in-proc server (=dll).
  • SimplestServer: Unlike SimpleServer this sample implements a custom, nondual interface, which works with VB, but not with WSH or VBScript.
  • ComExeSvr: This is mainly the same as SimpleServer, but implemented as an out-of-process server (=exe).
  • ComExeSvr2: This sample adds support for events.
These samples should demonstrate how "scriptable" COM components can be implemented in assembly. None of them provides a GUI interface, however. This requires quite some more COM interfaces to be implemented, which is beyond the scope of this introduction.
Some more sophisticated examples, all written in assembly (Masm style):
  • AsmCtrl, an example of a fully functional, embeddable OCX control, written without the help of libraries or complex macros.
  • AsmCtrlX, has quite the same functionality like AsmCtrl, but uses a lot of helper macros.
  • RegView, a COM object to extend Windows explorer. After installation it will allow to view the Windows registry inside explorer.
  • ExcelHost, an example of an application using automation to host COM objects.
  • IE Automation, demonstrates how to control Internet Explorer by COM Automation.
  • IE Webbrowser Container, demonstrates how the Internet Explorer ActiveX control may be used.

The include files which are refered to below are
  • OBJBASE.INC: this is an assembly version (Masm-style) of OBJBASE.H, which defines basic COM stuff. This file is part of WinInc.
  • DISPHLP.INC: this contains helper macros which are intended to make the IDispatch interface (almost) as easy to use with Masm/JWasm as it is with C(++).


B. Basic Macros in OBJBASE.INC


Macro BEGIN_INTERFACE starts definition of an interface. It has 2 parameters. The first is required and is the name of the interface, the second is optional and is the name of an interface to be inherited. An example is:

BEGIN_INTERFACE IShellFolder, IUnknown

This line will start declaration of interface IShellFolder, inheriting from IUnknown. The code generated by this macro will look like:

IShellFolderVtbl struct
	IUnknownVtbl <>



Macro STDMETHOD defines a method inside current interface definition. It has at least 1 parameter, which is the name of the method. Other parameters may follow, but do NOT declare THIS pointer as a parameter. It is automatically included.
Each STDMETHOD macro will result in two typedefs and one variable being included. First is a function prototype type definition, second is a pointer to this function type definition. The structure variable, which is defined at last, will get the type of the second type definition. So Masm/JWasm will be able to check number and type of parameters for each call of this method.
Example (from Interface IOleCommandTarget):


will be translated to the following 3 lines:

protoIOleCommandTarget_QueryStatus typedef proto :ptr, :ptr GUID, :DWORD, :ptr OLECMD, :ptr OLECMDTEXT
pIOleCommandTarget_QueryStatus typedef ptr protoIOleCommandTarget_QueryStatus
QueryStatus pIOleCommandTarget_QueryStatus ?



Macro END_INTERFACE has no parameters and finishes current interface definition. In fact, the vtable definition is terminated and the interface itself - which consists of a pointer to this vtable only - is defined. Example:

BEGIN_INTERFACE IShellFolder, IUnknown

will be translated to the following lines (the bold ones come from END_INTERFACE):

IShellFolderVtbl	struct
IShellFolderVtbl	ends

pIShellFolderVtbl	typedef ptr IShellFolderVtbl

IShellFolder struct
lpVtbl pIShellFolderVtbl ?
IShellFolder ends


4. Macro vf()

Macro vf() is used to call a method of an interface. It requires 3 parameters, which are:
  • interface pointer
  • name of interface
  • name of method to call
The macro is always used in conjunction with Masm's internal "invoke" macro, so for example:

invoke vf(pUnknown, IUnknown, QueryInterface), addr IID_IOleObject, addr m_pOleObject

The above line will in a first step be evaluated into:

mov edx, pUnknown
mov edx, [edx].IUnknown.pVtbl
invoke [edx].IUnknownVtbl.QueryInterface, pUnknown, addr IID_IOleObject, addr m_pOleObject

After invoke itself is evaluated, the code will look like:

mov edx, pUnknown
mov edx, [edx].IUnknown.pVtbl
lea eax, m_pOleObject
push eax
push offset IID_IOleObject
push pUnknown
call [edx].IUnknownVtbl.QueryInterface

where only the bold text comes from macro vf()! This implicates that register edx MUST NOT be used in any form as a parameter. Macro vf() is unable to detect usage of edx for any parameter behind the closing bracket, so no error message is displayed in this case.


C. IDispatch Support Macros in DISPHLP.INC

1. IDispatch Interface

If an interface is derived from IDispatch then there exists another possible way to call methods of this interface. That's by using method IDispatch::Invoke. Include File DISPHLP.INC - supplied with COMView - offers some support macros for calling methods this way. With these calling a method by IDispatch::Invoke is as simple as calling it by vtable with macro vf().



This macro describes a method being called with IDispatch::Invoke. In fact some text equates and a dummy procedure are defined.
Example (method "Close", interface "Window" with 3 variants as parameters and returning a BOOL):


will finally result in following equates being defined

Window_CloseDispId equ 0115h
Window_CloseType equ DISPATCH_METHOD
Window_CloseMD textequ <3, VT_VARIANT, VT_VARIANT, VT_VARIANT, 1, 2>
Window_CloseArgs textequ <th:LPDISPATCH, arg1:VARIANT, arg2:VARIANT, arg3:VARIANT, pRet:ptr BOOL>

and a dummy procedure

_TEXT$02	segment dword public 'CODE'
Window_Close proc pMD:ptr, pDisp:LPDISPATCH, arg1:VARIANT, arg2:VARIANT, arg3:VARIANT, pRet:ptr BOOL
Window_Close endp
_TEXT$02	ends

will be generated. The dummy procedure is necessary to make Masm/JWasm check parameters when calling this method. No code is generated, though, and "segment" _TEXT$02 MUST remain empty. This will result in proc InvokeHelper - located immediately "behind" in "segment" _TEXT$02$1 - being entered if proc Window_Close is being called.


3. Macro dm()

Macro dm() is used to call a method thru IDispatch::Invoke. It works only in conjunction with macro DEFINE_INVOKEHELPER (see below). So if you use dm() and haven't coded DEFINE_INVOKEHELPER, you will most likely get a linker error. Besides that macro dm() is used similar to macro vf(), with exactly the same parameters.
If using the example above dm() would look like:

invoke dm(pWindow, Window, Close), variant1, variant2, variant3

which is evaluated into (bold text from macro dm())

ifndef Window_CloseMDData
_TEXT$03	segment dword public 'CODE'
Window_CloseMDData label byte
	dd Window_CloseDispId
	dw Window_CloseMD
_TEXT$03	ends
extern DEFINE_INVOKEHELPER_is_missing:abs
invoke Window_Close, addr Window_CloseMDData, pWindow, variant1, variant2, variant3

As you may notice, there is - besides "this" pointer pWindow - another hidden parameter, the "metadata" pointer addr Window_CloseMDData. This "metadata" is defined for each method being used in code in section _TEXT$03, its value comes from equates defined by macro DEFINE_DISPMETHOD and will be used by proc InvokeHelper.
External DEFINE_INVOKEHELPER_is_missing ensures that macro DEFINE_INVOKEHELPER will exist in source code



Macro DEFINE_INVOKEHELPER defines proc InvokeHelper and public DEFINE_INVOKEHELPER_is_missing. This proc is not called directly. But in fact every call generated with macro dm() will hopefully result in a call of this procedure. That's because InvokeHelper will be located in section _TEXT$02$1, which the linker should place immediately behind section _TEXT$02, where dummy procs generated by macro DEFINE_DISPMETHOD are located

What will proc InvokeHelper do? Depending on first parameter, the "metadata" pointer, it will get the other parameters from stack, convert it to variants and put them in a dynamically build array of variants. After that it will call IDispatch::Invoke and, if a return parameter is expected, convert returned variant into appropriate return type and store it. Finally InvokeHelper clears the stack and returns to caller. For more details please take a look in include file DISPHLP.INC.


D. Client Event Support macros in DISPHLP.INC


This macro will define an event table. It requires one parameter, which should be the name of the event interface. A label xxx_EventTab will be defined, where xxx stands for the interface name.


This macro defines an entry in the current event table. One or two parameters are expected. First will be the name of a method for which event notifications should be received. Second - optional - parameter is local name of the event procedure. The macro can only occure between macros BEGIN_EVENTS and END_EVENTS.


This macro will finish definition of an event table. It defines a DWORD with value -1 to indicate the end of the table.
Finally a sample for an event table:

BEGIN_EVENTS DWebBrowserEvents2

will create the following code:

DWebBrowserEvents2_EventTab label dword
ifndef DWebBrowserEvents2_OnQuitMDData
_TEXT$03	segment dword public 'CODE'
DWebBrowserEvents2_OnQuitMDData label byte
    dd DWebBrowserEvents2_OnQuitDispId
    dw DWebBrowserEvents2_OnQuitMD
_TEXT$03	ends
    dd DWebBrowserEvents2_OnQuitMDData     ;metadata describing arguments expected
    dd _DWebBrowserEvents2_OnQuit          ;address of event proc
    dd -1


Macro DEFINE_EVENTHELPER defines an IDispatch interface which will act as event sink object. As well the following procedures are defined:
  • Create@CEvents :LPVOID
    just will create a CEvent object, the parameter supplied will be used as a Cookie and delivered as first parameter to the event procs defined. It doesn't need to be an IDispatch or even IUnknown object.
  • Connect@CEvents :ptr CEvents, pServer:LPUNKNOWN, pEventInterfaceIID:REFIID, pEventTab:LPVOID
    connects to a server object. Parameter pServer is server object to connect to, pEventInterfaceIID is interface IID of outgoing interface for which events are requested and pEvents is a pointer to an event table defined with macros BEGIN_EVENTS, DEFINE_DISPEVENT and END_EVENTS.
  • Disconnect@CEvents :ptr CEvents
    disconnects from server object.


E. Server Event Support macros in DISPHLP.INC

1. Macro FIREEVENT()

Macro FIREEVENT() requires 3 parameters and is used only in conjunction with macro invoke, so its syntax is similiar to macros vf() or dm(). As well, it requires macro DEFINE_FIREEVENTHELPER to be inserted somewhere in the code, since it calls procedure FireEvent.
Example: Let's assume an event dispinterface is defined as:
DEFINE_DISPMETHOD _EventInterface, OnClick, 01h, METHOD, , VT_I4, VT_I4
DEFINE_DISPMETHOD _EventInterface, OnClose, 02h, METHOD, ,

To fire event OnClick one have to code:

invoke FIREEVENT(pConnectionPoint, _EventInterface, OnClick), dwXPos, dwYPos

which will result in code:

ifndef _EventInterface_OnClickMDData
_TEXT$03	segment dword public 'CODE'
_EventInterface_OnClickMDData label byte
    dd _EventInterface_OnClickDispId
    dw _EventInterface_OnClickMD
_TEXT$03	ends
invoke FireEvent, addr _EventInterface_OnClickMDData, pConnectionPoint, dwXPos, dwYPos


Macro DEFINE_FIREEVENTHELPER defines public proc FireEvent, which will be called from inside macro FIREEVENT(). It moves all parameters to an array of variants, prepares a DISPPARAMS structure and will notify all connected sinks of the event. For this to work the server must support interface IEnumConnections.

F. Screen Shots

The following screenshots are done inside COMView, which is a powerful tool to view and handle COM objects.


1. SimpleServer

a. COMView displays the type library of the "SimpleServer" class. The "CanCreate" attribute is a hint that an object of this type could be created:

b. The type information for ISimpleServer interface reveals that it is derived from IDispatch and that there is just 1 additional property with name "Property1" (properties are implemented by a pair of methods to get/set their value) available:

c. COMView created a "SimpleServer" object. Note the presence of the IDispatch interface:

d. COMView showing the properties of the just created "SimpleServer" object:


2. SimplestServer

a. Now the same procedure done with the "SimplestServer" object. The type library already shows a difference, there is no "dual" interface implemented:

b. The type information for ISimplestServer is quite different with just 2 entries. The interface is directly derived from IUnknown, however, but the 3 IUnknown methods QueryInterface(), AddRef() and Release() are not shown here:

c. The "SimplestServer" object has been created. No IDispatch interface is implemented. This forces the hosting application to use the vtable mechanism to "talk" to the object:

d. Properties of "SimplestServer" object. COMView displays "vtable mode is on" on the status line, indicating that no IDispatch is available.


3. ComExeSvr2

a. The "ComExeSvr2" object is different from SimpleServer in 2 regards: it is an out-of-process server and it has an event interface implemented. The type library also shows that the event interface has its own type information (_ComExeSvr2Event) and that it is not marked as "dual":

b. The type information for _ComExeSvr2Event. There is no vtable for this interface, it is "dispatchonly", which is common practice for event interfaces. The interface contains just 1 method:

c. The "ComExeSvr2" object has been created. COMView displays event interfaces in the "outgoing interfaces" part of the dialog screen. Compared to "SimpleServer" there are additional interfaces in the first list. IMarshal, IClientSecurity and IMultiQI are created automatically by COM because this server runs as a separate process.