Android 官方团队分享:Android 平台中 Rust 与 C++ 的互联互通

2,020 阅读13分钟

本文翻译自:Rust/C++ interop in the Android Platform 2021年6月8日

作者:Joel Galenson 和 Matthew Maurer(Android团队)

评估Rust在Android平台内使用的主要挑战之一是确保我们能与现有代码库提供足够的互联互通的可操作性。如果Rust要实现其在Android范围内提高安全性、稳定性和质量的目标,我们就必须能够在代码库中需要使用本地代码的任何地方使用Rust。为了实现这一目标,我们需要提供平台开发者使用的大部分功能。正如我们之前讨论的那样,我们有太多的C++,不能考虑忽略它,重写所有的C++是不可行的,而且重写旧的代码可能会适得其反,因为这些代码中的错误已经基本修复。这意味着互操作性是最实用的前进方式。

在将Rust引入安卓开源项目(AOSP)之前,我们需要证明Rust与C和C++的互操作性足以在安卓中实际、方便、安全地使用。增加一种新的语言是有成本的;我们需要证明Rust能够在整个代码库中进行扩展并发挥其潜力,以证明这些成本是合理的。这篇文章将涵盖我们一年多前在评估Rust在Android中的使用时所做的分析。我们还介绍了一个后续分析,其中有一些关于原始分析在Android项目采用Rust后的表现的见解。

Android中的语言互操作性

Android中现有的语言互操作性侧重于定义好的外国功能接口(FFI)边界,也就是用一种编程语言编写的代码调用另一种语言编写的代码。Rust的支持同样也会集中在FFI边界上,因为这与AOSP项目的开发方式、代码的共享方式以及依赖关系的管理方式是一致的。对于Rust与C语言的互操作性,C语言的应用二进制接口(ABI)已经足够。

与C++的互操作性则更具挑战性,也是本篇文章的重点。虽然Rust和C++都支持使用C的ABI,但对于这两种语言的习惯性使用来说,它是不够的。简单地列举每种语言的特点,就会得出一个不令人惊讶的结论:许多概念不容易翻译,我们也不一定希望它们是这样。毕竟,我们之所以引入Rust,是因为C++的许多特征和特性使我们难以编写安全和正确的代码。因此,我们的目标不是考虑所有的语言特性,而是分析Android是如何使用C++的,并确保互操作对我们绝大多数的用例都是方便的。

