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

Mut*_*Bob 3 java-native-interface rust

我有一个 Java 库,com.purplefrog.batikExperiment.ToPixels其中包含一个具有方法的类static void renderToPixelsShape3(int width, int height, byte[] rgbs)。调用 Java 方法并访问新填充的rgbs数组需要哪些 Rust 代码?

我打算ToPixels.renderToPixelsShape3从 Rustmain()函数调用,因此 Rust 代码必须构建 JNI 环境。

Sve*_*rev 8

这是一个简单的单文件项目,用于演示如何使用 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");
    }
}
Run Code Online (Sandbox Code Playgroud)
  1. 在启动时加载本机库。我正在使用load它需要一个绝对路径。或者,您可以使用loadLibrarywhich 只需要库的名称,但另一方面要求它位于特定位置。

  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
Run Code Online (Sandbox Code Playgroud)

锈面

现在我们知道了所需的方法签名,我们可以创建我们的 Rust 库!首先创建一个 Cargo.toml 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"]
Run Code Online (Sandbox Code Playgroud)

然后添加一个lib.rs包含以下内容的文件:

[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"]
Run Code Online (Sandbox Code Playgroud)

请注意,我们使用了生成的头文件中丑陋的方法名称和签名。否则 JVM 将无法找到我们的方法。

首先我们加载所需的类。在这种情况下,它并不是真正必要的,因为我们传递的类与名为 的参数完全相同_class。然后我们使用env我们收到的作为参数调用所需的 java 方法。

第一个参数是目标类。

第二个 - 目标方法名称。

第三个 - 描述参数类型和返回值:(arguments)return-type。你可以找到更多有关花哨的语法和神秘的信件这里在我们的例子中,我们没有任何参数和返回值类型是V其中的手段VOID

第四个 - 包含实际参数的数组。由于该方法不需要任何,我们传递一个空数组。

现在构建 Rust 库,然后运行 ​​Java 应用程序。因此,您必须在终端中看到Called From JNI

main()在 Rust 中调用 Java

首先,您必须生成一个 JVM 实例。您必须在 jni crate 上使用“调用”功能:

[dependencies.jni]
version = "0.12.3"
features = ["invocation", "default"]
Run Code Online (Sandbox Code Playgroud)

您可能希望使用.option()以下方法自定义 jvm 设置:

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();
}
Run Code Online (Sandbox Code Playgroud)

一切都一样,除了我们现在使用AttachGuard来调用 Java 方法而不是传递的JNIEnv对象。

这里的棘手部分是LD_LIBRARY_PATH在启动 Rust 应用程序之前正确设置环境变量,否则将无法找到 libjvm.so。就我而言,它是:

export LD_LIBRARY_PATH=/usr/lib/jvm/java-1.11.0-openjdk-amd64/lib/server/

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


Mut*_*Bob 4

以斯维特林·扎列夫的回答为起点,我设法对其进行扩展,并找出如何回答问题的其余部分。我不认为这是一个明确的答案,因为我预计仍然存在缺陷,因为我所做的只是用石头敲击它,直到它看起来起作用为止。

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"]
Run Code Online (Sandbox Code Playgroud)

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");
Run Code Online (Sandbox Code Playgroud)

由于我打算调用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)?;
Run Code Online (Sandbox Code Playgroud)

我不确定我是否正确完成了数组复制,但它似乎可以正常工作而不会爆炸。更有经验的 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(());
}
Run Code Online (Sandbox Code Playgroud)

请告诉我有更好的方法将 a 写入Vec<i8>文件(因为虽然这是谷歌搜索结果中显示的解决方案,但诉诸块让我感到难过unsafe)。

我省略了 的定义,heinous_classpath()因为这只是类路径的大约 30 个 jar 的列表。我想知道一个 Maven 命令行来为我计算这些,而不需要进行应用程序组装并将它们从 shell 脚本中复制出来,但这是一个不同的谷歌搜索。

我要重申的是,我希望学习 Rust 超过 3 周的人可以改进这段代码。