用 Rust 開發(fā)跨平臺 App 探索和實(shí)踐

FeatureProbe 作為一個開源的『功能』管理服務(wù)阳液,包含了灰度放量、AB實(shí)驗(yàn)咖为、實(shí)時配置變更等針對『功能粒度』的一系列管理操作秕狰。需要提供各個語言的 SDK 接入,其中就包括移動端的 iOS 和 Android 的 SDK躁染,那么要怎么解決跨平臺 SDK 的問題呢鸣哀?

一、為什么要跨平臺吞彤?

  • 減少人力成本我衬,減少開發(fā)時間。
  • 兩個平臺共享一套代碼饰恕,后期產(chǎn)品維護(hù)簡單挠羔。

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

  • C++

很多公司的跨平臺移動基礎(chǔ)庫基本都有 C++ 的影子埋嵌,如微信破加,騰訊會議,還有早期的 Dropbox雹嗦,知名的開源庫如微信的 Mars 等范舀。好處是一套代碼多端適配,但是需要大公司對 C++ 有強(qiáng)大的工具鏈支持了罪,還需要花重金聘請 C++ 研發(fā)人員锭环,隨著團(tuán)隊(duì)人員變動,產(chǎn)品維護(hù)成本也不可忽視泊藕,所以 Dropbox 后期也放棄了使用 C++ 的跨端方案辅辩。

  • Rust + FFI

Rust 和對應(yīng)平臺的 FFI 封裝。常見的方法如飛書和 AppFlow 是通過類似 RPC 的理念娃圆,暴露少量的接口汽久,用作數(shù)據(jù)傳輸。好處是復(fù)雜度可控踊餐,缺點(diǎn)是要進(jìn)行大量的序列化和反序列化,同時代碼的表達(dá)會受到限制臀稚,比如不好表達(dá)回調(diào)函數(shù)吝岭。

  • Flutter

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

三窜管、為什么用 Rust 散劫?

  • 開發(fā)成本

不考慮投入成本的話,原生方案在發(fā)布幕帆、集成和用戶 Debug 等方面都會更有優(yōu)勢获搏。但考慮到初創(chuàng)團(tuán)隊(duì)配置兩個資深的研發(fā)人員來維護(hù)兩套 SDK 需要面臨成本問題。

  • 有豐富的 Rust 跨平臺經(jīng)驗(yàn)

我們之前有用過 Rust 實(shí)現(xiàn)過跨平臺的網(wǎng)絡(luò)棧失乾,用 tokio 和 quinn 等高質(zhì)量的 crate 實(shí)現(xiàn)了一個長連接的客戶端和服務(wù)端常熙。

  • 安全穩(wěn)定

(1) FeatureProbe 作為灰度發(fā)布的功能平臺,肩負(fù)了降級的職責(zé)碱茁,對 SDK 的穩(wěn)定性要求更高裸卫。

(2) 原生移動端 SDK 一旦出現(xiàn)多線程崩潰的問題,難以定位和排查纽竣,需要較長的修復(fù)周期墓贿。

(3) Rust 的代碼天生是線程安全的,無需依賴于豐富經(jīng)驗(yàn)的移動端開發(fā)人員蜓氨,也可以保證提供高質(zhì)量聋袋、穩(wěn)定的 SDK。

四穴吹、Uniffi-rs

uniffi-rs 是 Mozilla 出品, 應(yīng)用在 Firefox mobile browser 上的 Rust 公共組件幽勒,uniffi-rs 有以下特點(diǎn):

安全

  • uniffi-rs 的設(shè)計(jì)目標(biāo)第一條就是“安全優(yōu)先”,所有暴露給調(diào)用語言的 Rust 生成的方法刀荒,都不應(yīng)該觸發(fā)未定義的行為代嗤。

  • 所有暴露給外部語言的 Rust Object 實(shí)例都要求是 Send + Sync。

簡單

  • 不需要使用者去學(xué)習(xí) FFI 的使用
  • 只定義一個 DSL 的接口抽象缠借,框架生成對應(yīng)平臺實(shí)現(xiàn)干毅,不用操心跨語言的調(diào)用封裝。

高質(zhì)量

  • 完善的文檔和測試泼返。
  • 所有生成的對應(yīng)語言硝逢,都符合風(fēng)格要求。

