Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What exactly is "lightweight COM"?

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)?

Footnotes:

  1. 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."

  2. 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."

  3. 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)."

like image 649
stakx - no longer contributing Avatar asked Jul 03 '16 20:07

stakx - no longer contributing


2 Answers

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");
    }
}
like image 59
stakx - no longer contributing Avatar answered Nov 16 '22 17:11

stakx - no longer contributing


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:

  • Object definition (ODL/IDL, metadata, TLB, type system, etc.)
  • Object activation (registry, progids, CoCreateInstance, etc.)
  • Object RPC (Cross thread or process marshaling, proxy/stub, etc.)
  • Automation (VARIANT, BSTR, universal marshaler, late binding, support for scripting languages, etc.)
  • Component services (Object Pooling, Surrogates, DLLHost, MTC/DTC, etc.)

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.

like image 5
Simon Mourier Avatar answered Nov 16 '22 15:11

Simon Mourier