Rumah > Java > javaTutorial > teks badan

rs Membina Rangka Kerja JNI

Patricia Arquette
Lepaskan: 2024-10-02 06:10:30
asal
463 orang telah melayarinya

rs Building a JNI Framework

Hei! Ini adalah siaran kedua saya, secara teknikal, tetapi ini adalah siaran sebenar pertama saya. (Hanya akan mengabaikan siaran t3d.)

Saya juga tidak pernah menggunakan akaun ini selama-lamanya tetapi apa pun

Dalam siaran ini, saya akan membincangkan apa itu rs4j, cara menggunakannya dan cara saya membinanya.

Apa itu?

rs4j ialah perpustakaan Rust yang saya cipta untuk memudahkan penciptaan perpustakaan Java yang menggunakan kod asli yang ditulis dalam Rust. Ia menjana kod JNI (Antara Muka Asli Java) untuk mencapainya.

Okay, itu bagus, tetapi mengapa saya perlu peduli?

rs4j membolehkan anda memunggah kerja pengiraan tinggi ke masa jalan yang lebih pantas (melihat anda, pengumpul sampah) dan bukannya menjalankan semuanya dalam JVM dan merosakkan prestasi. Mod Minecraft seperti Cipta Aeronautik (atau Cipta Simulasi, untuk menjadi lebih tepat) menggunakan teknik ini untuk melakukan beberapa pengiraan fizik mereka yang sebaliknya akan menjadi sangat ketinggalan dengan Java.

rs4j membolehkan anda membuat antara muka asli seperti ini dengan mudah dengan kod minimum dan mudah mengalihkan seluruh perpustakaan untuk digunakan dengan Java dengan kod minimum.

Okay, sekarang saya berminat - tetapi bagaimana saya boleh menggunakannya?

Menggunakannya adalah mudah! Cuma ikut langkah ini:

  1. Sediakan jenis pustaka anda:
# Cargo.toml

[lib]
crate-type = ["cdylib"]
Salin selepas log masuk
  1. Tambahkan rs4j pada kebergantungan anda:
cargo add rs4j
Salin selepas log masuk
  1. Tambahkan rs4j pada kebergantungan binaan anda:
cargo add rs4j --build -F build # Enable the `build` feature

# Also add anyhow for error handling
cargo add anyhow --build
Salin selepas log masuk
  1. Sediakan skrip binaan anda:
// 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(())
}
Salin selepas log masuk
  1. Sediakan skrip pasca binaan anda (pilihan):

rs4j menggunakan skrip pasca binaan untuk mencapai tindakan selepas membina.
Ini adalah pilihan teknikal, tetapi disyorkan.

# 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
Salin selepas log masuk
// 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(())
}
Salin selepas log masuk
  1. Pasang CLI rs4j (pilihan)

Ini adalah pilihan jika anda tidak mahu menggunakan skrip pasca binaan.

cargo install rs4j --features cli
Salin selepas log masuk
  1. Bina!

Ubah suai mana-mana skrip seperti itu:

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

Sintaks

Berikut ialah run-down asas sintaks:

// 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;
};
Salin selepas log masuk

Bagaimana ia dibuat?

rs4j menggunakan penghurai pasak untuk memproses bahasanya. Penghurai ini secara langsung menukar struktur yang dihuraikan menjadi pokok sintaks abstrak, yang ditukar kepada kod.

rs4j ditaip kuat. Saya mempunyai struct Jenis dan enum TypeKind untuk mencapai ini.

JenisJenis

Ini dihuraikan menggunakan kod ini:

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

        ...
    }
}
Salin selepas log masuk

Seperti yang anda lihat, terdapat peraturan yang berbeza untuk setiap jenis primitif, dan kemudiannya. Ini membolehkan saya mengesahkan dan mengeluarkan kod yang betul dengan mudah.

Anda boleh melihat lebih banyak penghurai di sini.

Codegen

