In this section we demonstrate the GNAT features for interfacing with C++ by means of an example making use of Ada 2005 abstract interface types. This example consists of a classification of animals; classes have been used to model our main classification of animals, and interfaces provide support for the management of secondary classifications. We first demonstrate a case in which the types and constructors are defined on the C++ side and imported from the Ada side, and latter the reverse case.
The root of our derivation will be the Animal
class, with a
single private attribute (the Age
of the animal) and two public
primitives to set and get the value of this attribute.
class Animal { public: virtual void Set_Age (int New_Age); virtual int Age (); private: int Age_Count; };
Abstract interface types are defined in C++ by means of classes with pure
virtual functions and no data members. In our example we will use two
interfaces that provide support for the common management of Carnivore
and Domestic
animals:
class Carnivore { public: virtual int Number_Of_Teeth () = 0; }; class Domestic { public: virtual void Set_Owner (char* Name) = 0; };
Using these declarations, we can now say that a Dog
is an animal that is
both Carnivore and Domestic, that is:
class Dog : Animal, Carnivore, Domestic { public: virtual int Number_Of_Teeth (); virtual void Set_Owner (char* Name); Dog(); // Constructor private: int Tooth_Count; char *Owner; };
In the following examples we will assume that the previous declarations are
located in a file named animals.h
. The following package demonstrates
how to import these C++ declarations from the Ada side:
with Interfaces.C.Strings; use Interfaces.C.Strings; package Animals is type Carnivore is interface; pragma Convention (C_Plus_Plus, Carnivore); function Number_Of_Teeth (X : Carnivore) return Natural is abstract; type Domestic is interface; pragma Convention (C_Plus_Plus, Set_Owner); procedure Set_Owner (X : in out Domestic; Name : Chars_Ptr) is abstract; type Animal is tagged record Age : Natural := 0; end record; pragma Import (C_Plus_Plus, Animal); procedure Set_Age (X : in out Animal; Age : Integer); pragma Import (C_Plus_Plus, Set_Age); function Age (X : Animal) return Integer; pragma Import (C_Plus_Plus, Age); type Dog is new Animal and Carnivore and Domestic with record Tooth_Count : Natural; Owner : String (1 .. 30); end record; pragma Import (C_Plus_Plus, Dog); function Number_Of_Teeth (A : Dog) return Integer; pragma Import (C_Plus_Plus, Number_Of_Teeth); procedure Set_Owner (A : in out Dog; Name : Chars_Ptr); pragma Import (C_Plus_Plus, Set_Owner); function New_Dog return Dog; pragma CPP_Constructor (New_Dog); pragma Import (CPP, New_Dog, "_ZN3DogC2Ev"); end Animals;
Thanks to the compatibility between GNAT run-time structures and the C++ ABI, interfacing with these C++ classes is easy. The only requirement is that all the primitives and components must be declared exactly in the same order in the two languages.
Regarding the abstract interfaces, we must indicate to the GNAT compiler by
means of a pragma Convention (C_Plus_Plus)
, the convention used to pass
the arguments to the called primitives will be the same as for C++. For the
imported classes we use pragma Import
with convention C_Plus_Plus
to indicate that they have been defined on the C++ side; this is required
because the dispatch table associated with these tagged types will be built
in the C++ side and therefore will not contain the predefined Ada primitives
which Ada would otherwise expect.
As the reader can see there is no need to indicate the C++ mangled names
associated with each subprogram because it is assumed that all the calls to
these primitives will be dispatching calls. The only exception is the
constructor, which must be registered with the compiler by means of
pragma CPP_Constructor
and needs to provide its associated C++
mangled name because the Ada compiler generates direct calls to it.
With the above packages we can now declare objects of type Dog on the Ada side and dispatch calls to the corresponding subprograms on the C++ side. We can also extend the tagged type Dog with further fields and primitives, and override some of its C++ primitives on the Ada side. For example, here we have a type derivation defined on the Ada side that inherits all the dispatching primitives of the ancestor from the C++ side.
with Animals; use Animals; package Vaccinated_Animals is type Vaccinated_Dog is new Dog with null record; function Vaccination_Expired (A : Vaccinated_Dog) return Boolean; end Vaccinated_Animals;
It is important to note that, because of the ABI compatibility, the programmer does not need to add any further information to indicate either the object layout or the dispatch table entry associated with each dispatching operation.
Now let us define all the types and constructors on the Ada side and export them to C++, using the same hierarchy of our previous example:
with Interfaces.C.Strings; use Interfaces.C.Strings; package Animals is type Carnivore is interface; pragma Convention (C_Plus_Plus, Carnivore); function Number_Of_Teeth (X : Carnivore) return Natural is abstract; type Domestic is interface; pragma Convention (C_Plus_Plus, Set_Owner); procedure Set_Owner (X : in out Domestic; Name : Chars_Ptr) is abstract; type Animal is tagged record Age : Natural := 0; end record; pragma Convention (C_Plus_Plus, Animal); procedure Set_Age (X : in out Animal; Age : Integer); pragma Export (C_Plus_Plus, Set_Age); function Age (X : Animal) return Integer; pragma Export (C_Plus_Plus, Age); type Dog is new Animal and Carnivore and Domestic with record Tooth_Count : Natural; Owner : String (1 .. 30); end record; pragma Convention (C_Plus_Plus, Dog); function Number_Of_Teeth (A : Dog) return Integer; pragma Export (C_Plus_Plus, Number_Of_Teeth); procedure Set_Owner (A : in out Dog; Name : Chars_Ptr); pragma Export (C_Plus_Plus, Set_Owner); function New_Dog return Dog'Class; pragma Export (C_Plus_Plus, New_Dog); end Animals;
Compared with our previous example the only difference is the use of
pragma Export
to indicate to the GNAT compiler that the primitives will
be available to C++. Thanks to the ABI compatibility, on the C++ side there is
nothing else to be done; as explained above, the only requirement is that all
the primitives and components are declared in exactly the same order.
For completeness, let us see a brief C++ main program that uses the
declarations available in animals.h
(presented in our first example) to
import and use the declarations from the Ada side, properly initializing and
finalizing the Ada run-time system along the way:
#include "animals.h" #include <iostream> using namespace std; void Check_Carnivore (Carnivore *obj) {...} void Check_Domestic (Domestic *obj) {...} void Check_Animal (Animal *obj) {...} void Check_Dog (Dog *obj) {...} extern "C" { void adainit (void); void adafinal (void); Dog* new_dog (); } void test () { Dog *obj = new_dog(); // Ada constructor Check_Carnivore (obj); // Check secondary DT Check_Domestic (obj); // Check secondary DT Check_Animal (obj); // Check primary DT Check_Dog (obj); // Check primary DT } int main () { adainit (); test(); adafinal (); return 0; }