"lightweight COM" 到底是什么?

What exactly is "lightweight COM"?

在研究 Direct2D 支持哪些 COM 单元线程模型时,我发现尽管表面上和事实上可以使用 .NET 中的 API 使用 COM 互操作性,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) 使用“轻量级 COM”方法引用这些 API。

但是,我还没有找到关于这个“轻量级 COM”到底是什么的任何官方定义。有没有这样的定义(可能是微软的)?

脚注:

  1. Mike Danes 对 MSDN 论坛问题的回答,'CoInitialize/CoUninitialize, are they really needed for Direct2D and DirectWrite?'。这是有趣的一点:

    "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 提到以下作为 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)."

鉴于 and the fact that there has never been a published specification for COM (except for this version 0.9 draft paper from 1995),要求 "lightweight COM" 的定义可能毫无意义:如果 "COM" 不是一个精确定义的东西(但更像是一个想法),那么也许 "lightweight COM" 也是如此。理论上,对于不同的API使用这个想法的人来说,它可能意味着略有不同的东西。

以下尝试定义 DirectX 风格的 API 使用何种类型的 "lightweight COM"。我还包含了我自己的 "lightweight COM" 组件的代码示例。

与 COM 的相似之处:

  • "Lightweight COM" API 看起来像 COM。他们有着相同的"everything is accessible through an interface"、"interfaces only have methods"、"all interfaces directly or indirectly inherit from IUnknown"、"interfaces never change, once published"世界观。
  • 使用的IUnknown接口与COM的IUnknown相同。
  • 这意味着 "lightweight COM" APIs 也使用引用计数来进行内存管理,并且 QueryInterface 和 IIDs 来检索接口指针。
  • "Lightweight COM" APIs 具有与 COM 相同的应用程序二进制接口 (ABI);这包括对象/vtable 内存布局、__stdcall 调用约定、HRESULT return 值等
  • 因此,"lightweight COM" APIs 可以通过 COM 互操作从 .NET 使用。 (参见下面的示例。)

与 COM 的区别:

  • 组件未通过 CLSID 在注册表中注册。也就是说,组件不是通过调用 CoCreateInstance 来实例化的;相反,客户端直接引用 API 的库,它公开了工厂函数(例如 Direct2D 的 D2D1CreateFactory in d2d1.dll)。可以从此 "entry point" 工厂对象检索其他对象。

  • 由于 DLL 直接加载到客户端进程中,因此 "lightweight COM" API(与 COM 不同)仅支持进程内服务器。因此不需要也不支持远程存根和代理。

  • 理论上,"lightweight COM" 库根本不依赖 OLE32.dll,即不需要调用 CoXXX 函数(例如 CoInitialize 设置线程的套间,CoCreateInstance 实例化 co-类,等等)。

  • ("lightweight COM" 库可能仍然必须使用 COM 内存分配器(CoTaskMemAllocCoTaskMemReallocCoTaskMemFree)如果它互操作使用实际的 COM 库……或使用 .NET 编组器,它假定它正在处理 COM 库。)

  • 由于不需要CoInitialize,因此"lightweight COM"不使用COM的单元线程模型。 "Lightweight COM" APIs 通常实现自己的线程模型,例如Direct2D's multithreading model。 (事实上​​,该页面不包含 Direct2D 支持哪种 COM 单元模型的任何提示,这暗示 COM 单元根本不适用于 Direct2D!)

"lightweight COM" 组件示例:

以下 C++ 文件 (Hello.cc) 实现了 "lightweight COM" 组件 Hello。为了说明这将独立于 COM,我不包括任何 COM 或 OLE 头文件:

#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;
}

我用 Clang 编译了上面的代码(链接到 Visual Studio 2015 年的 C++ 标准库),同样没有链接任何 COM 或 OLE 库:

clang++ -fms-compatibility-version=19 --shared -m32 -o Hello.dll Hello.cc

针对上述组件的 .NET 互操作示例:

现在,假定生成的 DLL 在我的 .NET 代码的搜索路径中(例如在 bin\Debug\bin\Release\ 目录中),我可以在 .NET 中使用上述组件COM 互操作:

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");
    }
}

当你使用VTable绑定(也曾被称为"early binding")时,你做的是轻量级COM + IUnknown定义.就是这样。 您永远不会在古老的 Microsoft 出版物中找到它的定义,因为它从来没有以这个名称存在过。

作为一名 API 开发人员,当您声明您这样做时 "lightweight COM",您基本上声明您不关心以下内容:

  • 对象定义(ODL/IDL、元数据、TLB、类型系统等)
  • 对象激活(注册表、progids、CoCreateInstance 等)
  • 对象 RPC(跨线程或进程编组,proxy/stub,等)
  • 自动化(VARIANT、BSTR、通用编组器、后期绑定、脚本语言支持等)
  • 组件服务(对象池、代理、DLLHost、MTC/DTC 等)

请注意,这并不意味着您不会拥有它,事实上,您 免费获得它,具体取决于您使用的工具(即 Visual Studio 工具,例如,依赖 (M)IDL 提供的抽象在某种程度上更容易),只是您喜欢拥有可扩展 API 的想法(使用 COM,您可以 "cast" 对象之间二进制组件)而无需支持所有这些服务的负担。

另请注意,"lightweight COM" 可以跨任何平台和任何语言(称之为 "open")移植,这在今天更有趣。