近期工作中有Rust和Java互相調用需求,這篇文章主要介紹如何用Rust通過JNI和Java進行交互,還有記錄一下開發過程中遇到的一些坑。
JNI簡單來說是一套Java與其他語言互相調用的標準,主要是C語言,官方也提供了基于C的C++接口。 既然是C語言接口,那么理論上支持C ABI的語言都可以和Java語言互相調用,Rust就是其中之一。
關于JNI的歷史背景以及更詳細的介紹可以參考官方文檔
在Rust中和Java互相調用,可以使用原始的JNI接口,也就是自己聲明JNI的C函數原型,在Rust里按照C的方式去調用,但這樣寫起來會很繁瑣,而且都是unsafe的操作; 不過Rust社區里已經有人基于原始的JNI接口,封裝好了一套safe的接口,crate的名字就叫jni,用這個庫來開發就方便多了
文中涉及的代碼放在了這個github倉庫https://github.com/metaworm/rust-java-demo
Rust JNI 工程配置
如果你熟悉Cargo和Maven,可以跳過這一節,直接看我提供的github源碼即可
Rust工程配置
首先,通過cargo new java-rust-demo
創建一個rust工程
然后切換到工程目錄cd java-rust-demo
,并編輯Cargo.toml
:修改類型為動態庫、加上對 jni crate 的依賴
[package] name = "rust-java-demo" version = "0.1.0" edition = "2021" [lib] crate-type = ['cdylib'] [dependencies] jni = {version = '0.19'}
重命名src目錄下的main.rs
為lib.rs
,Rust庫類型的工程編譯入口為 lib.rs,然后添加以下代碼
use jni::*; use jni::JNIEnv; #[no_mangle] pub unsafe extern "C" fn Java_pers_metaworm_RustJNI_init(env: JNIEnv, _class: JClass) { println!("rust-java-demo inited"); }
然后執行cargo build
構建,生成的動態庫默認會位于target/debug
目錄下,我這里用的linux系統,動態庫文件名為librust_java_demo.so
,如果是Windows系統,文件名為rust_java_demo.dll
這樣,我們第一個JNI函數就創建成功了! 通過Java_pers_metaworm_RustJNI_init
這個導出函數,給了Java的pers.metaworm.RustJNI
這個類提供了一個native的靜態方法init
; 這里只是簡單地打印了一句話,后面會通過這個初始化函數添加更多的功能
Java工程配置
還是在這個工程目錄里,把Java部分的代碼放在java
這個目錄下,在其中創建pers/metaworm/RustJNI.java
文件
package pers.metaworm; public class RustJNI { static { System.loadLibrary("rust_java_demo"); } public static void main(String[] args) { init(); } static native void init(); }
我們使用流行的 maven 工具來構建Java工程,在項目根目錄下創建 maven 的工程文件pom.xml
xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0modelVersion> <groupId>pers.metawormgroupId> <artifactId>RustJNIartifactId> <version>1.0-SNAPSHOTversion> <properties> <exec.mainClass>pers.metaworm.RustJNIexec.mainClass> <maven.compiler.source>1.8maven.compiler.source> <maven.compiler.target>1.8maven.compiler.target> <maven.compiler.encoding>UTF-8maven.compiler.encoding> properties> <dependencies> dependencies> <build> <sourceDirectory>javasourceDirectory> <plugins> <plugin> <groupId>org.apache.maven.pluginsgroupId> <artifactId>maven-compiler-pluginartifactId> <version>2.4version> <configuration> <encoding>UTF-8encoding> configuration> plugin> plugins> build> project>
運行 DMEO 工程
上面的工程配置弄好之后,就可以使用cargo build
命令構建Rust提供的JNI動態庫,mvn compile
命令來編譯Java代碼
Rust和Java代碼都編譯好之后,執行java -Djava.library.path=target/debug -classpath target/classes pers.metaworm.RustJNI
來運行
其中-Djava.library.path=target/debug
指定了我們JNI動態庫所在的路徑,-classpath target/classes
指定了Java代碼的編譯輸出的類路徑,pers.metaworm.RustJNI
是Java main方法所在的類
不出意外的話,運行之后會在控制臺輸出init函數里打印的"rust-java-demo inited"
Java調用Rust
接口聲明
前面的Java_pers_metaworm_RustJNI_init
函數已經展示了如何給Java暴露一個native方法,即導出名稱為Java_<類完整路徑>_<方法名>
的函數,然后在Java對應的類里聲明對應的native方法
拓展:除了通過導出函數給Java提供native方法,還可以通過 RegisterNatives 函數動態注冊native方法,對應的jni封裝的函數為JNIEnv::register_native_methods,一般動態注冊會在JNI_Onload
這個導出函數里執行,jvm加載jni動態庫時會執行這個函數(如果有的話)
當在Java里首次調用native方法時,JVM就會尋找對應名稱的導出的或者動態注冊的native函數,并將Java的native方法和Rust的函數關聯起來;如果JVM沒找到對應的native函數,則會報java.lang.UnsatisfiedLinkError
異常
為了演示,我們再添加一些代碼來覆蓋更多的交互場景
lib.rs
use jni::*; use jni::{jint, jobject, jstring}; use jni::JNIEnv; #[no_mangle] pub unsafe extern "C" fn Java_pers_metaworm_RustJNI_addInt( env: JNIEnv, _class: JClass, a: jint, b: jint, ) -> jint { a + b } #[no_mangle] pub unsafe extern "C" fn Java_pers_metaworm_RustJNI_getThisField( env: JNIEnv, this: JObject, name: JString, sig: JString, ) -> jobject { let result = env .get_field( this, &env.get_string(name).unwrap().to_string_lossy(), &env.get_string(sig).unwrap().to_string_lossy(), ) .unwrap(); result.l().unwrap().into_inner() }
RustJNI.java
package pers.metaworm; public class RustJNI { static { System.loadLibrary("rust_java_demo"); } public static void main(String[] args) { init(); System.out.println("test addInt: " + (addInt(1, 2) == 3)); RustJNI jni = new RustJNI(); System.out.println("test getThisField: " + (jni.getThisField("stringField", "Ljava/lang/String;") == jni.stringField)); System.out.println("test success"); } String stringField = "abc"; static native void init(); static native int addInt(int a, int b); native Object getThisField(String name, String sig); }
其中,addInt方法接收兩個int參數,并返回相加的結果;getThisField是一個實例native方法,它獲取this對象指定的字段并返回
參數傳遞
從上一節的例子里可以看到,jni函數的第一個參數總是JNIEnv
,很多交互操作都需要通過這個對象來進行; 第二個參數是類對象(靜態native方法)或this對象(實例native方法); 從第三個參數開始,每一個參數對應Java的native方法所聲明的參數
對于基礎的參數類型,可以直接用use jni::*
提供的j開頭的系列類型來聲明,類型對照表:
Java 類型 | Native 類型 | 類型描述 |
---|---|---|
boolean | jboolean | unsigned 8 bits |
byte | jbyte | signed 8 bits |
char | jchar | unsigned 16 bits |
short | jshort | signed 16 bits |
int | jint | signed 32 bits |
long | jlong | signed 64 bits |
float | jfloat | 32 bits |
double | jdouble | 64 bits |
void | void | not applicable |
對于引用類型(復合類型/對象類型),可以統一用jni::JObject
聲明;JObject是對jobject的rust封裝,帶有生命周期參數;對于String類型,也可以用 JString 來聲明,JString是對JObject的一層簡單封裝
拋異常
前面的Java_pers_metaworm_RustJNI_getThisField
函數里,用了很多unwrap,這在生產環境中是非常危險的,萬一傳了一個不存在的字段名,就直接crash了;所以我們改進一下這個函數,讓他支持拋異常,出錯的時候能讓Java捕獲到
#[no_mangle] pub unsafe extern "C" fn Java_pers_metaworm_RustJNI_getThisFieldSafely( env: JNIEnv, this: JObject, name: JString, sig: JString, ) -> jobject { let result = (|| { env.get_field( this, &env.get_string(name)?.to_string_lossy(), &env.get_string(sig)?.to_string_lossy(), )? .l() })(); match result { Ok(res) => res.into_inner(), Err(err) => { env.exception_clear().expect("clear"); env.throw_new("Ljava/lang/Exception;", format!("{err:?}")) .expect("throw"); std::null_mut() } } }
Java層的測試代碼為
try { System.out.println("test getThisFieldSafely: " + (jni.getThisFieldSafely("stringField", "Ljava/lang/String;") == jni.stringField)); jni.getThisFieldSafely("fieldNotExists", "Ljava/lang/String;"); } catch (Exception e) { System.out.println("test getThisFieldSafely: catched exception: " + e.toString()); }
通過env.throw_new("Ljava/lang/Exception;", format!("{err:?}"))
拋出了一個異常,從JNI函數返回后,Java就會捕獲到這個異常; 代碼里可以看到在拋異常之前,調用了env.exception_clear()
來清除異常,這是因為前面的get_field已經拋出一個異常了,當env里已經有一個異常的時候,后續再調用env的函數都會失敗,這個異常也會繼續傳遞到上層的Java調用者,所以其實這里沒有這兩句,直接返回null的話,Java也可以捕獲到異常;但我們通過throw_new可以自定義異常類型及異常消息
這其實不是一個典型的場景,典型的場景應該是Rust里的某個調用返回了Error,然后通過拋異常的形式傳遞到Java層,比如除0錯誤
#[no_mangle] pub unsafe extern "C" fn Java_pers_metaworm_RustJNI_divInt( env: JNIEnv, _class: JClass, a: jint, b: jint, ) -> jint { if b == 0 { env.throw_new("Ljava/lang/Exception;", "divide zero") .expect("throw"); 0 } else { a / b } }
Rust調用Java
創建對象、調用方法、訪問字段...
下面用一段代碼展示如何在Rust中創建Java對象、調用方法、獲取字段、處理異常等常見用法
#[allow(non_snake_case)] fn call_java(env: &JNIEnv) { match (|| { let File = env.find_class("java/io/File")?; // 獲取靜態字段 let separator = env.get_static_field(File, "separator", "Ljava/lang/String;")?; let separator = env .get_string(separator.l()?.into())? .to_string_lossy() .to_string(); println!("File.separator: {}", separator); assert_eq!(separator, format!("{}", std::MAIN_SEPARATOR)); // env.get_static_field_unchecked(class, field, ty) // 創建實例對象 let file = env.new_object( "java/io/File", "(Ljava/lang/String;)V", &[JValue::Object(env.new_string("")?.into())], )?; // 調用實例方法 let abs = env.call_method(file, "getAbsolutePath", "()Ljava/lang/String;", &[])?; let abs_path = env .get_string(abs.l()?.into())? .to_string_lossy() .to_string(); println!("abs_path: {}", abs_path); jni::Result::Ok(()) })() { Ok(_) => {} // 捕獲異常 Err(jni::JavaException) => { let except = env.exception_occurred().expect("exception_occurred"); let err = env .call_method(except, "toString", "()Ljava/lang/String;", &[]) .and_then(|e| Ok(env.get_string(e.l()?.into())?.to_string_lossy().to_string())) .unwrap_or_default(); env.exception_clear().expect("clear exception"); println!("call java exception occurred: {err}"); } Err(err) => { println!("call java error: {err:?}"); } } } #[no_mangle] pub unsafe extern "C" fn Java_pers_metaworm_RustJNI_callJava(env: JNIEnv) { println!("call java"); call_java(&env) }
總結一下常用的函數,具體用法可以參考JNIEnv的文檔
-
創建對象
new_object
-
創建字符串對象
new_string
-
調用方法
call_method
call_static_method
-
獲取字段
get_field
get_static_field
-
修改字段
set_field
set_static_field
要注意的是調用方法、創建對象等需要傳一個方法類型簽名,這是因為Java支持方法重載,同一個類里一個名稱的函數可能有多個,所以需要通過類型簽名來區分,類型簽名的規則可以參考官方文檔
異常處理
call_java
函數展示了如何在Rust中處理Java的異常: 通過JNIEnv對象動態獲取字段或者調用方法,都會返回一個jni::Result
類型,對應的Error類型為jni::Error
;如果Error是jni::JavaException
則表明在JVM執行過程中,某個地方拋出了異常,這種情況下就可以用exception_occurred
函數來獲取異常對象進行處理,然后調用exception_clear
來清除異常,如果再返回到Java便可以繼續執行
在非Java線程中調用Java
從Java中調用的Rust代碼,本身就處于一個Java線程中,第一個參數為JNIEnv對象,Rust代碼用這個對象和Java進行交互; 實際應用場景中,可能需要從一個非Java線程或者說我們自己的線程中去調用Java的方法,但我們的線程沒有JNIEnv對象,這時就需要調用JavaVM::attach_current_thread
函數將當前線程附加到JVM上,來獲得一個JNIEnv
#[no_mangle] pub unsafe extern "C" fn Java_pers_metaworm_RustJNI_callJavaThread(env: JNIEnv) { let vm = env.get_java_vm().expect("get jvm"); std::spawn(move || { println!("call java in another thread"); let env = vm.attach_current_thread().expect("attach"); call_java(&env); }); }
attach_current_thread
函數返回一個AttachGuard
對象,可以解引用為JNIEnv,并且在作用域結束drop的時候自動調用detach_current_thread
函數;原始的AttachCurrentThread
JNI函數,如果當前線程已經attach了,則會拋異常,jni crate里的JavaVM::attach_current_thread
做了一層封裝,如果當前已經attach了,則會返回之前attach的對象,保證不會重復attach
JavaVM對象通過JNIEnv::get_java_vm
函數獲取,可以在初始化的時候將這個變量存起來,給后續的其他線程使用
局部引用、全局引用與對象緩存
關于局部引用與全局引用的官方文檔
Rust提供的native函數,傳過來的對象引用都是局部引用,局部引用只在本次調用JNI調用范圍內有效,而且不能跨線程使用;如果跨線程,必須使用全局引用
可以通過JNIEnv::new_global_ref
來獲取JClass、JObject的全局引用,這個函數返回一個GlobalRef對象,可以通過GlobalRef::as_object
轉成JObject或者JClass等對象;GlobalRef對象drop的時候,會調用DeleteGlobalRef將JVM內部的引用刪除
前面的代碼,從Rust調用Java方法都是通過名稱加方法簽名調用的,這種方式,寫起來很舒服,但運行效率肯定是非常低的,因為每次都要通過名稱去查找對應的方法
其實JNI原始的C接口,是通過jobjectID、jclassID、jmethodID、jfieldID來和Java交互的,只不過是jni crate給封裝了一層比較友好的接口
如果我們對性能要求比較高,則可以在初始化的時候獲取一些JClass、JObject的全局引用,緩存起來,后面再轉成JClass、JObject來使用,千萬不要對jmethodID、jfieldID獲取全局引用,因為這倆都是通過jclassID生成的,其聲明周期和jclassID對應的對象相同,不是需要GC的對象,如果對jmethodID獲取全局引用然后調用,會導致某些JVM Crash;對于jmethodID、jfieldID,則可以基于JClass、JObject的全局引用獲取,后面直接使用即可
獲取到這些全局的ID之后,就可以通過JNIEnv::call_method_unchecked
系列函數,來更高效地調用Java
我用Rust強大的宏,實現了這個過程,可以讓我們直接在Rust中以聲明的方式緩存的所需類及其方法ID
#[allow(non_snake_case)] pub mod cache { use anyhow::Context; use jni::Result as JniResult; use jni::*; use jni::JNIEnv; pub fn method_global_ref<'a>( env: JNIEnv<'a>, class: JClass, name: &str, sig: &str, ) -> JniResult
審核編輯:湯梓紅
-
接口
+關注
關注
33文章
8691瀏覽量
151911 -
JAVA
+關注
關注
19文章
2974瀏覽量
105137 -
C語言
+關注
關注
180文章
7614瀏覽量
137704 -
C++
+關注
關注
22文章
2114瀏覽量
73854 -
Rust
+關注
關注
1文章
230瀏覽量
6664
原文標題:【Rust筆記】Rust與Java交互-JNI模塊編寫-實踐總結
文章出處:【微信號:Rust語言中文社區,微信公眾號:Rust語言中文社區】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論