#java #jni #api-bindings #language

j4rs-171h

j4rs 表示 'Java for Rust',允许从 Rust 无痛调用 Java 代码。

2 个版本

0.18.1 2024年3月31日
0.18.0 2024年3月31日

#96 in 编程语言

MIT/Apache

2MB
7.5K SLoC

包含 (JAR 文件, 2.5MB) j4rs-0.18.0-SNAPSHOT-jar-with-dependencies.jar

j4rs

crates.io Maven Central Build

j4rs 表示 ‘Java for Rust’,允许从 Rust 无痛调用 Java 代码,反之亦然。

功能

用法

基础

use j4rs::{Instance, InvocationArg, Jvm, JvmBuilder};

// Create a JVM
let jvm = JvmBuilder::new().build()?;

// Create a java.lang.String instance
let string_instance = jvm.create_instance(
    "java.lang.String",     // The Java class to create an instance for
    InvocationArg::empty(), // An array of `InvocationArg`s to use for the constructor call - empty for this example
)?;

// The instances returned from invocations and instantiations can be viewed as pointers to Java Objects.
// They can be used for further Java calls.
// For example, the following invokes the `isEmpty` method of the created java.lang.String instance
let boolean_instance = jvm.invoke(
  &string_instance,       // The String instance created above
  "isEmpty",              // The method of the String instance to invoke
  InvocationArg::empty(), // The `InvocationArg`s to use for the invocation - empty for this example
)?;

// If we need to transform an `Instance` to some Rust value, the `to_rust` should be called
let rust_boolean: bool = jvm.to_rust(boolean_instance)?;
println!("The isEmpty() method of the java.lang.String instance returned {}", rust_boolean);
// The above prints:
// The isEmpty() method of the java.lang.String instance returned true

// Static invocation
let _static_invocation_result = jvm.invoke_static(
  "java.lang.System",     // The Java class to invoke
  "currentTimeMillis",    // The static method of the Java class to invoke
  InvocationArg::empty(), // The `InvocationArg`s to use for the invocation - empty for this example
)?;

// Access a field of a class
let system_class = jvm.static_class("java.lang.System")?;
let system_out_field = jvm.field(&system_class, "out");

// Retrieve an enum constant using the field
let access_mode_enum = jvm.static_class("java.nio.file.AccessMode")?;
let access_mode_write = jvm.field(&access_mode_enum, "WRITE")?;

// Retrieve a nested class (note the use of `$` instead of `.`)
let state = jvm.static_class("java.lang.Thread$State")?;

Instances 的 Java ListMap 可以使用 java_listjava_map 函数创建

let rust_vec = vec!["arg1", "arg2", "arg3", "arg33"];

// Generate a Java List. The Java List implementation is the one that is returned by java.util.Arrays#asList
let java_list_instance = jvm.java_list(
    JavaClass::String,
    rust_vec)?;

let rust_map = HashMap::from([
    ("Potatoes", 3),
    ("Tomatoes", 33),
    ("Carrotoes", 333),
]);

// Generate a java.util.HashMap.
let java_map_instance = jvm.java_map(
    JavaClass::String,
    JavaClass::Integer,
    rust_map)?;

从 Rust 传递参数到 Java

j4rs 使用 InvocationArg 枚举将参数传递到 Java 世界。

用户可以利用现有的一些基本类型的 TryFrom 实现

let i1 = InvocationArg::try_from("a str")?;      // Creates an arg of java.lang.String
let my_string = "a string".to_owned();
let i2 = InvocationArg::try_from(my_string)?;    // Creates an arg of java.lang.String
let i3 = InvocationArg::try_from(true)?;         // Creates an arg of java.lang.Boolean
let i4 = InvocationArg::try_from(1_i8)?;         // Creates an arg of java.lang.Byte
let i5 = InvocationArg::try_from('c')?;          // Creates an arg of java.lang.Character
let i6 = InvocationArg::try_from(1_i16)?;        // Creates an arg of java.lang.Short
let i7 = InvocationArg::try_from(1_i64)?;        // Creates an arg of java.lang.Long
let i8 = InvocationArg::try_from(0.1_f32)?;      // Creates an arg of java.lang.Float
let i9 = InvocationArg::try_from(0.1_f64)?;      // Creates an arg of java.lang.Double

