Heim > Java > javaLernprogramm > Hauptteil

rs Erstellen eines JNI-Frameworks

Patricia Arquette
Freigeben: 2024-10-02 06:10:30
Original
463 Leute haben es durchsucht

rs Building a JNI Framework

Hey! Technisch gesehen ist dies mein zweiter Beitrag, aber es ist mein erster richtiger. (Ich werde den t3d-Beitrag einfach ignorieren.)

Ich habe dieses Konto auch schon ewig nicht mehr genutzt, aber egal

In diesem Beitrag werde ich erläutern, was rs4j ist, wie man es verwendet und wie ich es erstellt habe.

Was ist das?

rs4j ist eine Rust-Bibliothek, die ich erstellt habe, um die Erstellung von Java-Bibliotheken zu vereinfachen, die nativen, in Rust geschriebenen Code verwenden. Um dies zu erreichen, wird JNI-Code (Java Native Interface) generiert.

Okay, das ist cool, aber warum sollte es mich interessieren?

rs4j ermöglicht es Ihnen, rechenintensive Arbeit auf eine viel schnellere Laufzeit zu verlagern (sieht Sie an, Garbage Collector), anstatt alles in der JVM auszuführen und die Leistung zu beeinträchtigen. Minecraft-Mods wie Create Aeronautics (oder Create Simulated, um genauer zu sein) verwenden diese Technik, um einige ihrer physikalischen Berechnungen durchzuführen, die sonst mit Java sehr verzögert wären.

rs4j ermöglicht Ihnen die einfache Erstellung nativer Schnittstellen wie dieser mit minimalem Code und die einfache Portierung ganzer Bibliotheken für die Verwendung mit Java mit minimalem Code.

Okay, jetzt bin ich interessiert – aber wie kann ich es verwenden?

Die Verwendung ist einfach! Befolgen Sie einfach diese Schritte:

  1. Richten Sie Ihren Bibliothekstyp ein:
# Cargo.toml

[lib]
crate-type = ["cdylib"]
Nach dem Login kopieren
  1. Fügen Sie rs4j zu Ihren Abhängigkeiten hinzu:
cargo add rs4j
Nach dem Login kopieren
  1. Fügen Sie rs4j zu Ihren Build-Abhängigkeiten hinzu:
cargo add rs4j --build -F build # Enable the `build` feature

# Also add anyhow for error handling
cargo add anyhow --build
Nach dem Login kopieren
  1. Richten Sie Ihr Buildskript ein:
// 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(())
}
Nach dem Login kopieren
  1. Richten Sie Ihr Post-Build-Skript ein (optional):

rs4j verwendet ein Post-Build-Skript, um Aktionen nach dem Build auszuführen.
Dies ist technisch optional, wird aber empfohlen.

# 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
Nach dem Login kopieren
// 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(())
}
Nach dem Login kopieren
  1. Installieren Sie die CLI von rs4j (optional)

Dies ist optional, wenn Sie das Post-Build-Skript nicht verwenden möchten.

cargo install rs4j --features cli
Nach dem Login kopieren
  1. Bauen!

Ändern Sie alle Skripte wie folgt:

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

Syntax

Hier ist eine grundlegende Übersicht über die Syntax:

// 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;
};
Nach dem Login kopieren

Wie wurde es hergestellt?

rs4j verwendet einen Peg-Parser, um seine Sprache zu verarbeiten. Dieser Parser wandelt die analysierte Struktur direkt in einen abstrakten Syntaxbaum um, der in Code umgewandelt wird.

rs4j ist stark typisiert. Ich habe eine Type-Struktur und eine TypeKind-Enumeration, um dies zu erreichen.

TypeArts

Diese werden mit diesem Code analysiert:

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

        ...
    }
}
Nach dem Login kopieren

Wie Sie sehen können, gibt es für jeden primitiven Typ eine andere Regel und dann ein Allheilmittel. Dadurch kann ich den richtigen Code einfach überprüfen und ausgeben.

Mehr vom Parser können Sie hier sehen.

Codegen

rs4j verwendet ein benutzerdefiniertes Codegenerierungssystem, das format!() stark nutzt, um den Code zu erstellen. Dies ist zwar nicht die korrekteste oder sicherste Lösung, erzeugt aber in fast allen meinen Tests korrekten Code (das einzige Problem sind Generika, an denen ich arbeite).

Die Codegenerierung erfolgt, wobei jeder AST-Knoten seine eigenen Funktionen hat, um ihn in Java- und Rust-Code umzuwandeln.

Native Implementierung

In Ihre lib.rs müssen Sie!() Ihre bindings.rs-Datei einbinden, die die nativen Implementierungen enthält.

Jede Struktur, für die Sie Bindungen generieren, wird mit JNI umschlossen. Hier ist ein Beispiel, wie das aussieht:

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);
};
Nach dem Login kopieren
// 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()
    }
}
Nach dem Login kopieren

Wenn ein Objekt erstellt wird, ruft es die Wrapped-Methode auf, die absichtlich jedes verschachtelte Objekt verliert, um seinen Zeiger zu erhalten. Dadurch kann ich jederzeit und in jedem Kontext auf das Objekt zugreifen.

Alle Methoden sind verpackt, damit JNI sie viel einfacher aufrufen kann.

JNI-Code

Apropos, der JNI-Code sieht so aus:

// 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()
}
Nach dem Login kopieren

Das ist ziemlich normal für die JNI-Kiste, außer für den Zugriff auf das Objekt. Das &*(ptr as *mut __JNI_MyOtherStruct) könnte unsicher aussehen, und das liegt daran, dass es ist. Dies ist jedoch beabsichtigt, da der Zeiger bei korrekter Ausführung immer gültig sein sollte.

Beachten Sie, dass der Setter am Ende den Zeiger des Objekts zurückgibt. Das ist beabsichtigt. Dadurch kann Java seinen internen Zeiger zurücksetzen und den letzten gültigen Zeiger verfolgen.

Speicher freigeben

Durch das Freigeben von Speicher wird im Wesentlichen der Zeiger zurückgefordert und dann gelöscht. Es gibt auch alle nicht-primitiven Felder frei.

// #[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);
}
Nach dem Login kopieren

Es gibt jedoch einen bekannten Fehler bei dieser Methode, der darin besteht, dass die Methode immer zu einem Speicherverlust führt, wenn ein verschachteltes Objekt mehr als eine Ebene tief ist. Ich habe einige Ideen, wie ich das beheben kann, aber ich habe mich auf andere Dinge konzentriert.

Die Java-Seite

Jede Java-Klasse, die rs4j generiert, erbt von zwei anderen Schnittstellen, ParentClass und NativeClass.

Hier ist die Definition von beidem:

// 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);
}
Nach dem Login kopieren

Jede Klasse besteht aus einigen Teilen, darunter:

  • 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);
Nach dem Login kopieren
  • 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;
Nach dem Login kopieren
  • Constructors
public MyOtherStruct() {
    // Sets the pointer using the constructor
    __ptr = jni_init_new();
}
Nach dem Login kopieren
  • 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);
}
Nach dem Login kopieren
  • 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");
}
Nach dem Login kopieren
  • 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);
}
Nach dem Login kopieren
  • 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);
    }
}
Nach dem Login kopieren

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

Das obige ist der detaillierte Inhalt vonrs Erstellen eines JNI-Frameworks. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Quelle:dev.to
Erklärung dieser Website
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn
Neueste Artikel des Autors
Beliebte Tutorials
Mehr>
Neueste Downloads
Mehr>
Web-Effekte
Quellcode der Website
Website-Materialien
Frontend-Vorlage