Here is a simple one-file project to demonstrate how to use the jni crate:
Java side
package org.example.mcve.standalone;
public class Mcve {
static {
System.load("/Users/svetlin/CLionProjects/mcve/target/debug/libmcve.dylib");
}
public static void main(String[] args) throws Exception {
doStuffInNative();
}
public static native void doStuffInNative();
public static void callback() {
System.out.println("Called From JNI");
}
}
Load the native library on startup. I'm using load which requires an absolute path. Alternatively you can use loadLibrary which requires just the name of the library, but on the other hand requires it to be in a specific location.
In order to be able to call the native method from Java, you have to find what signature to use in your library. In order to do that you have to generate a C header file. This can be done in the following way:
cd src/main/java/org/example/mcve/standalone/
javac -h Mcve.java
As a result you should get a file that looks like
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class org_example_mcve_standalone_Mcve */
#ifndef _Included_org_example_mcve_standalone_Mcve
#define _Included_org_example_mcve_standalone_Mcve
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: org_example_mcve_standalone_Mcve
* Method: doStuffInNative
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_org_example_mcve_standalone_Mcve_doStuffInNative
(JNIEnv *, jclass);
#ifdef __cplusplus
}
#endif
#endif
Rust side
Now that we know the required method signature, we can create our Rust library! First create a Cargo.toml with crate_type = "cdylib":
[package]
name = "mcve"
version = "0.1.0"
authors = ["Svetlin Zarev <svetlin.zarev@hidden.com>"]
edition = "2018"
[dependencies]
jni = "0.12.3"
[lib]
crate_type = ["cdylib"]
Then add a lib.rs file with the following content:
use jni::objects::JClass;
use jni::JNIEnv;
#[no_mangle]
#[allow(non_snake_case)]
pub extern "system" fn Java_org_example_mcve_standalone_Mcve_doStuffInNative(
env: JNIEnv,
_class: JClass,
) {
let class = env
.find_class("org/example/mcve/standalone/Mcve")
.expect("Failed to load the target class");
let result = env.call_static_method(class, "callback", "()V", &[]);
result.map_err(|e| e.to_string()).unwrap();
}
Note that we used the ugly method name and signature from the generated header file. Otherwise the JVM would not be able to find our method.
First we load the required class. In this case it's not really necessary, as we have the very same class passed as the parameter named _class. Then we call the desired java method using the env we've received as a parameter.
The first argument is the target class.
The second - the target method name.
The third - describes the parameter types and return values: (arguments)return-type. You can find out more about that fancy syntax and arcane letters here In our case we do not have any parameters and the return type is V which means VOID
The fourth - an array that contains the actual arguments. As the method does not expect any, we pass an empty array.
Now build the Rust library and then run the Java application. As a result you have to see in your terminal Called From JNI
Calling Java from main() in Rust
First you have to spawn a JVM instance. You have to use the "invocation" feature on the jni crate:
[dependencies.jni]
version = "0.12.3"
features = ["invocation", "default"]
You may want to customize the jvm settings using .option():
fn main() {
let jvm_args = InitArgsBuilder::new()
.version(JNIVersion::V8)
.option("-Xcheck:jni")
.build()
.unwrap();
let jvm = JavaVM::new(jvm_args).unwrap();
let guard = jvm.attach_current_thread().unwrap();
let system = guard.find_class("java/lang/System").unwrap();
let print_stream = guard.find_class("java/io/PrintStream").unwrap();
let out = guard
.get_static_field(system, "out", "Ljava/io/PrintStream;")
.unwrap();
if let JValue::Object(out) = out {
let message = guard.new_string("Hello World").unwrap();
guard
.call_method(
out,
"println",
"(Ljava/lang/String;)V",
&[JValue::Object(message.into())],
)
.unwrap();
}
}
Everything is the same, except that we now use the AttachGuard to call Java methods instead of the passed JNIEnv object.
The tricky part here is to properly set the LD_LIBRARY_PATH environment variable before launching the Rust application, otherwise it will not be able to find the libjvm.so. In my case it is:
export LD_LIBRARY_PATH=/usr/lib/jvm/java-1.11.0-openjdk-amd64/lib/server/
but the path may be different on your system