13

This is a bit of special case of interfaces, where a class implements multiple versions of the same interface, ie. something like the following

IBase = interface
   procedure Foo;
end;

ISub = interface (IBase)
   procedure Bar;
end;

ISpecialBase = interface (IBase) end;

ISpecialSub = interface (ISub) end;

TMyClass = class(TInterfacedObject, ISpecialBase, ISpecialSub)

   procedure SpecialFoo1;
   procedure SpecialFoo2;
   procedure SpecialBar;

   procedure ISpecialBase.Foo = SpecialFoo1;

   procedure ISpecialSub.Foo = SpecialFoo2;
   procedure ISpecialSub.Bar = SpecialBar;

   function GetTheRightOne(parameters) : IBase;

end;

...

function TMyClass.GetTheRightOne(parameters) : IBase;
begin
   if (something complex depending on parameters) then
      Result := ISpecialBase(Self)
   else Result := ISpecialSub(Self)
end;

of course there are about a dozen ISpecialXxxx in the real case.

There is a very important need to have only one instance, ie. I want to avoid having to create adapters or dummy instances just to defer the ISpecialXxxx implementations, as the sole purpose of the previous design is precisely to have a single instance handle many outstanding interfaces (ie. the RefCount of the TMyClass can get in the thousandths).

Now the problem is that GetTheRightOne() returns an IBase, yet at some point I want to check if that IBase can be cast to an ISub.

Is there a way to do it with the above declaration form?

One way could be add a

function GetSub : ISub;

to IBase, but that really makes the design a lot heavier, as it would have to be implemented for each ISpecialXxxx, and would be redundant with the ISpecialXxxx "inheritence", so I'm looking for a more elegant solution (assuming it exists).

(I have other "bloat" solutions, so I'm really want to emphasize I'm looking for a non-bloat solution)

edit : some more details

  • GUIDs are there in the original code (but their lack isn't what's causing the difficulty)
  • Supports & QueryInterface don't work, because of the ISpecialXxx needed to have multiple version of the interface per class, ISub isn't listed explitly, and so isn't found. Both works however when using an adapter/dummy class to defer the interface (as then ISub can be listed explicitly)

edit2 : if you want the gory details

Check https://code.google.com/p/dwscript/source/browse/trunk/Source/dwsJSONConnector.pas (r2492), the TdwsJSONConnectorType class, and the IJSONLow interface, the goal is to have the IConnectorFastCall detected from it when it's passed as an IConnectorCall, and thus be able to have LowFastCall invoked rather than LowCall.

The detection has to occur in TConnectorCallExpr.AssignConnectorSym, line 294, where there is currently a QueryInterface.

Note that the QueryInterface works in the case of TdwsJSONIndexReadCall & TdwsJSONIndexWriteCall as they implement the IConnectorCall & IConnectorFastCall from distinct classes & instances. But that's what I'd like to avoid.

Of course ideally, the goal would be to fold back everything into the ConnectorType class (single class, single instance), and for each interface, a particular ConnectorType class should be free to implement either IConnectorCall or IConnectorFastCall.

