Rust interacts with other languages primarily using the C ABI (although it also supports other ABIs, C ABI is more widely used). Creating FFI (Foreign Function Interface) between Java or JVM platform languages and Rust is more complex compared to Python or JavaScript, where frameworks like PyO3 for Python and WASM/napi for JavaScript make integration seamless.
How does Java interact with Rust (or the JVM platform)?
Java interacts with native code like C/C++ through a technology called JNI (Java Native Interface). JNI is a standard programming interface provided by Java that allows Java code to interoperate with native code such as C, C++, or Rust. Its core goal is to provide Java programs with the ability to interact with underlying operating systems or high-performance native libraries. While JNI usage may be less common in Java web development, it’s extensively used in Android development.
In Rust, there is a library called jni-rs that enables interaction between Rust and JVM platform languages. However, jni-rs isn’t entirely plug-and-play; it merely wraps JNI functionality. To implement functions in Rust for the JVM platform, one must learn the details of JNI.
Type Mapping Table
Here is a type mapping table between JNI and Java:
| jni-rs | Java |
|---|---|
| jboolean | boolean |
| jbyte | byte |
| jchar | char |
| jshort | short |
| jint | int |
| jlong | long |
| jfloat | float |
| jdouble | double |
| jstring、JString | String |
| jobject、JObject | Object |
| jclass、JClass | Object |
| jarray | Array |
| jbooleanArray | boolean[] |
| jbyteArray | byte[] |
| jcharArray | char[] |
| jshortArray | short[] |
| jintArray | int[] |
| jlongArray | long[] |
| jfloatArray | float[] |
| jdoubleArray | double[] |
| jobjectArray | Object[] |
Function Namespace
We all know that Java’s namespace system is based on directories, e.g., com.kkch.example.HelloWorld corresponds to the file path com/kkch/example/HelloWorld.java. Additionally, Java is purely object-oriented.
Therefore, exported JNI functions must contain four key elements: Java, namespace, class name, and method name. For example, consider this function signature:
pub extern "C" fn Java_com_kkch_example_HelloWorld_new(env: JNIEnv, _class: JClass, java_pattern: JString ) -> jstring {}
This clearly describes a method com.kkch.example.HelloWorld.new(). Note that the first two parameters in an exported function—env: JNIEnv, _class: JClass—are mandatory and fixed.
With this foundation, you can now create functions that pass basic data types. For instance:
#[unsafe(no_mangle)]
pub extern "C" fn Java_com_kkch_example_HelloWorld_new(env: JNIEnv, _class: JClass, java_pattern: JString ) -> jstring {
let s = env.new_string("new function").unwrap();
s.into_raw()
}
I’ll use Android as an example here.
Preparation Steps
The following are required steps:
- On the Rust side, add targets suitable for Android:
rustup target add aarch64-linux-android armv7-linux-androideabi i686-linux-android
- In Android Studio, download the NDK. We will use the NDK as a cross-compiler. Configure the compiler in Rust as follows:
[target.aarch64-linux-android]
ar = "/home/kkch/Android/Sdk/ndk/27.2.12479018/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ar"
linker = "/home/kkch/Android/Sdk/ndk/27.2.12479018/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android33-clang"
#[target.'cfg(any(target_arch = "arm", target_arch = "aarch64"))']
#rustflags = ["-C", "target-feature=+fp16"]
- Create the class
com.kkch.example.HelloWorld. I’m directly using Kotlin:
package com.kkch.example
class HelloWorld {
init {
System.loadLibrary("example")
}
external fun new(): String
}
The name example depends on what your compiled library is named. For example, the shared library might be named libexample.so.
Finally, invoke it as follows:
val hello = HelloWorld()
val r = hello.new()
Log.d("output new", r)
Advanced Usage
The above examples demonstrate calling Rust code from the Java layer, but the reverse is also possible. Here’s how you can call Java code from Rust using JNI rules:
JNI Type Descriptor Mapping Table
| Descriptor | Type | Example & Description |
|---|---|---|
V | void | ()V: Method with no arguments and returns void |
I | int | (I)V: Method accepting an int argument and returns void |
B | byte | (B)V: Method accepting a byte argument and returns void |
C | char | (C)V: Method accepting a char argument and returns void |
D | double | (D)V: Method accepting a double argument and returns void |
F | float | (F)V: Method accepting a float argument and returns void |
J | long | (J)V: Method accepting a long argument and returns void |
S | short | (S)V: Method accepting a short argument and returns void |
Z | boolean | (Z)V: Method accepting a boolean argument and returns void |
[element-type | Array | ([I)V: Method accepting an int[] argument and returns void |
Lfully.qualified.ClassName; | Object | Ljava/lang/String;: Represents a String object |
- A 2D array uses
[[; for example,[[IrepresentsVec<Vec<i32>>. - Object types must start with
Land end with;.
Here’s an interface defined at the Java/Kotlin layer:
interface Callback {
fun ok(msg: String)
}
external fun call(callback: Callback)
Calling this interface from Rust looks like this:
#[unsafe(no_mangle)]
pub extern "C" fn Java_com_kkch_example_HelloWorld_call(
mut env: JNIEnv,
_class: JClass,
callback: JObject,
) {
let output = env.new_string("callback is ok").unwrap();
let args = [JValue::Object(&output)];
env.call_method(callback, "ok", "(Ljava/lang/String;)V", &args)
.unwrap();
}
Overall, interacting between Rust and JNI is more cumbersome compared to other languages.