本手册旨在提供使用Java 22和Rust 1.81.0创建 Java 到 Rust 库的绑定的全面指南。
它将介绍允许 Java 应用程序调用 Rust 函数所需的基本步骤和概念,并利用外部函数和内存 API。
在本手册结束时,开发人员将能够将 Rust 的高性能、内存安全功能无缝集成到他们的 Java 应用程序中,从而实现跨语言功能。
FFM API
Java 22 引入了外部函数和内存 API (FFM API),这是旧版Java 本机接口 (JNI)的现代替代方案。JNI 传统上用于与外部库中的类 C 函数和数据类型进行交互。但是,JNI 繁琐、容易出错,并且由于重复的本机函数调用和缺乏即时 (JIT)优化而引入了大量开销。Java 对象需要通过 JNI 传递,这需要在本机端进行额外的工作来识别对象类型和数据位置,从而使整个过程变得繁琐而缓慢。借助 FFM API,Java 现在将大部分集成工作推到了 Java 端,从而无需自定义 C 标头,并为 JIT 编译器提供更高的可见性。
这一变化带来了更好的性能,因为 JIT 编译器现在可以更有效地优化对本机库的调用。它还简化了集成,因为对本机函数签名的要求更少。这减少了本机到 Java 转换的开销。此外,API 还提供了增强的灵活性,因为它支持使用 Rust 等各种语言,同时完全控制内存和函数调用的处理方式。
Java 22 是第一个稳定此 API 的版本,使其成为本手册的理想选择。它能够高效、直接地与 Rust 库交互,而没有 JNI 的历史缺陷。
Java 和 Rust 如何协同工作
Java 和 Rust 如何协同工作 Rust 是一种系统级语言,可提供对内存管理的细粒度控制,使其成为性能关键型应用程序的热门选择。另一方面,Java 在提供可移植性和高级抽象方面表现出色。通过使用Java 22 中的FFM API,开发人员可以在 Java 应用程序中利用 Rust 的性能和内存安全性。
它提供了对 SymbolLookup、FunctionDescriptor、Linker、MethodHandle、Arena 和 MemorySegment 等类的访问,使 Java 能够以更有效的方式调用外来函数和管理内存。 在 Rust 端,暴露给 Java 的函数必须遵守 C ABI,以确保两种语言之间的兼容性。 本手册将探讨如何在 Java 和 Rust 之间安全地分配、管理和释放内存,以确保最佳性能并避免内存泄漏或未定义的行为。
本手册将:
- 提供分步指南:开发人员将指导如何在 Rust 和 Java 之间建立绑定,并为项目配置这些绑定。
- 演示实际示例:将提供并解释正确设计的绑定示例。这些示例将针对简单和复杂的主题提供,包括公开 Rust 函数、处理复杂数据类型、管理生命周期和内存以及处理多线程。
- 简化 Rust-Java 集成:该手册将揭开集成过程的神秘面纱,帮助开发人员避免与所有权、内存管理和数据布局差异相关的常见陷阱。
- 解决高级主题:除了基础知识之外,该手册还将探讨高级主题,如线程安全、处理 Rust 在 Java 中的所有权和借用规则,以及如何处理复杂的数据结构和边缘情况。
详细点击标题
Rust 和 Java 如何通信
Java 和 Rust 通过动态链接进行通信,其中 Rust 编译成共享库文件(例如,.dll在 Windows、.soLinux 和.dylibmacOS 上)。Java 加载此库并可以与其函数交互。从高层次来看,该过程如下所示:
- Rust:编写 Rust 函数,用#[no_mangle]和导出它们extern "C",并将它们编译成共享库。
- Java:使用 Java FFM API加载共享库,找到 Rust 函数并调用它们。
接下来的部分将逐步介绍 Rust 和 Java 的设置。
步骤 1:导出 Rust 函数
为了使 Rust 函数可以从 Java 调用,我们需要做两件事:
用于#[no_mangle]防止 Rust 在内部重命名(混淆)函数名称。这可确保 Java 可以通过其确切名称找到该函数。
声明该函数以extern "C"确保它使用Java 可以理解的C 应用程序二进制接口 (ABI) 。
Rust 示例:
#[no_mangle] |
解释:
- *mut Point:该函数返回一个原始指针(*mut Point),Java 可以使用FFM API对其进行管理。
第 2 步:将 Rust 编译成共享库
要将 Rust 代码编译为 Java 可以加载的格式,请修改Cargo.toml文件:
[lib] |
然后,将 Rust 项目编译为共享库:
cargo build --release
该命令将在目录中生成一个共享库文件(例如libmyrustlib.so或myrustlib.dll)target/release/,Java 可以动态加载该文件。
一旦 Rust 库被编译,Java 就可以加载共享库并访问 Rust 函数。
步骤 3:加载 Rust 共享库
Java 用于SymbolLookup加载共享库并检索 Rust 函数的地址。JavaLinker允许我们将这些地址绑定到可调用MethodHandle对象,这些对象代表 Java 中的本机函数。以下是如何加载 Rust 库并链接函数create_point:
Java 示例
import java.lang.foreign.*; |
解释:
libraryLookup:加载 Rust 共享库(libmyrustlib.so)。该库必须在 Java 类路径或系统的库路径中可用。
FunctionDescriptor:用 Java 术语定义 Rust 函数的签名。例如:
- ValueLayout.ADDRESS:对应于一个指针(Rust 的*mut)。
- ValueLayout.JAVA_INT:对应于 Rust 的i32。
- MethodHandle:表示链接的 Rust 函数。这是 Java 调用 Rust 函数的方式。
步骤 4:从 Java 调用 Rust 函数
加载库并链接函数后,我们现在可以使用从 Java 调用 Rust 函数。以下是在 Rust 中创建一个点、获取其值并释放内存的MethodHandle.invokeExact()方法:x
Java 示例
public class Main { |
解释:
- MemorySegment:这是 Java 处理传入和传出 Rust 的内存的方式。
这里,它表示指向 Rust Point 结构的原始指针。
invokeExact():使用指定的参数调用链接的 Rust 函数。在本例中:
- RustBindings.createPoint.invokeExact(10, 20) 在 Rust 中创建一个 x = 10、y = 20 的点。
- RustBindings.getX.invokeExact(point) 从 Rust 点中获取 x 值。
- RustBindings.freePoint.invokeExact(point) 释放 Rust 中的内存。
在 Rust 中识别所有权和借用
Rust 执行严格的所有权规则。当 Rust 中的函数取得某个值的所有权(例如Box、Vec)时,这意味着调用者不再拥有该值,除非归还所有权,否则不能再次使用它。借用(&T或&mut T)允许临时访问某个值而不转移所有权。
例子:
fn take_ownership(v: Vec<i32>) -> Vec<i32> { |
在 Java 中处理所有权
当 Rust 函数取得值的所有权时,Java 需要管理何时释放底层内存。如果 Java 创建了对象(例如,Box在 Rust 中调用),则必须明确释放该对象。Java 还必须确保内存在借用的引用的生命周期内有效。
您需要做什么:
- 对于承担所有权的函数: 您需要使用 Java 中的 MethodHandle调用相应的 Rust 清理函数(如 drop 或 free)。
- 对于借用引用: 使用 Arena管理内存,确保内存在借用期限内保持有效。
Java Example (Handling Ownership):
// Create a Rust-owned Box and pass ownership MemorySegment rustBox = (MemorySegment) |
解释:
MemorySegment表示 Java 中 Rust 分配的内存。 将所有权转移给 Rust 后,Java 会显式调用 freeBox 来释放内存。
识别 Rust 中的结构和内存布局
当 Rust 返回结构体或数组等复杂数据类型时,Java 需要正确解释它们的内存布局。Rust 的结构体字段根据其类型大小在内存中对齐,因此 Java 必须使用StructLayout和ValueLayout来精确匹配 Rust 的内存布局。
例子:
#[repr(C)] |
该#[repr(C)]属性确保的内存布局Point遵循C ABI,使其与 Java 的FFM API兼容。
在 Java 中处理结构体
Java 使用StructLayout来定义与 Rust 结构布局相匹配的内存布局。处理 Rust 结构时,必须确保 Java 端分配的内存正确对齐且大小正确,以匹配 Rust 结构的布局。
您需要做什么:
- 使用StructLayout定义镜像 Rust 结构字段的内存布局。
- 分配一个足够大并且正确对齐的MemorySegment来保存结构的数据。
Java 示例(处理结构):
// Define the memory layout of the Rust |
解释:
- StructLayout:定义 Rust 结构的布局Point,其中每个字段根据其类型对齐(在本例中,两个字段都是i32,因此每个字段为 4 个字节)。
- VarHandle:用于访问和设置为结构分配的内存段中的x和y
- MemorySegment:表示为结构体分配的内存,Java 可以根据结构的布局安全地操作它。
识别 Rust 中的线程安全
在 Rust 中,线程安全是通过 Send 和 Sync 特性来确保的。 如果 Rust 函数跨多个线程运行,函数中使用的类型必须实现 Send 或 Sync。 例如,如果一个 Rust 函数使用 Mutex 或 Arc 来管理共享数据,那么它就是线程安全的。
use std::sync::{Arc, Mutex}; |
确保 Java 中的线程安全
在跨语言处理线程安全问题时,Java 必须确保在线程之间安全地共享内存。 Java 的 FFM API 提供了共享区域(Shared Arenas),允许多个线程安全地访问内存。
怎么办:
- 当 Rust 中需要共享内存或线程安全操作时,请使用共享区域(Shared Arenas)。
- Java 还提供了同步块等同步机制,以确保线程安全。
// Create a shared arena for multi-threaded operations |
- Shared Arena: 当与 Rust 的线程安全类型(如 Arc 和 Mutex)交互时,可确保内存在 Java 的线程间安全共享。
- 同步块(Synchronized Block): 确保每次只有一个线程访问共享内存,模仿 Rust 的共享数据所有权规则。
其他数据结构处理点击标题