Rust for NDK development
By: Hoang Phan
Tags: Android Development, Ndk, Rust

The following are examples to render Fractal images 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.

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.

Build and Run project

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 the method simply makes a call to that native function Java_com_example_myktapp_MainActivity_greeting

private external fun greeting(): String

We need to call the function to get a 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 the project again

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 module 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 the project again

Find the code