Interfaces without objects

19

Delphi interfaces are usually implemented by objects, but that is not necessary. Like you can implement a COM interface in pure C, you can implement a Delphi interface (which follows COM specifications in general) in pure Pascal without using objects.

To demonstrate the technique let us consider the following example:

unit CalcIntf;

interface

type
  ICalculator = interface
    procedure Clear;
    procedure Add(AValue: Integer);
    procedure Sub(AValue: Integer);
    function GetAccumulator: Integer;
  end;

implementation

end.

Here is a standard Delphi implementation of ICalculator interface by TInterfacedObject descendant:

unit CalcObjects;

interface

uses CalcIntf;

function GetCalculator: ICalculator;

implementation

type
  TCalcObj = class(TInterfacedObject, ICalculator)
    FAccumulator: Integer;
    procedure Clear;
    procedure Add(AValue: Integer);
    procedure Sub(AValue: Integer);
    function GetAccumulator: Integer;
  end;

procedure TCalcObj.Clear;
begin
  FAccumulator:= 0;
end;

function TCalcObj.GetAccumulator: Integer;
begin
  Result:= FAccumulator;
end;

procedure TCalcObj.Add(AValue: Integer);
begin
  Inc(FAccumulator, AValue);
end;

procedure TCalcObj.Sub(AValue: Integer);
begin
  Dec(FAccumulator, AValue);
end;

function GetCalculator: ICalculator;
begin
  Result:= TCalcObj.Create;
end;

end.

Our task is to implement ICalculator interface using a record instance instead of an object instance. I have described the internals of Delphi interfaces before. In the above example the GetCalculator function returns a pointer to the hidden vtable field of TCalcObj instance. If we use a record instead of an object we should declare this field explicitely; if we declare this field first we also get rid of stub code that converts a pointer to vtable field of an instance to a pointer to an instance itself and make interface functions’ calls faster:

type
  PCalcData = ^TCalcData;
  TCalcData = record
    FVTable: Pointer;
    FRefCount: Integer;
    FAccumulator: Integer;
  end;

Now the GetCalculator function should return a pointer to FVTable field of a record; since FVTable field is the first field of the TCalcData record, it is a pointer to record instance itself. A working solution is presented below; I used static class methods of an advanced record instead of ordinary functions only to scope the functions’ names:

unit CalcRecords;

interface

uses CalcIntf;

function GetCalculator: ICalculator;

implementation

type
  PCalcData = ^TCalcData;
  TCalcData = record
    FVTable: Pointer;
    FRefCount: Integer;
    FAccumulator: Integer;
  end;

  TCalcRec = record
    Data: PCalcData;
    class function QueryIntf(Inst: PCalcData; const IID: TGUID;
                             out Obj): HResult; stdcall; static;
    class function Addref(Inst: PCalcData): Integer; stdcall; static;
    class function Release(Inst: PCalcData): Integer; stdcall; static;
    class procedure Clear(Inst: PCalcData); static;
    class procedure Add(Inst: PCalcData; AValue: Integer); static;
    class procedure Sub(Inst: PCalcData; AValue: Integer); static;
    class function GetAccumulator(Inst: PCalcData): Integer; static;
  end;

function InterlockedAdd(var Value: Integer; Increment: Integer): Integer;
asm
      MOV   ECX,EAX
      MOV   EAX,EDX
 LOCK XADD  [ECX],EAX
      ADD   EAX,EDX
end;

function InterlockedIncrement(var Value: Integer): Integer;
asm
      MOV   EDX,1
      JMP   InterlockedAdd
end;

function InterlockedDecrement(var Value: Integer): Integer;
asm
      MOV   EDX,-1
      JMP   InterlockedAdd
end;

class function TCalcRec.QueryIntf(Inst: PCalcData; const IID: TGUID; out Obj): HResult;
begin
  Result:= E_NOINTERFACE;
end;

class function TCalcRec.Addref(Inst: PCalcData): Integer;
begin
  Result:= InterlockedIncrement(Inst^.FRefCount);
end;

class function TCalcRec.Release(Inst: PCalcData): Integer;
begin
  Result:= InterlockedDecrement(Inst^.FRefCount);
  if Result = 0 then
    Dispose(Inst);
end;

class procedure TCalcRec.Clear(Inst: PCalcData);
begin
  Inst^.FAccumulator:= 0;
end;

class function TCalcRec.GetAccumulator(Inst: PCalcData): Integer;
begin
  Result:= Inst^.FAccumulator;
end;

class procedure TCalcRec.Add(Inst: PCalcData; AValue: Integer);
begin
  Inc(Inst^.FAccumulator, AValue);
end;

class procedure TCalcRec.Sub(Inst: PCalcData; AValue: Integer);
begin
  Dec(Inst^.FAccumulator, AValue);
end;

const
  CalcVTable: array[0..6] of Pointer =
  (
    @TCalcRec.QueryIntf,
    @TCalcRec.Addref,
    @TCalcRec.Release,
    @TCalcRec.Clear,
    @TCalcRec.Add,
    @TCalcRec.Sub,
    @TCalcRec.GetAccumulator
  );

function GetCalculator: ICalculator;
var
  P: PCalcData;

begin
  New(P);
  P^.FVTable:= @CalcVTable;
  P^.FRefCount:= 0;
  P^.FAccumulator:= 0;
  Result:= ICalculator(P);
end;

end.

Here is a simple test code:

uses CalcIntf, CalcObjects, CalcRecords;

procedure TForm1.Button1Click(Sender: TObject);
var
  ICalc: ICalculator;

begin
  ICalc:= CalcObjects.GetCalculator;
  ICalc.Add(42);
  ShowMessage(IntToStr(ICalc.GetAccumulator));
end;

procedure TForm1.Button2Click(Sender: TObject);
var
  ICalc: ICalculator;

begin
  ICalc:= CalcRecords.GetCalculator;
  ICalc.Add(42);
  ShowMessage(IntToStr(ICalc.GetAccumulator));
end;