对于 Vec

let my_vec: Vec<String> = vec![
    "abc".to_owned(),
    "def".to_owned(),
    "ghi".to_owned()];

let i10 = InvocationArg::try_from(my_vec.as_slice())?;

j4rs api 接受 InvocationArg 作为引用或值

let inv_args = InvocationArg::try_from("arg from Rust")?;
let _ = jvm.create_instance("java.lang.String", &[&inv_args])?; // Pass a reference
let _ = jvm.create_instance("java.lang.String", &[inv_args])?;  // Move

j4rs 返回的 Instance 可以转换为 InvocationArg 并用于调用方法

let one_more_string_instance = jvm.create_instance(
  "java.lang.String",     // The Java class to create an instance for
  InvocationArg::empty(), // The `InvocationArg`s to use for the constructor call - empty for this example
)?;

let i11 = InvocationArg::try_from(one_more_string_instance)?;

要创建一个表示 null Java 值的 InvocationArg,请使用 From 实现与 Null 结构体

let null_string = InvocationArg::from(Null::String);                // A null String
let null_integer = InvocationArg::from(Null::Integer);              // A null Integer
let null_obj = InvocationArg::from(Null::Of("java.util.List"));     // A null object of any other class. E.g. List

从 Rust 传递自定义参数到 Java

对于没有 TryFrom 实现的自定义类型,也支持通过序列化。

要使用自定义结构 MyBean 作为 InvocationArg,它需要是可序列化的

#[derive(Serialize, Deserialize, Debug)]
#[allow(non_snake_case)]
struct MyBean {
    someString: String,
    someInteger: isize,
}

然后,可以创建一个如下的 InvocationArg

let my_bean = MyBean {
    someString: "My String In A Bean".to_string(),
    someInteger: 33,
};
let ia = InvocationArg::new(&my_bean, "org.astonbitecode.j4rs.tests.MyBean");

它可以作为一个参数传递给接受org.astonbitecode.j4rs.tests.MyBean实例的Java方法。

当然,为了使反序列化工作以及创建自定义Java对象,类路径中应该存在相应的Java类。

package org.astonbitecode.j4rs.tests;

public class MyBean {
    private String someString;
    private Integer someInteger;

    public MyBean() {
    }

    public String getSomeString() {
        return someString;
    }

    public void setSomeString(String someString) {
        this.someString = someString;
    }

    public Integer getSomeInteger() {
        return someInteger;
    }

    public void setSomeInteger(Integer someInteger) {
        this.someInteger = someInteger;
    }
}

异步支持

(从v0.16.0开始)

j4rs通过.async/.await函数支持Jvm::invoke_async。该函数返回一个Future,它通过一个oneshot channelReceiver来完成。

在Java端,可以被invoke_async调用的方法必须返回一个Java Future。当Java Future完成时,j4rs的Java端调用本地Rust代码,使用oneshot channel的Sender来完成挂起的Rust Future,无论是成功还是失败。

例如,假设我们有一个返回Future的Java方法

package org.astonbitecode.j4rs.tests;

public class MyTest {
  private static ExecutorService executor = Executors.newSingleThreadExecutor();

  // Just return the passed String in a Future
  public Future<String> getStringWithFuture(String string) {
    CompletableFuture<String> completableFuture = new CompletableFuture<>();
    executor.submit(() -> {
      completableFuture.complete(string);
      return null;
    });
    return completableFuture;
  }
}

我们可以像下面这样调用它

let s_test = "j4rs_rust";
let my_test = jvm.create_instance("org.astonbitecode.j4rs.tests.MyTest", InvocationArg::empty())?;
let instance = jvm.invoke_async(&my_test, "getStringWithFuture", &[InvocationArg::try_from(s_test)?]).await?;
let string: String = jvm.to_rust(instance)?;
assert_eq!(s_test, string);

请注意,对于通过invoke_async函数调用的Java方法,返回CompletableFuture会更好,因为这可以提高性能。

j4rs使用内部的单线程ScheduledExecutorService通过轮询处理不是CompletableFuture的简单Java Futures。

这显然存在性能问题。

类型转换

