While researching which COM apartment threading models are supported by Direct2D, I discovered that despite appearances and the fact that one can use the API from .NET using COM interoperability, Direct2D (like other DirectX APIs) is not actually a COM API at all.(1) Both the Wikipedia article on Direct2D(2) as well as a MSDN blog post by Thomas Olsen(3) refer to these APIs using a "lightweight COM" approach.
However, I haven't found any official definition of what exactly this "lightweight COM" is. Is there any such definition (possibly by Microsoft)?
Mike Danes' answer to the MSDN forum question, 'CoInitialize/CoUninitialize, are they really needed for Direct2D and DirectWrite?'. Here's the interesting bit:
"DirectWrite/Direct2D/Direct3D are COM like APIs but they don't use COM. They are not registered in registry like normal COM components, they do not following COM threading models, they don't support any sort of marshaling etc. They're not COM."
Wikipedia article on Direct2D (Overview section):
"Direct2D is a native code API based on C++ that can be called by managed code and uses a "lightweight COM" approach just like Direct3D, with minimal amount of abstraction."
Thomas Olsen's MSDN blog post mentions the following as a design goal of Direct2D:
"Lightweight COM – Should use C++ style interfaces which model Direct3D usage. No support for proxies, cross-process remoting, BSTRs, VARIANTs, COM registration (e.g. the heavyweight stuff)."
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.
IUnknown
", and "interfaces never change, once published" world view.IUnknown
interface used is identical to COM's IUnknown
.QueryInterface
and IIDs to retrieve interface pointers.__stdcall
calling convention, HRESULT
return values, etc.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!)
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
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");
}
}
You do lightweight COM when you use VTable binding (also used to be called "early binding") + the IUnknown definition. That's about it. You'll never find a definition of that in ancient Microsoft publication, because it never existed with this name.
As an API developer, when you declare you do "lightweight COM", you basically declare that you don't care about the following:
Note that does not mean you won't have it, in fact, you will have it for free depending on the tooling you use (namely Visual Studio tooling, for example, it's somehow easier to rely on (M)IDL for the abstractions it provides), it's just that you like the idea of having an extensible API (with COM, you can "cast" objects between binary components) without the burden of supporting all these services.
Note also that "lightweight COM" is very very very portable across any platform and any language (call it "open") which is today more interesting.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With