Proxy2Rust

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

Warning

The Rust backend is experimental. It covers a subset of the proxy IR and several constructs are not yet supported (see Limitations chapter). Generated crates and the conventions described here are subject to change.

Using the tool

The Proxy2rust 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 proxy2rust <proxy-file> -o<output-path> {<switch>}
  • -h|--help

    Display Help.

  • -q

    Be quiet.

  • -v

    Verbose output.

  • -o|--output=<outputPath>

    Path to the directory in which the Rust 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 become Rust modules (mod), declared in src/lib.rs and emitted as src/<module>.rs. Nested modules become nested modules. Module, function and field names are lowercased to snake_case; class and enum names use PascalCase. Identifiers that collide with a Rust keyword are escaped with the raw-identifier syntax (r#priv, r#type …).

Functions

Function placement

Free functions of a module become module-level pub fn items. Functions carrying a method role instead become inherent methods of the role’s class (&self / &mut self — see Classes), and constructors follow the naming described there. Function names are lowercased to snake_case.

Because Rust has no overloading, functions that would otherwise share a name within the same scope (a module’s free functions, or one class’s methods) are disambiguated with a numeric suffix: the first keeps the base name, the rest become ..._1, ..._2

Parameters and return values

  • Scalars are passed and returned by value.

  • Class-typed parameters are passed by reference: &Class for a constant parameter, &mut Class for a mutable one.

  • For a method function, the controlling first parameter becomes the self receiver and is dropped from the argument list.

  • Returned class values follow the ownership rules described under Ownership.

Types

Scalars

Scalars are mapped one-to-one to fixed-size Rust primitives:

Proxy type

Rust type

bool

bool

character

u8

signed int 8

i8

signed int 16

i16

signed int 32

i32

signed int 64

i64

unsigned int 8

u8

unsigned int 16

u16

unsigned int 32

u32

unsigned int 64

u64

float 32

f32

float 64

f64

void

()

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

Enumerations

An enumeration in the proxy becomes a #[repr(...)] Rust enum, sized to the underlying proxy type and deriving the usual value traits. The representation values carried by the proxy are preserved.

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

#[repr(i8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Color {
    Red = 0,
    Green = 1,
    Blue = 2,
}

Arrays

A proxy array maps to the runtime type PolyglotArray<T>, where T is the element type. The supported element types are the native scalar widths (i8/u8/i16/u16/i32/u32/i64/u64, f32, f64 and bool) and record (class) element types; arrays of arrays (multidimensional arrays) are not supported.

Note

PolyglotArray<T> is part of the runtime support specific to the input language. Unlike scalars, enumerations or classes — which map to neutral Rust 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.

Indexing uses the Ada-style bounds carried by the proxy (1-based by default, but any lower bound is honoured) rather than being rebased to zero:

  • get(i) returns Some of the element at index i — a copy for a scalar element, a non-owning view for a record element — or None when i is outside the bounds.

  • set(i, value) overwrites the element at index i.

  • begin() / end() give the first and last valid index, len() the element count and is_empty() whether it has none. Iterate either over a begin()..=end() index range or directly with for x in &array (or array.iter()), which yields the same per-element value as get.

A new array is allocated by passing its bounds to PolyglotArray::<T>::new. Class-typed array parameters are passed by reference — &PolyglotArray<T> for a constant parameter, &mut PolyglotArray<T> for a mutable one — and a returned array follows the ownership rules described under Ownership (an owned PolyglotArray<T> frees its storage on drop, while an array returned by reference or with STATIC ownership is a non-owning view wrapped in std::mem::ManuallyDrop).

use test::test as pkg;

// a function returning, then consuming, an array of i32
let mut arr = pkg::make();
for x in &arr {
    println!("{x}");                 // iterates in Ada index order
}
println!("{:?}", arr.get(0));        // bounds-checked: None when out of range
arr.set(arr.begin(), 42);
pkg::consume(&arr);

// allocate a fresh array with bounds 1 .. 10
let fresh = PolyglotArray::<i32>::new(1, 10);

Strings

A proxy string maps to two runtime types — PolyglotString, an owned string that frees its backing storage on drop, and PolyglotStr, a borrowed view over it. They mirror Rust’s own String / str pair: PolyglotString derefs to PolyglotStr, and a read-only string parameter takes &PolyglotStr (so an &PolyglotString coerces to it). They are dedicated string types rather than a PolyglotArray<u8>, but rely on the same per-input-language runtime support, and for the same reason (see the note above).

  • PolyglotString::from(s) builds one from a Rust &str.

  • PolyglotString/PolyglotStr implement Display, so to_string() or {} formatting copies the contents back out into a Rust String.