我们具体分析了Android平台的代码和接口,而不是一般的代码库。虽然这意味着我们的具体结论对于其他代码库可能并不准确,但我们希望这个方法能够帮助其他人在将Rust引入他们的大型代码库时做出更明智的决定。我们在Chrome浏览器团队的同事也做了类似的分析,你可以找到[这里](www.chromium.org/Home/chromi…

这份分析报告最初并不打算在谷歌之外发布:我们的目标是以数据为导向,决定Rust是否是Android系统开发的一个好选择。虽然分析的目的是准确和可操作的,但它从来没有想过要全面,我们已经指出了几个可以更完整的地方。然而,我们也注意到,对这些领域的初步调查表明,它们不会对结果产生重大影响,这就是为什么我们决定不投入额外的努力。

#方法论

从Rust和C++库中导出的函数是我们认为互操作的关键所在。我们的目标很简单。

  • Rust必须能够调用C++库中的函数,反之亦然。
  • FFI应该只需要最少的模板
  • FFI不应该需要深厚的专业知识。

虽然让Rust的函数可以从C++中调用是一个目标,但本分析的重点是让C++的函数在Rust中可用,这样就可以在利用现有的C++实现的同时增加新的Rust代码。为此,我们研究了导出的C++函数,并考虑了通过C ABI和兼容库与Rust的现有和计划的兼容性。通过在共享库上运行objdump,找到它们使用的外部C++函数1,并运行c++filt来解析C++类型,从而提取出类型。这就给出了函数和它们的参数。它不考虑返回值,但有一个初步分析2

虽然让Rust函数可以从C++中调用是一个目标,但本分析的重点是让C++函数对Rust可用,这样就可以在利用C++中现有实现的同时增加新的Rust代码。为此,我们研究了导出的C++函数,并考虑了通过C ABI和兼容库与Rust的现有和计划的兼容性。通过在共享库上运行objdump,找到它们使用的外部C++函数1,并运行c++filt来解析C++类型,从而提取出类型。这就给出了函数和它们的参数。它不考虑返回值,但对这些返回值的初步分析2显示,它们不会对结果产生显著影响。

然后,我们把这些类型中的每一个都归入以下的一个桶中。

由bindgen支持

这些通常是涉及基元的简单类型(包括指针和对它们的引用)。对于这些类型,Rust现有的FFI会正确处理它们,Android的构建系统会自动生成绑定

###由cxx compat crate支持

这些都是由cxx crate处理的。目前包括std::string, std::vector,和C++方法(包括对这些类型的指针/引用)。用户只需define他们想在不同语言间共享的类型和函数,cxx将安全地生成代码来实现。

本地支持

这些类型没有被直接支持,但使用它们的接口已经被手动重做,以添加Rust支持。具体来说,这包括AIDLprotobufs所使用的类型。

我们还为StatsD实现了一个本地接口,因为现有的C++接口依赖于方法重载,而bindgen和cxx3对此的支持并不好。这个系统的使用并没有在分析中显示出来,因为C++的API并没有使用任何独特的类型。

潜在的添加到cxx中

目前是常见的数据结构,如std::optionalstd::Chrono::duration以及自定义字符串和矢量的实现。

这些可以通过未来对 cxx 的贡献来支持,或者通过使用其 ExternType 设施来支持。我们在这个类别中只包括了我们认为相对简单的实现,并且有合理机会被cxx项目接受的类型。

我们不需要/不打算支持

今天的C++ API中暴露的一些类型,要么是API的隐含部分,要么不是我们期望从Rust中使用的API,要么是语言的特殊性。我们不打算支持的类型的例子包括。

  • Mutexes - 我们希望在一种语言或另一种语言中进行锁定,而不是像我们的粗粒度理念那样,需要在语言之间传递Mutexes。
  • native_handle - 这是一个JNI接口类型,所以不适合用于Rust/C++的通信。
  • std::locale& - Android使用独立于C++的locale系统。这种类型主要出现在输出中,因为例如cout的使用,在Rust中使用是不合适的。

总的来说,这个类别代表了我们认为Rust开发者不应该使用的类型。

HIDL

Android正在废除HIDL,并将新服务迁移到AIDL for HALs。我们也在将一些现有的实现迁移到稳定的AIDL。我们目前的计划是不支持HIDL,而是倾向于迁移到稳定的AIDL。因此,这些类型目前属于上面的 "我们不需要/打算支持''桶,但我们把它们分解出来,以便更具体。如果对HIDL的支持有足够的需求,我们以后可能会重新审视这个决定。

其他

这包括所有不适合上述任何一个桶的类型。目前主要是std::string通过值传递,这不被cxx所支持。

顶级C++库

支持互操作的主要原因之一是允许重复使用现有的代码。考虑到这一点,我们确定了Android中最常用的C++库:libloglibbaselibutilslibcutilslibhidlbaselibbinderlibhardwarelibzlibcrypto、和libui。然后我们分析了这些库所使用的所有外部C++函数及其参数,以确定它们与Rust的互通性。

图表

总的来说,81%的类型属于前三类(我们目前完全支持),87%的类型属于前四类(包括我们认为可以轻易支持的类型)。几乎所有剩下的类型都是我们认为不需要支持的类型。

主线模块

除了分析流行的C++库之外,我们还研究了主线模块。支持这一背景是至关重要的,因为Android正在将其部分核心功能迁移到主线,包括我们希望用Rust来增强的大部分本地代码。此外,它们的模块化为互操作支持提供了机会。

我们分析了21个模块的64个二进制文件和库。对于每个被分析的库,我们检查了它们使用的C++函数,并分析了它们的参数类型,以确定它们与Rust的互操作性,就像我们在上面对前10个库所做的一样。

图表

这里88%的类型属于前三类,90%属于前四类,剩下的几乎都是我们不需要处理的类型。

AOSP中Rust/C++互操作的分析

随着AOSP中Rust开发近一年的时间,以及用Rust编写的十几万行代码,我们现在可以根据目前AOSP中Rust调用C/C++代码的情况来考察我们最初的分析是否成立。4

图表

结果基本上符合我们的分析预期,bindgen处理了大部分的互操作需求。新的Keystore2服务对AIDL的广泛使用导致了我们最初的分析和 "本地支持 "类别中Rust的实际使用之间的主要差异。

目前几个互操作的例子是。

  • Cxx in Bluetooth - 虽然Rust打算成为蓝牙的主要语言,但从现有的C/C++实现迁移将分阶段发生。使用cxx可以让蓝牙团队更容易地为HIDL等传统协议提供服务,直到它们被淘汰,通过使用现有的C++支持来逐步迁移它们的服务。
  • keystore中的AIDL - Keystore实现了AIDL服务并通过AIDL与应用程序和其他服务进行交互。提供这种功能将很难用cxx或bindgen等工具来支持,但本地AIDL支持很简单,使用起来也很人性化。
  • profcollectd中手动编写的包装器 --虽然我们的目标是为大多数用例提供无缝的互操作,但我们也想证明,即使自动生成的互操作解决方案不是一种选择,手动创建它们也是简单明了的。Profcollectd是一个小的守护程序,只存在于非生产工程构建中。它不使用cxx,而是使用一些手动编写的C语言包装器,围绕C++库,然后传递给bindgen。

总结

Bindgen和cxx提供了Android所需的绝大部分Rust/C++的互操作性。对于一些例外情况,如AIDL,本地版本提供了Rust和其他语言之间方便的互操作。手动编写的包装器可以用来处理剩下的少数类型和其他选项不支持的函数,以及创建符合人体工程学的Rust APIs。总的来说,我们相信Rust和C++之间的互操作性在很大程度上已经足以在Android中方便地使用Rust。

如果你正在考虑如何将Rust整合到你的C++项目中,我们建议对你的代码库做一个类似的分析。在解决互操作的差距时,我们建议你考虑对现有的兼容库(如cxx)进行上游支持。

鸣谢

我们在量化Rust/C++互操作方面的第一次尝试是分析语言之间的潜在不匹配。这导致了很多有趣的信息,但很难从中得出可操作的结论。Stephen Hines建议我们不要列举所有可能发生互操作的地方,而是考虑目前C/C++项目之间的代码共享情况,作为我们可能希望Rust互操作的合理代理。这为我们提供了可操作的信息,可以直接确定优先次序和实施。回过头来看,我们在现实世界中使用Rust的数据已经证实了最初的方法论是正确的。谢谢Stephen!

此外,还要感谢。

  • Andrei Homescu和Stephen Crane为AOSP提供了AIDL支持。
  • Ivan Lozano为AOSP提供了protobuf支持。
  • David Tolnay发布了cxx并接受了我们的贡献。
  • bindgen的许多作者和贡献者。
  • Jeff Vander Stoep和Adrian Taylor为这篇文章做出了贡献。

  1. 我们使用由objdump报告的函数类型的未定义符号来进行这个分析。这意味着在我们的分析中,任何只用头的函数将不存在,而只用头的函数所调用的内部(非API)函数可能会出现在其中。
  2. 我们通过解析DWARF符号来提取返回值,它给出了函数的返回类型。
  3. 即使没有自动绑定生成,手动实现绑定也是很简单的。
  4. 在手写的C/C++包装器的情况下,我们分析了它们调用的函数,而不是包装器本身。对于我们的本地AIDL库的所有使用,我们分析了该库的C++版本中使用的类型。