Rust for NDK development


The following are examples to render Fractal image in Android bitmap with Rust.

In order to do Android development, we’ll need to set up our Android environment. First we need to install Android Studio. Once Android Studio is installed, we’ll need to install the NDK (Native Development Kit).

First, create a new Kotlin Android Project for your application
Open Android Studio and click Start a new Android Studio project on the welcome screen or File | New | New project. kt_step01
Select an activity that defines the behavior of your application. For your first “Hello world” application, select Empty Activity that just shows a screen, and click Next. kt_step01
Build and Run project kt_run
Open Android Studio. From the toolbar, go to Android Studio > Preferences > Appearance & Behaviour > Android SDK > SDK Tools. Check the following options for installation and click OK.

* Android SDK Tools
* NDK
* CMake
* LLDB

Once the NDK and associated tools have been installed, we need to set a few environment variables, first for the SDK path and the second for the NDK path. Set the following env vars

export ANDROID_HOME=/Users/$USER/Library/Android/sdk
export NDK_HOME=$ANDROID_HOME/ndk-bundle


If you do not already have Rust installed, we need to do this now. For this we will be using rustup. rustup installs Rust from the official release channels and enables you to easily switch between different release versions. It will be useful to you for all your future Rust development, not just here. rustup can also be used in conjunction with HomeBrew.

curl https://sh.rustup.rs -sSf | sh


Create rust project

cargo new rust-lib && cd rust-lib


The next step is to create standalone versions of the NDK for us to compile against. We need to do this for each of the architectures we want to compile against. We will be using the make_standalone_toolchain.py script inside the main Android NDK in order to do this Now let’s create our standalone NDKs. There is no need to be inside the NDK directory once you have created it to do this.

mkdir NDK && cd NDK
${NDK_HOME}/build/tools/make_standalone_toolchain.py --api 26 --arch arm64 --install-dir arm64
${NDK_HOME}/build/tools/make_standalone_toolchain.py --api 26 --arch arm --install-dir arm
${NDK_HOME}/build/tools/make_standalone_toolchain.py --api 26 --arch x86 --install-dir x86


Create a new file, cargo-config.toml This file will tell cargo where to look for the NDKs during cross compilation. Add the following content to the file, remembering to replace instances of <project path> with the path to your project directory.

[target.aarch64-linux-android]
ar = "<project path>/rust-lib/NDK/arm64/bin/aarch64-linux-android-ar"
linker = "<project path>/greetings/NDK/arm64/bin/aarch64-linux-android-clang"

[target.armv7-linux-androideabi]
ar = "<project path>/rust-lib/NDK/arm/bin/arm-linux-androideabi-ar"
linker = "<project path>/greetings/NDK/arm/bin/arm-linux-androideabi-clang"

[target.i686-linux-android]
ar = "<project path>rust-lib/rust-lib/NDK/x86/bin/i686-linux-android-ar"
linker = "<project path>/greetings/NDK/x86/bin/i686-linux-android-clang"


In order for cargo to see our new SDK’s we need to copy this config file to our .cargo directory like this:

cp cargo-config.toml ~/.cargo/config


Let’s go ahead and add our newly created Android architectures to rustup so we can use them during cross compilation:

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


Open rust-lib/Cargo.toml and modify the config.

[dependencies]
[target.'cfg(target_os="android")'.dependencies]
jni = { version = "0.5", default-features = false }

[lib]
name = "rust"
crate-type = ["dylib"]


Open rust-lib/src/lib.rs. At the bottom of the file add the following code:

/// Expose the JNI interface for android below
#[cfg(target_os = "android")]
#[allow(non_snake_case)]
pub mod android {
    extern crate jni;

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

    #[no_mangle]
    pub unsafe extern fn Java_com_example_myktapp_MainActivity_greeting(env: JNIEnv, _: JClass) -> jstring {
        let world_ptr = CString::new("Hello world from Rust world").unwrap();
        let output = env.new_string(world_ptr.to_str().unwrap()).expect("Couldn't create java string!");
        output.into_inner()
    }
}


Build library for Android x86 (Simulator)

cargo build --target i686-linux-android --release


Link dynamic library to Android project

cd app/src/main
ln -s <project_path>/rust-lib/target/i686-linux-android/release/librust.so jniLibs/x86/librust.so


Edit layout activity_main.xml to add id for TextView

<TextView
        android:id="@+id/txtHello"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />


In MainActivity.kt file, add the following code. Here we are defining the native interface to our Rust. The greeting method simply makes a call to that native function Java_com_example_myktapp_MainActivity_greeting

private external fun greeting(): String


We need to call function to get message from Rust and load our Rust library

class MainActivity : AppCompatActivity() {

    private external fun greeting(): String

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val textView: TextView = findViewById(R.id.txtHello)
        textView.text = greeting()
    }

    companion object {
        init {
            System.loadLibrary("rust")
        }
    }

}


Build and run project again kt_run
Edit layout main_activity.xml to add BitmapView

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <ImageView
        android:id="@+id/imageView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>


In MainActivity.kt file, add the following code. Here we are defining the native interface to our Rust. The renderFractal method simply makes a call to that native function and create new Bitmap that will be modified by the native function

package com.example.myktapp

import android.graphics.Bitmap
import android.os.Bundle
import android.widget.ImageView
import androidx.appcompat.app.AppCompatActivity

class MainActivity : AppCompatActivity() {

    private external fun renderFractal(bitmap: Bitmap)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val bitmap = Bitmap.createBitmap(800, 800, Bitmap.Config.ARGB_8888)
        renderFractal(bitmap)
        val imageView: ImageView = findViewById(R.id.imageView)
        imageView.setImageBitmap(bitmap)
    }

    companion object {
        init {
            System.loadLibrary("rust")
        }
    }

}


Open rust-lib/Cargo.toml and add image and num-complex crate to dependencies

[dependencies]
[target.'cfg(target_os="android")'.dependencies]
jni = { version = "0.5", default-features = false }
image = "0.22.0"
num-complex = "0.2"


Open rust-lib/lib.rs and add the following code inside android mod, graphic mod just a Rust port of NDK C++ Bitmap header, you can find the code for fractal::render on fractal.rs

#[no_mangle]
pub unsafe extern fn Java_com_example_myktapp_MainActivity_renderFractal(env: JNIEnv, _: JClass, bmp: JObject) {
    let mut info = graphic::AndroidBitmapInfo::new();
    let raw_env = env.get_native_interface();

    let bmp = bmp.into_inner();

    // Read bitmap info
    graphic::bitmap_get_info(raw_env, bmp, &mut info);
    let mut pixels = 0 as *mut c_void;

    // Lock pixel for draw
    graphic::bitmap_lock_pixels(raw_env, bmp, &mut pixels);

    let pixels =
        std::slice::from_raw_parts_mut(pixels as *mut u8, (info.stride * info.height) as usize);

    fractal::render(pixels, info.width as u32, info.height as u32);
    graphic::bitmap_unlock_pixels(raw_env, bmp);
}


Build and run project again kt_run

Find the code

References
- Rust on Android
- Fractal example