Rust unsafe 审计清单:把风险控制在可解释范围

背景 高性能或底层场景里,Rust 项目经常需要少量 unsafe。问题不在于有没有 unsafe,而在于是否可审计。 审计清单 每个 unsafe block 有明确不变量说明 边界输入做前置校验 单测覆盖成功与失败路径 关键模块做 fuzz 或 Miri 检查 // SAFETY: ptr 来自有效切片起始地址,len 已做边界校验。 unsafe { std::ptr::copy_nonoverlapping(src.as_ptr(), dst.as_mut_ptr(), len); } 总结 unsafe 管理的核心是制度化约束,不是个人经验。 能解释安全前提的 unsafe,才是工程可接受的 unsafe。

2026年5月7日 · 1 分钟 · BvBeJ

Rust Async 的 Cancellation Safety:避免半提交状态

关键事实 Future 在 .await 点可能被取消。若状态更新分布在多个 await 之间,就可能出现“写了一半”的业务状态。 设计原则 把副作用集中在单一提交点。 在可取消区间只做纯计算或幂等准备。 对外部系统写入使用幂等键。 反例与修正 // 反例:先扣库存再写订单,两个 await 中间可被取消 reserve_stock().await?; create_order().await?; // 修正:准备阶段无副作用,最后一次性提交 let plan = build_plan().await?; commit(plan).await?; 工程策略 为关键流程增加“中断注入测试”。 对每个 await 标注取消后的状态语义。 引入补偿任务清理孤儿状态。 小结 异步取消是默认行为,不是异常路径。把 cancellation safety 当作接口契约的一部分,才能避免线上出现“偶发且不可复现”的脏状态。

2026年5月5日 · 1 分钟 · BvBeJ

Rust async trait 性能取舍:泛型与动态分发

背景 async_trait 大幅提升了工程可读性,但在高频路径里也可能引入额外分配和动态分发开销。 选择建议 热路径优先泛型静态分发 插件式扩展再考虑 trait object 先 benchmark 再下结论 pub trait Storage { fn get<'a>(&'a self, key: &'a str) -> Pin<Box<dyn Future<Output = Option<String>> + Send + 'a>>; } 总结 抽象不是免费的,但可维护性也有价值,关键是按热点分层。 架构取舍从来不是二选一,而是按场景分配复杂度。

2026年5月4日 · 1 分钟 · BvBeJ

Rust Tokio 取消安全:避免半完成状态

背景 Tokio 里 select! 和超时很常用,但取消发生在任意 await 点,任务可能停在中间状态。 实践建议 把副作用操作放在不可分割阶段 写操作尽量幂等 关键路径加补偿或重试机制 tokio::select! { _ = shutdown.recv() => { tracing::info!("cancelled"); } res = do_commit_work() => { res?; } } 总结 取消安全本质是状态机设计,不是语法问题。 异步代码能停下来不难,停得干净才难。

2026年5月1日 · 1 分钟 · BvBeJ

Rust 与 C++ FFI:边界安全和所有权约定

背景 Rust 接入存量 C++ 代码是很多团队都会走的一步。 难点通常不在 extern "C",而在这些边界问题: 谁创建谁释放 错误如何跨边界传递 线程模型是否一致 基本原则 FFI 边界尽量窄 数据结构扁平、可序列化 所有权规则在接口文档里写死 #[no_mangle] pub extern "C" fn sum(a: i32, b: i32) -> i32 { a + b } extern "C" int32_t sum(int32_t a, int32_t b); 总结 FFI 能带来渐进迁移收益,但边界规范必须比普通模块更严格。 跨语言最怕“默认约定”,最好全部显式化。 边界是系统最脆弱的地方,跨语言边界更是。

2026年4月30日 · 1 分钟 · BvBeJ

Rust 无锁结构中的内存回收:Epoch 与 Hazard Pointer 对比

先澄清核心矛盾 无锁链表里,线程 A 可能刚把节点从链表摘下,线程 B 还在读取它。此时直接 drop 会造成悬垂引用。 两类主流方案 Epoch Based Reclamation(EBR) 线程进入临界区时 pin 当前 epoch。 删除节点先放入 retired 列表。 只有当所有线程都跨过该 epoch,节点才可释放。 Hazard Pointer(HP) 读取前先声明“我正在看这个指针”。 回收线程扫描所有 hazard slot。 未被保护的 retired 节点才能释放。 EBR 的工程特性 吞吐通常更高,读路径开销小。 线程暂停会拖慢全局回收进度。 更适合短临界区、线程活跃的服务。 HP 的工程特性 回收更精细,不容易被慢线程拖住。 读路径需要发布 hazard,CPU 开销更高。 更适合线程生命周期不可控的系统。 Rust 落地建议 // 伪代码:删除节点不立即释放,而是 retire fn remove(node: Shared<Node>, guard: &Guard) { if cas_unlink(node, guard) { guard.defer_destroy(node); // 交给回收器延迟释放 } } 使用经过验证的库(如 crossbeam)而不是手搓回收器。 把“最大 retired 数量”做成指标,防止隐性内存膨胀。 压测时加入 SIGSTOP 慢线程场景,验证回收鲁棒性。 小结 锁不一定是瓶颈,错误回收一定是灾难。无锁结构上线前,先证明回收策略在“慢线程、抖动、长尾”下仍可控。

