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|--helpDisplay Help.
-qBe quiet.
-vVerbose 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:
&Classfor a constant parameter,&mut Classfor a mutable one.For a
methodfunction, the controlling first parameter becomes theselfreceiver 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 |
|---|---|
|
|
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 #[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)returnsSomeof the element at indexi— a copy for a scalar element, a non-owning view for a record element — orNonewheniis outside the bounds.set(i, value)overwrites the element at indexi.begin()/end()give the first and last valid index,len()the element count andis_empty()whether it has none. Iterate either over abegin()..=end()index range or directly withfor x in &array(orarray.iter()), which yields the same per-element value asget.
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/PolyglotStrimplementDisplay, soto_string()or{}formatting copies the contents back out into a RustString.
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), andnew_1,new_2… for further overloads, since Rust has no overloading.method-role functions become&selfmethods (or&mut selfwhen the controlling parameter is mutable); the controlling object becomes the receiver.For a scalar field, the getter returns a copy, while a companion
_mutaccessor hands out a real&mutreference 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 Dropcalling 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 animpl 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
STATICownership — and any class returned by reference — is wrapped instd::mem::ManuallyDropso dropping it never frees the underlying object (a non-owning view into the object). Otherwise an owned wrapper is returned, which runs itsDrop(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 (withoutlibprefix 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).