Rust projects

GPR can build Rust code by delegating the work to Cargo. A project whose language is Rust indicates where the Cargo manifest lives and what the crate produces. GPRbuild runs cargo at the right point of the build. What Cargo produces is then treated like any other artifact of the tree.

An Ada executable can therefore link a Rust library, and a Rust crate can link a library built from a GPR project. One gprbuild invocation covers the whole tree.

GPR never compiles Rust itself. It runs Cargo and records what Cargo produced, so that the rest of the tree can depend on it.

Declaring a Rust project

Everything in this section applies to any Rust project, whether it produces a library or executables.

A Rust project declares Rust as its language and names the directory that holds Cargo.toml through the Root attribute of the Cargo package:

project Hello_From_Rust is
   for Languages   use ("Rust");
   for Source_Dirs use ("rust/src");

   package Cargo is
      for Root use Project'Project_Dir & "rust";
   end Cargo;
end Hello_From_Rust;
Cargo.Root

The directory containing the Cargo.toml of this project. A relative value is interpreted from the project’s own directory. Defaults to the project directory. It may name a member of a Cargo workspace, in which case everything below applies to that member.

Source_Dirs

The Rust sources, as for any other language. Cargo decides what to compile. GPR needs the sources to resolve the Main attribute, and to report the project’s contents to tools such as GPRls.

The .rs suffix is the default body suffix for Rust, so a Naming package is only needed if your sources use another one.

Warning

A project that declares Rust cannot declare another language. Cargo drives the whole build of the crate, so it cannot produce the artifacts of a second language. Mixing them is rejected when the tree is loaded.

Mixed-language systems are built by giving each language its own project and importing one from the other, which is the subject of Mixing Ada and Rust below.

The Cargo package is also not allowed in an aggregate or aggregate library project. Aggregated Rust projects are supported; the aggregate itself simply cannot carry the Cargo settings.

Target triples

Cargo is always given an explicit target triple. GPR derives it from the canonical GPR target of the tree:

Canonical GPR target

Rust triples

aarch64-elf

aarch64-unknown-none

aarch64-linux

aarch64-unknown-linux-gnu

aarch64-qnx

aarch64-unknown-nto-qnx800

aarch64-vx7r2

aarch64-wrs-vxworks-rtp (default)
aarch64-wrs-vxworks-dkm

arm-elf

armv7r-none-eabihf

x86_64-linux

x86_64-unknown-linux-gnu

x86_64-windows

x86_64-pc-windows-gnu

Without Cargo.Rust_Target, GPR uses the row’s default. Set the attribute to pick another triple from the same row, for instance a VxWorks DKM instead of the default RTP:

package Cargo is
   for Root        use Project'Project_Dir & "rust";
   for Rust_Target use "x86_64-wrs-vxworks-dkm";
end Cargo;

Warning

The table is built into GPR and cannot be extended. Cargo.Rust_Target may only name a triple from the row of the current GPR target. A GPR target the table does not list cannot build Rust at all.

The table will eventually be moved to the knowledge base.

Build profile

Cargo.Profile selects the Cargo profile, either "release" (the default) or "dev":

package Cargo is
   for Root    use Project'Project_Dir & "rust";
   for Profile use "dev";
end Cargo;

Note

The naming is Cargo’s own. The dev profile writes to a debug directory, so a dev build lands in <cargo target directory>/<triple>/debug.

Where the artifacts are

Cargo owns its output layout, and GPR does not move what it produces. Both libraries and executables stay in the target directory Cargo reports, and GPR looks for them wherever that is. It is normally target beside the manifest, and the workspace root’s for a workspace member:

<cargo target directory>/<triple>/<release|debug>/

Cargo also decides for itself what needs recompiling. GPRbuild therefore runs cargo build on every invocation, and lets Cargo report that everything is up to date.

Note

GPR attempts no up-to-date check of its own for a Rust project, so the cost of deciding that nothing changed is Cargo’s. GPRbuild does track two things: the manifest, and the libraries linked into the crate. A change to either makes the crate stale for the rest of the tree. An executable linking it is relinked, even if no Rust source changed.

Rust libraries

A Rust crate becomes a library project when its manifest declares a staticlib or a cdylib crate type. Those are the two Cargo produces that are linkable from other languages.

