关于 rust-unsafe-ffi
rust-unsafe-ffi 面向处理 unsafe 代码、原始内存和 C ABI 边界的 Rust 开发者,强调明确安全契约、缩小 unsafe 范围、理清所有权并验证实现。
主要功能
- 01论证并隔离 unsafe 操作
- 02记录安全契约
- 03谨慎处理原始内存
- 04明确布局、固定与 ABI
- 05定义狭窄的 FFI 边界
- 06使用 Miri 和平台测试验证
适用场景
- 01审查原始指针代码
- 02构建或审计 C ABI 绑定
- 03检查 FFI 边界两侧的所有权
- 04测试 unsafe 内存不变量
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ full-stack-skills/ rust-skills --skill rust-unsafe-ffi安装后在 Claude Code 中输入/rust-unsafe-ffi确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
agents/openai.yamlexamples/examples.mdexamples/golden-unsafe/Cargo.tomlexamples/golden-unsafe/src/lib.rsreferences/references.md详细介绍
Rust 不安全代码与 FFI
将 unsafe 视为一项证明义务。不安全块允许执行特定操作;它不会放宽有效性、别名、初始化、生命周期、数据竞争、布局或 ABI 要求。
范围与路由
在处理原始指针、NonNull、不安全函数和 trait、联合体、MaybeUninit、ManuallyDrop、布局、固定、分配器、手动实现 Send 或 Sync、C ABI 绑定、回调、句柄和所有权转移时,使用此技能。
普通所有权设计转交给 rust-stable,并发架构转交给 rust-concurrency,UniFFI 生成的多语言绑定转交给 rust-uniffi-building,通用构建脚本转交给 rust-cargo-build,审查报告转交给 rust-code-review。
工作流
1. 说明理由并隔离不安全操作
明确指出安全 Rust 无法表达的操作。如果经审计的 crate 或标准库抽象能够保留所需行为,优先使用它。将不安全块保持精简,并启用:
#![deny(unsafe_op_in_unsafe_fn)]
unsafe fn 仍必须将每个不安全操作放在显式的 unsafe {} 块中。只有在安全封装能够在内部建立并维持所有不变式时,才应提供这种封装。
2. 先编写安全契约,再编写代码
记录:
- 指针来源、非空性、对齐方式以及可解引用的字节范围;
- 被指向类型的初始化和有效性要求;
- 整个访问期间的别名和修改规则;
- 生命周期和所有权转移,包括由谁销毁或释放值;
- 线程安全和可重入要求;
- ABI、布局、调用约定、整数宽度和错误约定;
- 边界上的 panic 或外部异常行为。
对不安全的公共 API 使用 # Safety 文档,并在证明所在位置添加 // SAFETY: 注释。
3. 保守地实现原始内存操作
pub unsafe fn read_i32(ptr: *const i32) -> i32 {
// SAFETY: The caller guarantees that ptr is aligned, initialized,
// dereferenceable for one i32, and not concurrently mutated.
unsafe { ptr.read() }
}
- 仅当
ptr.add的操作范围位于同一已分配对象内或恰好指向其后一位时,才使用它。 - 区分对齐读取和未对齐读取;绝不为未对齐的打包字段创建引用。
- 当值可能尚未初始化时,使用
MaybeUninit<T>,并准确跟踪在析构或调用assume_init前哪些元素已初始化。 - 相比
transmute,优先使用from_ne_bytes、带布局检查的指针转换或显式字段转换。 - 不要用
ManuallyDrop掩盖所有权歧义或重复析构风险。
4. 精确定义布局和固定
- 对 C 兼容的结构体布局使用
#[repr(C)],对共享枚举使用显式 repr。 - 只有在满足其文档规定的字段限制时,才使用
#[repr(transparent)]。 - 将填充字节视为可能未初始化;不要通过复制结构体的原始字节来序列化它。
Pin<P>仅在固定契约下保护被指向对象。它不会使普通的Unpin数据无法移动,也不会自动保证自引用的安全性。- 优先使用成熟的投影辅助工具或结构化固定模式,而不是原始自引用指针。
5. 构建精简的 FFI 边界
Edition 2024 要求在适用情况下使用不安全 extern 块和不安全属性:
use std::ffi::{c_char, CStr};
unsafe extern "C" {
fn foreign_name() -> *const c_char;
}
pub fn name() -> Option<String> {
// SAFETY: The foreign contract promises either null or a valid,
// NUL-terminated string that remains alive for this call.
let ptr = unsafe { foreign_name() };
(!ptr.is_null()).then(|| {
// SAFETY: Non-nullness and termination follow from the contract above.
unsafe { CStr::from_ptr(ptr) }.to_string_lossy().into_owned()
})
}
- 使用
std::ffi中的 C 类型或生成的绑定,不要假定 Rust 整数的宽度。 - 在单一边界处转换可空指针、长度、所有权和错误代码。
- 除非所选 ABI 明确允许,否则不要让展开越过 C ABI;必要时在回调中捕获 panic。
- 明确规定回调的生命周期、线程、取消和注销行为。
- 在同一分配器域中配对分配与释放操作。
6. 验证证明
运行适用的检查:
cargo fmt --all --check
cargo check --workspace --all-targets --all-features
cargo test --workspace --all-targets --all-features
cargo clippy --workspace --all-targets --all-features -- -D warnings
cargo miri test
还应在支持的环境中使用 Sanitizer 或 Valgrind、ABI 布局断言、C 侧集成测试、多个优化级别,以及每种受支持的操作系统和架构。Miri 不会执行任意外部代码,因此测试 Rust 侧内存契约时,应隔离或模拟外部调用。
阅读 不安全代码与 FFI 参考,了解内存类型的详细信息。阅读 执行场景,了解具有代表性的边界。
审查清单
unsafe是否必要、范围最小,并且有局部依据?- 每项安全契约是否涵盖有效性、别名、生命周期、布局和并发?
- 安全调用方能否违反封装层的假设?
- 部分初始化和失败清理是否正确?
- FFI 两侧的所有权和释放是否对称?
panic或外部展开是否可能越过 ABI 边界?- 是否已证明手动实现的
Send和Sync对每个字段和回调都安全? - 测试是否覆盖空指针、空值、最大值、未对齐、错误、回调和关闭路径?
完成标准
- 记录每项调用方和实现方的安全义务。
- 将
unsafe块保持在最小范围,并确保安全封装不可能被安全 Rust 代码误用。 - 验证 ABI 类型、布局、所有权、错误和展开行为。
- 在适用情况下运行 Miri,并针对外部边界运行真实平台测试。
- 记录任何无法在本地验证的属性。
上游来源
数据隐私
此技能不会收集、存储或传输用户数据。未经授权,不要将专有头文件、生成的绑定、崩溃转储或内存内容发送到外部服务。