用 Rust 開發跨平臺 SDK 探索和實踐

FeatureProbe 作爲一個開源的『功能』管理服務,包含了灰度放量、AB 實驗、實時配置變更等針對『功能粒度』的一系列管理操作。需要提供各個語言的 SDK 接入,其中就包括移動端的 iOS 和 Android 的 SDK,那麼要怎麼解決跨平臺 SDK 的問題呢?

一、爲什麼要跨平臺?

二、目前常見的跨平臺方案

很多公司的跨平臺移動基礎庫基本都有 C++ 的影子,如微信,騰訊會議,還有早期的 Dropbox,知名的開源庫如微信的 Mars 等。好處是一套代碼多端適配,但是需要大公司對 C++ 有強大的工具鏈支持,還需要花重金聘請 C++ 研發人員,隨着團隊人員變動,產品維護成本也不可忽視,所以 Dropbox 後期也放棄了使用 C++ 的跨端方案。

Rust 和對應平臺的 FFI 封裝。常見的方法如飛書和 AppFlow 是通過類似 RPC 的理念,暴露少量的接口,用作數據傳輸。好處是複雜度可控,缺點是要進行大量的序列化和反序列化,同時代碼的表達會受到限制,比如不好表達回調函數。

更適合於有 UI 功能的跨平臺完整 APP 解決方案,不適用於跨平臺移動端 SDK 的方案。

三、爲什麼用 Rust ?

不考慮投入成本的話,原生方案在發佈、集成和用戶 Debug 等方面都會更有優勢。但考慮到初創團隊配置兩個資深的研發人員來維護兩套 SDK 需要面臨成本問題。

我們之前有用過 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 是如何工作的?

首先我們 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 屬於圖中的哪個部分:

圖中最左邊 Interface Definition File 對應 arithmetic.udl 文件,圖中最下面紅色的 Rust Business Logic 對應到 example 中的 lib.rs,test/bindings/ 目錄下的各平臺的調用文件對應最上面綠色的方塊,那方框中藍色的綁定文件去哪裏了呢?

 我們發現 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 的集成文檔,但是讀過之後,還是很難操作。

簡單來說,就是有個 Rust 的殼工程作爲唯一生成二進制的 crate,其他組件如 autofill, logins, sync_manager 作爲殼工程的依賴,把 udl 文件統一生成到一個路徑,最終統一生成綁定文件和二進制。好處是避免了多個 rust crate 之間的調用消耗,只生成一個二進制文件,編譯發佈集成會更容易。

安卓平臺:是生成一個 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] 來學習如何打造自己的跨平臺組件:

這裏大家也可以參考 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