Java/Android
This example shows off how to use rust to build a native library from Android and use it through an automatically generated JNI wrapper.
Android Studio can be used to work with Rust via its Rust plugin. So it's not a bad idea to integrate invocations of cargo into gradle, so you can build and run Rust inside an ordinary Java/Kotlin Android application.
Project Structure
The file build.rs
defines how flapigen generates wrapper code:
// build.rs
let swig_gen = flapigen::Generator::new(LanguageConfig::JavaConfig(
JavaConfig::new(
Path::new("app")
.join("src")
.join("main")
.join("java")
.join("net")
.join("akaame")
.join("myapplication"),
"net.akaame.myapplication".into(),
)
.use_null_annotation_from_package("android.support.annotation".into()),
))
.rustfmt_bindings(true);
The file src/lib.rs
contains real code that will be invoked from Java:
// src/lib.rs
struct Session {
a: i32,
}
impl Session {
pub fn new() -> Session {
#[cfg(target_os = "android")]
android_logger::init_once(
android_logger::Config::default()
.with_max_level(log::LevelFilter::Debug)
.with_tag("Hello"),
);
log_panics::init(); // log panics rather than printing them
info!("init log system - done");
Session { a: 2 }
}
pub fn add_and1(&self, val: i32) -> i32 {
self.a + val + 1
}
// Greeting with full, no-runtime-cost support for newlines and UTF-8
pub fn greet(to: &str) -> String {
format!("Hello {} ✋\nIt's a pleasure to meet you!", to)
}
}
And the file src/java_glue.rs.in
contains descriptions for flapigen to export this API to Java:
// src/java_glue.rs.in
foreign_class!(class Session {
self_type Session;
constructor Session::new() -> Session;
fn Session::add_and1(&self, val: i32) -> i32;
fn Session::greet(to: &str) -> String;
});
Then the app/build.gradle
contains rules to invoke cargo
to build a shared library from Rust code,
and then build it into apk:
// app/build.gradle
def rustBasePath = ".."
def archTriplets = [
'armeabi-v7a': 'armv7-linux-androideabi',
'arm64-v8a': 'aarch64-linux-android',
]
// TODO: only pass --release if buildType is release
archTriplets.each { arch, target ->
// execute cargo metadata and get path to target directory
tasks.create(name: "cargo-output-dir-${arch}", description: "Get cargo metadata") {
new ByteArrayOutputStream().withStream { os ->
exec {
commandLine 'cargo', 'metadata', '--format-version', '1'
workingDir rustBasePath
standardOutput = os
}
def outputAsString = os.toString()
def json = new groovy.json.JsonSlurper().parseText(outputAsString)
logger.info("cargo target directory: ${json.target_directory}")
project.ext.cargo_target_directory = json.target_directory
}
}
// Build with cargo
tasks.create(name: "cargo-build-${arch}", type: Exec, description: "Building core for ${arch}", dependsOn: "cargo-output-dir-${arch}") {
workingDir rustBasePath
commandLine 'cargo', 'build', "--target=${target}", '--release'
}
// Sync shared native dependencies
tasks.create(name: "sync-rust-deps-${arch}", type: Sync, dependsOn: "cargo-build-${arch}") {
from "${rustBasePath}/src/libs/${arch}"
include "*.so"
into "src/main/libs/${arch}"
}
// Copy build libs into this app's libs directory
tasks.create(name: "rust-deploy-${arch}", type: Copy, dependsOn: "sync-rust-deps-${arch}", description: "Copy rust libs for (${arch}) to jniLibs") {
from "${project.ext.cargo_target_directory}/${target}/release"
include "*.so"
into "src/main/libs/${arch}"
}
// Hook up tasks to execute before building java
tasks.withType(JavaCompile) {
compileTask -> compileTask.dependsOn "rust-deploy-${arch}"
}
preBuild.dependsOn "rust-deploy-${arch}"
// Hook up clean tasks
tasks.create(name: "clean-${arch}", type: Delete, description: "Deleting built libs for ${arch}", dependsOn: "cargo-output-dir-${arch}") {
delete fileTree("${project.ext.cargo_target_directory}/${target}/release") {
include '*.so'
}
}
clean.dependsOn "clean-${arch}"
}
Building
To build the demo, you will need the latest version of Cargo, Android NDK and install the proper Rust toolchain targets:
rustup target add arm-linux-androideabi
rustup target add aarch64-linux-android
To link Rust code into a shared library you need add the path to the proper clang binary into your PATH
environment variable or change the path to the linker here:
[target.aarch64-linux-android]
linker = "aarch64-linux-android21-clang++"
runner = "./run-on-android.sh"
[target.armv7-linux-androideabi]
linker = "armv7a-linux-androideabi21-clang++"
runner = "./run-on-android.sh"
Invocation
Gradle will take care of building and deploying the Rust sources. Thus, to build
the project in release mode, simply call ./gradlew androidRelease
.
To build only the rust libraries for a specific target, call cargo as usual, e.g.
cargo build --target arm-linux-androideabi
.
Testing
It is possible to run Rust unit tests on Android phone via run-on-android.sh
script mentioned in .cargo/config
,
there are also instrumentation unit tests in Java that invoke Rust code.