Reputation: 3549
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
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
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
Reputation: 15663
Here is a simple one-file project to demonstrate how to use the jni crate:
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
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
main()
in RustFirst 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