#abi #cbindgen #macro #self-reference #api-bindings

无需 std cglue

FFI 安全抽象,用于创建插件和与 C 兼容的库

16 个版本

0.3.4 2024年7月31日
0.2.14 2023年11月4日
0.2.12 2022年11月16日
0.2.11 2022年3月20日
0.1.2 2021年7月23日

#21 in FFI

Download history 578/week @ 2024-05-03 336/week @ 2024-05-10 257/week @ 2024-05-17 295/week @ 2024-05-24 202/week @ 2024-05-31 203/week @ 2024-06-07 211/week @ 2024-06-14 191/week @ 2024-06-21 120/week @ 2024-06-28 88/week @ 2024-07-05 265/week @ 2024-07-12 164/week @ 2024-07-19 1034/week @ 2024-07-26 255/week @ 2024-08-02 209/week @ 2024-08-09 183/week @ 2024-08-16

1,709 下载/每月
用于 21 个 Crates (6 直接)

MIT 许可证

190KB
3.5K SLoC

CGlue

Crates.io API Docs Build and test MIT licensed Rustc 1.45

如果所有代码都粘合在一起,我们的粘合剂就是市场上最安全的。

最完整的动态特质对象实现,毫无疑问。

概述

CGlue 以 FFI 安全的方式暴露 dyn Trait。它将 Rust 特质桥接到 C 和其他语言。它的目标是无缝集成 - 只需在特质周围添加一些注释,它们就应该可以使用了!

use cglue::*;

// One annotation for the trait.
#[cglue_trait]
pub trait InfoPrinter {
    type Mark;
    fn print_info(&self, mark: Self::Mark);
}

struct Info {
    value: usize
}

impl InfoPrinter for Info {
    type Mark = u8;

    fn print_info(&self, mark: Self::Mark) {
        println!("{} - info struct: {}", mark, self.value);
    }
}

fn use_info_printer<T: InfoPrinter>(printer: &T, mark: T::Mark) {
    println!("Printing info:");
    printer.print_info(mark);
}

fn main() -> () {
    let mut info = Info {
        value: 5
    };

    // Here, the object is fully opaque, and is FFI and ABI safe.
    let obj = trait_obj!(&mut info as InfoPrinter);

    use_info_printer(&obj, 42);
}

Rust 无法保证您的代码将与 2 个不同编译器版本冲突,也不会与 任何其他小的更改 兼容,CGlue 将它们全部粘合在一起,使其有效。

这是通过为指定的特质生成包装虚拟表(虚拟函数表)来完成的,并创建一个与表匹配的不可见对象。

cglue_trait 注释生成一个 InfoPrinterVtbl 结构,并为实现 InfoPrinter 特质的类型构建所需的全部代码。然后,构造一个 CGlueTraitObj,它包装输入对象并实现 InfoPrinter 特质。

但这还不是全部,您还可以将特质分组在一起!

use cglue::*;

// Extra trait definitions

#[cglue_trait]
pub trait InfoChanger {
    fn change_info(&mut self, new_val: usize);
}

impl InfoChanger for Info {
    fn change_info(&mut self, new_val: usize) {
        self.value = new_val;
    }
}

#[cglue_trait]
pub trait InfoDeleter {
    fn delete_info(&mut self);
}

// Define a trait group.
//
// Here, `InfoPrinter` is mandatory - always required to be implemented,
// whereas `InfoChanger` with `InfoDeleter` are optional traits - a checked
// cast must be performed to access them.
cglue_trait_group!(InfoGroup, InfoPrinter, { InfoChanger, InfoDeleter });

// Implement the group for `Info` structure, defining
// only that `InfoChanger` is optionally implemented.
// This is not required if `unstable` feature is being used!
cglue_impl_group!(Info, InfoGroup, InfoChanger);

let mut info = Info { value: 5 };

let mut obj = group_obj!(info as InfoGroup);

// Object does not implement `InfoDeleter`
assert!(as_ref!(&obj impl InfoDeleter).is_none());

change_info(&mut cast!(obj impl InfoChanger).unwrap(), 20);

fn change_info(change: &mut (impl InfoPrinter + InfoChanger), new_val: usize) {
    println!("Old info:");
    change.print_info();
    change.change_info(new_val);
    println!("New info:");
    change.print_info();
}

