Skip to main content

Crate jni

Crate jni 

Source
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:

  1. Export the function from a shared library with a mangled name that the JVM can look up.
  2. 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
  • Next, if the native method is static, the second argument is a reference to the class that owns the method.
  • Else, if the native method is non-static, the second argument is a this object reference
  • 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::jint for int)
    • Java object types are passed as their corresponding JNI reference types (e.g. jni::objects::JString for java.lang.String)
    • Alternatively you can use the raw jni::sys::jobject type for Java object arguments if you want to work with the raw JNI types.
  • 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], add jni = "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

§Macros

  • jni_str! for compile-time encoding of JNI strings
  • jni_sig! for compile-time encoding of JNI type signatures
  • bind_java_type for generating full Rust bindings for Java types
  • native_method for individual native method bindings
  • jni_mangle for mangling native method names

§JNI Documentation

§Open-Source Users

§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.lang Exceptions
ids
JNI identifiers for methods and fields
objects
Reference type implementations and API bindings for various java.lang Objects
refs
Base support for reference types and auto-release wrappers like refs::Auto and refs::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 &str literal.
jni_str
Converts UTF-8 string literals to a MUTF-8 encoded &'static JNIStr.
native_method
Create a compile-time type-checked NativeMethod for registering native methods with the JVM.

Structs§

Env
A non-transparent wrapper around a raw sys::JNIEnv pointer 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 (via ErrorPolicy), with access to an Env reference.
EnvUnowned
Represents an external (unowned) JNI stack frame and thread attachment that was passed to a native method call.
JNIVersion
JNI Version
MonitorGuard
Guard for a lock on a java object. This gets returned from the lock_obj method.
NativeMethod
Native method descriptor for use with Env::register_native_methods.

Enums§

JValue
A Java borrowed local reference or primitive value.
JValueOwned
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§

JNIEnvDeprecated
An FFI safe alias for EnvUnowned for (safer) compatibility with existing code.

Attribute Macros§

jni_mangle
Export a Rust function with a JNI-compatible, mangled method name.