Given the above comments and the fact that there has never been a published specification for COM (except for this version 0.9 draft paper from 1995), asking for a definition of "lightweight COM" might be pointless: If "COM" isn't a precisely defined thing (but more of an idea), then perhaps the same is true for "lightweight COM". It could in theory mean slightly different things for different APIs making use of the idea.
The following is an attempt to define what kind of "lightweight COM" is used by DirectX-style APIs. I am also including a code example for a "lightweight COM" component of my own.
Similarities to COM:
- "Lightweight COM" APIs look like COM. They have the same "everything is accessible through an interface", "interfaces only have methods", "all interfaces directly or indirectly inherit from
IUnknown
", and "interfaces never change, once published" world view.
- The
IUnknown
interface used is identical to COM's IUnknown
.
- This means that "lightweight COM" APIs also use reference counting for memory management, and
QueryInterface
and IIDs to retrieve interface pointers.
- "Lightweight COM" APIs have the same Application Binary Interface (ABI) as COM; this includes things like object / vtable memory layout, the
__stdcall
calling convention, HRESULT
return values, etc.
- For this reason, "lightweight COM" APIs can be used from .NET via COM interop. (See example below.)
Differences from COM:
Components are not registered in the registry by a CLSID. That is, components are not instantiated through a call to CoCreateInstance
; instead, a client directly references the API's library, which exposes factory functions (such as Direct2D's D2D1CreateFactory
in d2d1.dll
). Other objects can be retrieved from this "entry point" factory object.
Since the DLL is directly loaded into a client process, a "lightweight COM" API (unlike COM) only supports in-process servers. Remoting stubs & proxies are therefore not needed, nor supported.
In theory, a "lightweight COM" library does not rely on OLE32.dll
at all, i.e. makes / requires no calls to the CoXXX
functions (such as CoInitialize
to set a thread's apartment, CoCreateInstance
to instantiate co-classes, etc.).
(A "lightweight COM" library might however still have to use the COM memory allocator (CoTaskMemAlloc
, CoTaskMemRealloc
, CoTaskMemFree
) if it interoperates with an actual COM library… or with the .NET marshaller, which assumes that it is dealing with a COM library.)
Since CoInitialize
is not needed, it follows that "lightweight COM" does not use COM's apartment threading model. "Lightweight COM" APIs usually implement their own threading model, e.g. Direct2D's multithreading model. (The fact that this page contains no hint whatsoever which COM apartment model Direct2D supports is a hint that COM apartments simply don't apply to Direct2D at all!)
Example of a "lightweight COM" component:
The following C++ file (Hello.cc
) implements a "lightweight COM" component Hello
. In order to make the point that this will be independent from COM, I am not including any COM or OLE header files:
#include <cinttypes>
#include <iostream>
// `HRESULT`:
typedef uint32_t HRESULT;
const HRESULT E_OK = 0x00000000;
const HRESULT E_NOINTERFACE = 0x80004002;
// `GUID` and `IID`:
typedef struct
{
uint32_t Data1;
uint16_t Data2;
uint16_t Data3;
uint8_t Data4[8];
} GUID;
bool operator ==(const GUID &left, const GUID &right)
{
return memcmp(&left, &right, sizeof(GUID)) == 0;
}
typedef GUID IID;
// `IUnknown`:
const IID IID_IUnknown = { 0x00000000, 0x0000, 0x0000, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46 };
class IUnknown
{
public:
virtual HRESULT __stdcall QueryInterface(const IID *riid, void **ppv) = 0;
virtual uint32_t __stdcall AddRef() = 0;
virtual uint32_t __stdcall Release() = 0;
};
// `IHello`:
const IID IID_IHello = { 0xad866b1c, 0x5735, 0x45e7, 0x84, 0x06, 0xcd, 0x19, 0x9e, 0x66, 0x91, 0x3d };
class IHello : public IUnknown
{
public:
virtual HRESULT __stdcall SayHello(const wchar_t *name) = 0;
};
// The `Hello` pseudo-COM component:
class Hello : public IHello
{
private:
uint32_t refcount_;
public:
Hello() : refcount_(0) { }
virtual HRESULT __stdcall QueryInterface(const IID *riid, void **ppv)
{
if (*riid == IID_IUnknown)
{
*ppv = static_cast<IUnknown*>(this);
}
else if (*riid == IID_IHello)
{
*ppv = static_cast<IHello*>(this);
}
else
{
*ppv = nullptr;
return E_NOINTERFACE;
}
reinterpret_cast<IUnknown*>(*ppv)->AddRef();
return E_OK;
}
virtual uint32_t __stdcall AddRef()
{
return ++refcount_;
}
virtual uint32_t __stdcall Release()
{
auto refcount = --refcount_;
if (refcount == 0)
{
delete this;
}
return refcount;
}
virtual HRESULT __stdcall SayHello(const wchar_t *name)
{
std::wcout << L"Hello, " << name << L"!" << std::endl;
return E_OK;
}
};
// Factory method that replaces `CoCreateInstance(CLSID_Hello, …)`:
extern "C" HRESULT __stdcall __declspec(dllexport) CreateHello(IHello **ppv)
{
*ppv = new Hello();
return E_OK;
}
I compiled the above with Clang (linking against Visual Studio 2015's C++ standard libraries), again without linking in any COM or OLE libraries:
clang++ -fms-compatibility-version=19 --shared -m32 -o Hello.dll Hello.cc
Example of .NET interop against the above component:
Now, given that the produced DLL is in the search path of my .NET code (e.g. in the bin\Debug\
or bin\Release\
directory), I can use the above component in .NET using COM interop:
using System.Runtime.InteropServices;
[ComImport]
[Guid("ad866b1c-5735-45e7-8406-cd199e66913d")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IHello
{
void SayHello([In, MarshalAs(UnmanagedType.LPWStr)] string name);
}
class Program
{
[DllImport("Hello.dll", CallingConvention=CallingConvention.StdCall)]
extern static void CreateHello(out IHello outHello);
static void Main(string[] args)
{
IHello hello;
CreateHello(out hello);
hello.SayHello("Fred");
}
}