Self modifying virtual table entries to point to concrete implementation
Short version:
Can a COM class modify its own virtual table entries at runtime? (disregarding thread issues)
Full version:
I'm providing a number of C++ classes which implement a com interface. The COM interface is defined in a third-party framework.
(Edited: Th开发者_JAVA技巧e vtable of the COM interface is standard on Windows platform; but the relationship between the C++ vtable and the COM vtable isn't clear to me.)
In order to inject my implementations correctly into that framework, it needs to use two-step initialization: first create an object with no parameters, then call an Initialize
method with all parameters. This is the moment when one of my implementations can be chosen.
To make this happen, I added a "resolver" class (or simply a wrapper class), whose sole responsibility is to choose an implementation in the Initialize
method. After that, every call to its COM methods will be forwarded to the actual implementation.
The resolver class is then injected into the framework. This gets around the framework limitation.
Now, seeing that the resolver class doesn't really have any use after initialization, I'm wondering if there is a way to get rid of the virtual method indirection cost? My idea is to copy each COM vtable entry from the concrete implementation to the vtable of the resolver class.
Will this work?
Example:
// (FYI) #define HRESULT unsigned long
struct IInterface
{
// ... the usual AddRef, Release and QI omitted
virtual HRESULT Initialize(VARIANT v) = 0; // the Initialize method, where implementation gets chosen
virtual HRESULT DoWork() = 0; // can only call after initialization.
};
class MyResolver : public IInterface
{
// ... the usual AddRef, Release and QI omitted
public:
virtual HRESULT Initialize(VARIANT v)
{
if ( /* some conditions based on v */ )
return Implem_One.Create((void**) &m_pImpl);
else
return Implem_Two.Create((void**) &m_pImpl);
"Here, can I copy the vtable entries from m_pImpl to the vtable of MyResolver (*this) ?";
for (int k = 0; k < num_virtual_methods; ++k)
this->vtbl[k] = m_pImpl->vtbl[k];
}
virtual HRESULT DoWork()
{
if (!m_pImpl) return ERROR_NOT_INITIALIZED;
m_pImpl->DoWork();
}
public:
// this creation method is injected into the framework
static HRESULT Create(void**) { /* create an instance of this class and return it */ }
private:
MyResolver() : m_pImpl(NULL) {}
virtual ~MyResolver() { if (m_pImpl) m_pImpl->Release(); }
IInterface* m_pImpl;
};
You can't safely copy vtable entries; since the this
pointer will still refer to your proxy class, the real implementation won't find its private data.
If the base classes support COM aggregation, you can use this to avoid the overhead however. When constructing your base object, you'd pass your outer class's IUnknown as the outer aggregation IUnknown. This means that the QueryInterface/AddRef/Release on all derived interfaces of the base object will now delegate to your outer object, making it look like a part of your outer object.
Now you can have QueryInterface return your original object (with proxying methods) prior to initialization, or the base object's interface directly when initialization is complete. Example:
class MyProxy : public IInterface
{
long refCt;
IUnknown *pUnkInner;
IInterface *pIfaceInner;
~MyProxy() {
AddRef(); // pUnkInner may take references to us in its destruction; prevent reentrancy per aggregating object rules
if (pUnkInner) {
pUnkInner->Release();
}
}
public:
MyProxy() : refCt(1), pUnkInner(NULL), pIfaceInner(NULL) { }
STDMETHODIMP QueryInterface(REFIID riid, void **ppvObject)
{
if (!ppvObject) return E_POINTER;
if (riid == IID_IUnknown) {
AddRef();
*ppvObject = (void *)static_cast<IUnknown *>(this);
return S_OK;
} else if (riid == IID_IInterface && pUnkInner) {
// increments refcount of _outer_ object
return pUnkInner->QueryInterface(riid, ppvObject);
} else if (riid == IID_IInterface) {
AddRef();
*ppvObject = (void *)static_cast<IInterface *>(this);
return S_OK;
} else {
return E_NOINTERFACE;
}
}
STDMETHODIMP_(DWORD) AddRef(void)
{ return InterlockedIncrement(&refCt); }
STDMETHODIMP_(DWORD) Release(void)
{ if (!InterlockedDecrement(&refCt)) delete this; }
HRESULT Initialize(VARIANT v) {
// You can use another protocol to create the object as well as long as it supports aggregation
HRESULT res = CoCreateInstance(CLSID_InnerClass, this, CLSCTX_INPROC_SERVER, IID_IUnknown, (LPVOID *)&pUnkInner);
if (FAILED(res)) return res;
res = pUnkInner->QueryInterface(IID_IInterface, (void **)&pIfaceInner);
if (FAILED(res)) {
pUnkInner->Release();
pUnkInner = NULL;
return res;
}
Release(); // the above QueryInterface incremented the _outer_ refcount, we don't want that.
return S_OK;
}
HRESULT DoWork() {
if (!pIfaceInner) return ERROR_NOT_INITIALIZED;
return pIfaceInner->DoWork();
}
};
Although the initial IInterface
pointer the client gets has the double-call overhead, once the initialization is complete, they can re-QueryInterface
to get a more direct pointer. What's more, if you can move the initialization to a different interface, you can force the client to re-QueryInterface
, thus ensuring they get a direct pointer.
That said, it's vitally important that the aggregated objects support the aggregation protocol; otherwise you'll end up with inconsistent reference counts and other badness. Read the MSDN documentation carefully before implementing aggregation.
精彩评论