」工欲善其事,必先利其器。「—孔子《論語.錄靈公》
首頁 > 程式設計 > rs 建構 JNI 框架

rs 建構 JNI 框架

發佈於2024-11-08
瀏覽:366

rs Building a JNI Framework

Hey! This is my second post, technically, but it's my first real one. (Just gonna ignore the t3d post.)

I also haven't used this account in forever but whatever

In this post, I'm going to cover what rs4j is, how to use it, and how I built it.

What is it?

rs4j is a Rust library I created to ease the creation of Java libraries that use native code written in Rust. It generates JNI (Java Native Interface) code to accomplish this.

Okay, that's cool, but why should I care?

rs4j allows you to offload high-computation work to a much faster runtime (looks at you, garbage collector) instead of running it all in the JVM and ruining performance. Minecraft mods like Create Aeronautics (or Create Simulated, to be more correct) use this technique to do some of their physics calculations that would otherwise be very laggy with Java.

rs4j allows you to easily create native interfaces like this with minimal code and easily port entire libraries for use with Java with minimal code.

Okay, now I'm interested - but how can I use it?

Using it is easy! Just follow these steps:

  1. Set up your library type:
# Cargo.toml

[lib]
crate-type = ["cdylib"]
  1. Add rs4j to your dependencies:
cargo add rs4j
  1. Add rs4j to your build dependencies:
cargo add rs4j --build -F build # Enable the `build` feature

# Also add anyhow for error handling
cargo add anyhow --build
  1. Set up your buildscript:
// 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. Set up your post-build script (optional):

rs4j uses a post-build script to accomplish actions after building.
This is technically optional, but recommended.

# 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. Install rs4j's CLI (optional)

This is optional if you don't want to use the post-build script.

cargo install rs4j --features cli
  1. Build!

Modify any scripts like so:

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

Syntax

Here's a basic run-down of the syntax:

// This class, Thing, takes in one type parameter, `A`.
// You can omit this if it doesn't take any type parameters.
class Thing {
    // 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;
};

How was it made?

rs4j uses a peg parser to process its language. This parser directly turns the parsed structure into an abstract syntax tree, that is turned into code.

rs4j is strongly-typed. I have a Type struct and a TypeKind enum to accomplish this.

TypeKinds

These are parsed using this code:

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()

        ...
    }
}

As you can see, there's a different rule for each primitive type, and then a catch-all. This allows me to verify and output the correct code easily.

You can see more of the parser here.

Codegen

rs4j uses a custom codegen system that heavily uses format!() to create the code. While this isn't the most correct or safe, it creates correct code in almost all of my tests (the only issue is generics, which I'm working on).

The codegen is done with each AST node having its own functions to turn it into Java and Rust code.

Native Implementation

In your lib.rs, you have to include!() your bindings.rs file, which contains the native implementations.

Each struct you generate bindings for will be wrapped with JNI. Here's an example of what this looks like:

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()
    }
}

When an object is constructed, it calls the wrapped method which intentionally leaks every nested object to get its pointer. This allows me to access the object whenever I need to, in any context.

All methods are wrapped to allow JNI to call them much more easily.

JNI Code

Speaking of which, the JNI code looks like this:

// 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(
    mut env: JNIEnv,
    class: JClass,
    ptr: jlong,
    val: JString,
) -> 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(
    mut env: JNIEnv,
    class: JClass,
    ptr: jlong,
) -> jstring {
    let it = &*(ptr as *mut __JNI_MyOtherStruct);
    env.new_string(it.a.clone()).unwrap().as_raw()
}

This is pretty standard stuff for the jni crate, except for accessing the object. That &*(ptr as *mut __JNI_MyOtherStruct) might look unsafe, and that's because it is. This is intentional, however, as the pointer should always be valid if done correctly.

Notice that at the end of the setter, it returns the pointer of the object. This is intended. This allows Java to reset its internal pointer, keeping track of the latest valid pointer.

Freeing Memory

Freeing memory essentially reclaims the pointer and then drops it. It frees all non-primitive fields too.

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

#[no_mangle]
pub unsafe extern "system" fn Java_com_example_MyOtherStruct_jni_1free(_env: JNIEnv, _class: JClass, 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);
}

There is a known bug with this method, however, which is that the method will always end up leaking memory if there is a nested object more than one level deep. I have some ideas on how to fix this, but I've been focused on other things.

The Java Side

Every Java class that rs4j generates will inherit from two other interfaces, ParentClass, and NativeClass.

Here's the definition of both:

// 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);
}

