关于 ffi-code-review
ffi-code-review 是专注于 Rust 外部函数接口的代码审查技能。它检查 crate 与构建配置、ABI 和类型布局、指针与所有权处理及回调,并要求完成验证关卡后再报告问题。
主要功能
- 01检查 Cargo.toml 与 build.rs
- 02核查 ABI 声明和符号导出
- 03审查 FFI 类型布局与字符串处理
- 04评估所有权、回调和 panic 安全
- 05通过验证关卡后报告问题
适用场景
- 01审查 Rust 到 C 库的绑定
- 02检查 FFI 更改中的内存与 ABI 风险
- 03评估构建脚本和生成的绑定
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ existential-birds/ beagle --skill ffi-code-review安装后在 Claude Code 中输入/ffi-code-review确认已加载。
附带文件references/safety-patterns.mdreferences/type-mapping.md
详细介绍
FFI 代码审查
审查工作流
- 检查 Cargo.toml —— 记录 Rust 版本(2024 版对 extern 块和 unsafe 属性有破坏性变更)、
build-dependencies(bindgen、cc、pkg-config)、crate-type(cdylib、staticlib)以及links键 - 检查 build.rs —— 验证链接指令(
cargo:rustc-link-lib、cargo:rustc-link-search)、bindgen 配置和 C 源代码编译 - 检查 extern 块 —— 验证调用约定、符号声明和安全注解
- 检查类型布局 —— 所有跨越 FFI 边界的类型都必须是
#[repr(C)]或原始 FFI 类型 - 检查字符串和指针处理 —— CStr/CString 的使用、空指针检查、所有权转移
- 检查回调 ——
extern "C" fn指针,以及 FFI 边界上的 panic 安全性 - 关卡 —— 在报告前完成下方的关卡;不要因为“内部验证”而跳过步骤
关卡
按顺序完成。每个问题都必须通过关卡 4后,才能报告发现项。
关卡 1 — Crate 上下文(磁盘上的文件)
通过条件: 打开已审查 crate 的 Cargo.toml(如适用,使用工作区成员路径),并记录 edition =,以及与此 FFI 相关的 links、crate-type 或 build-dependencies。
禁止合理化: 与版本相关的发现项(unsafe extern "C" {}、#[unsafe(no_mangle)] 等)需要满足此条件——如果 edition 不是 2024,就不要标记仅适用于 2024 版的要求。
关卡 2 — 链接和绑定来源
通过条件: 如果 crate 链接了原生代码或使用 bindgen/pkg-config,则打开 build.rs(或已检入代码库的绑定入口)。如果没有 build.rs,则说明绑定是手写的,并审查相应的 extern / include! 位置。
产物: 至少一个已打开文件的路径(例如 build.rs、src/ffi.rs 或通过 include! 引用的 OUT_DIR 绑定)。
关卡 3 — 代码证据
通过条件: 每项计划报告的发现都必须有对应的 [FILE:LINE],证据须来自完整阅读过的函数/代码块,而不只是差异代码块或部分代码片段。
关卡 4 — 报告前协议
通过条件: 已加载并应用 review-verification-protocol 技能,其中包括针对 repr(C)、安全注释、所有权/回调或大量使用 bindgen 的代码的FFI 专项验证。
输出格式
按以下格式报告发现项:
[FILE:LINE] ISSUE_TITLE
Severity: Critical | Major | Minor | Informational
Description of the issue and why it matters.
快速参考
| 问题类型 | 参考资料 |
|---|---|
| C 到 Rust 的类型映射、repr(C) 布局、枚举、不透明类型 | references/type-mapping.md |
| 安全封装、所有权转移、回调、build.rs、测试 | references/safety-patterns.md |
审查清单
extern 块和调用约定
- 外部函数声明使用
extern "C"(显式声明,而非裸extern) - 2024 版:
extern "C" {}块写作unsafe extern "C" {} - 暴露给 C 的函数使用
extern "C" fn(而非默认的 Rust 调用约定) - 调用约定与外部库匹配(Win32 API 使用
"C"、"system") -
#[link(name = "...")]指定正确的库名称 - 静态链接时使用
#[link(name = "...", kind = "static")]
符号管理
- 导出的函数使用
#[no_mangle]保留符号名称 - 2024 版:
#[no_mangle]写作#[unsafe(no_mangle)] - 2024 版:
#[export_name = "..."]写作#[unsafe(export_name = "...")] - Rust 名称与 C 符号不同时,使用
#[link_name = "..."] - 导出项为
pub(只有公开的#[no_mangle]符号会出现在库输出中)
类型布局
- 每个跨越 FFI 的结构体/联合体都使用
#[repr(C)]—— Rust 的默认布局未定义 - 基本类型使用
std::ffi/std::os::raw中的对应类型(c_int、c_char、c_void) - C 使用
int时,不要直接使用i32—— 应使用c_int(宽度因平台而异) - 对于
__be32等特殊 C 类型,应使用字节数组([u8; 4]),而非 Rust 整数 - 跨越 FFI 的枚举使用
#[repr(C)]或带显式判别值的#[repr(u8)]/#[repr(u32)] - C 风格的位标志枚举使用整数类型的新类型包装(或
bitflagscrate),而非 Rust 枚举 - 对应可能新增值的 C 枚举的 Rust 枚举上使用
#[non_exhaustive]
字符串处理
- C 字符串使用
CStr(借用)或CString(拥有),绝不使用&str或String - 检查
CString::new()的结果中是否包含内部空字节(遇到\0时会返回Err) - 由
CString派生的*const c_char指针通过.as_ptr()获取后,确保CString的生命周期长于该指针的使用 - 在
unsafe中使用CStr::from_ptr()验证传入的*const c_char - 不要假定 C 字符串是有效的 UTF-8 —— 使用会返回
Result的to_str() - OS 路径使用
OsStr/OsString和CStr,而非&str
所有权与内存分配
- 明确所有权约定:由谁分配、由谁释放
- Rust 分配的内存由 Rust 释放(
Box::from_raw),C 分配的内存由 C 释放 - 正确配对用于堆内存转移的
Box::into_raw/Box::from_raw - 将数组传给 C 时使用
Vec::into_raw_parts(指针 + 长度 + 容量) - 为每个传给 C 的不透明 Rust 类型提供析构函数
- 不要让
Drop作用于 C 分配的内存(反之亦然)
回调
- 回调类型使用
extern "C" fn(...),而非闭包或fn(...) - 回调使用
std::panic::catch_unwind,防止 panic 展开穿越 FFI - 回调上下文通过
*mut c_void传递,并在调用处安全地重建 - 对可空函数指针使用
Option<extern "C" fn(...)>(空值优化)
Bindgen 与构建脚本
- 检查 Bindgen 的输出是否正确(自动生成的类型可能需要调整)
- 使用
-syscrate 模式提供原始绑定,并使用单独的 crate 提供安全封装 - 在
build.rs中正确使用cargo:rustc-link-lib和cargo:rustc-link-search - 在
Cargo.toml中使用links键,避免同一原生库被重复链接 - 按构建目标生成平台专属绑定(而非只为单个平台生成后提交到代码库)
安全文档
- 每个
unsafe代码块都有解释不变量的// SAFETY:注释 - 每个公开的 FFI 封装函数都记录安全要求
- Edition 2024:
unsafe fn函数体在不安全操作周围使用显式的unsafe {}代码块
严重级别
严重(阻止合并)
- 跨越 FFI 边界的类型缺少
#[repr(C)](内存布局未定义) - 字符串处理错误:需要
CStr/CString时使用了&str/String - 所有权混淆:使用 Rust 分配器释放 C 分配的内存(或反之)
- 未使用
catch_unwind,导致 panic 展开穿越 FFI 边界 - 将 Rust 枚举用于 C 位标志(无效判别值会导致未定义行为)
- 需要
extern "C" fn指针时传入了闭包
主要(应修复)
unsafe代码块或公开 FFI 函数缺少安全文档- 解引用传入的
*const T/*mut T前未检查空指针 - C 使用指针之前
CString已被释放(悬空指针) - 缺少
#[link(name = "...")],导致某些平台上链接失败 - Edition 2024:
extern代码块未标记为unsafe extern - Edition 2024:
#[no_mangle]未包裹在#[unsafe(...)]中
次要(建议修复)
- C 的
int使用了i32而非c_int(大多数平台上正确,但不具可移植性) - 映射可扩展 C 枚举的枚举缺少
#[non_exhaustive] - 存在更易维护的 Bindgen 方案,却使用了冗长的手动绑定
- 已提交到代码库的绑定缺少平台保护
信息性
- 建议将原始绑定拆分到
-syscrate 中 - 建议为不同的
*mut c_void指针添加不透明包装类型 - 建议对可空指针使用
Option<NonNull<T>>
有效模式(请勿标记)
- edition 2024 中的
unsafe extern "C" {}—— 外部声明的正确形式 - edition 2024 中的
#[unsafe(no_mangle)]—— 符号导出的正确形式 - 用于可空回调的
Option<extern "C" fn(...)>—— 保证使用 niche 优化 - 用于可空指针的
Option<NonNull<T>>—— 零成本可空指针模式 - 用于不透明 C 类型的
*mut c_void—— 内部布局无关时的标准做法 - 用
c_void封装的不同空结构体,用于类型安全的不透明指针 —— 防止指针混淆 - 使用编译期字面量调用
CStr::from_bytes_with_nul_unchecked—— 已知字面量以空字符结尾时安全 - 使用
extern "C-unwind"进行受控展开 —— 符合 RFC 2945 - bindgen crate 中的
include!(concat!(env!("OUT_DIR"), "/bindings.rs"))—— 标准模式 - 用于所有权转移的
Box::into_raw/Box::from_raw配对 —— 配对使用时是正确模式
提交发现之前
报告任何问题之前,请按顺序完成 Gate 1-4;Gate 4 包含 review-verification-protocol 技能。