Text crosses the boundary as a UTF-8 C string: an embedded NUL byte is rejected (the conversion panics), and on the way back any bytes that are not valid UTF-8 are replaced (Ada strings are not UTF-8 guaranteed). A returned PolyglotString follows the same Ownership rules as any other returned value.

use test::test as pkg;

// a function returning, then consuming, a string
let s = pkg::greeting();
println!("{}", s);
pkg::greet(&s);

// a string built on the Rust side
pkg::greet(&PolyglotString::from("hello"));

Classes

A proxy class maps to a newtype wrapping the opaque pointer to the internal object:

#[repr(transparent)]
pub struct Value(pub(crate) std::ptr::NonNull<std::ffi::c_void>);

The pointer field is pub(crate) so wrappers can be built and unwrapped across the generated crate’s modules while staying hidden externally.

Constructors and methods are emitted as inherent functions:

  • Constructors are named new (the first one), new_default (the zero-parameter constructor), and new_1, new_2 … for further overloads, since Rust has no overloading.

  • method-role functions become &self methods (or &mut self when the controlling parameter is mutable); the controlling object becomes the receiver.

  • For a scalar field, the getter returns a copy, while a companion _mut accessor hands out a real &mut reference into the object.

use test::test as pkg;

let mut v = pkg::Value::new(10);
println!("v = {}", v.get_v());   // copy
*v.get_v_mut() = 20;             // mutate in place
println!("v = {}", v.get_v());

Ownership

Whether a wrapper frees its underlying object is governed by two things:

  • Per type. A class that has a destructor gets an impl Drop calling the free function exposed by the proxy, so an owned value frees the object at end of scope. A class that has a copy function gets an impl Clone.

  • Per returned value. When a value is returned, the proxy’s ownership annotation decides whether the returned wrapper owns the object. A value returned with STATIC ownership — and any class returned by reference — is wrapped in std::mem::ManuallyDrop so dropping it never frees the underlying object (a non-owning view into the object). Otherwise an owned wrapper is returned, which runs its Drop (if any) at end of scope.

Inheritance

A type that derives from a (non-virtual) parent gets Deref/DerefMut implementations that transparently coerce to the parent type, so a &Child can be passed wherever a &Root is expected for class-wide dispatch.

use test::test as pkg;

let root = pkg::Root::new(1, 2);
let child = pkg::Child::new(3, 4, 5);

// Deref coercion: &Child coerces to &Root
pkg::p_root(&root);
pkg::p_root(&child);

Crate layout

Generation produces a complete Cargo crate:

$> gnatpolyglot proxy2rust 2proxy/proxy.json -o 2rust
$> find 2rust
2rust/
2rust/Cargo.toml          # package metadata (name = <proxy name>, edition 2021)
2rust/build.rs            # links the native proxy library
2rust/src
2rust/src/lib.rs          # `pub mod` declaration per module
2rust/src/<module>.rs     # one file per module: enums, structs, impls, free functions
2rust/src/arrays.rs       # PolyglotArrayElement impls for record element types (only if any)

Each generated module file contains a private ffi submodule holding the extern "C" declarations of the proxy symbols; the safe API wraps those calls.

The crate depends on the gnatpolyglot_runtime support crate (installed under the runtime directory by the setup step, via a path dependency in Cargo.toml), which provides the array and string types. Those live under a module path that encodes the input language.

lib.rs re-exports the runtime module tree, preserving its structure, so downstream code can name these type through the generated crate without depending on the runtime crate directly.

Building

build.rs reads two environment variables to locate the statically built, encapsulated proxy library, and emits the right link directives:

  • POLYGLOT_PROXY_LIB_DIR — directory containing the static archive.

  • POLYGLOT_PROXY_LIB_NAME — name of the archive (without lib prefix or extension).

It also links the platform’s required system libraries (dl and pthread on Linux; advapi32 on Windows, which the GNAT runtime needs).

$> POLYGLOT_PROXY_LIB_DIR=<proxy-lib-dir> \
   POLYGLOT_PROXY_LIB_NAME=<proxy-lib-name> \
   cargo build --manifest-path 2rust/Cargo.toml

Supported platforms

Proxy2Rust currently supports only 64-bit Linux and Windows platforms (see Limitations).