GPRbuild checks the project file against the manifest, and rejects the library project if any of the following does not hold:

  • The crate declares a library target, of type staticlib or cdylib. No other type will do.

  • It declares only one of the two. A manifest declaring both is rejected rather than disambiguated by Library_Kind.

  • Library_Name is the name of the library crate, spelled the way Rust spells it. Cargo normalizes dashes to underscores, so a crate named hello-from-rust is declared as for Library_Name use "hello_from_rust";.

  • Library_Kind matches the crate type: static, the default, for staticlib, and dynamic or relocatable for cdylib.

  • Library_Version is not set. It is not supported, so a cdylib gets no versioned soname from GPR.

A complete example

A shared Rust library:

greeter/
├── greeter.gpr
└── rust/
    ├── Cargo.toml
    └── src/
        └── lib.rs

greeter.gpr, where Library_Kind is dynamic because the crate is a cdylib:

library project Greeter is
   for Languages    use ("Rust");
   for Library_Name use "greeter";
   for Library_Kind use "dynamic";
   for Library_Dir  use "lib";
   for Source_Dirs  use ("rust/src");

   package Cargo is
      for Root use Project'Project_Dir & "rust";
   end Cargo;
end Greeter;

rust/Cargo.toml, whose crate name matches Library_Name:

[package]
name = "greeter"
version = "0.1.0"
edition = "2024"

[lib]
crate-type = ["cdylib"]

rust/src/lib.rs, exporting a C-callable symbol:

#[unsafe(no_mangle)]
pub extern "C" fn greet() {
    println!("Hello from Rust!");
}

Building the project runs Cargo and leaves the shared library where Cargo put it, under the name GPR derives from Library_Name:

$ gprbuild -p -P greeter.gpr
$ ls rust/target/x86_64-unknown-linux-gnu/release/libgreeter.so
rust/target/x86_64-unknown-linux-gnu/release/libgreeter.so

Note

Library_Dir is still required of a library project, as the tree will not load without it, but nothing is written there. The lib/ of this example stays empty: the library stays where Cargo put it, and that is the file GPR hands to the linker. Importing projects are unaffected, since they never name the path themselves.

On its own a library project only produces that file. Linking it into an executable is the subject of Mixing Ada and Rust below.

Rust executables

A standard (non-library) Rust project produces the binaries its manifest declares. With no Main attribute, every binary of the package is built. Main restricts that to the binaries built from the named sources:

project Codec is
   for Languages   use ("Rust");
   for Source_Dirs use ("rust/src");
   for Main        use ("encode.rs", "decode.rs");

   package Cargo is
      for Root use Project'Project_Dir & "rust";
   end Cargo;
end Codec;

matching a manifest that declares:

[[bin]]
name = "encode"
path = "src/encode.rs"

[[bin]]
name = "decode"
path = "src/decode.rs"

Each value names the Rust source of a [[bin]] target, not the binary name. That source must be found in Source_Dirs for GPR to resolve it. Cargo is then asked for the matching binaries only.

Note

It is an error to name a source that no binary is built from, and an error for the manifest to declare no binary at all.

Mains given on the gprbuild command line work the same way. As for the other languages, they restrict the build to the projects that own them. A Rust project that owns none of the requested mains is left alone, rather than building every binary of its manifest.

A complete example

A standalone Rust executable, built entirely by Cargo but driven by GPRbuild:

hello_rust/
├── hello_from_rust.gpr
└── rust/
    ├── Cargo.toml
    └── src/
        └── main.rs

hello_from_rust.gpr:

project Hello_From_Rust is
   for Languages   use ("Rust");
   for Source_Dirs use ("rust/src");

   package Cargo is
      for Root use Project'Project_Dir & "rust";
   end Cargo;
end Hello_From_Rust;

rust/Cargo.toml:

[package]
name = "hello_from_rust"
version = "0.1.0"
edition = "2024"

rust/src/main.rs:

fn main() {
    println!("hello from Rust!");
}

The manifest declares no [[bin]], so Cargo builds one binary named after the package, from src/main.rs:

$ gprbuild -P hello_from_rust.gpr
$ ./rust/target/x86_64-unknown-linux-gnu/release/hello_from_rust
hello from Rust!

Note

The project declares no Exec_Dir, and declaring one would change nothing. Cargo decides where the binary goes, and GPR does not move it.

Mixing Ada and Rust

