rs 构建 JNI 框架
やあ!厳密に言えば、これは私の 2 回目の投稿ですが、本当の投稿は初めてです。 (t3d の投稿は無視します。)
私もこのアカウントをずっと使っていませんが、何はともあれ
この投稿では、rs4j とは何か、その使用方法、およびそれをどのように構築したかについて説明します。
それは何ですか?
rs4j は、Rust で書かれたネイティブ コードを使用する Java ライブラリの作成を容易にするために私が作成した Rust ライブラリです。これを実現するために JNI (Java Native Interface) コードを生成します。
それは素晴らしいことですが、なぜ私が気にする必要があるのでしょうか?
rs4j を使用すると、大量の計算作業をすべて JVM で実行してパフォーマンスを低下させるのではなく、はるかに高速なランタイム (ほら、ガベージ コレクター) にオフロードできます。 Create Aeronautics (正確には Create Simulated) のような Minecraft MOD は、この技術を使用して、Java では非常に遅れてしまう物理計算の一部を実行します。
rs4j を使用すると、最小限のコードでこのようなネイティブ インターフェイスを簡単に作成でき、最小限のコードで Java で使用できるようにライブラリ全体を簡単に移植できます。
さて、興味はありますが、どうやって使用すればよいでしょうか?
使い方は簡単!次の手順に従ってください:
- ライブラリのタイプを設定します。
# Cargo.toml [lib] crate-type = ["cdylib"]
- rs4j を依存関係に追加します。
cargo add rs4j
- 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 は、ビルド後のアクションを実行するためにビルド後のスクリプトを使用します。
これは技術的にはオプションですが、推奨されます。
# 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(()) }
- rs4j の CLI をインストールします (オプション)
ビルド後のスクリプトを使用したくない場合、これはオプションです。
cargo install rs4j --features cli
- 構築!
スクリプトを次のように変更します:
- 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 はペグパーサーを使用して言語を処理します。このパーサーは、解析された構造を抽象構文ツリーに直接変換し、それがコードに変換されます。
rs4j は厳密に型指定されています。これを実現するために Type struct と TypeKind enum を用意しました。
種類の種類
これらは次のコードを使用して解析されます:
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!() を多用するカスタム codegen システムを使用します。これは最も正確でも安全でもありませんが、ほとんどすべてのテストで正しいコードが作成されます (唯一の問題は、私が取り組んでいるジェネリックです)。
コード生成は、Java および Rust コードに変換する独自の関数を持つ各 AST ノードで実行されます。
ネイティブ実装
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) は安全ではないように見えるかもしれませんが、それはであるためです。ただし、正しく実行された場合、ポインタは常に有効である必要があるため、これは意図的なものです。
セッターの最後で、オブジェクトのポインターが返されることに注意してください。これは意図したものです。これにより、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); }
ただし、このメソッドには既知のバグがあり、複数レベルの深さでネストされたオブジェクトがある場合、このメソッドでは常にメモリ リークが発生するということです。これを修正する方法についてはいくつかアイデアがありますが、他のことに集中しています。
Java側
rs4j が生成するすべての Java クラスは、他の 2 つのインターフェイス、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中文网其他相关文章!

热AI工具

Undresser.AI Undress
人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover
用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool
免费脱衣服图片

Clothoff.io
AI脱衣机

Video Face Swap
使用我们完全免费的人工智能换脸工具轻松在任何视频中换脸!

热门文章

热工具

记事本++7.3.1
好用且免费的代码编辑器

SublimeText3汉化版
中文版,非常好用

禅工作室 13.0.1
功能强大的PHP集成开发环境

Dreamweaver CS6
视觉化网页开发工具

SublimeText3 Mac版
神级代码编辑软件(SublimeText3)

公司安全软件导致部分应用无法正常运行的排查与解决方法许多公司为了保障内部网络安全,会部署安全软件。...

将姓名转换为数字以实现排序的解决方案在许多应用场景中,用户可能需要在群组中进行排序,尤其是在一个用...

系统对接中的字段映射处理在进行系统对接时,常常会遇到一个棘手的问题:如何将A系统的接口字段有效地映�...

在使用MyBatis-Plus或其他ORM框架进行数据库操作时,经常需要根据实体类的属性名构造查询条件。如果每次都手动...

在使用IntelliJIDEAUltimate版本启动Spring...

Java对象与数组的转换:深入探讨强制类型转换的风险与正确方法很多Java初学者会遇到将一个对象转换成数组的�...

电商平台SKU和SPU表设计详解本文将探讨电商平台中SKU和SPU的数据库设计问题,特别是如何处理用户自定义销售属...

Redis缓存方案如何实现产品排行榜列表的需求?在开发过程中,我们常常需要处理排行榜的需求,例如展示一个�...
