• [Rust笔记] 我也谈 Box<T>智能指针·实践领悟


    我也谈Box智能指针·实践领悟

    先概述,再逐一展开

    • Box既是【所有权·智能指针】— 即,它是【堆·数据】在【栈】内存上的“全权·代表”。

    • Box也是FFI按【引用】传值的C ABI指针 — 即,它是Box::into_raw(Box) -> *mut T语法糖

    • 相较于Box::into_raw(Box) -> *mut TCString::as_ptr(&self) -> *const T的【原始指针】返回值绝不能作为FFI参数·传递。

    Box是【所有权·智能指针】

    1. Box是【智能指针】,因为impl Deref for Boximpl DerefMut for Box。于是,当&Box作为函数的实参时,就有了从&Box&T的【自动解引用】语法糖。从效果上看,这就让以&T为形参的函数func(&T)能够接收&Box的实参 — 形似OOP多态。但,这和GC类语言【多态性】的最大区别在于:

      1. 由【智能指针】【自动解引用】模仿的【多态】是编译时行为(【术语】单态化;它会延长编译时长)与【零(运行时)成本】 。所以,我的一贯观点是:Rust编译费时不是“瑕疵”,而是语言特征。它就是要在这个环节“变戏法”。倘若你真那么介意编译时间的话,没准【脚本语言】才更符合你的产品需求。

      2. GC类语言的【多态】是由强大的VM提供的运行时语言特性。即,将“变戏法”的时间点选择在了【运行时】。

    2. Box是【所有权·变量】,因为它的生命周期与被引用【堆·数据】的生命周期绝对同步。具体地讲,

      1. 于是,【堆·数据】何时被释放·就得看【栈】上的Box实例会“活”到什么时候了。

      2. 虽然Box指针自身被保存在【栈】上,但由它所指向的数据却是在【堆】上。

      3. 其它变量只能通过&Box(即,指针的引用)来间接地访问到【堆】上的原始数据。

      4. Box::new(T)既将【栈】数据搬移至【堆】内存,同时也获取了原数据的【所有权】。

      5. impl Drop for BoxBox指针的析构时间点与【堆·数据】生命周期的终止时间点·严格地对齐。

    不夸张地讲,Box就是【堆·数据】在【栈】内存中的“全权·代理人”。具有同类特点的【智能指针】还包括StringCString等。

    BoxFFIC ABI指针

    Box可直接作为“载体”,在RustC之间,穿越FFI边界,传输数据。

    使用场景·介绍

    • 场景一:将Rust内存上的一整段数据·扣出来(连同【所有权】一起)“移交”给FFIC(调用)端。对FFIRust端,这意味着:被“移交出”的数据“已死”。即,

      • Drop Checker将其视为“已释放”,而不会再隐式地调用 as Drop>::drop(self)成员方法了。

      • Borrow Checker将其视为“已无效”。因为该变量的【所有权】被“消费”consumed掉,所以禁止对该变量的任何后续·引用·与·移动·操作。

    • 场景二:将在【场景一】由FFI接口“移交出”的内存数据·重新再给接收回来。进而,析构与释放掉(最初由Rust端分配的)内存。即,自己分配的内存必须由自己回收

      • 经验法则:由Rust端分配的内存数据最终还是要由Rust端“出手”以相同的memory layout析构与释放。而不是,由C端的free()函数就地释放,因为由Rust端默认采用的std::alloc::Global非常可能与C端【分配器】不一样。这不完犊子了吗!

    适用场景

    总结起来,Box和被“糖”的完整语法形式(包括

    • Rust -> C导出】Box::into_raw(Box) -> *mut T

    • C -> Rust导入】unsafe Box::from_raw(*mut T) -> Box

    )仅适用于由【场景一】+【场景二】构成的“闭环”使用场景:

    1. Rust

      1. 定义与导出FFI函数接口

      2. 定义与实例化FFI数据结构

    2. C

      1. 调用Rust - FFI接口函数

      2. 获取Rust - FFI数据结构实例

      3. 使用该实例搞一系列操作

      4. 再调用Rust - FFI接口函数,将该实例给释放掉

    题外话,你有没有对这个套路略感眼熟呀?再回忆回忆,它是不是FFI: Object-Based APIs设计模式。英雄所见略同!

    不适用场景

    另外,Box和被“糖”的完整语法形式(包括

    • C -> Rust导入】unsafe Box::from_raw(*mut T) -> Box

    不能用来接收C端数据结构的变量值(即,

    1. 数据结构在C端定义

    2. 变量值也在C端被实例化

    )。因为Box对被接收的原始指针有如下(确定性)假设invariant

    • 内存对齐

    • 非空

    • Global Allocator内存分配

    而这些假设,C端他保证不了!所以,我强烈推荐使用libc crate定义的各种数据类型与原始指针(比如,libc::c_char)来最贴切地“镜像”C数据类型到Rust端。我没有推荐其它的crate,因为我没用过,我不会!而不是因为libc crate真的有多好!

    场景一·技术细节·展开

    Rust FFI导出函数而言,函数返回值可直接使用Box作为返回值类型,而不是原始指针*mut T [例程1]。这样就绕开了Rust 2015版次要求的完整语法形式 [例程2]。

    RustC导出值的关键语句(伪码)let ptr: *mut T = Box::into_raw(Box::new::(data: T));。它完成的任务可被拆解为:

    • 将【栈·数据】搬移至【堆】内存上 — 只有【堆·数据】才能被传递给C端,因为

      • 【栈·数据】会随着函数执行结束而被【栈pop操作】给释放掉

      • 【堆·数据】可以被假装释放和不再被追踪。

    • “消费”掉·原数据实例·所有权 — 【借入·检查器】将进一步禁止对该·变量·的任何后续操作。

    • 取出【堆·数据】的原始指针 — 该指针是要被传输给C端的。

    • 将该数据从Drop Checker监控清单“除名”。这样,当函数结束时,Drop Checker就不会调用 as Drop>::drop(T)成员方法和自动释放内容了。

    • 返回【原始指针】作为函数返回值

    上面看似繁复的处理流程,以Rust术语,一言概之:将·变量值·的【所有权】从FFIRust端转移至C调用端。或称,穿越FFI边界的变量【所有权】转移。

    场景二·技术细节·展开

    Rust FFI导出函数而言,函数·形参·类型可直接使用Option>,而不是原始指针*mut T [例程1]。这样就绕开Rust 2015版次要求的完整语法形式 [例程2]。好处显而易见:

    • 避免明文地编写unsafe code(伪码:let data: Box = unsafe {Box::from_raw::(ptr: *mut T)};),就能达成:

      • 将由【原始指针】引用的C端变量值·纳入到·RustDrop Checker的生命周期监控范围内。

      • RustBorrow Checker也会开始“抱怨”任何对C端变量值有【内存泄漏风险】的操作语句。在Rust词典中,对此有一个术语叫Hygienic — 我打趣地将它翻译为“大保健”。

    • 将对C端变量值的【判空】处理,从依赖开发者自觉性的随机行为

      1. if ptr.is_null() { // 原始指针【判空】没有来自`Borrow Checker`监督。
      2. return; // 若忘记了,那就等着运行时的内存段错误吧!
      3. }

      转变成由Borrow Checker监督落实的显示None值处理(再一次Hygienic

      1. let value = if let Some(value) = input { // 开发忘记指针【判空】没有关系。编译失败会提醒你的。
      2. value
      3. } else {
      4. return;
      5. };

    CString::as_ptr(&self) -> *const T返回值不可暴露给FFIC

    要说清楚这其中的关窍,就得把CString::as_ptr(&self) -> *const TCString::into_raw(self) -> *mut T对照着来讲。

    先介绍CString::into_raw(self) -> *mut T

    Box::into_raw(Box) -> *mut T关联函数很容易就联想到CString::into_raw(self) -> *mut T成员方法,因为它们的功能极为相似,且在FFI编程中也十分常见。那你是否曾经纠结过:为什么into_raw()Box上是关联函数,而在CString上却是成员方法呢?回答:

    1. into_raw()设计为Box关联函数是因为Box通用【智能指针】呀!所以,我们的设计·有必要最大限度地避免由【自动解引用】造成的【智能指针Box as Deref】与【内部·实例 as Deref>::Target】成员方法之间的“命名·冲突”。即,Box的成员方法千万别遮蔽了 as Deref>::Target的成员方法。

    2. into_raw()设计为CString的成员方法是因为CString仅只是CStr一个类型的【智能指针】,且已知CStr结构体没有into_raw()成员方法。于是,符合程序员直觉与看着顺眼就是首要关切。那还有什么比.操作符更减压的呢?— 若你非较真儿的话,我更偏向认为?操作符·语法糖才是最令人欲罢不能的!

    再讲原因

    一方面,CString::as_ptr(&self) -> *const T仅只返回了内部数据的内存地址“快照”(不携带任何生命周期信息与约束力)。所以,[例程3]

    • *const T指针的存在并不能暗示Drop CheckerCString实例“续命”。

      • 即,Drop Checker会无视*const T指针的存在而在块作用域(或函数)结束时立即dropCString实例。

    • Borrow Checker也不会,因为*const T指针正在借入已经被释放的CString实例,而编译失败和抱怨:“正在借入一个已dropped的变量”。

      唯一令人欣慰的好消息是:rustc 2021已经能够linter警告”由【成员方法·链式调用】造成的dangling原始指针“了。该lint规则被称作temporary_cstring_as_ptr。[例程3]

      • 一旦该【dangling原始指针】在某处被【解引用】取值,这馁馁地就是一个运行时内存段错误。错误原因你猜去吧!

    另一方面,CString::into_raw(self) -> *mut T在返回内部数据【原始指针】的同时

    • 消费掉CString实例的所有权。于是,Borrow Checker会监督该CString实例不会再被借入或移动。

    • 通知Drop Checker莫要真释放CString实例内存,和将该CString实例从Drop Checker监管清单除名。于是,::drop(self)成员方法就不会被Drop Checker隐式自动执行。

    综上所述,由CString::as_ptr(&self) -> *const T返回的【原始指针】只可用来看(我现在也没有领会到:只能看,能有什么用?),而不能被【解引用】拿来用。

    最后,结合FFI使用场景

    1. CString::into_raw(self) -> *mut T等效于将CString实例【所有权】转移给了FFIC端。但是,绝对不可使用C端的free()函数来回收CString实例占用的内存。相反,得模仿Box的作法:

      1. 先,将该CString实例,经由FFI Rust ABI,传回给Rust端。

      2. 再,使用CString::from_raw(*mut T)恢复Rust对该CString实例的【所有权】管控

      3. 最后,由Drop Checker自动地在【作用域】(结束)边界处调用::drop()成员方法将此CString实例给回收掉。

    2. CString::as_ptr(&self) -> *const T是没有资格被使用于FFI场景的,因为一旦FFI - Rust导出函数被执行结束,那么

      1. *const T指向的CString实例内存就立即被Drop Checker给回收掉了。

      2. FFI - C端拿到的仅仅是一个【野指针】。

    结束语

    这次,我就分享这些心得体会。我对rust的实践机会少,所以不仅文章产出少,对技术知识点阐述的深度也有限。希望路过的神仙哥哥,仙女妹妹多评论指正,共同进步。

  • 相关阅读:
    Android C++系列:C++最佳实践2抽象类
    【Java第29期】:Tomcat的安装和使用
    比selenium体验更好的ui自动化测试工具: cypress介绍
    Rust流程控制
    基于Struts2开发仓库管理系统
    QT--day1
    二分查找 binarySearch 适合初学分析的例子
    前端-(1)
    JSqlParser 解析 sql
    JavaScript小技能:运算符
  • 原文地址:https://blog.csdn.net/u012067469/article/details/127438006