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
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;
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);
Assert will still fail, even though both variables are of type
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 fix is really simple: just
as-cast both variables to
IInterface. This normalizes them, and puts them on an even footing for comparison.
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.
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.