如何通过JNI从Rust调用Java方法?

8

我有一个Java库,其中包含一个名为com.purplefrog.batikExperiment.ToPixels的类,该类具有一个方法static void renderToPixelsShape3(int width, int height, byte[] rgbs)。如何编写Rust代码来调用Java方法并访问刚生成的rgbs数组?

我打算从Rust的main()函数中调用ToPixels.renderToPixelsShape3,因此Rust代码将需要构建JNI环境。


3
你尝试过什么?有很多木箱可以让你从Rust调用Java。在谷歌搜索后,第一个出现的是https://github.com/jni-rs/jni-rs,但还有其他选择。那么你尝试了什么? - Svetlin Zarev
我已经尝试过https://stackoverflow.com/questions/56825201/how-can-i-import-javavm-from-the-jni-crate-without-causing-compile-errors。我希望我知道其他的crate,但是在有人提到它们之前,我的关于存在哪些crate的问题就被关闭了。 - Mutant Bob
3个回答

13

这是一个简单的单文件项目,用于演示如何使用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");
    }
}
  1. 启动时加载本地库。我使用load需要绝对路径。或者你可以使用loadLibrary只需要库的名称,但另一方面需要将其放置在特定位置。

  2. 为了能够从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方面

现在,我们知道了所需的方法签名,可以创建我们的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”的参数传递了进来。然后,我们使用作为参数接收到的 env 来调用所需的java方法。
第一个参数是目标类。
第二个-目标方法名称。
第三个-描述参数类型和返回值:(arguments)return-type。您可以在此处了解有关该花式语法和神秘字母的更多信息here。在我们的情况下,我们没有任何参数,返回类型为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/

但是在您的系统上路径可能不同。


这是一个调用Java从Rust中调用Java的示例。我有兴趣从Rust main() 调用Java。我将编辑我的问题以使其更清晰。 - Mutant Bob
请注意,javac -h需要一个输出文件夹,而不仅仅是源文件。请参见https://dev59.com/R1YN5IYBdhLWcg3w-cnx#46625782。 - Jorn

4

使用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"]
的第一部分与 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> 写入文件(因为虽然这是谷歌搜索结果中出现的解决方案,但使用 unsafe 语句令我感到沮丧)。
我省略了 heinous_classpath() 的定义,因为那只是一个包含大约30个jar包的类路径列表。 我想知道一个Maven命令行可以为我计算它们,而不需要执行 appassemble 并从 shell 脚本中复制它们,但这是不同的谷歌搜索。
我再次强调,我相信有人在 Rust 学习超过3周后可以改进此代码。

我答案的第一个版本可能缺少的一件事是调用 env.delete_local_ref(rgbs),或者只需将其包装在 AutoLocal 实例中,在 drop() 上执行相同的操作。 - Mutant Bob

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(())
}


这对于一个大小为3的数组看起来不错,但你会注意到我正在创建一个400*400*3的数组。 - Mutant Bob
1
这只是一个例子。编辑以创建一个400 * 400 * 3的数组。 - Aska

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接