Joe White’s Blog

Life, .NET, and Cats


Interfaces and reference equality: beware

In Delphi for Win32, you can’t necessarily use the “equals” operator to tell whether two interfaces both point to the same object. The following code:

var
  Foo: IFoo;
  Bar: IBar;
begin
  Foo := MyObj as IFoo;
  Bar := MyObj as IBar;
  Assert(Foo = Bar);

will fail — even though both Foo and Bar point to the same object.

Let’s look at why that happens, and what you can do if you want a comparison that would succeed.

Memory layout of an interface reference

Note: If you don’t care about the technical details, and just want to know how to work around the problem, skip ahead to the “Workaround” section.

Let’s dig into the memory layout of an interface reference:

var
  Foo: IFoo;
begin
  Foo := TFoo.Create;

Memory layout of a Delphi/Win32 interface reference

An interface is a pointer to a pointer to an interface VMT. This means there’s a slot in the object’s instance data, just like any other instance field (except that it’s not visible in code), that automatically holds a VMT pointer. This field is automatically initialized during object construction. This is exactly the same way VMTs for Delphi classes are handled, except that the class’s VMT pointer is always at offset 0 within the block of memory allocated for the instance. Interface VMT pointers have to be at a nonzero offset, since offset 0 is already spoken for; they’re added at the end just like fields. (This will be familiar to those who remember Turbo Pascal objects, whose VMTs didn’t have to be at offset zero.)

And there’s the reason for the problem. Every interface you define gets a VMT all its own (even if it has the exact same methods as its parent interface, it still has its own VMT), so there has to be a slot in the instance data for the IFoo interface VMT, and another slot for the IBar interface VMT.

When you cast the object instance to IFoo, you’re really taking a pointer to the invisible “IFoo VMT pointer” field inside the object. When you cast to IBar, you’re taking a pointer to the “IBar VMT pointer” field. Those two are at different memory locations, so a reference-equality check will fail.

Bonus trivia: Because the interface pointer (which is also what’s passed as the implicit Self pointer when you call a method on the interface) isn’t at the same address as the object, the compiler has to generate a method stub that does a Self-pointer fixup, then call the actual method. The interface VMT points to this thunk method, not to the actual method body. You always wanted to know that, didn’t you?

A failed workaround: changing the variable types

So if the problem is the variables being of different types, let’s just make them both the same type:

var
  Foo: IInterface;
  Bar: IInterface;
begin
  Foo := MyObj as IFoo;
  Bar := MyObj as IBar;
  Assert(Foo = Bar);

The Assert will still fail, even though both variables are of type IInterface! Why?

That’s because the Foo variable still points to the IFoo VMT pointer inside the instance. IFoo descends from IInterface (as all interfaces do), so an IFoo reference is assignment-compatible with an IInterface variable, and is stored as-is in that variable — still pointing to the IFoo VMT pointer.

So even though a reference may be stored in a variable of type IInterface, it does not necessarily point to the IInterface VMT pointer for that class. This becomes a particular problem when you add different interfaces to an IInterfaceList: they’re stored in a list of IInterface variables, but they’re not canonicalized to point to the IInterface VMT pointer. So, for example, TInterfaceList.IndexOf can give you the wrong answer, if you’re not careful about the types of your interface references.

The workaround

The fix is really simple: just as-cast both variables to IInterface. This normalizes them, and puts them on an even footing for comparison.

When you as-cast to an interface type, you get back a pointer to the interface VMT pointer for that exact interface — none of that weirdness with assignment-compatibility and putting an IFoo reference into an IInterface variable: when you cast to IInterface, by golly, you get an IInterface all the way. QueryInterface and SysUtils.Supports will also canonicalize to the interface you specify.

(You could even use something other than IInterface, as long as you know both instances will support it. But every interfaced object can be cast to IInterface, so using IInterface makes for a nice general solution.)

So to revisit the code at the top of the article, the correct way to compare the interfaces would be something like this:

var
  Foo: IFoo;
  Bar: IBar;
begin
  Foo := MyObj as IFoo;
  Bar := MyObj as IBar;
  Assert(Foo as IInterface = Bar as IInterface);

Now, the assertion will pass.