2026年4月28日 · 1 分钟 · BvBeJ

Rust Axum 中间件分层:认证、限流、追踪怎么排

背景 Axum 用起来很顺手,但真实项目里经常出现一个隐性问题: 中间件越加越多 顺序靠经验调整 出故障时不知道是哪个层拦住了请求 推荐分层 从外到内通常建议: request id / tracing panic recover 全局限流 认证鉴权 业务路由 let app = Router::new() .route("/api/user", get(get_user)) .layer(TraceLayer::new_for_http()) .layer(RequestIdLayer::new()) .layer(TimeoutLayer::new(Duration::from_secs(2))); 总结 中间件不是“越多越安全”,而是“每层职责清晰、顺序可解释”。 发布前做一次链路压测,往往能提前发现大部分分层问题。 架构清晰的系统,异常路径也应该清晰。

2026年4月27日 · 1 分钟 · BvBeJ

Rust 错误分层:把排障信息留在正确位置

背景 Rust 的 Result 很强,但很多项目还是会遇到同一个问题: 错误被一路 ? 传上去 日志里只有一个模糊报错 出问题时不知道是哪个环节失败 这通常是错误分层没有做好。 一条实用原则 库层定义结构化错误类型 应用层补充上下文并统一输出 use thiserror::Error; #[derive(Debug, Error)] pub enum RepoError { #[error("record not found")] NotFound, #[error("db error: {0}")] Database(String), } use anyhow::{Context, Result}; pub async fn get_user_handler(id: i64, svc: &UserService) -> Result<UserDto> { let user = svc .find_user(id) .await .with_context(|| format!("get user failed, id={id}"))?; Ok(UserDto::from(user)) } 日志里要带可关联字段 只打印错误文本通常不够,至少带上: 请求 ID 用户或租户标识 关键资源 ID tracing::error!( request_id = %request_id, user_id = user_id, error = %err, "failed to get user" ); 总结 Rust 错误处理做得好,排障效率会明显提升。 ...

2026年4月24日 · 1 分钟 · BvBeJ

Rust 零拷贝序列化:什么时候值得做

先明确目标 “零拷贝”不是为了炫技,而是为了减少: 内存分配次数 数据复制成本 GC 或 allocator 压力 在高吞吐场景里,收益通常很直接。 借用驱动的数据视图 Rust 的借用模型天然适合做零拷贝读取: #[derive(Debug)] struct Header<'a> { trace_id: &'a str, method: &'a str, } fn parse_header<'a>(trace_id: &'a str, method: &'a str) -> Header<'a> { Header { trace_id, method } } 这里没有分配新字符串,只是借用了输入切片。 适用边界 适合: 协议解析 日志处理 消息中间件消费链路 不适合: 需要长期持有数据跨线程传递 接口边界复杂,生命周期管理成本过高 工程上的折中 热路径零拷贝 冷路径允许复制换可读性 用基准测试验证收益,而不是主观判断 小结 零拷贝是性能工具,不是教条。只有在瓶颈路径上,它才是值得支付复杂度的优化。

2026年4月23日 · 1 分钟 · BvBeJ

Rust 与 C++ FFI 实战:先稳住边界再谈性能

场景 很多团队不是从零重写,而是把热点模块逐步迁到 Rust。最现实的路径是: C++ 主程序继续跑 Rust 提供一个动态库 双方通过 C ABI 通信 核心原则很简单:跨语言只传 C 兼容类型。 先定义稳定接口 #[repr(C)] pub struct CalcResult { pub code: i32, pub value: i64, } #[no_mangle] pub extern "C" fn calc_sum(a: i64, b: i64) -> CalcResult { CalcResult { code: 0, value: a + b } } #[repr(C)] 保证结构体布局可预测 extern "C" 保证调用约定一致 #[no_mangle] 让符号名可被 C++ 链接 字符串内存谁分配谁释放 跨边界最容易出问题的是字符串: Rust 分配、C++ 释放,或者反过来 不同分配器混用导致崩溃 推荐做法:统一由一侧分配和释放,并显式提供 free 函数。 错误处理不要 panic 穿透 FFI 层应该“防炸”: #[no_mangle] pub extern "C" fn safe_div(a: i64, b: i64, out: *mut i64) -> i32 { if out.is_null() { return -2; } if b == 0 { return -1; } unsafe { *out = a / b; } 0 } 返回错误码是最朴素、也最稳的方式。 ...

2026年4月19日 · 1 分钟 · BvBeJ