Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Are Delphi and C++ class VMTs compatible?

I need to call some C++ code from Delphi. The C++ code needs to be able to callback into the Delphi code in return. The example shown here Calling a callback function in Delphi from a C++ DLL works perfectly well. However, instead of passing to the C++ a single Delphi function as a callback, I would like to pass a Delphi object implementing an interface.

Edit: By interface I am referring to the C++ terminology, which is a class with pure virtual functions. This is not necessarily a type defined with the Delphi interface keyword. In other words, the following class defines an interface I would like to call from C++:

ICallable = class 
    procedure callMe stdcall; virtual; abstract;
    procedure CallMeAgain stdcall; virtual; abstract;
end;

The ICallable interface would in turn be implemented in Delphi as follows:

MyCallable = class(ICallable)
   procedure callMe override;
   procedure callMeAgain override;
end;

procedure MyCallable.callMe
begin
   WriteLine('I was called');
end;

procedure MyCallable.callMeAgain
begin
   WriteLine('I was called again');
end;

On the C++ side, which is compiled as a DLL, I want to define the ICallable interface as follows:

class ICallable{
public:
  virtual void callMe()=0;
  virtual void callMeAgain()=0;
}

And export the following DLL function so that it can be called by Delphi:

#define DllExport   extern "C" __declspec( dllexport )

DLLExport bool Callback(ICallable* callable){
   callable->callMe();
   callable->callMeAgain();
   return true;
}  

And finally back in Delphi:

function Callback(myCallable: ICallable) : Boolean cdecl; external 'dllname'

Question:

  • This can only work if C++ and Delphi implement their virtual method tables in the same way. Is this the case?
like image 432
BigONotation Avatar asked Dec 19 '22 18:12

BigONotation


1 Answers

This can only work of C++ and Delphi implement their virtual method tables in the same way. Is it the case?

I originally thought that a Delphi class does not have a VMT compatible with a C++ class. I thought that because all Delphi classes derive from TObject which declares virtual methods. These virtual methods appear in the VMT I assumed that these methods would appear first in the VMT. However, it transpires that the compiler arranges that the built in virtual methods of TObject have negative indices in the VMT. Which means that the user-defined virtual methods (those define in the subclasses of TObject) start from index 0.

This means that the Delphi and C++ classes in your code in the question do indeed have compatible VMTs. I believe that this design choice was made to support COM in earlier versions of Delphi. To back up my claims, I refer you to the documentation says, with my emphasis:

The layout of a VMT is shown in the following table. On the 32-bit platforms, at positive offsets, a VMT consists of a list of 32-bit method pointers (64-bit method pointers on the 64-bit platform)--one per user-defined virtual method in the class type--in order of declaration. Each slot contains the address of the corresponding entry point of the virtual method. This layout is compatible with a C++ v-table and with COM. At negative offsets, a VMT contains a number of fields that are internal to Delphi's implementation. Applications should use the methods defined in TObject to query this information, since the layout is likely to change in future implementations of the Delphi language.

It should be stressed that nothing in the C++ standard mandates the use of VMTs for virtual methods, even less how VMTs are implemented. In reality, every mainstream Windows compiler has VMTs implemented this way in order to support COM.

Rather than relying on such implementation details, you could use Delphi interfaces. However, as your know, these are COM interfaces and so you must implement IUnknown. You say you want to avoid the machinery of COM, but the only thing you need to add is IUnknown. That is not especially onerous in my view. I get the sense that you think you need to register CLSIDs, implement class factories and so on. You don't. You would just need to implement IUnknown.

Anyway, if you really are set on avoiding IUnknown then you cannot use Delphi interfaces and have two options so far as I can tell:

  1. Implement the VMT manually in your Delphi code. A VMT is just an array of function pointers. This would lead you to code that looks the way COM does in C. Perfectly possible, but not exactly pleasant.
  2. Use the approach outlined in your question, and rely on the implementation detail that TObject uses negative VMT indices for its built in virtual methods.

Compilable for code option 2 looks like this:

Delphi

{$APPTYPE CONSOLE}

type
  ICallable = class
  public
    procedure CallMe cdecl; virtual; abstract;
    procedure CallMeAgain cdecl; virtual; abstract;
  end;

  MyCallable = class(ICallable)
  public
    procedure CallMe; override;
    procedure CallMeAgain; override;
  end;

procedure MyCallable.CallMe;
begin
  Writeln('CallMe');
end;

procedure MyCallable.CallMeAgain;
begin
  Writeln('CallMeAgain');
end;

const
  dllname = 'C:\Users\heff\Desktop\Win32Project1\Debug\Win32Project1.dll';

function Callback(Callable: ICallable): Boolean; cdecl; external dllname;

var
  Callable: ICallable;

begin
  Callable := MyCallable.Create;
  Writeln(Callback(Callable));
  Callable.Free;
  Readln;
end.

C++

#include <Windows.h>

BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

class ICallable
{
public:
    virtual void CallMe() = 0;
    virtual void CallMeAgain() = 0;
};

extern "C" __declspec(dllexport) bool Callback(ICallable* callable)
{
    callable->CallMe();
    callable->CallMeAgain();
    return true;
}

Output

CallMe
CallMeAgain
TRUE
like image 51
David Heffernan Avatar answered Dec 24 '22 02:12

David Heffernan