现在,不只是互联网时代,更是移动互联网时代。Rust 是当前很多程序员“最想学”的程序设计语言,而 Android 则是市场占有率最高的智能手机操作系统。熟悉 Rust 和 Android 的开发人员,对于将 Rust 用在 Android 项目开发中,估计是非常期待的。但是,目前非谷歌官方的集成方式,均复杂而不便,往往尝试后不得不放弃。
2021 年 5 月 11 日,谷歌官方,Android 安全与隐私团队成员 Ivan Lozano,在 Google Online Security 发布了一篇文章 Integrating Rust Into the Android Open Source Project,对 Rust Android 的集成开发进行了设计阐述,如:无需嵌套的(nested)构建系统、用过程宏(proc_macro
)而无需构建脚本(build.rs
)、以 crate 形式生成源码,以及动态链接 crate 等。
让我们来看看 Android 官方的集成开发设计方案——
自 2019 年以来,Android 团队一直致力于将 Rust 程序设计语言引入到 Android 开源项目(AOSP)中,以作为 Android 平台原生(native)代码开发时的内存安全替代方案。与任何大型项目一样,引入一种新语言需要仔细考虑。对于 Android 来说,重要的问题是:评估如何将 Rust 最佳地融入到 Android 的构建系统。目前,在 Soong
构建系统中,对 Rust 提供了支持。但是,当 AOSP 迁移到 Bazel
构建系统时(译注:Bazel
是 AOSP 的下一代构建系统,Soong
和 Make
都将迁移),这些设计决策和思路是同样适用的。本文,讨论了一些关键的设计思路,以及一些关于在 Android 构建系统中对 Rust 提供集成支持的决策。
在大型项目中集成 Rust
2019 年的 RustConf 会议上,大型团队中的 Rust 使用研讨会(Rust in Large Organizations Meeting)强调了一些挑战,比如:避开 Cargo 而使用 Rust 编译器(Compiler) rustc
,所带来的风险等(译注:指有些大型项目中,避开 Cargo,直接使用 Rust 编译器 rustc
。此次研讨会指出社区生态 crate 依赖于 Cargo,避开会带来风险。具体请查阅研讨会链接)。对此,我们有同样的关切。当第三方 crate 可能对广泛的社区更有益时,我们的目标是将 crate 优先引入。同样地,当为 Android 开发的 crate 能够使广泛的 Rust 社区更为受益时,我们希望将其作为独立的库发布。我们相信:Rust 在 Android 中的成功依赖于:最大限度地减少 Android 和 Rust 社区之间的分歧,并希望 Rust 社区能够从 Android 团队的参与中获益。
无需嵌套的(nested)构建系统
Rust 提供了 Cargo 作为默认的构建系统和包管理器,收集依赖项并调用 rustc(Rust 编译器)来构建目标 crate(Rust 语言包)。Soong
在 Android 中扮演了这个角色,并直接调用 rustc。这是基于如下原因:
- 在 Cargo 中,C 语言的依赖项,通过
build.rs
脚本,以ad-hoc
模式处理。Soong
已经提供了一种构建 C 语言库,并将其定义为依赖项的机制。而 Android 谨慎地控制编译器版本和全局编译标记,以确保库是以特定的方式构建的。依靠 Cargo,将引入第二个non-Soong
机制,以定义/构建 C 语言库,该机制不会受到Soong
中精心选择的编译控制的限制。 - 通过
Soong
直接调用编译器,可以为 Android 支持的各种构建配置提供所需的稳定性和控制能力(例如,指定特定于目标的依赖项位置,以及要使用的编译标记)。从技术上讲,虽然有可能实现通过 Cargo 调用rustc
,但Soong
会不明白怎么通过Cargo.toml
(Rust 项目构建清单)发出指令。再者,Cargo 是独立演化的,这将严重限制Soong
对构建的精确控制能力。 - 自包含的构建,且对主机配置不敏感,我们称之为炼金术(hermetic builds),这是 Android 可重复构建的必要条件。Cargo,则依赖于
build.rs
脚本,还未提供密封性(译注:指与外部依赖隔绝)保证。 - 对于保持工程生产力,
增量构建
尤其重要;构建 Android 需要大量的资源。Cargo 不是为集成到现有的构建系统而设计的,也没有公开它的编译单元。Cargo 构建,所有 crate 的依赖关系图是给定的Cargo.toml
,跨项目则需要多次重构构建相关 crate(译注:Rust 1.51 已经在这方面做出了努力,参见《Rust 1.51.0 已正式发布,及其新特性详述》中小节Cargo 新特性 resolver
)。这对于集成到Soong
的增量构建支持来说,太粗糙了,后者期望更小的编译单元。这种支持,对于在 Android 中扩大 Rust 的使用,是必要的。
直接使用 Rust 编译器,则可以避免这些问题,并且,这与我们在 AOSP 中编译其它代码的方式是一致的。它提供了对构建过程的最大控制,并简化了与 Android 现有构建系统的集成。不幸的是,避免它会带来一些挑战,并影响许多其它构建系统的决策。因为,Cargo 的使用,在 Rust 生态系统中根深蒂固。
无需 build.rs 脚本
build.rs
脚本编译为 Rust 二进制文件,它在构建过程中执行 Cargo 构建,以及处理预生成任务。通常用于设置生成环境,或者使用其它语言的构建库(例如 C/C ),类似于配置使用其它语言的脚本。
避开 build.rs
脚本,从某种程度上说,也是不依赖于 Cargo。因为支持这些脚本,是需要复制 Cargo 的构建行为和假设的。除此之外,AOSP 也有充分的理由避免构建脚本:
build.rs
脚本可以在执行构建的主机上,执行任意代码。从安全的角度来看,这在添加或更新第三方代码时,会带来额外的负担,因为需要仔细检查build.rs
脚本文件。- 第三方
build.rs
脚本,可能无法密封(译注:指与外部依赖隔绝),或以潜在的微妙方式复制。build.rs
文件访问build
目录之外的文件(例如/usr/lib
),是很常见的。 build.rs
最常见的任务是构建 Rust 代码所依赖的 C 语言库,我们已经通过Soong
支持了。- Android 同样避免了其它语言的构建脚本,而是简单地使用它们,以告知
Android.bp
文件的结构。
为什么支持过程宏(proc_macro),而非构建脚本(build.rs)?
为什么我们支持过程宏(proc_macro
)?因为它们是编译器插件,在编译器上下文中的主机上执行代码。
虽然 build.rs
是作为一次性代码编写的,用于处理构建单个 crate,但过程宏(proc_macro
)在编译器中定义了可重用的功能,这些功能在 Rust 社区中可以得到广泛的依赖。因此,流行的过程宏(proc_macro
)通常会得到更好的维护,以及更严格的检查,这使得代码检查过程更易于管理。作为构建过程的一部分,它们也更容易被“沙盒化”,因为它们不太可能具有编译器外部的依赖关系。
过程宏(proc_macro
)也是一种语言特性,而不是构建代码的方法。
在 Android 构建系统中,编译器插件支持也有优先权。例如,参阅 Soong
构建系统中的模块 java_plugin
。
源码生成 crate
与 C/C 编译器不同,rustc
只接受代表二进制或库入口点的单个源文件。它希望源码树的结构能够自动发现所有必需的源文件。这意味着生成的源文件需要放在源码树中,或者通过 include
指令提供:
include!("/path/to/hello.rs");
Rust 社区中,依赖 build.rs
脚本和 Cargo 构建环境来绕过这个限制。在构建时,cargo 命令会设置一个 OUT_DIR
环境变量,build.rs
脚本需要将生成的源代码放入该环境变量中。然后可通过以下方式包含该源文件:
include!(concat!(env!("OUT_DIR"), "/hello.rs"));
对于 Soong
来说,这是一个挑战。因为每个模块的输出都放在自己的 out/directory
目录中;在依赖项输出其生成源代码的地方,没有单独的目录。
对于 Android 平台的代码,我们更喜欢将生成的源代码打包到可以引入的 crate 中。这是因为:
- 防止生成的源文件名发生冲突。
- 减少在整个源码树中签入(并需要维护)的模板代码。使生成的源代码编译成一个 crate,其所需的任何模板代码都可以集中维护。
- 避免生成的代码和其它 crate 之间有隐式交互。
- 通过动态地生成源代码,来减少对内存和磁盘的压力。
因此,所有 Android 中的 Rust 源代码,生成的模块类型和代码,都可以作为一个 crate,以进行编译和使用。
我们仍然支持无需修改的第三方 crate。
默认为动态链接(dynamic linkage)
默认情况下,Rust 生态系统中,假定 crate 将静态链接到二进制文件中。动态库的优势在于升级(无论是安全性还是功能性),以及减少内存使用。Rust 缺乏稳定的二进制接口和跨 crate 信息流的使用,妨碍了在不升级所有依赖代码的情况下升级库。即使系统上的两个不同程序使用同一个 crate,也不太可能由同一个 crate 共享提供,因为 Rust 识别 crate 的精度很高。这使得 Rust 二进制文件更具可移植性,但也会导致更大的磁盘和内存占用。
对于 Android 设备来说,这是个问题,因为静态地将所有 crate 链接到 Rust 的二进制文件中,会导致过度的代码重复(尤其是在标准库中)。因此,默认情况下,我们选择动态链接 crate。允许 crate 在依赖它们的多个二进制文件之间重用,这减少了 Android 平台中 Rust 的总体内存占用。
由于动态链接在 Rust 社区中不常见,因此并非所有第三方 crate 都支持动态编译。有时,我们必须携带小补丁文件。这需要我们与 crate 维护人员一起工作,以增加支持。
当前状态
我们的构建支持 rustc
的所有输出类型(rlibs
、dylibs
、proc_macros
、cdylibs
、staticlibs
,以及可执行文件)。Rust 模块可以为给定的依赖关系(rlib
vs dylib
)自动请求适当的 crate。C/C 模块可以依赖于 Rust cdylib
或者 staticlib
生成,它们与 C/C 库的方式相同。
除了能够构建 Rust 代码外,Android 的构建系统还支持 protobuf
、gRPC
,以及 AIDL
生成的 crate。
Rust 社区为开发人员提供了很好的工具,比如 Rust 语言服务器 rust-analyzer
。我们在构建系统中集成了对 rust-analyzer
的支持,因此任何支持它的 IDE 都可以为 Android 模块提供代码补全
和 goto 定义
。
支持基于源代码的代码覆盖构建,以向平台开发人员提供测试。
在不久的将来,我们计划在站点 source.android.com 上添加文档,说明如何在 Soong
中定义和使用 Rust 模块。我们期望 Android 平台对 Rust 语言的支持,能随着 Rust 生态系统的发展而不断发展。而对于将 Rust 集成到现有的构建系统中,我们也希望继续讨论。
谢谢您的阅读,欢迎交流。