Martin Kimani Innovative frontend developer with five years of experience building responsive websites. Proficient in HTML, CSS, and JavaScript, as well as modern libraries and frameworks. I'm passionate about usability and I possess a working knowledge of graphic design.

Integrating a Rust module into an Android app

5 min read 1601

Integrate Rust Android App

Since I have experience in cross-compiling and building automation, I often choose Rust as my primary programming language. In addition, Rust is still a relatively new language, and utilizing any technology from the previous decade is setting you up to fail. Sometimes, following the hype train is the quickest route to success.

If you want one of your project’s selling points to be that users can control their own data, you can’t use a completely browser-based service. Instead, you’d need to deliver something that consumers can operate on their own Android devices. If you already have some headless instances running internally, with just a little more effort, you can develop redistributable packages for Windows and Linux.

However, keep in mind that only having a desktop version of the app would be a significant roadblock. If you want your app to really take off, you’d also need a mobile version. Therefore, you need to figure out how to make your app work on Android.

This GitHub repo, which has a basic, built-in Reverse Polish Notation calculator, demonstrates integrating a Rust module into an Android app. However, our project, which will look like the image below, will use a more low-level approach, directly using the C and Rust compilers. All issues related to cross-compilation, like compiler flags, linker flags, and more, are addressed explicitly.

We use Gradle mainly to assemble everything into an .apk package. If you’re looking for a quick and simple solution, you can use a Rust plugin for Gradle to considerably simplify the procedure. Let’s get started!

Rust Module Android Example

Table of contents

Prerequisites

First, we’ll cover some of the required tools and platforms needed for our project implementation. You’ll need:

  • Android Native Development Kit r21
  • GCC
  • Bash
  • Android Software Development Kit

You can get the Android tools from the Android Developer portal. Only the SDK is required, not the entire Android Studio installation. Be sure to get the r21 LTS release of the NDK, not the most recent r22 release.

Rust with cross-compilers

First, install Rust with rustup-init. Once Rust is installed, use rustup to install support for Android targets. To begin, you’ll need to obtain the Rust cross-compilers. Thankfully, Rust makes this extremely simple by calling the following:

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

Java runtime environment

Our example project utilizes Gradle to generate the .apk, which requires a JRE to run. While Android Studio provides you with the option to download and specify a JRE and Gradle to run with it, this project uses your system’s default, JRE.

Environment variables

You’ll also need Android development tools. Despite its 80+ MB size, the SDK package contains the minimal required tools, so you can utilize the SDK manager to add the recommended extras. Download the command-line tools archive from the Android Studio downloads page.

While Android can run native code, most apps are written in Java or Kotlin, as reflected in the SDK. Finally, to work with native code, you’ll need the Native Development Kit. There are numerous versions of the NDK available for download on the NDK downloads page, but as mentioned previously, we’ll want to use the r21 version.

Finally, set up the environment variables shown below before starting a build:

  • ANDROID_SDK_ROOT: Points to the Android SDK directory
  • ANDROID_NDK_ROOT: Points to the Android NDK r21 directory
  • ANDROID_API: Specifies the Android API level you want to target, i.e., 29

Directories

The leading directory contains scripts used to complete the build and tie it together.

android/

The android/ directory includes the Android app files, the AndroidManifest.xml file, activity with a rudimentary UI, and glue classes for interacting with the Rust code.

rust/

We’ll keep our Rust code in the rust/ folder. It is divided into three sections:

  • android.rs
  • lib.rs
  • main.rs

The app can be built as a standalone executable, which will read input from stdin one line at a time, evaluate it, and output the result.



Building the application and activities

To understand how to combine Rust code with the Java code, which provides the user interface, you can research “how to combine Rust code APK with Java code”. Off the bat, you’ll likely notice that it isn’t easily doable without understanding Android apps. At least, not in a clean and simple matter.

Most articles suggest a different approach: integrating the activities into a single application:

package pl.martin.blog.RustOnAndroid;

import androidx.appcompat.app.AppCompatActivity;
import android.graphics.Color;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity {
        private Button button;
        private EditText input;
        private TextView resultBox;

        private int colourRed;
       private int colourGreen;

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

                button = (Button)findViewById(R.id.button);
                input = (EditText)findViewById(R.id.exprInput);
                resultBox = (TextView)findViewById(R.id.exprResult);

                colourRed = Color.parseColor("#AA0000");
                colourGreen = Color.parseColor("#007F00");

                button.setOnClickListener(new View.OnClickListener() {
                        public void onClick(View v) {
                                String expr = input.getText().toString();
                                Result result = RpnCalculator.rpn(expr);
                                if(result.isOk()) {
                                        resultBox.setTextColor(colourGreen);
                                        resultBox.setText(result.getValue());
                                } else {
                                        resultBox.setTextColor(colourRed);
                                        resultBox.setText(result.getError());
                                }
                        }
                });
        }
}

If you’ve never dealt with Android before, activity is what you’d consider a screen or view while you’re developing your app. For example, consider a shopping app. Some activities may be the login screen and the checkout cart.

Some interactive components, like the iconic hamburger menu, may be included. You could theoretically put the entire program in a single activity if you wanted to, but this would be difficult to implement. Of course, there is more to discuss about activities, but that isn’t the focus of this article.

Now, let’s return to our Rust application. While integrating activities into one application seemed the solution to my dilemma, I wasn’t sure how my Rust.apk fit into all of this. After poking around in the cargo-apk code, I discovered that it wraps my code in some magic glue code and generates a NativeActivity for Android to run.

You may have to tamper with AndroidManifest.xml and add an activity node to the document to unite the activities into one app. When cargo-apk completes its task, it creates a small AndroidManifest.xml file and saves it alongside the final APK. You can use this file to find out what properties the NativeActivity has.

Go ahead and add the code below to the Java app’s manifest:

 <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="pl.martin.blog.RustOnAndroid">

<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.RustOnAndroid">
<activity android:name="pl.martin.blog.RustOnAndroid.MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>

</manifest>

The Java app’s build process will not automatically figure out where your libstartup.so file is included. You need to copy the library files to a particular directory, where Gradle, the Android app’s build mechanism, would automatically detect them. Doing so added a declaration to the app’s manifest stating that the app will contain certain activities:

$ mkdir -p android/app/src/main/jniLibs/arm64-v8a
$ cp sqlite-autoconf-3340000/.libs/libsqlite3.so android/app/src/main/jniLibs/arm64-v8a/
$ cp target/aarch64-linux-android/debug/libstatup.so android/app/src/main/jniLibs/arm64-v8a/
$ cd android/ &amp;&amp; ./gradlew &amp;&amp; ./gradlew build

After that, start the build. You’ll see that it is working, so you can install the .apk on your Android smartphone.

Completing the build

To complete the procedure, use the build-all.sh script. It will save the resulting .apk files next to this script in the build/ directory. You’ll build GMP and Rust code as well:

build-libgmp.sh

The script above is used for creating the GMP library only:

build-librpn.sh

This script is used to generate Rust code only. Note that you must first create GMP and the .so files. GMP must be present in LIBS DIR, otherwise, linking will fail:

build-apk.sh

Create only the .apk file using the script above. It is essential first to create GMP and the Rust code. The app will compile without native libraries, but it will crash when it is run.

Conclusion

It wasn’t easy getting our Rust code to operate on Android, but we found the solution. We wrote our code in standard Rust, then compiled it to a shared library, which the JVM then loaded at runtime. While the JNI first appeared frightening, employing this standardized method meant that neither the Java code nor the Gradle build system was necessary because we wrote the native code in Rust.

Cross-compiling with Cargo was still a challenge because we had to configure many environmental variables with cargo-apk to make it all work. There was also an issue with external libraries that our code relied upon, but we solved all of this with a few shell scripts.

Feel free to check out the GitHub repository on how to integrate a Rust module into an Android app. I hope you enjoyed this article, and be sure to leave a comment if you have any questions. Happy coding!

LogRocket: Full visibility into production Rust apps

Debugging Rust applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking performance of your Rust apps, automatically surfacing errors, and tracking slow network requests and load time, try LogRocket.

LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your Rust app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app’s performance, reporting metrics like client CPU load, client memory usage, and more.

Modernize how you debug your Rust apps — .

Martin Kimani Innovative frontend developer with five years of experience building responsive websites. Proficient in HTML, CSS, and JavaScript, as well as modern libraries and frameworks. I'm passionate about usability and I possess a working knowledge of graphic design.

Leave a Reply