#java #java-class #jni #jvm #external-ffi-bindings

jaffi_support

对 jaffi 代码生成器的支持

1 个不稳定版本

0.2.0 2022年8月6日

#1581开发工具

Download history 24/week @ 2024-04-02

78 每月下载量
用于 jaffi

MIT/Apache

35KB
556

简介

这个库的功能类似于 C 的 bindgen,但由于 Java 和 JNI 的调用约定不如其他语言简单,因此它生成了一些桥接函数,以便在 Rust 和 Java 之间进行集成。名字来源于 Ja(va)FFI -> Jaffi

警告 这还处于非常早期阶段,没有全面的测试套件来验证 FFI 边界上的每个功能是否正常。

构建此项目

需要安装 Rust 和 Java 工具链。

  • 安装 Rust: rustup
  • 安装 Just: cargo install just // 用于简单脚本执行
  • 安装 Java: OpenJDK // 此处使用 Java 18 进行了测试

现在测试应该像预期那样工作(可能会有很多警告,此项目正在积极开发中),如果一切正常,构建详情后应该有此输出

$> just test
...
Running tests
loadLibrary succeeded for jaffi_integration_tests
running tests jaffi_integration_tests
void_1void: do nothing
void_1long__J: got 100
void_1long__JI: 100 + 10 = 110
void_1long__JI: 2147483647 + 2147483647 = 4294967294
add_1values_1native: calling java with: 2147483647, 2147483647
add_1values_1native: got result from java: 4294967294
print_1hello_1native_1static: calling print_hello, statically
hello!
print_1hello_1native: calling print_hello
hello!
call_1dad_1native with 732
All tests succeeded

入门指南

Jaffi 库将根据指定的配置参数扫描类文件。目前存在一些不足,目前只支持解压缩的类路径,即如果类路径中包含 jar 文件,则构建将失败。

要使用此库,它尚未发布到 Crates.io,您需要在 Cargo.toml 中添加类似以下的依赖项

[build-dependencies]
jaffi = "0.2.0"

[dependencies]
jaffi_support = "0.2.0"

添加后,您需要创建一个 build.rs 脚本来执行 Jaffi,如下所示(参见集成测试中的示例 build.rs

fn main() -> Result<(), Box<dyn Error>> {
    let class_path = class_path();
    let classes = vec![Cow::from("net.bluejekyll.NativeClass")];
    let classes_to_wrap = vec![Cow::from("net.bluejekyll.ParentClass")];
    let output_dir = PathBuf::from(std::env::var("OUT_DIR").expect("OUT_DIR not set"));

    let jaffi = Jaffi::builder()
        .native_classes(classes)
        .classes_to_wrap(classes_to_wrap)
        .classpath(vec![Cow::from(class_path)])
        .output_dir(Some(Cow::from(output_dir)))
        .build();

    jaffi.generate()?;

    Ok(())
}

如果Jaffi运行成功,它将在构建路径中生成一个名为 generated_jaffi.rs 的文件,位于 OUT_DIR。这个Rust文件对接口的实现方式有一些期望。它寻找一个名为 super::{Class}RsImpl 的类型,即它期望这个类型在 super 模块中,即在包含生成代码的模块之上。可以将 generated_jaffi.rs 文件包含在一个模块中来实现这一点,请参见示例 NativeClassRsImpl

use crate::net_bluejekyll::{net_bluejekyll_NativeClass, net_bluejekyll_NativeClassClass};

mod net_bluejekyll {
    include!(concat!(env!("OUT_DIR"), "/generated_jaffi.rs"));
}

impl<'j> net_bluejekyll::NativeClassRs<'j> for NativeClassRsImpl<'j> {
    // implement methods here
}

该文件是从具有原生接口定义的Java类文件生成的,例如

public class NativeClass extends ParentClass {
    // basic test
    public static native void void_void();
}

使用生成的代码

生成文档

所有可用函数的发现都可以通过 cargo doc --document-private-items --open 轻松完成。

RsImpl实现了Rs特质

这是该库的主要优点之一。它将从Java生成类型安全的绑定,并要求*RsImpl类型实现所有必需的原生函数。此外,它正确地将Rust调用和Java之间的类型进行转换(在撰写本文时,仅测试了基本类型)。编译器将有助于失败,直到所有原生接口都已实现。

integration_tests 的示例,这些Java原生接口

public class NativePrimitives extends ParentClass {
    // basic test
    public static native void voidVoid();

    // a parameter
    public static native void voidLong(long foo);

    // ...
}

然后被生成到Rust中的一个特质

pub trait NativePrimitivesRs<'j> {
    fn from_env(env: JNIEnv<'j>) -> Self;
    fn void_void(&self, class: NetBluejekyllNativePrimitivesClass<'j>);
    fn void_long_j(
        &self, 
        class: NetBluejekyllNativePrimitivesClass<'j>, 
        arg0: i64
    );

    // ...
}

其中 env 应在 from_env 中捕获,它构建Rust类型。然后从关联的C FFI函数(无需直接使用这些函数)调用绑定

#[no_mangle]
pub extern "system" fn Java_net_bluejekyll_NativePrimitives_voidVoid<'j>(
    env: JNIEnv<'j>, 
    class: NetBluejekyllNativePrimitivesClass<'j>
) -> JavaVoid {
    // ...
}

