Mutant Bob
Mutant Bob

Reputation: 3549

How can I invoke a Java method from Rust via JNI?

I have a Java library with a class com.purplefrog.batikExperiment.ToPixels which has a method static void renderToPixelsShape3(int width, int height, byte[] rgbs). What Rust code is needed to invoke the Java method and access the freshly-populated rgbs array?

I intend to call ToPixels.renderToPixelsShape3 from a Rust main() function, so the Rust code will have to construct the JNI environment.

Upvotes: 8

Views: 14658

Answers (3)

Aska
Aska

Reputation: 63

Alternatively, You could use j4rs.

The somehow "complicated" stuff is the creation of the Java byte array. Otherwise, everything else is pretty straightforward:

In Cargo.toml:

j4rs = "0.12.0"

Your Rust main:

use std::convert::TryFrom;
use j4rs::{Instance, InvocationArg, Jvm, JvmBuilder};

fn main() -> Result<(), J4RsError> {
    // Create a Jvm
    let jvm = JvmBuilder::new().build()?;
    // Create the values for the byte array
    let rgbs: Vec<InvocationArg> = [0i8; 400 * 400 * 3]
       .iter()
       .map(|r| InvocationArg::try_from(r).unwrap()
                .into_primitive().unwrap())
       .collect();

    // Create a Java array from the above values
    let byte_arr = jvm.create_java_array("byte", &rgbs)?;
    // Invoke the static method
    jvm.invoke_static(
        "com.purplefrog.batikExperiment.ToPixels",
        "renderToPixelsShape3",
        &[
            InvocationArg::try_from(33_i32)?.into_primitive()?,
            InvocationArg::try_from(333_i32)?.into_primitive()?,
            InvocationArg::try_from(byte_arr)?
        ])?;

    Ok(())
}

Upvotes: 3

Mutant Bob
Mutant Bob

Reputation: 3549

Using Svetlin Zarev's answer as a starting point I have managed to expand on it and figure out how to answer the rest of the question. I do not consider this a definitive answer because I anticipate there are still shortcomings because all I did was bang on it with a rock until it seemed to work.

The Cargo.toml is:

[package]
name = "rust_call_jni"
version = "0.1.0"
authors = ["Robert Forsman <[email protected]>"]
edition = "2018"


[dependencies.jni]
version="0.12.3"
features=["invocation"]

The first part of main.rs is almost identical to Svetlin's:

use jni::{InitArgsBuilder, JNIVersion, JavaVM, AttachGuard, JNIEnv};
use jni::objects::{JValue, JObject};

fn main() -> Result<(), jni::errors::Error>
{
    let jvm_args = InitArgsBuilder::new()
            .version(JNIVersion::V8)
            .option("-Xcheck:jni")
            .option(&format!("-Djava.class.path={}", heinous_classpath()))
            .build()
            .unwrap_or_else(|e|
            panic!("{}", e));

    let jvm:JavaVM = JavaVM::new(jvm_args)?;

    let env:AttachGuard = jvm.attach_current_thread()?;
    let je:&JNIEnv = &env; // this is just so intellij's larval rust plugin can give me method name completion

    let cls = je.find_class("com/purplefrog/batikExperiment/ToPixels").expect("missing class");

Since I intend to call static void renderToPixelsShape3(int width, int height, byte[] rgbs) instead of System.out.println(String) the code begins to diverge:

let width = 400;
let height = 400;
let rgbs = env.new_byte_array(width*height*3)?;
let rgbs2:JObject = JObject::from(rgbs);

let result = je.call_static_method(cls, "renderToPixelsShape3", "(II[B)V", &[
    JValue::from(width),
    JValue::from(height),
    JValue::from(rgbs2),
])?;

println!("{:?}", result);

let blen = env.get_array_length(rgbs).unwrap() as usize;
let mut rgbs3:Vec<i8> = vec![0; blen];
println!("byte array length = {}", blen);

env.get_byte_array_region(rgbs, 0, &mut rgbs3)?;

I am not absolutely certain I've done the array copy correctly, but it appears to work without exploding. A more experienced Rust/Java coder might spot some mistakes (and leave a comment).

And to wrap up this hairball, let's write the bytes to a file so we can look at the image in GIMP:

    {
        use std::fs::File;
        use std::path::Path;
        use std::io::Write;
        let mut f = File::create(Path::new("/tmp/x.ppm")).expect("why can't I create the image file?");
        f.write_all(format!("P6\n{} {} 255\n", width, height).as_bytes()).expect("failed to write image header");
        let tmp:&[u8] =unsafe { &*(rgbs3.as_slice() as *const _ as *const [u8])};
        f.write_all( tmp).expect("failed to write image payload");
        println!("wrote /tmp/x.ppm");
    }

    return Ok(());
}

Please tell me there's a better way to write a Vec<i8> to a file (because while that is the solution that shows up in google search results it makes me sad to resort to an unsafe block).

I am omitting the definition of heinous_classpath() because that is just a list of about 30 jars for the classpath. I'd like to know a maven command line to compute those for me without doing an appassemble and copying them out of the shell script but that is a different google search.

I will reiterate that I expect this code can be improved upon by someone who has been studying rust for more than 3 weeks.

Upvotes: 4

Svetlin Zarev
Svetlin Zarev

Reputation: 15663

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");
    }
}
  1. 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.

  2. 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 <[email protected]>"]
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

Upvotes: 15

Related Questions