还有更多!以下是一些亮点

  1. 能够使用自消耗的特质函数。

  2. 暴露了一些标准库特质(Clone)。

  3. 能够将关联特质类型包装成新的 CGlue 特质对象和组。

  4. 上述功能也适用于可变和const引用关联类型返回值。

  5. 泛型特性和它们的组。

  6. 库引用计数.

  7. 使用 abi_stable 进行可选运行时ABI/API验证(启用 layout_checks 功能)。

深入了解

安全假设

该crate依赖于假设不透明的对象不会被篡改,即vtable函数不会被修改。这通过使用隐藏的子模块来封装字段以确保这一点。然而,不可验证的用户(C库)仍然可能能够修改表。该库假定它们不是恶意的,并且不执行任何运行时验证。使用 abi_stable 进行API版本不匹配检查是一个可选功能(需要rustc 1.46+)。

除了在关联类型包装中的2位之外,该crate应该是安全的。

该crate使用了一些unsafe特性和自动实现它们,或者带有unsafe函数的特性和它们的使用在代码生成器内部应该是安全的,它们被标记为不允许手动实现从而引入未定义行为。

命名生成

#[cglue_trait] 宏为 MyTrait 将生成以下重要的类型

名称 用途 实例类型 上下文
MyTraitBox 常规的拥有CGlue对象。 CBox<c_void> NoContext
MyTraitCtxBox<Ctx> 拥有CGlue对象,具有上下文 CBox<c_void> Ctx
MyTraitArcBox 拥有CGlue对象,具有引用计数上下文。 CBox<c_void> CArc<c_void>
MyTraitMut 通过mut-ref的CGlue对象。 &mut c_void. NoContext
MyTraitCtxMut<Ctx> 通过mut-ref的CGlue对象,具有上下文。 &mut c_void. Ctx
MyTraitArcMut 通过mut-ref的CGlue对象,具有引用计数上下文。 &mut c_void. CArc<c_void>
MyTraitRef 通过ref(const)的CGlue对象。 &c_void. NoContext
MyTraitCtxRef<Ctx> 通过ref(const)的CGlue对象,具有上下文。 &c_void. Ctx
MyTraitArcRef 通过ref(const)的CGlue对象,具有引用计数上下文。 &c_void. CArc<c_void>

只有不透明类型提供功能。非不透明类型可以用作 Into 特性界限,并且需要类型检查特性界限。

这些是需要用于界限检查的泛型类型

名称 用途 实例类型 上下文
MyTraitBaseBox<T> 基本拥有CGlue对象。 CBox<T> NoContext
MyTraitBaseCtxBox<T, Ctx> 带有一些上下文的基本拥有CGlue对象。 CBox<T> Ctx
MyTraitBaseArcBox<T, Ctx> 带有引用计数上下文的基本拥有CGlue对象。 CBox<T> CArc<Ctx>
MyTraitBaseMut<T> 基本通过mut-ref的CGlue对象。 &mutT. NoContext
MyTraitBaseRef<T> 泛型通过ref(const)的CGlue对象的别称。 &T. NoContext
MyTraitBase<Inst, Ctx> 基本(非不透明)CGlue对象。它可以具有任何兼容的实例和上下文 Inst Ctx

最后,以下底层类型存在,但不需要在Rust中进行交互

名称 用途
MyTraitVtbl<C> 特质的全部函数表。对用户应该是透明的。
MyTraitRetTmp<Ctx> 临时返回值的结构。对用户应该是透明的。

相反,每个不透明的CGlue对象都实现了 MyTraitOpaqueObj 特性,其中包含vtable的类型。

cglue_trait_group! 宏为 MyGroup 将生成以下主要类型

