前言
受到2022年“谷歌使用Rust重寫Android系統且所有Rust代碼的內存安全漏洞為零” [1] 的啟發,最近筆者懷著濃厚的興趣也順應Rust 的潮流,嘗試著將一款C語言開發的基礎軟件轉化為 Rust 語言。本文的主要目的是通過記錄此次轉化過程中遇到的比較常見且有意思的問題以及解決此問題的方法與大家一起做相關的技術交流和討論。
問題描述
本文將記錄轉化過程中遇到的另外一個問題。該問題是由已經轉化完成的 Rust 代碼使用到軟件中引入的第三方軟件包和鏈接庫所導致的。設想這樣一個場景:Rust 項目中完成某一個功能點需要用到一個或多個第三方軟件包和鏈接庫。這顯然是很常見的用戶場景,但是由于用戶環境不同,用戶安裝的第三方軟件包和鏈接庫的版本不同,使得轉化后的 Rust 代碼必須要做適當的兼容處理。
這里所說的用戶的環境不同,可以理解為芯片指令集的平臺不同,如 Intel x86 以及國產的 ARM 麒麟服務器。當然更常見的情形是芯片平臺相同,但是存在操作系統層面第三方軟件包和鏈接庫安裝的差異,如 x86 下的 Ubuntu 和 CentOS 中用戶安裝了不同版本的第三方軟件包和鏈接庫等。
事實上,即使排除所有平臺和系統層面的差異,由于用戶安裝了該基礎軟件所依賴的不同版本的第三方軟件包和鏈接庫,然而這些第三方軟件包或者鏈接庫由于自身的演進導致不同版本之間存在較大差異(可能實現相同功能的函數和函數簽名都有千差萬別),這給我重寫該軟件的工作帶來了一些挑戰。基于上述說明,在完成重寫該基礎軟件的過程中如何使得轉化后的 Rust 代碼能兼容該基礎軟件所依賴的主流第三方軟件包和鏈接庫則是我遇到的最大挑戰。需要說明的是這里的第三方軟件包和鏈接庫可能是基于 Rust 語言開發的,也可能是基于 C 語言開發的。
解決方案
對于此問題的解決方案需要使用 Rust FFI(Foreign Function Interface) [1],這基本上是沒有太大爭議的。因為在本次軟件重寫過程中我遇到的場景是:對于不同版本的鏈接庫使用哪個版本的函數取決于用戶的安裝運行時環境,所以除了 Rust FFI,在代碼適配上我還考慮了使用 Rust features [2] 機制。
下面我簡化了一下場景和解決方案,同時我把樣本代碼放到了我的 github [3] 里,歡迎大家一起交流。如樣本代碼所示,my-rust-bin 文件夾中的一段業務代碼需要調用到靜態鏈接庫 my_rust_lib 中的函數,該鏈接庫有兩個版本 v1(在文件夾 my-rust-lib-v1 中) 和 v2(在文件夾 my-rust-lib-v2 中), 且不同版本的庫其函數不一樣。
my-rust-lib-v1 對應的業務函數為:pub fn my_rust_lib_v1(left: usize, right: usize) -> usize
my-rust-lib-v2 對應的業務函數為:pub fn my_rust_lib_v2(left: usize, right: usize) -> usize
另外一個 lib 文件夾的目的其實是為了模擬用戶本地安裝的鏈接庫。可以分別編譯不同版本的靜態鏈接庫,然后把生成的庫文件(在本例中是)libmy_rust_lib.a, 然后把不同版本的庫文件拷貝到此文件夾下,以此來模擬用戶環境中安裝的不同版本的鏈接庫。解決方案中的關鍵點在于 my-rust-bin 中,
首先在 my-rust-bin 的 Cargo.toml 中有定義對應的 features,如下所示:
[features] v1=[] v2=[]
其次在 my-rust-bin 的 src/main.rs 下的代碼如下:
#[cfg(feature="v1")] modbindingmylib{ extern"C"{ pubfnmy_rust_lib_v1(left:usize,right:usize)->usize; } } #[cfg(feature="v2")] modbindingmylib{ extern"C"{ pubfnmy_rust_lib_v2(left:usize,right:usize)->usize; } } #[cfg(not(any(feature="v1",feature="v2")))] compile_error!("Pleasespecifyeither'v1'or'v2'feature"); pubfnmy_rust_lib(left:usize,right:usize)->usize{ #[cfg(feature="v1")] unsafe{ returnbindingmylib::my_rust_lib_v1(left,right); } #[cfg(feature="v2")] unsafe{ returnbindingmylib::my_rust_lib_v2(left,right); } } fnmain(){ letr_value:usize=my_rust_lib(3,5); println!("Thereturnvalueofmy_rust_libis[{}]",r_value); }
現在我來解讀一下這段代碼。代碼先分別定義一個相同的模塊 bindingmylib,然后根據 features 分別引入的依賴,使用的不同的靜態鏈接庫函數(my_rust_lib_v1 和 my_rust_lib_v2), 同時通過 compile_error! 定義一個沒有設置 v1 和 v2 features 的編譯錯誤(防止編譯時忘記設置 features選項,下面在編譯環節的時候有用)。最后將兩個有差異的函數統一為函數 my_rust_lib,并在該函數中根據 features 定義分別調用不同的函數并返回相應的值。
最后是在 my-rust-bin 中編譯二進制文件:
編譯并運行 v1 的二進制文件
#編譯v1版本的my-rust-bin $cdmy-rust-bin $cargobuild--features="v1" #運行v1版本的my-rust-bin $target/debug/my-rust-bin my_rust_lib_v1:8 Thereturnvalueofmy_rust_libis[8]
編譯并運行 v2 的二進制文件
#編譯v2版本的my-rust-bin $cdmy-rust-bin $cargobuild--features="v2" #運行v2版本的my-rust-bin $target/debug/my-rust-bin my_rust_lib_v2:8 Thereturnvalueofmy_rust_libis[8]
備注:如果編譯的時候沒有設置 --features 則會有如下輸出:
$cargobuild error:Pleasespecifyeither'v1'or'v2'feature -->src/main.rs1 | 16|compile_error!("Pleasespecifyeither'v1'or'v2'feature"); |^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
至此,用戶在編譯好該基礎軟件之后,就可以無感知的通過統一的函數入口調用不同版本的相同鏈接庫中的不同函數了。
總結
本文主要是在簡化了問題的實際場景以后,解決不同版本的同一軟件包或者鏈接庫中,函數及其函數簽名不同導致的調用問題。之所以說簡化,主要是本文所描述的場景中,my-rust-bin 和其依賴的外部鏈接庫均是 Rust 編寫。而在我的實際場景中則會更復雜一些,存在著 Rust 代碼依賴 C 編寫的外部鏈接庫,同時存在混合的原來 C 代碼部分依賴新改寫的 Rust 外部鏈接庫的情況。但是無論哪種情況,萬變不離其宗,我們都可以從這種最簡單的場景出發去解決遇到的問題。
關于作者
張懷龍曾就職于阿爾卡特朗訊,百度,IBM等企業從事云計算研發相關的工作。目前就職于 Intel 中國,擔任云原生開發工程師并致力于云原生、服務網格等技術領域研究實踐,也是Istio 的maintainer的開發者。曾多次在 KubeCon、ServiceMeshCon、IstioCon、GOTC 和 InfoQ/QCon 等大會上發表演講。
審核編輯:湯梓紅
-
操作系統
+關注
關注
37文章
6737瀏覽量
123190 -
C語言
+關注
關注
180文章
7598瀏覽量
136178 -
代碼
+關注
關注
30文章
4744瀏覽量
68345 -
Rust
+關注
關注
1文章
228瀏覽量
6570
原文標題:一次Rust重寫基礎軟件的實踐(二)
文章出處:【微信號:Rust語言中文社區,微信公眾號:Rust語言中文社區】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論