Supercharging Your Android App with Rust Native Libraries

Before you start reading, click here to clone the project.

In the realm of Android app development, prioritizing code correctness and security is essential. While managed languages like Java and Kotlin are well-suited for Android app development, system-level programming often requires languages like C and C++. However, memory safety bugs in C and C++ pose ongoing challenges, contributing to stability issues and security vulnerabilities within the Android ecosystem. To address these concerns, the Android Open Source Project (AOSP) now supports the Rust programming language for developing the OS itself.

Rust, renowned for its memory safety guarantees, concurrency support, and impressive performance, presents a powerful solution for preventing memory bugs and enhancing overall code correctness. Through a combination of compile-time and runtime checks, Rust ensures valid memory accesses while delivering performance on par with C and C++. By seamlessly integrating Rust native libraries into Android apps, we can tap into the unique capabilities of Rust to bolster security, stability, and performance.

This comprehensive guide will explore the seamless integration of Rust native libraries into Android projects, empowering developers to leverage Rust’s memory safety features effectively. We will delve into the necessary tools, libraries, and techniques that enable developers to harness the power of Rust, creating safer and more efficient Android applications.

💡 The following instructions are for Linux, you might have to make adjustments for Windows/MacOS for the following instructions.

  • Project: rustapp
  • droid Application ID: com.example.rustapp
  • Native Library: rustnat

Creating an Android Project

💡 If you already have an existing Android project set up, you can skip this section

Setting up an Android project can be done using Android Studio’s "New Project" feature, or you can choose to set it up manually. We will setup a simple app with a "Hello" button. Here are the steps for manual setup:

Install Gradle and JDK. 💡 Recommended: Install Gradle v8.0.2 and JDK 17 to avoid compatibility issues.

Create a new project directory called rustapp and navigate into it. Now, initialize a Gradle project with default choices using command gradle init inside a terminal.

We need to create or edit a few files.

  • Graddle settings file settings.gradle

    pluginManagement {
        repositories {
            google()
            mavenCentral()
            gradlePluginPortal()
        }
    }
    
    dependencyResolutionManagement {
        repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
        repositories {
            google()
            mavenCentral()
        }
    }
    
    rootProject.name = "Rust App"
    include ':app'
  • Grandle properties gradle.properties

    org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
    android.useAndroidX=true
  • Build configuration app/build.gradle

    plugins {
        id 'com.android.application' version '8.0.2'
    }
    
    android {
        namespace 'com.example.rustapp'
        compileSdk 33
    
        defaultConfig {
            applicationId "com.example.rustapp"
            minSdk 24
            targetSdk 33
            versionCode 1
            versionName "1.0"
        }
    
        buildTypes {
            release {
                minifyEnabled true
                shrinkResources true
                proguardFiles getDefaultProguardFile('proguard-android-optimize.txt')
            }
        }
    
        compileOptions {
            sourceCompatibility JavaVersion.VERSION_1_8
            targetCompatibility JavaVersion.VERSION_1_8
        }
    }
    
    dependencies {
        implementation 'androidx.appcompat:appcompat:1.6.1'
    }
  • App manifest file app/src/main/AndroidManifest.xml

    
        <?xml version = "1.0" encoding = "utf-8"?>
        <manifest
            xmlns:android ="<http://schemas.android.com/apk/res/android>"
            xmlns:tools ="<http://schemas.android.com/tools >">
    
            <application 
                android:label ="Rust App"
                android:theme="@style/Theme.AppCompat">
                <activity 
                    android:name =".MainActivity" 
                    android:exported="true" >
                    <intent-filter>
                        <action android:name="android.intent.action.MAIN" />
                        <category android:name="android.intent.category.LAUNCHER" />
                    </intent-filter>
                </activity>
            </application>
    
        </manifest>
    
    • app/src/main/res/layout/activity_main.xml
    
        <?xml version = "1.0" encoding = "utf-8"?>
        <LinearLayout
            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"
            android:gravity ="center"
            android:orientation ="vertical"
            tools:context =".MainActivity" >
    
            <Button
                android:id ="@+id/helloButton"
                android:layout_height ="wrap_content"
                android:layout_width ="wrap_content"
                android:text ="Hello"
                android:textSize ="20sp" />
    
        </LinearLayout>
    
  • main activity file app/src/main/java/com/example/rustapp/MainActivity.java

    package com.example.rustapp;
    
    import android.content.Intent;
    import android.os.Bundle;
    
    import androidx.appcompat.app.AppCompatActivity;
    
    public class MainActivity extends AppCompatActivity {
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
        }
    }

