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.3 | 2023 年 3 月 30 日 |
#323 in FFI
每月 44,576 次下载
在 13 个软件包中使用 (通过 stabby-abi)
175KB
4K SLoC
[!WARNING]
由于 Rust 1.78 的破坏性更改,stabby
的 trait 对象实现可能会引起性能问题
- 仅受非夜间版,>= 1.78 版本的 Rust 影响
- 现在将 trait 对象的 v-table 插入到全局无锁集中。
- 这个集是泄漏的:
valgrind
会 对你生气。- 这个集随着不同的
(type, trait-set)
对的数量增长。其当前实现是一个向量
- 查找是通过线性搜索 (O(n)) 完成的,对于 <100 个元素,它仍然是最快的。
- 插入是通过克隆向量 (O(n)) 并原子地替换它来完成的,如果发生冲突,则重复此操作。
- 正在努力用不可变 b-tree 映射替换此实现(如果发现比当前实现慢得多,则将其丢弃)。
随着情况的发展,此说明将进行更新。同时,如果您的项目使用了许多
stabby
定义的 trait 对象,我建议使用nightly
或编译器的 <1.78
版本。
Rust 的具有紧凑求和类型的稳定 ABI
stabby
是您创建共享库的稳定二进制接口的一站式商店,而无需让您的求和类型(枚举)爆炸。
您与 stabby
的主要交互方式是 #[stabby::stabby]
proc-macro,您可以通过它来注释许多内容。
为什么我 需要 一个稳定的ABI?ABI究竟是什么意思?
ABI代表应用程序二进制接口(Application Binary Interface),它是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-table 不会考虑超特性。
为了让 stabby::dynptr!(Box<dyn Traits + 'a>)
具有特性 'Trait' 的方法,您需要 use trait::{TraitDyn, TraitDynMut};
,所以请确保您不会意外地密封这些特性,这些特性与您的 Trait
具有相同的可见性。
stabby::closure
导出了CallN
、CallMutN
和CallOnceN
特质,其中N
(在0..=9
)是参数的数量,作为ABI稳定的Fn
、FnMut
和FnOnce
的等价物。
从版本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]
并生成两个其他无打乱(no-mangle)函数
- 外部函数
fn_name_stabbied(&stabby::abi::report::TypeReport) -> Option<.>
将返回一个函数指针,如果类型报告与fn_name
的签名匹配,确保它们确实具有相同的签名。 - 外部函数
fn_name_stabbied_report() -> &static stabby::abi::report::TypeReport
将返回fn_name
的类型报告,允许在先前函数返回None
时进行调试。
#[stabby::导出(金丝雀)]
此工具适用于任何函数,包括FFI不安全的函数。除了在原始函数上添加#[no_mangle]
之外,它还会在生成的共享库中添加一组小的fn_name_canary
符号。这些金丝雀包括rustc
的版本、优化级别以及其他可能使编译器使用不同ABI的属性。
当加载共享库时,链接器会检查这些符号的存在,以防止加载器请求不兼容版本的金丝雀时进行链接。
#[stabby::导入(...)]
使用此注解标注extern
块相当于使用#[link(...)]
,但符号将通过使用fn_name_stabbied
进行懒加载初始化,确保在调用它之前函数参数的报告是一致的。
如果您想要在不恐慌的情况下处理潜在的不匹配错误,可以调用fn_name.as_ref()
,这将在失败的情况下允许您检查fn_name
的报告。
#[stabby::导入(金丝雀="rustc, opt_level", ...)]
使用此方法注释extern
块等价于#[link(...)]
,但需要对应您规格的canaries才能实现链接。这类似于export(canaries)
,它总是导出所有可用的canaries,但您可以从以下集合中选择要启用的canaries。
paranoid
:启用所有canaries,如果您使用canaries=""
,则也会选择此项。rustc
:启用rustc
版本上的canary(始终是最新的commit
版本)。opt_level
:启用opt_level
上的canary(对于extern "" fn
是必要的,因为优化级别可能会改变调用约定)。target
:启用用于构建对象的编译器目标三元组的canary。num_jobs
(paranoid):启用用于构建对象的作业数上的canary。这可能会影响优化,从而可能影响ABI(未经证实)。debug
(paranoid):启用是否使用调试符号构建对象的canary。对ABI的影响未经证实,但不是被排除的。host
(paranoid):启用编译器主机三元组上的canary,该三元组由编译器设置。对ABI的影响未经证实,但不是被排除的。none
:主要在这里让您可以完全禁用canaries,风险自负。
stabby::libloading::StabbyLibrary
特质
为libloading::Library
提供的额外方法,这些方法公开了符号获取器,如果canaries不存在或报告不匹配,则这些获取器将失败。
这些方法仍然被认为是不可安全的,但它们将减少意外加载ABI不兼容代码的风险。报告还充当运行时类型检查,从而降低误输入符号的风险。
异步
任何在稳定类型上实现core::future::Future
的实现都将工作,无论稳定类型是在FFI边界哪一侧构建的。但是,由异步块和异步函数创建的未来不是ABI稳定的,因此必须通过特质对象使用。
stabby
通过 stabby::future::Future
特性支持未来。异步函数通过 #[stabby::stabby]
转换为返回类型为 Dyn<Box<()>, vtable!(stabby::future::Future + Send + Sync)>
的函数(Send
和 Sync
约束可以通过使用 #[stabby::stabby(unsync, unsend)]
来移除),它本身实现了 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
告诉它某些事物即使您无法使用 #[stabby::stabby]
也能保持 ABI 稳定。
结合 stabby::compiler_version
中的 ZSTs,它们实现了 stabby::IStable
,但仅在用相应的编译器版本编译时才有效。这使您可以声明某些类型仅在用适当的编译器版本编译时才是稳定的。但是,即使没有这样的 ZSTs,这些类型也仍然可以在任何没有 stabby::IStable
约束的地方使用。
stabby
的“宣言”
stabby
是为了解决 Rust 生态系统中缺乏 ABI 稳定性问题而构建的,这使得编写插件和其他基于动态链接的程序变得痛苦。目前,Rust 唯一的稳定 ABI 是 C ABI,它没有类型之和的概念,更不用说特定用途的利用了。
然而,我们的软件工程经验表明,类型大小对性能有很大影响,因此类型之和应该以占用空间最小的方式编码。
我对 stabby
的希望有两个方面
- Rust 生态系统的采用:这是我最不喜欢的选项,但至少在需要动态链接的情况下,人们可以用更好的方式使用 Rust。
- 引发了一场关于为Rust提供版本化ABI而不是稳定ABI的讨论:
stabby
通过所选版本的stabby-abi
crate实际上已经提供了版本化ABI。然而,让库实现类型布局,这通常是编译器的任务,强迫abi-stability变得针对每种类型是明确的,而不是适用于整个编译单元。在我看来,在cargo清单中添加一个abi = "<stabby/crabi/c>"
键将是一种更好的做法。更好的是,将这个想法与RFC 3435合并,允许基于每个函数选择ABI,并让编译器将所选稳定ABI污染注释函数接口的类型,这将更加粒度化,但仍然允许最终用户通过承诺依赖项的单个版本来实现ABI稳定性。
stabby
的SemVer策略
Stabby将其所有stabby_abi::IStable
实现包含在其公共API中:对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版本化
依赖项
~4MB
~80K SLoC