1个不稳定版本

0.2.0 2022年8月8日
0.1.0 2022年7月1日

#337 in 过程宏

MIT/Apache

115KB
2K SLoC

简介

此库在功能上类似于bindgen对C的支持,尽管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"

一旦添加,您需要为执行Jaffi创建一个build.rs脚本,如下所示(请参阅集成测试以获取工作示例 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处理器中,并根据需要将错误转换为异常(反之亦然)。请参阅下面的ExceptionsErrorsPanics

特定类的包装器,即回调到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 将捕获恐慌并确保从原生方法返回一个适当的默认 null 值,这个值实际上毫无用处,因为异常应该使 Java 中的返回短路。

Java 类文件中的类型签名将用于评估异常。Rust 将生成一个枚举类型,其中包含可以在 Java 中抛出或由 jaffi 自动将 Rust 错误转换为 Java 异常的各种异常类型。Rust 生成的接口抽象出了方法接口中的这些对话。如果一个方法在其 throws 节中没有列出异常,但需要捕获这些异常,则可以通过生成的 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 时的生产力。

谢谢

该项目大量使用了这些crate,感谢所有为它们工作的人

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

谢谢!

许可证

根据您选择的以下任一项进行许可

任选其一。

贡献

除非您明确说明,否则根据 Apache-2.0 许可证定义的,您有意提交给作品以供包含的贡献,将根据上述方式双许可,而无需任何额外的条款或条件。

依赖项

~1.6–3MB
~52K SLoC