Instance可以被转换成其他类型

let instantiation_args = vec![InvocationArg::try_from("Hi")?];
let instance = jvm.create_instance("java.lang.String", instantiation_args.as_ref())?;
jvm.cast(&instance, "java.lang.Object")?;

Java 数组和可变参数

// Create a Java array of Strings
let s1 = InvocationArg::try_from("string1")?;
let s2 = InvocationArg::try_from("string2")?;
let s3 = InvocationArg::try_from("string3")?;

let arr_instance = jvm.create_java_array("java.lang.String", &[s1, s2, s3])?;
// Invoke the Arrays.asList(...) and retrieve a java.util.List<String>
let list_instance = jvm.invoke_static("java.util.Arrays", "asList", &[InvocationArg::from(arr_instance)])?;

Java 泛型

// Assuming the following map_instance is a Map<String, Integer>
// we may invoke its put method
jvm.invoke(&map_instance, "put", &[InvocationArg::try_from("one")?, InvocationArg::try_from(1)?])?;

Java 原始类型

即使有自动装箱和拆箱,j4rs也不能使用Integer实例调用带有原始类型int参数的方法。

例如,以下代码无法工作

let ia = InvocationArg::try_from(1_i32)?;
jvm.create_instance("java.lang.Integer", &[ia])?;

它抛出InstantiationException异常,因为Integer的构造函数需要一个原始的int作为参数

异常在"main"线程中 org.astonbitecode.j4rs.errors.InstantiationException: 无法创建java.lang.Integer实例 at org.astonbitecode.j4rs.api.instantiation.NativeInstantiationImpl.instantiate(NativeInstantiationImpl.java:37) Caused by: java.lang.NoSuchMethodException: java.lang.Integer.(java.lang.Integer) at java.base/java.lang.Class.getConstructor0(Class.java:3349) at java.base/java.lang.Class.getConstructor(Class.java:2151) at org.astonbitecode.j4rs.api.instantiation.NativeInstantiationImpl.createInstance(NativeInstantiationImpl.java:69) at org.astonbitecode.j4rs.api.instantiation.NativeInstantiationImpl.instantiate(NativeInstantiationImpl.java:34)

在这种情况下,应首先将java.lang.Integer实例转换为原始的int

let ia = InvocationArg::try_from(1_i32)?.into_primitive()?;
jvm.create_instance("java.lang.Integer", &[ia]);

Java实例链式调用

use j4rs::{Instance, InvocationArg, Jvm, JvmBuilder};

// Create a JVM
let jvm = JvmBuilder::new().build()?;

// Create an instance
let string_instance = jvm.create_instance(
  "java.lang.String",
  &[InvocationArg::try_from(" a string ")?],
)?;

// Perform chained operations on the instance
let string_size: isize = jvm.chain(string_instance)
    .invoke("trim", InvocationArg::empty())?
    .invoke("length", InvocationArg::empty())?
    .to_rust()?;

// Assert that the string was trimmed
assert!(string_size == 8);

回调支持

j4rs提供了对Java到Rust回调的支持。

这些回调通过Rust Channels进入Rust世界。

为了初始化一个提供Java回调值的channel,应该调用Jvm::invoke_to_channel。它返回一个包含Channel ReceiverInstanceReceiver结构体的结果。

// Invoke of a method of a Java instance and get the returned value in a Rust Channel.

// Create an Instance of a class that supports Native Callbacks
// (the class just needs to extend the 
// `org.astonbitecode.j4rs.api.invocation.NativeCallbackToRustChannelSupport`)
let i = jvm.create_instance(
  "org.astonbitecode.j4rs.tests.MyTest",
  InvocationArg::empty())?;

// Invoke the method
let instance_receiver_res = jvm.invoke_to_channel(
  &i,                         // The instance to invoke asynchronously
  "performCallback",          // The method to invoke asynchronoysly
  InvocationArg::empty()      // The `InvocationArg`s to use for the invocation - empty for this example
);

// Wait for the response to come
let instance_receiver = instance_receiver_res?;
let _ = instance_receiver.rx().recv();

在Java领域,一个能够进行本地回调的类必须继承自org.astonbitecode.j4rs.api.invocation.NativeCallbackToRustChannelSupport