五绅喉、Uniffi-rs是如何工作的渠鸽?

首先我們 clone uniffi-rs 的項(xiàng)目到本地, 用喜歡的 IDE 打開 arithmetic 這個項(xiàng)目:

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 的作用是在代碼生成時,作為對應(yīng)語言的包名是必須的革屠。我們接下來看看 lib.rs 中 rust 部分是怎么寫的:

#[derive(Debug, thiserror::Error)]
pub enum ArithmeticError {
    #[error("Integer overflow on an operation with {a} and 凿试")]
    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 對應(yīng) arithmetic.udl 文件那婉,圖中最下面紅色的 Rust Business Logic 對應(yīng)到 example 中的 lib.rs板甘,test/bindings/ 目錄下的各平臺的調(diào)用文件對應(yīng)最上面綠色的方塊,那方框中藍(lán)色的綁定文件去哪里了呢详炬, 我們發(fā)現(xiàn) lib.rs 最下面有這樣一行代碼 uniffi_macros::include_scaffolding!("arithmetic"); 這句代碼會在編譯的時候引入生成的代碼做依賴盐类,我們這就執(zhí)行一下測試用例,看看編譯出來的文件是什么:

cargo test

如果順利的話呛谜,你會看到:

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

這個測試用例在跳,運(yùn)行了 python, ruby, swift 和 kotlin 四種語言的調(diào)用,需要本地有對應(yīng)語言的環(huán)境呻率,具體如何安裝對應(yīng)環(huán)境超出了本文的范圍硬毕,但是這里給大家一個方法看具體測試用例是如何啟動的,我們以 kotlin 為例礼仗,在 uniffi-rs/uniffi_bindgen/src/bindings/kotlin/mod.rs 文件中的 run_script 方法里吐咳,在 Ok(()) 前面加上一行 println!("{:?}", cmd); 再次運(yùn)行:

cargo test -- --nocapture

對應(yīng)平臺下的 run_script 方法都可以這樣拿到實(shí)際執(zhí)行的命令行內(nèi)容,接下來我們就能在 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)
  }
    )
}

可以看到實(shí)際調(diào)用的是 FFI 中的 arithmetic_77d6_add 方法,我們記住這個奇怪名字象浑。目前還缺圖中的 Rust scaffolding 文件沒找到蔫饰,它實(shí)際藏在 /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 杖剪,實(shí)際是封裝了 add 方法實(shí)際的返回值, call_with_result 方法定義在 uniffi-rs/uniffi/src/ffi/rustcalls.rs 中驰贷,主要是設(shè)置了 panichook, 讓 Rust 代碼發(fā)生崩潰時有排查的信息盛嘿。arithmetic_77d6_add 的核心邏輯是 let _retval = r#add(a, b), 其中的 a,b 在一個 match 語句包裹括袒,里面的 lift 和 lower 主要做的是 Rust 類型和 C 的 FFI 中的類型轉(zhuǎn)換次兆,具體可以看 這里。

到這里锹锰,我們就湊齊了上圖中的所有部分芥炭,明白了 uniffi-rs 的整體流程狈邑。

六、如何集成到項(xiàng)目中蚤认?

現(xiàn)在,我們知道如何用 uniffi-rs 生成對應(yīng)平臺的代碼糕伐,并通過命令行可以調(diào)用執(zhí)行砰琢,但是我們還不知道如何集成到具體的 Android 或者 Xcode 的項(xiàng)目中。在 uniffi-rs 的幫助文檔中良瞧,有 Gradle 和 XCode 的集成文檔陪汽,但是讀過之后,還是很難操作褥蚯。

[圖片上傳失敗...(image-7a51a3-1667471094710)]
簡單來說挚冤,就是有個 Rust 的殼工程作為唯一生成二進(jìn)制的 crate,其他組件如 autofill, logins, sync_manager 作為殼工程的依賴赞庶,把 udl 文件統(tǒng)一生成到一個路徑训挡,最終統(tǒng)一生成綁定文件和二進(jìn)制。好處是避免了多個 rust crate 之間的調(diào)用消耗歧强,只生成一個二進(jìn)制文件澜薄,編譯發(fā)布集成會更容易。

