In this post I'll take a look at a concept called interface aggregation.

It's the last major piece required to understand the design and implementation of NexusRemoting and is also a useful technique to understand in general.

I will be using a lot of the concepts explained in the last 2 posts, if you haven't read the Interface Fundamentals and Advanced Interface Usage and Patterns yet I would recommend you check these posts out first to make sure you understand the terms and concepts used in this post.

As already explained, the implementation of different interfaces that make up a single instance are independent of each other. Not only is it possible to use different methods of the same class to implement the same method in 2 different interfaces, but it's also possible to have multiple classes implement complete interfaces and then aggregate these classes in a way that makes it appear to the outside as if all interfaces were implemented by the same object.

The Host

The first thing we need is a host class. This host is what provides the main implementation for the 3 IInterface methods that will be externally visible. The following is one of many possible designs for an aggregation host.

type
TnxAggregationHost = class(TnxInterfacedObject, InxInterface)
protected {private}
ahAggregatedInterfaces: array of InxInterface;
protected
procedure ahAddAggregatedInterface(const aInterface: InxInterface);

{--- InxInterface ---}
function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;
end;

procedure TnxAggregationHost.ahAddAggregatedInterface(const aInterface: InxInterface);
begin
SetLength(ahAggregatedInterfaces, Succ(Length(ahAggregatedInterfaces)));
ahAggregatedInterfaces[High(ahAggregatedInterfaces)] := aInterface;
end;

function TnxAggregationHost.QueryInterface(const IID: TGUID; out Obj): HResult;
var
i: Integer;
begin
Result := inherited QueryInterface(IID, Obj);
if not Succeeded(Result) then
for i := Low(ahAggregatedInterfaces) to High(ahAggregatedInterfaces) do begin
Result := ahAggregatedInterfaces[i].QueryInterface(IID, Obj);
if Succeeded(Result) then
Exit;
end;
end;

For AddRef and Release the normal implementation is retained which simply increments and decrements a counter and frees the object when the reference count reaches 0. They don't need to be re-declared as TnxInterfacedObject already implements them.

ahAddAggregatedInterface is a function which can be used to add additional interfaces to the ahAggregatedInterfaces array.

