同 FFI 交互,主要是在和指针打交道,另外两个有用的工具为:
工具 | 作用 |
---|---|
为 Rust 代码导出 C 接口的头文件。 | |
为 C 头文件生成 Rust FFI 声明。 |
为 Rust 生成 C 接口
指针
首先是两个基本概念:
引用本质上是指针。
Rust 中的 Option 本质上是一个 TaggedUnion,但是对于引用(即指针)会执行 空指针优化 。也就是说,空指针视为 None,非空指针视为 Some。
因此对于指针,可以将其写作:
fn set_error(ret: Option<&mut Error>){
todo!()
}
其效果等同于
fn set_error(ret: *mut Error){
todo!()
}
函数指针
由于 Rust 中没有函数指针的概念,因此函数总不是空的。因此函数指针总是需要使用 Option:
pub type FnGetSize = extern "C" fn(obj: *const c_void) -> usize;
隐藏字段
当结构体没有使用 repr©
表示且没有创建过对象(只创建过指针)时,cbindgen 只会生成对应结构体的前置类型声明。借助这一方式可以避免 wrap 结构体泄露:
pub struct WrapPerson(usize);
impl WrapPerson {
#[no_mangle]
pub extern "C" fn new() -> *mut WrapPerson{
let obj = Box::new(Person::new());
let ptr = Box::into_raw(obj);
ptr as *mut WrapPerson
}
#[no_mangle]
pub extern "C" fn print(&self) {
let obj = Box::from_raw(core::ptr::addr_of!(*self) as *const Person); (1)
println!("{obj:?}");
}
}
此处不得拷贝 self
正如上述所注,一个常见错误是将 self 进行了拷贝。这回导致 addr_of 得到的是假的地址,从而引发段错误。
包装对象
一个常见的需求是将某个对象包装起来暴露给 C 中,这种情况下可以借助 Box 来进行不透明对象类型包装,例如:
pub struct WrapObj(usize);
impl WrapObj {
#[no_mangle]
pub extern "C" fn new() -> *mut Self {
let obj = Box::new(Obj::default());
Box::into_raw(obj) as *mut Self
}
}
这种方式有两点关键:
WrapObj 不添加
repr©
,且代码中只通过指针操纵 WrapObj。这样就能避免 WrapObj 被导出。Obj 使用 Box 或者 Arc 创建在堆上,然后又使用 into_raw 和 as 将指针强转成
*mut WrapObj
。
一般情况下还需要实现 AsRef 和 AsMut 来实现 WrapObj 和 &Obj 之间的方便转换。添加 drop 来释放对象。 |
堆上对象转栈上对象
Rust 的接口一般使用栈上对象拿取所有权的形式,但是使用 [包装对象] 时所有对象都是堆上对象。这时候如果被包装对象又没有实现 Copy 语义的话,就会导致两边接口不同。这时候可以借助 mem::replace
或者 mem::take
来行使 “移动语义” :
match obj_ptr.is_null() {
true => Default::default(),
false => {
let heap_obj = unsafe {
&mut *(*obj_ptr as *mut Obj)
};
mcore::Obj {
field1: core::mem::take(&mut heap_obj.field1),
field2: core::mem::take(&mut heap_obj.field2),
}
}
}
这样,复杂类型使用 take 取走所有权,同时原地留下 default(),简单类型直接拷贝即可。