Box智能指针·实践领悟先概述,再逐一展开
Box既是【所有权·智能指针】— 即,它是【堆·数据】在【栈】内存上的“全权·代表”。
Box也是FFI按【引用】传值的C ABI指针 — 即,它是Box::into_raw(Box的语法糖。
相较于Box::into_raw(Box,CString::as_ptr(&self) -> *const T的【原始指针】返回值绝不能作为FFI参数·传递。
Box是【所有权·智能指针】Box是【智能指针】,因为impl Deref for Box和impl DerefMut for Box。于是,当&Box作为函数的实参时,就有了从&Box到&T的【自动解引用】语法糖。从效果上看,这就让以&T为形参的函数func(&T)能够接收&Box的实参 — 形似OOP多态。但,这和GC类语言【多态性】的最大区别在于:
由【智能指针】【自动解引用】模仿的【多态】是编译时行为(【术语】单态化;它会延长编译时长)与【零(运行时)成本】 。所以,我的一贯观点是:Rust编译费时不是“瑕疵”,而是语言特征。它就是要在这个环节“变戏法”。倘若你真那么介意编译时间的话,没准【脚本语言】才更符合你的产品需求。
GC类语言的【多态】是由强大的VM提供的运行时语言特性。即,将“变戏法”的时间点选择在了【运行时】。
Box是【所有权·变量】,因为它的生命周期与被引用【堆·数据】的生命周期绝对同步。具体地讲,
于是,【堆·数据】何时被释放·就得看【栈】上的Box实例会“活”到什么时候了。
虽然Box指针自身被保存在【栈】上,但由它所指向的数据却是在【堆】上。
其它变量只能通过&Box(即,指针的引用)来间接地访问到【堆】上的原始数据。
Box::new(T)既将【栈】数据搬移至【堆】内存,同时也获取了原数据的【所有权】。
impl Drop for Box将Box指针的析构时间点与【堆·数据】生命周期的终止时间点·严格地对齐。
不夸张地讲,Box就是【堆·数据】在【栈】内存中的“全权·代理人”。具有同类特点的【智能指针】还包括String和CString等。
Box是FFI的C ABI指针Box可直接作为“载体”,在Rust与C之间,穿越FFI边界,传输数据。
场景一:将Rust内存上的一整段数据·扣出来(连同【所有权】一起)“移交”给FFI的C(调用)端。对FFI的Rust端,这意味着:被“移交出”的数据“已死”。即,
Drop Checker将其视为“已释放”,而不会再隐式地调用成员方法了。
Borrow Checker将其视为“已无效”。因为该变量的【所有权】被“消费”consumed掉,所以禁止对该变量的任何后续·引用·与·移动·操作。
场景二:将在【场景一】由FFI接口“移交出”的内存数据·重新再给接收回来。进而,析构与释放掉(最初由Rust端分配的)内存。即,自己分配的内存必须由自己回收。
经验法则:由Rust端分配的内存数据最终还是要由Rust端“出手”以相同的memory layout析构与释放。而不是,由C端的free()函数就地释放,因为由Rust端默认采用的std::alloc::Global非常可能与C端【分配器】不一样。这不完犊子了吗!
总结起来,Box和被“糖”的完整语法形式(包括
【Rust -> C导出】Box::into_raw(Box
【C -> Rust导入】unsafe Box::from_raw
)仅适用于由【场景一】+【场景二】构成的“闭环”使用场景:
Rust端
定义与导出FFI函数接口
定义与实例化FFI数据结构
C端
调用Rust - FFI接口函数
获取Rust - FFI数据结构实例
使用该实例搞一系列操作
再调用Rust - FFI接口函数,将该实例给释放掉
题外话,你有没有对这个套路略感眼熟呀?再回忆回忆,它是不是FFI: Object-Based APIs设计模式。英雄所见略同!
另外,Box和被“糖”的完整语法形式(包括
【C -> Rust导入】unsafe Box::from_raw
)不能用来接收C端数据结构的变量值(即,
数据结构在C端定义
变量值也在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]。
从Rust向C导出值的关键语句(伪码)let ptr: *mut T = Box::into_raw(Box::new::。它完成的任务可被拆解为:
将【栈·数据】搬移至【堆】内存上 — 只有【堆·数据】才能被传递给C端,因为
【栈·数据】会随着函数执行结束而被【栈pop操作】给释放掉
【堆·数据】可以被假装释放和不再被追踪。
“消费”掉·原数据实例·所有权 — 【借入·检查器】将进一步禁止对该·变量·的任何后续操作。
取出【堆·数据】的原始指针 — 该指针是要被传输给C端的。
将该数据从Drop Checker监控清单“除名”。这样,当函数结束时,Drop Checker就不会调用成员方法和自动释放内容了。
返回【原始指针】作为函数返回值
上面看似繁复的处理流程,以Rust术语,一言概之:将·变量值·的【所有权】从FFI的Rust端转移至C调用端。或称,穿越FFI边界的变量【所有权】转移。
就Rust FFI导出函数而言,函数·形参·类型可直接使用Option,而不是原始指针*mut T [例程1]。这样就绕开Rust 2015版次要求的完整语法形式 [例程2]。好处显而易见:
避免明文地编写unsafe code(伪码:let data: Box),就能达成:
将由【原始指针】引用的C端变量值·纳入到·Rust端Drop Checker的生命周期监控范围内。
Rust端Borrow Checker也会开始“抱怨”任何对C端变量值有【内存泄漏风险】的操作语句。在Rust词典中,对此有一个术语叫Hygienic — 我打趣地将它翻译为“大保健”。
将对C端变量值的【判空】处理,从依赖开发者自觉性的随机行为
- if ptr.is_null() { // 原始指针【判空】没有来自`Borrow Checker`监督。
- return; // 若忘记了,那就等着运行时的内存段错误吧!
- }
转变成由Borrow Checker监督落实的显示None值处理(再一次Hygienic)
- let value = if let Some(value) = input { // 开发忘记指针【判空】没有关系。编译失败会提醒你的。
- value
- } else {
- return;
- };
CString::as_ptr(&self) -> *const T返回值不可暴露给FFI的C端要说清楚这其中的关窍,就得把CString::as_ptr(&self) -> *const T与CString::into_raw(self) -> *mut T对照着来讲。
CString::into_raw(self) -> *mut T从Box::into_raw(Box关联函数很容易就联想到CString::into_raw(self) -> *mut T成员方法,因为它们的功能极为相似,且在FFI编程中也十分常见。那你是否曾经纠结过:为什么into_raw()在Box上是关联函数,而在CString上却是成员方法呢?回答:
将into_raw()设计为Box的关联函数是因为Box是通用【智能指针】呀!所以,我们的设计·有必要最大限度地避免由【自动解引用】造成的【智能指针Box】与【内部·实例】成员方法之间的“命名·冲突”。即,Box的成员方法千万别遮蔽了的成员方法。
将into_raw()设计为CString的成员方法是因为CString仅只是CStr一个类型的【智能指针】,且已知CStr结构体没有into_raw()成员方法。于是,符合程序员直觉与看着顺眼就是首要关切。那还有什么比.操作符更减压的呢?— 若你非较真儿的话,我更偏向认为?操作符·语法糖才是最令人欲罢不能的!
一方面,CString::as_ptr(&self) -> *const T仅只返回了内部数据的内存地址“快照”(不携带任何生命周期信息与约束力)。所以,[例程3]
*const T指针的存在并不能暗示Drop Checker给CString实例“续命”。
即,Drop Checker会无视*const T指针的存在而在块作用域(或函数)结束时立即drop掉CString实例。
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 Checker隐式自动执行。
综上所述,由CString::as_ptr(&self) -> *const T返回的【原始指针】只可用来看(我现在也没有领会到:只能看,能有什么用?),而不能被【解引用】拿来用。
FFI使用场景CString::into_raw(self) -> *mut T等效于将CString实例【所有权】转移给了FFI的C端。但是,绝对不可使用C端的free()函数来回收CString实例占用的内存。相反,得模仿Box的作法:
先,将该CString实例,经由FFI Rust ABI,传回给Rust端。
再,使用CString::from_raw(*mut T)恢复Rust对该CString实例的【所有权】管控
最后,由Drop Checker自动地在【作用域】(结束)边界处调用成员方法将此CString实例给回收掉。
而CString::as_ptr(&self) -> *const T是没有资格被使用于FFI场景的,因为一旦FFI - Rust导出函数被执行结束,那么
由*const T指向的CString实例内存就立即被Drop Checker给回收掉了。
FFI - C端拿到的仅仅是一个【野指针】。
这次,我就分享这些心得体会。我对rust的实践机会少,所以不仅文章产出少,对技术知识点阐述的深度也有限。希望路过的神仙哥哥,仙女妹妹多评论指正,共同进步。