A Rust project cannot hold Ada sources, so the two languages meet through project imports. Both directions work, and each is described below.

Using a Rust library from Ada

Importing a Rust library project from an Ada one is all it takes to link against it:

with "greeter.gpr";

project Main is
   for Languages use ("Ada");
   for Main      use ("main.adb");
end Main;

gprbuild -P main.gpr then builds the Rust library through Cargo and links it into the Ada executable. A Rust static library needs extra system libraries at the final link, and GPR adds them for you. They also reach the link when the Rust project is imported indirectly, through another library.

A complete example, an Ada executable calling into a Rust static library:

ada_calls_rust/
├── hello_from_ada.gpr
├── hello_from_rust.gpr
├── src/
│   └── hello_from_ada.adb
└── rust/
    ├── Cargo.toml
    └── src/
        └── lib.rs

hello_from_rust.gpr, the library project:

library project Hello_From_Rust is
   for Languages    use ("Rust");
   for Library_Name use "hello_from_rust";
   for Library_Dir  use "lib";
   for Source_Dirs  use ("rust/src");

   package Cargo is
      for Root use Project'Project_Dir & "rust";
   end Cargo;
end Hello_From_Rust;

rust/Cargo.toml, whose crate name matches Library_Name and whose crate type matches the default Library_Kind of static:

[package]
name = "hello_from_rust"
version = "0.1.0"
edition = "2024"

[lib]
crate-type = ["staticlib"]

rust/src/lib.rs, exporting a C-callable symbol:

#[unsafe(no_mangle)]
pub extern "C" fn hello_from_rust() {
    println!("hello from Rust!");
}

hello_from_ada.gpr, which only has to import the library project:

with "hello_from_rust";

project Hello_From_Ada is
   for Languages   use ("Ada");
   for Source_Dirs use ("src");
   for Object_Dir  use "obj";
   for Exec_Dir    use ".";
   for Main        use ("hello_from_ada.adb");
end Hello_From_Ada;

src/hello_from_ada.adb, importing the symbol with C convention:

with Ada.Text_IO; use Ada.Text_IO;

procedure Hello_From_Ada is
   procedure Hello_From_Rust with Import, Convention => C;
begin
   Put_Line ("Hello from Ada!");
   Hello_From_Rust;
end Hello_From_Ada;

One invocation builds both (-p creates the missing obj/ and lib/ directories):

$ gprbuild -p -P hello_from_ada.gpr
$ ./hello_from_ada
Hello from Ada!
hello from Rust!

Nothing in hello_from_ada.gpr mentions Rust or Cargo: the link switches the Rust static library needs are added by GPR.

Using an Ada library from Rust

A Rust project may import library projects written in other languages. GPR tells Cargo where each library is and what to link against. For a shared library, it also indicates where to find it at run time. Whatever linker options the imported library recorded for its own link are passed on too.

with "math_lib.gpr";

project Main is
   for Languages   use ("Rust");
   for Source_Dirs use ("rust/src");

   package Cargo is
      for Root use Project'Project_Dir & "rust";
   end Cargo;
end Main;

Note

An imported Ada library must be encapsulated (for Library_Standalone use "encapsulated";) so that it carries the elaboration code and the Ada runtime with it. One that is not is rejected.

A complete example, a Rust executable calling into an encapsulated Ada library:

rust_calls_ada/
├── main.gpr
├── math_lib.gpr
├── src_mathlib/
│   ├── math_lib.ads
│   └── math_lib.adb
└── rust/
    ├── Cargo.toml
    └── src/
        └── main.rs

math_lib.gpr. Library_Auto_Init runs the elaboration when the library is loaded, so the Rust side has nothing to call first:

project Math_Lib is
   for Languages         use ("Ada");
   for Source_Dirs       use ("src_mathlib");
   for Object_Dir        use "obj";
   for Library_Name      use "mathlib";
   for Library_Dir       use "lib";
   for Library_Kind      use "dynamic";
   for Library_Interface use ("Math_Lib");
   for Library_Standalone use "encapsulated";
   for Library_Auto_Init use "true";
end Math_Lib;

src_mathlib/math_lib.ads, exporting a C-callable subprogram:

with Interfaces.C; use Interfaces.C;

package Math_Lib is
   function Add (A, B : int) return int;
   pragma Export (C, Add, "ada_add");
end Math_Lib;

