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.