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>}
--helpDisplay Help.
-qbe quiet.
-vverbose 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 |
|---|---|
|
|
character |
|
signed int 8 |
|
signed int 16 |
|
signed int 32 |
|
signed int 64 |
|
unsigned int 8 |
|
unsigned int 16 |
|
unsigned int 32 |
|
unsigned int 64 |
|
float 32 |
|
float 64 |
|
|
|
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 indexi;set(i, value)overwrites it.begin()/end()— iterators, so the array works with range-basedforand 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 anstd::string.strings::to_string(const polyglot_string &)— copy the contents out into anstd::string.at(i)(bounds-checked, throws on overflow) andoperator[](i)(unchecked) return a reference to the character at indexi;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/(thegnatpolyglot::...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.