C++/COM/代理 Dll : method override/method forwarding (COM implementation inheritance)

标签 c++ windows com interface

你好,祝你有美好的一天。

情况:
出于某种原因,有时我会遇到需要覆盖 COM 接口(interface)的一个或两个方法(用于一些没有源代码的旧应用程序)的情况,这通常与 Direct3D/DirectInput 相关(即它是通过调用 DLL 方法而不是 CoCreateInstance 创建)。通常我通过编写一个代理 DLL 来处理这种情况,该 DLL 覆盖创建我需要“修改”的接口(interface)的方法,并用我自己的接口(interface)替换原始接口(interface)。通常这是使一些较旧的应用程序正常工作而不会崩溃/伪影所必需的。

编译器:
我在 Windows 机器上使用 Visual Studio Express 2008,因此没有 C++0x 功能。该系统安装了 msysgit、msys、python、perl、gnu 实用程序 (awk/sed/wc/bash/etc)、gnu make 和 qmake (Qt-4.7.1)(并在 PATH 中可用)。

问题:
覆盖 COM 接口(interface)的一个方法是一件痛苦的事情(尤其是当原始接口(interface)有大约一百个方法时),因为我需要将许多调用转发到原始接口(interface),目前我看不到简化或自动化该过程的方法。例如,IDirect3D9 的覆盖看起来像这样:

class MyD3D9: public IDirect3D9{
protected:
    volatile LONG refCount;
    IDirect3D9 *orig;
public:
    STDMETHOD(QueryInterface)(THIS_ REFIID riid, LPVOID * ppvObj){
        if (!ppvObj)
            return E_INVALIDARG;
        *ppvObj = NULL;
        if (riid == IID_IUnknown  || riid == IID_IDirect3D9){
            *ppvObj = (LPVOID)this;
            AddRef();
            return NOERROR;
        }
        return E_NOINTERFACE;
    }

    STDMETHOD_(ULONG,AddRef)(THIS){
        InterlockedIncrement(&refCount);
        return refCount;
    }
    STDMETHOD_(ULONG,Release)(THIS){
        ULONG ref = InterlockedDecrement(&refCount);
        if (refCount == 0)
            delete this;
        return ref;
    }

    /*** IDirect3D9 methods ***/
    STDMETHOD(RegisterSoftwareDevice)(THIS_ void* pInitializeFunction){
        if (!orig)
            return E_FAIL;
        return orig->RegisterSoftwareDevice(pInitializeFunction);
    }

    STDMETHOD_(UINT, GetAdapterCount)(THIS){
        if (!orig)
            return 0;
        return orig->GetAdapterCount();
    }

    STDMETHOD(GetAdapterIdentifier)(THIS_ UINT Adapter,DWORD Flags, D3DADAPTER_IDENTIFIER9* pIdentifier){
        if (!orig)
            return E_FAIL;
        return orig->GetAdapterIdentifier(Adapter, Flags, pIdentifier);
    }

    STDMETHOD_(UINT, GetAdapterModeCount)(THIS_ UINT Adapter,D3DFORMAT Format){
        if (!orig)
            return 0;
        return orig->GetAdapterModeCount(Adapter, Format);
    }
/* some code skipped*/

    MyD3D9(IDirect3D9* origD3D9)
        :refCount(1), orig(origD3D9){
    }

    ~MyD3D9(){
        if (orig){
            orig->Release();
            orig = 0;
        }
    }
};

如您所见,这是非常低效的、容易出错的并且需要大量的复制粘贴。

问题:
在这种情况下,如何简化对 COM 接口(interface)的单个​​方法的覆盖?我只想指定我更改的方法,但我目前看不到这样做的方法。我也没有看到一种使用宏或模板或宏来优雅地缩短“转发”方法的方法,因为它们具有可变数量的参数。我看到的另一种方法是直接使用另一个方法返回的补丁方法表(使用VirtualProtect修改访问权限,然后写入方法表),我不太喜欢。

