#api #function #safe #transfer #methods #utility #language

ffizz-passby

实现按值传递和按指针传递的FFI辅助工具

11个不稳定版本 (4个破坏性版本)

0.5.0 2023年6月19日
0.4.1 2023年2月4日
0.3.4 2023年1月17日
0.3.2 2022年8月30日
0.1.1 2022年8月11日

#446 in 数据结构

Download history 3/week @ 2024-03-07 6/week @ 2024-03-14 36/week @ 2024-03-28 27/week @ 2024-04-04

每月下载量69
用于 ffizz-string

MIT 许可证

46KB
557

此crate提供了一些有用的类型,用于实现C API到Rust库的转换。这些结构体提供了鼓励使用规范、安全的C API的实用函数。

典型用法是在提供C API的crate中,包含一系列#[repr(C)]类型和一些#[no_mangle]函数,这些函数可以从C调用。这种风格的C API通常为每个类型有几种“方法”。例如,在palmtree库中,一个Node类型可能有几个C可访问的字段和C“方法”函数,如palmtree_node_newpalmtree_node_freepalmtree_node_coconut。这些函数使用此crate中的方法将值安全地在C和Rust之间传递。

用法

提供的每个类型都是一个空结构体,暴露了实用方法。要使用这些类型,定义一个类型别名,指定相关的Rust和(如果需要)C类型

# struct InfPrec(u32);
# #[repr(C)]
# struct infprec_t(u32);
# impl From<InfPrec> for infprec_t {
#     fn from(rval: InfPrec) -> infprec_t { infprec_t(rval.0) }
# }
# impl Into<InfPrec> for infprec_t {
#     fn into(self) -> InfPrec { InfPrec(self.0) }
# }
type InfPrecValue = ffizz_passby::Value<InfPrec, infprec_t>;

然后,根据需要在该类型上使用实用函数

# struct InfPrec(u32);
# #[repr(C)]
# struct infprec_t(u32);
# impl From<InfPrec> for infprec_t {
#     fn from(rval: InfPrec) -> infprec_t { infprec_t(rval.0) }
# }
# impl Into<InfPrec> for infprec_t {
#     fn into(self) -> InfPrec { InfPrec(self.0) }
# }
# type InfPrecValue = ffizz_passby::Value<InfPrec, infprec_t>;
let unlucky = InfPrec(13);
// SAFETY: value does not contain an allocation
unsafe {
    let cval = InfPrecValue::return_val(unlucky);
}

以下类型可用

  • Value,允许在C和Rust之间传递简单的Copy-able值。
  • Boxed,允许通过指针传递值,其中Rust管理分配。
  • Unboxed,允许通过指针传递值,但C管理分配(如在栈上或在某些其他结构体中)。

安全性

此crate不会自动使任何内容安全。它只是将C和Rust之间值处理的处理方式限制在几种方法中。一旦理解了这些方法,Rust和C程序员更容易理解和遵守安全要求。

本仓库中的每个不安全特性格式方法都描述了其安全性要求。对这些方法中的任何一个的调用都必须在一个unsafe { }代码块中,并且该代码块之前应该有一个注释来针对每个安全性要求进行说明。这在文档中的示例中得到了说明。

一般建议

一般来说,C API依赖于程序员仔细阅读API文档并遵循其规则,而不需要编译器的帮助。在可能的情况下,设计API使其变得简单,有易于记忆的规则、一致的行为,以及在实用的运行时进行检查。

获取所有权

当一个函数被记录为获取一个值的所有权时,确保在发生错误的情况下也总是这样,例如,考虑一个无限精度除法函数

# struct InfPrec(u32);
# impl InfPrec {
#     fn is_zero(&self) -> bool { self.0 == 0 }
# }
# impl std::ops::Div<InfPrec> for InfPrec {
#     type Output = Self;
#     fn div(self, other: Self) -> Self { Self(self.0 / other.0) }
# }
# #[repr(C)]
# struct infprec_t(u32);
type InfinitePrecision = ffizz_passby::Unboxed<InfPrec, infprec_t>;
unsafe extern "C" fn infprec_div(a: infprec_t, b: infprec_t, c_out: *mut infprec_t) -> bool {
    // SAFETY: b is valid and caller will not use it again (documented in API)
    let b = unsafe { InfinitePrecision::take(b) };
    if b.is_zero() {
        return false;
    }
    // SAFETY: a is valid and caller will not use it again (documented in API)
    let a = unsafe { InfinitePrecision::take(a) };
    // SAFETY:
    //  - c_out is not NULL, properly aligned, and has enough space for an infprec_t (documented in API)
    //  - c_out will eventually be passed back to Rust to be freed.
    unsafe {
        InfinitePrecision::to_out_param_nonnull(a / b, c_out);
    }
    true
}

调用者期望infprec_div获取ab的所有权,并且在返回时不会释放它们。按照现在的写法,当b为零时,在a转换为Rust值之前就发生了提前返回,所以它不会被丢弃,并且会泄漏。在这种情况下,修复方法是提前将let a语句移动到提前返回之前。

隐藏的可变性

Rust在共享、只读引用和独占、可变引用之间做出了严格的区分。C通常不做出这样的区分,并通过仔细的注释来说明哪些方法可以同时调用,以避免数据竞争。在大多数情况下,C程序员会“猜测”哪些方法可以同时调用。一个好的C API会明确这些猜测。

例如,一个类型可能被记录为不安全地跨线程,这意味着一个类型的单个值上不能同时调用任何函数。然而,对于许多类型,只要它们不是修改操作,只读方法就可以同时调用。例如,可以在不与任何kvstore_set调用重叠的情况下,同时调用kvstore_get

内部可变性需要更加仔细的文档。继续上面的例子,如果kvstore_t数据结构在读取时自行重新平衡,在这种情况下,即使它看起来是一个只读操作,调用kvstore_get也是不安全的。在Rust中,签名将是KVStore::get(&mut self),并且编译器将阻止这种并发调用。在C中,这必须在文档中清楚地解释。

无运行时依赖