<acronym id="s8ci2"><small id="s8ci2"></small></acronym>
<rt id="s8ci2"></rt><rt id="s8ci2"><optgroup id="s8ci2"></optgroup></rt>
<acronym id="s8ci2"></acronym>
<acronym id="s8ci2"><center id="s8ci2"></center></acronym>
0
  • 聊天消息
  • 系統消息
  • 評論與回復
登錄后你可以
  • 下載海量資料
  • 學習在線課程
  • 觀看技術視頻
  • 寫文章/發帖/加入社區
創作中心

完善資料讓更多小伙伴認識你,還能領取20積分哦,立即完善>

3天內不再提示

如何用Rust通過JNI和Java進行交互

jf_wN0SrCdH ? 來源:Rust語言中文社區 ? 2023-10-17 11:41 ? 次閱讀

近期工作中有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.rslib.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_methodcall_static_method

  • 獲取字段get_fieldget_static_field

  • 修改字段set_fieldset_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函數;原始的AttachCurrentThreadJNI函數,如果當前線程已經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'a>> { let method = env.get_method_id(class, name, sig)?.into_inner(); Ok(JMethodID::from(method.cast())) } pub fn static_method_global_ref<'a>( env: JNIEnv<'a>, class: JClass, name: &str, sig: &str, ) -> ::Result'a>> { let method = env.get_static_method_id(class, name, sig)?.into_inner(); Ok(JStaticMethodID::from(method.cast())) } macro_rules! gen_global_ref { (@method_type) => { JMethodID<'static> }; (@method_type static) => { JStaticMethodID<'static> }; (@method_ref) => { method_global_ref }; (@method_ref static) => { static_method_global_ref }; ( $( #[name = $classname:literal] class $name:ident { $($method:ident : $($modify:ident)* $sig:literal,)* } )* ) => { $( #[allow(non_snake_case)] pub struct $name { pub class: JClass<'static>, $(pub $method: gen_global_ref!(@method_type $($modify)*),)* } impl $name { pub fn from_env(env: JNIEnv<'static>) -> anyhow::Result<Self> { Self::from_class(env, env.find_class($classname)?) } pub fn from_class(env: JNIEnv<'static>, class: JClass) -> anyhow::Result<Self> { let cls = env.new_global_ref(class)?; let class = JClass::from(*cls.as_obj()); core::forget(cls); Ok(Self { class, $( $method: gen_global_ref!(@method_ref $($modify)*)( env, class, stringify!($method), $sig).context(stringify!($method) )?, )* }) } } // TODO: impl Drop )* pub struct CachedClasses { $(pub $name: $name,)* } impl CachedClasses { pub fn from_env(env: JNIEnv<'static>) -> anyhow::Result<Self> { Ok(Self { $($name: $name::from_env(env).context(stringify!($name))?,)* }) } } unsafe impl Sync for CachedClasses {} unsafe impl Send for CachedClasses {} } } gen_global_ref! { #[name = "java/lang/Thread"] class Thread { currentThread: static "()Ljava/lang/Thread;", getStackTrace: "()[Ljava/lang/StackTraceElement;", } #[name = "java/lang/StackTraceElement"] class StackTraceElement { getLineNumber: "()I", toString: "()Ljava/lang/String;", } #[name = "java/io/File"] class File { getAbsolutePath: "()Ljava/lang/String;", } } static mut CLASSES: Option<Box> = None; pub unsafe fn init(env: JNIEnv<'static>) -> anyhow::Result<Option<Box>> { Ok(CLASSES.replace(CachedClasses::from_env(env)?.into())) } pub fn get() -> &'static CachedClasses { unsafe { CLASSES.as_ref().expect("Cached Java Classed not inited") } } }



審核編輯:湯梓紅

聲明:本文內容及配圖由入駐作者撰寫或者入駐合作網站授權轉載。文章觀點僅代表作者本人,不代表電子發燒友網立場。文章及其配圖僅供工程師學習之用,如有內容侵權或者其他違規問題,請聯系本站處理。 舉報投訴
  • 接口
    +關注

    關注

    33

    文章

    7765

    瀏覽量

    148768
  • JAVA
    +關注

    關注

    19

    文章

    2905

    瀏覽量

    103111
  • C語言
    +關注

    關注

    180

    文章

    7540

    瀏覽量

    130566
  • C++
    C++
    +關注

    關注

    21

    文章

    2066

    瀏覽量

    72947
  • Rust
    +關注

    關注

    1

    文章

    224

    瀏覽量

    6402

原文標題:【Rust筆記】Rust與Java交互-JNI模塊編寫-實踐總結

文章出處:【微信號:Rust語言中文社區,微信公眾號:Rust語言中文社區】歡迎添加關注!文章轉載請注明出處。

收藏 人收藏

    評論

    相關推薦

    聊聊Rust與C語言交互的具體步驟

    rust FFI 是rust與其他語言互調的橋梁,通過FFI rust 可以有效繼承 C 語言的歷史資產。本期通過幾個例子來聊聊
    發表于 07-06 11:15 ?1078次閱讀

    【NanoPi Duo開發板試用申請】智能家居,Java高級語言控制

    服務器進行交互,并通過Android端進行控制。 實現輕松的接入,為智能硬件助力,將底層硬件的控制通過j
    發表于 09-21 15:35

    請問Labview與Java交互是否可以實現?

    各位大佬,小弟目前在做畢業設計是關于人工智能的,本人想利用laview myrio 機器人的sensor 傳送數據到 java, 然后java進行信息處理然后輸出一個相應的信息,然后傳遞到labview 使得機器人
    發表于 08-13 17:09

    芯靈思SinlinxA33開發板的安卓控制LED-2-JNI基礎

    ,對應于在java層使用native關鍵字聲明的方法的。直白的說,就是在Java層聲明,C/C++語言實現的。當然,這個函數并不一般,它會通過JNI某種機制與
    發表于 02-22 16:55

    怎么通過JNI訪問AgU2701A.dll?

    嗨,我為AgU2702A示波器的IVI-C驅動程序編寫了一個JNI Wrapper。驅動程序DLL稱為AgU2701A.dll。包裝器工作正常,除非我在我的java應用程序中使用兩個不同的線程。當我
    發表于 10-23 07:24

    基于JNI的嵌入式手機軟件該如何去設計?

    Java的性能問題及幾種解決方案什么是JNI技術基于JNI的嵌入式手機軟件開發實例
    發表于 04-23 07:17

    何用 rust 語言開發 stm32

    本文介紹如何用 rust 語言開發 stm32。開發平臺為 linux(gentoo)。硬件準備本文使用的芯片為 STM32F103C8T6。該芯片性價比較高,價格低廉,適合入門學習。需要
    發表于 11-26 06:20

    怎樣去使用Rust進行嵌入式編程呢

    使用Rust進行嵌入式編程Use Rust for embedded development篇首語:Rust的高性能、可靠性和生產力使其適合于嵌入式系統。在過去的幾年里,
    發表于 12-22 07:20

    JNI如何實現Android stdio IIC與從機通信的呢

    RK3288如何實現JNI對接上層Java和下層的C++呢?JNI如何實現Android stdio IIC與從機通信的呢?
    發表于 03-04 06:04

    何用java映射創建java對象和調用java對象呢

    java是一種解析語言,java程序是通過java虛擬機解析.class的方式運行起來。因此,java中就存在
    發表于 04-11 14:43

    java與c之間的數據交互

    最近作一個tiemsten數據庫的項目,用到了jni技術。在這個項目中,我們用java來寫界面和業務邏輯,用c語言寫數據庫odbc訪問。單純的odbc其實沒有什么難的,但是在java和c之間
    發表于 11-27 10:22 ?1692次閱讀

    JNI java調用so動態庫方法

    JNI Java調用so包相關問題總結,出現了很多問題,按照操作應該不會發生不到so包的錯誤,其實最后出現的也是說加載不到libpython2.7.x.so.1,我就納悶了,怎么和python扯上
    發表于 11-28 13:13 ?3312次閱讀

    RSA算法的JNI封裝步驟

    要求較高的算法往往是基于C/C++語言(與硬件關聯性更強)實現的。如果應用程序需要基于JAVA編程實現時,這就會有一些矛盾。此時,通過JNI技術,Java開發者可以在不了解算法內容的情
    的頭像 發表于 06-04 17:45 ?2019次閱讀
    RSA算法的<b class='flag-5'>JNI</b>封裝步驟

    Go/Rust挑戰Java/Python地位

    編程語言方面,Java 和 Python 仍然遙遙領先,并且分別微小增長了 1.7% 和 3.4%;圍繞 Go (增長 20%) 和 Rust (增長 22%) 的興趣則大幅增加。報告稱,如果這種
    的頭像 發表于 03-06 10:19 ?523次閱讀

    何用Java代碼調用

    CloneNotSupportedException ; 你敢說你沒用過這些方法?如果你用過,那你就是一定用過不是Java語言編寫的方法。 答案就是【native】關鍵詞,用此關鍵詞修飾的方法,多數情況就不是用Java實現的。 那么為什么要用 native 來修飾方法,
    的頭像 發表于 10-11 15:29 ?277次閱讀
    如<b class='flag-5'>何用</b><b class='flag-5'>Java</b>代碼調用
    亚洲欧美日韩精品久久_久久精品AⅤ无码中文_日本中文字幕有码在线播放_亚洲视频高清不卡在线观看
    <acronym id="s8ci2"><small id="s8ci2"></small></acronym>
    <rt id="s8ci2"></rt><rt id="s8ci2"><optgroup id="s8ci2"></optgroup></rt>
    <acronym id="s8ci2"></acronym>
    <acronym id="s8ci2"><center id="s8ci2"></center></acronym>