You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Rust包装C返回指针时选*mut T、NonNull<T>还是Unique<T>?

Rust中三类指针包装类型的差异与C FFI场景选型

核心特性差异

三类类型的语义边界非常清晰,不存在重叠的模糊地带:

  • *mut T:原生可变裸指针
    这是Rust和C ABI完全对齐的最底层指针类型,没有任何内置约束和语义标记:不保证非空、不携带所有权、不对别名关系做任何假设,支持任意指针运算。所有指针操作的安全性完全由开发者自己保证,仅解引用操作需要放在unsafe块中。它的核心特点是无任何额外语义承诺,和C侧拿到的原始指针行为完全一致。
    该类型默认不实现Send/Sync,无法直接跨线程传递。
  • NonNull<T>:带非空约束的裸指针封装
    本质是零开销的*mut T包装,仅新增了一个硬约束:指针值永远不为null。它和*mut T完全一致:不持有指向值的所有权、不承诺指针的独占性、不保证指向的内存一定合法有效。它的存在价值有两个:一是省去冗余的非空校验逻辑,二是让编译器可以做内存布局优化——Option<NonNull<T>>的大小和原生指针完全相同,不需要额外空间存储空值标记。
    和*mut T一样,该类型默认不实现Send/Sync,跨线程传递需要自行做安全性保证并手动实现对应trait。
  • Unique<T>:带独占所有权语义的非空指针
    这是目前标准库内部使用的不稳定API,普通业务代码不建议直接使用。它在NonNull<T>的基础上新增了两层核心语义:
    • 持有指向值的所有权:当Unique<T>被丢弃时,指向的内存必须被合理释放,语义层级和Box<T>持有的内部指针一致
    • 承诺指针独占性:表示不存在其他活跃别名可以访问指向的内存,编译器可以基于这个承诺做别名优化
      因为自带独占所有权语义,只要T本身实现Send/Sync,Unique<T>就会自动实现对应trait,支持安全跨线程传递。标准库中Box、Vec等自带内存管理的智能指针/集合类型,底层都是用Unique<T>做指针支撑。

C构造函数返回指针的选型规则

按照场景按优先级判断即可,不需要过度设计:

  1. 当调用的C构造函数可能返回NULL(比如内存分配失败返回空、参数非法返回空),直接用*mut T接收返回值。拿到指针第一时间做非空判断,走对应错误处理逻辑,确认非空后再根据后续使用场景转换为其他封装类型。
    示例代码:
    // 对接C侧: T* c_allocate_t();
    let raw_ptr = unsafe { c_allocate_t() };
    if raw_ptr.is_null() {
        return Err(AllocError);
    }
    // 非空后再转NonNull做后续逻辑
    let non_null_ptr = unsafe { NonNull::new_unchecked(raw_ptr) };
    
    不要为了省一步判空直接把可能为空的指针强转成NonNull,这会直接触发未定义行为。
  2. 当你已经通过判空确认指针非空,且Rust侧不持有指针所有权(比如C侧负责内存释放、指针只是临时借用的引用),优先用NonNull<T>。它不会额外附加错误的所有权语义,还能享受到非空带来的布局优化。
  3. 当你拿到C侧指针后需要Rust侧负责内存释放(比如要在自定义包装类型的Drop逻辑中调用C侧的释放函数),且能保证Rust侧持有指针的独占访问权、C侧不会留存指针副本偷偷修改内容,不需要直接使用不稳定的Unique<T>,可以用NonNull<T>加PhantomData<T>自行实现等价的所有权、Send/Sync语义即可,等后续Unique<T>稳定后再做替换。

避坑提醒:不要随便给指针附加独占/所有权语义,除非你100%确认指针的生命周期、别名关系完全符合Rust的安全规则,否则会直接引入难以排查的未定义行为。

内容的提问来源于stack exchange,提问作者tony

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.30 08:18:18