#[no_mangle]
pub extern "system" fn Java_net_bluejekyll_NativePrimitives_voidLong__J<'j>(
    env: JNIEnv<'j>, 
    class: NetBluejekyllNativePrimitivesClass<'j>, 
    arg0: JavaLong
) -> JavaVoid {
    // ...
}

所有调用Rust的代码都正确地包装在panic处理程序中,并将错误转换为异常(反之亦然)

指定类的包装器,即回调到Java

在所有函数调用中,都提供了一个this参数,可以调用类上的任何public方法。对于静态方法,this绑定到生成的类型 *Class。静态方法调用和对象方法调用都共享一个特质,该特质为两者都实现了,暴露了所有public static方法。

inegration_tests 的示例

    /// A constructor method wrapped and then the type returned from Rust to Java
    fn ctor(
        &self,
        _class: NetBluejekyllNativeStringsClass<'j>,
        arg0: String,
    ) -> NetBluejekyllNativeStrings<'j> {
        println!("ctor: {arg0}");
        NetBluejekyllNativeStrings::new_1net_bluejekyll_native_strings_ljava_lang_string_2(
            self.env, arg0,
        )
    }

支持超类

如果已在 build.rs 中指定为 classes_to_wrap 选项,则除了指定的超类之外,任何超类也会被包装,包括出现在参数中的任何类(以及它们在类路径中找到)都将生成包装器。要访问超类或接口及其方法,只需在对象上调用 this.as_{package}_{Class}(),然后可以在对象上调用超类方法。

integration_tests 的示例

    fn call_dad_native(
        &self,
        this: net_bluejekyll::NetBluejekyllNativePrimitives<'j>,
        arg0: i32,
    ) -> i32 {
        println!("call_dad_native with {arg0}");

        let parent = this.as_net_bluejekyll_parent_class();
        parent.call_1dad(self.env, arg0)
    }

异常、错误和恐慌

Rust 代码中的任何恐慌将通过 std::panic::set_hookstd::panic::catch_unwind 被捕获。恐慌钩子将在 Java 中创建一个 RuntimeException(基于 Rust 中的 PanicInfo)。catch_unwind 将捕获恐慌并确保从本地方法返回一个适当的默认空值,这个值基本上是无用的,因为异常应该短路 Java 中的返回。

Java 类文件中的类型签名将用于异常。Rust 将生成一个枚举类型,包含可以在 Java 中抛出或由 jaffi 自动将 Rust 错误转换为 Java 异常的各种异常类型。Rust 中生成的接口将这些对话从方法接口中抽象出来。如果一个方法在其 throws 部分中没有列出异常,但需要捕获这些异常,可以通过生成的 JNIEnv 手动完成,这些 JNIEnv 对生成的方 法是可用的。

来自 integration_tests 的示例

    fn throws_something(
        &self,
        _this: NetBluejekyllExceptions<'j>,
    ) -> Result<(), Error<SomethingExceptionErr>> {
        Err(Error::new(
            SomethingExceptionErr::SomethingException(SomethingException),
            "Test Message",
        ))
    }

    fn catches_something(
        &self,
        this: net_bluejekyll::NetBluejekyllExceptions<'j>,
    ) -> net_bluejekyll::NetBluejekyllSomethingException<'j> {
        let ex = this
            .i_always_throw(self.env)
            .expect_err("error expected here");

        #[allow(irrefutable_let_patterns)]
        if let SomethingExceptionErr::SomethingException(SomethingException) = ex.throwable() {
            net_bluejekyll::NetBluejekyllSomethingException::from(JObject::from(ex.exception()))
        } else {
            panic!("expected SomethingException")
        }
    }

    /// this panic will generate an RuntimeException in Java.
    fn panics_are_runtime_exceptions(&self, _this: NetBluejekyllExceptions<'j>) {
        panic!("{}", "Panics are safe".to_string());
    }

接下来是什么?

我构建这个是为了帮助我正在做的另一个项目,我在那里不断地追踪 FFI 绑定中的错误,因为变量改变了,签名没有得到适当的更新。这应该有助于减少这些简单的错误,并在使用 JNI 和 Rust 的工作中提高生产力。

谢谢

这个项目大量使用了这些 crates,感谢为它们工作过的每个人

  • cafebabe - 一个 Java 类文件读取器
  • jni - Rust 中的最先进的 JNI 支持
  • tinytemplate - 用于所有 Rust 代码生成

谢谢!

许可

根据您的选择,许可为以下之一

贡献

除非您明确说明,否则任何旨在包含在作品中的贡献(根据 Apache-2.0 许可证定义),都应按上述方式双许可,不附加任何额外条款或条件。

依赖关系

~1.3–2.6MB
~44K SLoC