ARTICLE DETAIL

资讯详情

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

Comprehensive Rust 课程:深入理解 Unsafe Rust 函数——声明、调用与安全性契约

Comprehensive Rust 课程:深入理解 Unsafe Rust 函数——声明、调用与安全性契约 Comprehensive Rust 课程深入理解 Unsafe Rust 函数——声明、调用与安全性契约【免费下载链接】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在 Rust 中将函数标记为unsafe是对调用者发出的一份明确安全契约只有当特定的前置条件preconditions得到满足时调用该函数才不会触发未定义行为undefined behaviour。本文以 Google Android 团队维护的 Rust 课程Comprehensive Rust中 rust.md 为核心系统讲解如何声明自己的unsafe函数、如何书写# Safety文档、unsafe块与unsafe函数之间的关系以及 Rust 2024 edition 带来的关键变化。读完本文你将能够正确设计、文档化并安全调用自定义的 unsafe 函数并理解 unsafe 代码应当小而隔离、包裹在安全抽象层中这一核心理念。Unsafe Rust 与五种特权能力在深入 unsafe 函数之前先明确 unsafe 在 Rust 语言中的位置。如课程 unsafe.md 所述Rust 语言由两部分组成Safe Rust安全 Rust内存安全不可能发生未定义行为Unsafe Rust非安全 Rust如果前置条件被违反可能触发未定义行为。需要强调的是Unsafe Rust 并不意味着代码一定是错误的——它只是意味着开发者关闭了部分编译器安全特性必须自行保证代码正确编译器不再强制 Rust 的内存安全规则。Unsafe Rust 共开放五种新能力解引用裸指针dereference raw pointers访问或修改可变静态变量mutable static variables访问union的字段调用unsafe函数包括extern函数实现unsafetrait。其中调用 unsafe 函数正是本文的主题。课程同时强调了一条重要的工程准则unsafe 代码应当保持小而隔离small and isolated其正确性应被仔细文档化并包裹在一层安全的抽象safe abstraction之内。声明自定义 unsafe 函数rust.md 开篇即点明核心如果你的函数要求特定的前置条件以避免未定义行为就可以把它标记为unsafe。下面这段示例是课程的完整演示——用裸指针实现一个swap函数它要求调用者保证指针有效、对齐正确且期间无其他访问/// Swaps the values pointed to by the given pointers. /// /// # Safety /// /// The pointers must be valid, properly aligned, and not otherwise accessed for /// the duration of the function call. unsafe fn swap(a: *mut u8, b: *mut u8) { // SAFETY: Our caller promised that the pointers are valid, properly aligned // and have no other access. unsafe { let temp *a; *a *b; *b temp; } } fn main() { let mut a 42; let mut b 66; // SAFETY: The pointers must be valid, aligned and unique because they came // from references. unsafe { swap(mut a, mut b); } println!(a {}, b {}, a, b); }这个例子浓缩了编写 unsafe 函数的全部要点# Safety文档函数声明上方的 Rustdoc 注释中以# Safety为标题段落逐条写明调用者必须保证的前置条件。本例中的条件为指针必须有效valid、正确对齐properly aligned、且在函数调用期间不被其他方式访问。函数体内的// SAFETY:注释每个unsafe块内部都应附带一条安全注释说明为什么这段代码实际上是安全的。本例中函数体内再次引用调用者的承诺作为安全依据。调用侧再次注释main中的unsafe块同样附带// SAFETY:注释说明为什么调用swap是安全的——因为传入的指针来自mut a、mut b引用满足有效、对齐且唯一访问的要求。swap只是一个教学示例实际工程中并不会用裸指针实现交换——用引用references可以安全地完成同样的事情。它存在的意义是展示 unsafe 函数的最小完整形态声明、文档、调用三处各司其职。unsafe 函数与 unsafe 块两条独立边界理解 unsafe 函数与 unsafe 块的区别至关重要这也是 rust.md 在details部分着重强调的内容unsafe fn改变的是调用边界它声明调用者必须满足前置条件因此在外部调用该函数时调用处必须位于unsafe块或另一 unsafe 函数内。unsafe块改变的是函数体内的操作权限函数体内部若要进行裸指针解引用等 unsafe 操作需要自己的unsafe块。值得注意的版本差异是Rust 2021 及更早的 edition 允许在 unsafe 函数体内直接编写 unsafe 代码而无需unsafe块这一行为在2024 edition 中发生了改变——unsafe 函数体内的 unsafe 操作同样必须显式包裹在unsafe块中。如果希望在旧 edition 下强制这种显式写法可以添加 lint 属性#[deny(unsafe_op_in_unsafe_fn)]该属性会禁止在 unsafe 函数中隐式地执行 unsafe 操作。课程建议读者亲自尝试加上它观察编译器对示例代码的反应。这正是使 unsafe 代码审查变得可行的关键机制每一处 unsafe 操作都必须有独立的unsafe块进而必须附带独立的// SAFETY:注释。调用 unsafe 函数安全注释缺失的代价调用 unsafe 函数时未能满足安全要求会直接破坏内存安全。课程在 calling.md 中给出了一个精心设计的反例#[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()); }这个示例缺失安全注释且实际上是**不健全unsound**的原因层层递进参数语义陷阱slice::from_raw_parts的第二个参数是元素个数elements而非字节数PK_BYTE_LEN 8被当作 8 个u16元素传入导致切片越过了pk数组的末尾读到了sk数组的数据——打印结果会意外地混入私钥内容。未定义行为由于读取超出了指针来源数组的末尾这段代码构成未定义行为。设计缺陷log_public_key本应被声明为unsafe函数因为pk_ptr必须满足特定前置条件才能避免未定义行为。一个安全的函数却可能引发未定义行为就被称为 unsound不健全。作为练习课程请读者思考log_public_key的 Safety 文档应当写些什么由此引出三条实践建议标准库中存在大量底层 unsafe 函数在可能的情况下优先使用安全的替代方案每个unsafe块都必须附带安全注释说明其为何实际上是安全的如果以优化为目的使用 unsafe 函数务必配套基准测试benchmark以证明性能收益确实存在。两种 unsafe 函数来源课程 unsafe-functions.md 指出unsafe 函数可能来自两个地方Rust 自身声明为 unsafe 的函数即上一节讨论的自定义 unsafe 函数extern C块中的非安全外部函数FFI。第二种来源在 extern-c.md 中有详细展开。可以使用unsafe extern为 Rust 声明外部函数以供访问——这是不安全的因为编译器无法推理外部函数的行为。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())); } }关于extern块有几个值得记录的事实Rust 曾将所有 extern 函数一律视为 unsafe这一行为在Rust 1.82 引入unsafe extern块后发生变化函数声明现在必须显式区分safe与unsafe。abs必须显式标记为safe因为它不涉及指针操作而strlen处理指针并涉及NUL终止字符串的前置条件故标记为unsafe。调用外部函数的风险在于任何 C 函数在任意情形下都可能存在未定义行为尤其是涉及指针、可能违反 Rust 内存模型的操作。示例中的C是 ABI 标识Rust 还支持其他 ABI。编译器不会验证 Rust 函数签名与外部函数定义是否一致——这完全由开发者负责。这也是 FFI 绑定代码通常由 bindgen 等工具生成如课程 exercise.md 中Safe FFI Wrapper练习所示而非手工书写的原因之一。与 unsafe trait 的关系同一种契约思维理解了 unsafe 函数unsafe trait 便水到渠成如课程 unsafe-traits.md 所述与函数类似如果 trait 的实现必须保证特定条件以避免未定义行为就可以将 trait 标记为unsafe。zerocopycrate 的IntoBytes就是一个典型示例其声明同样带有# Safety文档段落如类型必须有确定的表示且无填充。Rust 内置的Send与Synctrait 也是 unsafe trait。这与 unsafe 函数的模式完全同构契约以# Safety文档的形式固化由实现者/调用者负责遵守。实践要点总结结合 rust.md 及课程相关章节编写和使用 unsafe 函数的最终检查清单如下是否真的需要 unsafe优先使用安全抽象swap用引用即可实现标准库的安全替代方案应作为首选。签名即契约只要函数存在调用者必须满足的前置条件就声明为unsafe fn安全函数可能引发未定义行为即属 unsound。# Safety文档必写在 Rustdoc 中逐条列出前置条件如指针有效、对齐、唯一访问、NUL 终止等。每个 unsafe 操作独立成块在 2024 edition 中函数体内的 unsafe 操作也必须显式包裹unsafe块旧 edition 可用#[deny(unsafe_op_in_unsafe_fn)]强制这一纪律。每条unsafe块附// SAFETY:注释说明此处为何安全而不是笼统地写这很安全。FFI 函数显式标记unsafe extern块内的函数按有无前置条件分别标记safe/unsafe并牢记编译器不校验签名一致性。用基准测试证明 unsafe 优化的收益避免为微小的性能收益引入巨大的安全性负担。Unsafe Rust 是 Rust 中关闭部分编译器保护的显式通道而 unsafe 函数正是这条通道上最常用的入口。正确掌握声明、文档化与调用这三个环节是写出既高效又健全sound的 unsafe 代码的前提。【免费下载链接】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),仅供参考
返回列表