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.
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.
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.
Die Verwendung ist einfach! Befolgen Sie einfach diese Schritte:
# Cargo.toml [lib] crate-type = ["cdylib"]
cargo add rs4j
cargo add rs4j --build -F build # Enable the `build` feature # Also add anyhow for error handling cargo add anyhow --build
// 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(()) }
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
// 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(()) }
Dies ist optional, wenn Sie das Post-Build-Skript nicht verwenden möchten.
cargo install rs4j --features cli
Ändern Sie alle Skripte wie folgt:
- cargo build + rs4j build # `rs4j build` supports all of `cargo build`'s arguments after a `--`.
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; };
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.
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() ... } }
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.
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.
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); };
// 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() } }
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.
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() }
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.
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); }
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.
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); }
Jede Klasse besteht aus einigen Teilen, darunter:
// 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);
// 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;
public MyOtherStruct() { // Sets the pointer using the constructor __ptr = jni_init_new(); }
// 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); }
// 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"); }
// 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); }
// 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); } }
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!
Thanks to @RyanHCode for giving me a few tips on this!
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!