Rust for NDK development
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.myktappimport android.graphics.Bitmap
import android.os.Bundle
import android.widget.ImageView
import androidx.appcompat.app.AppCompatActivityclass 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