例如,考虑以下Java类。

performCallback方法会启动一个新线程,并在该线程中调用doCallback方法。该方法由NativeCallbackToRustChannelSupport类继承。

package org.astonbitecode.j4rs.tests;

import org.astonbitecode.j4rs.api.invocation.NativeCallbackToRustChannelSupport;

public class MyTest extends NativeCallbackToRustChannelSupport {

    public void performCallback() {
        new Thread(() -> {
            doCallback("THIS IS FROM CALLBACK!");
        }).start();
    }

}

使用Maven工件

从0.6.0版本开始,可以从Maven仓库下载Java工件。虽然可以定义更多的仓库,但默认情况下是maven central,并且始终可用。

例如,以下是下载并部署dropbox依赖项以及将其用于rust代码的方法

let dbx_artifact = MavenArtifact::from("com.dropbox.core:dropbox-core-sdk:3.0.11");
jvm.deploy_artifact(dbx_artifact)?;

也可以使用额外的工件库

let jvm: Jvm = JvmBuilder::new()
.with_maven_settings(MavenSettings::new(vec![
    MavenArtifactRepo::from("myrepo1::https://my.repo.io/artifacts"),
    MavenArtifactRepo::from("myrepo2::https://my.other.repo.io/artifacts")])
)
.build()
?;

jvm.deploy_artifact(&MavenArtifact::from("io.my:library:1.2.3"))?;

Maven工件会自动添加到类路径中,无需显式添加。

一种良好的实践是,通过构建脚本来执行Maven工件的部署,在crate编译期间。这确保了在执行实际的Rust代码时类路径被正确填充。

注意:部署还没有处理传递依赖。

将jar添加到类路径中

如果我们有一个需要使用j4rs访问的jar,我们需要在创建JVM时将其添加到类路径中

let entry = ClasspathEntry::new("/home/myuser/dev/myjar-1.0.0.jar");
let jvm: Jvm = JvmBuilder::new()
    .classpath_entry(entry)
    .build()?;

j4rs Java库

j4rs的jar在Maven Central中可用。可以在pom文件中添加以下依赖项来使用它

<dependency>
    <groupId>io.github.astonbitecode</groupId>
    <artifactId>j4rs</artifactId>
    <version>0.18.0</version>
    <scope>provided</scope>
</dependency>

请注意,scopeprovided。这是因为j4rs的Java资源始终与j4rs crate一起提供。

使用以下方式以避免可能的类加载错误。

j4rs在Android中的应用

Rust端

  1. Cargo.toml中将您的crate定义为cdylib
[lib]
name = "myandroidapp"
crate-type = ["cdylib"]
  1. 实现一个jni_onload函数并将提供的JavaVM应用到j4rs,如下所示
const JNI_VERSION_1_6: jint = 0x00010006;

#[allow(non_snake_case)]
#[no_mangle]
pub extern fn jni_onload(env: *mut JavaVM, _reserved: jobject) -> jint {
    j4rs::set_java_vm(env);
    jni_version_1_6
}

Java端

创建一个Activity并定义您的本地方法,如此处所述。

注意:如果您在使用j4rs的旧Android版本时遇到任何问题,这可能是由于Java 8兼容性问题引起的。这就是为什么有j4rs的Java 7版本。

<dependency>
    <groupId>io.github.astonbitecode</groupId>
    <artifactId>j4rs</artifactId>
    <version>0.13.1-java7</version>
</dependency>

更新:Java 7不再受支持。j4rs 0.13.1是最后一个版本。

JavaFX支持

(从0.13.0版本开始)

构建JavaFX UI的步骤

1. 已安装Rust、cargo和JDK 11(或更高版本)

2. 获取j4rs的JavaFX依赖项

这是一个好主意,这样在构建时就会发生,以便当实际的Rust应用程序启动并且JVM初始化时,依赖项可用。这可以通过在build脚本中添加以下内容来实现

	use j4rs::JvmBuilder;
use j4rs::jfx::JavaFxSupport;

fn main() {
    let jvm = JvmBuilder::new().build().unwrap();
    jvm.deploy_javafx_dependencies().unwrap();
}

3. 实现UI

