FFI
极简应用场景【字符串·传输】浅谈这篇文章分享了我对Rust
与C
程序之间字符串(字节序列)传输机制的“悟道”成果。【FFI
字符串·传输】是FFI
诸多概念中:
它算是难度适中,既能讲出点内容来,又不会知识点太过生涩劝退读者。上干货!
这还真是一张图。一图抵千词,再配上一些文字描述,应该能够把概念讲清楚。
首先,libc crate是操作系统常用ABI
的FFI binding
。
Cargo.toml
中添加libc
依赖项·就相当于·在C
代码插入一行导入系统头文件的#include
语句。libc crate
不是系统ABI
的跨平台解决方案。所以,libc crate
的下游使用者得自己区分在哪个操作系统平台上,调用libc crate
的哪个API
— 即便实现功能相同,在不同操作系统平台上,多半也得调用不同libc crate API
。libc crate
不是包罗万象的。你要知道操作系统ABI
有多少,有多庞大。libc crate
的绑定范围很窄,粗略包括-inux
系统上,libc
,libm
,librt
,libdl
,libutil
和libpthread
OSX
系统上,libsystem_c
,libsystem_m
,libsystem_pthread
,libsystem_malloc
和libdyld
Windows
系统上,CRT
。若做win32
开发,我还是比较推荐winapi crate
。其次,【Rust
字符串】与【C
字符串】指的是采用了不同【字节序列·编码格式】的字符串,而不是特指Rust
内存里或C
内存里的字符串。
Rust
字符串】严格遵循UTF-8
编码格式。它的长度信息被保存于String
智能指针·结构体的私有字段self.vec.len
内。&str
胖指针内。C
字符串】是以\0
(或NUL
)结尾的,由任意非\0
字节拼合而成的字节序列。vec![0_u8, N + 1]
字节数组;然后,用字符串有效内容复写前N
个字节;最后,保留尾字节是\0
[例程2]。Vec::with_capacity(N)
划出一段连续且未初始化内存;再,填充字符串有效内容;最后,由Vec::resize(N, 0)
扩展字节数组至N + 1
个字节和给尾字节写入\0
值 [例程1]。N
代表C
字符串的有效内容长度。vec![0_u8, N + 1]
宏要比系统指令zmalloc()
慢得多。如果你特别看重性能,那么下面描述的另一条技术路线应该更合你的意。N
代表C
字符串的有效内容长度。vec![0_u8, N]
宏了。C
字符串】的实际长度总比它的有效内容长度多1
个字节 — \0
。C
字符串】向【Rust
字符串】的转换是refutable
,因为【C
字符串】可以是任意的非零字节序列,而不一定是有效的UTF-8
字节数组。C
字符串】不是被保存于C
内存的字符串。相反,Rust
内存区域内也能存储【C
字符串】。libc::strlen(_: *const libc::c_char) -> usize
返回的是字符串【有效内容·长度】。当做字符串的逐字节内存复制时,千万别忘了人工地在字符串复本末端添加一个\0
字节 [例程1]。C
字符串】的\0
终结位是一个编码“大坑”,因为在对【C
字符串】做逐字节内存复制时,\0
位需要由开发者自己人工增补上:接着,【C
字符串】的CString
与&CStr
封装类型就相当于【Rust
字符串】的String
与&str
。
CString
与String
的共同点Rust
内存里CString
与String
的不同点就是:【字节序列·编码格式】不同。CString
是以\0
(或NUL
)结尾的,任意非\0
字节序列。String
是UTF-8
。&CStr
与&str
的共同点是&CStr
与&str
的不同点是&CStr
引用【C
内存】里的【C
字符串】。&str
是【胖指针】;CStr
是【智能指针】,但被【自动·解引用】之后的CStr
也是一个【胖指针】。&CStr
既能引用C
内存里的C
字符串,也能引用Rust
内存里的C
字符串。CString / &CStr
的直接语法指令。CString::from_raw(_: *mut libc::c_char)
仅能导入由CString::into_raw() -> *mut libc::c_char
导出的原始指针。CString::from_raw()
导入任意【C
字符串】会导致“未定义行为”。C
端程序(或libc::malloc()
)构造的【字符串·字节序列】还是得由&CStr
引用才是最安全的。最后,相对于Vec<u8>
的Rust
内存字节数组,libc::malloc()
就是从C
内存里圈出一段连续且未初始化的内存空间,来保存【字符串·字节序列】。所以,由libc::malloc()
分配出的内存段完全不受Rust
内存安全机制的管控 — 馁馁地“放飞大自然”了。
Rust
技术术语来讲,libc::malloc()
输出【字符串·字节序列】的【所有权】属C
端,但【引用】却在Rust
端。这馁馁是从C
至Rust
的【按·引用】字符串传递!Rust
以FFI
函数【返回值】的方式向C
程序传递【字符串·字节序列】(下面有详细的解释)。在其它任何场景下,libc::malloc()
都极不推荐,因为更多的unsafe
代码和更高的内存泄漏风险。第一,最小化unsafe
代码的数量。即,
Rust
标准库封装的C
字符串类型CString
&CStr
*const libc::c_char
与*mut libc::c_char
)。比如,
等等libc::malloc(_: usize) -> libc::c_void
, 在C
内存区域内,开辟一段连续的内存空间std::ptr::write<T>(dest: *mut T, src: T)
向指定位置写某个类型的数据。std::ptr::null()
构造一个未初始化的只读·空指针std::ptr::null_mut()
构造一个未初始化的可修改·空指针std::ptr::copy_nonoverlapping<T>(src: *const T, dest: *mut T, count: usize)
逐字节的内存复制第二,尽量【按·引用】传递字符串,而不是【按·值】传递(即,逐字节·内存复制)。
干讲教条很抽象,下面我结合具体的使用场景,来详细地解释
Rust
导出extern "C" fn
函数供C
程序调用场景一:Rust
端,导出#[no_mangle] extern “C” fn set(_input: *const libc::c_char)
函数,以【只读·入参】的形式,接收完全由C
程序构造的C
字符串。
C
字符串·字节序列】。即,借助mut Vec<u8> + std::ptr::copy_nonoverlapping() --> CString --> String
的组合“暴击”,将C
内存上的C
字符串逐字节地复制到Rust
内存,再将其转码为Rust
字符串 [偏简单·例程2] 和 [偏性能·例程1]。&CStr --> &str
,构造一个从Rust
指向C
内存的【引用】 [例程3]。【按·引用】传递才是对内存使用效率最高的做法。场景二:Rust
端,导出#[no_mangle] extern “C” fn get() -> *mut libc::c_char
函数,以【返回值】的形式,向C
程序发送在Rust
内存构造的C
字符串。
Rust
借入检查器不允许·引用的生命周期·比·被引用数据的生命周期·更长。即,在get()
函数里构造的C
字符串·字节序列在函数结束时就被自动释放了,但是它的引用还要在被其它函数使用。这会招致编译失败。unsafe
代码与原始指针而言,被指针引用的数据脱离了Drop Checker
监控会造成内存泄漏风险。C
调用端。于是,先libc::malloc(...)
在C
内存划出一段未初始化的字节数组;然后,将C
字符串有效内容都给填过去;再,塞上尾字节\0
;接着,把原始指针丢给C
调用端程序;最后,Rust
函数安全、合规地结束 [例程4]。完美甩锅!我们的程序已经结束了,数据“本尊”也已经在C
内存里,C
程序你看着办吧,别漏了!哈哈...Rust
导入与执行C
函数场景三:Rust
端,导入extern "C" {fn set(_: *const libc::c_char);}
函数,以【只读·实参】的形式,向C
程序发送在Rust
内存构造的C
字符串。
C
字符串·字节序列】。即,借助libc::malloc() + std::ptr::copy_nonoverlapping() + std::ptr::write()
组合,将Rust
内存上的C
字符串逐字节地复制到C
内存。String -> CString
,先本地构造一个C
字符串·字节序列;再,传递它的原始指针*const libc::c_char
给C
程序 [例程5]。C
字符串·字节序列的内存。即,让它的生命周期足够地长。C
字符串·字节序列内的字节值。C
端函数被执行期间,C
程序需要长期持有此字符串数据,那就得C
端开发者考虑:是否需要做一下字符串数据的【按·值】接收了。又一次完美“甩锅”!场景四:Rust
端,导入extern "C" {fn get(buffer: *mut c_char, size: c_uint) -> c_uint;}
函数,以【可修改out
实参】的形式,接收完全由C
程序构造的C
字符串。
extern "C" { fn get(buffer: *mut c_char, size: c_uint) -> c_uint;
}
libc::malloc()
,将接收C
字符串的\0
字节数组buffer
直接·放到C
端内存中去。Vec<u8>
字节数组需要被显示地绑定于Rust
函数内的某个具名变量,以确保该字节数组的生命周期足够地长,至少也得>= C
端函数执行周期。否则,C
端程序就会遭遇悬垂指针了。vec![0_u8; N] -> *mut libc::c_char
,本地构造一个\0
初始化的Vec<u8>
字节数组,和等着C
程序向该Rust
字节数组写数据。Vec<u8> -> CString -> String
,将收到的C
字符串·字节序列转码成String
实例。其实,FFI
传递复杂【自定义·数据结构】的底层原理与处理【字符串】非常相似。只不过,数据结构的编码方式变得更复杂了,没有C
字符串与Rust
字符器那么泾渭分明。所以,需要使用#[repr(C)]
元属性等技术手段加以显示地标注。我对这块知识点还是处于“悟道”但未“悟透”的阶段。目前,实在写不明白,逻辑不自恰,应该还有地方理解错了。哎,真难!
这里,与大家共勉,共同进步吧。