news 2026/9/11 5:53:11

Comprehensive Rust 教程:深入理解 Unsafe Functions(安全前提、调用规范与 FFI 声明)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Comprehensive Rust 教程:深入理解 Unsafe Functions(安全前提、调用规范与 FFI 声明)

Comprehensive Rust 教程:深入理解 Unsafe Functions(安全前提、调用规范与 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

本篇技术指南围绕 Google Android 团队维护的 Rust 课程(Comprehensive Rust)中Unsafe Functions(不安全函数)一讲展开,系统讲解:为什么函数需要被标记为unsafe、Rust 不安全函数与extern "C"外部函数这两类不安全函数的区别、调用时必须满足的前提条件(preconditions)、安全注释(Safety Comment)的写法,以及 Rust 2024 edition 与 Rust 1.82 之后相关语法的变化。读完本文,你将掌握安全地声明、调用和封装 unsafe 函数的方法,并能在真实项目中用安全抽象层包裹底层 FFI 调用。

Unsafe Functions 是什么:两类不安全函数的来源

在正式进入 unsafe 函数的细节之前,先回到课程对Unsafe Rust的整体定位。unsafe.md 明确指出,Rust 语言由两部分构成:

  • Safe Rust(安全 Rust):内存安全,不可能产生未定义行为(undefined behaviour, UB);
  • Unsafe Rust(不安全 Rust):一旦违反前提条件,就可能触发未定义行为。

Unsafe Rust 并非"代码写错了",而是开发者主动关闭了部分编译器安全检查,需要自己保证正确性。它一共解锁了5 种新能力

  1. 解引用裸指针(dereference raw pointers);
  2. 访问或修改可变静态变量(mutable static variables);
  3. 访问union字段;
  4. 调用unsafe函数,包括extern外部函数;
  5. 实现unsafetrait。

本文聚焦其中的第 4 项——调用不安全函数。按照 unsafe-functions.md 的定义:

如果一个函数或方法带有额外的、必须由调用方维护的前提条件(preconditions)以避免未定义行为,就可以把它标记为unsafe

不安全函数可能来自两个地方:

  • Rust 自身声明的unsafe函数:代码由你(或某个 crate)编写,编译器无法替你验证前提条件;
  • extern "C"块中声明的外部函数:来自 C/C++ 等语言的符号,编译器完全没有办法推断其行为。

课程接下来分别讨论了这两个来源,下文依次展开。

调用不安全函数:前提条件一旦失守就是 UB

调用不安全函数(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。课程在这里埋了三个关键的坑:

  1. 第二个参数是"元素个数"而非"字节数"std::slice::from_raw_parts(ptr, len)lenu16元素的个数,而不是字节长度。这里PK_BYTE_LEN = 8被当作元素个数传入,切片会越过pk数组的末尾,一路读进相邻的sk数组——在示例中你会看到私钥数据被意外打印出来。

  2. 这属于未定义行为。因为我们在读取"指针所源自对象(pk数组)边界之外"的内存,编译器不会为这种越界读取提供任何保证。

  3. log_public_key本身应该被声明为unsafe。它的参数pk_ptr必须满足一系列前提条件(非空、指向合法对象、对象存活且未被并发访问等)才能避免 UB。一个可以被安全调用却导致未定义行为的函数,被称为unsound(不健全)的。课程建议思考:这个函数的# Safety文档应该怎么写?

安全注释:每个 unsafe 块都必须有

无论代码是否正确,课程(以及 Android Rust 风格指南)都要求:每个unsafe块都要附带一条安全注释,解释为什么这段代码实际上是安全的。上面这个示例恰恰缺少安全注释,因此课程明确把它判定为 unsound。

在 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); } }

对于"裸指针解引用"这一类操作,注释需要覆盖的前提条件(与标准库ptr模块的 [Safety] 要求一致)包括:

  • 指针必须非空(non-null);
  • 指针必须可解引用(dereferenceable,位于单个已分配对象的边界内);
  • 底层对象不得已被释放(deallocated);
  • 同一位置不得存在并发访问;
  • 若指针由引用转换而来,底层对象必须存活,且不能有引用被用于访问该内存;
  • 大多数情况下指针还必须正确对齐(properly aligned)。

