Wednesday, January 19, 2005

Interface-to-interface casts

When you already have an interface reference and want to (try and) convert it into another interface reference, you have to use some kind of cast or conversion function. Just as with object-to-interface casts, there are a number of options.

The possibilities are hard-casts, as-casts, is-checks, the Supports function and the QueryInterface method. As we shall see not all of these are available in both Win32 and .NET - and some of them have different behaviour.

Interface as-casts
In Win32, if you try to as-cast to an interface that is not implemented, an exception is raised. In Delphi 8 for .NET, nil is returned instead. The behaviour should be the same on both platforms. This bug has been fixed in Delphi 2005.

Interface hard-casts
The compiler allows hard-cast syntax from an interface reference to another interface in both Win32 and .NET. While the .NET cast is safe and performs a logical conversion (returning nil if it fails), the Win32 cast is completely unsafe and does normally not do what you want. You are basically telling the compiler that the interface reference on the right side already contains a reference of the cast-to interface type (and not the declared type). In general, this type of cast is not currently useful in Win32.

Interface is-checks
For some reason, in Win32, is-checks are not supported on interfaces. To check that an interface reference implements another interface, you have to use Supports or call QueryInterface directly. .NET however, does support is-checks on interface references.

Supports function
Both the Win32 and .NET platforms support (sic) using Supports to check that an interface reference implements another interface - and to return this new interface reference. On .NET the Supports function has the same run-time issues as when casting from Object to Interface (in fact, it is exactly the same Supports function overload that is used - any interface reference is compatible with TObject); it is relatively slow.

QueryInterface
Specific to Win32, all interfaces have a QueryInterface method inherited from the base interface IInterface (or IUnknown). Under the hood, the Supports function (and even the is and as operators) calls QueryInterface. There is nothing stopping you from calling QueryInterface directly, of course, but then you have tied yourself to Win32 and the code needs to be changed to work in .NET.

Code sample
Let's write a small sample that demonstrates all the different ways to cast and check from one interface reference to another.

program TestIntf2Inf;
{$APPTYPE CONSOLE}

uses
SysUtils;

type
IMyInterface = interface
['{99D91C44-BCE7-4D35-A661-DE32E8AE56FC}']
procedure Foo;
end;
IMyInterface2 = interface
['{2E200094-0643-46C8-87AF-AAB0A1F5801D}']
procedure Bar;
end;
INotImplemented= interface
['{BAEE6F63-FF47-4877-9657-443B6D1555FA}']
procedure Zoo;
end;
TMyObject = class(TInterfacedObject,
IMyInterface, IMyInterface2)
procedure Foo;
procedure Bar;
end;

procedure TMyObject.Foo;
begin
Writeln(ClassName, '.Foo!');
end;

procedure TMyObject.Bar;
begin
Writeln(ClassName, '.Bar');
end;

procedure Foo(const I: IMyInterface);
var
MyInterface2: IMyInterface2;
NotImplemented: INotImplemented;
begin
// Win32 suppports as and Supports.
// Hard-cast is unsafe, is does not compile
// .NET suppports as, Supports, Hard-cast and is
MyInterface2 := I as IMyInterface2;
MyInterface2.Bar;
// hard-cast .NET: safe, returns nil on failure
// hard-cast Win32: Unsafe, compiles but may crash/fail
MyInterface2 := IMyInterface2(I);
// .Win32: Calls TMyObject.Foo!
// .NET: Calls TMyObject.Bar
MyInterface2.Bar;
try
NotImplemented := I as INotImplemented;
if not Assigned(NotImplemented) then
writeln('D8 .NET bug: as returns nil'+
' - should raise exception');
NotImplemented.Zoo;
except
{$IFDEF Win32}
on E: EIntfCastError do
{$ENDIF}
{$IFDEF CLR}
on E: InvalidCastException do
{$ENDIF}
writeln('As designed: ', E.ClassName, E.Message);
on E: Exception do
writeln('Bug: ', E.ClassName, E.Message);
end;
// Supports works in both Win32 and
// .NET, but is relativly slow in .NET
if Supports(I, IMyInterface2, MyInterface2) then
MyInterface2.Bar;
{$IFDEF CLR}
// intf is intf is not supported in Win32
if I is IMyInterface2 then
writeln('interface is interface suppported in .NET');
{$ENDIF}
{$IFDEF Win32}
if I.QueryInterface(IMyInterface2, MyInterface2) = 0 then
MyInterface2.Bar;
Writeln('QueryInterface supported in Win32');
{$ENDIF}
end;

var
I : IMyInterface;
begin
I := TMyObject.Create;
try
Foo(I);
except
on E: Exception do
writeln(E.Message);
end;
readln;
end.


This code should compile and run from D7, D8.NET, D2005 Win32 and D2005 .NET. The following is the output in each case.


Output Delphi 8 .NET:
TMyObject.Bar
TMyObject.Bar
D8 .NET bug: as returns nil - should raise exception
Bug: NullReferenceExceptionObject reference not set to an instance of an object.
TMyObject.Bar
interface is interface suppported in .NET


Output Delphi 2005 .NET:
TMyObject.Bar
TMyObject.Bar
As designed: InvalidCastExceptionSpecified cast is not valid.
TMyObject.Bar
interface is interface suppported in .NET


Output Delphi 2005 Win32 and D7:
TMyObject.Bar
TMyObject.Foo!
As designed: EIntfCastErrorInterface not supported
TMyObject.Bar
TMyObject.Bar
QueryInterface supported in Win32


Specifically notice that the D8 as-cast bug has been fixed in D2005 and that Win32 hard-casts have strange effects, calling the wrong method! IMO, this is not a bug, but a consequence of Win32 hard-cast semantics.


When hard-casting on Win32 you are essentially telling the compiler; "Forget about the declared type of this variable and treat it like it was this type instead". You have to know what you're doing. I really cannot see any case were the current Win32 interface-to-interface cast is useful, so maybe Borland could beef up the dcc32 compiler to make it work like it does in dccil in a future version…? Determined hackers would still be able to do a raw binary cast by casting to Pointer first.


Hard-casting on .NET is still safe and normally performs a logical conversion, not a binary re-interpretation.

4 comments:

Anonymous said...

Very useful! I have used intefaces for ages and I wasn't fully aware of some of what you described.

I have been burnt by the hard-cast in Win32 a few times. I agree the semantics of the Win32 compiler should change to much those of the .NET compiler. At least a warning would be useful.

Paul Milenkovic said...

Deepak Shenoy indicates that you are the originator of GetImplementingObject() which casts an interface reference to the underlying object.

I have found a use for that which I describe here http://www.medsch.wisc.edu/~milenkvc/pdf/multievent.htm for allowing Delphi ActiveX object to permit multiple event listeners. ActiveX controls written in Delphi won't support events under Matlab Version 7 and under the Mark Hammond Python ActiveX module because those containers attempt to register multiple listeners and the default behavior of any ActiveX control written in Delphi is to accept only one listener.

Hallvard Vassbotn said...

Yes, it's correct that I wrote GetImplementingObject. I intended to blog about it, too (I thought I had actually) - but I've been too busy (both professionally and privatly) the last 6 months to do much blogging.

I'm glad you found a use for my hack, but it would of course be better if Borland could fix this in the DAX framework. I would suggest you make a report about it in QC.

Hallvard Vassbotn said...

Actually I did blog about it here... ;)



Copyright © 2004-2007 by Hallvard Vassbotn