Rust和JVM一起使用 - itnext


我已经使用 JVM 二十年了,主要是在 Java 中。JVM 是一项了不起的技术。恕我直言,它最大的好处是它能够使本机代码适应当前的工作负载;如果工作负载发生变化并且本机代码不是最佳的,它将相应地重新编译字节码。
另一方面,当不再需要对象时,JVM 会自动从内存中释放它们。这个过程被称为垃圾收集。在没有 GC 的语言中,开发人员必须负责释放对象。对于遗留语言和大型代码库,发布应用不一致,并且在生产中发现了错误。
虽然 GC 算法随着时间的推移有所改进,但 GC 本身仍然是一个大型复杂机器。微调 GC 很复杂,并且在很大程度上取决于上下文。昨天有效的方法今天可能无效。总而言之,在您的上下文中配置 JVM 以最好地处理 GC 就像魔术一样。
由于围绕 JVM 的生态系统非常发达,因此使用 JVM 开发应用程序并将需要可预测性的部分委托给 Rust 是有意义的。
 
通过 JNI 集成 Java 和 Rust
集成Java和Rust需要以下几步:

  1. 在 Java 中创建“skeleton”方法
  2. 从它们生成 C 头文件
  3. 在 Rust 中实现它们
  4. 编译 Rust 生成系统库
  5. 从 Java 程序加载库
  6. 调用第一步中定义的方法。此时,库包含实现,并且集成已完成。

老手会意识到这些步骤与您需要与 C 或 C++ 集成时的步骤相同。因为他们也可以生成系统库。让我们详细看看每个步骤。
先需要创建 Java skeleton方法。native方法将其实现委托给库。
public native int doubleRust(int input);

接下来,我们需要生成相应的C头文件。为了自动生成,我们可以利用 Maven 编译器插件:

<plugin> 
    <artifactId>maven-compiler-plugin</artifactId> 
    <version>3.8.1</version> 
    <configuration> 
        <compilerArgs> 
            <arg>-h</arg> <!--1--> 
            <arg >target/headers</arg> <!--2--> 
        </compilerArgs> 
    </configuration> 
</plugin>

上述 Java 片段的生成头应如下所示:

include 
ifndef _Included_ch_frankel_blog_rust_Main
define _Included_ch_frankel_blog_rust_Main
ifdef __cplusplus
extern "C" {
endif
/*
 * Class:     ch_frankel_blog_rust_Main
 * Method:    doubleRust
 * Signature: (I)I
 */

JNIEXPORT jint JNICALL Java_ch_frankel_blog_rust_Main_doubleRust
  (JNIEnv *, jobject, jint);
ifdef __cplusplus
}
endif
endif
 

Rust 实现
现在,我们可以开始 Rust 实现。让我们创建一个新项目:
cargo new lib-rust
[package]
name = "dummymath"
version =
"0.1.0"
authors = [
"Nicolas Frankel "]
edition =
"2018"
[dependencies]
jni =
"0.19.0"                                     
[lib]
crate_type = [
"cdylib"]                 

有几种 crate 类型可用:cdylib用于动态系统库,您可以从其他语言加载。您可以在文档中查看所有其他可用类型。
API 一对一地映射到生成的 C 代码。我们可以相应地使用它:

#[no_mangle] 
pub extern "system" fn Java_ch_frankel_blog_rust_Main_doubleRust(_env: JNIEnv, _obj: JObject, x: jint) -> jint { 
    x * 2 
}

在上面的代码中发生了很多事情。让我们详细说明一下。

  • 该no_mangle宏告诉编译器在编译后的代码中保留相同的函数签名。这很重要,因为 JVM 将使用此签名。
  • 大多数时候,我们extern在 Rust 函数中使用将实现委托给其他语言:这称为 FFI。这与我们在 Java 中使用native. 然而,Rust 也extern用于相反的情况,即,使函数可以从其他语言调用。
  • 签名本身应该精确地模仿 C 头文件中的代码,因此这个名字看起来很有趣
  • 最后,x是一个jint,是i32的别名。

现在构建:
cargo build

构建会生成一个系统相关的库。例如,在 OSX 上,工件有一个dylib扩展名;在 Linux 上,它将有一个so。
 
使用Java端的库
最后一部分是在Java端使用生成的库。它需要首先加载它。有两种方法可用于此目的,System.load(filename)以及System.loadLibrary(libname)。
load()需要库的绝对路径,包括其扩展名,例如, /path/to/lib.so. 对于需要跨系统工作的应用程序,这是不切实际的。loadLibrary()允许您只传递库的名称 - 没有扩展名。请注意,库是在java.library.pathSystem 属性指示的位置加载的。

public class Main {
    static {
        System.loadLibrary("dummymath");
    }
}
请注意,在 Mac OS 上,lib前缀不是库名称的一部分。
 
处理对象
上面的代码非常简单:它涉及一个纯函数,根据定义,它仅取决于其输入参数。假设我们想要更多的东西。我们提出了一种新方法,将参数与对象状态中的另一个参数相乘:

public class Main {
    private int state;
    public Main(int state) {
        this.state = state;
    }
    public static void main(String[] args) {
        try {
            var arg1 = Integer.parseInt(args[1]);
            var arg2 = Integer.parseInt(args[2]);
            var result = new Main(arg1).timesRust(arg2);                // 1
            System.out.println(arg1 +
"x" + arg2 + " = " + result);
        } catch (NumberFormatException e) {
            throw new IllegalArgumentException(
"Arguments must be ints");
        }
    }
    public native int timesRust(int input);
}

native方法看起来与上面完全相同,但名称不同。因此,生成的 C 头文件看起来也一样。魔法需要发生在 Rust 方面。
在纯函数中,我们没有使用JNIEnv和JObject参数:JObject表示 Java 对象,即,Main和JNIEnv允许访问其数据(或行为)。

#[no_mangle]
pub extern "system" fn Java_ch_frankel_blog_rust_Main_timesRust(env: JNIEnv, obj: JObject, x: jint) -> jint {                    // 1
    let state = env.get_field(obj,
"state", "I");            // 2
    state.unwrap().i().unwrap() * x                        
// 3
}

第二行:传递对象的引用、Java 中的字段名称及其类型。类型是指正确的JVM 类型签名,例如 "I"代表 int。
第三行:state是一个Result<JValue>。我们需要将它解包到一个 JValue,然后将它“投射”到一个Result<jint>viai()

这篇文章的完整源代码可以在Github上找到。