Rust 在服务端和嵌入式领域已经有很多跨平台模式的应用案例了,本文主要是来探究一下 Rust 在移动开发领域的跨平台开发模式。
本文中涉及所有信息均来自于互联网,如有错漏,请及时反馈。
目前 Rust 对 iOS 和 Android 平台 Target 都是 Tier 2 和 Tier 3 级别的支持,并且都支持 std 。
对 iOS 的支持:
Tier 2:
aarch64-apple-ios
,即 ARM64 iOSaarch64-apple-ios-sim
,即 Apple iOS Simulator on ARM64x86_64-apple-ios
,即 64-bit x86 iOSTier 3:
aarch64-apple-ios-macabi
,即Apple Catalyst on ARM64armv7-apple-ios
,即ARMv7 iOS, Cortex-a8armv7s-apple-ios
i386-apple-ios
,即32-bit x86 iOSx86_64-apple-ios-macabi
,即Apple Catalyst on x86_64对 Android 的支持:
Tier 2:
aarch64-linux-android
,即 ARM64 Androidarm-linux-androideabi
,即 ARMv7 Androidarmv7-linux-androideabi
,即 ARMv7a Androidi686-linux-android
,即 32-bit x86 Androidthumbv7neon-linux-androideabi
, 即 Thumb2-mode ARMv7a Android with NEONx86_64-linux-android
,即 64-bit x86 Android看得出来,Rust 对 iOS 和 Android 的支持还算可以。
一些公司使用 Rust 来开发跨平台组件,即,在 iOS 和 Andriod 之间共享代码。
对于 iOS ,Rust 可以直接通过FFi 的方式绑定 C-ABI 来进行交互,可以参考 Mozilla 近期发布的这篇文章来了解如何在 iOS 应用中部署 Rust 库。对于 Android ,Rust 通过 JNI 相关的库来进行相关绑定,Rust 社区生态也有一些类似于 android-ndk-rs的工具来进行 NDK 开发。
可以把这种模式称为:应用的 Rust 后端(App Backend-Rust)。当然,Rust 也可以编译到 WebAssembly ,也可以共享到浏览器端(应用条件比客户端相对严格一些)或桌面端(比如 Electron)。
为什么不用 Cpp 来共享代码?
也许可以从 Dropbox 在 2019 年发布的文章《在 iOS 和 Android 之间共享代码的隐藏成本》中看到问题所在:Cpp共享代码会带来四类隐藏成本。以下摘要:
如果写一次代码到处复用的成本开销超过了其收益,就没有必要使用这种策略了。所以, Dropbox 又回归了使用本地语言进行移动开发,直到现在 2022年。今年3月份 Dropbox 的一篇博客也说明了这一情况:使 Android 的相机上传更快更可靠 。
Dropbox Capture 桌面客户端使用 Rust 开发跨平台组件
虽然在移动领域,Dropbox 放弃了使用 Cpp 共享代码这种策略,但也没有使用 Rust。然而在 2021 年,Dropbox 的另一个团队,Dropbox Capture 则使用 Rust 来构建其桌面客户端的跨平台同步引擎组件。Capture 是一个视觉交流工具,旨在使团队可以轻松地使用屏幕录像、视频消息、屏幕截图或 GIF 异步共享他们的工作。
为什么使用 Rust 呢?
一方面,Rust 在 Dropbox 的产品中应用面越来越广,团队经验提升;另一方面,Rust 本身非常符合 Capture 这个场景,希望更好地控制截屏和录制能力、更好的错误处理以及更快的幕后性能。
使用 Rust 的收益:
根据以上 Dropbox 的实践,可以对 Rust vs Cpp 在跨平台共享组件方面的优劣有一定了解。接下来再看看其他公司的实践。
FullStory 公司 提供了数字体验智能 (DXI) 平台,提供了数据分析服务。该平台也提供了针对移动应用的数据采集功能,其中跨平台组件使用 Rust 开发。
为什么选择 Rust ?
当前的移动生态系统主要由 iOS 和 Android 设备组成。任何想要接触大多数移动用户的人都会发现自己必须开发两个版本的应用程序。
这两个平台存在显着差异:在语言级别上,iOS 应用程序主要是用 Swift(历史上是 ObjC)编写的;另一方面,Android 应用程序主要是用 Java 编写的,如今 Kotlin 越来越受欢迎。因为平台对现代移动应用程序想要做的所有事情都有不同的抽象,从创建按钮小部件到地理定位都不同,所以维护两个不同语言的版本会随着业务发展越来越困难。
FullSotry 公司面临的问题可能更加复杂,因为它们是提供框架给别人去使用,而不是直接发布应用程序。所以要考虑技术选择对客户的影响,例如与他们的代码进行有害交互的可能性,以及在 CPU 和内存使用方面对用户可见的性能影响。
如何使用 Rust ?
首先制定了一个粗略的设计准则:Rust 代码应包含与平台无关的通用代码,特定平台的代码应该保留在特定平台的代码中。
代码组织结构:
shared-core
crate,包含了大部分通用代码,并且使用 trait 来定义特定平台部分的接口。shared-android
crate,用于提供与 Java 共享的代码接口。shared-ios
crate, 用于提供与 ObjectiveC 共享的代码接口。shared-mock
crate ,为了便于核心 Rust 代码的开发而实现了一个“模拟”平台,它提供所有相同的接口,但只是在桌面环境中运行。这有助于在纯 Rust 环境中开发许多东西,并避免了移动工具链的集成痛苦。对于 iOS ,将 Rust 代码构建为静态库,使用 Ditto 编写脚本来构建 Rust 工具链,使用与 XCode 一致的 LLVM 版本。
对于 Android,将 Rust 构建为静态库,通过 CMake 与一些 C 代码链接,再给 Java 库共享。
使用 Rust 以后的优势
移动开发中使用 Rust 带来的痛苦是什么?
FullStory 工程师在 hacknews 上面透露:
"The only real pain we felt was around iOS bitcode and that was mainly Apple's fault because they make the whole process so Byzantine if you aren't using clang."
"我们唯一真正感到痛苦的是围绕着iOS的位码,这主要是苹果的错,因为如果你不使用clang,他们会使整个过程变得非常复杂。"
关于此问题,在 Rust issues 中有记录:https://github.com/rust-lang/rust/issues/35968 (未解决),导致FullStory 团队无法直接使用上游 Rust 工具链,他们的解决办法是使用与 Xcode 相同版本的 LLVM来提供带有嵌入式 LLVM 位码的框架。
1Password 现在也加入了 Rust 基金会。其产品大约 63% 的 1Password 核心代码(加密和同步数据)使用了 Rust 。用 Rust 开发的跨平台组件来支持多个平台,包括移动端和浏览器(将 Rust 编译为 WebAssembly)。但 1Password 把这种方式叫「混合(hybrid)应用程序开发」。
“1Password 也开源了一些跨平台库。比如, sys-locale,轻量级获取位置的跨平台库,支持 iOS/ Android/ MacOS/ Linux/ Windows/ WebAssembly 。还有一个早期开源的
TypeShare
库,用于把一些用Rust写的类型生成为其他语言的对应类戏,目前已经不再维护,但是其公司内部还在使用。
下面这张图展示了1Password Linux 版本下 core 相关架构。
1password
首先,1Password 这个 core 库是完全独立的 Rust 库,它定义了明确的 API 来供各个客户端来使用。
op-app
和op-ui
用于整合其他crates。大部分状态被完全保留在内部,以确保密钥和其他secrets得到正确的处理,同时也使每个客户端UI能够专注于他们的优势,而不是业务逻辑。foundation
crate 为 core 提供特定平台的服务(OS Services)。等等。
目前这个通用的 core 库在 Linux 端比较成功,1Password 团队表示会在下一代 1Password 的产品中也采用这种架构。
“1Password 趣事: 翻到一篇2019年论文,探讨了流行的密码管理器的安全最佳实践,包括 1Password 。并且同年,在 1P 社区论坛中针对该论文中提到 1P的安全问题引起激烈讨论,1P 成员也第一次提到 Rust 语言,也许这是 1P使用 Rust 的开端,现在1Password 是 Rust 基金会成员。
关于飞书使用 Rust 的公开资料很少,只有2019年字节跳动王枞在QCon分享的《Rust 跨平台客户端开发在字节跳动的实践》,从其中可以了解到,飞书使用 Rust 也是做跨平台组件。飞书客户端非 UI 部分由 Rust 跨平台实现,目前包括移动端和桌面端共 5 个平台。
为什么跨平台使用Rust?
考虑跨平台能带来的收益:
如何做跨平台:
语言选择:
飞书架构:
如图,基本架构是通过一个线程池来管理不同的线程进行交互。
Rust 进程 和 Swift / Java / NodeJS 通过 FFi 和 Protocol Buffers (类似 RPC)的方式来进行调用。
Rust 库的模块组织结构如下:
每个crate都是独立构建、运行、测试和依赖。底层的一些 crate 不会频繁变动。
用到的 Rust 技术栈为:
遇到的问题:
Piccolo 是一款适用于 iPhone、iPad 和(通过Mac Catalyst)macOS 的 Othello(黑白棋) 应用程序,收获了 AppStore 2022年2月精选和2021年最爱游戏荣誉。
作者在其博客文章里探讨了 Rust 是比 Swfit 更适合编写游戏 AI 模块的语言。
博客文章主要观点摘要:
cbindgen
这个工具时需要注意,有时会在 Swift 代码的上下文中生成对 C 绑定不友好的函数签名,因为 Swfit 对某些类型支持不完善, 所以还需要在生成之后手工检查一下。AppFlowy是一款开源的笔记类应用,类似于 Notion 。但 AppFlowy 相比Notion更加灵活,用户不仅可以DIY自己的域名、页面外观,还可以跨多个平台使用。
“虽然飞书使用 Rust 开发跨平台组件的细节不得而知,但是据飞书开发成员透露,其应用思路应该和 AppFlowy 是相似的。
AppFlowy 使用 Rust 和 Flutter 来开发,官方也通过一篇文章来介绍 AppFlowy 的整体架构。
AppFlowy 的前端和后端服务都用到了 Rust。这里主要谈前端架构。
前端架构 AppFlowy 采用了领域驱动设计(DDD)的概念,采用分层架构
“DDD,简单来说,就是业务架构映射到了系统架构之上,并能充分隔离业务和技术的边界。一般通过分层架构和六边形架构实现业务与技术实现的隔离。 分层架构:遵循“关注点分离”原则来进行上下分层,将属于业务逻辑的关注点放到领域层(Domain Layer)中,而将支撑业务逻辑的技术实现放到基础设施层(Infrastructure Layer)中。另外还有一个应用层(Application Layer),一方面通过暴露业务应用服务的 API 来作为业务的逻辑门面(Facade),另一方面它又是业务和技术实现的粘合剂。 六边形架构(端口-适配器):遵循“内外分离”原则来分层。内部代表了应用的业务逻辑,外部代表应用的驱动逻辑、基础设施和其他诸如UI/DB 等层,这样更加清晰地勾勒出业务逻辑与技术实现的边界,且将业务逻辑放在了架构的核心位置。体现业务逻辑的应用层与领域层处于六边形架构的内核,并通过内部的六边形边界与基础设施的模块隔离开。在进行软件开发时,只要恪守架构上的六边形边界,就不会让技术实现的复杂度污染到业务逻辑,保证了领域的整洁。如果我们在领域层或应用层抽象了技术实现的接口,再通过依赖注入将控制的方向倒转,业务内核就会变得更加的稳定,不会因为技术选型或其他决策的变化而导致领域代码的修改。
AppFlowy 的前端架构分层了两大部分:
抽象层级由基础设施层到表示层递增,复杂度则相反。
如何跨平台
拿 Flutter 来说,AppFlowy 通过实现 FlowySDK 来做到跨平台:在 Flutter 中定义接口,在 Rust 中实现接口,然后通过 Dart FFi 来绑定 Rust 实现,利用事件分发机制来。
这种模式的优点是:
缺点:
AppFlowy DDD 架构整体业务处理流程
流程如下:
Dart_ffi
将其送入FlowySDK
。protobuf
对象到上层future
的完成,如果状态发生变化,则重建 Widget。相关
flutter_rust_bridge
的作者 fzyzcjy 也在和 AppFlowy 的开发者沟通,后续 AppFlowy 有可能使用 flutter_rust_bridge
,但目前这个进展比较缓慢。
Glean 是 Mozilla 开源的一款现代化跨平台遥测(Telemetry)库。它提供了 Glean SDK支持 Rust/ Kotlin / Swift / Python / JavaScript / QML
等多语言和开发环境。其中 Rust/ Kotlin/ Swift/ Python
SDK 是基于一个 Rust core 库来构建的,而 Javascript/ QML
是基于 JavaScript 核心库构建。
“Telemetry(遥测技术),一般是指从物理网元或者虚拟网元上远程实时高速采集数据,实现对网络实时、高速和更精细的监控技术。
跨平台支持:
Glean 使用 uniffi-rs 来自动生成 Rust 库 FFi 绑定,通过需要编写一个接口定义语言(基于WebIDL)文件来描述目标语言可用的方法和数据结构。然后可以生成 Kotlin / Swift 绑定。
除了使用 Rust 开发跨平台共享组件之外,在 Rust 生态中,也有一些框架和游戏引擎来帮助你实现跨平台的应用和游戏。
Tauri 主要是用于开发桌面平台,相比于 Electron 来说,更加轻量、性能更好。Tauri 可以使开发者利用每个平台的 Webview 技术栈,通过 JS Api 调用后台接口。目前支持 Windows/ MacOS/ Linux 等平台,对于 iOS / Android 移动平台的支持正在进行中。
“1Password 8 使用 Electron 开发,引起很多用户的不满。因为 Electron 占用资源过多,也存在内存泄漏问题和安全隐患。用户们考虑到只是一个密码管理软件,不应该占用过多系统资源。也许未来 Tauri 会是一个好的替代。
Tauri 架构
Tauri 主要由两大组件构成:
tauri-build
,实现了一些宏来帮助 Tauri 使用 Cargo 进行编译tauri-codegen
,用于处理内嵌的资源(assets)tauri-macros
,基于 tauri-codegen
为 上下文、处理程序和命令提供了一些宏。tauri-runtime
, WebView 粘合层tauri-runtime-wry
,为tauri自己维护的跨平台渲染库 wry 提供直接的系统级交互,比如打印、监视器检测和其他与窗口相关的任务tauri-utils
,通用代码库。tauri
,门面层抽象,整合了上面几个库,提供最终 APIapi
,是由 TypeScript 实现的,用于创建ESM(ES6Module)和CJS(CommonJS),便于前端框架导入 js endpoint,这样 Webview 就可以调用并监听后端的活动了。bundler
,由 Rust 实现,实际是改造了cargo-bundle
crate,用于为各个操作系统平台打包程序。cli-rs
,Rust 实现的跨平台 Cli 。cli-js
,是对 cli-rs
的包装,使用 napi-rs
为每个平台生成 npm 包。webdriver
,之前叫tauri-driver
,是一个跨平台 WebDriver Server。tauri-app/create-tauri-app
,TypeScript实现的一个脚手架工具,帮助开发者快速创建 Tauri App。跨平台支持
tauri-app/tao
,纯 Rust 实现的跨平台应用程序窗口创建库,支持 Windows、macOS、Linux、iOS 和 Android 等所有主要平台,基于 winit
二次开发。tauri-app/wry
,纯 Rust 实现的跨平台 WebView 渲染库,支持 Windows、macOS 和 Linux 等所有主要桌面平台。Tauri 使用 WRY 作为抽象层,负责确定使用哪个 webview(以及如何进行交互)。因为现在 iOS 和 macOS 都是用 wkwebview
,所以相当于也支持了 iOS 。而对于 Andriod 的支持还在进行中。tauri-app/tao
利用 Rust 的 trait 和 features 机制,完美实现了跨平台架构:
使用统一的 Window
、Clipboard
、 EventLoop
等窗口抽象:
// Window 抽象
pub struct Window {
pub(crate) window: platform_impl::Window,
}
// Clipboard 抽象
#[derive(Debug, Clone, Default)]
/// Object that allows you to access the `Clipboard` instance.
pub struct Clipboard(ClipboardPlatform);
// EventLoop 抽象
pub struct EventLoop<T: 'static> {
pub(crate) event_loop: platform_impl::EventLoop<T>,
pub(crate) _marker: ::std::marker::PhantomData<*mut ()>, // Not Send nor Sync
}
其内部由 platform_impl
门面模块来负责调用各个平台的特定实现:
#[cfg(target_os = "windows")]
#[path = "windows/mod.rs"]
mod platform;
#[cfg(any(
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
))]
#[path = "linux/mod.rs"]
mod platform;
#[cfg(target_os = "macos")]
#[path = "macos/mod.rs"]
mod platform;
#[cfg(target_os = "android")]
#[path = "android/mod.rs"]
mod platform;
#[cfg(target_os = "ios")]
#[path = "ios/mod.rs"]
mod platform;
对于 tauri-app/wry
来说,跨平台架构的实现也是和 tao
类似的。
建立统一的跨平台抽象:
// WebView 抽象
pub struct WebView {
window: Rc<Window>,
webview: InnerWebView,
}
pub struct WebViewBuilder<'a> {
pub webview: WebViewAttributes,
web_context: Option<&'a mut WebContext>,
window: Window,
}
impl<'a> WebViewBuilder<'a> {
pub fn build(self) -> Result<WebView> {
let window = Rc::new(self.window);
let webview = InnerWebView::new(window.clone(), self.webview, self.web_context)?;
Ok(WebView { window, webview })
}
}
而内部的 InnerWebView
则是平台特定行为代码,通过 cfg 和 features 来构造一个统一的门面模块,完成跨平台分发。
#[cfg(target_os = "android")]
pub(crate) mod android;
#[cfg(target_os = "android")]
use android::*;
#[cfg(any(
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
))]
pub(crate) mod webkitgtk;
#[cfg(any(
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
))]
use webkitgtk::*;
#[cfg(any(target_os = "macos", target_os = "ios"))]
pub(crate) mod wkwebview;
#[cfg(any(target_os = "macos", target_os = "ios"))]
use wkwebview::*;
#[cfg(target_os = "windows")]
pub(crate) mod webview2;
#[cfg(target_os = "windows")]
use self::webview2::*;
看得出来,使用 Rust 进行跨平台开发,是非常简洁的。
Bevy 是一个数据驱动的游戏引擎,目前在社区的声望很高,也得到了很多公司的赞助。Bevy 也是跨平台的,在主流桌面操作系统(Linux、macOS、Windows)上开箱即用,无需特殊配置。Bevy 的目标还有 Web 浏览器(通过 WebAssembly,有一定限制)、移动设备(Android 和 iOS,可以构建但不一定能运行)和游戏主机(等待支持)。
跨平台支持
Bevy 本身的抽象程度比较高,跨平台支持主要是依赖于底层的一些库:
winit
,包装为 bevy_winit
crate,来提供跨平台窗口。跟tauri-app 类似。wgpu
,用于 bevy_render
crate,来提供跨平台渲染。这样在 bevy_ui
中就不需要关心特定平台细节了。这些底层库实现跨平台的模式和上面 Tauri 的架构是相似的。
cargo-mobile 是一个与框架无关的工具,可以生成 Rust 移动项目的所有样板文件,并且包含大量用于在移动设备上构建和运行的便捷命令,可以提升 Rust 在移动领域的开发体验。目前仅支持 macOS 和 Linux 。
ndk-rs, Android NDK 的 Rust 绑定库。该库在 Rust 移动领域生态中应用比较广泛,上面提到到公司产品和项目基本都用到了它。
自 2019 年以来,Android 团队一直致力于将 Rust 编程语言引入 Android 开源项目 (AOSP)中。在 2021 年 4月,谷歌宣布 Android 开源项目(AOSP)现在支持 Rust 编程语言来开发 OS。
关键信息摘录:
build.rs
构建脚本。因为 build.rs
需要仔细审查,为了避免在添加或更新第三方时引起供应链攻击之类的事情发生。而且 build.rs
的常用功能,Soong 已经支持了。但支持过程宏。bindgen
/ cxx
/ protobuf
/ grpc
等 crates 的支持,为了方便开发。Android 团队充分验证了 Rust 和 C++ 之间的互操作性已经在很大程度上足以在 Android 中方便地使用 Rust。rust-analyzer
/ clippy
/ rustfmt
集成到了 构建系统中。IronCore Labs 公司 CEO( Patrick Walsh )去年(2021.10)在其官网发布了一篇 给Apple 的一封公开信:请用 Rust 替换 Objective-C 的文章,内容摘要:
这是一项艰巨的工作,但它会带来巨大的投资回报。迁移到 Rust 可以消除 70% 的漏洞,并且可能更接近 95% 的可远程利用的漏洞。专注于解析来自不受信任来源的数据的库将使Apple平台更加安全。
https://foundation.rust-lang.org/posts/2022-03-08-member-spotlight-1password/
https://www.fullstory.com/blog/rust-at-fullstory-part-2-mobile-sdk/
https://digvijayu.medium.com/building-cross-platform-library-with-rust-for-ios-and-android-c56a448e4804
https://foundation.rust-lang.org/news/2022-03-08-member-spotlight-1password/
https://dteare.medium.com/behind-the-scenes-of-1password-for-linux-d59b19143a23
https://github.com/QConChina/QConBeijing2019/tree/master/%E7%A7%BB%E5%8A%A8%E6%96%B0%E7%94%9F%E6%80%81
https://rustrepo.com/repo/i-schuetz-rust_android_ios-rust-mobile
https://github.com/Dushistov/flapigen-rs
https://github.com/ivanschuetz/wasm-rust-d3
https://github.com/Co-Epi/app-backend-rust
https://macroquad.rs/tutorials/android/
http://dockone.io/article/2434630
https://dev.to/tauri/announcing-tauri-beta-more-efficient-crossplatform-apps-with-better-features-1nbd
https://tauri.studio/docs/development/security
https://rustmagazine.github.io/rust_magazine_2021/chapter_5/running_rust_on_android.html
https://source.android.com/setup/build/rust/building-rust-modules/overview
https://blog.mozilla.org/data/2022/01/31/this-week-in-glean-building-and-deploying-a-rust-library-on-ios/
https://blog.ironcorelabs.com/an-open-letter-to-apple-please-please-replace-objective-c-with-rust-10df606c3dce
https://nadim.computer/posts/2022-02-11-maccatalyst.html
https://security.googleblog.com/2021/06/rustc-interop-in-android-platform.html
https://source.android.com/setup/build/rust/building-rust-modules/android-rust-modules