9 Responses to “Interfaces and reference equality: beware”

  1. Joe White’s Blog » Blog Archive » DUnitLite 0.5 released Says:

    […] Should.ReferTo now works with interfaces, and can mix and match between objects and interfaces. It also correctly works around the interface reference-equality problem. […]

  2. oxffff Says:

    HI, Joe White.

    :)

    And what about the next case?

    SomeA=interface
    [’{008F39E4-9C7F-4ED8-AD56-56CD7FAD6477}’]
    procedure Method1;
    end;

    TABC=class(TInterfacedObject,SomeA)
    protected
    function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;
    procedure Method1;
    public
    end;

    procedure TForm1.Button1Click(Sender: TObject);
    var a:IUnknown;
    begin
    a:=TABC.create;
    if not (A=a as someA as IUnknown) then showmessage(’Failed’);
    end;

    procedure TABC.Method1;
    begin
    //
    end;

    function TABC.QueryInterface(const IID: TGUID; out Obj): HResult;
    var A:TInterfacedObject;
    begin
    Iunknown(Obj):=TInterfacedObject.Create;
    Result:=S_OK;
    end;

    :)

  3. Joe Says:

    Uh-huh. If you’re writing code that returns new instances from QueryInterface, you deserve to have your tests fail.

  4. oxffff Says:

    You assert

    >In Delphi for Win32, you can’t necessarily use the “equals” operator to tell whether two >interfaces both point to the same object.

    And you offer the solution, that is to cast both interfaces to IInterface

    >Assert(Foo as IInterface = Bar as IInterface);

    My sample show that although both Interfaces (First IInterface and SomeA) use the same object
    (you may find out this by inspecting the hidden compiler generated stub that fix EAX(in register calling convention), i.e. self param of interface Method

    But casting both Interfaces to Iunknown fails because of overridden QueryInterface method of SomeA Interface, but both interfaces still use the same object.

    To force your solution fail you may

    1. create new instance, i.e. delegate implementation to an other object.
    2. delegate implementation to inner(aggregated) object.
    3. suppress returning any interfaces from QueryInterface and get exception.

    :)

    Best regards Sergey Antonov.

    This sample shows that if QueryInterface is override

  5. Hallvard Vassbotn Says:

    If the as-IInterface cast produce (potential) problems, you could move down a level and explicitly look at the thunka to compiler creates to find the implementing object, like in my hack:

    http://hallvards.blogspot.com/2004/07/hack-7-interface-to-object-in-delphi.html

  6. Uwe Raabe Says:

    Well, the cast to IInterface may not help if you use the implements feature. In the example below the assertion will fail. This scenario is of quite common use and not somewhat constructed.


    uses classes;

    type
    TImplementor = class(TInterfacedPersistent, IStreamPersist, IInterfaceList)
    private
    FList: IInterfaceList;
    function GetList: IInterfaceList;
    protected
    property List: IInterfaceList read GetList implements IInterfaceList;
    public
    procedure LoadFromStream(Stream: TStream);
    procedure SaveToStream(Stream: TStream);
    end;

    function TImplementor.GetList: IInterfaceList;
    begin
    if FList = nil then
    FList := TInterfaceList.Create;
    result := FList;
    end;

    procedure TImplementor.LoadFromStream(Stream: TStream);
    begin
    end;

    procedure TImplementor.SaveToStream(Stream: TStream);
    begin
    end;

    var
    intf1: IStreamPersist;
    intf2: IInterfaceList;
    obj: TImplementor;
    begin
    obj := TImplementor.Create;
    try
    intf1 := obj;
    intf2 := obj;
    Assert((intf1 as IInterface) = (intf2 as IInterface), 'interfaces not equal');
    finally
    obj.Free;
    end;
    end;

  7. Marc Rohloff Says:

    Another way to subvert the comparison is to use the ‘implements’ keyword.
    For example:
    TBar = class(IFoo, IBar)
    property SubObject:IFoo implements IFoo;
    Then the comparison
    (Bar as IFoo) as IUnknown = Bar as IUnknown will also fail.

    However Microsoft’s COM guidelines are very specific that casting an object to a given interface, directly or indirectly, should always return the same pointer. In the above case you should override QueryInterface on the subobject to return the parent’s IUnknown.

  8. Joe Says:

    Marc is correct. If you’re really going to do the thing correctly, any interface should be able to QueryInterface to any other.

    So if you’re using the “implements” keyword, the object being targeted by “implements” really should descend from TAggregatedObject instead of TInterfacedObject. From the comments in System.pas:

    TAggregatedObject and TContainedObject are suitable base classes for interfaced objects intended to be aggregated or contained in an outer controlling object. When using the “implements” syntax on an interface property in an outer object class declaration, use these types to implement the inner object.

    Interfaces implemented by aggregated objects on behalf of the controller should not be distinguishable from other interfaces provided by the controller. … TAggregatedObject simply reflects QueryInterface calls to its controller.

  9. Chris Morgan Says:

    Please can you test your CSS layout with Internet Explorer.

Leave a Reply

XHTML: You can use these tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>


Joe White's Blog copyright © 2004-2008. Portions of the site layout use Yahoo! YUI Reset, Fonts, and Grids.
Proudly powered by WordPress. Entries (RSS) and Comments (RSS).