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.