这里有两种选择;要么使用FXML构建UI,要么使用Java代码的传统方式构建。在下面的代码片段中,您可以找到对每行的简短描述。

3.a 使用对JavaFX API的Java调用实现UI
// Create a Jvm with JavaFX support
let jvm = JvmBuilder::new().with_javafx_support().build()?;

// Start the JavaFX application.
// When the JavaFX application starts, the `InstanceReceiver` channel that is returned from the `start_javafx_app` invocation
// will receive an Instance of `javafx.stage.Stage`.
// The UI may start being built using the provided `Stage`.
let stage = jvm.start_javafx_app()?.rx().recv()?;

// Create a StackPane. Java code: StackPane root = new StackPane();
let root = jvm.create_instance("javafx.scene.layout.StackPane", InvocationArg::empty())?;

// Create the button. Java code: Button btn = new Button();
let btn = jvm.create_instance("javafx.scene.control.Button", InvocationArg::empty())?;
// Get the action channel for this button
let btn_action_channel = jvm.get_javafx_event_receiver(&btn, FxEventType::ActionEvent_Action)?;
// Set the text of the button. Java code: btn.setText("Say Hello World to Rust");
jvm.invoke(&btn, "setText", &["A button that sends events to Rust".try_into()?])?;
// Add the button to the GUI. Java code: root.getChildren().add(btn);
jvm.chain(&root)?
  .invoke("getChildren", InvocationArg::empty())?
  .invoke("add", &[btn.try_into()?])?
  .collect();

// Create a new Scene. Java code: Scene scene = new Scene(root, 300, 250);
let scene = jvm.create_instance("javafx.scene.Scene", &[
  root.try_into()?,
  InvocationArg::try_from(300_f64)?.into_primitive()?,
  InvocationArg::try_from(250_f64)?.into_primitive()?])?;
// Set the title for the scene. Java code: stage.setTitle("Hello Rust world!");
jvm.invoke(&stage, "setTitle", &["Hello Rust world!".try_into()?])?;
// Set the scene in the stage. Java code: stage.setScene(scene);
jvm.invoke(&stage, "setScene", &[scene.try_into()?])?;
// Show the stage. Java code: stage.show();
jvm.invoke(&stage, "show", InvocationArg::empty())?;

3.b 使用FXML实现UI

我个人更喜欢使用FXMLs构建UI,例如使用Scene Builder

需要注意的事项是,控制器类应该在根FXML元素中定义,并且应该这样定义:fx:controller="org.astonbitecode.j4rs.api.jfx.controllers.FxController""

下面是一个FXML示例;它创建了一个带有标签和按钮的窗口。

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.text.Font?>

<VBox alignment="TOP_CENTER" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="725.0" spacing="33.0" xmlns="http://javafx.com/javafx/11.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="org.astonbitecode.j4rs.api.jfx.controllers.FxController">
    <children>
        <Label text="JavaFX in Rust">
            <font>
                <Font size="65.0" />
            </font>
        </Label>
        <Label text="This UI is loaded with a FXML file" />
        <HBox alignment="CENTER" prefHeight="100.0" prefWidth="200.0" spacing="10.0">
            <children>
                <Button id="helloButton" mnemonicParsing="false" text="Say Hello" />
            </children>
        </HBox>
    </children>
</VBox>

可以使用元素的id来获取相应的节点在Rust中,并对它们进行操作(例如,添加事件监听器,更改文本或效果等)。

// Create a Jvm with JavaFX support
let jvm = JvmBuilder::new().with_javafx_support().build()?;

// Start the JavaFX application.
// When the JavaFX application starts, the `InstanceReceiver` channel that is returned from the `start_javafx_app` invocation
// will receive an Instance of `javafx.stage.Stage`.
// The UI may start being built using the provided `Stage`.
let stage = jvm.start_javafx_app()?.rx().recv()?;

// Set the title for the scene. Java code: stage.setTitle("Hello Rust world!");
jvm.invoke(&stage, "setTitle", &["Hello JavaFX from Rust!".try_into()?])?;
// Show the stage. Java code: stage.show();
jvm.invoke(&stage, "show", InvocationArg::empty())?;

