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
1,709 下载/每月
用于 21 个 Crates (6 直接)
190KB
3.5K SLoC
CGlue
如果所有代码都粘合在一起,我们的粘合剂就是市场上最安全的。
最完整的动态特质对象实现,毫无疑问。
概述
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();
}
还有更多!以下是一些亮点
-
能够使用自消耗的特质函数。
-
暴露了一些标准库特质(
Clone
)。 -
能够将关联特质类型包装成新的 CGlue 特质对象和组。
-
上述功能也适用于可变和const引用关联类型返回值。
-
泛型特性和它们的组。
-
使用 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
函数,因为返回的引用绑定到相同的生命周期,并且在借用期间不能重新创建。
此外,当在匿名生命周期引用中包装相关类型时,会破坏相当多的类型安全性。这应该没问题,但情况如下
-
由于没有 GAT,
CGlueObjRef/Mut<'_>
正在提升为CGlueObjRef/Mut<'static>
。这应该没问题,因为无法克隆非 CBox 对象,并且这些对象是通过引用返回的,而不是值(有关如何避免此情况的说明,请参阅 GAT 部分)。 -
特徵界限只针对一个生命周期进行检查(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中。
限制
-
由于不透明转换是单向的,因此无法实现关联类型函数参数。
-
由于相同的原因,无法为接受额外
Self
类型的函数实现。 -
cglue特质的自定义泛型参数目前尚不支持,但计划进行改进。
-
在路径导入方面可能存在一些边缘情况。如果您发现任何情况,请提交问题报告:)
不稳定特性
cglue_impl_group
可能会迫使您在保守的特例选择上做出选择,因为目前无法使用稳定的Rust功能对这些情况进行特化。但这并不总是理想的。您可以通过启用unstable
功能来解决此问题。
此功能将cglue_impl_group
变为无操作,并自动为给定对象启用最宽泛的特例集。
要使用它,您需要
-
夜间Rust编译器。
-
在构建时设置环境变量
RUSTC_BOOTSTRAP=try_default
。
请注意,然而,这两种选择中的任何一种都会使Rust的稳定性保证无效。
使用 CGlue 的项目
如果您想将项目添加到列表中,请提交问题报告:)
变更日志
它在CHANGELOG.md文件中可用。
依赖项
~4–5.5MB
~110K SLoC