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.

There are no responses to “Interfaces and reference equality: beware” yet.

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-2011. Portions of the site layout use Yahoo! YUI Reset, Fonts, and Grids.
Proudly powered by WordPress. Entries (RSS) and Comments (RSS). Privacy policy