44个版本 (稳定版)
| 36.1.1 | 2024年7月31日 |
|---|---|
| 6.4.1-rc1 | 2024年6月27日 |
| 5.1.0 | 2024年6月7日 |
| 4.0.5 | 2024年4月9日 |
| 0.1.5 | 2023年3月30日 |
#55 in FFI
每月38,377次下载
用于13个crate(通过stabby)
500KB
11K SLoC
[!WARNING]
由于Rust 1.78中的破坏性更改,stabby对trait对象的实现可能会引起性能问题
- 仅受非夜间、>= 1.78版本的Rust影响
- 支持trait对象的v-table现在被插入到一个全局无锁集合中。
- 此集合是泄漏的:
valgrind会 对您表示不满。- 此集合的大小随着不同
(类型, trait-集合)对的数量的增加而增长。其当前实现是一个向量
- 通过线性搜索(O(n))进行查找,对于小于100个元素的情况下保持最快。
- 通过克隆向量(O(n))并原子地替换它来进行插入,在发生冲突时重复此操作。
- 正在努力用不可变的b-tree映射替换此实现(如果发现它们比当前实现慢得多,则将取消此操作)。
此说明将根据情况发展而更新。同时,如果您的项目使用许多
stabby定义的trait对象,我建议使用nightly或编译器的< 1.78版本。
具有紧凑求和类型的Rust稳定ABI
stabby 是您创建稳定二进制接口的一站式解决方案,可以轻松地创建共享库,而无需让您的类型(枚举)爆炸式增长。
您与 stabby 的主要交互方式是 #[stabby::stabby] proc-macro,您可以用它来注解很多东西。
为什么我 需要 稳定的 ABI?ABI 又是什么意思?
ABI 代表 应用程序二进制接口,它是 API 的更详细的兄弟。虽然 API 定义了函数期望的数据类型及其属性,但 ABI 定义了 如何 在内存中布局这些数据,以及函数调用是如何工作的。
内存中数据的布局通常被称为“表示”:字段顺序、枚举变体的区分、填充、大小等。为了使用某些类型进行通信,两个软件单元 必须 就这些类型在内存中的样子达成一致。
函数调用在底层也非常复杂(尽管开发者很少需要考虑它们):调用者或被调用者负责保护调用者的寄存器不受被调用者操作的影响?参数应该在哪些寄存器/栈顺序上传递?实际触发调用的 CPU 指令是什么?对这些问题的回答(以及一些其他问题)被称为“调用约定”。
在 Rust 中,除非您通过 #[repr(_)] 显式选择您的类型的已知表示,或者通过 extern "_" 显式为您的函数选择调用约定,否则编译器可以随意处理这些软件的方面:它处理这些方面的过程是明确不稳定的,取决于您的编译器版本、您选择的优化级别、伯克郡附近一个羊毛农场里某头 llama 的心情……谁知道呢?
当涉及到动态链接时,这个问题就出现了:由于 Rust 中大多数内容的 ABI 都是不稳定的,通过不同编译器调用构建的软件单元(如动态库和需要它的可执行文件)可能会就这些 ABI 决策达成一致,尽管链接器无法知道这一点。
具体来说,这可能意味着您的可执行文件认为 Vec<Potato> 的前 8 个字节是指向堆分配的指针,而库认为它们是长度。这也可能意味着当库的函数被调用时,库认为它可以随意破坏寄存器,而可执行文件依赖它在返回之前保存并恢复它们。
stabby 通过帮助您锁定程序子集的 ABI 来帮助您解决这些问题,同时帮助您保留一些 rustc 在使用其不稳定 ABI 时提供的布局优化。此外,stabby 允许您以作为 stabby::abi::IStable 类型依赖版本检查的方式注解函数导出和导入。
结构体
当您用 #[stabby::stabby] 注解结构体时,会发生两件事。
- 结构体会变成
#[repr(C)]。除非您指定了其他选项或者您的结构体具有泛型字段,否则stabby将会在编译时断言您没有以次优的方式排列字段。 stabby::abi::IStable将为您实现类型。它类似于abi_stable::Stable,但通过关联类型表示布局(包括空隙)。这是在枚举中提供空隙优化(至少,直到#[feature(generic_const_exprs)]变得稳定)的关键。
枚举
当您用 #[stabby::stabby] 注解枚举时,您可以选择现有的稳定表示(就像您必须使用 abi_stable 一样),但您也可以选择 #[repr(stabby)](默认表示)让 stabby 将您的枚举转换为带标签的联合体,但有一个特点:标签可能是一个 ZST,它可以检查联合体以模拟 Rust 的空隙优化。
请注意,使用 #[repr(stabby)] 会使您失去模式匹配的能力。
由于特质的求解器限制,使用 #[repr(stabby)] 枚举有一些边缘情况。
- 编译时间因
#[repr(stabby)]枚举而受影响。 - 在编写泛型枚举的
impl-块时,需要额外的特质界限。它们将始终是形如一个或多个A: stabby::abi::IDeterminantProvider<B>界限(尽管rustc的错误可能建议更复杂的界限,但界限始终应该是这种IDeterminantProvider形状)。
#[repr(stabby)] 枚举被实现为平衡二叉树,其结构为 stabby::result::Result<Ok, Err>,因此区分符总是通过以下过程在两种类型之间计算
- 如果某些
Err的禁止值(例如,对于非零类型,考虑0)适合在Ok不关心的位中,则该值用于表示我们处于Ok变体。 - 尝试使用
Err和Ok互换其角色做同样的事情。 - 如果找不到单个值区分符,则将
Ok和Err的未使用位相交。如果存在交集,则使用最低有效位,而其他位则保留作为包含Result<Ok, Err>变体的累加类型潜在空间。 - 如果找不到任何空间,则将两个类型中最小的一个向右移位,并重复尝试过程。如果联合体的大小会变大,或者尝试了 8 次,则停止移位过程。如果在找到空间之前过程停止,则将使用单个位作为决定因素(将联合体向右移位,其自身对齐,其中
1表示Ok)。
联合体
如果您想创建自己的内部标记联合体,您可以使用 #[stabby::stabby] 标记它们,让 stabby 确保您只使用了稳定的变体,并让它知道您联合体的大小和对齐方式。请注意,stabby 总是认为联合体没有空间。
特质
当您使用 #[stabby::stabby] 注解特质时,将为其生成一个 ABI 稳定的 v-table。然后,您可以使用以下类型等价性中的任何一个:
&'a dyn Traits→DynRef<'a, vtable!(Traits)>或dynptr!(&'a dyn Trait)&'a mut dyn Traits→Dyn<&'a mut (), vtable!(Traits)>或者dynptr!(&'a mut dyn Traits)Box<dyn Traits + 'a>→Dyn<'a, Box<()>, vtable!(Traits)>或者dynptr!(Box<dyn Traits + 'a>)Arc<dyn Traits + 'a>→Dyn<'a, Arc<()>, vtable!(Traits)>或者dynptr!(Arc<dyn Traits + 'a>)
请注意,vtable!(Traits) 和 dynptr!(..dyn Traits..) 支持任何数量的特质:例如 vtable!(TraitA + TraitB<Output = u8>) 或者 dynptr!(Box<dyn TraitA + TraitB<Output = u8>>) 都是有效的,但顺序必须保持一致。
然而,stabby生成的v表不会考虑超特质。
为了使 stabby::dynptr!(Box<dyn Traits + 'a>) 具有特质 Trait 的方法,您需要使用 use trait::{TraitDyn, TraitDynMut};,确保您不会意外地密封这些特质,这些特质会自动与您的 Trait 具有相同的可见性。
stabby::closure 导出 CallN、CallMutN 和 CallOnceN 特质,其中 N(在 0..=9)是参数数量,分别是 Fn、FnMut 和 FnOnce 的 ABI 稳定等价物。
从版本 1.0.1 开始,#[stabby::stabby] 生成的 v-table 假定所有方法参数都是 ABI 稳定的,以防止 rustc 冻结的风险。除非您的特质有引用其自身 v-table 的方法,否则建议使用 #[stabby::stabby(checked)] 来避免即使其接口中的一些类型实际上不稳定,v-table 仍被标记为稳定。
由于 stabby 在放置在特质上时会产生一个额外的 struct,您可能想要向该 struct 的声明中添加属性。您可以这样做,您可以使用 #[stabby::vt_attr(your_attribe = "goes here")]。任何其他属性都将放置在原始特质上。
函数
#[stabby::stabby]
使用 #[stabby::stabby] 标注函数会使其成为 extern "C"(但不是 #[no_mangle]),并检查其签名以确保所有交换的类型都标记为 stabby::abi::IStable。您还可以指定您选择的调用约定。
#[stabby::导出]
与 #[stabby::stabby] 工作方式相同,但会在标注的函数中添加 #[no_mangle],并生成另外两个无混叠函数
extern "C" fn <fn_name>_stabbied(&stabby::abi::report::TypeReport) -> Option<...>,如果类型报告与<fn_name>的签名匹配,将返回<fn_name>作为函数指针,确保它们确实具有相同的签名。extern "C" fn <fn_name>_stabbied_report() -> &'static stabby::abi::report::TypeReport将返回<fn_name>的类型报告,允许在之前函数返回None时进行调试。
#[stabby::导出(金丝雀)]
适用于任何函数,包括那些会被 FFI-不安全函数。除了在原始函数中添加 #[no_mangle],它还会向生成的共享库中添加一组小的 <fn_name>_<canary> 符号。这些金丝雀包括 rustc 的版本、优化级别以及其他可能导致编译器为 <fn_name> 使用不同 ABI 的属性。
当加载共享库时,链接器可以检查这些符号的存在,防止在加载器请求不兼容版本的金丝雀时进行链接。
#[stabby::导入(...)]
使用此功能注释一个 extern 块与以下代码等效:#[link(...)],但符号将使用 <fn_name>_stabbied 进行惰性初始化,确保在调用之前函数参数的报告中保持一致。
如果您想在出现潜在不匹配错误时避免恐慌,可以调用 <fn_name>.as_ref(),这将允许您在失败的情况下检查 <fn_name> 的报告。
#[stabby::导入(金丝雀="rustc, opt_level", ...)]
使用此功能注释一个 extern 块与以下代码等效:#[link(...)],但需要对应您的规范的canary才能进行链接。这类似于 export(canaries),它总是导出所有可用的canary,但您可以从以下集合中选择您想启用的canary。
paranoid:启用所有canary,如果您使用canaries="",则会选择此选项。rustc:启用对rustc版本(总是到commit版本)的canary。opt_level:启用对opt_level的canary(对于extern "rust" fn是必要的,因为优化级别可能会更改调用约定)。target:启用用于构建对象的编译器目标三联组的canary。num_jobs(paranoid):启用用于构建对象的作业数量的canary。这可能影响优化,因此可能会影响ABI(未经证实)。debug(paranoid):启用是否使用调试符号构建对象的canary。对ABI的影响未经证实,但未排除。host(paranoid):启用由编译器设置的编译器主机三联组的canary。对ABI的影响未经证实,但未排除。none:主要是在您自己的风险下完全禁用canary。
stabby::libloading::StabbyLibrary 特性
为 libloading::Library 添加了额外的功能,这些功能公开了符号获取器,如果canary不存在或报告不匹配,则这些获取器会失败。
这些方法仍然被认为是非安全的,但它们将减少意外加载ABI不兼容代码的风险。报告还充当运行时类型检查,降低误写符号的风险。
异步
在稳定类型上实现 core::future::Future 的工作,无论该稳定类型是在 FFI 边界哪一侧构建的,都是通用的。然而,由异步块和异步函数创建的 future 不可作为 ABI 稳定使用,因此必须通过特质对象使用。
stabby 通过 stabby::future::Future 特质支持 future。异步函数通过 #[stabby::stabby] 转换为返回 Dyn<Box<()>, vtable!(stabby::future::Future + Send + Sync)>(可以通过使用 #[stabby::stabby(unsync, unsend)] 来移除 Send 和 Sync 的界限),它本身实现了 core::future::Future。
stabby 目前还不支持异步特质,但您可以使用以下模式来实现它们
use stabby::{slice::SliceMut, future::DynFuture};
#[stabby::stabby]
pub trait AsyncRead {
fn read<'a>(&'a mut self, buffer: SliceMut<'a, [u8]>) -> DynFuture<'a, usize>;
}
impl MyAsyncTrait for SocketReader {
extern "C" fn read<'a>(&'a mut self, mut buffer: SliceMut<'a, [u8]>) -> DynFuture<'a, usize> {
Box::new(
async move {
let slice = buffer.deref_mut();
let read = SocketReader::read_async(&mut self.socket, slice).await;
buffer = slice.into();
read
}
).into()
}
}
增量稳定性
stabby 还允许您使用 stabby::abi::StableLike 告诉它某些东西是 ABI 稳定的,即使您无法始终使用 #[stabby::stabby]。
结合 stabby::compiler_version 中的 ZST,它实现 stabby::IStable,但仅在用相应版本的编译器编译时才有效。这可以让您声明某些类型只有在用适当的编译器版本编译时才是稳定的。但是,即使没有使用相应版本的编译器,ZST 仍然存在,因此这些类型仍然可以在没有 stabby::IStable 界限的地方使用。
stabby 的“宣言”
stabby 的开发是为了解决 Rust 生态系统中缺乏 ABI 稳定性的问题,这使得编写插件和其他基于动态链接的程序变得痛苦。目前,Rust 唯一的稳定 ABI 是 C ABI,它没有 sum-type 的概念,更不用说特定用途的利用了。
然而,我们的软件工程经验表明,类型大小对性能有很大影响,因此 sum-type 应该以占用空间最少的方式编码。
我对 stabby 的希望有两个版本
- 在Rust生态系统中的采用:这并不是我最喜欢的选择,但至少在需要动态链接的情况下,这可以让人们更好地使用Rust。
- 关于为Rust提供版本化而不是稳定ABI的讨论:
stabby实际上已经通过选定的stabby-abi包版本提供了版本化ABI。然而,让库实现通常由编译器负责的类型布局,强制要求类型稳定性是针对每个类型显式的,而不是适用于整个编译单元。在我看来,在Cargo清单中添加一个abi = "<stabby/crabi/c>"键将是一种更好的做法。更好的是,将这个想法与RFC 3435合并,以允许按函数选择ABI,并让编译器将所选稳定ABI污染到带有注释的函数接口的类型中,这将更加精细,但仍然允许最终用户通过承诺依赖关系的单个版本来成为ABI稳定的。
stabby的SemVer策略
Stabby在其公共API中包含其所有stabby_abi::IStable实现:对IStable类型内存表示的任何更改都是一个破坏性更改,这将导致MAJOR版本更改。
从6.1.1开始,Stabby遵循SemVer Prime,使用api, abi作为键。以下是您可以这样解释的几种方式
stabby.version[level] = 2^(api[level]) * 3^(abi[level])让您计算stabby的ABI和API的确切版本。- 当升级stabby时,您可以通过除以前一个版本来检查发生了什么变化:如果除法结果是2的倍数,则更改影响了API;如果是3的倍数,则影响了ABI。
- ABI版本化
- 将新类型添加到ABI稳定类型集将增加ABI补丁。
- 以任何方式修改现有类型的ABI将增加ABI主要。
- API版本化严格遵循SemVer策略。任何在文档中可见的API都被视为公共API,以及文档中提到的所有合同。
- ABI版本化
依赖关系
~8–15MB
~98K SLoC