C ++ / COM / Proxy Dlls: переопределение метода / переадресация метода (наследование реализации COM) - PullRequest
2 голосов
/ 17 августа 2011

Здравствуйте и хорошего вам дня.

Положение:
По какой-то причине время от времени я сталкиваюсь с ситуацией, когда мне нужно переопределить один или два метода интерфейса COM (который используется для некоторых старых приложений без исходного кода), который обычно связан с Direct3D / DirectInput (т.е. создается путем вызова метода DLL, а не CoCreateInstance). Обычно я сталкиваюсь с ситуацией, когда пишу прокси-библиотеку DLL, которая переопределяет метод, который создает интерфейс, который мне нужно «модифицировать», и заменяет оригинальный интерфейс своим собственным. Обычно это требуется для правильной работы некоторых старых приложений без сбоев / артефактов.

Компилятор:
Я использую Visual Studio Express 2008 на компьютере с Windows, поэтому нет никаких особенностей C ++ 0x. В системе установлены утилиты msysgit, msys, python, perl, gnu (awk / sed / wc / bash / etc), gnu make и qmake (Qt-4.7.1) (доступные в PATH).

Проблема:
Переопределение одного метода интерфейса COM - это боль (особенно, если в исходном интерфейсе есть около сотни методов), потому что мне нужно перенаправить много вызовов на оригинальный интерфейс, и в настоящее время я не вижу способа упростить или автоматизировать процесс. Например, переопределение 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 в этой ситуации? Я хотел бы указать только метод, который я изменяю, но в настоящее время я не вижу способа сделать это. Я также не вижу способа элегантно сократить «перенаправленные» методы с помощью макросов, шаблонов или макросов, потому что они имеют переменное количество аргументов. Другой подход, который я видел, - это использовать таблицу методов непосредственного исправления, возвращенную другим методом (изменить права доступа с помощью VirtualProtect, а затем записать в таблицу методов), что мне не совсем нравится.

Ограничения:
Я бы предпочел решить в исходном коде C ++ (макросы / шаблоны) и без генераторов кода (если использование генератора кода не является чрезвычайно простым / элегантным - то есть, написание генератора кода не в порядке, с использованием уже доступного генератора кода, который я могу настроить за минуты и решить все в одной строке кода в порядке). Boost - это нормально, только если он не добавляет дополнительную зависимость от DLL. Специфичные для MS директивы компилятора и языковые расширения также в порядке.

Идеи? Заранее спасибо.

1 Ответ

1 голос
/ 03 октября 2011

Хорошо, так как я не люблю вопросы без ответов ...

Для реализации «наследования реализации COM» в настоящее время не существует разумного и компактного решения, написанного на чистом C ++. Это происходит главным образом потому, что в C ++ запрещено создавать экземпляр абстрактного класса или напрямую манипулировать таблицей виртуальных методов. В результате есть 2 часто используемых решения:

  1. Переадресация метода для каждого метода вручную
  2. Таблица отправки взлома.

Преимущество # 1 в том, что этот подход безопасен, и вы можете хранить дополнительные данные в пользовательском классе. Недостаток # 1 заключается в том, что написание оболочки для каждого отдельного метода является чрезвычайно утомительной процедурой.

Преимущество # 2 в том, что этот подход компактен. Вы заменяете единственный метод. Недостаток # 2 в том, что таблица диспетчеризации может быть расположена в защищенном от записи пространстве (скорее всего, этого не произойдет, но теоретически это может произойти), и вы не можете хранить пользовательские данные в взломанном интерфейсе. В результате, хотя это просто / коротко, это довольно ограничивающее.

И есть 3-й подход . (который никто не предложил по какой-то причине )

Краткое описание: вместо использования таблицы виртуальных методов, предоставляемой C ++, напишите не виртуальный класс, который будет эмулировать таблицу виртуальных методов.

* * 1 022 Пример: * 1 023 *
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-битная версия). Плюс это небезопасно (как при взломе таблицы рассылки).

Преимущество по сравнению с таблицами диспетчеризации заключается в том, что вы можете использовать собственный класс и хранить дополнительные данные в интерфейсе. Однако:

  1. Все виртуальные методы запрещены. (Насколько я знаю, любая попытка использовать виртуальный метод мгновенно вставит невидимый 4-байтовый указатель в начале класса, что сломает все).
  2. Соглашение о вызовах должно быть stdcall (хотя должно работать с cdecl, но для всего остального вам понадобится другая оболочка)
  3. Вы должны инициализировать весь vtable самостоятельно (очень подвержен ошибкам). Одна ошибка, и все рухнет.
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...