Rust 的 Java 绑定:综合指南


本手册旨在提供使用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 之间安全地分配、管理和释放内存,以确保最佳性能并避免内存泄漏或未定义的行为。

本手册将:

  1. 提供分步指南:开发人员将指导如何在 Rust 和 Java 之间建立绑定,并为项目配置这些绑定。
  2. 演示实际示例:将提供并解释正确设计的绑定示例。这些示例将针对简单和复杂的主题提供,包括公开 Rust 函数、处理复杂数据类型、管理生命周期和内存以及处理多线程。
  3. 简化 Rust-Java 集成:该手册将揭开集成过程的神秘面纱,帮助开发人员避免与所有权、内存管理和数据布局差异相关的常见陷阱。
  4. 解决高级主题:除了基础知识之外,该手册还将探讨高级主题,如线程安全、处理 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]
pub extern "C" fn create_point(x: i32, y: i32) -> *mut Point {
    Box::into_raw(Box::new(Point { x, y }))
}

#[no_mangle]
pub extern
"C" fn get_x(point: *mut Point) -> i32 {
    unsafe { (*point).x }
}

#[no_mangle]
pub extern
"C" fn free_point(point: *mut Point) {
    unsafe { Box::from_raw(point); }
// Frees the allocated memory
}

struct Point {
    x: i32,
    y: i32,
}

解释:
  • *mut Point:该函数返回一个原始指针(*mut Point),Java 可以使用FFM API对其进行管理。

第 2 步:将 Rust 编译成共享库
要将 Rust 代码编译为 Java 可以加载的格式,请修改Cargo.toml文件:

[lib]
crate-type = ["cdylib"]

然后,将 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.*;
import java.lang.invoke.MethodHandle;

public class RustBindings {
    static MethodHandle createPoint;
    static MethodHandle getX;
    static MethodHandle freePoint;

    static {
        var linker = Linker.nativeLinker(); // Initializes the native linker
        var lib = SymbolLookup.libraryLookup(
"libmyrustlib.so", Arena.global()); // Loads the Rust library

       
// Link the Rust functions
        createPoint = linker.downcallHandle(
            lib.find(
"create_point").orElseThrow(), 
            FunctionDescriptor.of(ValueLayout.ADDRESS, ValueLayout.JAVA_INT, ValueLayout.JAVA_INT)
        );
        getX = linker.downcallHandle(
            lib.find(
"get_x").orElseThrow(), 
            FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS)
        );
        freePoint = linker.downcallHandle(
            lib.find(
"free_point").orElseThrow(), 
            FunctionDescriptor.ofVoid(ValueLayout.ADDRESS)
        );
    }
}

解释:
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 {
    public static void main(String[] args) throws Throwable {
        // Create a point in Rust
        MemorySegment point = (MemorySegment) RustBindings.createPoint.invokeExact(10, 20);

       
// Get the x value from the point
        int xValue = (int) RustBindings.getX.invokeExact(point);
        System.out.println(
"X value: " + xValue);

       
// Free the Rust point
        RustBindings.freePoint.invokeExact(point);
    }
}

解释:

  • 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> {
    // Takes ownership of v
    v
}

fn borrow(v: &Vec<i32>) -> i32 {
   
// Borrows v temporarily
    v[0]
}

在 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) 
RustBindings.createBox.invokeExact(10); 

// Call Rust function to take ownership of the box 
RustBindings.takeOwnership.invokeExact(rustBox); 

// Manually free the Box when done 
RustBindings.freeBox.invokeExact(rustBox);
// Ensures no memory leaks

解释:
MemorySegment表示 Java 中 Rust 分配的内存。 将所有权转移给 Rust 后,Java 会显式调用 freeBox 来释放内存。


识别 Rust 中的结构和内存布局
当 Rust 返回结构体或数组等复杂数据类型时,Java 需要正确解释它们的内存布局。Rust 的结构体字段根据其类型大小在内存中对齐,因此 Java 必须使用StructLayout和ValueLayout来精确匹配 Rust 的内存布局。

例子:

#[repr(C)]
struct Point {
    x: i32,
    y: i32,
}

该#[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 Point struct in Java
StructLayout pointLayout = MemoryLayout.structLayout(
    ValueLayout.JAVA_INT.withName(
"x"),  // Field x (i32 in Rust)
    ValueLayout.JAVA_INT.withName(
"y")   // Field y (i32 in Rust)
);

// Allocate memory for the struct
var arena = Arena.ofConfined();  
// Confined Arena for memory management
MemorySegment pointSegment = arena.allocate(pointLayout);

// Set the fields of the Point struct
VarHandle xHandle = pointLayout.varHandle(PathElement.groupElement(
"x"));
VarHandle yHandle = pointLayout.varHandle(PathElement.groupElement(
"y"));
xHandle.set(pointSegment, 0, 10);  
// Set x to 10
yHandle.set(pointSegment, 0, 20);  
// Set y to 20

解释:

  • 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};

pub fn create_shared_data() -> Arc<Mutex<i32>> {
    Arc::new(Mutex::new(42))
}


确保 Java 中的线程安全
在跨语言处理线程安全问题时,Java 必须确保在线程之间安全地共享内存。 Java 的 FFM API 提供了共享区域(Shared Arenas),允许多个线程安全地访问内存。

怎么办:

  • 当 Rust 中需要共享内存或线程安全操作时,请使用共享区域(Shared Arenas)。
  • Java 还提供了同步块等同步机制,以确保线程安全。

// Create a shared arena for multi-threaded operations
var sharedArena = Arena.ofShared();
MemorySegment sharedSegment = sharedArena.allocate(8);  
// Allocate space for shared memory

// Call Rust function that operates on shared data
RustBindings.createSharedData.invokeExact(sharedSegment);

// Access shared data across threads (ensure proper synchronization in Java)
synchronized (sharedSegment) {
   
// Safe access to shared memory here
}

解释:

  • Shared Arena: 当与 Rust 的线程安全类型(如 Arc 和 Mutex)交互时,可确保内存在 Java 的线程间安全共享。
  • 同步块(Synchronized Block): 确保每次只有一个线程访问共享内存,模仿 Rust 的共享数据所有权规则。

其他数据结构处理点击标题