安卓平臺:是生成一個 aar 的包摊册,Mozilla 團(tuán)隊(duì)提供了一個 org.mozilla.rust-android-gradle.rust-android 的 gradle 插件肤京,可以在 Mozilla 找到具體使用。

蘋果平臺:是一個 xcframework茅特,Mozilla 的團(tuán)隊(duì)提供了一個 build-xcframework.sh 的腳本忘分,可以在 Mozilla 找到具體的使用。

我們只需要適當(dāng)?shù)男薷南掳仔蓿涂梢詣?chuàng)建出自己的跨平臺的項(xiàng)目妒峦。

實(shí)際上我們使用 uniffi-rs Mozilla 的項(xiàng)目還是比較復(fù)雜的,這里你可以使用 mobile sdk 來學(xué)習(xí)如何打造自己的跨平臺組件:

  • rust-core 是純 rust 的 crate
  • rust-uniffi 是 udl 和 rust-core 依賴一起生成綁定的 crate
    - rust-android 是生成 aar 包的安卓項(xiàng)目熬荆,具體是通過 gradle 插件來進(jìn)行集成
  • rust-ios 是生成 xcframework 的蘋果項(xiàng)目舟山,通過 build-xcframewok.sh 腳本集成

這里大家也可以參考 Github Actions 編譯和構(gòu)建。

七卤恳、總結(jié)

本文主要介紹了如何使用 Rust 來開發(fā)跨平臺 App累盗,你可以在 GitHubGitee 獲取到我們用 Rust 實(shí)現(xiàn)跨平臺開發(fā)的所有代碼。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末突琳,一起剝皮案震驚了整個濱河市若债,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌拆融,老刑警劉巖蠢琳,帶你破解...
    沈念sama閱讀 218,284評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件啊终,死亡現(xiàn)場離奇詭異,居然都是意外死亡傲须,警方通過查閱死者的電腦和手機(jī)蓝牲,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,115評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來泰讽,“玉大人例衍,你說我怎么就攤上這事∫研叮” “怎么了佛玄?”我有些...
    開封第一講書人閱讀 164,614評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長累澡。 經(jīng)常有香客問我梦抢,道長,這世上最難降的妖魔是什么愧哟? 我笑而不...
    開封第一講書人閱讀 58,671評論 1 293
  • 正文 為了忘掉前任奥吩,我火速辦了婚禮,結(jié)果婚禮上翅雏,老公的妹妹穿的比我還像新娘圈驼。我一直安慰自己,他們只是感情好望几,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,699評論 6 392
  • 文/花漫 我一把揭開白布绩脆。 她就那樣靜靜地躺著,像睡著了一般橄抹。 火紅的嫁衣襯著肌膚如雪靴迫。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,562評論 1 305
  • 那天楼誓,我揣著相機(jī)與錄音玉锌,去河邊找鬼。 笑死疟羹,一個胖子當(dāng)著我的面吹牛主守,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播榄融,決...
    沈念sama閱讀 40,309評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼参淫,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了愧杯?” 一聲冷哼從身側(cè)響起涎才,我...
    開封第一講書人閱讀 39,223評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎力九,沒想到半個月后耍铜,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體邑闺,經(jīng)...
    沈念sama閱讀 45,668評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,859評論 3 336
  • 正文 我和宋清朗相戀三年棕兼,在試婚紗的時候發(fā)現(xiàn)自己被綠了陡舅。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,981評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡伴挚,死狀恐怖蹭沛,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情章鲤,我是刑警寧澤,帶...
    沈念sama閱讀 35,705評論 5 347
  • 正文 年R本政府宣布咆贬,位于F島的核電站败徊,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏掏缎。R本人自食惡果不足惜皱蹦,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,310評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望眷蜈。 院中可真熱鬧沪哺,春花似錦、人聲如沸酌儒。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,904評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽忌怎。三九已至籍滴,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間榴啸,已是汗流浹背孽惰。 一陣腳步聲響...
    開封第一講書人閱讀 33,023評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留鸥印,地道東北人勋功。 一個月前我還...
    沈念sama閱讀 48,146評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像库说,于是被迫代替她去往敵國和親狂鞋。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,933評論 2 355

推薦閱讀更多精彩內(nèi)容