Eric Grange
  • 5,931
  • 1
  • 39
  • 61
  • 2
    I believe that you just need to add a GUID to each interface and then call `Supports` – David Heffernan Apr 03 '14 at 14:28
  • See [Are GUIDs necessary to use interfaces in Delphi?](http://stackoverflow.com/q/2992183/576719). – LU RD Apr 03 '14 at 14:31
  • 2
    @Eric If the real code has GUIDs, why did you strip them here? Can't you provide real code so that we are all on the same page. Doesn't have to be your actual code. But it does need to correctly demonstrate the issue. – David Heffernan Apr 03 '14 at 15:23
  • GUIDs are present in the original code, updated the OP. If you want to see the full monty, it's an evolution/experiment for TdwsJSONConnectorType in the DWScript dwsJSONConnector unit, where I want to add an alternative (and optional) IConnectorFastCall under IConnectorCall, and those can be requested thousandths of times in hundreds of programs (with ideally only one base instance of the connector type class). The IConnectorFastCall detection only needs to happen at script compile time, so detection performance much less critical than avoiding hoops/jumps. – Eric Grange Apr 03 '14 at 15:24
  • We don't want to see the full monty. We just want to see enough to answer the question. – David Heffernan Apr 03 '14 at 15:34
  • 1
    Why don't you just add `ISub` to the inheritance list? Then you can `QueryInterface` for it directly. Or, you can `QueryInterface` for `ISpecialSub`, and then you implicitly know you have an `ISub` as well. I don't understand the issue. – Rob Kennedy Apr 03 '14 at 15:59
  • 2
    This question might be relevant: http://stackoverflow.com/q/8467945/723693 – ain Apr 03 '14 at 19:18
  • 1
    @Rob because then the ISub would be a single method set for the whole class, while the ISpecials define a distinct ISub method set each. – Eric Grange Apr 04 '14 at 05:50
  • 1
    @ain the problem is different here as ISpecialXxxx each has a different method set, there isn't just one ISub method set for the class – Eric Grange Apr 04 '14 at 05:51

4 Answers4

7

To see if an implementor of an interface implements another interface you can use Supports or QueryInterface, as in the following pseudo-code:

var
  Base: IBase;
  Sub: ISub;
begin
  Base := X.GetTheRightOne(Params);  
  if Supports(Base, ISub, Sub) then
    Sub.Bar;
end;

Edit: For the above to work you need to add IIDs to the declarations of the interfaces.

Ondrej Kelle
  • 36,941
  • 2
  • 65
  • 128
  • 1
    This won't work for the code in the Q 'cos there are no GUIDs – David Heffernan Apr 03 '14 at 14:34
  • I think the OP has GUIDs in the interfaces because he says that the ref count can get into the thousands - and the only way to have ref count is by using the GUIDs. – Andrea Raimondi Apr 03 '14 at 15:00
  • IID are not the issue (they are there in the original code), and Supports (or QueryInterface) doesn't work because of the ISpecialXxx, so ISub isn't there explicitly. Your code only works in the case I named at the end, when you have an adapter/dummy instance that lists ISub explicitly. – Eric Grange Apr 03 '14 at 15:12
  • @EricGrange Yes, all interfaces have to be explicitly listed in the implementor class declaration. I forgot to mention that. – Ondrej Kelle Apr 03 '14 at 17:09
6

One hackish way relies on how the compiler stores the interface VTable data. The compiler stores separate VTables for each interface an object implements. After each VTable it stores the number of interfaces the object implements.

So we can use this to determine if we got the VTable of the ancestor interface, or that of a descendant.

At least this is how it works in XE3 and XE5, I must admit I'm a bit of a n00b when it comes to how interfaces are implemented.

Downsides to this, besides relying on implementation details, is that you'll have to keep the GetSub function in sync if you add methods to the IBase interface. Also, if you have two different, unrelated, ISub's then this code cannot detect which you got. You may be able to hack that in but I'd rather not go there...

{$APPTYPE CONSOLE}

uses
  System.SysUtils;

type
  IBase = interface
    procedure Foo;
  end;

  ISub = interface (IBase)
    procedure Bar;
  end;

  ISpecialBase = interface (IBase)
  end;

  ISpecialSub = interface (ISub)
  end;

  TMyClass = class(TInterfacedObject, ISpecialBase, ISpecialSub)

    procedure SpecialFoo1;
    procedure SpecialFoo2;
    procedure SpecialBar;

    procedure ISpecialBase.Foo = SpecialFoo1;

    procedure ISpecialSub.Foo = SpecialFoo2;
    procedure ISpecialSub.Bar = SpecialBar;

    function GetTheRightOne(const Param: boolean) : IBase;
  end;


{ TMyClass }

function TMyClass.GetTheRightOne(const Param: boolean): IBase;
begin
  if Param then
    Result := ISpecialBase(Self)
  else
    Result := ISpecialSub(Self);
end;

procedure TMyClass.SpecialBar;
begin
  WriteLn('SubBar');
end;

procedure TMyClass.SpecialFoo1;
begin
  WriteLn('BaseFoo');
end;

procedure TMyClass.SpecialFoo2;
begin
  WriteLn('SubFoo');
end;

function GetSub(const Intf: IInterface): ISub;
type
  PPVtable = ^PVtable;
  PVtable = ^TVtable;
  TVtable = array[0..MaxInt div SizeOf(Pointer) - 1] of Pointer;
var
  intfVTable: PPVtable;
  caddr: NativeUInt;
begin
  result := nil;
  intfVTable := PPVTable(Intf);
  // 3 is offset to user methods
  // +0 = first user method, +1 = second user method etc
  // get the "address" of the first method in ISub
  caddr := NativeUInt(intfVTable^[3+1]);
  // compiler stores number of interface entries the
  // implementing object implements right after the interface vtable
  // so if we get a low number here, it means Intf is the IBase interface
  // and not the ISub
  if caddr > $100 then
    result := ISub(Intf);
end;

procedure CallIt(const b: IBase);
var
  s: ISub;
begin
  b.Foo;

  s := GetSub(b);
  if Assigned(s) then
    s.Bar;
end;

var
  c: TMyClass;
  b: IBase;
begin
  try
    c := TMyClass.Create;

    b := c.GetTheRightOne(True);
    CallIt(b);

    WriteLn('---');

    b := c.GetTheRightOne(False);
    CallIt(b);

    WriteLn('...');

  except
    on E: Exception do
      Writeln(E.ClassName, ': ', E.Message);
  end;
  ReadLn;
end.

This outputs

BaseFoo
---
SubFoo
SubBar
...

as we want.

  • 1
    Nice. Combined with interface RTTI (cf http://hallvards.blogspot.fr/2006/06/simple-interface-rtti.html), it should be possible to keep it automatically in synch, and maybe eliminate some/all of the hacks. – Eric Grange Apr 04 '14 at 13:44
  • 1
    Ah yes, I got the impression you did not want RTTI at all. In any case, just note that while you get a different VTable, it may not be unique to that interface. What I mean is that in the code I pasted above, the VTable you get from the ISub's is actually the VTable for TMyClass, and thus contains the additional virtual methods that TObject introduces. That's the reason I check for IBase and assume ISub instead of the other way around. –  Apr 04 '14 at 15:25
  • 1
    So what GetSub really does is return the passed interface as ISub when it contains more methods than the base type you are checking. Be careful as this might fail for interface references that are not ISub but have more methods than the base type you are checking. – Stefan Glienke Apr 04 '14 at 23:43
  • 1
    Yes, though this is all very special-case. My other solution (posted below) is more generic but also involves requirements (or you'll pay for weak reference management). AFAICT this is a shortcoming of interfaces "inheritance" (which isn't really one) and there is no efficient generic solution. – Eric Grange Apr 07 '14 at 06:00
5

Here is my current "best" solution:

I abandoned the method resolution clause, and moved to dummy classes tied to the main class, and that are instantiated only once.

This way, GetInterface & Supports can be used, as the ISub is explicit again.

However, this raises an issue of circular reference: the main class needs to reference the specials (if only to return them in GetTheRightOne()), and the specials need to reference the main class (to access parameters stored there or redirect to methods of the main class).

Both the main class and the specials are reference-counted interfaces, and of course, the usage context is multi-threaded, so usual weak reference schemes would introduce the need for a global lock.

However given that the specials are dummy classes used only for interface resolution of the main class, we can override their _AddRef & _Release to have the reference count centralized on the main class (i.e. the _AddRef & _Release just redirect to the main class's _AddRef & _Release, and no longer maintain a reference count of their own).

Eric Grange
  • 5,931
  • 1
  • 39
  • 61
3

Interface inheritance does not follow the same principles as class inheritance. So in order to test if an IBase supports ISub the implementing class needs to explicitly declare ISub:

TMyClass = class(TInterfacedObject, ISub, ISpecialBase, ISpecialSub)

Querying for an interface does not check for inherited interfaces. AFAIR when interfaces where introduced in Delphi 2 (?) one of the compiler guys once noted that interface inheritance was nothing more than syntactic sugar.

iamjoosy
  • 3,299
  • 20
  • 30
  • This does not work ad each ISpecialXxx has its own ISub method set, this declaration results in a single ISub for the class, not tied to the ISpecialXxx – Eric Grange Apr 04 '14 at 05:46