首頁  >  文章  >  Java  >  rs 建構 JNI 框架

rs 建構 JNI 框架

Patricia Arquette
Patricia Arquette原創
2024-10-02 06:10:30466瀏覽

rs Building a JNI Framework

嘿!從技術上來說,這是我的第二篇文章,但這是我的第一篇真正的文章。 (只是忽略 t3d 帖子。)

我也已經很久沒用過這個帳號了,不過無論如何

在這篇文章中,我將介紹 rs4j 是什麼、如何使用它以及我如何建造它。

它是什麼?

rs4j 是我建立的一個 Rust 函式庫,旨在簡化使用 Rust 編寫的本機程式碼的 Java 函式庫的建立。它會產生 JNI(Java 本機介面)程式碼來完成此操作。

好吧,這很酷,但我為什麼要關心呢?

rs4j 允許您將高計算工作卸載到更快的運行時(垃圾收集器,看看您),而不是在 JVM 中全部運行並破壞性能。像 Create Aeronautics(或更準確地說,Create Simulated)這樣的 Minecraft mods 使用這種技術來進行一些物理計算,否則使用 Java 會非常延遲。

rs4j 允許您使用最少的程式碼輕鬆創建這樣的本機接口,並使用最少的程式碼輕鬆移植整個庫以便與 Java 一起使用。

好的,現在我感興趣了 - 但我該如何使用它?

使用起來很簡單!只需按照以下步驟操作:

  1. 設定您的庫類型:
# Cargo.toml

[lib]
crate-type = ["cdylib"]
  1. 將 rs4j 加入您的依賴項:
cargo add rs4j
  1. 將 rs4j 加入您的建置依賴項:
cargo add rs4j --build -F build # Enable the `build` feature

# Also add anyhow for error handling
cargo add anyhow --build
  1. 設定你的建置腳本:
// build.rs

use rs4j::build::BindgenConfig;
use anyhow::Result;

fn main() -> Result<()> {
    // Make a new config
    BindgenConfig::new()

        // Set the package for export
        .package("your.package.here")

        // Where to save the Rust bindings
        .bindings(format!("{}/src/bindings.rs", env!("CARGO_MANIFEST_DIR")))

        // Where the input files are
        .glob(format!("{}/bindings/**/*.rs4j", env!("CARGO_MANIFEST_DIR")))?

        // Where to save java classes (is a directory)
        .output(format!("{}/java", env!("CARGO_MANIFEST_DIR")))

        // Enable JetBrains annotations (this is a TODO on my end)
        .annotations(true)

        // Go!
        .generate()?;

    Ok(())
}
  1. 設定建置後腳本(選用):

rs4j 使用建置後腳本來完成建置後的操作。
這在技術上是可選的,但建議這樣做。

# Cargo.toml

[features]
default = []
post-build = ["rs4j/build", "anyhow"]

[[bin]]
name = "post-build"
path = "post-build.rs"
required-features = ["post-build"]

[dependencies]
anyhow = { version = "[...]", optional = true } # Set the version to whatever you want
rs4j = "[...]" # Whatever you had before
// post-build.rs

use anyhow::Result;
use rs4j::build::BindgenConfig;

fn main() -> Result<()> {
    let out_path = format!("{}/generated", env!("CARGO_MANIFEST_DIR"));
    let src_path = format!("{}/java/src/generated", env!("CARGO_MANIFEST_DIR"));

    BindgenConfig::new()
        // This should be the same as the normal buildscript
        .package("com.example")
        .bindings(format!("{}/src/bindings.rs", env!("CARGO_MANIFEST_DIR")))
        .glob(format!("{}/bindings/**/*.rs4j", env!("CARGO_MANIFEST_DIR")))?
        .output(&out_path)
        .annotations(false)

        // Run post-build actions
        .post_build()?

        // Copy it to your Java project
        .copy_to(src_path)?;

    Ok(())
}
  1. 安裝 rs4j 的 CLI(選購)

如果您不想使用建置後腳本,這是可選的。

cargo install rs4j --features cli
  1. 建造!

修改任何腳本,如下:

- cargo build
+ rs4j build # `rs4j build` supports all of `cargo build`'s arguments after a `--`.

句法

這是文法的基本摘要:

// This class, Thing, takes in one type parameter, `A`.
// You can omit this if it doesn't take any type parameters.
class Thing<A> {
    // This makes it so that Rust knows that the type for `A`
    // will have `Clone + Copy`. This doesn't change anything
    // on the Java side, it's just so that Rust will compile.
    bound A: Clone + Copy;

    // This will generate getters and setters for the field `some`.
    field some: i32;

    // Here, the Rust function's name is `new`, and Java will treat
    // it as a constructor.
    static init fn new(value: A) -> Thing;

    // This gets the value. Since this is in snake_case, rs4j will
    // automatically convert it into camelCase, renaming this to
    // `getValue` on the Java side.
    fn get_value() -> A;

    // This marks this function as mutable, meaning in Rust it will
    // mutate the struct, as if it took a `&mut self` as an argument.
    mut fn set_value(value: A);

    // You can even include trait methods, as long as Rust can find the
    // trait it belongs to!
    fn clone() -> A;
};

它是如何製作的?

rs4j 使用 peg 解析器來處理其語言。此解析器直接將解析後的結構轉換為抽象語法樹,進而轉換為程式碼。

rs4j 是強型的。我有一個 Type 結構和一個 TypeKind 枚舉來完成此任務。

類型種類

使用以下程式碼解析這些:

parser! {
    /// The rs4j parser.
    pub grammar rs4j_parser() for str {
        ...

        // Type kinds

        rule _u8_k() -> TypeKind = "u8" { TypeKind::U8 }
        rule _u16_k() -> TypeKind = "u16" { TypeKind::U16 }
        rule _u32_k() -> TypeKind = "u32" { TypeKind::U32 }
        rule _u64_k() -> TypeKind = "u64" { TypeKind::U64 }
        rule _i8_k() -> TypeKind = "i8" { TypeKind::I8 }
        rule _i16_k() -> TypeKind = "i16" { TypeKind::I16 }
        rule _i32_k() -> TypeKind = "i32" { TypeKind::I32 }
        rule _i64_k() -> TypeKind = "i64" { TypeKind::I64 }
        rule _f32_k() -> TypeKind = "f32" { TypeKind::F32 }
        rule _f64_k() -> TypeKind = "f64" { TypeKind::F64 }
        rule _bool_k() -> TypeKind = "bool" { TypeKind::Bool }
        rule _char_k() -> TypeKind = "char" { TypeKind::Char }
        rule _str_k() -> TypeKind = "String" { TypeKind::String }
        rule _void_k() -> TypeKind = "()" { TypeKind::Void }
        rule _other_k() -> TypeKind = id: _ident() { TypeKind::Other(id) }
        rule _uint_k() -> TypeKind = _u8_k() / _u16_k() / _u32_k() / _u64_k()
        rule _int_k() -> TypeKind = _i8_k() / _i16_k() / _i32_k() / _i64_k()
        rule _float_k() -> TypeKind = _f32_k() / _f64_k()
        rule _extra_k() -> TypeKind = _bool_k() / _char_k() / _str_k() / _void_k()

        ...
    }
}

如您所見,每種原始類型都有不同的規則,然後是一個包羅萬象的規則。這使我可以輕鬆驗證並輸出正確的程式碼。

您可以在此處查看更多解析器。

程式碼產生器

rs4j 使用自訂程式碼產生系統,該系統大量使用 format!() 來建立程式碼。雖然這不是最正確或最安全的,但它在我的幾乎所有測試中都創建了正確的程式碼(唯一的問題是我正在研究的泛型)。

程式碼產生是透過每個 AST 節點完成的,每個 AST 節點都有自己的函數將其轉換為 Java 和 Rust 程式碼。

本地實現

在您的 lib.rs 中,您必須包含! ()您的 bindings.rs 文件,其中包含本機實作。

您為其產生綁定的每個結構都將用 JNI 包裝。下面是一個範例:

class MyOtherStruct {
    field a: String;
    field b: MyStruct;

    static init fn new() -> Self;

    fn say_only(message: String);
    fn say(p2: String);
    fn say_with(p1: MyStruct, p2: String);
};
// lib.rs

...

#[derive(Debug)]
pub struct MyOtherStruct {
    pub a: String,
    pub b: MyStruct,
}

impl MyOtherStruct {
    pub fn new() -> Self {
        Self {
            a: String::new(),
            b: MyStruct::new(),
        }
    }