课程还专门演示了一种常见的 UB 写法(unsound 的反面教材):把&*p1直接当作引用使用——借由裸指针创建引用会绕过编译器对"引用到底指向哪个对象"的认知,借用检查器因此不会冻结x,即使存在指向它的引用,x仍可能被修改,从而触发 UB。从指针创建引用必须格外小心

为什么课程推荐优先使用安全替代品

标准库中有一批底层 unsafe 函数(如slice::from_raw_partsptr::readmem::transmute等)。课程给出的建议是:

  • 尽可能优先使用安全替代品(如用&key_pair.pk直接切片,而不是from_raw_parts);
  • 如果为了性能优化而使用 unsafe 函数,务必配套编写基准测试(benchmark)来证明优化收益,而不是"感觉更快"。

声明自己的 unsafe 函数:以swap为例

Unsafe Rust 函数(rust.md) 说明:你可以把自己的函数标记为unsafe,只要它要求调用方满足特定前提条件以避免 UB。课程用经典的指针交换函数演示:

/// 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); }

这个例子有两点值得深挖:

文档与代码的双层安全契约

  • 在函数文档中# Safety小节向调用方声明前提条件——"两个指针必须有效、正确对齐,并且在函数调用期间不被其他方式访问"。
  • 在函数体内:每个unsafe块都要有SAFETY:注释,说明此处假设调用方已经履行了承诺。

两层注释互相呼应,构成了 unsafe 函数完整的"契约文档"。这也呼应了 unsafe-traits.md 中 unsafe trait 的写法——zerocopyIntoBytes之类的 trait 同样要求在 Rustdoc 中提供# Safety小节。

Edition 差异:unsafe_op_in_unsafe_fn

课程特别指出一个重要语法演进:

  • Rust 2021 及更早版本:在unsafe fn函数体内使用 unsafe 操作不需要再包一层unsafe
  • Rust 2024 edition:在unsafe fn内部执行 unsafe 操作也必须显式写出unsafe

对于老版本项目,可以用 lint 强制要求显式 unsafe 块:

#[deny(unsafe_op_in_unsafe_fn)]

课程建议读者亲自加上这个属性试一下,观察编译器报错——这正是本仓库 src/unsafe-rust/Cargo.toml 使用edition = "2024"的背景下,现代 unsafe 代码的标配写法。

一个教学层面的提醒

课程同时提醒:真实的swap根本不需要指针,用引用就可以安全完成。这个例子纯粹是为了演示unsafe fn的声明、文档与调用机制——能用安全代码解决的,不要为了炫技引入 unsafe

Unsafe 外部函数:extern "C"块与safe fn

Unsafe 外部函数(extern-c.md) 讲解第二类不安全函数:通过unsafe extern声明外部(foreign)函数。之所以需要 unsafe,是因为编译器无法推断外部函数的行为。课程示例同时展示了safe fnunsafe fn两种声明:

use std::ffi::c_char; unsafe extern "C" { // `abs` doesn't deal with pointers and doesn't 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(c"String".as_ptr())); } }

课程在这段代码的讲解(details)中给出四点关键知识:

  1. 历史演变:Rust 曾经把所有 extern 函数一律视为 unsafe;Rust 1.82 引入unsafe extern之后,extern 块中的每个函数必须显式标记为safeunsafe,取决于它是否带有安全使用的前提条件。

  2. abs为什么必须写safe:因为它是外部(FFI)函数,默认继承块级的不安全属性;而像abs这样不碰指针、没有任何安全要求的函数,可以(也应该)显式标记为safe,从而允许在安全代码中直接调用。需要注意的是:任何 C 函数都可能在任意情况下出现未定义行为,所以"该函数是否安全"需要逐个函数判断,不能想当然。

  3. "C"是 ABI 名称:本示例使用的是 C ABI;Rust 参考手册(Reference)的 external blocks 章节列出了其他可用的 ABI(如"system""stdcall"等)。

  4. 签名匹配全靠自觉:编译器不会校验 Rust 侧声明的函数签名与外部真实定义是否一致——这是调用方必须自己负责的约束,一旦签名对不上,就是未定义行为。

实战案例:逐步封装abs(3)

abs.md 提供了封装 C 标准库abs(3)的完整演练,正好把上面语法点串成一条可操作的路径,其核心步骤是:

  1. 查外部定义:找到目标函数的真实 C 签名——int abs(int j);(可参考man 3 abs);
  2. 写出匹配的 extern 声明
  3. 确认安全不变量abs只接收和返回i32,不涉及指针,无安全前提;
  4. 决定能否标记为 safe

过程中的关键细节:

  • 许多 POSIX 函数之所以可直接调用,是因为Cargo 默认链接 C 标准库(libc),其符号天然在程序作用域内;
  • 签名应使用 C 类型别名std::ffi::c_int而不是硬编码i32:C 标准规定int可能是i16c_int由目标平台决定宽度,使用别名能提高可移植性(在主流平台上它通常就是i32的类型别名);
  • 早期写法extern "C"会被编译器报错"extern blocks must be unsafe",需要把块升级为unsafe extern "C"
  • 块写为 unsafe 后,函数默认是 unsafe 的;只有当确认无前提条件时,才在函数上追加safe fn,让它能在安全代码中直接调用。

最终完整程序如下:

use std::ffi::c_int; unsafe extern "C" { safe fn abs(x: c_int) -> c_int; } fn main() { let x = -42; let abs_x = abs(x); println!("{x}, {abs_x}"); }

课堂实战:用 Safe FFI Wrapper 把不安全函数封装成安全迭代器

课程在 exercise.md 中提供了一个 30 分钟的实战练习,把"声明 unsafe extern 函数 → 提供安全抽象"的全流程走一遍:为libc的目录读取函数opendir(3)readdir(3)closedir(3)编写一个安全封装,实现一个可以迭代目录条目名的DirectoryIterator

练习涉及 FFI 中最核心的一个环节——字符串类型转换。课程给出了对照表:

TypesEncodingUse
strStringUTF-8Rust 内的文本处理
CStrCStringNUL 结尾与 C 函数通信
OsStrOsString操作系统相关与操作系统通信

需要在上述类型之间完成一系列转换,每一步都有明确目的:

  • &strCString:需要为结尾的\0分配空间;
  • CString*const c_char:得到可传给 C 函数的指针;
  • *const c_char&CStr:借以找到结尾的\0
  • &CStr&[u8]:字节切片是"未知数据"的通用接口;
  • &[u8]&OsStr:借助OsStrExt创建,向OsString过渡;
  • &OsStrOsString:克隆数据,因为下一次readdir调用会复用缓冲。

仓库中的参考实现 exercise.rs 展示了这个安全封装在源码层面的完整形态,几个值得对照学习的要点:

extern 块声明(ANCHOR: ffi

mod ffi { use std::os::raw::{c_char, c_int}; // ... // Opaque type. See https://doc.rust-lang.org/nomicon/ffi.html. #[repr(C)] pub struct DIR { _data: [u8; 0], _marker: core::marker::PhantomData<(*mut u8, core::marker::PhantomPinned)>, } // Layout according to the Linux man page for readdir(3) ... #[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; } }

注意其中的工程细节:

  • DIR不透明类型(opaque type),Rust 侧只需要知道它是一个指针大小的句柄,内部布局不对外暴露;
  • dirent结构体必须用#[repr(C)]并按readdir(3)手册的内存布局逐字段复刻,字段类型随平台而定(源码中针对 Linux 与 macOS 分别定义了布局,macOS x86_64 还通过#[link_name = "readdir$INODE64"]处理了_DARWIN_FEATURE_64_BIT_INODE的符号名差异);
  • 平台相关的 FFI 声明本身就是 unsafe 函数"前提条件随目标平台变化"的典型例证。

用安全注释逐点交代前提条件

每个unsafe调用点都配有精确的SAFETY:注释,例如:

// SAFETY: path.as_ptr() cannot be NULL. let dir = unsafe { ffi::opendir(path.as_ptr()) }; // SAFETY: self.dir is never NULL. let dirent = unsafe { ffi::readdir(self.dir) }; // SAFETY: dirent is not NULL and dirent.d_name is NUL terminated. let d_name = unsafe { CStr::from_ptr((*dirent).d_name.as_ptr()) };
  • CString::new保证生成的缓冲区以\0结尾,因此as_ptr()非空;
  • DirectoryIterator的不变量是dir指针永不为 NULL(构造失败时返回Err,成功时才持有该指针);
  • d_name以 NUL 结尾是dirent的内存布局与readdir(3)契约共同保证的。

用 RAII 收尾:Drop 里关闭句柄

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); } } }