QueryInterface is the really interesting part. Any time a specific interface is requested (identified by it's IID, which is the GUID that uniquely identifies the interface), first the inherited implementation of QueryInterface is called which checks if the current class implements the requested interface. Only if that was not successful will the aggregated interfaces be checked. This ensures that any interface that the host itself implements has priority.

Because the host always implements InxInterface (InxInterface is only an type alias of either IInterface or IUnknown, depending on Delphi version) it is impossible any of the aggregated interfaces will be called with the IID for IInterface as the QueryInterface on the host will always succeed for this IID.

 If we would be implementing the aggregated interfaces just using TnxInterfacedObject, there would be serious problems:

  • The reference count of the host and the aggregated interfaces would be independent. It would be possible for the host to be freed because at some point in time there might only be a reference to interfaces implemented by the aggregated interfaces.
  • It would violate the symmetric and transitive properties because there would be no way to get back from this aggregated interface to the host or to one of the other aggregated interfaces

The Aggregateable Object

To prevent these problems a specific pattern needs to be followed. It is possible to implement a base class once which can be used as ancestor for classes that should be aggregateable. It is not an requirement that all aggregated objects derive from this specific base class. Any object that implements this specific pattern will work.

type
TnxAggregatableObject = class(TnxInterfacedObject, InxInterface)
{ Important: when deriving do NOT specify InxInterface again.
For correct functioning InxInterface and other
interfaces need to use different implementations of
the 3 IUnknown methods. }
protected {private}
aoHost : Pointer{InxInterface};
function aoGetHost: InxInterface;
protected
{--- InxInterface methods ONLY for InxInterface ---}
{ Override this InternalQueryInterface method to add additional interfaces }
function InternalQueryInterface(const IID: TGUID; out Obj): HResult; virtual; stdcall;
function InternalAddRef: Integer; virtual; stdcall;
function InternalRelease: Integer; virtual; stdcall;
function InxInterface.QueryInterface = InternalQueryInterface;
function InxInterface._AddRef = InternalAddRef;
function InxInterface._Release = InternalRelease;

{--- InxInterface methods for all interfaces EXCEPT InxInterface ---}
{ Do NOT override this QueryInterface to add additional interfaces, it will mess
with aggregated objects support. }
function QueryInterface(const IID: TGUID; out Obj): HResult; virtual; stdcall;
function _AddRef: Integer; virtual; stdcall;
function _Release: Integer; virtual; stdcall;
public
constructor Create(const aHost: InxInterface; aKeepAlive: Boolean = False);
property Host: InxInterface
read aoGetHost;
end;

The constructor of this class takes a reference to the Host. If this would be a normal strong reference we would end up with a circular reference between host and aggregated interface, keeping both alive indefinitely. To prevent this the field that holds the host reference is using a weak reference by being defined as just a Pointer.

function TnxAggregatableObject.aoGetHost: InxInterface;
begin
Result := InxInterface(aoHost);
end;

constructor TnxAggregatableObject.Create(const aHost: InxInterface; aKeepAlive: Boolean);
begin
aoHost := Pointer(aHost);
inherited Create(aKeepAlive);
end;

The class implements 2 sets of the 3 IInterface methods. One which uses the standard names and will automatically be used for any interface implemented by a derived class. The other one which is specifically redirected to InxInterface.

It is very important that no derived class ever explicitly specified InxInterface as an implemented interface again. This would cause the compiler to match the interface methods to instance methods simply by name which would mean that InxInterface would use the same methods that are supposed to be used only by all other interfaces.

There will always only be a single reference to InxInterface of an aggregated object, the one stored inside the Host which is never exposed.

function TnxAggregatableObject.InternalAddRef: Integer;
begin
{ handled ourself }
Result := inherited _AddRef;
end;

function TnxAggregatableObject.InternalQueryInterface(const IID: TGUID; out Obj): HResult;
begin
{ handled ourself }
Result := inherited QueryInterface(IID, Obj);
end;

function TnxAggregatableObject.InternalRelease: Integer;
begin
{ handled ourself }
Result := inherited _Release;
end;

Looking at the implementation of the methods used for InxInterface we see that they all just pass through to the implementation inherited from TnxInterfacedObject. AddRef and Release count references to the object normally. Release will Free the object when the reference count reaches 0. QueryInterface checks the object for any interfaces it implements itself.

function TnxAggregatableObject.QueryInterface(const IID: TGUID; out Obj): HResult;
begin
if Assigned(aoHost) then
{let the Host handle it}
Result := InxInterface(aoHost).QueryInterface(IID, Obj)
else
{specifying a Host is optional, behave as standalone object}
Result := InternalQueryInterface(IID, Obj);
end;

function TnxAggregatableObject._AddRef: Integer;
begin
if Assigned(aoHost) then
{let the Host handle it}
Result := InxInterface(aoHost)._AddRef
else
{specifying a Host is optional, behave as standalone object}
Result := InternalAddRef;
end;

function TnxAggregatableObject._Release: Integer;
begin
if Assigned(aoHost) then
{let the Host handle it}
Result := InxInterface(aoHost)._Release
else
{specifying a Host is optional, behave as standalone object}
Result := InternalRelease;
end;

Looking at the implementation of the methods used for all other interfaces we can see 2 important things

  • It is possible to pass nil as Host. In that case an TnxAggregateableObject behaves just like a TnxInterfacedObject would. That's the reason why it's called an aggregateable object, not an aggregated object.
  • If a host is provided, then all 3 methods are redirected to the host.

By redirecting AddRef and Release to the Host, any reference to an aggregated interface will be counted as a reference to the Host, keeping it alive. The Host in turn holds a reference to the InxInterface of the aggregateable object, which in turn keeps the aggregateable object alive.

By redirecting QueryInterface to the Host the symmetric and transitive properites of the overall aggregate is preserved. It becomes possible to get from an interface implemented by one aggregated object back to an interface implemented by the host or another aggregated object hosted by the same host.

The reflective property is also preserved because a call to QueryInterface on the aggregated object asking for exactly that interface will be passed to the host and will end up being passed back from the host to this aggregated object via the InxInterface reference held by the Host. The call will end up in InternalQueryInterface which then returns the interface.

This is why it is absolutely essential that derived classes don't list InxInterface again as an implemented interface. InxInterface.QueryInterface would then refer to the QueryInterface method instead of InternalQueryInterface, resulting in an endless loop of calls between QueryInterface of the Host and QueryInterface of the aggregated object.

Aggregateable Example

To see how this works in practice here is an example. For this example I'll create an aggregateable object which implements IMultiQI. IMultiQI is a simple interface which allows to query for multiple other interfaces in a single call. There is normally no need for you to implement this interface manually, NexusRemoting already generally implements it on it's proxy hub. But it does make for an easy and clear example.

First the definition of IMultiQI (which is missing from the Windows API units that come with Delphi):

type
TMULTI_QI = record
IID : PGUID;
pItf : IInterface;
hr : HRESULT;
end;
PMULTI_QI = ^TMULTI_QI;

IMultiQI = interface(IInterface)
['{00000020-0000-0000-c000-000000000046}']
function QueryMultipleInterfaces(cMQIs: Cardinal; pMQIs : PMULTI_QI): HRESULT; stdcall;
end;

IMultiQI only has a single method (beyond the usual 3 that are inherited from IInterface) which takes an array of TMULTI_QI records (given as a count and the pointer to the first element of the array).

Each element in that array has an IID on entry and expects the interface reference and HRESULT (error code) to be filled out on exit. The implementation should ignore any element where the interface reference is not nil on entry.

If all requested interfaces where filled in the function should return S_OK, if only some could be filled in it should return S_FALSE and if none could be filled it it should return E_NOINTERFACE.

The implementation is quite straight forward:

type
TnxAggregateableMultiIQ = class(TnxAggregatableObject, IMultiQI)
protected
{--- IMultiQI ---}
function QueryMultipleInterfaces(cMQIs: Cardinal; pMQIs : PMULTI_QI): HRESULT; stdcall;
end;


function TnxAggregateableMultiIQ.QueryMultipleInterfaces(cMQIs : Cardinal;
pMQIs : PMULTI_QI)
: HRESULT;
var
AnyAssigned : Boolean;
AnyErrors : Boolean;
Target : InxInterface;
begin
AnyAssigned := False;
AnyErrors := False;

Target := Host;
if not Assigned(Target) then
Target := Self;

while cMQIs > 0 do begin
with pMQIs^ do
if not Assigned(pItf) then
if Assigned(IID) then begin
hr := Target.QueryInterface(IID^, pItf);
if not Succeeded(hr) then
AnyErrors := True
else
AnyAssigned := True;
end else begin
hr := E_INVALIDARG;
AnyErrors := True;
end;

Dec(cMQIs);
Inc(pMQIs);
end;

if AnyAssigned then
if AnyErrors then
Result := S_FALSE
else
Result := S_OK
else
Result := E_NOINTERFACE;
end;

Depending on wether the object has been created with or without a Host, the target for the individual QueryInterface calls is either the Host or the Object itself. The implementation then just goes over the array and calls QueryInterface for each requested IID.

Putting it all together

type
ITest1 = interface
['{94264C33-FC05-43D5-8119-8B19042BBB2D}']
procedure Test;
end;

ITest2 = interface
['{E5B7E848-0BF8-4ECE-B86F-DCEAE6387C41}']
procedure Test;
end;

ITest3 = interface
['{4FFD81B7-6728-4249-9BC8-6B28F26E90C8}']
procedure Test;
end;

TTestAggregateable = class(TnxAggregatableObject, ITest2)
protected
{---ITest2---}
procedure Test;
end;

TTestHost = class(TnxAggregationHost, ITest1)
protected
{---ITest1---}
procedure Test;
public
constructor Create;
end;


{ TTestAggregateable }

procedure TTestAggregateable.Test;
begin
ShowMessage(ClassName + '.Test');
end;

{ TTestHost }

constructor TTestHost.Create;
begin
ahAddAggregatedInterface(TTestAggregateable.Create(Self));
ahAddAggregatedInterface(TnxAggregateableMultiIQ.Create(Self));
inherited Create;
end;

procedure TTestHost.Test;
begin
ShowMessage(ClassName + '.Test');
end;

procedure Test;
var
Intf : InxInterface;

Test1 : ITest1;
Test2 : ITest2;
Test3 : ITest3;
begin
Intf := TTestHost.Create;
nxSupportsMulti(Intf, [ITest1, ITest2, ITest3], [@Test1, @Test2, @Test3]);
if Assigned(Test1) then Test1.Test;
if Assigned(Test2) then Test2.Test;
if Assigned(Test3) then Test3.Test;
end;

Running the above code will result in 2 dialog boxes:

---------------------------
AggregateTest
---------------------------
TTestHost.Test
---------------------------
OK
---------------------------

followed by:

---------------------------
AggregateTest
---------------------------
TTestAggregateable.Test
---------------------------
OK
---------------------------

nxSupportsMulti is a helper function which is included in NexusRemoting to simplify calling IMultiQI:

function nxSupportsMulti(const Instance: IInterface; const IIDs: array of TGUID; const Intfs: array of PnxInterface): TnxSupportsMultiResult;
var
MultiQI : IMultiQI;
i : Integer;
AnyAssigned : Boolean;
AnyError : Boolean;
MultiQIs : array of TMULTI_QI;
begin
AnyAssigned := False;
AnyError := False;

Assert(Length(IIDs) = Length(Intfs));
if (Length(IIDs) > 1) and Supports(Instance, IMultiQI, MultiQI) then begin
SetLength(MultiQIs, Length(IIDs));
for i := Low(IIDs) to High(IIDs) do
with MultiQIs[i] do begin
IID := @IIDs[i];
pItf := nil;
hr := E_UNEXPECTED;
end;
if Succeeded(MultiQI.QueryMultipleInterfaces(Length(IIDs), @MultiQIs[0])) then begin
for i := Low(IIDs) to High(IIDs) do
with MultiQIs[i] do
if Succeeded(hr) then begin
Intfs[i]^ := pItf;
AnyAssigned := True;
end else
AnyError := True;
end else begin
AnyError := True;
for i := Low(IIDs) to High(IIDs) do
Intfs[i]^ := nil;
end;
end else
for i := Low(IIDs) to High(IIDs) do begin
if Supports(Instance, IIDs[i], Intfs[i]^) then
AnyAssigned := True
else begin
AnyError := True;
Intfs[i]^ := nil;
end;
end;

if AnyAssigned then
if AnyError then
Result := smrSome
else
Result := smrAll
else
Result := smrNone;
end;

More to come...

This was the last post looking at general interface functionallity. If you've followed the series so far you should know everything that is required to understand the following posts which will dive into the design and implementation of NexusRemoting itself.

Home | Community | Blogs | Thorsten's Blog