    pub fn say_only(&self, message: String) {
        println!("{}", message);
    }

    pub fn say(&self, p2: String) {
        println!("{}{}", self.b.a, p2);
    }

    pub fn say_with(&self, p1: MyStruct, p2: String) {
        println!("{}{}", p1.a, p2);
    }
}

include!("bindings.rs");

// bindings.rs
// #[allow(...)] statements have been removed for brevity.

#[allow(non_camel_case_types)]
pub struct __JNI_MyOtherStruct {
    pub a: String,
    pub b: *mut MyStruct,
}

impl __JNI_MyOtherStruct {
    pub unsafe fn of(base: MyOtherStruct) -> Self {
        Self {
            a: base.a.clone(),
            // yes, this is an intentional memory leak.
            b: Box::leak(Box::new(base.b)) as *mut MyStruct,
        }
    }

    pub unsafe fn to_rust(&self) -> MyOtherStruct {
        MyOtherStruct {
            a: self.a.clone(),
            b: (&mut *self.b).clone(),
        }
    }

    pub unsafe fn __wrapped_new() -> Self {
        let base = MyOtherStruct::new();

        Self::of(base)
    }

    pub unsafe fn __wrapped_say_only(&self, message: String) -> () {
        MyOtherStruct::say_only(&self.to_rust(), message).clone()
    }

    pub unsafe fn __wrapped_say(&self, p2: String) -> () {
        MyOtherStruct::say(&self.to_rust(), p2).clone()
    }

    pub unsafe fn __wrapped_say_with(&self, p1: MyStruct, p2: String) -> () {
        MyOtherStruct::say_with(&self.to_rust(), p1, p2).clone()
    }
}

當建構一個物件時,它會呼叫包裝方法,該方法會故意洩漏每個嵌套物件以取得其指標。這使我可以在任何上下文中隨時存取該物件。

所有方法都經過包裝,以便 JNI 更輕鬆地呼叫它們。

JNI代碼

說到這裡,JNI 程式碼如下圖:

// This is a field, here's the getter and setter.
// #[allow(...)] statements have been removed for brevity.

#[no_mangle]
pub unsafe extern "system" fn Java_com_example_MyOtherStruct_jni_1set_1a<'local>(
    mut env: JNIEnv<'local>,
    class: JClass<'local>,
    ptr: jlong,
    val: JString<'local>,
) -> jlong {
    let it = &mut *(ptr as *mut __JNI_MyOtherStruct);
    let val = env.get_string(&val).unwrap().to_str().unwrap().to_string();

    it.a = val;

    ptr as jlong
}

#[no_mangle]
pub unsafe extern "system" fn Java_com_example_MyOtherStruct_jni_1get_1a<'local>(
    mut env: JNIEnv<'local>,
    class: JClass<'local>,
    ptr: jlong,
) -> jstring {
    let it = &*(ptr as *mut __JNI_MyOtherStruct);
    env.new_string(it.a.clone()).unwrap().as_raw()
}

除了存取物件之外,這對 jni 箱來說是非常標準的東西。 &*(ptr as *mut __JNI_MyOtherStruct) 可能看起來不安全,那是因為它。然而,這是故意的,因為如果正確完成,指針應該始終有效。

請注意,在 setter 的末尾,它會傳回物件的指標。這是有意的。這允許 Java 重置其內部指針,追蹤最新的有效指針。

釋放記憶體

釋放記憶體本質上是回收指針,然後刪除它。它還釋放了所有非原始字段。

// #[allow(...)] statements have been removed for brevity.

#[no_mangle]
pub unsafe extern "system" fn Java_com_example_MyOtherStruct_jni_1free<'local, >(_env: JNIEnv<'local>, _class: JClass<'local>, ptr: jlong) {
    // Reclaim the pointer
    let it = Box::from_raw(ptr as *mut __JNI_MyOtherStruct);

    // Reclaim the other field
    let _ = Box::from_raw(it.b);
}

但是,此方法有一個已知的錯誤,即如果巢狀物件的深度超過一層,則該方法始終會導致記憶體洩漏。我對如何解決這個問題有一些想法,但我一直專注於其他事情。

爪哇方面

