More than ten years ago (how time flies!), when I published the basic sample of a COM+ server and client, I thought that I wouldn’t be touching this subject again. But here we are, in 2022, and I have so much interaction with COM at work that I decided to write a new, updated, and a bit more detailed post about this technology 😁 I don’t want to convince you to use COM as the backbone for your new applications. Instead, I want to show you how you may approach and use COM APIs if you need to work with them. We will also do some COM debugging in WinDbg. Additionally, I plan to release a new COM troubleshooting tool as part of the wtrace toolkit. Remember to subscribe to wtrace updates if you’re interested.
Today’s post will continue using the old Protoss COM classes, but we will update the code with various modern ideas. As you may remember, Nexus
and Probe
classes represent Blizzard’s Starcraft game objects. Nexus is a building that may produce Probes (CreateUnit
method in the INexus
interface), and Probe may build various structures, including Nexuses (ConstructBuilding
method in the IProbe
interface). I also added a new IGameObject
interface, shared by Nexus
and Probe
, that returns the cost in minerals and the time needed to build a given game object. In IDL, it looks as follows:
[object, uuid(59644217-3e52-4202-ba49-f473590cc61a)]
interface IGameObject : IUnknown
{
[propget]
HRESULT Name([out, retval] BSTR* name);
[propget]
HRESULT Minerals([out, retval]LONG* minerals);
[propget]
HRESULT BuildTime([out, retval]LONG* buildtime);
}
I also added a type library to the IDL:
[
uuid(0332a9ab-e3bb-4042-bc6a-b98aebd6532d),
version(1.0),
helpstring("Protoss 1.0 Type Library")
]
library ProtossLib
{
importlib("stdole2.tlb");
interface INexus;
interface IProbe;
[
uuid(F5353C58-CFD9-4204-8D92-D274C7578B53),
helpstring("Nexus Class")
]
coclass Nexus {
[default] interface INexus;
interface IGameObject;
}
[
uuid(EFF8970E-C50F-45E0-9284-291CE5A6F771),
helpstring("Probe Class")
]
coclass Probe {
[default] interface IProbe;
interface IGameObject;
}
}
If we run midl.exe after this change, it will generate a type library file (protoss.tlb). The type library provides a language-agnostic way to access COM metadata. For example, we may import it to a .NET assembly using the tlbimp.exe tool from .NET Framework SDK.
Updating the Protoss COM server
As you remember, the COM server requires a few DLL exports to make its COM classes instantiable. One of them is DllGetClassObject
. The DllGetClassObject
function from the old post directly constructed the Nexus
and Probe
objects. The more common approach is to return an IClassFactory
instance for each implemented class and let the clients call its CreateInstance
method. The clients often do this implicitly by calling the CoCreateInstance
or CoCreateInstanceEx
functions. These functions first ask for a class factory object and later use it to create a requested class instance. Supporting IClassFactory
is straightforward:
STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, LPVOID* ppv) {
if (rclsid == __uuidof(Nexus)) {
static ProtossObjectClassFactory<Nexus, INexus> factory{};
return factory.QueryInterface(riid, ppv);
}
if (rclsid == __uuidof(Probe)) {
static ProtossObjectClassFactory<Probe, IProbe> factory{};
return factory.QueryInterface(riid, ppv);
}
return CLASS_E_CLASSNOTAVAILABLE;
}
The ProtossObjectClassFactory
is a class template implementing the IClassFactory
interface. I want to bring your attention to the CreateInstance
method:
HRESULT __stdcall CreateInstance(IUnknown* pUnkOuter, REFIID riid, void** ppv) override {
if (pUnkOuter) {
return CLASS_E_NOAGGREGATION;
}
try {
wil::com_ptr_t<IUnknown> unknown{};
// attach does not call AddRef (we set ref_count to 1 in COM Objects)
unknown.attach(static_cast<IT*>(new T()));
return unknown->QueryInterface(riid, ppv);
} catch (const std::bad_alloc&) {
return E_OUTOFMEMORY;
}
return S_OK;
}
It uses the wil::com_ptr_t
class. It’s one of the many smart pointers provided by Windows Implementation Library. Thanks to wil::com_ptr_t
or wil::unique_handle
, we no longer need to call Release
or CloseHandle
methods explicitly – they are called automatically in the smart pointer destructors. Thus, we free the resources when the pointers go out of scope. WIL and modern C++ really make using RAII with Windows API straightforward 😁.
One missing piece in the old code was registration. I used reg files to register the Protoss COM library in the system. It’s not the best way to do so, and, instead, we should implement DllRegisterServer
and DllUnregisterServer
functions so that the clients may register and unregister our library with the regsvr32.exe tool. The code presented below is based on the sample from the great Windows 10 System Programming book by Pavel Yosifovich. Only in my version, I used WIL, and you may quickly see its usage benefits when you look at the original version (for example, no calls to CloseHandle
and no error checks thanks to WIL result macros):
std::array<std::tuple<std::wstring_view, std::wstring, std::wstring>, 2> coclasses{
std::tuple<std::wstring_view, std::wstring, std::wstring> { L"Protoss Nexus", wstring_from_guid(__uuidof(Nexus)), L"Protoss.Nexus.1" },
std::tuple<std::wstring_view, std::wstring, std::wstring> { L"Protoss Probe", wstring_from_guid(__uuidof(Probe)), L"Protoss.Probe.1" },
};
STDAPI DllRegisterServer() {
auto create_reg_subkey_with_value = [](HANDLE transaction, HKEY regkey, std::wstring_view subkey_name, std::wstring_view subkey_value) {
wil::unique_hkey subkey{};
RETURN_IF_WIN32_ERROR(::RegCreateKeyTransacted(regkey, subkey_name.data(), 0, nullptr, REG_OPTION_NON_VOLATILE,
KEY_WRITE, nullptr, subkey.put(), nullptr, transaction, nullptr));
RETURN_IF_WIN32_ERROR(::RegSetValueEx(subkey.get(), nullptr, 0, REG_SZ,
reinterpret_cast<const BYTE*>(subkey_value.data()), static_cast<DWORD>((subkey_value.size() + 1) * sizeof(wchar_t))));
return S_OK;
};
wil::unique_handle transaction{ ::CreateTransaction(nullptr, nullptr, TRANSACTION_DO_NOT_PROMOTE, 0, 0, INFINITE, nullptr) };
RETURN_LAST_ERROR_IF(!transaction.is_valid());
for (const auto& coclass : coclasses) {
auto name{ std::get<0>(coclass) };
auto clsid{ std::get<1>(coclass) };
auto progId{ std::get<2>(coclass) };
wil::unique_hkey regkey{};
// CLSID
RETURN_IF_WIN32_ERROR(::RegCreateKeyTransacted(HKEY_CLASSES_ROOT, (L"CLSID\\" + clsid).c_str(),
0, nullptr, REG_OPTION_NON_VOLATILE, KEY_WRITE, nullptr, regkey.put(), nullptr, transaction.get(), nullptr));
RETURN_IF_WIN32_ERROR(::RegSetValueEx(regkey.get(), L"", 0, REG_SZ,
reinterpret_cast<const BYTE*>(name.data()), static_cast<DWORD>((name.size() + 1) * sizeof(wchar_t))));
RETURN_IF_FAILED(create_reg_subkey_with_value(transaction.get(), regkey.get(), L"InprocServer32", dll_path));
RETURN_IF_FAILED(create_reg_subkey_with_value(transaction.get(), regkey.get(), L"ProgID", dll_path));
// ProgID
RETURN_IF_WIN32_ERROR(::RegCreateKeyTransacted(HKEY_CLASSES_ROOT, progId.c_str(),
0, nullptr, REG_OPTION_NON_VOLATILE, KEY_WRITE, nullptr, regkey.put(), nullptr, transaction.get(), nullptr));
RETURN_IF_WIN32_ERROR(::RegSetValueEx(regkey.get(), L"", 0, REG_SZ,
reinterpret_cast<const BYTE*>(name.data()), static_cast<DWORD>((name.size() + 1) * sizeof(wchar_t))));
RETURN_IF_FAILED(create_reg_subkey_with_value(transaction.get(), regkey.get(), L"CLSID", clsid));
}
RETURN_IF_WIN32_BOOL_FALSE(::CommitTransaction(transaction.get()));
return S_OK;
}
As you maybe noticed, I also added the registration of ProgIDs (Protoss.Nexus.1 and Protoss.Probe.1), which are human-friendly names for our COM classes. With these functions implemented, registering our COM classes is now a matter of calling regsvr32.exe protoss.dll from the administrator’s command line.
Updating the Protoss COM client
Thanks to the type library, we no longer need to explicitly generate and include the header files, but we may import the type library directly into the source code. The #import
directive that we use for this purpose has several attributes controlling the representation of the type library in C++. For example, in the Protoss COM client, I’m using the raw_interfaces_only
attribute as I want to work with the Protoss interfaces directly using the WIL com_ptr_t
smart pointers. Our COM server uses IClassFactory
, so we may call the CoCreateInstance
function to create an instance of the Nexus
class:
#include <iostream>
#include <Windows.h>
#include <wil/com.h>
#import "..\protoss.tlb" raw_interfaces_only
using namespace ProtossLib;
HRESULT show_game_unit_data(IUnknown* unknwn) {
wil::com_ptr_t<IGameObject> unit{};
RETURN_IF_FAILED(unknwn->QueryInterface(unit.put()));
wil::unique_bstr name{};
RETURN_IF_FAILED(unit->get_Name(name.put()));
LONG minerals;
RETURN_IF_FAILED(unit->get_Minerals(&minerals));
LONG buildtime;
RETURN_IF_FAILED(unit->get_BuildTime(&buildtime));
std::wcout << L"Name: " << name.get() << L", minerals: " << minerals
<< L", build time: " << buildtime << std::endl;
return S_OK;
}
void start_from_probe() {
wil::com_ptr_t<IProbe> probe{};
THROW_IF_FAILED(::CoCreateInstance(__uuidof(Probe), nullptr, CLSCTX_INPROC_SERVER, __uuidof(IProbe), probe.put_void()));
THROW_IF_FAILED(show_game_unit_data(probe.get()));
auto name{ wil::make_bstr(L"Nexus") };
wil::com_ptr_t<INexus> nexus{};
THROW_IF_FAILED(probe->ConstructBuilding(name.get(), nexus.put_unknown()));
THROW_IF_FAILED(show_game_unit_data(nexus.get()));
}
int main(int argc, char* argv[]) {
UNREFERENCED_PARAMETER(argc);
UNREFERENCED_PARAMETER(argv);
try {
// a "smart call object" that will execute CoUnitialize in destructor
auto runtime{ wil::CoInitializeEx(COINIT_APARTMENTTHREADED) };
start_from_probe();
return 0;
} catch (const wil::ResultException& ex) {
std::cout << ex.what() << std::endl;
return 1;
} catch (const std::exception& ex) {
std::cout << ex.what() << std::endl;
return 1;
}
}
If you run the client, you should see the calls to the QueryInterface
method and logs from constructors and destructors in the console:
Component: Nexus::QueryInterface: 246a22d5-cf02-44b2-bf09-aab95a34e0cf Component: Probe::AddRef() ref_count = 2 Component: Probe::Release() ref_count = 1 Component: Probe::AddRef() ref_count = 2 Component: Probe::Release() ref_count = 1 Component: Nexus::QueryInterface: 246a22d5-cf02-44b2-bf09-aab95a34e0cf Component: Probe::AddRef() ref_count = 2 Component: Probe::Release() ref_count = 1 Component: Nexus::QueryInterface: 59644217-3e52-4202-ba49-f473590cc61a Component: Probe::AddRef() ref_count = 2 Name: Probe, minerals: 50, build time: 12 Component: Probe::Release() ref_count = 1 Component: Nexus::QueryInterface: 59644217-3e52-4202-ba49-f473590cc61a Component: Nexus::AddRef() ref_count = 2 Name: Nexus, minerals: 400, build time: 120 Component: Nexus::Release() ref_count = 1 Component: Nexus::Release() ref_count = 0 Component: Nexus::~Nexus() Component: Probe::Release() ref_count = 0 Component: Probe::~Probe()
We can see that all class instances are eventually freed, so, hurray 🎉, we aren’t leaking any memory!
If you’d like to practice writing COM client code, you may implement a start_from_nexus
function to output the same information, but create the Nexus
class first. Don’t look at the client code in the repository, as this function is already there 😊
C++ is not the only language to write a COM client. Let’s now implement the same logic in C#. I picked C# not without reason. .NET Runtime provides excellent support for working with native COM objects. Each COM class receives a Runtime Callable Wrapper that makes the COM class look like any other .NET class. Now, you can imagine the number of magic layers to make it happen. So, there is no surprise that sometimes, you may need to wear a magical debugging hat to resolve a problem in COM interop 😅 But if you look at the code, it’s effortless:
using ProtossLib;
public static class Program
{
static void ShowGameUnitData(IGameObject go)
{
Console.WriteLine($"Name: {go.Name}, minerals: {go.Minerals}, build time: {go.BuildTime}");
}
static void StartingFromProbe()
{
var probe = new Probe();
ShowGameUnitData((IGameObject)probe);
var nexus = probe.ConstructBuilding("Nexus");
ShowGameUnitData((IGameObject)nexus);
//_ = Marshal.ReleaseComObject(nexus);
//_ = Marshal.ReleaseComObject(probe);
}
[STAThread]
static void Main()
{
StartingFromProbe();
// force release of the COM objects
GC.Collect();
}
}
If you decompile the ProtossLib.dll assembly, you will discover that Probe
is, in fact, an interface with a CoClass attribute. And, although it does not implement IGameObject
, we may cast it to IGameObject
. Magical, isn’t it? 😊 Mixed-mode debugging helps a lot when debugging COM interop in .NET. For example, if you set a breakpoint on the QueryInterface
method in the Probe
class, you will discover that it i called when you cast the managed Probe
instance to IGameObject
.
Debugging COM in WinDbg
In this paragraph, I want to focus on debugging COM servers and clients in WinDbg. I will show you some commands, hoping they will be helpful also in your COM troubleshooting.
Let’s start with a breakpoint on the typical entry point for creating COM objects, i.e., the CoCreateInstance
function (if the COM client does not use CoCreateInstance
, you may set a breakpoint on the CoGetClassObject
function):
HRESULT CoCreateInstance(
[in] REFCLSID rclsid,
[in] LPUNKNOWN pUnkOuter,
[in] DWORD dwClsContext,
[in] REFIID riid,
[out] LPVOID *ppv
);
Our goal is to print the function parameters (CLSID, IID, and the object address), so we know which object the client creates. If we have combase.dll private symbols, it’s a matter of calling the dv command. Otherwise, we need to rely on the dt command. For 32-bit, I usually create the CoCreateInstance
breakpoint as follows:
bp combase!CoCreateInstance "dps @esp L8; dt ntdll!_GUID poi(@esp + 4); dt ntdll!_GUID poi(@esp + 10); .printf /D \"==> obj addr: %p\", poi(@esp+14);.echo; bp /1 @$ra; g"
And the 64-bit version is:
bp combase!CoCreateInstance "dps @rsp L8; dt ntdll!_GUID @rcx; dt ntdll!_GUID @r9; .printf /D \"==> obj addr: %p\", poi(@rsp+28);.echo; bp /1 @$ra; g"
I’m using bp /1 @$ra; g
to break at the moment when the function returns. I didn’t want to use, for example, gu
because one CoCreateInstance
may call another CoCreateInstance
, and one-time breakpoints are more reliable in such situations. An example 32-bit breakpoint hit might look as follows (notice that when we have private symbols, dps
command nicely prints the GUIDs):
009cfe00 008c36ae ProtossComClient!main+0x6e 009cfe04 008c750c ProtossComClient!_GUID_eff8970e_c50f_45e0_9284_291ce5a6f771 009cfe08 00000000 009cfe0c 00000001 009cfe10 008c74b4 ProtossComClient!_GUID_246a22d5_cf02_44b2_bf09_aab95a34e0cf 009cfe14 009cfe3c 009cfe18 36e9dfe6 009cfe1c 00e8b3e0 {eff8970e-c50f-45e0-9284-291ce5a6f771} +0x000 Data1 : 0xeff8970e +0x004 Data2 : 0xc50f +0x006 Data3 : 0x45e0 +0x008 Data4 : [8] "???" {246a22d5-cf02-44b2-bf09-aab95a34e0cf} +0x000 Data1 : 0x246a22d5 +0x004 Data2 : 0xcf02 +0x006 Data3 : 0x44b2 +0x008 Data4 : [8] "???" ==> obj addr: 009cfe3c ModLoad: 76fb0000 7702e000 C:\Windows\System32\clbcatq.dll ModLoad: 618b0000 618b9000 C:\Windows\SYSTEM32\ktmw32.dll ModLoad: 76df0000 76e66000 C:\Windows\System32\sechost.dll ModLoad: 75c40000 75cbb000 C:\Windows\System32\ADVAPI32.dll ModLoad: 031a0000 031ae000 C:\Users\me\repos\protoss-com-example\Release\protoss.dll Breakpoint 1 hit eax=00000000 ebx=00628000 ecx=00e84ea0 edx=00000000 esi=00e84310 edi=00e8b3e0 eip=008c36ae esp=009cfe18 ebp=009cfe58 iopl=0 nv up ei pl nz na pe nc cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000206 ProtossComClient!start_from_probe+0x23 [inlined in ProtossComClient!main+0x6e]: 008c36ae 8b4d04 mov ecx,dword ptr [ebp+4] ss:002b:009cfe5c=008c56b1
In the output, we can find CLSID (eff8970e-c50f-45e0-9284-291ce5a6f771
), IID (246a22d5-cf02-44b2-bf09-aab95a34e0cf
) and the created object address: 010ff620
. Before we start examining it, we need to check the returned status code. We can do that with the !error @$retreg
command (or look at the eax
/rax
register). If it’s 0
(S_OK
), we may set breakpoints on the returned object methods. As each COM object implements at least one interface (virtual class), it will have at least one virtual method table. Thanks to the CoCreateInstance
breakpoint, we know the queried IID, and we may find the interface method list in the associated type library. If we don’t have access to the type library (or our IID is IID_IUnknown
), we still may learn something about this object by placing breakpoints on the IUnknown
interface methods (as you remember, all COM interfaces need to implement it):
struct IUnknown
{
virtual HRESULT STDMETHODCALLTYPE QueryInterface(
/* [in] */ REFIID riid,
/* [iid_is][out] */ _COM_Outptr_ void __RPC_FAR *__RPC_FAR *ppvObject) = 0;
virtual ULONG STDMETHODCALLTYPE AddRef( void) = 0;
virtual ULONG STDMETHODCALLTYPE Release( void) = 0;
};
The breakpoint is very similar to what we did for CoCreateInstace
. The code snippet below presents the 32- and 64-bit versions:
bp 031a6160 "dt ntdll!_GUID poi(@esp + 8); .printf /D \"==> obj addr: %p\", poi(@esp + C);.echo; bp /1 @$ra; g" bp 00007ffe`1c751e6a "dt ntdll!_GUID @rdx; .printf /D \"==> obj addr: %p\", @r8;.echo; bp /1 @$ra; g"
Let me show you how I got the address of the QueryInterface
function for the 32-bit breakpoint (031a6160
). The first four bytes at the object address (009cfe3c
) point to the virtual method table. We may find the vtable address by calling dpp 009cfe3c L1
:
0:000> dpp 009cfe3c L1 009cfe3c 00e84ea0 031a860c protoss!Probe::`vftable'
We can now dump the content of the vtable:
0:000> dps 031a860c L4 031a860c 031a6160 protoss!Probe::QueryInterface 031a8610 031a6070 protoss!Probe::AddRef 031a8614 031a60b0 protoss!Probe::Release 031a8618 031a6260 protoss!Probe::ConstructBuilding
I knew that the IProbe
interface (246A22D5-CF02-44B2-BF09-AAB95A34E0CF
) has four methods (the first three coming from the IUnknown
interface). Without this knowledge, I would have printed only the first three methods (QueryInterface
, AddRef
, and Release
).
On each QueryInterface
return, we may again examine the status code and returned object. The output below presents a QueryInterface
hit for an IProbe
instance. Let’s spend a moment analyzing it:
{59644217-3e52-4202-ba49-f473590cc61a} +0x000 Data1 : 0x59644217 +0x004 Data2 : 0x3e52 +0x006 Data3 : 0x4202 +0x008 Data4 : [8] "???" ==> obj addr: 009cfe00 Breakpoint 2 hit eax=00000000 ebx=00628000 ecx=5a444978 edx=00000000 esi=00e84310 edi=00e8b3e0 eip=008c34f6 esp=009cfdec ebp=009cfe10 iopl=0 nv up ei pl zr na pe nc cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000246 ProtossComClient!show_game_unit_data+0x46: 008c34f6 8bf0 mov esi,eax
The 59644217-3e52-4202-ba49-f473590cc61a
GUID represents the IGameObject
interface. If you scroll up to the class definitions, you will find that it’s the second interface that the Probe class implements. The vtable at the object address looks as follows:
0:000> dpp 009cfe00 L1 009cfe00 00e84ea4 031a8620 protoss!Probe::`vftable' 0:000> dps 031a8620 L6 031a8620 031a5c40 protoss![thunk]:Probe::QueryInterface`adjustor{4}' 031a8624 031a5c72 protoss![thunk]:Probe::AddRef`adjustor{4}' 031a8628 031a5c4a protoss![thunk]:Probe::Release`adjustor{4}' 031a862c 031a36f0 protoss!Probe::get_Name 031a8630 031a3720 protoss!Probe::get_Minerals 031a8634 031a3740 protoss!Probe::get_BuildTime
You may now be wondering what the adjustor
methods are? If we decompile any of them, we will find an interesting assembly code:
0:000> u 031a5c40 protoss![thunk]:Probe::QueryInterface`adjustor{4}': 031a5c40 836c240404 sub dword ptr [esp+4],4 031a5c45 e916050000 jmp protoss!Probe::QueryInterface (031a6160)
To better understand what’s going on here, let’s put the last dpp
commands (after CoCreateInstance
and QueryInterface
) next to each other:
0:000> dpp 009cfe3c L1 009cfe3c 00e84ea0 031a860c protoss!Probe::`vftable' <- CoCreateInstance 0:000> dpp 009cfe00 L1 009cfe00 00e84ea4 031a8620 protoss!Probe::`vftable' <- QueryInterface
In the above output, we see that QueryInterface
for IProbe
(called by CoCreateInstance
) sets the object pointer to the address 00e84ea0
. While QueryInterface
for IGameObject
sets the object pointer to the address 00e84ea4
(four bytes further). And both calls were made on the same instance of the Probe
class. Looking at the QueryInterface
source code, we can see that this difference is caused by a static_cast
:
HRESULT __stdcall Probe::QueryInterface(REFIID riid, void** ppv) {
std::cout << "Component: Nexus::QueryInterface: " << riid << std::endl;
if (riid == IID_IUnknown || riid == __uuidof(IProbe)) {
*ppv = static_cast<IProbe*>(this);
} else if (riid == __uuidof(IGameObject)) {
*ppv = static_cast<IGameObject*>(this);
} else {
*ppv = NULL;
return E_NOINTERFACE;
}
AddRef();
return S_OK;
}
The instruction *ppv = static_cast<IProbe*>(this)
is here equivalent to *ppv = this
, as IProbe
is the default (first) interface of the Probe
class, and a pointer to its vtable occupies the first four bytes of the Probe
instance memory. IGameObject
is the second interface and a pointer to its vtable occupies the next four bytes of the Probe
instance memory. After these two vtables, we can find fields of the Probe
class. I draw the diagram below to better visualize these concepts:

So, what are those adjustors in the IGameObject
vtable? Adjustors allow the compiler to reuse the IUnknown
methods already compiled for the IProbe
implementation. The only problem with reusing is that methods implementing IProbe
expect this
to point to the beginning of the Probe
class instance. So we can’t simply use their addresses in the IGameObject
vtable – we need first to adjust the this
pointer. And that’s what the sub dword ptr [esp+4],4
instruction is doing. Then, we can safely jump to the IProbe
‘s QueryInterface
implementation, and everything will work as expected.
To end the vtables discussion, I have one more WinDbg script for you:
.for (r $t0 = 0; @$t0 < N; r $t0= @$t0 + 1) { bp poi(VTABLE_ADDRESS + @$t0 * @$ptrsize) }
This script sets breakpoints on the first N methods of a given vtable (replace N with any number you need). For example, to break on all the methods of the IGameObject
interface, I would run:
.for (r $t0 = 0; @$t0 < 6; r $t0= @$t0 + 1) { bp poi(031a8620 + @$t0 * @$ptrsize) }
We may also track COM objects from a specific DLL. When the application loads the target DLL, we need to set a breakpoint on the exported DllGetClassObject
function. For example, let’s debug what is happening when we call CoCreateInstance
for the Probe
COM object. We start by setting a break on the protoss.dll load:
0:000> sxe ld:protoss.dll 0:000> g ... ModLoad: 66c90000 66cd4000 C:\temp\protoss-com-example\Debug\protoss.dll
Next, we set a breakpoint on the protoss!DllGetClassObject
function and wait for it to hit:
0:000> bp protoss!DllGetClassObject "dps @esp L8; dt ntdll!_GUID poi(@esp + 4); dt ntdll!_GUID poi(@esp + 8); .printf /D \"==> obj addr: %p\", poi(@esp+c);.echo; bp /1 @$ra; g" 0:000> g 009cea10 75d6b731 combase!CClassCache::CDllPathEntry::GetClassObject+0x5a [onecore\com\combase\objact\dllcache.cxx @ 2581] 009cea14 00e9f354 009cea18 75ce84c8 combase!IID_IClassFactory 009cea1c 009cec40 009cea20 00000000 009cea24 00e9b3f8 009cea28 75ce84c8 combase!IID_IClassFactory 009cea2c 00e9f354 {eff8970e-c50f-45e0-9284-291ce5a6f771} +0x000 Data1 : 0xeff8970e +0x004 Data2 : 0xc50f +0x006 Data3 : 0x45e0 +0x008 Data4 : [8] "???" {00000001-0000-0000-c000-000000000046} +0x000 Data1 : 1 +0x004 Data2 : 0 +0x006 Data3 : 0 +0x008 Data4 : [8] "???" ==> obj addr: 009cec40 Breakpoint 1 hit
We can see that CoCreateInstance
uses the Probe
class CLSID and asks for the IClassFactory
instance. IClassFactory
inherits from IUnknown
(as all COM interfaces) and contains only two methods:
struct IClassFactory : public IUnknown
{
virtual HRESULT STDMETHODCALLTYPE CreateInstance(
_In_opt_ IUnknown *pUnkOuter, _In_ REFIID riid, _COM_Outptr_ void **ppvObject) = 0;
virtual HRESULT STDMETHODCALLTYPE LockServer(/* [in] */ BOOL fLock) = 0;
};
Let’s set a breakpoint on the CreateInstance
method and continue execution:
0:000> dpp 009cec40 L1 009cec40 031ab020 031a863c protoss!ProtossObjectClassFactory<Probe,IProbe>::`vftable' 0:000> dps 031a863c L5 031a863c 031a45e0 031a8640 031a45d0 031a8644 031a45d0 031a8648 031a4500 031a864c 031a44f0 0:000> bp 031a4500 "dt ntdll!_GUID poi(@esp + c); .printf /D \"==> obj addr: %p\", poi(@esp + 10);.echo; bp /1 @$ra; g" 0:000> g {246a22d5-cf02-44b2-bf09-aab95a34e0cf} +0x000 Data1 : 0x246a22d5 +0x004 Data2 : 0xcf02 +0x006 Data3 : 0x44b2 +0x008 Data4 : [8] "???" ==> obj addr: 009cec58 Breakpoint 3 hit
Our breakpoint gets hit, and we see that the requested IID equals IID_IProbe
, which proves what I mentioned previously, that CoCreateInstance
internally uses an IClassFactory
instance to create a new Probe
class instance.
Finally, when we deal with COM automation and need to decode parameters passed to the IDispatch
instance, we may use the dt -r1 combase!tagVARIANT ARG_ADDRESS
command. It nicely formats all the VARIANT
fields but requires the combase.dll symbols.
We reached the end of this long post, and I hope I haven’t bored you too much 😅 I also hope that the presented materials will help you better understand and troubleshoot COM APIs. The source code of the Protoss COM example is available at https://github.com/lowleveldesign/protoss-com-example.
Until the next time! 👋
Great post, thanks.
OOC what did you use to generate the memory layout picture?
Thank you. I created the graphics in Microsoft Whiteboard.
Man, your grey on white is SO abhorridly hard to read… please fix that!
Thanks for the feedback. I was trying a new WordPress theme and I agree that the colors weren’t too good. I switched to the old one.
The excellent post gained so much information, Keep posting like this.
https://espirittech.com/dot-net-application-development-company/