Proxy2Cpp

Consumes the proxy IR and generates idiomatic C++ bindings, so that C++ code can call into the bound library across the C ABI.

Using the tool

The Proxy2cpp tool is invoked as a subcommand of the GNATpolyglot program. It must be provided with at least a proxy file and an output path:

$> gnatpolyglot proxy2cpp <proxy-file> -o<output-path> {<switch>}
  • --help

    Display Help.

  • -q

    be quiet.

  • -v

    verbose output.

  • -o|--output=<outputPath>

    path to the directory in which the C++ interface should be generated. Directories along the path are created if they do not exist yet.

  • --header-file=<file>

    Prepend the content of the given file to all sources generated by GNATpolyglot.

Modules

Modules in the JSON Proxy are translated to namespaces and are each in a separate file. Nested modules become nested namespaces.

Ada declarations

Generated C++ headers

-- example.ads
package Example is
   procedure Foo;
end Example;
// example.h
namespace example {

void Foo();

} // namespace example
-- example-child.ads
package Example.Child is
   procedure Bar;
end Example.Child;
// example-child.h
namespace example {
namespace child {

void Bar();

} // namespace child
} // namespace example

Functions

Function placement

By default, functions will be placed in their parent namespace. Functions with roles become member functions to the type of the role.

proxy.json

(proxy2cpp →) C++ header

{
  "kind": "function",
  "name": {"names": ["n1", "f"]}
},
{
  "kind": "class",
  "name": {"names": ["n1", "c"]}
},
{
  "kind": "function",
  "name": {"names": ["n1", "fm"]},
  "role": {
    "kind": "method",
    "type": {"kind": "typename", "name": {"names": ["n1", "c"]}}
  }
}
namespace n1 {

void f();

class C {
  void fm();
};

} // namespace n1

Operators

Functions from a proxy that have a name matching a placeholder name will be translated to an operator overload.

Ada declaration

(ada2proxy →) proxy.json

(proxy2cpp →) C++ header

type T is private

function "+"(I: Long_Integer; V: T) return Integer;
{
  "kind": "function",
  "name": {"names": ["operator_plus"]}
}
int operator+(long i, T v);

Types

Scalars

Scalars are mapped to fixed-width C++ types (the integer types come from <cstdint>):

Proxy type

C++ type

bool

bool

character

char

signed int 8

int8_t

signed int 16

int16_t

signed int 32

int32_t

signed int 64

int64_t

unsigned int 8

uint8_t

unsigned int 16

uint16_t

unsigned int 32

uint32_t

unsigned int 64

uint64_t

float 32

float

float 64

double

void

void

128-bit scalars are not supported (see Limitations).

Enumerations

An enumeration in the proxy becomes a scoped enum class. Its underlying type is the smallest signed integer that can hold the largest enumerator value (int8_t, int16_t, int32_t or int64_t), and the representation values carried by the proxy are preserved. Enumerator names are emitted in upper case.

For C++11 and later, a companion constexpr std::array holding every enumerator is also emitted — named <enum>_values (lower case) — so the values can be iterated over.

A proxy enumeration Color with enumerators Red, Green and Blue generates:

// example.h
namespace example {

enum class Color : int8_t {
    RED = 0,
    GREEN = 1,
    BLUE = 2,
};

#if __cplusplus >= 201103L
constexpr const std::array<Color, 3> color_values = {
    Color::RED,
    Color::GREEN,
    Color::BLUE,
};
#endif

} // namespace example

Arrays

A proxy array becomes an instance of the runtime class template polyglot_array<T>, where T is the element type. Scalars, class types and pointers (polyglot_ptr<C>) are supported as element types; arrays of arrays (multidimensional arrays) are not.

Note

These classes are part of the runtime support specific to the input language. Unlike scalars, enumerations or classes — which map to neutral C++ types — an array carries semantics that belong to the input language’s own array model: most visibly its index bounds, which need not start at zero, but also how its storage is allocated and freed. No single neutral type can capture that for every possible input language, so an array maps instead to runtime support provided per input language.

The array preserves the index bounds carried by the proxy rather than normalising to zero-based indexing:

  • get_begin() / get_end() — the inclusive lower and upper bounds.

  • size() — the number of elements.

  • get(i) — a reference to the element at index i; set(i, value) overwrites it.

  • begin() / end() — iterators, so the array works with range-based for and the standard algorithms.

A new array is allocated by passing its bounds to the constructor. An array returned from a function owns its underlying storage and frees it when it goes out of scope; an array returned by reference instead yields a non-owning polyglot_array<T>::view (a view, like the reference types described above).

