乐趣区

关于ios:用-Rust-开发跨平台-App-探索和实践

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 属于图中的哪个局部:

图中最右边 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 找到具体应用。

苹果平台 :是一个 xcframework,Mozilla 的团队提供了一个 build-xcframework.sh 的脚本,能够在 Mozilla 找到具体的应用。

咱们只须要适当的批改下,就能够创立出本人的跨平台的我的项目。

实际上咱们应用 uniffi-rs Mozilla 的我的项目还是比较复杂的,这里你能够应用 mobile sdk 来学习如何打造本人的跨平台组件:

  • rust-core 是纯 rust 的 crate
  • rust-uniffi 是 udl 和 rust-core 依赖一起生成绑定的 crate
    – rust-android 是生成 aar 包的安卓我的项目,具体是通过 gradle 插件来进行集成
  • rust-ios 是生成 xcframework 的苹果我的项目,通过 build-xcframewok.sh 脚本集成

这里大家也能够参考 Github Actions 编译和构建。

七、总结

本文次要介绍了如何应用 Rust 来开发跨平台 App,你能够在 GitHub 或 Gitee 获取到咱们用 Rust 实现跨平台开发的所有代码。

退出移动版