src_mathlib/math_lib.adb:

package body Math_Lib is
   function Add (A, B : int) return int is
   begin
      return A + B;
   end Add;
end Math_Lib;

main.gpr, which imports the Ada library and is otherwise an ordinary Rust project:

with "math_lib";

project Main is
   for Languages   use ("Rust");
   for Source_Dirs use ("rust/src");

   package Cargo is
      for Root use Project'Project_Dir & "rust";
   end Cargo;
end Main;

rust/Cargo.toml:

[package]
name = "main_from_rust"
version = "0.1.0"
edition = "2024"

rust/src/main.rs, declaring the Ada subprogram as an external C function:

use std::os::raw::c_int;

unsafe extern "C" {
    fn ada_add(a: c_int, b: c_int) -> c_int;
}

fn main() {
    let result = unsafe { ada_add(10, 32) };
    println!("Calling encapsulated Ada library from Rust!");
    println!("10 + 32 = {}", result);
}

Building main.gpr builds the Ada library first, then hands it to Cargo:

$ gprbuild -p -P main.gpr
$ ./rust/target/x86_64-unknown-linux-gnu/release/main_from_rust
Calling encapsulated Ada library from Rust!
10 + 32 = 42

Nothing in Cargo.toml mentions the Ada library. GPR tells Cargo where to find it and what to link, and the executable finds it again at run time.

Using a Rust library from Rust

A Rust project may import another Rust library project. Each keeps its own manifest and its own target directory, and GPR builds them in order.

The two crates are linked through the C ABI, not as Cargo dependencies. The library exports C-callable symbols and the executable declares them as external C functions. A crate that should be an ordinary Cargo dependency belongs in Cargo.toml, not in a separate GPR project.

A complete example, a Rust executable calling into a Rust static library:

rust_calls_rust/
├── main.gpr
├── rust_lib.gpr
├── rustlib/
│   ├── Cargo.toml
│   └── src/
│       └── lib.rs
└── rust/
    ├── Cargo.toml
    └── src/
        └── main.rs

rust_lib.gpr, the library project:

library project Rust_Lib is
   for Languages    use ("Rust");
   for Library_Name use "rust_lib";
   for Library_Dir  use "lib";
   for Source_Dirs  use ("rustlib/src");

   package Cargo is
      for Root use Project'Project_Dir & "rustlib";
   end Cargo;
end Rust_Lib;

rustlib/Cargo.toml:

[package]
name = "rust_lib"
version = "0.1.0"
edition = "2024"

[lib]
crate-type = ["staticlib"]

rustlib/src/lib.rs, exporting a C-callable symbol:

use std::os::raw::c_int;

#[unsafe(no_mangle)]
pub extern "C" fn rust_lib_double(x: c_int) -> c_int {
    x * 2
}

main.gpr, which imports the library project:

with "rust_lib";

project Main is
   for Languages   use ("Rust");
   for Source_Dirs use ("rust/src");

   package Cargo is
      for Root use Project'Project_Dir & "rust";
   end Cargo;
end Main;

rust/Cargo.toml, which indicates nothing about the library:

[package]
name = "main_from_rust"
version = "0.1.0"
edition = "2024"

rust/src/main.rs, declaring the symbol as an external C function:

use std::os::raw::c_int;

unsafe extern "C" {
    fn rust_lib_double(x: c_int) -> c_int;
}

fn main() {
    let result = unsafe { rust_lib_double(21) };
    println!("Calling a Rust library from a Rust executable!");
    println!("21 * 2 = {}", result);
}

Building main.gpr builds the library first, then the executable:

$ gprbuild -p -P main.gpr
$ ./rust/target/x86_64-unknown-linux-gnu/release/main_from_rust
Calling a Rust library from a Rust executable!
21 * 2 = 42

Selecting a custom cargo

A GNAT installation provides a suitable cargo, which GPR finds on its own. A project normally has nothing to declare.

To use a different one, name it explicitly:

package Compiler is
   for Driver ("Rust") use "cargo";
end Compiler;

Cleaning

gprclean removes the artifacts Cargo built for the project, but leaves the Cargo target directory itself in place. The --remove-cargo-build-dir switch removes that directory too. Use it with caution: other projects may have built into the same directory, and they go with it.

Installing

Warning

gprinstall has no support for Rust projects: the artifacts Cargo produces are not installed.