rs4j menggunakan sistem codegen tersuai yang banyak menggunakan format!() untuk mencipta kod. Walaupun ini bukan yang paling betul atau selamat, ia mencipta kod yang betul dalam hampir semua ujian saya (satu-satunya isu adalah generik, yang sedang saya usahakan).

Kodgen dilakukan dengan setiap nod AST mempunyai fungsi tersendiri untuk mengubahnya menjadi kod Java dan Rust.

Pelaksanaan Asli

Dalam lib.rs anda, anda perlu memasukkan!() fail bindings.rs anda, yang mengandungi pelaksanaan asli.

Setiap struct yang anda hasilkan pengikatan akan dibalut dengan JNI. Berikut ialah contoh rupanya:

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);
};
Salin selepas log masuk
// 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()
    }
}
Salin selepas log masuk

Apabila objek dibina, ia memanggil kaedah berbalut yang dengan sengaja membocorkan setiap objek bersarang untuk mendapatkan penunjuknya. Ini membolehkan saya mengakses objek pada bila-bila masa saya perlu, dalam sebarang konteks.

Semua kaedah dibalut untuk membolehkan JNI menghubungi mereka dengan lebih mudah.

Kod JNI

Bercakap mengenainya, kod JNI kelihatan seperti ini:

// 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()
}
Salin selepas log masuk

Ini adalah bahan yang agak standard untuk peti jni, kecuali untuk mengakses objek. Itu &*(ptr sebagai *mut __JNI_MyOtherStruct) mungkin kelihatan tidak selamat, dan itu kerana ia ialah. Ini adalah disengajakan, bagaimanapun, kerana penunjuk sepatutnya sentiasa sah jika dilakukan dengan betul.

Perhatikan bahawa pada penghujung penetap, ia mengembalikan penunjuk objek. Ini bertujuan. Ini membolehkan Java menetapkan semula penunjuk dalamannya, menjejaki penunjuk yang sah terbaharu.

Membebaskan Memori

Membebaskan ingatan pada asasnya menuntut semula penuding dan kemudian menjatuhkannya. Ia membebaskan semua medan bukan primitif juga.

// #[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);
}
Salin selepas log masuk

Terdapat pepijat yang diketahui dengan kaedah ini, bagaimanapun, iaitu kaedah itu akan sentiasa membocorkan ingatan jika terdapat objek bersarang lebih daripada satu tahap dalam. Saya mempunyai beberapa idea tentang cara membetulkan perkara ini, tetapi saya telah memberi tumpuan kepada perkara lain.

Bahagian Jawa

Setiap kelas Java yang rs4j hasilkan akan mewarisi daripada dua antara muka lain, ParentClass dan NativeClass.

Berikut ialah definisi kedua-duanya:

// 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);
}
Salin selepas log masuk

Setiap kelas terdiri daripada beberapa bahagian, termasuk:

  • 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);
Salin selepas log masuk
  • 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;
Salin selepas log masuk
  • Constructors
public MyOtherStruct() {
    // Sets the pointer using the constructor
    __ptr = jni_init_new();
}
Salin selepas log masuk
  • 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);
}
Salin selepas log masuk
  • 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");
}
Salin selepas log masuk
  • 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);
}
Salin selepas log masuk
  • 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);
    }
}
Salin selepas log masuk

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

Atas ialah kandungan terperinci rs Membina Rangka Kerja JNI. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!

sumber:dev.to
Kenyataan Laman Web ini
Kandungan artikel ini disumbangkan secara sukarela oleh netizen, dan hak cipta adalah milik pengarang asal. Laman web ini tidak memikul tanggungjawab undang-undang yang sepadan. Jika anda menemui sebarang kandungan yang disyaki plagiarisme atau pelanggaran, sila hubungi admin@php.cn
Artikel terbaru oleh pengarang
Tutorial Popular
Lagi>
Muat turun terkini
Lagi>
kesan web
Kod sumber laman web
Bahan laman web
Templat hujung hadapan