// a function returning, then consuming, an array of int32_t
arrays::polyglot_array<int32_t> arr = example::make();

for (int i = arr.get_begin(); i <= arr.get_end(); ++i)
    std::cout << arr.get(i) << '\n';

arr.set(arr.get_begin(), 42);
example::consume(arr);

// allocate a fresh array with bounds 1 .. 10
arrays::polyglot_array<int32_t> fresh(1, 10);

Strings

A proxy string maps to the runtime class polyglot_string. It is a dedicated string type rather than a polyglot_array<char>, but it relies on the same per-input-language runtime support, and for the same reason (see the note above).

Construct one from a C string or an std::string, and read it back as an std::string:

  • polyglot_string(const char *) — construct from a literal or C string; strings::from_string(const std::string &) — construct from an std::string.

  • strings::to_string(const polyglot_string &) — copy the contents out into an std::string.

  • at(i) (bounds-checked, throws on overflow) and operator[](i) (unchecked) return a reference to the character at index i; size() gives the length.

A polyglot_string owns its underlying buffer and frees it when it goes out of scope. Characters are handled as raw bytes: text is passed through unchanged, with no re-encoding performed.

// a function returning, then consuming, a string
strings::polyglot_string s = example::greeting();
std::cout << strings::to_string(s) << '\n';

example::greet(strings::from_string("hello"));

Classes

Classes marked final in the proxy should not be inherited. They lack the support for dynamic dispatch in the bound library.

Currently, nothing prevents inheriting from such types. However, in the future, we will add a mechanism to prevent inheritance, and as such incorrect behaviours when that would happen.

Function members

As mentioned previously, functions with a role will be function members of the class. The first parameter has to be of the same type as the role’s. The latter becomes the implicit this value. If the first argument type in the proxy is constant, then the the function member will be marked as const.

Uncopyable classes (copy elision (C++17))

C++ only supports guaranteed copy elision since C++17. However, there may be times where input languages support returning uncopyable values through similar concepts to copy elision. In order to avoid errors with invalid code generation, when these values are returned before with standards prior to C++17, they will be returned though a pointer. These values, since cloned before by the proxy before returning, will be owned by the user.

Inheritance

Types with a shadow constructor can be inherited. It is mandatory to use the shadow constructor in order to enable overriding non static function members from bound types.

The shadow constructor can be distinguished by its last argument: when constructing the shadow object of a type T, its constructor will expect a T*. This argument expects the this value.

package Example is

   type Foo is tagged null record;

end Example;
// example.h

class Foo {
   // Normal constructor: creates an `Example.Foo` object in the bound library.
   Foo();

   // Shadow constructor
   Foo(Foo *);
};

// main.cpp

class Bar : public example::Foo {
    // valid: creates a shadow object
    Bar() : example::Foo(this) {}

    // Invalid: the input library will not create a shadow object,
    // dispatching will not work
    Bar() : example::Foo() {}
}

Note

This is necessary due to C++ initializing the vtable for a given type only at the time that is it being constructed. When executing example::Foo‘s constructor, the vtable of Bar is not yet initialized, meaning that it cannot be aware that the object being constructed actually inherits from example::Foo.

Overriding virtual functions

Virtual functions from bound types can be overriden. This allows inside the bound libraries to dynamically dispatch back to overrides.

Ada declaration

C++ code using and overriding the generated bindings