名称 用途 实例类型 上下文
MyGroupBox 拥有CGlue特性组。 CBox<c_void> NoContext
MyGroupCtxBox<Ctx> 拥有CGlue特性组,具有一些上下文 CBox<c_void> Ctx
MyGroupArcBox 用于具有引用计数的上下文的不可见CGlue特质组类型的定义。 CBox<c_void> CArc<c_void>
MyGroupMut 用于不可见按引用修改的CGlue特质组类型的定义。 &mut c_void. NoContext
MyGroupCtxMut<Ctx> 用于具有自定义上下文的不可见按引用修改的CGlue特质组类型的定义。 &mut c_void. Ctx
MyGroupArcMut 用于具有引用计数的上下文的不可见按引用修改的CGlue特质组类型的定义。 &mut c_void. CArc<c_void>
MyGroupRef 用于不可见按引用(const)的CGlue特质组类型的定义。 &c_void. NoContext
MyGroupCtxRef<Ctx> 用于具有自定义上下文的不可见按引用(const)的CGlue特质组类型的定义。 &c_void. Ctx
MyGroupArcRef 用于具有引用计数的上下文的不可见按引用(const)的CGlue特质组类型的定义。 &c_void. CArc<c_void>

基本类型如下

名称 用途 实例类型 上下文
MyGroupBaseBox<T> 基本拥有的CGlue特质组。其容器是CBox<T>
MyGroupBaseCtxBox<T, Ctx> 具有一些上下文的基本拥有的CGlue特质组。 CBox<T> Ctx
MyGroupBaseArcBox<T, Ctx> 具有引用计数的上下文的基本拥有的CGlue特质组。 CBox<T> CArc<Ctx>
MyGroupBaseMut<T> 基本按引用修改的CGlue特质组。 &mutT. NoContext
MyGroupBaseCtxMut<T, Ctx> 具有上下文的基本按引用修改的CGlue特质组。 &mutT. Ctx
MyGroupBaseArcMut<T, Ctx> 具有引用计数的上下文的基本按引用修改的CGlue特质组。 &mutT. CArc<Ctx>
MyGroupBaseRef<T> 基本按引用(const)的CGlue特质组。 &T. NoContext
MyGroupBaseCtxRef<T, Ctx> 具有上下文的基本按引用(const)的CGlue特质组。 &T. Ctx
MyGroupBaseArcRef<T, Ctx> 具有引用计数的上下文的基本按引用(const)的CGlue特质组。 &T. CArc<Ctx>
MyGroup<Inst, Ctx> 组的基本定义。需要手动将其转换为不可见。 Inst Ctx

放置在组内的容器类型(对Rust用户不可见)

名称 用途
MyGroupContainer<Inst, Ctx> 存储临时返回存储。为此类型构建了虚表。

最后,对象要成为可分组对象所需的填充特质

名称 用途
MyGroupVtableFiller 允许对象通过使用enable_trait函数指定哪些可选特质可用,的特质。

宏生成还会为所使用的所有可选特质的组合生成结构。为了方便通过宏使用,内部可选特质的名称已按字母顺序排序。如果不使用宏,请检查MyGroup文档中的底层转换函数定义。

分组泛型

组相当灵活 - 它们不仅限于基本类型。它们还可以包含泛型参数、关联类型和self返回(这也适用于单特质对象)。

特质组中泛型的使用相当直接,但有几个小的细节。

使用标准模板语法定义一个组

cglue_trait_group!(GenGroup<T>, Getter<T>, { TA });

也可以指定特质界限

cglue_trait_group!(GenGroup<T: Eq>, Getter<T>, { TA });

或者

cglue_trait_group!(GenGroup<T> where T: Eq {}, Getter<T>, { TA });

在泛型类型上实现组

cglue_impl_group!(GA<T: Eq>, GenGroup<T>, { TA });

请注意,在上面的情况下,如果GA<T>实现了Getter<T>TA,则GA将是可分组的。如果GA使用不同类型参数的不同集合实现了可选特质,则提供多个实现,并指定类型。在每个实现中,仍然添加泛型类型T,但在某一行上指定其类型相等

cglue_impl_group!(GA<T = u64>, GenGroup<T>, {});
cglue_impl_group!(GA<T>, GenGroup<T = usize>, { TA });

在这里,GA<u64> 只实现了 Getter<T>,而 GA<usize> 则实现了 Getter<usize>TA

最后,你也可以混合使用这两个,假设最通用的实现定义了最多的可选特性。

cglue_impl_group!(GA<T: Eq>, GenGroup<T>, { TA });
cglue_impl_group!(GA<T = u64>, GenGroup<T>, {});

手动实现组

注意:如果启用了 unstable 特性,则不支持此功能。相反,您无需做任何事情!

还可以通过实现 MyGroupVtableFiller 来手动实现组。以下是上述两个宏调用的展开形式

