Deploying Rust app on android.

Friday, March 27, 2020

Hey everyone and welcome!

Let’s run our rust app on Android.
If you will search this query on the web, you will find only Mozilla’s 2017 blog post and some copypastes. Some rules have changed (but a little).
I’am working on Windows 10 with bash as cmd tool, which is bundled with git.

What you need is:

cargo new pong
cd pong

rustup target add aarch64-linux-android armv7-linux-androideabi i686-linux-android

We add 3 most popular android architectures as build targets.
After you need to configure your system variables.

  • Set the environment variable JAVA_HOME to the JDK location, typically C:\Program Files\Android\Android Studio\jre.
  • Set the environment variable ANDROID_HOME to the Android SDK location, typically C:\Users[username]\AppData\Local\Android\Sdk.
  • Set the environment variable ANDROID_NDK_HOME to the Android NDK location, typically C:\Users[username]\AppData\Local\Android\Sdk\ndk-bundle.
  • Add the JDK tools directory to your PATH, typically C:\Program Files\Android\Android Studio\jre\bin.
  • Add the Android SDK platform-tools directory to your PATH, typically C:\Users[username]\AppData\Local\Android\Sdk\platform-tools.
  • Add the Android SDK tools directory to your PATH, typically C:\Users<username>\AppData\Local\Android\Sdk\tools.

Open Android Studio and download NDK, LLDB, CMake

SDK SDK

As SDK platform use 21 version (Android 5.0).

Create a new project in android

New project

Finish creation

Now create file called andorid_lib.rs in folder src and put next code there:

use std::os::raw::{c_char};
use std::ffi::{CString, CStr};

#[no_mangle]
pub extern fn rust_greeting(to: *const c_char) -> *mut c_char {
    let c_str = unsafe { CStr::from_ptr(to) };
    let recipient = match c_str.to_str() {
        Err(_) => "there",
        Ok(string) => string,
    };

    CString::new("Rust says ".to_owned() + recipient).unwrap().into_raw()
}

Let’s evaluate not ordinary syntax stuff here:

  1. #[no_mangle] tells the compiler not to mangle the function name as it usually does by default, ensuring our function name is exported as if it had been written in C.
  2. extern tells the Rust compiler that this function will be called from outside of Rust and to therefore ensure that it is compiled using C calling conventions.
  3. The string that rust_greeting accepts is a pointer to a C char array. We have to then convert the string from a C string to a Rust str.

Now go to your Android app and create new class:

New class

Put next code in your class

public class RustBindings {

    private static native String greeting(final String pattern);

    public String sayHello(String to) {
        return greeting(to);
    }
}

We want to expose our functions through JNI. The way that JNI constructs the name of the function that it will call is Java_<domain>_<class>_<methodname>. In the case of the method, greeting that we have declared here, the function in our Rust library that JNI will attempt to call will be Java_com_krupitskas_pong_RustBindings_greeting.

Next go back to rust and add next code to the bottom of lib.rs file

#[cfg(target_os="android")]
#[allow(non_snake_case)]
pub mod android {
    extern crate jni;

    use super::*;
    use self::jni::JNIEnv;
    use self::jni::objects::{JClass, JString};
    use self::jni::sys::{jstring};

    #[no_mangle]
    pub unsafe extern fn Java_com_krupitskas_pong_RustBindings_greeting(env: JNIEnv, _: JClass, java_pattern: JString) -> jstring {
        // Our Java companion code might pass-in "world" as a string, hence the name.
        let world = rust_greeting(env.get_string(java_pattern).expect("invalid pattern string").as_ptr());
        // Retake pointer so that we can use it below and allow memory to be freed when it goes out of scope.
        let world_ptr = CString::from_raw(world);
        let output = env.new_string(world_ptr.to_str().unwrap()).expect("Couldn't create java string!");

        output.into_inner()
    }
}
  1. #[cfg(target_os="android")] is telling the compiler to target Android when compiling this module. #[cfg] is a special attribute that allows you to compile code based on a flag passed to the compiler.
  2. #[allow(non_snake_case)] tells the compiler not to warn if we are not using snake_case for a variable or function name.

We declared that we needed the jni crate, that means we need to include the crate in the Cargo.toml file. Open it up and add the following between the [package] and [lib] declarations.

[target.'cfg(target_os="android")'.dependencies]
jni = "0.16"

We also need to tell the compiler what type of a library it should produce. You can specify this in the Cargo.toml files [lib] section:

```toml
[lib]
name = "pong_android"
crate-type = ["dylib"]
path = "src/android_lib.rs"

Now we need to produce Rust dynamic libraries and load it via android app runtime. Add next code to your MainActivity class.

static {
    System.loadLibrary("pong_android");
}

This looks for a library called libpong_android.so inside the jniLibs directory and picks the correct one for the current architecture.

In android project find your activity_main.xml

Click on TextView “HelloWorld” and in attributes in id field write helloWorldField.

And let’s finish MainActivity class.

package com.krupitskas.pong;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity {
    static {
        System.loadLibrary("pong_android");
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        RustBindings g = new RustBindings();
        String r = g.sayHello(" hello to java class!");
        ((TextView)findViewById(R.id.helloWorldText)).setText(r);
    }
}

Here we basically on finish line. Now we need only actual glue, which will call build of our rust project and copy all libraries.

This is rust android gradle

Going to your build.gradle of project Pong and put

maven {
    url "https://plugins.gradle.org/m2/"
}

under repositories and

classpath 'gradle.plugin.org.mozilla.rust-android-gradle:plugin:0.8.3'

under dependencies.
Now go to your project’s android main repository and put ander android { ... }

apply plugin: 'org.mozilla.rust-android-gradle.rust-android'

cargo {
    module  = "../../../"
    libname = "pong_android"
    targets = ["arm64", "x86", "x86_64", "arm"]
    prebuiltToolchains = true
    apiLevel = 21
}

And final stroke. Let’s automatically call our rust project build, when building android app.

Add

preBuild.dependsOn "cargoBuild"

in your module’s build.gradle, under dependencies section.

Boom. done.

Now lets test it. Make any compatible emulator or just connect a phone. Press run, it will install new app called Pong on your phone and instantly launch it.

Code is here -> github click

oculus questvrrustandroid

Sublime Text 3 - Rust

Thoughts about VR.

comments powered by Disqus