限制:
我更愿意在 C++ 源代码(宏/模板)中解决并且没有代码生成器(除非代码生成器的使用非常简单/优雅 - 即编写代码生成器是不行的,使用已经可用的代码生成器我可以在几分钟内设置并解决一行代码中的所有内容都可以)。只有在不添加额外的 DLL 依赖项的情况下,Boost 才可以。 MS 特定的编译器指令和语言扩展也可以。

想法?提前致谢。

最佳答案

好吧,因为我不喜欢没有答案的问题...

要实现“COM 实现继承”,目前还没有用纯 C++ 编写的健全而紧凑的解决方案。这主要是因为在C++中禁止创建抽象类的实例或直接操作虚方法表。因此,常用的解决方案有2种:

  1. 手动为每个方法编写方法转发。
  2. 破解调度表。

#1 的优点是这种方法是安全的,您可以在自定义类中存储额外的数据。 #1 的缺点是为每个方法编写包装器是极其繁琐的过程。

#2 的优点是这种方法很紧凑。您替换单一方法。 #2 的缺点是调度表可能位于写保护空间(很可能不会发生,但理论上可能发生)并且您不能在被黑接口(interface)中存储自定义数据。因此,虽然它很简单/简短,但它的局限性很大。

还有第三种方法。 (没有人出于某种原因建议)

简短描述:不使用 C++ 提供的虚方法表,而是编写将模拟虚方法表的非虚类。

例子:

template<typename T1, typename T2> void unsafeCast(T1 &dst, const T2 &src){
    int i[sizeof(dst) == sizeof(src)? 1: -1] = {0};
    union{
        T2 src;
        T1 dst;
    }u;
    u.src = src;
    dst = u.dst;
}

template<int Index> void __declspec(naked) vtblMapper(){
#define pointerSize 4 //adjust for 64bit
    static const int methodOffset = sizeof(void*)*Index;
    __asm{
        mov eax, [esp + pointerSize]
        mov eax, [eax + pointerSize]
        mov [esp + pointerSize], eax
        mov eax, [eax]
        add eax, methodOffset
        mov eax, [eax]
        jmp eax
    };
#undef pointerSize
}

struct MyD3DIndexBuffer9{
protected:
    VtblMethod* vtbl;
    IDirect3DIndexBuffer9* orig;
    volatile LONG refCount;
    enum{vtblSize = 14};
    DWORD flags;
    bool dynamic, writeonly;
public:
    inline IDirect3DIndexBuffer9*getOriginalPtr(){
        return orig;
    }

    HRESULT __declspec(nothrow) __stdcall QueryInterface(REFIID riid, LPVOID * ppvObj){
        if (!ppvObj)
            return E_INVALIDARG;
        *ppvObj = NULL;
        if (riid == IID_IUnknown  || riid == IID_IDirect3DIndexBuffer9){
            *ppvObj = (LPVOID)this;
            AddRef();
            return NOERROR;
        }
        return E_NOINTERFACE;
    }

    ULONG __declspec(nothrow) __stdcall AddRef(){
        InterlockedIncrement(&refCount);
        return refCount;
    }

    ULONG __declspec(nothrow) __stdcall Release(){
        ULONG ref = InterlockedDecrement(&refCount);
        if (refCount == 0)
            delete this;
        return ref;
    }

    MyD3DIndexBuffer9(IDirect3DIndexBuffer9* origIb, DWORD flags_)
            :vtbl(0), orig(origIb), refCount(1), flags(flags_), dynamic(false), writeonly(false){
        dynamic = (flags & D3DUSAGE_DYNAMIC) != 0;
        writeonly = (flags & D3DUSAGE_WRITEONLY) != 0;
        vtbl = new VtblMethod[vtblSize];
        initVtbl();
    }

    HRESULT __declspec(nothrow) __stdcall Lock(UINT OffsetToLock, UINT SizeToLock, void** ppbData, DWORD Flags){
        if (!orig)
            return E_FAIL;
        return orig->Lock(OffsetToLock, SizeToLock, ppbData, Flags);
    }

