This example shows how to integrate Rust code in a Scala project thanks to the interoperability of Rust and Scala Native with C. The project is structured in the following way:
- There are two subprojects: native and core.
- The native project contains the Rust code and is managed using Cargo.
- The core project contains Scala code and is managed using SBT.
- Gradle is used to manage the two subprojects, it calls Cargo and SBT.
The Rust code exposes some public functions, like the following one:
#[no_mangle]
pub extern "C" fn divide(a: i32, b: i32) -> i32 {
a / b
}
The extern "C"
makes this function adhere to the C calling convention. The no_mangle
attribute turns off Rust's name mangling so that it has a well-defined symbol to link to. Then, to compile Rust code as a static library that can be called from C, the following was added to Cargo configuration:
[lib]
crate_type = ["staticlib"]
The Rust library can be compiled using the following command:
./gradlew cargoBuildRelease
cbindgen
can be used to generate C headers, this is useful to get the functions' signature, but it is not mandatory. In particular, there is a Gradle task that uses cbindgen
to generate the headers.
cbindgen
can be useful because the Scala code that uses the Rust implementation must comply with the related C functions' signature.
To generate C headers, run:
./gradlew generateHeaders
Given the following Rust module:
pub use operations::*;
pub mod operations {
#[no_mangle]
pub extern "C" fn divide(a: i32, b: i32) -> i32 {
a / b
}
#[no_mangle]
pub extern "C" fn generic_operation(
x: i32,
fun: fn(i32) -> i32
) -> i32 {
fun(x)
}
}
The following header will be generated:
/* Warning, this file is autogenerated by cbindgen. Don't modify this manually. */
#include <stdarg.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <stdlib.h>
int32_t divide(int32_t a, int32_t b);
int32_t generic_operation(int32_t x, int32_t (*fun)(int32_t));
The core project is built using Scala Native and SBT.
During compilation, native code is integrated in the final binary. In the SBT configuration are defined the clang parameters needed to link the Rust library.
nativeLinkingOptions ++= {
val path = s"${baseDirectory.value.getParentFile}/native/target/release"
val library = "operations"
Seq(s"-L$path", "-rpath", path, s"-l$library")
}
Scala Native will identify the library as a dependency that has native code and will unpack the library. Next, it will compile, link, and optimize any native code along with the Scala Native runtime and the application code. No additional information is needed in the build file other than the normal dependency so it is transparent to the library user.
The Rust module is wrapped in the following Scala object:
@extern
object Binding {
def divide(a: cInt, b: cInt): cInt = extern
def generic_operation(x: cInt, f: cFunIntToInt): cInt = extern
}
The functions in this object take advantage of the implementation provided in Rust and can be called like common Scala functions.
To run the project, use the following commands:
./gradlew cargoBuildRelease
./gradlew sbtRun
It is also possible to run the tests:
./gradlew cargoBuildRelease
./gradlew sbtTest