We'll see here how we can reflect a C++ class hierarchy using Ada tagged types, having virtual C++ methods being dispatched in Ada.
The C++ example
Let's consider the following C++ hierarchy (file):
// file *** cpp.h *** struct S { virtual void vm() const; void m() const; }; struct T: public S { virtual void vm() const; };
Give these methods some trivial code (file):
// file *** cpp.cpp *** #include <iostream> #include "cpp.h" void S::vm() const { std::cout << "(Cpp) S::vm\n"; } void S::m() const { std::cout << "(Cpp) S::m, invoquing vm()...\n"; vm(); } void T::vm() const { std::cout << "(Cpp) T::vm\n"; }
Note that the m() method calls the virtual method vm(), line 11.
Corresponding Ada package
The previous C++ code is translated into Ada as follow (file):
-- file *** ada_cpp.ads *** with Ada.Finalization; package Ada_Cpp is type Dummy is tagged null record; type Cpp_Ptr is access all Dummy'Class; type Cpp_Access is new Ada.Finalization.Controlled with record ptr: Cpp_Ptr; end record; type S is new Cpp_Access with null record; overriding procedure Initialize(obj: in out S); not overriding procedure vm(obj: in S); not overriding procedure m(obj: in S'Class); type T is new S with null record; overriding procedure Initialize(obj: in out T); overriding procedure vm(obj: in T); end Ada_Cpp;
So in the Ada package we have the same type hierarchy as in C++: the type T inherits
from the type S, both being tagged types having Ada.Finalization.Controlled
as base type.
The types Dummy and Cpp_Ptr have the only purpose to have a general
access type to a C++ class, which will be seen as a pointer on the C++ side. Maybe the type
Interfaces.C.Strings.chars_ptr would be enough for this goal.
Step One: translating C++ to C
The first thing to do now is to translate the C++ code to C code, as well as creating a wrapper for our classes (the reason for this will be explained soon). Here is the wrapper and the C code (file):
// file *** c.h *** #include "cpp.h" struct Ada_S: public S { virtual void vm() const; void* _ada; }; struct Ada_T: public T { virtual void vm() const; void* _ada; }; extern "C" { // exported functions Ada_S* Create_S(void* ada); void S_vm(Ada_S* obj); void S_m(Ada_S* obj); Ada_T* Create_T(void* ada); void T_vm(Ada_T* obj); // imported functions void Dispatch_S_vm(void* ada); void Dispatch_T_vm(void* ada); }
Each C++ class is derived into a new class, in which all virtuel method is
overdefined. The pointer member void* _ada will be used to gain
access to instances created on the Ada side.
Maybe we could have created a common ancestor class containing this pointer and use multiple inheritance, howether strange issues occured when using this technic, so we'll avoid it.
The exported function in the extern "C" block will be implemented
on the C++ side and called from Ada, whereas the imported procedure will be
implemented on the Ada side and called from C++.
Here's the code corresponding to this header (file):
// file *** c.cpp *** #include <iostream> #include "c.h" void Ada_S::vm() const { std::cout << "(Cpp) Ada_S::vm()\n"; // perform a dispatching call in Ada Dispatch_S_vm(_ada); } void Ada_T::vm() const { std::cout << "(Cpp) Ada_T::vm()\n"; // perform a dispatching call in Ada Dispatch_T_vm(_ada); } extern "C" { Ada_S* Create_S(void* ada) { std::cout << "(C ) Create_S(void*)\n"; Ada_S* as = new Ada_S; as->_ada = ada; return as; } void S_vm(Ada_S* obj) { std::cout << "(C ) S_vm(Ada_S*)\n"; // calls the ancestor method, the one in // the S class obj->S::vm(); } void S_m(Ada_S* obj) { std::cout << "(C ) S_m(Ada_S*)\n"; obj->m(); } Ada_T* Create_T(void* ada) { std::cout << "(C ) Create_T(void*)\n"; Ada_T* at = new Ada_T; at->_ada = ada; return at; } void T_vm(Ada_T* obj) { std::cout << "(C ) T_vm(Ada_T*)\n"; // calls the ancestor method, the one in // the T class obj->T::vm(); } }
First trick, we call ancestor virtual methods in their corresponding C functions, here
S_vm() and T_vm(). To put it in other words, calling S_vm()
(respectively T_vm()) from Ada will result in a call to S::vm()
(respectively T::vm()), not any other method.
On the other hand, calling S_m() from Ada will result in a call to S::m(),
which in turn will call the virtual method vm() of the current instance: this last call
is thus polymorphic.
Step two: exporting Ada dispatching
Now let's have a look at the package's body (file):
-- file *** ada_cpp.adb *** with Ada.Text_IO; use Ada.Text_IO; package body Ada_Cpp is -- first of all, we import C functions which we'll use -- to gain access to C++ code: function Create_S(obj: in S) return Cpp_Ptr; pragma Import(C, Create_S, "Create_S"); procedure S_vm(ptr: in Cpp_Ptr); pragma Import(C, S_vm, "S_vm"); procedure S_m(ptr: in Cpp_Ptr); pragma Import(C, S_m, "S_m"); function Create_T(obj: in T) return Cpp_Ptr; pragma Import(C, Create_T, "Create_T"); procedure T_vm(ptr: in Cpp_Ptr); pragma Import(C, T_vm, "T_vm"); -- now giving types' operations' bodies overriding procedure Initialize(obj: in out S) is -- creating an instance of Ada_S on C++ side cpp_s: constant Cpp_Ptr := Create_S(obj); begin Put_Line("(Ada) Initialize(S)"); obj.ptr := cpp_s; end Initialize; not overriding procedure vm(obj: in S) is begin Put_Line("(Ada) vm(S)"); S_vm(obj.ptr); end vm; not overriding procedure m(obj: in S'Class) is begin Put_Line("(Ada) m(S'Class)"); S_m(obj.ptr); end m; -- exporting the dispatching procedure procedure Dispatch_S_vm(obj: in S'Class); pragma Export(C, Dispatch_S_vm, "Dispatch_S_vm"); procedure Dispatch_S_vm(obj: in S'Class) is begin Put_Line("(Ada) Dispatch_S_vm(S'Class)"); -- the following call is dispatching vm(obj); end Dispatch_S_vm; -- overriding procedure Initialize(obj: in out T) is cpp_t: constant Cpp_Ptr := Create_T(obj); begin Put_Line("(Ada) Initialize(T)"); obj.ptr := cpp_t; end Initialize; overriding procedure vm(obj: in T) is begin Put_Line("(Ada) vm(T)"); T_vm(obj.ptr); end vm; -- exporting the dispatching procedure procedure Dispatch_T_vm(obj: in T'Class); pragma Export(C, Dispatch_T_vm, "Dispatch_T_vm"); procedure Dispatch_T_vm(obj: in T'Class) is begin Put_Line("(Ada) Dispatch_T_vm(T'Class)"); -- the following call is dispatching vm(obj); end Dispatch_T_vm; end Ada_Cpp;
As you've already guessed, the trick resides in the Dispatch_* procedures.
Note that they're not even declared in the package's specification: they shall only be
called from the "wrapper" C++ code.
Both Dispatch_S_vm() and Dispatch_T_vm take a 'Class
parameter, and both call a primitive operation of the type: we actually have dispatching. Thus,
if the primitive operation (here, vm()) is overriden, then the overriding one will
be called, even from within C++, because we have overdefined the virtual method and
we're using a subclass, not directly the class itself.
Deriving C++ types in Ada
To make sure all of this works, let's create two new Ada types (file):
-- file *** ders.ads *** with Ada_Cpp; package Ders is type Der_1 is new Ada_Cpp.S with null record; overriding procedure vm(obj: in Der_1); type Der_2 is new Ada_Cpp.T with null record; overriding procedure vm(obj: in Der_2); end Ders;
And the body, pure Ada (file):
-- file *** ders.adb *** with Ada.Text_IO; use Ada.Text_IO; package body Ders is overriding procedure vm(obj: in Der_1) is begin Put_Line("(Ada) vm(Der_1)"); end vm; overriding procedure vm(obj: in Der_2) is begin Put_Line("(Ada) vm(Der_2)"); end vm; end Ders;
And at last, a small test program (file):
-- file *** test.adb *** with Ada.Text_IO; use Ada.Text_IO; with Ada_Cpp; with Ders; procedure Test is s: Ada_Cpp.S; t: Ada_Cpp.T; d1: Ders.Der_1; d2: Ders.Der_2; begin Put_Line("--- START ---"); s.m; Put_Line("--"); t.m; Put_Line("--"); d1.m; Put_Line("--"); d2.m; Put_Line("--- END ---"); end Test;
You can use the following commands to compile (GCC 4.1.1):
$ g++ -c c.cpp cpp.cpp $ gnatmake -gnat05 test -largs c.o cpp.o -lstdc++
And here's the result:
|
Simply the various initializations. |
|
The first call to the The call path is pictured below (green for Ada, yellow for C, blue for C++), the virtual/dispatching calls being drawn as red arrows:/p>
|
|
Now
|
|
Calling
|
|
Just for the sake of completeness, calling
|







