The complete source and prebuilt release binaries (all four versions) are available here.
To implement a custom vtable interface "on the fly".
CoProxyImpl encapsulates the small amount of Intel assembler code needed to implement a vtable interface "on the fly". It is similar in nature to the Universal Delegator, however, the Universal Delegator works under the assumption that there is already an underlying vtable interface that you simply wish to augment in some way.
CoProxyImpl is designed for those situations when you don't have an underlying vtable interface to delegate to, but rather you want to implement this interface using other meta data. This meta data is nearly always going to be a COM "type library" of some sort, but it could be anything that can tell you how many bytes of parameters were pushed on the "stack" for a given method call.
Because COM interfaces use the __stdcall calling convention, in which the callee must know how many bytes were pushed on the stack, assembler was required. Therefore, this code is definitely not portable!
WriteOnly Interfaces
In my case, I've fallen in love with the idea of "WriteOnly" interfaces. WriteOnly interfaces are interfaces for which information only ever travels from the caller to the callee. In COM IDL, any WriteOnly interface could be expressed as:
interface IWriteOnlyDispatch : IUnknown
{
HRESULT InvokeMethod
[in] long nWhichMethod,
[in] SAFEARRAY(VARIANT) ArgList,
[out, retval] IWriteOnlyDispatch** ppNewContext);
};
The cool thing about such interfaces is that because information does not flow from the callee back to the caller, you can do all sorts of cool things:
On the other hand, the uncool thing about the above interface is that it loses all type information.
I started looking at COM+ Queued Components, but it has the annoying restriction that you cannot return interfaces from method calls, even if those interfaces that are themselves "WriteOnly". This means that every method call must have all context information, or all previous context must be established by other method calls on the same interface. This pretty much forces you to introduce "chronological dependencies" into your interfaces. That is, instead of being able to, say, have a "Connect" method that returns an "ISession" interface, you must instead write an interface where you "must" call "Connect" before you call, say, "SendQuote". It's really quite limiting and unnecessary - besides, it only works on Windows 2000 anyway ;-)
What I wanted is something that would automatically convert from a strongly typed vtable interface described in a type library into method calls on the IWriteOnlyDispatch interface described above. I started looking at the Universal Delegator described above, and saw that it didn't quite cater to my needs. However, I was able to look at the code and see how it worked. After that, CoProxyImpl wasn't very hard to write at all.
You must provide a suitable implementation of IProxyHook:
[
object,
uuid(E8F03FB0-2AB6-11d4-9D22-009027133993),
local
]
interface IProxyHook : IUnknown
{
// PARAMETER
// nWhichMethod [0..1021]
// Methods are numbered from the first non-IUnknown method.
// That is, the first method after "Release()" in the vtable is numbered 0.
// The second method is number 1.
// A maximum of 1021 methods are included (I think I read somewhere that
// the standard COM proxy supports 1024 vtable entries).
// pParams
// Pointer to the start of the parameters passed in to the method.
// pParams does not include the "this" pointer.
// HRESULTOUT
// What you want the HRESULT return value of the method to be
// Return value
// Number of bytes pointed to by pParams. This is the number of bytes that the
// client pushed on the stack, excluding the "this" pointer and the return address.
DWORD InvokeMethod(DWORD nWhichMethod,
void *pParams,
HRESULT *HRESULTOUT);
};
InvokeMethod must return the number of bytes pointed to by pParams. This number is the number of bytes that the client pushed on the stack before calling the method. The ASM code needs to know this so that it knows how many bytes to pop off the stack before returning to the caller. Returning an incorrect value from InvokeMethod will cause unpredictable results, but hopefully it will crash early with an access violation during testing! ;-). Always structure your implementations of IProxyHook so that they will know what to return from InvokeMethod, no matter what happens.
Don't blame me for this restriction - blame the __stdcall calling convention mandated for all COM interfaces. If the proverbial they had chosen __cdecl, this would not be an issue (in fact, no assembly code would have been necessary - you could just use the macros defined in <stdarg.h>).
Given the following COM interface:
interface IPerson : IUnknown
{
HRESULT SetDimensions([in] DWORD nHeight, [in] DWORD nWeight);
HRESULT SetName([in] BSTR pName);
};
Here is an implementation of InvokeMethod that won't crash:
STDMETHOD(InvokeMethod)(DWORD nWhichMethod,
void *pParams,
HRESULT *HRESULTOUT)
{
*HRESULTOUT = S_OK;
// sizeof two DWORDs is 8 bytes
if(nWhichMethod == 0)
return 8;
// sizeof(BSTR) == sizeof(short *) == sizeof(pointer) == 4 bytes
if(nWhichMethod == 1)
return 4;
// cannot return - we don't know how many parameters they pushed on the stack
// before calling us
AbortMessage("Invalid Method Called!");
exit(1);
}
Given that you have a suitable implementation of IProxyHook, you can create vtable interfaces by calling IProxyFactory::CreateProxy:
[
object,
uuid(E8F03FB9-2AB6-11d4-9D22-009027133993),
pointer_default(unique)
]
interface IProxyFactory : IUnknown
{
// Create an IUnknown instance, which, when QId
// for riid, will return a vtable interface that forwards
// all method calls to pProxyHook->InvokeMethod.
// Specify a non-null pUnkOuter to aggregate the
// the specified interface instead.
// PARAMETERS
// pUnkOuter
// 0 - create standalone
// non-zero to aggregate
// pProxyHook
// Your implementation that should be used for each method
// riid
// The IID that this interface should implement
// ppOut
// Upon return will point to an interface that responds
// to a QueryInterface for riid.
// Return Value
// Normal COM HRESULT code (e.g. E_OUTOFMEMORY)
HRESULT CreateProxy([in] IUnknown *pUnkOuter,
[in] IProxyHook *pProxyHook,
[in] REFIID riid,
[out, retval] IUnknown **ppOut);
};
A given "Proxy" can be instantiated either as a standalone object or it can be "aggregated". If pUnkOuter is null, the resultant IUnknown pointer will only respond to a QueryInterface request for riid. If you aggregate, then it obeys the normal rules for COM aggregation. That is, all non-IUnknown interfaces obtained from (*ppOut)->QueryInterface will forward their QueryInterface call to pUnkOuter. The "inner" IUnknown interface returned to you will respond to QueryInterface for riid.
CoProxyFactory is a CoClass that implements IProxyFactory:
[
uuid(E8F03FBB-2AB6-11D4-9D22-009027133993)
]
coclass CoProxyFactory
{
[default] interface IProxyFactory;
};
Note that it is not a ClassFactory, but rather it is a CoClass that implements IProxyFactory. Therefore, you use CoCreateInstance rather than CoGetClassObject to get a hold of the IProxyFactory implementation. This decision was made because CoProxyImpl was implemented in ATL, and it's a lot easier in ATL to just use the standard class factory object!
CoProxyFactory is a Singleton that aggregates the COM Free Threaded Marshaler. You can safely pass references to it between apartments in the same process - this is part of the semantics of "IProxyFactory", I've decided!
CoProxyFactory is your entry point into the CoProxyImpl .DLL.
I've tried to make the whole thing pretty lightweight - after all, it doesn't do much!
The chief overhead is that the ReleaseMinSize .DLL is 26Kb. Most of this consists of the statically generated 1021 entry vtable. This overhead could be reduced by instead generating the vtable entries by writing out the x86 op codes when the .DLL is first loaded. However, you'll only pay for this 26Kb overhead once per process, and I just couldn't be bothered doing this optimisation.
Each proxy object is 44 bytes in size, which I hope you'll agree, isn't very much!
The source includes a test harness - CoProxyImplTest - that tests the component for both aggregation and standalone use, and for concurrent access from multiple threads (this test is probably pretty ineffective - if you think of a way to improve it, please let me know :-) )
The test harness also shows you how to use the component.