#abi #cglue #macro #cbindgen #api-bindings

cglue-gen

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

14 个版本

新版本 0.3.1 2024 年 7 月 29 日
0.2.9 2023 年 6 月 10 日
0.2.7 2022 年 11 月 17 日
0.2.5 2022 年 6 月 28 日
0.1.3 2021 年 7 月 23 日

FFI 中排名 130

Download history 236/week @ 2024-04-14 727/week @ 2024-04-21 976/week @ 2024-04-28 470/week @ 2024-05-05 281/week @ 2024-05-12 315/week @ 2024-05-19 246/week @ 2024-05-26 223/week @ 2024-06-02 213/week @ 2024-06-09 221/week @ 2024-06-16 178/week @ 2024-06-23 104/week @ 2024-06-30 119/week @ 2024-07-07 288/week @ 2024-07-14 262/week @ 2024-07-21 676/week @ 2024-07-28

每月下载 1,355
23 个 crate 中使用(通过 cglue-macro

MIT 许可证

305KB
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/API验证,使用abi_stable (启用 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的类型。

MyGroup 定义的 cglue_trait_group! 宏将生成以下主要类型

名称 用途 实例类型 上下文
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> 存储临时返回存储。为此类型构建了虚表。

最后,对象需要实现以便成为可分组的填充特性

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

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

分组泛型

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

在 traits 组中使用泛型相当简单,但有几个细微差别。

使用标准模板语法定义组

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

也可以指定 traits 约束

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 对于 T: Eq 时才能分组。如果 GA 实现了具有不同类型参数的不同集合的可选 traits,则提供多个实现,并指定类型。在每个实现中,仍然添加一个泛型类型 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>,

具有 IntError 类型的 Result 可以返回一个整数代码,并将 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 的函数中封装 &Self::ReturnType,从技术上讲,可能会违反Rust的安全规则,因为它可能会覆盖已经作为const借用的数据。然而,在现实世界中,接收 &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 是某些静态字符串。在 main 函数中,返回一个指向库内存的错误,卸载它,然后尝试打印它,可能会导致段错误。如果可能的话,请尝试使用 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
~84K SLoC