如何通过 JNI 从 Rust 调用 Java 方法?
How can I invoke a Java method from Rust via JNI?
我有一个 Java 库,其中 class com.purplefrog.batikExperiment.ToPixels
有一个方法 static void renderToPixelsShape3(int width, int height, byte[] rgbs)
。调用 Java 方法并访问新填充的 rgbs
数组需要哪些 Rust 代码?
我打算从 Rust main()
函数中调用 ToPixels.renderToPixelsShape3
,因此 Rust 代码必须构建 JNI 环境。
这里有一个简单的 one-file 项目来演示如何使用 jni crate:
Java边
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
。或者,您可以使用 loadLibrary
,它只需要库的名称,但另一方面要求它位于特定位置。
为了能够从 Java 调用本机方法,您必须找到要在您的库中使用的签名。为此,您必须生成一个 C 头文件。这可以通过以下方式完成:
cd src/main/java/org/example/mcve/standalone/
javac -h Mcve.java
因此你应该得到一个看起来像
的文件
/* 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 库了!首先用 crate_type = "cdylib"
创建一个 Cargo.toml:
[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"]
然后添加一个lib.rs
文件,内容如下:
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();
}
请注意,我们使用了生成的头文件中丑陋的方法名称和签名。否则 JVM 将无法找到我们的方法。
首先我们加载所需的 class。在这种情况下,它并不是真正必要的,因为我们将相同的 class 作为名为 _class
的参数传递。然后我们使用作为参数收到的 env
调用所需的 java 方法。
第一个参数是目标 class。
第二个 - 目标方法名称。
第三个 - 描述了参数类型和 return 值:(arguments)return-type
。你可以找到更多关于花哨的语法和神秘字母 here 在我们的例子中我们没有任何参数并且 return 类型是 V
这意味着 VOID
第四个 - 包含实际参数的数组。由于该方法不期望任何内容,因此我们传递一个空数组。
现在构建 Rust 库,然后 运行 Java 应用程序。因此,您必须在终端中看到 Called From JNI
在 Rust 中从 main()
调用 Java
首先,您必须生成一个 JVM 实例。您必须使用 jni crate 上的 "invocation" 功能:
[dependencies.jni]
version = "0.12.3"
features = ["invocation", "default"]
您可能希望使用 .option()
自定义 jvm 设置:
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();
}
}
一切都一样,除了我们现在使用 AttachGuard
来调用 Java 方法而不是传递的 JNIEnv
对象。
这里棘手的部分是在启动 Rust 应用程序之前正确设置 LD_LIBRARY_PATH
环境变量,否则将无法找到 libjvm.so。在我的例子中是:
export LD_LIBRARY_PATH=/usr/lib/jvm/java-1.11.0-openjdk-amd64/lib/server/
但您系统上的路径可能不同
以 Svetlin Zarev 的回答为起点,我设法对其进行了扩展,并找出了如何回答其余问题的方法。我不认为这是一个明确的答案,因为我预计仍然存在缺点,因为我所做的只是用石头敲打它,直到它 似乎 起作用。
Cargo.toml是:
[package]
name = "rust_call_jni"
version = "0.1.0"
authors = ["Robert Forsman <git@thoth.purplefrog.com>"]
edition = "2018"
[dependencies.jni]
version="0.12.3"
features=["invocation"]
main.rs 的第一部分几乎与 Svetlin 的相同:
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");
由于我打算调用 static void renderToPixelsShape3(int width, int height, byte[] rgbs)
而不是 System.out.println(String)
代码开始出现分歧:
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)?;
我不确定我是否正确完成了数组复制,但它似乎可以正常工作而不会爆炸。更有经验的 Rust/Java 编码人员可能会发现一些错误(并发表评论)。
为了结束这个毛团,让我们将字节写入一个文件,以便我们可以在 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(());
}
请告诉我有更好的方法将 Vec<i8>
写入文件(因为虽然这是 google 搜索结果中显示的解决方案,但求助于 unsafe
块)。
我省略了 heinous_classpath()
的定义,因为那只是类路径的大约 30 个 jar 的列表。我想知道一个 maven 命令行来为我计算那些,而不用做一个 appassemble 并将它们从 shell 脚本中复制出来,但这是一个不同的 google 搜索。
我要重申,我希望研究 Rust 超过 3 周的人可以改进这段代码。
或者,您可以使用 j4rs。
有点“复杂”的东西是 Java 字节数组的创建。否则,其他一切都非常简单:
在Cargo.toml中:
j4rs = "0.12.0"
你的 Rust 主线:
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(())
}
我有一个 Java 库,其中 class com.purplefrog.batikExperiment.ToPixels
有一个方法 static void renderToPixelsShape3(int width, int height, byte[] rgbs)
。调用 Java 方法并访问新填充的 rgbs
数组需要哪些 Rust 代码?
我打算从 Rust main()
函数中调用 ToPixels.renderToPixelsShape3
,因此 Rust 代码必须构建 JNI 环境。
这里有一个简单的 one-file 项目来演示如何使用 jni crate:
Java边
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
。或者,您可以使用loadLibrary
,它只需要库的名称,但另一方面要求它位于特定位置。为了能够从 Java 调用本机方法,您必须找到要在您的库中使用的签名。为此,您必须生成一个 C 头文件。这可以通过以下方式完成:
cd src/main/java/org/example/mcve/standalone/
javac -h Mcve.java
因此你应该得到一个看起来像
的文件/* 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 库了!首先用 crate_type = "cdylib"
创建一个 Cargo.toml:
[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"]
然后添加一个lib.rs
文件,内容如下:
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();
}
请注意,我们使用了生成的头文件中丑陋的方法名称和签名。否则 JVM 将无法找到我们的方法。
首先我们加载所需的 class。在这种情况下,它并不是真正必要的,因为我们将相同的 class 作为名为 _class
的参数传递。然后我们使用作为参数收到的 env
调用所需的 java 方法。
第一个参数是目标 class。
第二个 - 目标方法名称。
第三个 - 描述了参数类型和 return 值:(arguments)return-type
。你可以找到更多关于花哨的语法和神秘字母 here 在我们的例子中我们没有任何参数并且 return 类型是 V
这意味着 VOID
第四个 - 包含实际参数的数组。由于该方法不期望任何内容,因此我们传递一个空数组。
现在构建 Rust 库,然后 运行 Java 应用程序。因此,您必须在终端中看到 Called From JNI
在 Rust 中从 main()
调用 Java
首先,您必须生成一个 JVM 实例。您必须使用 jni crate 上的 "invocation" 功能:
[dependencies.jni]
version = "0.12.3"
features = ["invocation", "default"]
您可能希望使用 .option()
自定义 jvm 设置:
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();
}
}
一切都一样,除了我们现在使用 AttachGuard
来调用 Java 方法而不是传递的 JNIEnv
对象。
这里棘手的部分是在启动 Rust 应用程序之前正确设置 LD_LIBRARY_PATH
环境变量,否则将无法找到 libjvm.so。在我的例子中是:
export LD_LIBRARY_PATH=/usr/lib/jvm/java-1.11.0-openjdk-amd64/lib/server/
但您系统上的路径可能不同
以 Svetlin Zarev 的回答为起点,我设法对其进行了扩展,并找出了如何回答其余问题的方法。我不认为这是一个明确的答案,因为我预计仍然存在缺点,因为我所做的只是用石头敲打它,直到它 似乎 起作用。
Cargo.toml是:
[package]
name = "rust_call_jni"
version = "0.1.0"
authors = ["Robert Forsman <git@thoth.purplefrog.com>"]
edition = "2018"
[dependencies.jni]
version="0.12.3"
features=["invocation"]
main.rs 的第一部分几乎与 Svetlin 的相同:
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");
由于我打算调用 static void renderToPixelsShape3(int width, int height, byte[] rgbs)
而不是 System.out.println(String)
代码开始出现分歧:
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)?;
我不确定我是否正确完成了数组复制,但它似乎可以正常工作而不会爆炸。更有经验的 Rust/Java 编码人员可能会发现一些错误(并发表评论)。
为了结束这个毛团,让我们将字节写入一个文件,以便我们可以在 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(());
}
请告诉我有更好的方法将 Vec<i8>
写入文件(因为虽然这是 google 搜索结果中显示的解决方案,但求助于 unsafe
块)。
我省略了 heinous_classpath()
的定义,因为那只是类路径的大约 30 个 jar 的列表。我想知道一个 maven 命令行来为我计算那些,而不用做一个 appassemble 并将它们从 shell 脚本中复制出来,但这是一个不同的 google 搜索。
我要重申,我希望研究 Rust 超过 3 周的人可以改进这段代码。
或者,您可以使用 j4rs。
有点“复杂”的东西是 Java 字节数组的创建。否则,其他一切都非常简单:
在Cargo.toml中:
j4rs = "0.12.0"
你的 Rust 主线:
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(())
}