ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Comprehensive Rust 实战:用 unsafe 与 FFI 构建类型安全的 libc 目录读取包装器

Comprehensive Rust 实战:用 unsafe 与 FFI 构建类型安全的 libc 目录读取包装器 Comprehensive Rust 实战用 unsafe 与 FFI 构建类型安全的 libc 目录读取包装器【免费下载链接】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本篇文章基于 Google Android 团队使用的 Rust 课程《Comprehensive Rust》中 unsafe-rust 部分的练习与参考答案src/unsafe-rust/exercise.md、src/unsafe-rust/solution.md、src/unsafe-rust/exercise.rs完整还原如何用 Rust 的 FFIForeign Function Interface能力为 C 语言的opendir/readdir/closedir三个 libc 函数手工编写绑定并在此基础上封装出一个内存安全、符合 Rust 惯用风格的DirectoryIterator迭代器。读完本文你将掌握FFI 绑定代码的组织方式、std::ffi中三组字符串类型str/String、CStr/CString、OsStr/OsString之间的完整转换链路、// SAFETY:注释的规范写法以及如何用DropRAII保证文件描述符不泄漏、如何用单元测试验证 FFI 包装器的正确性。练习背景为 libc 目录读取 API 构建安全外壳Rust 课程在讲解 unsafe 能力时强调一个核心设计原则不安全代码应当小而集中正确性应当被仔细记录并最终封装在安全抽象层中见 src/unsafe-rust/unsafe.md。本练习正是这一原则的完整落地底层是三个必须用unsafe extern C声明的 libc 函数中间是一个持有裸指针、需要手动管理生命周期的DirectoryIterator结构体外层则是Iteratortrait 与Drop对外只暴露next() - OptionOsString这样完全安全的接口。练习目标是把在 C 语言中读取目录文件名的过程——DIR *dir opendir(path); struct dirent *entry; while ((entry readdir(dir)) ! NULL) { /* 使用 entry-d_name */ } closedir(dir);——改写成一段安全、可复用、不会泄漏文件描述符、也不会因d_name缺少 NUL 终止符而崩溃的 Rust 代码。官方建议先查阅opendir(3)、readdir(3)、closedir(3)的手册页并浏览std::ffi模块中的字符串类型。三个字符串类型家族str、CStr、OsStr练习涉及的关键难点在于字符串表示形式的转换。C 的d_name是 NUL 结尾的字节数组OS 的文件名则可能是任意字节序列不一定是合法 UTF-8而 Rust 的String要求 UTF-8。课程用一张表概括了三组类型的差异见 src/unsafe-rust/exercise.md类型编码用途str/StringUTF-8Rust 内部文本处理CStr/CStringNUL 结尾与 C 函数通信OsStr/OsString平台相关OS-specific与操作系统通信三者的关系可以类比所有权模型CString拥有数据类似StringCStr是借用视图类似str。因此CStr::from_ptr解引用裸指针得到的CStr只是对 C 字符串的借用其生命周期由 unsafe 代码的约定来保证。FFI 字符串转换全链路从用户传入的str路径到readdir返回的d_name练习要求打通六步转换每一环都有明确目的str→CString为路径分配一块带有尾部\0的内存因为 C 函数期望 NUL 结尾字符串CString→*const c_char调用 C 函数时传入裸指针as_ptr()*const c_char→CStr需要一个能自动定位尾部\0的类型CStr::from_ptr会扫描到 NUL 为止CStr→[u8]字节切片是未知数据的通用接口to_bytes()去掉结尾的\0[u8]→OsStr在 Unix 上通过OsStrExt::from_bytes直接由字节构造OsStrOsStr→OsString必须克隆数据为拥有型因为下次调用readdir会覆盖缓冲区借用OsStr无法安全存活到返回值被消费之后。解决方案中的关键代码在 src/unsafe-rust/exercise.rs 的参考答案里这条链路分别出现在DirectoryIterator::new与Iterator::next中。路径入参的转换第 92-95 行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()) };注意CString::new返回Result如果输入的str内部含有\0转换会失败并返回错误而不是把截断后的字符串悄悄传给 C 函数——这正是安全包装器该有的边界处理。目录项名称的读取与转换第 111-120 行// SAFETY: self.dir is never NULL. let dirent unsafe { ffi::readdir(self.dir) }; if dirent.is_null() { // We have reached the end of the directory. 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())其中OsStrExt需要显式引入use std::os::unix::ffi::OsStrExt;。课程特别指出该 trait 在 Unix 系统上用于把字节直接转换为OsStr这也是文件系统路径可以包含非 UTF-8 字节的体现。手工编写 FFI 绑定extern C与平台差异参考答案用mod ffi隔离所有不安全的绑定代码其核心是一个unsafe extern C块src/unsafe-rust/exercise.rsunsafe 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; }不透明类型DIRDIR在 C 中是只应通过指针使用的不透明句柄Rust 侧无法也不应该知道其内部布局因此参考答案用零大小结构体来建模src/unsafe-rust/exercise.rs#[repr(C)] pub struct DIR { _data: [u8; 0], _marker: core::marker::PhantomData(*mut u8, core::marker::PhantomPinned), }PhantomData与PhantomPinned的组合让该类型不可Send/Sync、不可随意移动从类型层面阻止用户绕过封装直接构造或复制DIR。dirent的平台差异dirent的布局因平台而异参考答案用条件编译分别声明src/unsafe-rust/exercise.rsLinux非 macOS按readdir(3)手册与/usr/include/x86_64-linux-gnu/下sys/types.h、bits/typesizes.h的定义d_ino为c_ulong、d_off为c_long、d_reclen为c_ushort、d_type为c_uchard_name为 256 字节的[c_char; 256]macOS按dir(5)手册字段为d_fileno: u64、d_seekoff: u64、d_reclen: u16、d_namlen: u16、d_type: u8d_name为 1024 字节。macOS 上还有一个值得注意的历史细节Intel x86_64 平台需要借助#[link_name readdir$INODE64]链接到 64 位 inode 版本src/unsafe-rust/exercise.rs这与 libc 的_DARWIN_FEATURE_64_BIT_INODE兼容层有关是真实世界 FFI 经常被平台细节纠缠的生动例证。练习文档也提醒这种手工绑定通常只在教学或受限环境下出现实际工程中 FFI 绑定一般交给 bindgen 这类工具自动生成之所以手工编写是因为 bindgen 无法在在线 playground 中运行见 src/unsafe-rust/exercise.md 的补充说明。RAII用Drop保证closedir必定被调用裸指针*mut ffi::DIR不会自动释放如果忘记调用closedir就会泄漏文件描述符。参考答案为DirectoryIterator实现Dropsrc/unsafe-rust/exercise.rsimpl 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); } } }这正是 Rust RAII资源获取即初始化模式的直接体现无论迭代器在何处、以何种方式离开作用域正常结束、提前return、?传播错误、panic栈展开drop都会执行closedir都会被调用文件描述符不会泄漏。作为对比在 C 语言中每一处return和goto cleanup都需要开发者手动记得closedir。安全接口实现Iteratortrait外层接口把一次性的readdir循环封装为标准迭代器src/unsafe-rust/exercise.rsimpl 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; // We have reached the end of the directory. } // ... } }返回值设计为OptionOsString返回None表示目录读取完毕readdir返回空指针否则返回拥有所有权的OsString避免借用被下一次readdir调用覆盖。调用方因此可以安全地链式使用fn main() - Result(), String { let iter DirectoryIterator::new(.)?; println!(files: {:#?}, iter.collect::Vec_()); Ok(()) }// SAFETY:注释Android Rust 风格指南的要求参考答案强调每个unsafe块前面都必须有一段// SAFETY:注释说明该操作为何安全。这是 Android Rust 风格指南的硬性要求也是本课程贯穿始终的规范。练习中的三处注释分别是// SAFETY: path.as_ptr() cannot be NULL.opendir调用// SAFETY: self.dir is never NULL.readdir/closedir调用// SAFETY: dirent is not NULL and dirent.d_name is NUL terminated.CStr::from_ptr。关于指针有效性valid的完整判据可参见 src/unsafe-rust/dereferencing.md指针必须非空、可解引用位于单个已分配对象边界内、底层对象未被释放、不存在并发访问若指针来自引用转换则底层对象必须存活且不能有其他引用访问该内存。课程还指出了两种常见的错误示范见 src/unsafe-rust/unsafe-functions/calling.md缺少 SAFETY 注释且不健全log_public_key用slice::from_raw_parts(pk_ptr, PK_BYTE_LEN)从 8 字节的u16数组上构造出 8 个u16元素的切片第二个参数是元素个数而非字节数会越界读到相邻的sk字段属于未定义行为安全函数不该引发 UB一个能触发未定义行为的安全函数被称作不健全unsound应当把log_public_key声明为unsafe fn并在文档中写明前置条件。此外Rust 2024 版本起unsafe 函数体内的 unsafe 操作必须显式包裹unsafe块在更早版本上可用#[deny(unsafe_op_in_unsafe_fn)]强制这一行为见 src/unsafe-rust/unsafe-functions/rust.md。单元测试验证包装器的正确性参考答案使用tempfilecrate 的TempDir在临时目录中构造测试场景因此需要先把tempfile添加为开发依赖cargo add --dev tempfilesrc/unsafe-rust/exercise.rs 中提供了三个测试分别覆盖不同场景test_nonexisting_directory对不存在的目录调用DirectoryIterator::new应返回Erropendir返回空指针被正确转化为错误test_empty_directory空目录迭代结果应为[., ..]排序后比较验证readdir的终止条件处理正确test_nonempty_directory向临时目录写入foo.txt、bar.png、crab.rs三个文件后迭代结果应包含这三个文件名与.、..。测试还顺带验证了一个隐蔽的正确性点TempDir的路径是Path需要先经to_str()转换为str才能传给new而to_str()在路径含非 UTF-8 字节时返回None因此测试用ok_or(Non UTF-8 character in path)?处理这种边界情况。完整解决方案把上述所有部分组合起来即得到完整、可运行的解决方案源码位于 src/unsafe-rust/exercise.rsmod ffi { use std::os::raw::{c_char, c_int}; #[cfg(not(target_os macos))] use std::os::raw::{c_long, c_uchar, c_ulong, c_ushort}; #[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], } #[cfg(target_os macos)] #[repr(C)] pub struct dirent { pub d_fileno: u64, pub d_seekoff: u64, pub d_reclen: u16, pub d_namlen: u16, pub d_type: u8, pub d_name: [c_char; 1024], } unsafe extern C { pub unsafe fn opendir(s: *const c_char) - *mut DIR; #[cfg(not(all(target_os macos, target_arch x86_64)))] pub unsafe fn readdir(s: *mut DIR) - *const dirent; #[cfg(all(target_os macos, target_arch x86_64))] #[link_name readdir$INODE64] pub unsafe fn readdir(s: *mut DIR) - *const dirent; pub unsafe fn closedir(s: *mut DIR) - c_int; } } use std::ffi::{CStr, CString, OsStr, OsString}; use std::os::unix::ffi::OsStrExt; #[derive(Debug)] struct DirectoryIterator { path: CString, dir: *mut ffi::DIR, } impl 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 }) } } } impl 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()) } } 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); } } } fn main() - Result(), String { let iter DirectoryIterator::new(.)?; println!(files: {:#?}, iter.collect::Vec_()); Ok(()) }可以在本地项目执行cargo add --dev tempfile cargo test运行上述三个单元测试或在 Rust Playground 中粘贴练习框架自行补全todo!()部分。课程为这个练习预留了 30 分钟适合作为理解 unsafe、FFI、RAII 三者的综合训练。核心要点回顾封装边界所有extern C声明集中在mod ffi裸指针只存在于DirectoryIterator内部外部 API 完全安全字符串转换六步链路str → CString → *const c_char → CStr → [u8] → OsStr → OsString是路径入、文件名出的完整闭环每一步都有明确理由RAII 资源管理Drop实现closedir让文件描述符的生命周期与迭代器对象绑定杜绝泄漏文档化安全每个unsafe块配// SAFETY:注释说明指针非空、NUL 终止等前置条件为何满足可测试性用tempfile构造临时目录三个单元测试分别覆盖错误路径、空目录与正常目录验证包装器的正确性。这篇练习所展示的不安全代码收缩在底层、对外暴露安全惯用接口的封装手法在 Rust 生态中极具代表性——从标准库到各类系统级 crate都是同一套思路。更深入的理论背景可继续阅读课程同目录下的 src/unsafe-rust/dereferencing.md、src/unsafe-rust/unsafe-functions/calling.md 等小节。【免费下载链接】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),仅供参考
返回列表