Expand description
§Safe JNI Bindings in Rust
This crate provides a (mostly) safe way to interface with Java and Kotlin using the Java Native Interface (JNI).
§Getting Started by implementing a native method
Lets write some Java code that defines a native method and then implement it in Rust.
§The Java side
First, you need a Java class definition. HelloWorld.java:
class HelloWorld {
// This declares that the static `hello` method will be provided
// by a native library.
private static native String hello(String input);
static {
// This actually loads the shared object that we'll be creating.
// The actual location of the .so or .dll may differ based on your
// platform.
System.loadLibrary("mylib");
}
// The rest is just regular ol' Java!
public static void main(String[] args) {
String output = HelloWorld.hello("World");
System.out.println(output);
}
}Compile this to a class file with javac HelloWorld.java.
Trying to run it now will give us the error Exception in thread "main" java.lang.UnsatisfiedLinkError: no mylib in java.library.path since we
haven’t written our native code yet.
§The Rust side
There are two approaches to implementing a native method in Rust.
In either case you first need to implement an extern "system" function
that accepts the appropriate arguments and has a matching return type.
And then to make the implementation visible to the JVM you can either:
- Export the function from a shared library with a mangled name that the JVM can look up.
- You can use the JNI API to explicitly register the implementation with
the JVM at runtime using
Env::register_native_methods(in this case the function name doesn’t need to be mangled or exported from a shared library)
The arguments and return type for a native method implementation are documented in the JNI specification under “Design” -> “Native Method Arguments”
https://docs.oracle.com/en/java/javase/11/docs/specs/jni/design.html#native-method-arguments
which can be summarized as:
- The first argument is always an implicit JNI environment pointer that
represents the current thread’s attachment to the Java VM
- Use
jni::EnvUnownedto capture this
- Use
- Next, if the native method is static, the second argument is a reference
to the class that owns the method.
- Use
jni::objects::JClassin this case - Alternatively, use
jni::sys::jclassif you want to work with the raw JNI type.
- Use
- Else, if the native method is non-static, the second argument is a
thisobject reference- Use
jni::objects::JObjector a more specific reference type likejni::objects::JStringin this case - Alternatively, use
jni::sys::jobjectif you want to work with the raw JNI type.
- Use
- The rest of the arguments are the Java arguments passed to the method:
- Java primitive types are passed as their corresponding JNI types (e.g.
jni::sys::jintforint) - Java object types are passed as their corresponding JNI reference
types (e.g.
jni::objects::JStringforjava.lang.String) - Alternatively you can use the raw
jni::sys::jobjecttype for Java object arguments if you want to work with the raw JNI types.
- Java primitive types are passed as their corresponding JNI types (e.g.
- The return type is the Java return type mapped in the same way as the arguments
§Exporting a native method from a shared library
Create your crate with cargo new mylib. This will create a directory
mylib that has everything needed to build a basic crate with cargo. We
need to make a couple of changes to Cargo.toml before we do anything else.
- Under
[dependencies], addjni = "0.22" - Add a new
[lib]section and under it,crate-type = ["cdylib"].
Now, if you run cargo build from inside the crate directory, you should
see a libmylib.so (if you’re on Linux) or a libmylib.dylib (if you are
on macOS) in the target/debug directory.
The last thing we need to do is implement our native method.
As an illustration, we’ll avoid using any of the helper macros (like
jni::jni_mangle or jni::native_method) so it’s clear how the
underlying API works.
Add this to your crate’s src/lib.rs:
// As we are going to implement a native method in Rust, the JVM is going
// to pass us a `jni_sys::JNIEnv` pointer that implicitly represents
// an attachment of the current thread to the Java VM.
//
// `EnvUnowned` is an FFI-safe type that lets us capture the pointer and also
// associate the caller's JNI stack frame with a lifetime.
//
// IMPORTANT: The first thing a native method must do is to upgrade this into
// an `Env`, which has a side effect of ensuring the `jni-rs` crate is initialized
// before any other JNI calls are made.
use jni::EnvUnowned;
// This is the JNI environment interface we'll call the majority of our methods on.
use jni::Env;
// These objects are what you should use as arguments to your native
// function. They carry extra lifetime information to prevent them escaping
// this context and getting used after being GC'd.
use jni::objects::{JClass, JString};
// This type represents an owned, JNI-compatible string.
use jni::strings::JNIString;
// This keeps Rust from "mangling" the name and making it unique for this crate.
#[unsafe(no_mangle)]
pub extern "system" fn Java_HelloWorld_hello<'caller>(
// This captures the raw `jni_sys::JNIEnv` pointer and associates it with the
// caller's JNI stack frame lifetime.
mut unowned_env: EnvUnowned<'caller>,
// This is the class that owns our static method. It's not going to be used,
// but still must be present to match the expected signature of a static
// native method.
//
// If we were implementing a non-static native method then this would be
// a `this: JObject<'caller>`, serving as a reference to the instance.
class: JClass<'caller>,
input: JString<'caller>)
-> JString<'caller>
{
// Before we can access JNI, jni-rs needs to know that the thread is
// attached to the Java VM.
//
// Within a native method we can assume the JVM attaches the thread
// before calling our implementation, and this is represented
// by the EnvUnowned type.
//
// We use `with_env` to upgrade the EnvUnowned to a Env, which gives us
// access to the full JNI API.
//
// IMPORTANT: Don't use `jni-rs` in any other way within a native method
// until you call `unowned_env.with_env` or `AttachGuard::from_unowned`.
//
// Internally `with_env()` creates a hidden `AttachGuard`` to track the thread
// attachment explicitly and this will also wrap the given closure with
// `catch_unwind` to ensure that your code can't panic and unwind across
// FFI boundaries.
let outcome = unowned_env.with_env(|env| -> Result<_, jni::errors::Error> {
// First, we have to get the string out of Java. Check out the `strings`
// module for more info on how this works.
let input: String = input.to_string();
// Then we have to create a new Java string to return. Again, more info
// in the `strings` module.
JString::from_str(env, format!("Hello, {}!", input))
});
// Finally, we have to resolve the `EnvOutcome` into a concrete return value.
//
// Our code above may have failed with a JNI error, or some other
// application-specific error, or it may have panicked. None of these things
// can pass over the FFI boundary back into the JVM.
//
// Mapping of errors and panics is done according to the selected
// `ErrorPolicy` trait implementation which is able to use JNI for throwing
// Java exceptions if necessary.
//
// This design lets you encapsulate your own approach to forwarding errors
// and panics to Java code and then easily reuse it across multiple native
// methods.
//
// In this case we use a built-in policy that throws a Java
// `RuntimeException` with a message containing the error/panic details.
outcome.resolve::<jni::errors::ThrowRuntimeExAndDefault>()
}The Java_HelloWorld_hello function name was mangled according to the JNI
Specification, under “Design” -> “Resolving Native Method Names”
https://docs.oracle.com/en/java/javase/11/docs/specs/jni/design.html#resolving-native-method-names
To avoid having to manually mangle the function name, the
jni::jni_mangle, jni::native_method and jni::bind_java_type
macros can all handle this for you.
§Final steps
That’s it! Build your crate and try to run your Java class again.
… Same error as before you say? Well that’s because JVM is looking for
mylib in all the wrong places. To tell it where to look you can pass this
argument to java: -Djava.library.path=/path/to/mylib/target/debug. Now,
you should get the expected output Hello, World! from your Java class.
For reference you can find this example in the
mylib-example directory of this repository:
§Launching JVM from Rust
It is possible to launch a JVM from a native process using the Invocation
API, provided by JavaVM.
§See Also
§Examples
- The examples directory in this repository contains a variety of examples that illustrate the utility macros provided by this crate.
- Example mylib project (as illustrated above)
- Our integration tests and benchmarks
§Macros
jni_str!for compile-time encoding of JNI stringsjni_sig!for compile-time encoding of JNI type signaturesbind_java_typefor generating full Rust bindings for Java typesnative_methodfor individual native method bindingsjni_manglefor mangling native method names
§JNI Documentation
- Java Native Interface Specification
- JNI tips — general tips on JNI development and some Android-specific
§Open-Source Users
- The Servo browser engine Android port
- The Exonum framework Java Binding
- MaidSafe Java Binding
§Other Projects Simplifying Java and Rust Communication
- Consider JNR if you just need to use a native library with C interface
- Watch OpenJDK Project Panama which aims to enable using native libraries with no JNI code
- Consider GraalVM — a recently released VM that gives zero-cost interoperability between various languages (including Java and Rust compiled into LLVM-bitcode)
- See a plugin for invoking cargo from Java Maven builds
Re-exports§
pub use jni_sys as sys;
Modules§
- descriptors
- Descriptors for classes and method IDs.
- elements
- Helpers for accessing array elements
- errors
- Error types and utilities.
- exceptions
- Reference type implementations and API bindings for various
java.langExceptions - ids
- JNI identifiers for methods and fields
- objects
- Reference type implementations and API bindings for various
java.langObjects - refs
- Base support for reference types and auto-release wrappers like
refs::Autoandrefs::Global. - signature
- Parser for java type signatures.
- strings
- Handling of strings in Java’s modified UTF-8 encoding, including conversion to and from Rust strings (which use standard UTF-8).
- vm
- Java VM interface.
Macros§
- bind_
java_ type - Binds a Java class to a Rust type with constructors, methods, fields, and native methods.
- jni_
cstr - Converts UTF-8 string literals to a MUTF-8 encoded CStr literal.
- jni_sig
- Parses a JNI method or field signature at compile time.
- jni_
sig_ cstr - Parses a JNI method or field signature at compile time and returns a CStr literal.
- jni_
sig_ jstr - Parses a JNI method or field signature at compile time and returns a
&'static JNIStr. - jni_
sig_ str - Parses a JNI method or field signature at compile time and returns a
&strliteral. - jni_str
- Converts UTF-8 string literals to a MUTF-8 encoded
&'static JNIStr. - native_
method - Create a compile-time type-checked
NativeMethodfor registering native methods with the JVM.
Structs§
- Env
- A non-transparent wrapper around a raw
sys::JNIEnvpointer that provides safe access to the Java Native Interface (JNI) functions. - EnvOutcome
- An opaque wrapper around an
Outcome<T, Error>that supports mapping errors within native methods (viaErrorPolicy), with access to anEnvreference. - EnvUnowned
- Represents an external (unowned) JNI stack frame and thread attachment that was passed to a native method call.
- JNIVersion
- JNI Version
- Monitor
Guard - Guard for a lock on a java object. This gets returned from the
lock_objmethod. - Native
Method - Native method descriptor for use with
Env::register_native_methods.
Enums§
- JValue
- A Java borrowed local reference or primitive value.
- JValue
Owned - A Java owned local reference or primitive value.
- Outcome
- The outcome of an operation that may succeed, fail with an error, or panic.
Type Aliases§
- JNIEnv
Deprecated - An FFI safe alias for
EnvUnownedfor (safer) compatibility with existing code.
Attribute Macros§
- jni_
mangle - Export a Rust function with a JNI-compatible, mangled method name.