Drop中调用closedir,把"释放目录句柄"的职责绑定到类型生命周期上——即使迭代中途 panic,句柄也不会泄漏。这正是课程在 unsafe.md 中强调的总体原则的落地:

Unsafe 代码应当小而隔离,正确性要仔细记录,并用安全抽象层包裹。

最后,课程提醒:真实的 FFI 绑定通常由bindgen这类工具自动生成,而不是手写;本例手写是为了在在线 playground 中教学演示。

配套测试与运行方式

参考实现还附带了三组单元测试(见 exercise.rs 中的mod tests),用于验证安全封装的正确性:

  • test_nonexisting_directory:不存在的目录应返回Err
  • test_empty_directory:空目录迭代结果应为["." , ".."]
  • test_nonempty_directory:写入foo.txtbar.pngcrab.rs后,迭代结果应包含全部条目。

该测试使用了tempfilecrate(声明于 Cargo.toml 的[dev-dependencies],版本 3.27.0),并通过[[bin]]exercise.rs注册为名为listdir的可执行程序。你可以在仓库中按常规方式运行与验证:

cargo run --bin listdir # 在 src/unsafe-rust 下运行,列出当前目录 cargo test # 运行三组 FFI 封装测试

小结

回到课程主线:unsafe 函数只是"把前提条件的责任移交给你",而不是"随便写的代码"。本讲的核心结论可以浓缩为四点:

  1. 两类来源:Rust 自身声明的unsafe fn,以及unsafe extern块中声明的外部函数;
  2. 契约精神:文档用# Safety小节写明前提条件,代码用SAFETY:注释解释每个 unsafe 块为何安全;缺少安全注释、可由安全代码触发 UB 的函数是unsound的;
  3. 语法演进:Rust 2024 edition 要求在unsafe fn内显式写unsafe块(可用#[deny(unsafe_op_in_unsafe_fn)]在旧版本强制);Rust 1.82 起 extern 块必须是unsafe extern "C",其中无前提条件的函数可标记为safe fn
  4. 工程实践:unsafe 代码应小而隔离、包在安全抽象层里(如DirectoryIterator用 RAII 封装opendir/readdir/closedir),优先使用标准库安全替代品,优化型 unsafe 要有基准测试支撑。

如果你想继续深入,同一课程的后续内容还覆盖了解引用裸指针(dereferencing.md)、可变静态变量(mutable-static.md)、union字段访问(unions.md)以及 unsafe trait(unsafe-traits.md)等其余四种 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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/11 5:52:57

SpringBoot与EasyPOI实现高效Word合同导出

1. 项目背景与需求解析在企业级应用开发中&#xff0c;合同文档的自动化生成与导出是高频需求场景。以某电商平台的供应商合作为例&#xff0c;技术团队每月需要处理3000份格式统一的合同文档。传统手动复制粘贴方式不仅效率低下&#xff08;单份合同平均耗时15分钟&#xff09…

作者头像 李华
网站建设 2026/9/11 5:51:41

国产分布式数据库选型的四大硬指标

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/11 5:49:44

BiLSTM轴承故障诊断:Matlab完整源码与参数调优实战指南

简介&#xff1a;双向长短期记忆神经网络的故障诊断与分类预测完整源码&#xff0c;面向机械故障诊断、轴承状态监测领域的研究者与工程师。数据采用西储大学轴承诊断数据经特征提取后的样本&#xff0c;基于Matlab2023环境构建&#xff0c;涵盖数据导入、BiLSTM网络搭建、训练…

作者头像 李华
网站建设 2026/9/11 5:48:07

物联网云平台低代码开发工具优缺点全解析:好用吗?一文读懂

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华