Introduction

Before I can go into the details of the Remoting system as implemented by NexusRemoting it's important to have a common understanding of Interfaces in Delphi. This post will explain the fundamental concepts involved with Interfaces in Delphi.

When reading the above paragraph closely it should jump out that I'm qualifying "Interfaces" with "in Delphi". Interfaces as a general concept are implemented by many programming languages and platforms in different ways. Within the context of Delphi Interfaces are based on the definition of interfaces from COM.

COM (Component Object Model) refers to a multitude of technologies including OLE, OLE Automation, ActiveX, COM+ and DCOM. All of these technologies are based on a common definition of how an interface is represented in memory and how methods of an interface are called. At the language and compiler level Delphi follows this specification, but it does not in any way depend on any OS API or other external library to implement Interfaces. Interfaces, in Delphi happen to share the memory layout and semantics of Interfaces in COM which makes it easy to use this language feature when interacting with COM, but they don't depend on COM and are as such platform-independant.

What is an interface?

An interface variable is a pointer to a pointer to an array of function pointers.

type
Intf = ^^array[..] of procedure;

Each of these function pointers takes as first (implicit) parameter the value of the interface variable.

type
IntfProcedure = procedure(Self: Intf; ...);

The number of function pointers and their signatures depend on the specific type of the interface. A specific type of interface is identified by it's GUID (which for the purpose of this discussion we can simply define as "a unique number"). An interface type can inherit from an existing interface type in which case it inherits the list of function signatures from it's parent and any member functions of the new interface are added to the end of the list. All interfaces that do not explicitly inherit from another interface implicitly inherit from IInterface (called IUnknown in COM).

type
IInterface = interface
['{00000000-0000-0000-C000-000000000046}']
function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;
function _AddRef: Integer; stdcall;
function _Release: Integer; stdcall;
end;

That means the ulimate parent type of any interface is always IInterface. Which means the first 3 entries in the array of function pointers always have the signatures of these 3 methods.

QueryInterface in IInterface is what fundamentally allows a single object to implement multiple different interfaces. As all interfaces always contain QueryInterface it is always possible to find out if the object instance behind an interface instance also implements a specific other interface and to retrieve an interface instance for it. It is a basic requirement that the interfaces of an object exhibit reflexive, symmetric and transitive properties. Specifically that means:

Reflexive

A call to QueryInterface using the same GUID as the interface it is called on must return the same interface instance.

var Intf1, Intf2: ISomeInterface;
...
Assert(Inft1.QueryInterface(ISomeInterface, Intf2) = S_OK);
Assert(Intf1 = Intf2);

Symmetric

If Interface B can be retrieved from Interface A via QueryInterface then Interface A must be retrievable from Interface B as well.

var 
IntfA1, IntfA2 : IInterfaceA;
IntfB : IInterfaceB;
...
Assert(Inft1A.QueryInterface(IInterfaceB, IntfB) = S_OK);
Assert(IntfB.QueryInterface(IInterfaceA, IntfA2) = S_OK);
Assert(IntfA1 = IntfA2);

Transitive

If Interface B can be retrieved from Interface A and Interface C can be retrieved from Interface B, then Interface C must be retrievable from Interface A.

var 
IntfA : IInterfaceA;
IntfB : IInterfaceB;
IntfC1, IntfC2 : IInterfaceA;
...
Assert(InftA.QueryInterface(IInterfaceB, IntfB) = S_OK);
Assert(IntfA.QueryInterface(IInterfaceC, IntfC1) = S_OK);
Assert(IntfB.QueryInterface(IInterfaceC, IntfC2) = S_OK);
Assert(IntfC1 = IntfC2);

Another important rule is that all objects which implement interfaces must implement IInterface. Meaning a call to QueryInterface with the GUID of IInterface must always succeed. At first glance this rule might appear redundant as all interfaces need to inherit (directly or indirectly) from IInterface, but as there is no rule that requires that all ancestors of an interface need to be implemented by the same object it becomes clear that this rule does indeed need to be stated specifically.

type
IInterfaceA = interface(IInterface)
...
end;
IInterfaceB = interface(IInterfaceA)
...
end;
var
IntfA : IInterfaceA;
IntfB : IInterfaceB;
Intf : IInterface;
...
IntfB.QueryInterface(IInterfaceA, IntfA);

The call above is allowed to fail, there is no rule that requires an object implementing IInterfaceB to also implement IInterfaceA. OTOH, it might succeed as there is no rule prohibiting the implementation of ancestors either.

  Assert(IntfB.QueryInterface(IInterface, Intf) = S_OK);

But this call must always succeed. All objects implementing interfaces need to implement IInterface.

Reference Counting

All interfaces are reference counted. This is implemented by calls to AddRef and Release. In Delphi the compiler already generates code to call AddRef and Release as appropriate which is the reason why they have been renamed to _AddRef and _Release to make it clear that these methods should usually not be called directly. It is still important to be aware of the rules behind AddRef and Release:

  • If a copy is made of an interface instance AddRef must be called on that instance.
  • Release must be called on an interface instance before it is overwritten or goes out of scope.
  • AddRef and Release must be called on the specific interface instance which is being referenced. Objects which implement multiple different interfaces are allowed to keep per-interface reference counts which could be used to manage the lifetime of resource which are only required for specific interfaces.
  • Functions (no matter if global functions/procedures or member methods of objects or interfaces) that return interface instances (be it as return value or "out" or "var" parameter) must increment the reference count of the returned interface by calling AddRef. As an example, the interface instance returned by QueryInterface via the "out Obj" parameter has already be incremented.

Objects that implement interfaces are supposed to free themselves once their reference count reaches 0.

This is not an absolute requirement. It is possible to always return -1 from AddRef and Release to signal that reference counting doesn't take place. But in most cases this is not recommended as some other means of freeing the object will have to be exposed and users of interfaces normally assume that not having any references to an object anymore will result in the object being freed.

Objects that implement interfaces are not allowed to be freed while there are any outstanding references to it. Otherwise, even if the user wouldn't make any calls against the interface anymore after the time the object was freed, the compiler will still emit automatic calls to Release when interface variables run out of scope which would end up calling Release on an already freed object.

It is one of the reasons why it is strongly recommended to implement proper reference counting instead of always returning -1. There are exceptions to this rule, but whenever it's broken special care needs to be taken.

More to come...

With the fundamentals now covered, we'll start looking at more advanced interface topics in the next post.

Home | Community | Blogs | Thorsten's Blog