impl<
        'cglue_a,
        CGlueInst: ::core::ops::Deref<Target = GA<T>>,
        CGlueCtx: cglue::trait_group::ContextBounds,
        T: Eq,
    > GenGroupVtableFiller<'cglue_a, CGlueInst, CGlueCtx, T> for GA<T>
where
    Self: TA,
    &'cglue_a TAVtbl<'cglue_a, GenGroupContainer<CGlueInst, CGlueCtx, T>,
    >:
        'cglue_a + Default,
    T: cglue::trait_group::GenericTypeBounds,
{
    fn fill_table(
        table: GenGroupVtables<'cglue_a, CGlueInst, CGlueCtx, T>,
    ) -> GenGroupVtables<'cglue_a, CGlueInst, CGlueCtx, T> {
        table.enable_ta()
    }
}
impl<
        'cglue_a,
        CGlueInst: ::core::ops::Deref<Target = GA<u64>>,
        CGlueCtx: cglue::trait_group::ContextBounds,
    > GenGroupVtableFiller<'cglue_a, CGlueInst, CGlueCtx, u64> for GA<u64>
{
    fn fill_table(
        table: GenGroupVtables<'cglue_a, CGlueInst, CGlueCtx, u64>,
    ) -> GenGroupVtables<'cglue_a, CGlueInst, CGlueCtx, u64> {
        table
    }
}

外部特质

某些特性可能不支持 #[cglue_trait] 注解。因此,存在一些机制来允许构建外部特性的 CGlue 对象。核心原语是 #[cglue_trait_ext]。本质上,用户需要为实际特性提供足够的定义,如下所示

#[cglue_trait_ext]
pub trait Clone {
    fn clone(&self) -> Self;
}

注意这个特性没有 clone_from 函数。不支持有单独的 &Self 参数,但该特性仍然可以实施,因为 clone_from 仅仅是一个可选优化,并且已经有一个通用的实现。

当构建单特性对象时,外部特性的使用方式相同。当涉及组时,情况会更加复杂。以下是 MaybeClone 组的实现方式

cglue_trait_group!(MaybeClone, { }, { ext::Clone }, {
    pub trait Clone {
        fn clone(&self) -> Self;
    }
});

第一个变化是使用 ext::Clone。这标记 cglue 创建外部特性粘合代码。第二部分是特性定义。是的,不幸的是,组需要另一个特性的定义。CGlue 没有包的上下文,需要知道函数签名。

这远远不够理想,因此存在另一个机制——内置的外部特性。它是一个存储特性定义的仓库,可以在不提供多个特性定义的情况下使用。由于 Clone 既是存储的一部分,也被标记为预导出,上述代码可以简化为以下内容

cglue_trait_group!(MaybeClone, { }, { Clone });

对于不在预导出中的特性,可以通过它们的完全限定 ::ext 路径来访问

cglue_trait_group!(MaybeAsRef<T>, { }, { ::ext::core::convert::AsRef<T> });

请注意,use 导入不起作用——需要完全限定的路径。

特性存储是本系统最不完整的一部分。如果您遇到缺少的特性并希望使用它们,请提交一个包含它们定义的拉取请求,我将很高兴将其包含在内。

类型包装

至于细节,常用的 Rust 结构会自动以有效的方式封装。

例如,切片和 str 类型会被转换为与 C 兼容的切片。

fn with_slice(&self, slice: &[usize]) {}

// Generated vtable entry:

with_slice: extern "C" fn(&CGlueC, slice: CSlice<usize>),

无法进行 可空指针优化Option 类型会被封装成 COption

fn non_npo_option(&self, opt: Option<usize>) {}

// Generated vtable entry:

non_npo_option: extern "C" fn(&CGlueC, opt: Option<usize>),

Result 会自动封装成 CResult

fn with_cresult(&self) -> Result<usize, usize> {}

// Generated vtable entry:

with_cresult: extern "C" fn(&CGlueC) -> CResult<usize, usize>,

结果IntError 类型返回时,可以将 Ok 值写入变量中

#[int_result]
fn with_int_result(&self) -> Result<usize> {}

// Generated vtable entry:

with_int_result: extern "C" fn(&CGlueC, ok_out: &mut MaybeUninit<usize>) -> i32,

所有包装和转换都在幕后透明地处理,并受用户控制。

