7 个版本

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

Rust 模式 中排名第 1618

每月下载量 33

MIT 许可证 MIT

115KB
1.5K SLoC

该软件包提供了一种方便从 Rust 和 C 中使用的字符串抽象。它提供了一种将字符串传递到 Rust 函数并从 C 返回字符串的方式,并具有明确的所有权规则。

用法

此软件包中的类型使用 ffizz_passby::UnboxedStruct,并具有相似的 API。有关创建有效 C API 的一般指导,请参阅 ffizz-passby 软件包的文档。

字符串类型

在您的 C 头文件中将 C 类型 fz_string_t 作为与 fz_string_t 文档字符串中相同的结构体公开。这足以容纳 FzString 类型,并确保 C 编译器将正确对齐值。

您可以为该类型命名。类型名称在 C ABI 中被擦除,因此可以编写使用 fz_string_t 的 Rust 声明和等效的 C 声明使用 mystrtype_t。如果您愿意,还可以使用 use ffizz_string::fz_string_t as .. 重命名 Rust 类型。

字符串实用函数

该软件包包括一些名为 fz_string_.. 的实用函数。您可以使用您喜欢的任何名称将这些函数重新导出到 C,并使用基于本软件包中那些的文档字符串,包括 C 声明

ffizz_snippet!{
#[ffizz(name="mystrtype_free")]
/// Free a mystrtype_t.
///
/// # Safety
///
/// The string must not be used after this function returns, and must not be freed more than once.
/// It is safe to free Null-variant strings.
///
/// ```c
/// EXTERN_C void mystrtype_free(mystrtype_t *);
/// ```
}
ffizz_string::reexport!(fz_string_free as mystrtype_free);

将字符串作为函数参数

接受字符串作为函数参数时,需要做出两个设计决策。首先,字符串的所有权是否从调用者转移到被调用者?或者用 Rust 术语来说,值是否被移动?这主要是调用者方便的问题,但最好在整个 API 中保持一致。

其次,您是想按值传递字符串还是按指针传递?按指针传递是推荐的,因为它通常更有效,并允许以防止使用后释放错误的方式使已移动的值无效。

按指针

定义你的 extern "C" 函数,使其接受一个 *mut fz_string_t 参数

pub unsafe extern "C" fn is_a_color_name(name: *const fz_string_t) -> bool { .. };

如果需要获取值的所有权,使用 FzString::take_ptr。否则,使用 FzString::with_refFzString::with_ref_mut 从指针借用一个引用。

所有这些方法都是不安全的。按照标准做法,调用每个不安全方法时,都要在“安全性”部分的每个条目上进行地址解析。这些通常可以参考C头文件中的文档字符串,因为这通常是由C调用者确保满足这些要求的责任。例如

ffizz_snippet!{
#[ffizz(name="mystrtype_free")]
/// Determine whether the given string contains a color name.
///
/// # Safety
///
/// The name argument must not be NULL.
///
/// ```c
/// EXTERN_C bool is_a_color_name(const fz_string_t *);
/// ```
}
pub unsafe extern "C" fn is_a_color_name(name: *const fz_string_t) -> bool { .. };
// SAFETY:
//  - name is not NULL (see docstring)
//  - no other thread will mutate name (type is documented as not threadsafe)
unsafe {
    FzString::with_ref(name, |name| {
        if let Some(name) = name.as_str() {
            return Colors::from_str(name).is_some();
        }
        false // invalid UTF-8 is _not_ a color name
    })
}

按值传递

或者,你可能需要调用者按值传递字符串。这样声明你的函数

pub unsafe extern "C" fn is_a_color_name(name: fz_string_t) -> bool { .. };

然后,使用 FzString::take 来获取字符串的所有权。按值传递时,调用者没有保留所有权的选项。

始终获取一切

如果你的C API定义表明函数接受其函数参数中的值的所有权,在发生任何早期返回之前,获取所有参数的所有权。例如

pub unsafe extern "C" convolve_strings(a: *const fz_string_t, b: *const fz_string_t) -> bool {
    // SAFETY: ...
    let a = unsafe { FzString::take_ptr(a) };
    if a.len() == 0 {
        return false; // BUG
    }
    // SAFETY: ...
    let b = unsafe { FzString::take_ptr(b) }; // BAD!
    // ...
}

在这里,如果 a 无效,函数将不会释放 b,尽管API合同承诺这样做。为了修复,将 let b 语句移动到早期返回之前。

字符串作为返回值

要返回一个字符串,定义你的 extern "C" 函数,使其返回一个 fz_string_t

pub unsafe extern "C" fn favorite_color() -> fz_string_t { .. }

然后使用 FzString::return_val 来返回值

pub unsafe extern "C" fn favorite_color() -> fz_string_t {
    let color = FzString::from("raw umber");
    // SAFETY:
    //  - caller will free the returned string (see docstring)
    unsafe {
        return FzString::return_val(color);
    }
}

字符串作为输出参数

在C和C++中,“输出参数”是一种常见的惯用语。为了将字符串返回到输出参数,使用 FzString::to_out_paramFzString::to_out_param_nonnull


/// Determine the complement of the given color, returning true on success. If
/// the color cannot be complemented, return false and leave the
/// `complement_out` string uninitialized.
pub unsafe extern "C" fn complement_color(
    color: *const fz_string_t,
    complement_out: *mut fz_string_t) -> fz_string_t {
    result = FzString::from("opposite");
    unsafe {
        FzString::to_out_param(complement_out, result);
    }
    true
}

线程安全性

通常,fz_string_t 不适合在多个线程中并发使用(在Rust术语中,它不是 Sync),但可以在线程之间传递(Send)。

更准确地说,接受 *const fz_string_t 的函数,相当于一个共享借用,可以与同一字符串并发调用。然而,任何接受 *mut fz_string_t 的函数,相当于一个独占借用,不得与任何其他接受同一字符串的函数并发调用。几个实用函数内部会修改字符串,因此接受 *mut fz_string_t

在许多情况下,可能只需要在C头文件中记录线程安全的第一、一般定义,以避免在C API中不必要的复杂性。

示例

请参阅本仓库中的 示例代码 kv,其中展示了如何使用 ffizz_string 的简单库。

性能

实现是通用的,可能会导致比必要更多的分配或字符串复制。特别是当 Rust 实现立即将 FzString 转换为 std::string::String 时,这种情况更为明显。这种转换带来了极大的简化,但涉及到字符串的分配和复制。

在 API 性能至关重要的场景下,可能更倾向于在整个实现中使用 FzString

依赖项

~2MB
~45K SLoC