Once done, your project structure will look something like this:

    rustapp
    ├─ app
    │   ├─ build.gradle
    │   └─ src
    │       └─ main
    │           ├─ AndroidManifest.xml
    │           ├─ java
    │           │   └─ com
    │           │       └─ example
    │           │           └─ rustapp
    │           │               └─ MainActivity.java
    │           └─ res
    │               └─ layout
    │                   └─ activity_main.xml
    ├─ gradle
    │   └─ wrapper
    │       ├─ gradle-wrapper.jar
    │       └─ gradle-wrapper.properties
    ├─ gradle.properties
    ├─ gradlew
    ├─ gradlew.bat
    └─ settings.gradle

You can now build the app using ./gradlew assembleDebug. To get a list of available commands, you can use ./gradlew tasks.

Integrating Rust Libraries

We will create a simple Rust function that returns some text, which will be called by MainActivity.java and displayed in a Toast. Following are the steps:

  1. Install rustup

  2. Install the stable toolchain

    rustup toolchain install stable
  3. Add the (required) android targets

    rustup target add aarch64-linux-android
    rustup target add armv7-linux-androideabi
    rustup target add i686-linux-android
    rustup target add x86_64-linux-android
  4. Set the following environment variables

    export JAVA_HOME=/usrfunction that can be called from
    export ANDROID_HOME=$HOME/.android_sdk

    💡 The java binary should be located in $JAVA_HOME/bin/

  5. Install NDK

    mkdir -p $ANDROID_HOME/ndk
    wget https://dl.google.com/android/repository/android-ndk-r25c-linux.zip -O temp.zip
    unzip -d $ANDROID_HOME/ndk temp.zip
    rm temp.zip
  6. Accept the SDK license

    mkdir $ANDROID_HOME/licenses
    echo "24333f8a63b6825ea9c5514f83c2829b004d1fee" > $ANDROID_HOME/licenses/android-sdk-license
  7. Initialize the Rust library

    cargo init --lib --name rustnat
  8. Create/Edit the following files:

    • Cargo.toml

      [lib]
      crate-type = ["dylib"]
      [dependencies]
      jni = "0.21.1"
      [profile.release]
      codegen-units = 1
      lto = true
      strip = true
    • src/lib.rs

    #[allow(non_snake_case)]
    pub mod android {
        use jni::{objects::JClass, sys::jstring, JNIEnv};
    
        #[no_mangle]
        pub extern "system" fn Java_com_example_rustapp_MainActivity_hello(
            env: JNIEnv,
            _: JClass,
        ) -> jstring {
            let message = env
                .new_string("Hello from Rust")
                .expect("Failed to create Java string");
    
            message.into_raw()
        }
    }

    💡 The function name follows a special format: Java&application_id&class_name&function&

    • app/src/main/java/com/example/rustapp/MainActivity.java
    import android.widget.Button;
    import android.widget.Toast;
    
    public class MainActivity extends AppCompatActivity {
        static {
            System.loadLibrary("rustnat");
        }
    
        private static native String hello();
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            :
            :
            :
            Button helloButton = findViewById(R.id.helloButton);
            helloButton.setOnClickListener(view -> Toast.makeText(this, hello(), Toast.LENGTH_SHORT).show());
        }
    }
    • app/build.gradle
    plugins {
        :
        id 'org.mozilla.rust-android-gradle.rust-android' version '0.9.3'
    }
    
    android {
        ndkVersion '25.2.9519653'
        :
        :
    }
    
    cargo {
        module  = '..'
        libname = 'rustnat'
        profile = gradle.startParameter.taskNames.any{it.toLowerCase().contains("debug")} ? "debug" : "release"
        targets = ['arm64'] // Available targets: 'arm', 'arm64', 'x86', 'x86_64'
    }
    preBuild.dependsOn 'cargoBuild'

💡 You can find the ndk version in $ANDROID_HOME/ndk

Now you can build the app.

References

Leave a Reply

Scroll to Top
%d bloggers like this: