近期工作中有Rust和Java互相調(diào)用需求,這篇文章主要介紹如何用Rust通過JNI和Java進(jìn)行交互,還有記錄一下開發(fā)過程中遇到的一些坑。
JNI簡(jiǎn)單來說是一套Java與其他語言互相調(diào)用的標(biāo)準(zhǔn),主要是C語言,官方也提供了基于C的C++接口。 既然是C語言接口,那么理論上支持C ABI的語言都可以和Java語言互相調(diào)用,Rust就是其中之一。
關(guān)于JNI的歷史背景以及更詳細(xì)的介紹可以參考官方文檔
在Rust中和Java互相調(diào)用,可以使用原始的JNI接口,也就是自己聲明JNI的C函數(shù)原型,在Rust里按照C的方式去調(diào)用,但這樣寫起來會(huì)很繁瑣,而且都是unsafe的操作; 不過Rust社區(qū)里已經(jīng)有人基于原始的JNI接口,封裝好了一套safe的接口,crate的名字就叫jni,用這個(gè)庫來開發(fā)就方便多了
文中涉及的代碼放在了這個(gè)github倉庫https://github.com/metaworm/rust-java-demo
Rust JNI 工程配置
如果你熟悉Cargo和Maven,可以跳過這一節(jié),直接看我提供的github源碼即可
Rust工程配置
首先,通過cargo new java-rust-demo
創(chuàng)建一個(gè)rust工程
然后切換到工程目錄cd java-rust-demo
,并編輯Cargo.toml
:修改類型為動(dòng)態(tài)庫、加上對(duì) 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"); }
然后執(zhí)行cargo build
構(gòu)建,生成的動(dòng)態(tài)庫默認(rèn)會(huì)位于target/debug
目錄下,我這里用的linux系統(tǒng),動(dòng)態(tài)庫文件名為librust_java_demo.so
,如果是Windows系統(tǒng),文件名為rust_java_demo.dll
這樣,我們第一個(gè)JNI函數(shù)就創(chuàng)建成功了! 通過Java_pers_metaworm_RustJNI_init
這個(gè)導(dǎo)出函數(shù),給了Java的pers.metaworm.RustJNI
這個(gè)類提供了一個(gè)native的靜態(tài)方法init
; 這里只是簡(jiǎn)單地打印了一句話,后面會(huì)通過這個(gè)初始化函數(shù)添加更多的功能
Java工程配置
還是在這個(gè)工程目錄里,把Java部分的代碼放在java
這個(gè)目錄下,在其中創(chuàng)建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 工具來構(gòu)建Java工程,在項(xiàng)目根目錄下創(chuàng)建 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>
運(yùn)行 DMEO 工程
上面的工程配置弄好之后,就可以使用cargo build
命令構(gòu)建Rust提供的JNI動(dòng)態(tài)庫,mvn compile
命令來編譯Java代碼
Rust和Java代碼都編譯好之后,執(zhí)行java -Djava.library.path=target/debug -classpath target/classes pers.metaworm.RustJNI
來運(yùn)行
其中-Djava.library.path=target/debug
指定了我們JNI動(dòng)態(tài)庫所在的路徑,-classpath target/classes
指定了Java代碼的編譯輸出的類路徑,pers.metaworm.RustJNI
是Java main方法所在的類
不出意外的話,運(yùn)行之后會(huì)在控制臺(tái)輸出init函數(shù)里打印的"rust-java-demo inited"
Java調(diào)用Rust
接口聲明
前面的Java_pers_metaworm_RustJNI_init
函數(shù)已經(jīng)展示了如何給Java暴露一個(gè)native方法,即導(dǎo)出名稱為Java_<類完整路徑>_<方法名>
的函數(shù),然后在Java對(duì)應(yīng)的類里聲明對(duì)應(yīng)的native方法
拓展:除了通過導(dǎo)出函數(shù)給Java提供native方法,還可以通過 RegisterNatives 函數(shù)動(dòng)態(tài)注冊(cè)native方法,對(duì)應(yīng)的jni封裝的函數(shù)為JNIEnv::register_native_methods,一般動(dòng)態(tài)注冊(cè)會(huì)在JNI_Onload
這個(gè)導(dǎo)出函數(shù)里執(zhí)行,jvm加載jni動(dòng)態(tài)庫時(shí)會(huì)執(zhí)行這個(gè)函數(shù)(如果有的話)
當(dāng)在Java里首次調(diào)用native方法時(shí),JVM就會(huì)尋找對(duì)應(yīng)名稱的導(dǎo)出的或者動(dòng)態(tài)注冊(cè)的native函數(shù),并將Java的native方法和Rust的函數(shù)關(guān)聯(lián)起來;如果JVM沒找到對(duì)應(yīng)的native函數(shù),則會(huì)報(bào)java.lang.UnsatisfiedLinkError
異常
為了演示,我們?cè)偬砑右恍┐a來覆蓋更多的交互場(chǎng)景
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方法接收兩個(gè)int參數(shù),并返回相加的結(jié)果;getThisField是一個(gè)實(shí)例native方法,它獲取this對(duì)象指定的字段并返回
參數(shù)傳遞
從上一節(jié)的例子里可以看到,jni函數(shù)的第一個(gè)參數(shù)總是JNIEnv
,很多交互操作都需要通過這個(gè)對(duì)象來進(jìn)行; 第二個(gè)參數(shù)是類對(duì)象(靜態(tài)native方法)或this對(duì)象(實(shí)例native方法); 從第三個(gè)參數(shù)開始,每一個(gè)參數(shù)對(duì)應(yīng)Java的native方法所聲明的參數(shù)
對(duì)于基礎(chǔ)的參數(shù)類型,可以直接用use jni::*
提供的j開頭的系列類型來聲明,類型對(duì)照表:
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 |
對(duì)于引用類型(復(fù)合類型/對(duì)象類型),可以統(tǒng)一用jni::JObject
聲明;JObject是對(duì)jobject的rust封裝,帶有生命周期參數(shù);對(duì)于String類型,也可以用 JString 來聲明,JString是對(duì)JObject的一層簡(jiǎn)單封裝
拋異常
前面的Java_pers_metaworm_RustJNI_getThisField
函數(shù)里,用了很多unwrap,這在生產(chǎn)環(huán)境中是非常危險(xiǎn)的,萬一傳了一個(gè)不存在的字段名,就直接crash了;所以我們改進(jìn)一下這個(gè)函數(shù),讓他支持拋異常,出錯(cuò)的時(shí)候能讓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層的測(cè)試代碼為
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:?}"))
拋出了一個(gè)異常,從JNI函數(shù)返回后,Java就會(huì)捕獲到這個(gè)異常; 代碼里可以看到在拋異常之前,調(diào)用了env.exception_clear()
來清除異常,這是因?yàn)榍懊娴膅et_field已經(jīng)拋出一個(gè)異常了,當(dāng)env里已經(jīng)有一個(gè)異常的時(shí)候,后續(xù)再調(diào)用env的函數(shù)都會(huì)失敗,這個(gè)異常也會(huì)繼續(xù)傳遞到上層的Java調(diào)用者,所以其實(shí)這里沒有這兩句,直接返回null的話,Java也可以捕獲到異常;但我們通過throw_new可以自定義異常類型及異常消息
這其實(shí)不是一個(gè)典型的場(chǎng)景,典型的場(chǎng)景應(yīng)該是Rust里的某個(gè)調(diào)用返回了Error,然后通過拋異常的形式傳遞到Java層,比如除0錯(cuò)誤
#[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調(diào)用Java
創(chuàng)建對(duì)象、調(diào)用方法、訪問字段...
下面用一段代碼展示如何在Rust中創(chuàng)建Java對(duì)象、調(diào)用方法、獲取字段、處理異常等常見用法
#[allow(non_snake_case)] fn call_java(env: &JNIEnv) { match (|| { let File = env.find_class("java/io/File")?; // 獲取靜態(tài)字段 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) // 創(chuàng)建實(shí)例對(duì)象 let file = env.new_object( "java/io/File", "(Ljava/lang/String;)V", &[JValue::Object(env.new_string("")?.into())], )?; // 調(diào)用實(shí)例方法 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) }
總結(jié)一下常用的函數(shù),具體用法可以參考JNIEnv的文檔
-
創(chuàng)建對(duì)象
new_object
-
創(chuàng)建字符串對(duì)象
new_string
-
調(diào)用方法
call_method
call_static_method
-
獲取字段
get_field
get_static_field
-
修改字段
set_field
set_static_field
要注意的是調(diào)用方法、創(chuàng)建對(duì)象等需要傳一個(gè)方法類型簽名,這是因?yàn)镴ava支持方法重載,同一個(gè)類里一個(gè)名稱的函數(shù)可能有多個(gè),所以需要通過類型簽名來區(qū)分,類型簽名的規(guī)則可以參考官方文檔
異常處理
call_java
函數(shù)展示了如何在Rust中處理Java的異常: 通過JNIEnv對(duì)象動(dòng)態(tài)獲取字段或者調(diào)用方法,都會(huì)返回一個(gè)jni::Result
類型,對(duì)應(yīng)的Error類型為jni::Error
;如果Error是jni::JavaException
則表明在JVM執(zhí)行過程中,某個(gè)地方拋出了異常,這種情況下就可以用exception_occurred
函數(shù)來獲取異常對(duì)象進(jìn)行處理,然后調(diào)用exception_clear
來清除異常,如果再返回到Java便可以繼續(xù)執(zhí)行
在非Java線程中調(diào)用Java
從Java中調(diào)用的Rust代碼,本身就處于一個(gè)Java線程中,第一個(gè)參數(shù)為JNIEnv對(duì)象,Rust代碼用這個(gè)對(duì)象和Java進(jìn)行交互; 實(shí)際應(yīng)用場(chǎng)景中,可能需要從一個(gè)非Java線程或者說我們自己的線程中去調(diào)用Java的方法,但我們的線程沒有JNIEnv對(duì)象,這時(shí)就需要調(diào)用JavaVM::attach_current_thread
函數(shù)將當(dāng)前線程附加到JVM上,來獲得一個(gè)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
函數(shù)返回一個(gè)AttachGuard
對(duì)象,可以解引用為JNIEnv,并且在作用域結(jié)束drop的時(shí)候自動(dòng)調(diào)用detach_current_thread
函數(shù);原始的AttachCurrentThread
JNI函數(shù),如果當(dāng)前線程已經(jīng)attach了,則會(huì)拋異常,jni crate里的JavaVM::attach_current_thread
做了一層封裝,如果當(dāng)前已經(jīng)attach了,則會(huì)返回之前attach的對(duì)象,保證不會(huì)重復(fù)attach
JavaVM對(duì)象通過JNIEnv::get_java_vm
函數(shù)獲取,可以在初始化的時(shí)候?qū)⑦@個(gè)變量存起來,給后續(xù)的其他線程使用
局部引用、全局引用與對(duì)象緩存
關(guān)于局部引用與全局引用的官方文檔
Rust提供的native函數(shù),傳過來的對(duì)象引用都是局部引用,局部引用只在本次調(diào)用JNI調(diào)用范圍內(nèi)有效,而且不能跨線程使用;如果跨線程,必須使用全局引用
可以通過JNIEnv::new_global_ref
來獲取JClass、JObject的全局引用,這個(gè)函數(shù)返回一個(gè)GlobalRef對(duì)象,可以通過GlobalRef::as_object
轉(zhuǎn)成JObject或者JClass等對(duì)象;GlobalRef對(duì)象drop的時(shí)候,會(huì)調(diào)用DeleteGlobalRef將JVM內(nèi)部的引用刪除
前面的代碼,從Rust調(diào)用Java方法都是通過名稱加方法簽名調(diào)用的,這種方式,寫起來很舒服,但運(yùn)行效率肯定是非常低的,因?yàn)槊看味家ㄟ^名稱去查找對(duì)應(yīng)的方法
其實(shí)JNI原始的C接口,是通過jobjectID、jclassID、jmethodID、jfieldID來和Java交互的,只不過是jni crate給封裝了一層比較友好的接口
如果我們對(duì)性能要求比較高,則可以在初始化的時(shí)候獲取一些JClass、JObject的全局引用,緩存起來,后面再轉(zhuǎn)成JClass、JObject來使用,千萬不要對(duì)jmethodID、jfieldID獲取全局引用,因?yàn)檫@倆都是通過jclassID生成的,其聲明周期和jclassID對(duì)應(yīng)的對(duì)象相同,不是需要GC的對(duì)象,如果對(duì)jmethodID獲取全局引用然后調(diào)用,會(huì)導(dǎo)致某些JVM Crash;對(duì)于jmethodID、jfieldID,則可以基于JClass、JObject的全局引用獲取,后面直接使用即可
獲取到這些全局的ID之后,就可以通過JNIEnv::call_method_unchecked
系列函數(shù),來更高效地調(diào)用Java
我用Rust強(qiáng)大的宏,實(shí)現(xiàn)了這個(gè)過程,可以讓我們直接在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
審核編輯:湯梓紅
-
接口
+關(guān)注
關(guān)注
33文章
8575瀏覽量
151015 -
JAVA
+關(guān)注
關(guān)注
19文章
2966瀏覽量
104702 -
C語言
+關(guān)注
關(guān)注
180文章
7604瀏覽量
136692 -
C++
+關(guān)注
關(guān)注
22文章
2108瀏覽量
73618 -
Rust
+關(guān)注
關(guān)注
1文章
228瀏覽量
6601
原文標(biāo)題:【Rust筆記】Rust與Java交互-JNI模塊編寫-實(shí)踐總結(jié)
文章出處:【微信號(hào):Rust語言中文社區(qū),微信公眾號(hào):Rust語言中文社區(qū)】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論