windows - C ++/COM/Proxy Dlls: metode tilsidesættelse/metode videresendelse (COM implementering arv)

Indlæg af Hanne Mølgaard Plasc

Problem



Hej og god dag til dig.


Situation:

Af en eller anden grund løber jeg fra tid til anden, når jeg skal tilsidesætte en eller to metoder til en COM-grænseflade (som bruges til en ældre applikation uden kildekode), som normalt er Direct3D/DirectInput-relateret (dvs. det er oprettet ved at kalde en DLL-metode, ikke af CoCreateInstance). Normalt behandler jeg situationen ved at skrive en proxy DLL, der tilsidesætter en metode, der skaber interface, jeg skal 'modificere' og erstatte original interface med min egen. Normalt kræves det, at nogle ældre applikationer fungerer korrekt uden at gå i stykker/genstande.


Compiler:

Jeg bruger Visual Studio Express 2008 på Windows-maskine, så der er ingen C ++ 0x-funktioner. Systemet har msysgit, msys, python, perl, gnu utilities (awk/sed/wc/bash/etc), gnu make og qmake (Qt-4.7.1) installeret (og tilgængelig inden for PATH).


Problem:

Overstyrende en metode til en COM-grænseflade er en smerte (især hvis den oprindelige grænseflade har hundrede metoder eller deromkring), fordi jeg har brug for at videresende mange opkald til den oprindelige grænseflade, og i øjeblikket ser jeg ingen måde at forenkle eller automatisere processen. For eksempel ser overstyring af IDirect3D9 ud:


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


Som du kan se, er dette meget ineffektivt, fejlagtigt og kræver meget kopiering.


Spørgsmål:

Hvordan kan jeg forenkle overstyring af en enkelt metode til en COM-grænseflade i denne situation? Jeg vil gerne specificere den eneste metode, jeg ændrer, men jeg ser for øjeblikket ingen måde at gøre det på. Jeg kan heller ikke se en måde at elegant forkorte 'fremsendte' metoder med makroer eller skabeloner eller makroer, fordi de har variabelt antal argumenter. En anden tilgang jeg så, er at bruge direkte patchmetode tabel returneret af en anden metode (ændre adgangsretten ved at bruge VirtualProtect, skriv derefter i metode tabellen), som jeg ikke kan lide.


Begrænsninger:

Jeg foretrækker at løse i C ++ kildekode (makroer/skabeloner) og uden kodegeneratorer (medmindre kodegeneratorbrug er ekstremt enkel/elegant - dvs. skrivekodegeneratoren er ikke ok, ved hjælp af allerede tilgængelig kodegenerator kan jeg opsætte i minutter og løse det hele i en linje med kode er ok). Boost er kun okay, hvis det ikke tilføjer ekstra DLL afhængighed. MS-specifikke compiler direktiver og sprogudvidelser er også ok.


Ideer? Tak på forhånd.

Bedste reference


Okay, da jeg ikke kan lide ubesvarede spørgsmål ...


For at implementere 'COM implementation arv' er der i øjeblikket ingen sund og kompakt løsning skrevet i ren C ++. Dette skyldes for det meste, at i C ++ er det forbudt at oprette en forekomst af abstrakt klasse eller manipulere virtuelle metortabel direkte. Som følge heraf er der 2 almindeligt anvendte løsninger:



  1. Skriv metodefremsendelse for hver metode manuelt.

  2. Hack dispatch tabel.



Fordelen ved # 1 er, at denne tilgang er sikker, og du kan gemme yderligere data i brugerklasse.
Ulempen ved # 1 er, at skrive en wrapper for hver enkelt metode er ekstremt kedelig procedure.


Fordelen ved # 2 er, at denne tilgang er kompakt. Du erstatter en enkelt metode.
Ulempen ved # 2 er, at forsendelsestabellen kan være placeret i skrivebeskyttet rum (sandsynligvis ville det ikke ske, men det kunne ske i teorien) og du kan ikke gemme brugerdefinerede data i hacket interface. Som følge heraf, selvom det er enkelt/kort, er det ret begrænsende.


Og der er en 3. tilgang . (Som ingen har foreslået af en eller anden grund )


Kort beskrivelse: I stedet for at bruge virtuel metode tabel leveret af C ++, skriv ikke-virtuelle klasse, der vil efterligne virtuelle metode tabel.


Eksempel:


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


For at bytte det med ægte grænseflade skal du bruge reinterpret\_cast.


        MyD3DIndexBuffer9* myIb = reinterpret\_cast<MyD3DIndexBuffer9*>(pIndexData);


Som du kan se denne metode kræver samling, makroer, skabeloner kombineret sammen med casting class methode pointer to void *. Det er også compiler-afhængigt (msvc, selvom du skal kunne gøre det samme trick med g ++) og arkitekturafhængige (32/64-bit). Plus det er usikkert (som med afsendelse bord hacking).


Fordelen i forhold til forsendelsestabeller kan du bruge brugerdefineret klasse og gemme yderligere data inden for grænsefladen. Imidlertid:



  1. Alle virtuelle metoder er forbudt. (så vidt jeg ved, vil ethvert forsøg på at bruge virtuel metode øjeblikkeligt indsætte usynlig 4-bytes-peger i begyndelsen af ​​klassen, som vil bryde alt).

  2. Opkaldskonventionen skal være stdcall (skal arbejde med cdecl, men for alt andet skal du have forskellige omslag)

  3. Du skal initialisere hele vtablet selv (meget fejlfæstet). En fejl, og alt vil gå ned.