Thursday, January 11, 2007

Fun with V-Tables and COM

If you’ve ever imported a Visual Basic (VB) ActiveX control with Delphi chances are you’ve called a method and gotten an AV. Fortunately this long standing bug having to do with sparse v-tables has been fixed in Delphi 2005 Update 2. But it turns out that VB isn’t the only language that generates sparse v-tables in type libraries. C# or any .NET language can do this rather easily.

For those of you who watch Star Trek instead of studying assembly you might be wondering what a v-table is. A v-table is an array of function pointers where each method is a 4 byte offset from the previous method. A sparse v-table has spaces in that list of offsets. IUnknown and IDispatch are the foundation of COM, and have the following v-table offsets.


Method Name V-Table Offset

IUnknown.QueryInterface 0
IUnknown.AddRef 4
IUnknown.Release 8

IDispatch.GetTypeInfoCount 12
IDispatch.GetTypeInfo 16
IDispatch.GetIDsOfNames 20
IDispatch.Invoke 24


These are defined in Delphi as:


IInterface = interface
['{00000000-0000-0000-C000-000000000046}']
function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;
function _AddRef: Integer; stdcall;
function _Release: Integer; stdcall;
end;

IDispatch = interface(IUnknown)
['{00020400-0000-0000-C000-000000000046}']
function GetTypeInfoCount(out Count: Integer): HResult; stdcall;
function GetTypeInfo(Index, LocaleID: Integer; out TypeInfo): HResult; stdcall;
function GetIDsOfNames(const IID: TGUID; Names: Pointer;
NameCount, LocaleID: Integer; DispIDs: Pointer): HResult; stdcall;
function Invoke(DispID: Integer; const IID: TGUID; LocaleID: Integer;
Flags: Word; var Params; VarResult, ExcepInfo,
ArgErr: Pointer): HResult; stdcall;
end;


Not that much fun, but I’m getting to the fun part so keep reading. Here is a C# interface with a sparse v-table:


[ComVisible(true)]
[GuidAttribute("7F328E68-C0FA-482D-8700-5D605DE9E4B5")]
public interface IFoo
{
void A();
void B();
[ComVisible(false)]
void C();
string D(string S);
[ComVisible(false)]
void E();
}


Compile this and run tlbexp.exe to generate a type library then view this interface with a type library viewer that shows the TFuncDesc.oVft such as Reflection.exe and you will see something like this:


Method Name V-Table Offset

IFoo.A 28
IFoo.B 32
IFoo.D 40


See the space? It’s right there between the 32 and 40. I told you this would be fun! Remember there was a method called C? It’s gone, but not really since the slot for method C is still there. When importing this with the version of tlibimp.exe in Delphi 2005 Update 2 a Pascal interface will be defined as:


IFoo = interface(IDispatch)
['{7F328E68-C0FA-482D-8700-5D605DE9E4B5}']
procedure A; safecall;
procedure B; safecall;
procedure GhostMethod_IFoo_36_1; safecall;
function D(const S: WideString): WideString; safecall;
end;


Notice the method GhostMethod_IFoo_36_1 is simply a space holder for method C. But what happened to Method E? Ah, this is the tricky part and is best explained with more source:


IGoo = interface(IDispatch)
['{A9EC35FF-EA31-4B94-ADDD-D9E64C518862}']
procedure F; safecall;
procedure G; safecall;
end;


Method Name V-Table Offset

IGoo.F 28
IGoo.G 32


Since the interface was created by .NET the parent interface is IDispatch and there is nothing to do, but if this interface was created with VB there’d be a few more things going on when generating the v-table for a child interface. Let’s say you created a child interface in VB from IFoo (I’m going to write this in C# because I can):


[ComVisible(true)]
[GuidAttribute("A9EC35FF-EA31-4B94-ADDD-D9E64C518862")]
public interface IGoo : IFoo
{
void E();
void F();
}


Method Name V-Table Offset

IGoo.F 48
IGoo.G 52


And after importing the type library with tlibimp.exe, your Pascal interface for IGoo would be:


IGoo = interface(IFoo)
['{A9EC35FF-EA31-4B94-ADDD-D9E64C518862}']
procedure GhostMethod_IFoo_44_1; safecall;
procedure F; safecall;
procedure G; safecall;
end;


Here you see that the method GhostMethod_IFoo_44_1 is a place holder in the v-table for method E.
All of this gets really interesting when the interface in the type library implement IUnknown or IDispatch (take a look at the type library for mscorlib and do a search for GetIDsOfNames, it'll surprise you how many times it shows up).

So the moral of the story is if you get an AV when calling a COM object, the interface may not be defined correctly so do some investigation with what I described here.

No comments:

Post a Comment