关联类型包装

相关类型可以包装到自定义的 CGlue 对象中。下面是一个简单的示例

use cglue::*;
#[cglue_trait]
pub trait ObjReturn {
    #[wrap_with_obj(InfoPrinter)]
    type ReturnType: InfoPrinter + 'static;

    fn or_1(&self) -> Self::ReturnType;
}

struct InfoBuilder {}

impl ObjReturn for InfoBuilder {
    type ReturnType = Info;

    fn or_1(&self) -> Self::ReturnType {
        Info {
            value: 80
        }
    }
}

let builder = InfoBuilder {};

let obj = trait_obj!(builder as ObjReturn);

let info_printer = obj.or_1();

info_printer.print_info();

如果特徵返回类型为 &Self::ReturnType&mut Self::ReturnType,这也同样适用。这是通过将包装后的返回值存储在中间存储中,然后返回对这些存储的引用来实现的。

然而,这里有一个 SAFETY WARNING

在将 &Self::ReturnType 包装到接收非可变引用 &self 的函数中时,从技术上讲,由于可能覆盖已作为 const 借用的数据,这违反了 Rust 的安全性规则。然而,在现实世界中,接收 &self 并返回 &T 的函数通常会返回相同的引用,这应该没问题,但是你要注意了。 TODO: Disallow this?

上述警告不适用于 &mut self 函数,因为返回的引用绑定到相同的生命周期,并且在借用期间不能重新创建。

此外,当在匿名生命周期引用中包装相关类型时,会破坏相当多的类型安全性。这应该没问题,但情况如下

  1. 由于没有 GAT,CGlueObjRef/Mut<'_> 正在提升为 CGlueObjRef/Mut<'static>。这应该没问题,因为无法克隆非 CBox 对象,并且这些对象是通过引用返回的,而不是值(有关如何避免此情况的说明,请参阅 GAT 部分)。

  2. 特徵界限只针对一个生命周期进行检查(vtable 的生命周期),并且 C 函数正在不安全地将 HRTB 转换为 vtable。这是因为无法指定 HRTB 的上界(for<'b: 'a>)。这应该没问题,因为可以为目标的生命周期创建 vtable,返回的引用不会超过 vtable 的生命周期,并且 C 函数在其他方面已完全类型检查。

然而,如果您发现了明显的遗漏,并且有解决这种不安全性的方法,请提交问题报告。

一般来说,您需要在 Self::ReturnType 函数中使用 wrap_with_obj/wrap_with_group,在 &mut Self::ReturnType 函数中使用 wrap_with_obj_mut/wrap_with_group_mut,在 &Self::ReturnType 函数中使用 wrap_with_obj_ref/wrap_with_group_ref。需要注意的是,如果有一个返回这些类型组合的特质,则无法使用包装,因为底层对象类型不同。如果可能的话,请将类型拆分为多个关联类型。

泛型关联类型

CGlue 对 GATs 的支持有限!更具体地说,只支持单生命周期 GATs,这允许实现一种形式的 LendingIterator

use cglue::*;
#[cglue_trait]
pub trait LendingPrinter {
    #[wrap_with_obj(InfoPrinter)]
    type Printer<'a>: InfoPrinter + 'a where Self: 'a;

    fn borrow_printer<'a>(&'a mut self) -> Self::Printer<'a>;
}

impl<'a> InfoPrinter for &'a mut Info {
    fn print_info(&self) {
        (**self).print_info();
    }
}

struct InfoStore {
    info: Info,
}

impl LendingPrinter for InfoStore {
    type Printer<'a> = &'a mut Info;

    fn borrow_printer(&mut self) -> Self::Printer<'_> {
        &mut self.info
    }
}

let builder = InfoStore { info: Info { value: 50 } };

let mut obj = trait_obj!(builder as LendingPrinter);

let info_printer = obj.borrow_printer();

info_printer.print_info();

插件系统

完整的示例可以在仓库的 examples 子目录中找到。

CGlue 目前不提供现成的插件系统,但已提供用于使用动态加载库进行相对安全特质使用的原语。核心原语是一个可克隆的上下文,例如 libloading::Library Arc,它将保持库打开,直到所有 CGlue 对象被丢弃。

use cglue::prelude::v1::*;

#[cglue_trait]
pub trait PluginRoot {
    // ...
}