rs4j 產生的每個 Java 類別將從其他兩個介面繼承:ParentClass 和 NativeClass。

這是兩者的定義:

// NativeClass.java

package org.stardustmodding.rs4j.util;

public interface NativeClass {
    long getPointer();
}

// ParentClass.java

package org.stardustmodding.rs4j.util;

public interface ParentClass {
    void updateField(String field, long pointer);
}

每個類別由幾個部分組成,包括:

  • JNI Methods
// Notice how all of these functions take a `long ptr` as an argument. This is the pointer to the underlying struct in Rust.

// This is a constructor - it takes no pointer but returns one.
private native long jni_init_new();

// Methods
private static native void jni_say_only(long ptr, String message);
private static native void jni_say(long ptr, String p2);
private static native void jni_say_with(long ptr, long p1, String p2);

// Getters & Setters
private static native long jni_set_a(long ptr, String value);
private static native String jni_get_a(long ptr);
// Notice how this field isn't primitive, so it uses the pointer instead.
private static native long jni_set_b(long ptr, long value);
private static native long jni_get_b(long ptr);

// Freeing memory
private static native void jni_free(long ptr);
  • Fields
// The pointer to the Rust object
private long __ptr = -1;
// If this is a field in another class, it keeps track of it for updating purposes
private ParentClass __parent = null;
// The name of the field in the other class
private String __parentField = null;
  • Constructors
public MyOtherStruct() {
    // Sets the pointer using the constructor
    __ptr = jni_init_new();
}
  • Methods
// Notice how these all just call the JNI method, providing the pointer.

public void sayOnly(String message) {
    jni_say_only(__ptr, message);
}

public void say(String p2) {
    jni_say(__ptr, p2);
}

public void sayWith(MyStruct p1, String p2) {
    jni_say_with(__ptr, p1.getPointer(), p2);
}
  • Fields
// Notice how the setters all update the field in the parent. This allows the user to have Java-like behavior, where modifying a class that is a property of another will update that reference.

public void setA(String value) {
    __ptr = jni_set_a(__ptr, value);

    if (__parent != null) {
        __parent.updateField(__parentField, __ptr);
    }
}

public String getA() {
    return jni_get_a(__ptr);
}

public void setB(MyStruct value) {
    // .getPointer() gets the underlying pointer, this is from the NativeClass interface.
    __ptr = jni_set_b(__ptr, value.getPointer());

    if (__parent != null) {
        __parent.updateField(__parentField, __ptr);
    }
}

public MyStruct getB() {
    // Essentially this is a glorified cast.
    return MyStruct.from(jni_get_b(__ptr), this, "b");
}
  • Default constructors
// Just creates an instance from a pointer.
private MyOtherStruct(long ptr) {
    __ptr = ptr;
}

// Creates an instance from a pointer, with a parent
private MyOtherStruct(long ptr, ParentClass parent, String parentField) {
    __ptr = ptr;
    __parent = parent;
    __parentField = parentField;
}

// These are for other classes to "cast" to this class.

public static MyOtherStruct from(long ptr) {
    return new MyOtherStruct(ptr);
}

public static MyOtherStruct from(long ptr, ParentClass parent, String parentField) {
    return new MyOtherStruct(ptr, parent, parentField);
}
  • And finally, default methods.
// I'M FREE!!!!
// This is ESSENTIAL for memory management, as Rust will otherwise never know when to free the memory that was leaked.
public void free() {
    jni_free(__ptr);
}

// Override from NativeClass.
@Override
public long getPointer() {
    return __ptr;
}

// Override from ParentClass.
@Override
public void updateField(String field, long pointer) {
    // `b` is non-primitive, so when it's updated it also has to be updated here.
    if (field == "b") {
        __ptr = jni_set_b(__ptr, pointer);
    }
}

Wrapping Up

This project is probably one of my proudest projects right now, as it's taken so much work and is proving to be pretty useful for me. I hope you'll check it out and play around with it, too!

Anyway, see you in the next one! I'll try to post more often if I can!

Special Thanks

Thanks to @RyanHCode for giving me a few tips on this!

Links

  • GitHub Repository: https://github.com/StardustModding/rs4j
  • Crates.io Page: https://crates.io/crates/rs4j
  • Docs.rs Page: https://docs.rs/rs4j

以上是rs 建構 JNI 框架的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn