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!
First, we’ll cover some of the required tools and platforms needed for our project implementation. You’ll need:
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.
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
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.
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 directoryANDROID_NDK_ROOT
: Points to the Android NDK r21 directoryANDROID_API
: Specifies the Android API level you want to target, i.e., 29The 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.
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/ && ./gradlew && ./gradlew build
After that, start the build. You’ll see that it is working, so you can install the .apk
on your Android smartphone.
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.
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!
Debugging Rust applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking the 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 application. 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 — start monitoring for free.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
Bypass anti-bot measures in Node.js with curl-impersonate. Learn how it mimics browsers to overcome bot detection for web scraping.
Handle frontend data discrepancies with eventual consistency using WebSockets, Docker Compose, and practical code examples.
Efficient initializing is crucial to smooth-running websites. One way to optimize that process is through lazy initialization in Rust 1.80.
One Reply to "Integrating a Rust module into an Android app"
Yeeeaah…. There’s a wee bit of presupposition of knowledge here that makes this a less-than-useful tutorial for someone just starting out. Speaking as someone who’s just spent many, many hours trying to even get the damn thing to BUILD, I can say with confidence that if you don’t have fairly extensive knowledge in both Rust AND Android (and a generous smattering of one of the C languages), these are not the droids you are looking for; move along.
Not sure if you know enough? Take this simple test to find out!
In this tutorial’s first step following the Table of Contents – Prerequisites – you’ll note the author specifies GCC is a requirement. Perfect! Get it installed and set up… without using a package manager like Homebrew/MacPorts, Cygwin, OpenPkg, etc. If you can muddle your way through that? You can probably finish this tutorial without wanting to kill someone when you reach the final step of “You may have to tamper with AndroidManifest.xml and add an activity node to the document to unite the activities into one app,” with literally NO further instructions, waaaaaaaay down at the bottom.
To me, at least, this feels like a tutorial began with the best of intentions, in which the author got bored halfway through and cut enough corners to yield a triangle from the original square.