Android Rust
https://source.android.google.cn/docs/setup/build/rust/building-rust-modules/overview?hl=zh-cn
像Java和Kotlin这样的托管语言是Android应用程序开发的最佳选择。这些语言旨在实现易用性、可移植性和安全性。Android 运行时 (ART) 代表开发者管理内存。Android 操作系统广泛使用 Java,有效地保护了 Android 平台的大部分内存错误。不幸的是,对于操作系统的较低层,Java 和 Kotlin 不是一个选择,较低级别的操作系统需要 C、C++ 和 Rust 等系统编程语言。这些语言的设计以控制和可预测性为目标。它们提供对低级系统资源和硬件的访问。它们资源较少,具有更可预测的性能特征。对于 C 和 C++,开发人员负责管理内存生存期。不幸的是,这样做很容易出错,尤其是在复杂和多线程代码库中。Rust 通过使用编译时检查来强制执行对象生命周期/所有权和运行时检查以确保内存访问有效,从而提供内存安全保证。这种安全性是在提供与 C 和 C++ 相同的性能的同时实现的。
Rust 使一系列其他语言方面现代化,从而提高了代码的正确性:
-
内存安全 - 通过编译器和运行时检查的组合强制实施内存安全。
- 野指针:访问已释放的内存
- 数据竞争:多线程环境下对同一数据的非同步访问
- 缓冲区溢出:向缓冲区写入超出其大小的数据
- 悬垂指针:指向已删除对象的指针
1. 野指针Rust不允许野指针的存在,所有指针都必须指向一个有效的对象,否则无法编译:rustlet mut x = 5;let y = &mut x;x = 6; // error, `x` is borrowed*y = 7; // ok, can use `y` because it's a valid pointer2. 数据竞争Rust不允许线程间对同一数据的非同步访问,否则无法编译:rustlet mut x = 1;let thread1 = thread::spawn(move || {x = 2; });thread1.join();x = 3; // error, cannot use `x` because it's mutably borrowed3. 缓冲区溢出Rust Array或Vec等类型有固定大小,不允许访问超出范围的索引,可以防止缓冲区溢出:rustlet mut v = vec![1, 2, 3];v[10] = 4; // error, index out of bounds4. 悬垂指针Rust 的所有权规则要求指针的有效范围不能超出其所指向的值的生命周期,可以防止悬垂指针:rust let x = 5; // `x` is valid from this point forwardlet y = &x; // `y` is a reference to `x`,// so `y` is valid from this point forward// `y` is still valid here, and points to `x`.let x = 6; // error, cannot assign to `x`, it was// borrowed by `y`.
-
数据并发 - 防止数据争用。这允许用户编写高效、线程安全的代码的便利性催生了 Rust 的无畏并发口号。
Rust通过严格的安全内存管理模型实现了无畏并发和内存安全。主要体现在:- 所有权系统
Rust的所有权系统要求每个内存资源(heap数据或stack数据)都有且仅有一个变量拥有其所有权。当拥有所有权的变量离开作用域时,其所拥有的内存资源会自动释放。
这可以确保每片内存最多只被一个指针使用,避免出现悬垂指针(dangling pointer)等问题。 - 借用检查
Rust的借用检查可以确保在任何时间,只有不超过一个可变引用或多个不可变引用指向同一片内存。这可以防止数据竞争和并发修改导致的各种问题。 - RAII
Rust使用RAII(Resource Acquisition Is Initialization)管理各类资源,这可以确保每一块资源的生命周期与其拥有者绑定,即其拥有者离开作用域时自动释放。
这简化了资源管理逻辑,防止资源泄露等问题。
例如:
rust {let mut x = 5; // x拥有其内存,初始值为5let y = &mut x; // y可变借用x的内存*y += 1; // 使用y修改xlet z = &x; // z不可变借用x的内存// x离开作用域,其内存自动释放,y和z也随之失效 } let m = Mutex::new(5); {let mut num = m.lock().unwrap(); // lock()返回RAII MutexGuard*num = 6; } // num离开作用域,MutexGuard自动解锁
总之,Rust的所有权系统、借用检查和RAII可以在编译期和运行时确保内存安全和数据竞争的不存在,这使其成为一门真正的无畏并发语言
下面通过几个例子说明Rust如何防止数据竞争:
1. 静态变量Rust静态变量默认情况下是线程安全的,编译器会对其加锁保证并发访问安全:ruststatic mut X: i32 = 1;fn increment_x() {unsafe {X += 1;}} 2. mut关键字被mut关键字修饰的变量在同一作用域内只能被一个线程可变借用,其他线程无法访问,这可以防止竞争:rustlet mut x = 1;let thread1 = thread::spawn(move || {x = 2; // 可变借用`x`});x = 3; // 错误,`x`已被上一个线程借用 3. 锁Rust提供各类锁机制(Mutex, RwLock等)用于保护对共享数据的访问,这可以明确标识可能存在竞争的数据并加以保护:rustuse std::sync::Mutex;let mutex = Mutex::new(1);let thread1 = thread::spawn(move || {let mut num = mutex.lock().unwrap();*num = 2;});let mut num = mutex.lock().unwrap();*num = 3; // 错误,`num`已被上一个线程锁定
所以,Rust通过所有权规则、借用检查和锁机制可以在编译期和运行时识别和防止各种数据竞争,这使其成为一门真正"无畏"的并发语言。
- 所有权系统
-
更具表现力的类型系统 - 有助于防止逻辑编程错误(例如,newtype wrappers,带有内容的枚举变体)。
1. 新类型(Newtype) Rust可以通过newtype关键字创建一个新的类型,该类型具有与其内部类型相同的表示形式,但被视为完全不同的类型。这可以避免其内部类型的误用: rust struct Inches(i32);let length = Inches(10);let Inches(integer_length) = length; let _: i32 = integer_length; // Oklet _: i32 = length; // Error, `Inches` is not convertible to `i32` 2. 枚举变体 Rust的枚举可以带有数据,其每个变体代表一种类型,这可以确保在任何时间仅有一种类型的数据出现,避免逻辑错误: rust enum Message {Quit,Move { x: i32, y: i32 },Write(String),ChangeColor(i32, i32, i32), }let msg = Message::Move{ x: 10, y: 20 };match msg {Message::Move{ x, y } => { /* ... */ }, // OkMessage::Write(msg) => { /* ... */ }, } 3. trait约束 Rust的泛型使用trait约束可以确保只有满足特定约束(条件)的类型才能被使用,这可以静态保证逻辑正确性: rust fn add<T: std::ops::Add>(x: T, y: T) -> T {x + y }add(1, 2); // Ok, i32 implements Add add('a', 'b'); // Ok, char implements Add add(10, true); // Error, bool does not implement Add 所以,通过新类型、枚举和trait约束,Rust拥有一个更加表现力的类型系统,可以在编译期识别各类逻辑错误,这确保其程序的逻辑安全性和正确性。
-
默认情况下,引用和变量是不可变的 - 帮助开发人员遵循最小特权的安全原则,仅在他们实际打算这样做时才将引用或变量标记为可变。虽然C++具有常量,但它往往不经常使用且不一致。相比之下,Rust 编译器通过为永不变异的可变值提供警告来帮助避免杂散的可变性注释。
rust let x = 5; // immutable let mut y = 10; // mutablex = 6; // error, cannot assign to immutable variable y = 20; // ok, y is mutablelet a = &x; // immutable reference let b = &mut y; // mutable referencea = &mut x; // error, cannot have two mutable refs to same place 这里,x和a是不可变的,y和b是可变的。我们无法对x和a进行可变操作,这可以确保线程安全和逻辑正确。 Rust还会对永不变更的可变变量发出警告,提醒我们将其改为不可变,这可以最大限度的减少出现"的杂散可变"现象。
-
在标准库中更好地处理错误 - 将可能失败的调用包装在 Result 中,这会导致编译器要求用户检查失败,即使对于不返回所需值的函数也是如此。这可以防止诸如由未处理的错误导致的“对笼子的愤怒”漏洞之类的错误。通过使通过 ?运算符和优化 为了降低开销,Rust 鼓励用户以相同的风格编写他们的易错函数并获得相同的保护。
1. Result<T, E> 类型 Result<T, E>是一个枚举,代表一个函数的结果,它要么是Ok(T)表示成功,返回值T,要么是Err(E)表示失败,返回错误E。 这强制要求用户对每个可能失败的函数调用进行错误检查,否则无法编译。这可以防止未捕获错误导致的各种问题。 2. ?运算符 ?运算符用于简化对Result类型的值进行错误检查的过程。如果Result是Ok,它会返回内部的值。如果是Err,它会立即从函数返回,因此后续语句不会执行。 这让我们可以以一致的方式处理 Success 和 Failure,写出更加优雅的代码。 例如: rust fn read_file(path: &str) -> Result<String, io::Error> {let f = File::open(path)?; // 如果打开文件失败,自动返回Errlet mut v = String::new();f.read_to_string(&mut v)?; // 如果读取失败,自动返回ErrOk(v) }fn use_file() -> Result<(), io::Error> {let text = read_file("filename.txt")?; // 如果read_file失败,自动返回ErrOk(()) } 这里,read_file和use_file函数通过?运算符进行错误传播,这简化了错误处理逻辑,并确保所有可能的失败路径都得到妥善处理。
-
初始化 - 要求在使用前初始化所有变量。未初始化的内存漏洞历来是 Android 上 3-5% 安全漏洞的根本原因。在 Android 11 中,我们开始在 C/C++ 中自动初始化内存以减少此问题。但是,初始化为零并不总是安全的,特别是对于返回值之类的内容,这可能成为错误处理的新来源。Rust 要求每个变量在使用前初始化为其类型的合法成员,避免了无意中初始化为不安全值的问题。与 C/C++ 的 Clang 类似,Rust 编译器知道初始化要求,并避免了双重初始化的任何潜在性能开销。
Rust要求所有变量在第一次使用前进行初始化,这可以避免各种未初始化内存相关的安全问题。 1. 防止未初始化内存漏洞 未初始化的内存容易产生各种漏洞,Rust的初始化要求可以在编译期识别和消除这类问题,确保每个变量在使用前都有一个合法和安全的值。 2. 避免初始化为不安全值 自动初始化为0值并不总是安全的,特别是在涉及错误处理或特殊逻辑的情况下。Rust要求我们为每个变量选择一个合适的初始值,这可以避免各种初始化不当导致的问题。 3. 无性能损失 和C/C++的Clang类似,Rust编译器能够分析初始化要求,避免重复初始化导致的性能损失。因此,Rust的这个feature几乎可以在0成本下提高程序安全性。 例如: rust let x: i32; // error, `x` is used before initializationlet y: i32 = 5; // ok, y is initializedlet z; // error, z has inferred type but no initial value let a = 5; // a is initialized to 5, type is i32let b = false; // error, b is initialized to false but has no type 这里,x和z由于没有初始化值而无法编译。y和a有正确的初始化,而b虽有初始化值false但是没有指定类型,所以也报错。 所以,Rust的初始化要求可以让我们编写出内存安全的程序,避免各种未初始化内存相关的漏洞与问题
-
更安全的整数处理 - 默认情况下,Rust 调试版本启用了溢出清理,鼓励程序员指定一个wrapping_add,如果他们真的打算让计算溢出,或者如果他们不打算saturating_add。我们打算为 Android 中的所有版本启用溢出清理。此外,所有整数类型转换都是显式转换:开发人员在函数调用期间赋值给变量或尝试使用其他类型进行算术运算时,不会意外转换。
Rust提供了更加安全的整数处理方式,这可以防止各种整数漏洞和逻辑错误。 1. 溢出清理 Rust默认启用溢出清理(overflow check),这会在整数运算溢出时 panic,而不是默默地进行二进制溢出。这强制要求程序员考虑各种边界情况,选择一个合适的行为(wrapping_add, saturating_add等)。 这可以避免各种由于整数溢出导致的安全问题和逻辑漏洞。 2. 显式类型转换 Rust要求所有类型转换都必须是显式的,这意味着类型转换只会在程序员明确表示intent时发生。 这可以防止各种由于隐式类型转换导致的问题,确保类型转换的正确性和安全性。 例如: rust let x: u8 = 255; let y: u8 = 1;let z = x + y; // panics, overflow let z = x.wrapping_add(y); // z is 0let a: u8 = 5; let b: i32 = a; // error, implicit conversion let b: i32 = i32::from(a); // ok, explicit let c: i32 = 5; let d: u8 = c; // error, implicit conversion 这里,x + y会因整数溢出panic。b = a和d = c会因为要求显式类型转换而报错。 所以,Rust通过启用溢出检查和要求显式类型转换,可以让我们在编译期就消除各种整数相关的安全和逻辑问题
所以,通过编译器严密的检查,Rust可以在编写时就识别和消除各种内存错误,这确保了其程序在运行时的内存安全,这是其最重要的优点之一。
是的,Rust 通过静态分析可以在编译期就识别和消除数据竞争,这确保了其程序在多线程环境下的并发安全,这也是Rust被称为"无畏并发"的原因之一。
Rust 必须能够从C++库中调用函数,反之亦然。
FFI 应要求最少的样板文件。
FFI 不应要求深厚的专业知识。
由 bindgen 提供支持
通常是涉及基元(包括指针和对它们的引用)的简单类型。对于这些类型,Rust 现有的 FFI 将正确处理它们,Android 的构建系统将自动生成绑定。
Supported by cxx compat crate
目前包括 std::string 、 std::vector, 和 C++ 方法(包括对这些类型的指针/引用)。用
Native support
这些类型不直接支持,但使用它们的接口已被手动重新设计以添加 Rust 支持。具体来说,这包括 AIDL 和 protobufs 使用的类型。
Android 正在弃用 HIDL,并迁移到 AIDL for HAL 以获取新服务。我们还将一些现有实现迁移到稳定的 AIDL。我们目前的计划是不支持 HIDL,而是迁移到稳定的 AIDL。因此,这些类型目前属于上面的“我们不需要/打算支持”桶,但我们将它们分解为更具体。如果对 HIDL 支持有足够的需求,我们可能会稍后重新考虑此决定。
支持互操作的主要原因之一是允许重用现有代码。考虑到这一点,我们确定了 Android 中最常用的C++库: liblog 、 libbase 、 libutils 、 libcutils 、 libhidlbase 、 libbinder 、 libhardware 、 libz 、 libcrypto 和 libui
Bindgen 和 cxx 提供了 Android 所需的绝大多数 Rust/C++ 互操作性。对于某些例外,例如 AIDL,本机版本提供了 Rust 和其他语言之间的便捷互操作。手动编写的包装器可用于处理其他选项不支持的少数剩余类型和函数,以及创建符合人体工程学的 Rust API。总的来说,我们相信 Rust 和 C++ 之间的互操作性已经足以在 Android 中方便地使用 Rust。
如果你正在考虑如何将 Rust 集成到你的C++项目中,我们建议对你的代码库进行类似的分析。在解决互操作差距时,我们建议您考虑对现有兼容库(如 cxx)的上行支持。