We’re all chasing smoother, more responsive mobile apps, but the usual optimization paths don’t always work. What if the real bottleneck isn’t your UI code but the heavy logic running underneath? This is where Rust comes in. It’s gaining a reputation for delivering bare-metal speed with memory safety, making it a strong contender for performance-critical mobile logic and possibly changing how we think about app efficiency.
Key Takeaways
- Get your dev environment set up by installing Rustup and the right tools for cross-compilation: the Android NDK or Xcode’s command-line tools.
- You’ll build a simple Rust library (
.sofor Android,.afor iOS) with Cargo, making sure to use specific target flags like, target aarch64-linux-androidor, target aarch64-apple-ios. - For Android, you’ll drop the
.sofile intojniLibsand then declare JNI methods, thankfully without writing a ton of boilerplate by hand. - On iOS, you can integrate the Rust lib with a Swift Package Manager module or just link the
.afile directly and set up a bridging header. - To prove it was worth it, you have to measure the performance bump. Use Android Studio Profiler or Instruments to see exactly how much faster your Rust module is compared to the native code.
1. Setting Up Your Rust Mobile Development Environment
Before you write a line of code, you need a properly configured environment. For mobile Rust development, this means preparing your system for cross-compilation, building code on your machine that will run on a completely different architecture, specifically the ARM-based processors inside smartphones.
First, get Rustup, the official toolchain installer. Open your terminal and run curl, proto '=https', tlsv1.2 -sSf https://sh.rustup.rs | sh and follow the prompts. This will give you both rustc (the Rust compiler) and cargo (Rust’s build system and package manager).
Next, you need the platform-specific targets. For Android, that means adding the Android NDK toolchains. The quickest way is through Android Studio: go to SDK Manager > SDK Tools and make sure “NDK (Side by side)” is installed. You’ll need to note the NDK’s path which is usually something like ~/Library/Android/sdk/ndk/25.1.8937393 on macOS or C:\Users\YourUser\AppData\Local\Android\Sdk\ndk\25.1.8937393 on Windows. With that done, add the Rust targets for Android: rustup target add aarch64-linux-android armv7-linux-androideabi i686-linux-android. For iOS, if you have Xcode installed (which provides the necessary command-line tools), you just need to add the iOS targets: rustup target add aarch64-apple-ios armv7s-apple-ios x86_64-apple-ios.
Pro Tip: Setting an environment variable like NDK_HOME or ANDROID_NDK_HOME to your NDK path will save a lot of headaches in build scripts later. For instance, on macOS, adding export ANDROID_NDK_HOME=$HOME/Library/Android/sdk/ndk/25.1.8937393 to your .zshrc or .bash_profile is a good move.
Common Mistake: People often forget to install toolchains for all the Android ABIs (Application Binary Interfaces). If a build only targets aarch64-linux-android, the app will simply fail on older devices or emulators that need armv7 or x86.
2. Crafting Your High-Performance Rust Library
Now that the environment’s set, let’s build a Rust library. We’ll start with a function for a computationally heavy task, think prime number generation or complex matrix operations, the kind of thing that can bog down a JS or even a Java/Kotlin thread. For this demo, we’ll create a function to calculate the nth Fibonacci number using a recursive approach, which is an intentionally inefficient method that will make the performance difference obvious.
Pop open the terminal and create a new library project with cargo new, lib my_rust_lib, then cd my_rust_lib. In src/lib.rs, drop this code:
#[no_mangle]
pub extern "C" fn calculate_fibonacci_n(n: i32) -> i32 { if n <= 0 { return 0; } else if n == 1 { return 1; } else { return calculate_fibonacci_n(n - 1) + calculate_fibonacci_n(n - 2); }
}
The #[no_mangle] attribute is key because it stops the Rust compiler from changing the function's name, which ensures it can be found and called from other languages like Java (via JNI) or Swift. Then, pub extern "C" makes the function public and tells it to use the C calling convention, the standard for any FFI (Foreign Function Interface) work.
Next, we have to tell Cargo what kind of library to build by editing Cargo.toml. Add this:
[lib]
crate-type = ["cdylib"] # For dynamic library (.so, .dylib)
# crate-type = ["staticlib"] # For static library (.a)
A dynamic library (cdylib) is usually easier for Android since it just gets packed into the APK. iOS projects, on the other hand, tend to prefer static libraries (staticlib), especially when you're using Swift Package Manager. We'll stick with cdylib for our Android example.
Pro Tip: If you're passing more than just simple numbers back and forth, look into libraries like jni for the Rust-Android connection or cxx for Rust-C++ interop. They make marshaling complex data types between Rust and the mobile platforms much less painful.
3. Integrating Rust into an Android Application
Getting the Rust library into an Android project means building it for the right Android targets and then loading it up through JNI (Java Native Interface).
First, build the lib for Android. From your my_rust_lib directory, run:
cargo build, target aarch64-linux-android, release
This command compiles your code for 64-bit ARM Android devices in release mode (which turns on performance optimizations), and the output will land in target/aarch64-linux-android/release/libmy_rust_lib.so. Remember to repeat this for other ABIs if you need to support them.
Jump over to your Android Studio project. Inside the `app/` module, you'll need to create a directory path like app/src/main/jniLibs/aarch64-v8a/. Now, just copy your libmy_rust_lib.so file into that aarch64-v8a folder. Android Studio knows to look here and will package it automatically.
Time to call it from your code. In a Kotlin file like `MainActivity.kt`, you'll declare the native method and load the library:
package com.example.myandroidapp import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.TextView class MainActivity : AppCompatActivity() { init { System.loadLibrary("my_rust_lib") // Load the Rust library } external fun calculateFibonacciN(n: Int): Int // Declare the native function override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) val resultTextView: TextView = findViewById(R.id.resultTextView) val n = 40 // Example input val startTime = System.nanoTime() val fibResult = calculateFibonacciN(n) // Call the Rust function val endTime = System.nanoTime() val duration = (endTime - startTime) / 1_000_000.0 // milliseconds resultTextView.text = "Fibonacci($n) = $fibResult\nCalculated in %.2f ms".format(duration) }
}
Just make sure you actually have a TextView in your activity_main.xml with the id resultTextView. When you run the app, your Kotlin code makes a direct call to the Rust function, and that's where the mobile performance boost comes from, since you're skipping layers of interpretation.
Common Mistake: Mismatched function signatures will kill your app instantly. If the Rust function is expecting an i32 but the Kotlin code sends a Long, it's going to crash. The types must align perfectly across the boundary.
4. Integrating Rust into an iOS Application
Hooking Rust into an iOS app (Swift/Objective-C) usually means building a static library and linking it, which is the path we'll take for this example.
First, flip your `my_rust_lib` project over to build a static library in Cargo.toml:
[lib]
crate-type = ["staticlib"]
Now build it for iOS from the `my_rust_lib` directory:
cargo build, target aarch64-apple-ios, release
This will create a libmy_rust_lib.a file in target/aarch64-apple-ios/release/ for 64-bit ARM iOS devices. Over in Xcode, just drag libmy_rust_lib.a into your project navigator. Make sure "Copy items if needed" is checked and your app target is selected. The next piece you need is a bridging header so Swift knows the C-style Rust function exists.
Add a new Header File to your project (File > New > File > Header File) and name it something like YourProjectName-Bridging-Header.h. Inside, declare the function:
#ifndef YourProjectName_Bridging_Header_h
#define YourProjectName_Bridging_Header_h int calculate_fibonacci_n(int n); // Declare the Rust function #endif /* YourProjectName_Bridging_Header_h */
Go to your project's Build Settings, find Swift Compiler - General, and point the Objective-C Bridging Header setting to the path of your new header file (e.g., YourProjectName/YourProjectName-Bridging-Header.h).
And finally, you can call it from Swift, for example in your `ViewController.swift`:
import UIKit class ViewController: UIViewController { @IBOutlet weak var resultLabel: UILabel! override func viewDidLoad() { super.viewDidLoad() let n: Int32 = 40 // Example input let startTime = CFAbsoluteTimeGetCurrent() let fibResult = calculate_fibonacci_n(n) // Call the Rust function let endTime = CFAbsoluteTimeGetCurrent() let duration = (endTime - startTime) * 1000 // milliseconds resultLabel.text = "Fibonacci(\(n)) = \(fibResult)\nCalculated in \(String(format: "%.2f", duration)) ms" }
}
Connect a UILabel to the resultLabel IBOutlet in your storyboard, and when you run the app, it'll call the Rust code from Swift. I'll admit the iOS setup feels a bit more manual compared to Android's JNI discovery, but you get the same compelling performance benefits.
Pro Tip: For bigger projects on iOS, wrapping the Rust library in a Swift Package is a much cleaner approach. This gives you better dependency management and organization. You'd set up a Swift package that bundles your pre-compiled Rust .a file along with a module map. Apple's own guide on Distributing Binary Frameworks as Swift Packages is the place to start for that.
5. Benchmarking and Profiling Rust's Impact
Getting the code to run is one thing. Success is proving it runs faster. You have to benchmark and profile to actually quantify the mobile performance gains you got from using Rust.
On Android, the Android Studio Profiler is the tool. Run the app, open the Profiler, and look at the CPU profiler. After recording CPU activity, the call stack should show calls going into libmy_rust_lib.so and your calculate_fibonacci_n function. The real proof is comparing the execution time of the Rust version against a pure Kotlin/Java implementation of the same algorithm. I've personally seen speedups from 2x to 5x for heavy CPU tasks when porting them from Kotlin to Rust, especially for algorithms that can really take advantage of Rust's zero-cost abstractions and direct memory management.
On the iOS side, Apple's Instruments (Xcode > Open Developer Tool > Instruments) is what you'll use. The "Time Profiler" template is what you want. After attaching Instruments to your app, trigger the Rust function. The profiler will lay out the call stack and show exactly how much time was spent inside each function, and you'll be able to see the time spent within libmy_rust_lib.a clear as day. This hard data is what you need to justify the integration work to anyone who asks.
When you're running benchmarks, consistency is everything:
- Always test on a physical device, because emulator performance is often misleading.
- Take the average of multiple runs to smooth out any random system fluctuations.
- Make sure to test both debug and release builds. The optimizations in a release build will always make it faster.
- And try to isolate the code being tested to reduce noise from other parts of the app.
Here's something people often forget: the overhead of the FFI call itself (the cost of jumping from Java/Swift into Rust) can sometimes eat up the performance gain if the function you're calling is super trivial. Rust really pays off for complex, CPU-heavy logic where the work done inside the Rust function is much, much longer than the time it takes to cross that language boundary.
So, using Rust for specific, high-performance logic is a great option for mobile devs struggling with CPU-bound problems. It can deliver big speed boosts that users will actually feel in a smoother user experience. Yes, the initial setup with cross-compilation and FFI requires some focused effort, but being able to run heavy algorithms at near-native speed makes it a solid investment for the parts of your app that really need it.
Rust vs. C++ for mobile performance:
Rust gives you memory safety without needing a garbage collector, which helps you avoid whole classes of C++ bugs like null pointer errors and data races. This means your high-performance code is more stable and secure from the start, saving you a ton of debugging time and potential security headaches.
Which mobile tasks are best for Rust?
Anything that hammers the CPU is a good candidate for Rust. Think image and video processing, heavy encryption/decryption, real-time audio manipulation, game physics engines, parsing huge data files, or running machine learning models. If it needs a lot of CPU time and tight memory control, Rust is a great fit.
How much does Rust add to app size?
A Rust binary might be a bit larger than a super-optimized C one, but modern Rust compilers have gotten really good at optimizing for size. For a dynamic library, the size increase is usually pretty reasonable, especially given the performance win. You can also shrink the binary further by tweaking settings like lto = "fat" and codegen-units = 1 in your Cargo.toml.
Can I write my whole mobile app in Rust?
You could, but it's not the common approach. Most developers use Rust for specific, performance-critical modules inside their existing Swift/Kotlin/Java app. This lets you surgically optimize the parts that need it without having to throw out your entire codebase.
What tools make Rust mobile dev easier?
Besides the standard Cargo and Rustup, there are tools that make life easier. cargo-ndk is great for automating Android builds across different ABIs. For iOS, cargo-lipo can help create a single "universal" static library that works on multiple iOS architectures. These tools handle a lot of the tedious, platform-specific build steps for you.