Adding Ada to Rust
by Johannes Kliemann –
While writing every part of a software project in Ada or SPARK may provide the best safety and security, doing so requires significant effort and is not feasible for many projects. As we demonstrated in a previous blog post, one can use a C Software Development Kit (SDK)) to provide the base functionality of a project and only implement the application logic in Ada or SPARK.
While implementing the application logic in Ada or SPARK is an improvement over a pure C project, its weakest link is still the C code in the SDK. On the other hand there are many libraries, board support packages and SDKs written in Rust, easily usable with Cargo. So instead of building the Ada application on top of a C base, one could use a Rust base instead to combine the large catalog of ready-to-use software with Rust’s safety features, providing a much more solid base for an Ada project.
Using Cargo with external build systems #
The Rusts build system, Cargo, only supports Rust by default. However, Cargo allows the user to provide a build script called build.rs that can do any desired task, be it setting up a project directory or just calling another build system. Furthermore, Cargo allows passing custom flags to the Rust compiler and linker, enabling one to link with custom dynamic libraries:
println!(“cargo:rustc-link-lib=dylib=mycustomadalib”);
Thus, one can use Cargo to build an Ada/Rust program by compiling the Ada code outside of Cargo as a library and then manually linking the Ada library using the above line in our project’s build.rs.
However, having to build the Ada library manually every time is cumbersome. So we can add the Ada build step to build.rs too:
Command::new(“gprbuild”)
.args([“-j0”, “-p”, “-P”, “mycustomadalib.gpr”)
.spawn()
.unwrap()
.wait()
.unwrap()
Using the Library_Dir
, Library_Kind
and Library_Name
attributes defined in our project’s GPR file, we can link to the Ada library from Rust:
println!(“cargo:rustc-link-lib=dylib=mycustomadalib”);
println!(“cargo:rustc-link-search=/path/to/mycustomadalib_dir”);
While this does not look too difficult, it may become cumbersome if multiple projects are involved or if the project files generate different library names, kinds and directories depending on the environment or the passed options. Since the project file contains this information, why not directly take it from there?
Building Ada projects with the gpr crate #
To make things easier, I created the gpr crate in Rust. It takes libgpr2 and provides bindings for use in Cargo build scripts. It will load and parse the selected project file and provide the required information to build and link the Ada library, such as the default parameters for GPRbuild and the library paths, names and kinds.
Dependencies #
The gpr crate itself only requires alire to build itself. It does not provide a toolchain for the Ada project it is building so that must be provided separately.
Usage #
Using gpr is quite simple. The only requirement is to provide the path to the project file. The project file will provide all the information required for Rust to link to the library.
use gpr;
let project = gpr::Project::load(Path::new("/path/to/project.gpr")).unwrap();
Command::new("gprbuild")
.args(project.gprbuild_args().unwrap())
.spawn()
.unwrap()
.wait()
.unwrap();
println!(
"cargo:rustc-link-search{}",
project.library_dir().unwrap().to_str().unwrap()
);
println!(
"cargo:rustc-link-lib={}={}",
project.library_kind().unwrap(),
project.library_name().unwrap()
);
Building an example application #
To demonstrate the use of the gpr crate, let’s create a simple application in Rust that will print “Hello World” from both languages. To start, add the gpr crate as a build dependency:
[package]
name = "ada_hello"
version = "0.1.0"
edition = "2021"
[build-dependencies]
gpr = “0.1.0”
The implementation of the Ada library is straightforward,containing a single procedure that prints “Hello World”. We build the library as a dynamic library:
project Ada_Hello
is
for Source_Dirs use ("src");
for Object_Dir use "obj";
for Create_Missing_Dirs use "True";
for Library_Name use "adahello";
for Library_Kind use "dynamic";
for Library_Standalone use "encapsulated";
for Library_Interface use ("ada_hello");
for Library_Dir use "lib";
end Ada_Hello;
package Ada_Hello
is
procedure Hello with
Export,
Convention => C,
External_Name => "ada_hello";
end Ada_Hello;
with Ada.Text_IO;
package body Ada_Hello is
procedure Hello
is
begin
Ada.Text_IO.Put_Line ("Hello from Ada!");
end Hello;
end Ada_Hello;
A simple build script is required to build and link this library:
use gpr::Project;
use std::{path::Path, process::Command};
fn main() {
let ada_hello = Project::load(Path::new("ada_hello/ada_hello.gpr")).unwrap();
Command::new("gprbuild")
.args(ada_hello.gprbuild_args().unwrap())
.spawn()
.unwrap()
.wait()
.unwrap();
println!(
"cargo:rustc-link-search={}",
ada_hello.library_dir().unwrap().to_str().unwrap()
);
println!(
"cargo:rustc-link-lib={}={}",
ada_hello.library_kind().unwrap(),
ada_hello.library_name().unwrap()
);
}
And finally, the Rust main program that calls the Ada application:
extern "C" {
fn ada_hello();
}
fn main() {
println!("Hello from Rust!");
unsafe {
ada_hello();
}
}
Now the project is ready to be compiled and run. Since the Ada library is dynamic, the LD_LIBRARY_PATH
environment variable needs setting before running the example, as it would otherwise not find the library at runtime:
LD_LIBRARY_PATH=ada_hello/lib cargo run
Compiling gpr v0.1.0 (/.../gpr-rust)
Compiling ada_hello v0.1.0 (/.../gpr-rust/examples/ada_hello)
Finished dev [unoptimized + debuginfo] target(s) in 6.02s
Running `target/debug/ada_hello`
Hello from Rust!
Hello from Ada!
The gpr crate so far provides the basic functionality to build shared and static Ada libraries. If you find a bug or think a feature is missing, please open an issue at https://github.com/jklmnn/gpr-rust.