    ~MyD3DIndexBuffer9(){
        if (orig){
            orig->Release();
            orig = 0;
        }
        delete[] vtbl;
    }
private:
    void initVtbl(){
        int index = 0;
        for (int i = 0; i < vtblSize; i++)
            vtbl[i] = 0;

#define defaultInit(i) vtbl[i] = &vtblMapper<(i)>; index++
        //STDMETHOD(QueryInterface)(THIS_ REFIID riid, void** ppvObj) PURE;
        unsafeCast(vtbl[0], &MyD3DIndexBuffer9::QueryInterface); index++;
        //STDMETHOD_(ULONG,AddRef)(THIS) PURE;
        unsafeCast(vtbl[1], &MyD3DIndexBuffer9::AddRef); index++;
        //STDMETHOD_(ULONG,Release)(THIS) PURE;
        unsafeCast(vtbl[2], &MyD3DIndexBuffer9::Release); index++;

        // IDirect3DResource9 methods 
        //STDMETHOD(GetDevice)(THIS_ IDirect3DDevice9** ppDevice) PURE;
        defaultInit(3);
        //STDMETHOD(SetPrivateData)(THIS_ REFGUID refguid,CONST void* pData,DWORD SizeOfData,DWORD Flags) PURE;
        defaultInit(4);
        //STDMETHOD(GetPrivateData)(THIS_ REFGUID refguid,void* pData,DWORD* pSizeOfData) PURE;
        defaultInit(5);
        //STDMETHOD(FreePrivateData)(THIS_ REFGUID refguid) PURE;
        defaultInit(6);
        //STDMETHOD_(DWORD, SetPriority)(THIS_ DWORD PriorityNew) PURE;
        defaultInit(7);
        //STDMETHOD_(DWORD, GetPriority)(THIS) PURE;
        defaultInit(8);
        //STDMETHOD_(void, PreLoad)(THIS) PURE;
        defaultInit(9);
        //STDMETHOD_(D3DRESOURCETYPE, GetType)(THIS) PURE;
        defaultInit(10);
        //STDMETHOD(Lock)(THIS_ UINT OffsetToLock,UINT SizeToLock,void** ppbData,DWORD Flags) PURE;
        //defaultInit(11);
        unsafeCast(vtbl[11], &MyD3DIndexBuffer9::Lock); index++;
        //STDMETHOD(Unlock)(THIS) PURE;
        defaultInit(12);
        //STDMETHOD(GetDesc)(THIS_ D3DINDEXBUFFER_DESC *pDesc) PURE;
        defaultInit(13);
#undef defaultInit
    }
};

要将其与真实界面交换,您必须使用reinterpret_cast

        MyD3DIndexBuffer9* myIb = reinterpret_cast<MyD3DIndexBuffer9*>(pIndexData);

如您所见,此方法需要组合在一起的程序集、宏、模板以及将类方法指针转换为void*。它还依赖于编译器(msvc,尽管你应该能够用 g++ 做同样的事情)和依赖于体系结构(32/64 位)。此外,它也不安全(与调度表黑客攻击一样)。

与分派(dispatch)表相比,您可以使用自定义类并在界面中存储额外数据的优势。然而:

  1. 禁止所有虚拟方法。 (据我所知,任何使用虚方法的尝试都会立即在类的开头插入不可见的 4 字节指针,这会破坏一切)。
  2. 调用约定必须是 stdcall(虽然应该与 cdecl 一起使用,但对于其他所有内容,您将需要不同的包装器)
  3. 你必须自己初始化整个 vtable(非常容易出错)。一个错误,一切都会崩溃。

关于C++/COM/代理 Dll : method override/method forwarding (COM implementation inheritance),我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/7097740/

相关文章:

c++ - 为什么这里会出现这种歧义?

python - 如何在没有 pythonw.exe 的情况下将 Python 脚本放在后台?

c# - Windows UIAutomation 有时会挂起其他应用程序

c++ - 对包含类的 'std::vector' 进行排序

c++ - 序列容器-只能顺序访问元素

c++ - 检查真实文件后的假文件 "exist"?

java - 无法找到或加载主类

javascript - 在 JavaScript 中将 int64 值转换为 Number 对象

c# - 在用 C# 编写的 COM+ 组件上处理 dispose

c++ - C++中的赋值运算符删除内存