impl PluginRoot for () {}

let root = ();
// This could be a `libloading::Library` arc.
let ref_to_count = CArc::from(());
// Merely passing a tuple is enough.
let obj = trait_obj!((root, ref_to_count) as PluginRoot);
// ...

通过引用计数 Arc,可以确保动态加载的库不会被提前卸载。

如果 PluginRoot 分支扩展并构建可以在 PluginRoot 实例之后丢弃的新对象,例如一个 InfoPrinter 对象,Arc 就会被移动/克隆到新对象中。

#[cglue_trait]
pub trait PluginRoot {
    #[wrap_with_obj(InfoPrinter)]
    type PrinterType: InfoPrinter;

    fn get_printer(&self) -> Self::PrinterType;
}

impl PluginRoot for () {
    type PrinterType = Info;

    fn get_printer(&self) -> Self::PrinterType {
        Info { value: 42 }
    }
}

let root = ();
// This could be a `libloading::Library` arc.
let ref_to_count = CArc::from(());
let obj = trait_obj!((root, ref_to_count) as PluginRoot);
let printer = obj.get_printer();
// It is safe to drop obj now:
std::mem::drop(obj);
printer.print_info();

请注意,这并不是万无一失的,可能存在返回数据依赖于库的情况。最容易出现错误的是未处理的 Err(E) 条件,其中 E 是某个静态 str。main 函数可能返回一个指向库内存的错误,卸载它,然后尝试打印它,导致段错误。如果可能的话,请尝试使用 IntError 类型,并使用 #[int_result] 标记特质,以防止此类问题发生。

与 cbindgen 一起工作

可以使用 cbindgen 生成 C 和 C++ 绑定。需要进行一些重要的设置。

此外,cglue-bindgen 还提供了额外的辅助方法生成,使从 C/C++ 使用 CGlue 更加方便。

设置

首先,创建一个 cbindgen.toml,并确保包括 cglue 以及使用 cglue 的任何 crates,并且启用宏展开。

[parse]
parse_deps = true
include = ["cglue", "your-crate"]

[parse.expand]
crates = ["cglue", "your-crate"]

当前,宏展开需要 nightly Rust。因此,可以生成绑定如下。

rustup run nightly cbindgen --config cbindgen.toml --crate your_crate --output output_header.h

您可以通过添加 -l c-l c++ 标志来设置 C 或 C++ 语言模式。或者,在 toml 中设置它。

language = "C"

导出任何未在 extern C 函数中使用的缩短的 typedef。

[export]
include = ["FeaturesGroupArcBox", "PluginInnerRef", "PluginInnerMut"]

cglue-bindgen

cglue-bindgen是一个cbindgen包装器,旨在自动清理头文件。它还添加了使用+nightly标志自动调用夜间Rust的能力,并生成用于简化使用的vtable包装器。更改很简单——只需将所有cbindgen参数移动到--之后即可。

cglue-bindgen +nightly -- --config cbindgen.toml --crate your_crate --output output_header.h

这个包装器可能是CGlue中最脆弱的部分——如果有什么问题,请提交问题报告。在未来,我们将努力将CGlue直接集成到cbindgen中。

限制

  1. 由于不透明转换是单向的,因此无法实现关联类型函数参数。

  2. 由于相同的原因,无法为接受额外Self类型的函数实现。

  3. cglue特质的自定义泛型参数目前尚不支持,但计划进行改进。

  4. 在路径导入方面可能存在一些边缘情况。如果您发现任何情况,请提交问题报告:)

不稳定特性

cglue_impl_group可能会迫使您在保守的特例选择上做出选择,因为目前无法使用稳定的Rust功能对这些情况进行特化。但这并不总是理想的。您可以通过启用unstable功能来解决此问题。

此功能将cglue_impl_group变为无操作,并自动为给定对象启用最宽泛的特例集。

要使用它,您需要

  • 夜间Rust编译器。

  • 在构建时设置环境变量RUSTC_BOOTSTRAP=try_default

请注意,然而,这两种选择中的任何一种都会使Rust的稳定性保证无效。

使用 CGlue 的项目

如果您想将项目添加到列表中,请提交问题报告:)

变更日志

它在CHANGELOG.md文件中可用。

依赖项

~4–5.5MB
~110K SLoC