Each class is made up of a few parts, including:

  • 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
版本聲明 本文轉載於:https://dev.to/redstonewizard08/rs4j-building-a-jni-framework-3ljb?1如有侵犯,請聯絡[email protected]刪除
最新教學 更多>
  • Bootstrap 4 Beta 中的列偏移發生了什麼事?
    Bootstrap 4 Beta 中的列偏移發生了什麼事?
    Bootstrap 4 Beta:列偏移的刪除和恢復Bootstrap 4 在其Beta 1 版本中引入了重大更改柱子偏移了。然而,隨著 Beta 2 的後續發布,這些變化已經逆轉。 從 offset-md-* 到 ml-auto在 Bootstrap 4 Beta 1 中, offset-md-*...
    程式設計 發佈於2024-11-09
  • 在 Go 中使用 WebSocket 進行即時通信
    在 Go 中使用 WebSocket 進行即時通信
    构建需要实时更新的应用程序(例如聊天应用程序、实时通知或协作工具)需要一种比传统 HTTP 更快、更具交互性的通信方法。这就是 WebSockets 发挥作用的地方!今天,我们将探讨如何在 Go 中使用 WebSocket,以便您可以向应用程序添加实时功能。 在这篇文章中,我们将介绍: WebSoc...
    程式設計 發佈於2024-11-09
  • 為什麼在 Java 中使用相同的種子時會得到相同的隨機數?
    為什麼在 Java 中使用相同的種子時會得到相同的隨機數?
    具有固定種子的Java隨機數:為什麼輸出相同? 在您的程式碼中,您定義了一種使用以下命令產生隨機數的方法指定的種子。但是,您會注意到,當您提供相同的種子時,所有 100 個產生的數字都是相同的。 此行為是預期的,因為在 Random 建構函式中使用相同的種子會產生可預測的數字序列。種子是初始化隨機數...
    程式設計 發佈於2024-11-09
  • jQuery Chaining 如何簡化開發並提高程式碼效率?
    jQuery Chaining 如何簡化開發並提高程式碼效率?
    理解 jQuery 中的物件和方法連結在 jQuery 中,連結允許在單一語句中串聯多個 jQuery 方法。這使開發人員能夠簡化程式碼並輕鬆執行複雜的操作。 連結的基本原理涉及每個 jQuery 方法的回傳值。當呼叫 jQuery 方法時,它通常會傳回一個表示所選元素的 jQuery 物件。這允許...
    程式設計 發佈於2024-11-09
  • Hono.js 基準測試:Node.js、Deno 和 Bun — 哪個最快?
    Hono.js 基準測試:Node.js、Deno 和 Bun — 哪個最快?
    Deno 2.0 剛剛發布,並聲稱比 Bun 和 Node.js 更快,同樣,Bun 也聲稱更快。這引起了我的興趣,所以我決定測試它們的性能,看看它們在現實場景中的比較。 為了公平比較,我需要選擇一個與所有三種 JavaScript 執行時間環境(Node.js、Deno 和 Bun)相容的框架。...
    程式設計 發佈於2024-11-09
  • 大批
    大批
    方法是可以在物件上呼叫的 fns 數組是對象,因此它們在 JS 中也有方法。 slice(begin):將陣列的一部分提取到新數組中,而不改變原始數組。 let arr = ['a','b','c','d','e']; // Usecase: Extract till index ...
    程式設計 發佈於2024-11-09
  • 何時應該使用 Tkinter 的 Entry Get 函數來有效檢索輸入?
    何時應該使用 Tkinter 的 Entry Get 函數來有效檢索輸入?
    Tkinter Entry 的Get 函數:深入探討其功能和用法在Tkinter 中,Entry 小部件通常用於收集用戶輸入以進一步收集使用者輸入然而,與Entry 關聯的get() 函數通常無法產生所需的結果,這可能會讓開發人員感到困惑。本文深入探討 get() 的概念,全面了解其執行與應用。 理...
    程式設計 發佈於2024-11-09
  • 如何克服 PHP 中日期表示的 2038 限制?
    如何克服 PHP 中日期表示的 2038 限制?
    PHP 中的日期表示:克服2038 年限制雖然PHP 的原生日期函數在2038 年有一個截止日期,但還有其他方法處理超出此限制的日期。其中一種方法是僅儲存年、月和日,而忽略小時、分鐘、秒和毫秒部分。 透過丟棄這些附加時間部分,可以顯著擴展可表示日期的範圍。這是因為這些元件中的每一個都佔用了 PHP ...
    程式設計 發佈於2024-11-09
  • 如何在 Go (Gorilla) 中向特定客戶端發送有針對性的 Websocket 更新?
    如何在 Go (Gorilla) 中向特定客戶端發送有針對性的 Websocket 更新?
    在Go (Gorilla) 中向特定客戶端發送Websocket 更新儘管是Go 新手,但您尋求有關實現Websocket 通信的指導您的預輸入項目。您已嘗試利用 Gorilla 的 GitHub 儲存庫中的範例,但在理解如何識別特定客戶端並針對 websocket 更新進行定位方面遇到了挑戰。 要...
    程式設計 發佈於2024-11-09
  • 使用swoole作為基於ESP6的腳本可程式控制器的雲端物聯網閘道框架
    使用swoole作為基於ESP6的腳本可程式控制器的雲端物聯網閘道框架
    腳本可程式控制器的本機功能基本上已完成,開始實現遠端相關功能。 遠端系統整體架構如下: 使用ESP8266的SDK實作tcp伺服器和tcp客戶端。 在tcp伺服器的基礎上編寫http協議解析程式碼,設計簡單的http伺服器,處理與瀏覽器的資料交互,包括內建網頁的下載,並使用ajax技術獲取狀態並...
    程式設計 發佈於2024-11-09
  • 為什麼在 Java 的 Random 類別中設定種子會傳回相同的數字?
    為什麼在 Java 的 Random 類別中設定種子會傳回相同的數字?
    Java隨機數產生:為什麼設定種子會回傳相同的數字? 儘管將Random類別的種子設定為特定值,但隨機數產生器始終會傳回相同的數字。讓我們探討一下可能導致此問題的原因。 了解 Random 類別和種子初始化Java Random 類別旨在產生偽隨機數。預設情況下,它使用其內部時鐘作為種子值,使其產生...
    程式設計 發佈於2024-11-09
  • 如何克服使用反射設定結構體欄位值時 SetCan() 總是傳回 False 的問題?
    如何克服使用反射設定結構體欄位值時 SetCan() 總是傳回 False 的問題?
    使用結構體的 SetString 探索反射反射提供了動態操作 Go 結構的強大工具。在此範例中,我們在嘗試使用反射來設定結構體欄位的值時遇到一個常見問題:CanSet() 始終傳回 false。這種障礙阻止了字段修改,使我們陷入困境。 識別陷阱提供的程式碼片段突顯了兩個基本錯誤:傳遞值而非指標: ...
    程式設計 發佈於2024-11-09
  • 為什麼 MySQL 中帶有子查詢的「IN」查詢很慢,如何提升效能?
    為什麼 MySQL 中帶有子查詢的「IN」查詢很慢,如何提升效能?
    MySQL 中帶有子查詢的緩慢「IN」查詢當使用子查詢時,使用「IN」運算子的MySQL查詢可能會表現出顯著的效能下降檢索「IN」子句的值很複雜。在這種情況下,用明確值取代子查詢結果會顯著縮短執行時間。 要了解此行為的原因,需要注意的是,每次評估「IN」查詢時,MySQL 都會執行子查詢。在提供的範...
    程式設計 發佈於2024-11-09
  • 如何使用WinAPI取得螢幕解析度?
    如何使用WinAPI取得螢幕解析度?
    使用 WinAPI 取得螢幕解析度在 WinAPI 中,存在多個函數來決定目前螢幕解析度。適當的選擇取決於具體要求。 檢索顯示尺寸檢索顯示尺寸檢索顯示尺寸 主監視器:使用GetSystemMetrics(SM_CXSCREEN) 和GetSystemMetrics( SM_CYCYSEN) 取得主顯...
    程式設計 發佈於2024-11-09
  • 如何修復透過 Gmail REST API 發送電子郵件時出現的「400 錯誤請求 + 失敗前提條件」錯誤?
    如何修復透過 Gmail REST API 發送電子郵件時出現的「400 錯誤請求 + 失敗前提條件」錯誤?
    Gmail REST API:解決“400 Bad Request Failed Precondition”錯誤嘗試使用Gmail REST API 與伺服器發送電子郵件時-到伺服器授權時,您可能會遇到一條錯誤訊息,指出「400 Bad Request Failed Precondition」。此錯...
    程式設計 發佈於2024-11-09

免責聲明: 提供的所有資源部分來自互聯網,如果有侵犯您的版權或其他權益,請說明詳細緣由並提供版權或權益證明然後發到郵箱:[email protected] 我們會在第一時間內為您處理。

Copyright© 2022 湘ICP备2022001581号-3