Comprehensive Rust 教程Unsafe Rust 五大能力详解与安全 FFI 封装实战【免费下载链接】comprehensive-rustThis is the Rust course used by the Android team at Google. It provides you the material to quickly teach Rust.项目地址: https://gitcode.com/GitHub_Trending/co/comprehensive-rust本篇基于 Comprehensive RustGoogle Android 团队使用的 Rust 培训课程中 Unsafe Rust 章节 展开。该章节从Safe Rust 与 Unsafe Rust 的边界出发逐一剖析 unsafe 解锁的五大能力——解引用裸指针、读写可变静态变量、访问 union 字段、调用 unsafe 函数、实现 unsafe trait——并通过一个完整的安全 FFI 包装器实战练习演示如何用安全抽象封装底层 C API。读完本篇你将掌握 unsafe 代码的正确书写规范SAFETY 注释、前置条件论证、常见未定义行为UB陷阱以及一套可落地的 FFI 字符串转换与 RAII 资源管理方案。Unsafe Rust 的本质编译器守卫的边界Rust 语言由两大部分组成Safe Rust安全 Rust内存安全不可能触发未定义行为undefined behaviorUnsafe Rust不安全 Rust在前置条件preconditions被违反时可能触发未定义行为。在整个课程中绝大部分代码都是 Safe Rust。但理解 Unsafe Rust 是什么、边界在哪里是 Rust 开发者进阶的必修课详见 src/unsafe-rust/unsafe.md。需要特别澄清一个常见误解Unsafe Rust 并不意味着代码是错误的。它只意味着开发者主动关闭了部分编译器安全检查需要自行保证代码的正确性——编译器不再强制 Rust 的内存安全规则。因此 Unsafe Rust 的正确用法是小而隔离unsafe 代码块应尽量小、尽量集中正确性被仔细记录每个 unsafe 操作都需要用注释说明其安全性依据包裹在安全抽象层中对外暴露安全的 API把不安全细节封在内部。Unsafe 解锁的五大能力Unsafe Rust 额外授予开发者五种能力能力说明解引用裸指针raw pointers创建指针是安全的但解引用必须 unsafe访问或修改可变静态变量static mut的读写涉及跨线程数据竞争风险访问union字段union 的活动字段需要开发者自行跟踪调用 unsafe 函数包括extern声明的外部FFI函数实现 unsafe traittrait 的实现需要满足特定条件才能避免 UB接下来逐项深入。能力一解引用裸指针裸指针*const T/*mut T的创建是安全操作但解引用必须放在unsafe块中。课程 dereferencing.md 给出了完整示例fn main() { let mut x 10; let p1: *mut i32 raw mut x; let p2 p1 as *const i32; // SAFETY: p1 and p2 were created by taking raw pointers to a local, so they // are guaranteed to be non-null, aligned, and point into a single (stack-) // allocated object. // // The object underlying the raw pointers lives for the entire function, so // it is not deallocated while the raw pointers still exist. It is not // accessed through references while the raw pointers exist, nor is it // accessed from other threads concurrently. unsafe { dbg!(*p1); *p1 6; // Mutation may soundly be observed through a raw pointer, like in C. dbg!(*p2); } // UNSOUND. DO NOT DO THIS. /* let r: i32 unsafe { *p1 }; dbg!(r); x 50; dbg!(r); // Object underlying the reference has been mutated. This is UB. */ }这段代码有两个关键教学点。安全注释是硬性要求为每个unsafe块书写注释、解释其满足的 safety 要求是好习惯且在 Android Rust 风格指南中是强制要求。上例中的注释逐条论证了指针非空、对齐、指向单个栈上已分配对象、对象生命周期覆盖指针使用期间、没有同时通过引用访问、没有其他线程并发访问。指针有效的完整条件对于指针解引用而言指针必须是valid的即满足指针必须非空non-null指针必须可解引用dereferenceable即位于单个已分配对象的边界之内底层对象不能被释放deallocated同一位置不能存在并发访问如果指针由引用强转而来底层对象必须存活且不得再用引用访问该内存大多数情况下指针还必须正确对齐aligned。一个经典的 UB 陷阱从裸指针构造引用示例中被注释掉的 UNSOUND 代码演示了一种常见 UB直接unsafe { *p1 }从裸指针构造引用会绕过编译器对引用到底指向哪个对象的追踪——借用检查器因此不会冻结x导致尽管存在对它的引用我们仍能修改x。从指针创建引用需要极大的小心。能力二可变静态变量读取不可变静态变量是安全的static HELLO_WORLD: str Hello, world!; fn main() { println!(HELLO_WORLD: {HELLO_WORLD}); }但static mut的读写是不安全的因为多个线程可能在没有同步的情况下并发访问构成数据竞争data race。在编译器无法提供帮助的前提下安全使用可变静态需要开发者自行论证并发正确性见 mutable-static.mdstatic mut COUNTER: u32 0; fn add_to_counter(inc: u32) { // SAFETY: There are no other threads which could be accessing COUNTER. unsafe { COUNTER inc; } } fn main() { add_to_counter(42); // SAFETY: There are no other threads which could be accessing COUNTER. unsafe { dbg!(COUNTER); } }需要记住的四个要点上述程序是 sound 的因为它单线程运行但 Rust 编译器按函数逐个推理无法做这样的假设。可以尝试去掉unsafe观察编译器如何解释从多个线程访问可变静态属于未定义行为2024 edition更进一步默认情况下通过引用访问可变静态将成为编译错误使用可变静态通常不是好主意应优先使用内部可变性interior mutability但在底层no_std代码中确有少数场景可能必需例如实现堆分配器或对接某些 C API。此时应使用指针而非引用来操作它。能力三访问 union 字段union 与 enum 类似但活动字段需要你自己跟踪见 unions.md#[repr(C)] union MyUnion { i: u8, b: bool, } fn main() { let u MyUnion { i: 42 }; println!(int: {}, unsafe { u.i }); println!(bool: {}, unsafe { u.b }); // Undefined behavior! }代码中u.b的读取是未定义行为因为当前活动的字段是i却以bool视角解释内存。课程指出Rust 中极少需要 unionenum 是更优越的替代仅在与 C 库 API 交互时偶尔必需如果只是想按另一种类型重新解释字节应优先考虑std::mem::transmute或使用安全包装库如zerocopycrate。值得注意的是仓库中 src/bare-metal/useful-crates/zerocopy.md 章节正是围绕zerocopy展开的与本节内容形成呼应。能力四实现 unsafe trait与函数类似trait 也可以标记为unsafe条件是实现方必须保证特定约束以避免未定义行为见 unsafe-traits.md。课程以zerocopycrate 的IntoBytestrait 为例use std::{mem, slice}; /// ... /// # Safety /// The type must have a defined representation and no padding. pub unsafe trait IntoBytes { fn as_bytes(self) - [u8] { let len mem::size_of_val(self); let slf: *const Self self; unsafe { slice::from_raw_parts(slf.cast::u8(), len) } } } // SAFETY: u32 has a defined representation and no padding. unsafe impl IntoBytes for u32 {}要点trait 的 Rustdoc 中必须包含# Safety部分说明安全实现该 trait 所需满足的要求IntoBytes实际的 safety 段落远比示例更长更复杂内置的Send和Synctrait 就是 unsafe trait——这正是类型可跨线程发送/共享这类无法由编译器自动验证的性质需要手动承诺的典型例子。深入 unsafe 函数函数或方法可以在存在额外前置条件时标记为unsafe。unsafe 函数有两个来源见 unsafe-functions.md被声明为 unsafe 的 Rust 函数extern C块中的不安全外部FFI函数。调用 unsafe 函数一个真实的越界错误课程 calling.md 用一个刻意制造 bug 的例子说明不满足前置条件会破坏内存安全#[derive(Debug)] #[repr(C)] struct KeyPair { pk: [u16; 4], // 8 bytes sk: [u16; 4], // 8 bytes } const PK_BYTE_LEN: usize 8; fn log_public_key(pk_ptr: *const u16) { let pk: [u16] unsafe { std::slice::from_raw_parts(pk_ptr, PK_BYTE_LEN) }; println!({pk:?}); } fn main() { let key_pair KeyPair { pk: [1, 2, 3, 4], sk: [0, 0, 42, 0] }; log_public_key(key_pair.pk.as_ptr()); }该示例的教训非常深刻slice::from_raw_parts的第二个参数是元素个数而不是字节数这里传入了PK_BYTE_LEN 8实际会读取 8 个u16越过pk数组末尾、读进相邻的sk数组——这是未定义行为因为读取越过了指针所源自的数组边界log_public_key应该被声明为 unsafe因为pk_ptr必须满足特定前置条件一个可能引发 UB 的安全函数被称为unsound不健全标准库中有大量底层 unsafe 函数能使用安全替代时优先使用安全版本如果把 unsafe 函数用作优化手段务必添加基准测试benchmark证明收益每个unsafe块都必须有安全注释解释为什么实际是安全的——本示例缺少注释且不健全是反面教材。unsafe 外部函数FFI可以用unsafe extern从 Rust 中声明外部函数之所以 unsafe是因为编译器无法推理它们的行为见 extern-c.md。extern块中的函数必须根据其安全使用是否存在前置条件显式标记为safe或unsafeuse std::ffi::c_char; unsafe extern C { // abs doesnt deal with pointers and doesnt have any safety requirements. safe fn abs(input: i32) - i32; /// # Safety /// /// s must be a pointer to a NUL-terminated C string which is valid and /// not modified for the duration of this function call. unsafe fn strlen(s: *const c_char) - usize; } fn main() { println!(Absolute value of -3 according to C: {}, abs(-3)); unsafe { // SAFETY: We pass a pointer to a C string literal which is valid for // the duration of the program. println!(String length: {}, strlen(cString.as_ptr())); } }这里有几个值得注意的演进与细节Rust1.82 之前所有 extern 函数都被视为 unsafe1.82 引入unsafe extern块后允许显式把没有安全要求的函数标记为safe如abs之所以调用外部函数普遍是有问题的是因为任意 C 函数都可能在意料之外的场景下违反 Rust 的内存模型尤其是涉及指针操作的函数C是 ABI 名称Rust 还支持其他 ABI编译器不会校验 Rust 函数签名与 C 端定义是否一致——这完全由你来负责。实战用安全抽象封装 C 的目录读取 API本节的压轴练习见 exercise.md要求为libc中读取目录文件名的函数族opendir、readdir、closedir构建一个安全包装器。这正是把 unsafe 关进安全抽象层理念的完整落地。完整的带注释实现与单元测试都保存在仓库的 exercise.rs 中solution.md 给出了逐步讲解。FFI 涉及的字符串类型对照练习需要你浏览std::ffi模块在三种字符串体系间来回转换类型编码用途str与StringUTF-8Rust 内部文本处理CStr与CStringNUL 结尾与 C 函数通信OsStr与OsString操作系统相关与操作系统通信完整的类型转换链路str→CString需要为结尾的\0分配空间CString→*const c_char得到指针以调用 C 函数*const c_char→CStr需要能定位结尾\0的类型CStr→[u8]字节切片是某种未知数据的通用接口[u8]→OsStr使用 Unix 下的OsStrExttrait 从字节构造OsStr→OsString需要克隆OsStr中的数据才能返回值并再次调用readdir。解决方案核心实现1. FFI 声明与平台适配。通过unsafe extern C声明三个函数并为 Linux 与 macOS 分别定义dirent结构体的内存布局macOS Intel 上还需要通过#[link_name readdir$INODE64]处理 64 位 inode 兼容问题。DIR是一个不透明opaque类型用零大小数组加PhantomData占位mod ffi { use std::os::raw::{c_char, c_int}; // Opaque type. #[repr(C)] pub struct DIR { _data: [u8; 0], _marker: core::marker::PhantomData(*mut u8, core::marker::PhantomPinned), } #[cfg(not(target_os macos))] #[repr(C)] pub struct dirent { pub d_ino: c_ulong, pub d_off: c_long, pub d_reclen: c_ushort, pub d_type: c_uchar, pub d_name: [c_char; 256], } unsafe extern C { pub unsafe fn opendir(s: *const c_char) - *mut DIR; pub unsafe fn readdir(s: *mut DIR) - *const dirent; pub unsafe fn closedir(s: *mut DIR) - c_int; } }2. 构造函数new将str转为CString处理内嵌 NUL 的错误调用opendir并检查返回指针是否为 NULLimpl DirectoryIterator { fn new(path: str) - ResultDirectoryIterator, String { let path CString::new(path).map_err(|err| format!(Invalid path: {err}))?; // SAFETY: path.as_ptr() cannot be NULL. let dir unsafe { ffi::opendir(path.as_ptr()) }; if dir.is_null() { Err(format!(Could not open {path:?})) } else { Ok(DirectoryIterator { path, dir }) } } }3.Iterator实现反复调用readdir直到返回 NULL对每个条目从d_name构造CStr、取字节、经OsStrExt::from_bytes转为OsStr再克隆为OsStringimpl Iterator for DirectoryIterator { type Item OsString; fn next(mut self) - OptionOsString { // SAFETY: self.dir is never NULL. let dirent unsafe { ffi::readdir(self.dir) }; if dirent.is_null() { return None; } // SAFETY: dirent is not NULL and dirent.d_name is NUL terminated. let d_name unsafe { CStr::from_ptr((*dirent).d_name.as_ptr()) }; let os_str OsStr::from_bytes(d_name.to_bytes()); Some(os_str.to_owned()) } }4.Drop实现RAII迭代器离开作用域时自动调用closedir确保不泄漏文件描述符impl Drop for DirectoryIterator { fn drop(mut self) { // SAFETY: self.dir is never NULL. if unsafe { ffi::closedir(self.dir) } ! 0 { panic!(Could not close {:?}, self.path); } } }5. 入口fn main() - Result(), String { let iter DirectoryIterator::new(.)?; println!(files: {:#?}, iter.collect::Vec_()); Ok(()) }测试与验证仓库 exercise.rs 内置了三组单元测试覆盖关键场景test_nonexisting_directory打开不存在的目录应返回Errtest_empty_directory空目录应只包含.和..test_nonempty_directory创建foo.txt、bar.png、crab.rs三个文件后迭代结果应精确包含.、..及这三个文件名。测试使用tempfilecrate 创建临时目录需要添加 dev-dependencycargo add --dev tempfile练习的设计要点回顾对照 solution.md练习刻意训练了四件事Safety Comments每个unsafe块前都有// SAFETY:注释解释为何安全这是便于审计的标准做法String conversions完整走一遍str→CString→*const c_char→CStr→[u8]→OsStr→OsString的转换链其中CString拥有数据类似StringCStr是借用视图类似strUnix 上必须依赖OsStrExt才能把字节直接构造成OsStrRAIIDrop通过实现Drop自动关闭目录句柄避免文件描述符泄漏Iterator 接口把 C 的游标式 API 包装成 Rust 惯用的Iteratornext返回OptionOsString对外提供安全、地道的接口。另外课程提示这类 FFI 绑定代码在生产中通常由 bindgen 之类的工具自动生成而非手工编写——手工实现只适合在线 playground 等无法运行工具的场景。总结本教程章节完整覆盖了 Unsafe Rust 的核心知识体系先厘清 Safe/Unsafe 的边界与unsafe 不等于错误的心智模型再逐一掌握五大能力裸指针解引用、可变静态变量、union、unsafe 函数、unsafe trait及其安全性论证方法最后通过目录读取 FFI 包装器实战把 unsafe 代码封装进安全的迭代器抽象中。贯穿始终的三条原则值得牢记unsafe 代码要小而隔离、每个 unsafe 块都要有 SAFETY 注释、永远用安全抽象包裹底层不安全操作。仓库内 unsafe-rust 章节 的全部源码示例dereferencing.md、mutable-static.md、unions.md、unsafe-traits.md、calling.md、extern-c.md以及练习源码 exercise.rs均可作为继续深入学习的参考。【免费下载链接】comprehensive-rustThis is the Rust course used by the Android team at Google. It provides you the material to quickly teach Rust.项目地址: https://gitcode.com/GitHub_Trending/co/comprehensive-rust创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考