Tuesday, November 16, 2004

Object-to-interface casts

There are several ways to check if an object reference implements an interface or not - but there are some platform differences. The as-cast works (almost) identically in both Win32 and .NET.

MyObject: TMyObject;
MyInterface: IMyInterface;
// ...
MyInterface := MyObject as IMyInterface; // may raise EInvalidCast

The platform difference is that on Win32, the declared object type must implement the IInterface (or IUnknown) interface (as TInterfaceObject does, for instance), while in .NET any object type will do. If the cast fails, an exception will be raised. This is often undesirable - often you need to treat objects differently depending on if they implement a specific interface or not, and handling exceptions is too slow and cumbersome. On .NET you can cast from any object to any interface, using a hard-cast syntax. If the cast fails, nil is returned instead.

MyObject: TMyObject;
MyInterface: IMyInterface;
MyInterface := IMyInterface(MyObject); // .NET specific
if Assigned(MyInterface) then

Currently, the Win32 compiler does not support this cast in a generic way. It does support it in the specific case when the cast can be determined to be valid at compile-time. This can be done when the declared object type statically implements the given interface. If this is not the case, the cast fails with a compile-time error; [Error]: Incompatible types: 'IMyInterface' and 'TInterfacedObject' To check if an object supports an interface without raising exceptions, you can first cast it to IInterface then call the QueryInterface method. This only exists on Win32:

 Intf := IInterface(MyObject); 
if Intf.QueryInterface(IMyInterface, MyInteface) = 0 then

The Supports routine in the SysUtils unit gives is a more convenient and cross-platform way of doing the same thing:

 if Supports(MyObject,IMyInterface, MyInteface) then 

Supports is also available on .NET, but it is implemented in a way that makes it much slower than using the (currently) .NET-specific hard-cast. This is because the .NET version is implemented in terms of the new 'type of interface' construct:

TInterfaceRef = type of interface;
function Supports(const Instance: TObject; const IID: TInterfaceRef; out Intf): Boolean;
Result := Instance is IID;
if Result then
Intf := Instance as IID;

The 'type of interface' construct is pretty neat, actually. It allows you to pass any interface-type as a parameter to a method. This is used instead of a TGUID as an identifier or handle to the given interface. The Supports function itself compiles into the following IL:

.method public hidebysig static bool Supports(object Instance, [mscorlib]System.RuntimeTypeHandle IID, object& Intf) cil managed 
// Code Size: 50 byte(s)
.maxstack 3
.locals (
bool flag1,
[mscorlib]System.Type type1)
L_0000: ldarg.1
L_0001: call [mscorlib]System.Type [mscorlib]System.Type::GetTypeFromHandle([mscorlib]System.RuntimeTypeHandle)
L_0006: ldarg.0
L_0007: callvirt instance bool [mscorlib]System.Type::IsInstanceOfType(object)
L_000c: stloc.0
L_000d: ldloc.0
L_000e: brfalse.s L_0030
L_0010: ldarg.2
L_0011: ldarg.1
L_0012: call [mscorlib]System.Type [mscorlib]System.Type::GetTypeFromHandle([mscorlib]System.RuntimeTypeHandle)
L_0017: stloc.1
L_0018: ldloc.1
L_0019: ldarg.0
L_001a: callvirt instance bool [mscorlib]System.Type::IsInstanceOfType(object)
L_001f: brtrue.s L_0029
L_0021: ldarg.0
L_0022: ldloc.1
L_0023: call object Borland.Delphi.Units.System::@CreateCastException(object, [mscorlib]System.Type)
L_0028: throw
L_0029: ldarg.0
L_002a: castclass object
L_002f: stind.ref
L_0030: ldloc.0
L_0031: ret

Quite a bit of code!

With a little help from Reflector, I've found that it corresponds to something like this in Delphi code:

function Supports(Instance: TObject; IID: RuntimeTypeHandle; out Intf: TObject): boolean; 
InterfaceType: System.Type;
Result := System.Type.GetTypeFromHandle(IID).IsInstanceOfType(Instance);
if Result then
InterfaceType := System.Type.GetTypeFromHandle(IID);
if (not InterfaceType.IsInstanceOfType(Instance)) then
raise EInvalidCast.Create(SInvalidCast);
Intf := Instance;

Notice that the code is a little redundant. The precence of both the is-check and the as-check makes the compiler emit calls to GetTypeFromHandle and IsInstanceOfType twice each. The exception will never be raised in this method. So there is a little room for improvement here. I think that passing 'type of interface' parameters as System.Type instead of RuntimeTypeHandle would also make it a little more efficient - in most (all?) cases the call site has a Type reference anyway and first has to convert it into a RuntimeTypeHandle. The site calling Supports yields this IL:

ldtoken Obj2IntfCastNET.IMyInterface
call [mscorlib]System.Type [mscorlib]System.Type::GetTypeFromHandle([mscorlib]System.RuntimeTypeHandle)
ldtoken Obj2IntfCastNET.IMyInterface
ldloca.s obj2
call bool Borland.Vcl.Units.SysUtils::Supports(object, [mscorlib]System.RuntimeTypeHandle, object&)

By contrast, performing the same operation by using the object-to-interface hard-cast, gives this very simple and efficient IL:

    ldloc.s obj2
   isinst Obj2IntfCastNET.IMyInterface

Can you guess which is faster? No prizes for this one! So you can use the cross-platform Supports function - which is fairly efficient in Win32, but relatively slow in .NET, or the .NET platform specific and efficient hard-cast. Most of the time, the slow .NET version of Supports does not really matter, but for some performance critical points, you might want to use conditional compilation to select the best solution for each platform, like this:

if Supports(MyObject,IMyInterface, MyInterface) then
MyInterface := IMyInterface(MyObject);
if Assigned(MyInterface) then

I hope that a future version of the Win32 compiler can be extended to support hard-casts from object-to-interface with the same semantics as on .NET. The cast should return nil if the object does not support the interface (yes, this has been reported to Borland). In .NET you can also use the 'is' operator to check if an object implements an interface or not. This is not currently supported in Win32.

 if MyObject is IMyInterface then 


Anonymous said...

Prices? Or prizes? ;o)

Chee Wee

PS: Price is what you pay for buying something. Prize is something you win.

Hallvards New Blog said...

Thanks, CW - fixed now.

Michele said...

I found this post very interesting.

Copyright © 2004-2007 by Hallvard Vassbotn