#abi #cglue #macro #cbindgen

cglue-macro

用于制作插件和C兼容库的FFI安全代码生成宏

6个版本

0.3.0 2024年7月27日
0.2.3 2022年11月16日
0.2.2 2022年2月19日
0.2.1 2021年12月19日
0.0.0 2021年5月22日

#6#cbindgen

Download history 240/week @ 2024-04-14 737/week @ 2024-04-21 980/week @ 2024-04-28 478/week @ 2024-05-05 290/week @ 2024-05-12 320/week @ 2024-05-19 255/week @ 2024-05-26 217/week @ 2024-06-02 220/week @ 2024-06-09 230/week @ 2024-06-16 182/week @ 2024-06-23 105/week @ 2024-06-30 117/week @ 2024-07-07 304/week @ 2024-07-14 272/week @ 2024-07-21 533/week @ 2024-07-28

1,235 每月下载次数
22 个crate中(通过 cglue)使用

MIT 许可证

350KB
6K 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不能保证你的代码将与两个不同的编译器版本冲突任何其他小更改一起工作,CGlue以这种方式将其全部粘合在一起。

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

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 为通过mut-ref访问的不可见CGlue特质组的类型定义。 &mut c_void. NoContext
MyGroupCtxMut<Ctx> 为具有自定义上下文的通过mut-ref访问的不可见CGlue特质组的类型定义。 &mut c_void. Ctx
MyGroupArcMut 为具有引用计数的上下文的通过mut-ref访问的不可见CGlue特质组的类型定义。 &mut c_void. CArc<c_void>
MyGroupRef 为通过ref访问(const)的不可见CGlue特质组的类型定义。 &c_void. NoContext
MyGroupCtxRef<Ctx> 为具有自定义上下文的通过ref访问(const)的不可见CGlue特质组的类型定义。 &c_void. Ctx
MyGroupArcRef 为具有引用计数的上下文的通过ref访问(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> 基本通过mut-ref访问的CGlue特质组。 &mutT. NoContext
MyGroupBaseCtxMut<T, Ctx> 基本带有上下文的通过mut-ref访问的CGlue特质组。 &mutT. Ctx
MyGroupBaseArcMut<T, Ctx> 基本带有引用计数的上下文的通过mut-ref访问的CGlue特质组。 &mutT. CArc<Ctx>
MyGroupBaseRef<T> 基本通过ref访问(const)的CGlue特质组。 &T. NoContext
MyGroupBaseCtxRef<T, Ctx> 基本带有上下文的通过ref访问(const)的CGlue特质组。 &T. Ctx
MyGroupBaseArcRef<T, Ctx> 基本带有引用计数的上下文的通过ref访问(const)的CGlue特质组。 &T. CArc<Ctx>
MyGroup<Inst, Ctx> 组的基本定义。需要手动将其设置为不可见。 Inst Ctx

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

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

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

名称 用途
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实现了具有不同类型参数的不同集合的可选特质,则提供多个实现,并指定类型。在每个实现中,仍然添加一个泛型类型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>),

无法进行 null 指针优化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>,

Result 使用 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: 禁止此操作?

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

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

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

  2. 特质界限只检查一个生命周期(vtable 的生命周期),并且 C 函数正在被不安全地转换为 HRTB。这是因为无法指定 HRTB 上界(for<'b: 'a>)。应该没问题,因为可以为 vtable 的生命周期创建 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 是某些静态字符串。主函数可能返回一个指向库内存的错误,卸载它,然后尝试打印出来,从而导致段错误。如果可能,请尝试使用 IntError 类型,并将特质标记为 #[int_result],这将防止此类特定问题发生。

与cbindgen一起工作

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

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

设置

首先,创建一个 cbindgen.toml 文件,并确保 cglue 以及任何使用 cglue 的 crate 都被包含在内,并启用了宏展开。

[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"

导出任何不被任何外部 C 函数使用的缩写类型定义。

[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成为无操作,并自动为给定对象启用最广泛的特质集。

要使用它,您需要

  • nightly Rust编译器。

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

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

使用CGlue的项目

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

变更日志

它可在CHANGELOG.md文件中找到。

依赖关系

~4MB
~83K SLoC