package Example is
   type Foo is tagged private;

   procedure Hello (F : Foo) is
   begin
      Put_Line ("Hello from Ada");
   end Hello;

   procedure Call_Hello (F : Foo'Class) is
   begin
      F.Hello; -- Dispatching call
   end Call_Hello;
end Example;
// example.h

namespace example {

class Foo {
public:
    Foo();

    virtual void hello() const;
};

void call_hello(const Foo&);

} // namespace example

// main.cpp

class Bar : example::Foo {
    Bar() : example::Foo(this) {}

    void hello() const override {
        std::cout << "Hello from C++\n";
    }
};

int main() {
    example::Foo foo;
    Bar bar;
    example::call_hello(foo); // Hello from Ada;
    example::call_hello(bar); // Hello from C++;
}

Pointers

Pointer types are represented using a ref-counted smart pointer (polyglot_ptr) in order to avoid manual memory management. The smart pointer holds information on the ownership of the underlying pointer.

Creating a pointer from an existing object sets the owner to STATIC. It implies that the memory should not be freed when the pointer goes out of scope, and the owner cannot be changed.

When passing a pointer to a function, the function expects a minimum level of ownership. This is to make sure that after calling a function that may escape the pointer, the latter should not be freed inadvertently which could leave a dangling pointer.

package body Example is

   type Rec is record
      I: Integer;
   end record;

   type Rec_A is access all Rec;

   Acc : Rec_A := null;

   procedure Set (New_Acc: Rec_A) is
   begin
      Acc := New_Acc;
   end Add;

   procedure Increment is
   begin
      Acc.all := Acc.all + 1;
   end Add;

end Example;
// example.h

namespace example {

   class Rec {
      Rec(int);

      int &get_i();
      void set_i(int i);
   };

   void set(gnatpolyglot::polyglot_ptr<Rec> new_acc);
   void inc();

} // namespace example

// main.cpp

int main() {
    {
        gnatpolyglot::polyglot_ptr<example::Rec> ptr(new Rec(4));
        // `Example.Set` only accepts the default minimum ownership: LIBRARY
        ptr.set_owner(gnatpolyglot::memory_owner::LIBRARY);
        example::set(ptr);
        example::inc();
        // The managed C++ object of `ptr` is freed, but its underlying
        // data is not because it is owned by the library.
    }
    // We can keep calling `Example.Inc`
    example::inc();
    example::inc();
}

Note

For a concrete example of using pointers, checkout the “ex2” example

References and view types

Returning non-scalar references is not possible, so instead, view types are generated. Similarly to the std::vector<bool>::reference specialization, their goal is to provide a value that can be used as a reference of their underlying type. As a comparison, a pointer with a static ownership would result in a similar result, but the outcome would not reflect the behavior of the interface as well.

Destruction of such view objects do not cause the underlying data to be freed.

References to Scalar type are still translated to usual C++ references.

package Example

   type P is private;

   type Rec is record
       Comp: P;
   end record;

   procedure Foo (Val: in out P);

end Example;
// example.h

namespace example {
   class P {};

   class Rec {
       P::view get_comp();
       void set_comp(const P&);
   };

   void foo(P &);
} // namespace example

// main.cpp

int main() {
    Rec r;
    P::view ref = r.get_comp();
    example::foo(ref); // View types can automatically convert to their reference counterpart
    example::foo(r.get_comp());

    P &ref_invalid = r.get_comp(); // Invalid: the reference outlives the view object.
    example::foo(ref_invalid);
    // Undefined behaviour: the view object is no longer valid. The value of the reference is unknown.
}

Exceptions

Exceptions are bound as classes that inherit the std::exception type. GNATpolyglot guarantees the consistency of C++ exception types at run time.

Ada declaration

C++ code using the generated bindings

package Example is

   Exc : exception;

   procedure Raise_Exc is
   begin
      raise Exc with "message";
   end Raise_Exc;

end Example;
// example.h

namespace example {

class Exc : public gnatpolyglot::ada::exceptions::AdaException {
public:
    Exc();
    Exc(const gnatpolyglot::ada::strings::gnatpolyglot_string &);
};

void raise_exc();

} // namespace example

// main.cpp

#include "example.h"

int main() {
    try {
        example::raise_exc();
    } catch (const example::Exc& ex) {
        std::cout << ex.what() << '\n';
    }
}

Note

For a concrete example of using bound exceptions, checkout the “ex3” example

Building

Generation writes into the output directory:

  • the binding headers under include/;

  • the binding sources (*.cpp);

  • the C++ support runtime under runtimes/ada/src2cpp/ (the gnatpolyglot::... string, array, pointer and exception support);

  • a GNAT project file <Proxy>_2Cpp.gpr, to build the bindings as a C++ library if needed.

The bindings reach the bound library through the proxy library, built separately as an encapsulated standalone library; its archive lives in <proxy-dir>/lib_agg/ and is linked as <proxy>_proxy_agg.

To build a C++ program against the bindings, you can do it entirely manually by compiling your own sources together with the generated and runtime .cpp files, adding both include directories, and linking the proxy library:

$> g++ main.cpp \
       2cpp/*.cpp \
       2cpp/runtimes/ada/src2cpp/*.cpp \
       -I2cpp/include \
       -I2cpp/runtimes/ada/src2cpp \
       -L<proxy-lib-dir> \
       -l<proxy>_proxy_agg \
       -ldl -lpthread \
       -std=c++11 \
       -o main

Note

-ldl and -lpthread are required by the GNAT runtime on Linux; they are not needed on Windows. The bindings compile under C++11;

Alternatively, if you have access to gprbuild, you can also build the bindings as a C++ library using the generated <Proxy>_2Cpp.gpr project file, and link with it from your build system of choice.