用 Rust 開發跨平臺 SDK 探索和實踐
FeatureProbe 作爲一個開源的『功能』管理服務,包含了灰度放量、AB 實驗、實時配置變更等針對『功能粒度』的一系列管理操作。需要提供各個語言的 SDK 接入,其中就包括移動端的 iOS 和 Android 的 SDK,那麼要怎麼解決跨平臺 SDK 的問題呢?
一、爲什麼要跨平臺?
-
減少人力成本,減少開發時間。
-
兩個平臺共享一套代碼,後期產品維護簡單。
二、目前常見的跨平臺方案
- C++
很多公司的跨平臺移動基礎庫基本都有 C++ 的影子,如微信,騰訊會議,還有早期的 Dropbox,知名的開源庫如微信的 Mars 等。好處是一套代碼多端適配,但是需要大公司對 C++ 有強大的工具鏈支持,還需要花重金聘請 C++ 研發人員,隨着團隊人員變動,產品維護成本也不可忽視,所以 Dropbox 後期也放棄了使用 C++ 的跨端方案。
- Rust + FFI
Rust 和對應平臺的 FFI 封裝。常見的方法如飛書和 AppFlow 是通過類似 RPC 的理念,暴露少量的接口,用作數據傳輸。好處是複雜度可控,缺點是要進行大量的序列化和反序列化,同時代碼的表達會受到限制,比如不好表達回調函數。
- Flutter
更適合於有 UI 功能的跨平臺完整 APP 解決方案,不適用於跨平臺移動端 SDK 的方案。
三、爲什麼用 Rust ?
- 開發成本
不考慮投入成本的話,原生方案在發佈、集成和用戶 Debug 等方面都會更有優勢。但考慮到初創團隊配置兩個資深的研發人員來維護兩套 SDK 需要面臨成本問題。
- 有豐富的 Rust 跨平臺經驗
我們之前有用過 Rust 實現過跨平臺的網絡棧,用 tokio 和 quinn 等高質量的 crate 實現了一個長連接的客戶端和服務端。
- 安全穩定
(1) FeatureProbe 作爲灰度發佈的功能平臺,肩負了降級的職責,對 SDK 的穩定性要求更高。
(2) 原生移動端 SDK 一旦出現多線程崩潰的問題,難以定位和排查,需要較長的修復週期。
(3) Rust 的代碼天生是線程安全的,無需依賴於豐富經驗的移動端開發人員,也可以保證提供高質量、穩定的 SDK。
四、Uniffi-rs
uniffi-rs 是 Mozilla 出品, 應用在 Firefox mobile browser 上的 Rust 公共組件,uniffi-rs 有以下特點:
安全
-
uniffi-rs 的設計目標第一條就是 “安全優先”,所有暴露給調用語言的 Rust 生成的方法,都不應該觸發未定義的行爲。
-
所有暴露給外部語言的 Rust Object 實例都要求是 Send + Sync。
簡單
-
不需要使用者去學習 FFI 的使用
-
只定義一個 DSL 的接口抽象,框架生成對應平臺實現,不用操心跨語言的調用封裝。
高質量
-
完善的文檔和測試。
-
所有生成的對應語言,都符合風格要求。
五、Uniffi-rs 是如何工作的?
首先我們 clone uniffi-rs 的項目到本地, 用喜歡的 IDE 打開 arithmetic 這個項目:
git clone https://github.com/mozilla/uniffi-rs.git
cd examples/arithmetic/src
我們看下這個樣例代碼具體做了什麼:
[Error]
enum ArithmeticError {
"IntegerOverflow",
};
namespace arithmetic {
[Throws=ArithmeticError]
u64 add(u64 a, u64 b);
};
在 arithmetic.udl 中,我們看到定義裏一個 Error 類型,還定義了 add, sub, div, equal 四個方法,namespace 的作用是在代碼生成時,作爲對應語言的包名是必須的。我們接下來看看 lib.rs 中 rust 部分是怎麼寫的:
#[derive(Debug, thiserror::Error)]
pub enum ArithmeticError {
#[error("Integer overflow on an operation with {a} and {b}")]
IntegerOverflow { a: u64, b: u64 },
}
fn add(a: u64, b: u64) -> Result<u64> {
a.checked_add(b)
.ok_or(ArithmeticError::IntegerOverflow { a, b })
}
type Result<T, E = ArithmeticError> = std::result::Result<T, E>;
uniffi_macros::include_scaffolding!("arithmetic");
下圖是一張 uniffi-rs 各個文件示意圖,我們一起來看下,上面的 udl 和 lib.rs 屬於圖中的哪個部分:
我們發現 lib.rs 最下面有這樣一行代碼 uniffi_macros::include_scaffolding!("arithmetic"); 這句代碼會在編譯的時候引入生成的代碼做依賴,我們這就執行一下測試用例,看看編譯出來的文件是什麼:
cargo test
如果順利的話,你會看到:
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
這個測試用例,運行了 python, ruby, swift 和 kotlin 四種語言的調用,需要本地有對應語言的環境,具體如何安裝對應環境超出了本文的範圍,但是這裏給大家一個方法看具體測試用例是如何啓動的,我們以 kotlin 爲例,在 uniffi-rs/uniffi_bindgen/src/bindings/kotlin/mod.rs 文件中的 run_script 方法裏,在 Ok(()) 前面加上一行 println!("{:?}", cmd); 再次運行:
cargo test -- --nocapture
對應平臺下的 run_script 方法都可以這樣拿到實際執行的命令行內容,接下來我們就能在 uniffi-rs/target/debug 中看到生成的代碼:
arithmetic.jar
arithmetic.py
arithmetic.rb
arithmetic.swift
arithmetic.swiftmodule
arithmeticFFI.h
arithmeticFFI.modulemap
其中的 jar 包是 kotlin, py 是 python,rb 是 ruby,剩下 4 個都是 swift,這些文件是圖中上面的平臺綁定文件,我們以 swift 的代碼爲例,看下里面的 add 方法:
public
func add(a: UInt64, b: UInt64)
throws
->
UInt64
{
return try FfiConverterUInt64.lift(
try rustCallWithError(FfiConverterTypeArithmeticError.self) {
arithmetic_77d6_add(
FfiConverterUInt64.lower(a),
FfiConverterUInt64.lower(b), $0)
}
)
}
可以看到實際調用的是 FFI 中的 arithmetic_77d6_add 方法,我們記住這個奇怪名字。目前還缺圖中的 Rust scaffolding 文件沒找到,它實際藏在 /uniffi-rs/target/debug/build/uniffi-example-arithmetic 開頭目錄的 out 文件夾中,注意多次編譯可能有多個相同前綴的文件夾。我們以 add 方法爲例:
// Top level functions, corresponding to UDL `namespace` functions.
#[doc(hidden)]
#[no_mangle]
pub extern "C" fn r#arithmetic_77d6_add(
r#a: u64,
r#b: u64,
call_status: &mut uniffi::RustCallStatus
) -> u64 {
// If the provided function does not match the signature specified in the UDL
// then this attempt to call it will not compile, and will give guidance as to why.
uniffi::deps::log::debug!("arithmetic_77d6_add");
uniffi::call_with_result(call_status, || {
let _retval = r#add(
match<u64 as uniffi::FfiConverter>::try_lift(r#a) {
Ok(val) => val,
Err(err) => return Err(uniffi::lower_anyhow_error_or_panic::<FfiConverterTypeArithmeticError>(err, "a")),
},
match<u64 as uniffi::FfiConverter>::try_lift(r#b) {
Ok(val) => val,
Err(err) => return Err(uniffi::lower_anyhow_error_or_panic::<FfiConverterTypeArithmeticError>(err, "b")),
}).map_err(Into::into).map_err(<FfiConverterTypeArithmeticError as uniffi::FfiConverter>::lower)?;
Ok(<u64 as uniffi::FfiConverter>::lower(_retval))
})
}
其中 extern "C" 就是 Rust 用來生成 C 語言綁定的寫法。我們終於知道這個奇怪的 add 方法名是如何生成的了,arithmetic_77d6_add 是 namespace 加上代碼哈希和方法名 add 拼接而成。接着看 call_status ,實際是封裝了 add 方法實際的返回值, call_with_result 方法定義在 uniffi-rs/uniffi/src/ffi/rustcalls.rs 中,主要是設置了 panichook, 讓 Rust 代碼發生崩潰時有排查的信息。arithmetic_77d6_add 的核心邏輯是 let _retval = r#add(a, b), 其中的 a,b 在一個 match 語句包裹,裏面的 lift 和 lower 主要做的是 Rust 類型和 C 的 FFI 中的類型轉換,具體可以看 這裏。
到這裏,我們就湊齊了上圖中的所有部分,明白了 uniffi-rs 的整體流程。
六、如何集成到項目中?
現在,我們知道如何用 uniffi-rs 生成對應平臺的代碼,並通過命令行可以調用執行,但是我們還不知道如何集成到具體的 Android 或者 Xcode 的項目中。在 uniffi-rs 的幫助文檔中,有 Gradle 和 XCode 的集成文檔,但是讀過之後,還是很難操作。
安卓平臺:是生成一個 aar 的包,Mozilla 團隊提供了一個 org.mozilla.rust-android-gradle.rust-android 的 gradle 插件,可以在 Mozilla[1] 找到具體使用。
蘋果平臺:是一個 xcframework,Mozilla 的團隊提供了一個 build-xcframework.sh 的腳本,可以在 Mozilla[2] 找到具體的使用。
我們只需要適當的修改下,就可以創建出自己的跨平臺的項目。
實際上我們使用 uniffi-rs Mozilla 的項目還是比較複雜的,這裏你可以使用 mobile sdk[3] 來學習如何打造自己的跨平臺組件:
-
rust-core[4] 是純 rust 的 crate
-
rust-uniffi[5] 是 udl 和 rust-core 依賴一起生成綁定的 crate -rust-android [6] 是生成 aar 包的安卓項目,具體是通過 gradle 插件來進行集成
-
rust-ios [7] 是生成 xcframework 的蘋果項目,通過 build-xcframewok.sh 腳本集成
這裏大家也可以參考 Github Actions[8] 編譯和構建。
七、總結
本文主要介紹瞭如何使用 Rust 來開發跨平臺 App,你可以在 GitHub[9] 或 Gitee[10] 獲取到我們用 Rust 實現跨平臺開發的所有代碼。與此同時,我們提供了無需部署的在線試用環境:https://featureprobe.io/ 和一個僅需 5 分鐘即可體驗的示例項目:https://featureprobe.io/demo/
參考資料
[1]
Mozilla/application-service: https://github.com/mozilla/application-services/blob/main/megazords/full/android/build.gradle
[2]
Mozilla/application-service: https://github.com/mozilla/application-services/blob/main/megazords/full/android/build.gradle
[3]
mobile sdk: https://github.com/FeatureProbe/client-sdk-mobile
[4]
rust-core: https://github.com/FeatureProbe/client-sdk-mobile/tree/main/rust-core
[5]
rust-uniffi: https://github.com/FeatureProbe/client-sdk-mobile/tree/main/rust-uniffi
[6]
rust-android : https://github.com/FeatureProbe/client-sdk-mobile/tree/main/sdk-android
[7]
rust-ios: https://github.com/FeatureProbe/client-sdk-mobile/tree/main/sdk-ios
[8]
Github Actions: https://github.com/FeatureProbe/client-sdk-mobile/actions
[9]
GitHub: https://github.com/FeatureProbe
[10]
Gitee: https://gitee.com/featureprobe
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/3PU1s1FSAFceObus0cbj9A