43 个版本 (稳定)
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.6 | 2023 年 3 月 30 日 |
#14 in FFI
每月 39,905 次下载
在 12 个包中(直接使用 3 个) 使用
545KB
11K SLoC
[!WARNING]
由于 Rust 1.78 中的破坏性更改,stabby
对 trait 对象的实现可能会引起性能问题
- 受影响的仅为非夜间版本且 >= 1.78 的 Rust 版本
- 现在将支持 trait 对象的 v-tables 插入到一个全局的无锁集合中。
- 这个集合会泄漏:
valgrind
会 对你感到愤怒。- 这个集合的大小与不同的
(类型, 特质-集合)
对的数量成比例。它当前的实现是一个向量
- 查找是通过线性搜索(O(n))完成的,这在元素数量小于 100 时仍然是最快的。
- 插入是通过克隆向量(O(n))并原子地替换它来完成的,在发生冲突时重复此操作。
- 目前正在努力用不可变 b-tree 映射来替换此实现(如果发现比当前实现慢得多,则将放弃此操作)。
随着情况的发展,此说明将进行更新。同时,如果你的项目使用了许多
stabby
定义的 trait 对象,我建议使用nightly
或编译器< 1.78
版本。
Rust 的稳定 ABI 与紧凑的 sum-types
stabby
是您创建共享库的稳定二进制接口的一站式商店,无需让您的 sum-types(枚举)体积爆炸。
您与stabby
交互的主要向量将是#[stabby::stabby]
过程宏,您可以用它来注释很多内容。
我为什么想要稳定的ABI?ABI究竟是什么意思?
ABI代表应用程序二进制接口(Application Binary Interface),它是API的更详细的兄弟。虽然API定义了函数期望的数据类型及其属性,但ABI定义了这些数据在内存中的布局方式以及函数调用的工作方式。
数据在内存中的布局通常称为“表示”:字段顺序、枚举变体的区分、填充、大小等。为了使用某些类型进行通信,两个软件单元必须就这些类型在内存中的样子达成一致。
函数调用在底层也非常复杂(尽管开发者很少需要考虑这一点):调用者或被调用者负责保护调用者的寄存器免受被调用者的操作影响?应该在哪些寄存器/栈顺序中传递参数?实际触发调用的CPU指令是什么?对这些问题的回答(以及一些其他问题)被称为“调用约定”。
在Rust中,除非您通过#[repr(_)]
显式选择您的类型的已知表示,或者通过extern "_"
为您的函数显式指定调用约定,编译器可以自由地处理这些方面的软件:它处理这些方面的过程是明确不稳定的,这取决于您的编译器版本、您选择的优化级别、伯克郡附近一家羊毛农场里某只 llama 的心情……谁知道呢?
当涉及动态链接时,这个问题就出现了:由于Rust中大多数东西的ABI是不稳定的,通过不同编译器调用构建的软件单元(如动态库和需要它的可执行文件)可能在ABI的这些决策上不一致,尽管链接器根本不知道这一点。
具体来说,这可能意味着您的可执行文件认为Vec<Potato>
的左起8个字节是指向堆分配的指针,而库认为它们是其长度。这也可能意味着库在被调用时认为它可以随意破坏寄存器,而可执行文件则依赖它在返回之前保存和恢复它们。
stabby
旨在通过帮助您锁定程序子集的ABI来解决这些问题,同时帮助您保留使用不稳定ABI时rustc
提供的某些布局优化。此外,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 表总是假定所有方法参数都是 ABI 稳定的,以防止 rustc
冻结的风险。除非您的特质有引用其自身 v 表的方法,否则建议使用 #[stabby::stabby(checked)]
来避免尽管其接口中的一些类型实际上并不稳定,但 v 表被标记为稳定。
由于 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>
的签名匹配,则将返回一个函数指针,确保它们确实具有相同的签名。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的属性。
当加载共享库时,链接器可以检查这些符号的存在,从而防止在加载器请求不兼容版本的canaries时进行链接。
#[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 "rust" 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边界哪一侧。然而,由异步块和异步函数创建的future并不具有ABI稳定性,因此必须通过trait对象来使用。
stabby
通过 stabby::future::Future
trait 支持future。异步函数被 #[stabby::stabby]
转换为返回 Dyn<Box<()>, vtable!(stabby::future::Future + Send + Sync)>
(可以使用 #[stabby::stabby(unsync, unsend)]
移除 Send
和 Sync
的界限),它本身实现了 core::future::Future
。
stabby
目前还不支持异步trait,但你可以使用以下模式来实现它们
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
中实现的 stabby::IStable
相关的ZST相结合,但这仅限于使用相应编译器的版本编译时。这允许你声明某些类型只有在编译时使用适当的编译器版本才具有稳定性。但是,即使不使用相应的编译器版本,ZST仍然存在,因此这些类型仍然可以在没有 stabby::IStable
界定的任何地方使用。
stabby
的“宣言”
stabby
是针对Rust生态系统中缺乏ABI稳定性而构建的,这使得编写插件和其他基于动态链接的程序变得痛苦。目前,Rust的唯一稳定ABI是C ABI,它没有sum-type的概念,更不用说利基利用了。
然而,我们在软件工程方面的经验表明,类型大小对性能有很大影响,因此sum-type应该以占用空间最少的方式编码。
我对 stabby
的希望有两种口味
- Rust生态系统的采用:这是我最不喜欢的选项,但这至少可以让人们在需要动态链接的情况下有更好的Rust体验。
- 引发了一场关于为Rust提供非稳定而是版本化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版本化
依赖项
~8–13MB
~82K SLoC