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.
var
MyObject: TMyObject;
MyInterface: IMyInterface;
begin
// ...
MyInterface := MyObject as IMyInterface; // may raise EInvalidCast
MyInterface.Foo;
end;
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.
var
MyObject: TMyObject;
MyInterface: IMyInterface;
begin
...
MyInterface := IMyInterface(MyObject); // .NET specific
if Assigned(MyInterface) then
MyInterface.Foo;
end;
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
MyInterface.Foo;
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
MyInterface.Foo;
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:
type
TInterfaceRef = type of interface;
...
function Supports(const Instance: TObject; const IID: TInterfaceRef; out Intf): Boolean;
begin
Result := Instance is IID;
if Result then
Intf := Instance as IID;
end;
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;
var
InterfaceType: System.Type;
begin
Result := System.Type.GetTypeFromHandle(IID).IsInstanceOfType(Instance);
if Result then
begin
InterfaceType := System.Type.GetTypeFromHandle(IID);
if (not InterfaceType.IsInstanceOfType(Instance)) then
raise EInvalidCast.Create(SInvalidCast);
Intf := Instance;
end;
end;
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:
ldloc.1
ldtoken Obj2IntfCastNET.IMyInterface
call [mscorlib]System.Type [mscorlib]System.Type::GetTypeFromHandle([mscorlib]System.RuntimeTypeHandle)
pop
ldtoken Obj2IntfCastNET.IMyInterface
ldloca.s obj2
call bool Borland.Vcl.Units.SysUtils::Supports(object, [mscorlib]System.RuntimeTypeHandle, object&)
stloc.0
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
stloc.2
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:
{$IFDEF WIN32}
if Supports(MyObject,IMyInterface, MyInterface) then
{$ENDIF}
{$IFDEF CLR}
MyInterface := IMyInterface(MyObject);
if Assigned(MyInterface) then
{$ENDIF}
MyInterface.Foo;
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
Writeln('Yup');
3 comments:
Prices? Or prizes? ;o)
Chee Wee
PS: Price is what you pay for buying something. Prize is something you win.
Thanks, CW - fixed now.
I found this post very interesting.
Post a Comment