// Load a fxml. This returns an `FxController` which can be used in order to find Nodes by their id,
// add Event Listeners and more.
let controller = jvm.load_fxml(&PathBuf::from("./fxml/jfx_in_rust.fxml"), &stage)?;

// Wait for the controller to be initialized. This is not mandatory, it is here to shoe that the functionality exists.
let _ = controller.on_initialized_callback(&jvm)?.rx().recv()?;
println!("The controller is initialized!");

// Get the InstanceReceiver to retrieve callbacks from the JavaFX button with id helloButton
let hello_button_action_channel = controller.get_event_receiver_for_node("helloButton", FxEventType::ActionEvent_Action, &jvm)?;

要查看完整的示例,请访问这里

Java到Rust支持

(从v0.12.0版本开始)

  • Cargo.toml中添加两个必需的依赖项(j4rsj4rs_derive),并将项目标记为cdylib,以生成共享库作为输出。此库将被Java代码加载和使用,以实现JNI调用。

  • 使用call_from_java属性注释将从Java代码中可访问的函数

#[call_from_java("io.github.astonbitecode.j4rs.example.RustSimpleFunctionCall.fnnoargs")]
fn my_function_with_no_args() {
    println!("Hello from the Rust world!");
    // If you need to have a Jvm here, you need to attach the thread
    let jvm = Jvm::attach_thread().unwrap();
    // Now you may further call Java classes and methods as usual!
}

要查看完整的示例,请访问这里

注意:JNI在幕后使用,因此,对于JNI有效的任何命名约定也应该适用于j4rs。例如,下划线(_)应该转义,并在call_from_java定义中变为_1

Rust构建后的可移植性假设(分发j4rs应用程序)

在构建过程中,j4rs创建一个jassets目录,该目录包含使crate正常工作所需的"Java世界"。它始终自动填充Java库,可以将其视为默认类路径容器,始终应该可用。

默认情况下,jassets位于crate生成的工件相同的目录中(在CARGO_TARGET_DIR下),因此在开发期间不应有任何问题。

但在实现完成后,如何分发应用程序呢?

有人可能在JVM初始化期间为j4rs指定不同的基础路径,发出如下命令

let jvm_res = j4rs::JvmBuilder::new()
  .with_base_path("/opt/myapp")
  .build();

base_path定义了j4rs运行所需的两个目录的位置;即jassetsdeps

  1. jassets包含j4rs jar以及其他可能通过Maven部署的jar。
  2. deps应该包含j4rs动态库。这是从Java到Rust实现回调所需的。如果应用程序不执行Java->Rust回调,则不需要deps目录。

因此,有人可能将应用程序二进制文件放在例如/usr/bin下,而jassetsdeps目录放在/opt/myapp/下,或者$HOME/.myapp,或者任何其他地方。

一个示例目录结构可以是

/ 
+ --- usr
|      + --- bin
|             + --- myapp
| 
+ --- opt
       + --- myapp 
              + --- jassets
              + --- deps

此外,还有一个实用函数可以自动执行特定路径下两个目录的复制。可以通过正在分发的crate的构建脚本调用Jvm::copy_j4rs_libs_under函数。

Jvm::copy_j4rs_libs_under("/opt/myapp")?;

之后,/opt/myapp 将包含 j4rs 运行所需的所有内容,只要使用 with_base_path 方法创建 JVM。

let jvm_res = j4rs::JvmBuilder::new()
  .with_base_path("/opt/myapp")
  .build();

常见问题解答(FAQ)

我遇到了 java.lang.NoSuchMethodError: java.net.URLClassLoader<init>(Ljava/lang/String;[Ljava/net/URL;Ljava/lang/ClassLoader;)V

j4rs 使用自定义的 ClassLoader,需要最低 Java 版本 9。为了使用支持旧版 Java 的默认 classloader,在构建 Jvm 时调用 JvmBuilder::with_default_classloader

如何启用调试日志?

j4rs 使用 log crate,因此可以根据选择的实现相应地配置日志。

但是,它也支持控制台日志,通过设置环境变量 J4RS_CONSOLE_LOG_LEVEL 进行配置。

接受的值有 debuginfowarnerrordisabled

许可证

根据您的选择,在以下条件下:

依赖项

~2.6–9MB
~82K SLoC