5个稳定版本
1.1.2 | 2021年4月13日 |
---|---|
1.1.1 | 2021年3月10日 |
1.1.0 | 2021年3月7日 |
1.0.1 | 2021年2月21日 |
1.0.0 | 2021年2月16日 |
#312 在 FFI 中
每月43次下载
89KB
1.5K SLoC
thin_trait_object
一种宽度为指针宽度的trait对象,同时也支持FFI,允许将trait传递给C ABI代码。
概述
Rust中的trait对象存在几个基本限制
- 指针大小加倍,因为trait对象是通过指针转换而不是值转换构建的——这意味着无法将虚函数表或其指针存储在对象内部,而必须与指向该对象的指针一起存储,从而增加指针的大小开销,尤其是对于如
Vec<Box<dyn ...>>
这样的集合。 - 无法通过FFI边界传递trait对象,因为它们没有定义的内存布局和实现;
- 没有手动构建trait对象的方法,只给定调度表和值,即创建一个不对应于任何类型对trait的实现的自定义实现。
对于大多数用途,这些限制相对容易规避或根本不适用。然而,在几种情况下,没有可能的解决方案,这是Rust中trait对象工作本质的固有属性。例如
- 实现插件系统,其中动态加载库(
.dll
/.so
/.dylib
)中的插件可以通过Rust代码加载,并使用定义的接口扩展基础程序的功能; - 减少对trait对象(如
Vec<Box<dyn ...>>
)的引用/boxes/指针的存储开销; - 通过不同语言的JIT编译实现traits,尽管这是一个非常狭窄的场景。
所有这些工作负载都符合trait对象模式,但不符合实现。这个crate提供了一个trait对象的替代实现,它服务于模式同时克服了编译器内置实现的限制。功能以易于使用的属性宏的形式提供。
该宏受到了Michael-F-Bryan在FFI-Safe Polymorphism: Thin Trait Objects文章中描述的FFI安全的trait对象的设计和实现的强烈启发。该文章是手动编写此类trait对象的教程,而这个crate则作为宏自动执行相同任务。
使用方法
最基本的使用案例
use thin_trait_object::*;
#[thin_trait_object]
trait Foo {
fn fooify(&self);
}
impl Foo for String {
fn fooify(&self) {
println!("Fooified a string: {}", self);
}
}
BoxedFoo::new("Hello World!".to_string()).fooify();
该宏将生成两个结构(还有一个是实现细节)
FooVtable
,调度表(vtable)——一个包含所有trait方法类型擦除函数指针的#[repr(C)]
结构,以及一个额外的drop
函数指针,当BoxedFoo
被丢弃时由它调用(默认添加了另一个属性,#[derive(Copy, Clone, Debug, Hash)]
);BoxedFoo
,类似于Box<dyn Foo>
,在它作为有效的Foo
trait实现的同时,拥有所包含值的排他所有权,其内存布局与core::ptr::NonNull
到实现了Sized
的类型相同。
这两个结构将具有与放置#[thin_trait_object]
属性的trait相同的可见性修饰符,除非您覆盖它——下文将解释如何进行覆盖。
配置宏
基本调用形式,#[thin_trait_object]
,将使用所有可能配置值的合理默认值。要覆盖这些配置参数,请使用以下语法
#[thin_trait_object(
parameter1(value_for_the_parameter),
parameter2(another_value),
// Certain parameters require a slightly different syntax, like this:
parameter3 = value,
)]
trait Foo {
...
}
支持以下选项
-
vtable(<attributes> <visibility> <name>)
— 指定生成的vtable结构的可见性和名称,并且可以可选地将其属性附加到它上面(包括文档注释)。默认情况下,会附加
#[repr(C)]
和#[derive(Copy, Clone, Debug, Hash)]
,可见性从特质定义中获取,名称为<trait_name>Vtable
的形式,例如MyTraitVtable
。#[repr(C)]
将会被覆盖,而#[derive(...)]
不会,这意味着指定#[derive(PartialEq)]
,例如,将添加PartialEq
到正在派生的特质列表中,而不会覆盖它。示例
#[thin_trait_object( vtable( /// Documentation for my generated vtable. #[repr(custom_repr)] // Will override the default #[repr(C)] #[another_fancy_attribute] pub MyVtableName // No semicolon allowed! ) )]
-
trait_object(<attributes> <visibility> <name>)
— 与vtable(...)
相同,但它将对其生成的boxed特质对象结构产生效果。由于稳健性原因,不能附加
#[derive(...)]
属性(因此使用宏时不需要使用#[derive(Copy)]
就不会导致未定义的行为。)默认情况下,
#[repr(transparent)]
被附加(无法重写),可见性取自特性定义,名称形式为Boxed<trait_name>
,例如BoxedMyTrait
。 -
inline_vtable = <true/false>
——指定vtable是否应该直接存储在特性对象中(true
)或者存储为vtable的&'static
引用。默认设置为false
,除非特性具有非常少的方法(一个或两个),或者绝对必要以与某些第三方代码兼容,否则不建议重写。示例
#[thin_trait_object( inline_vtable = true )]
-
"..."
——指定vtable中drop
函数指针的ABI(在extern "C"
中为"C"
)。vtable中其他所有方法的ABI可以直接在特性定义中指定。示例
#[thin_trait_object( drop_abi = "C" // Equivalent to extern "C" on a function/method )]
-
marker_traits(...)
——指定一个逗号分隔的特列表,这些特性被认为是标记特性,即在生成的瘦特性对象结构中通过空impl
块实现,如果特性定义将它们列为超特性。列表中的不安全特性需要用unsafe
关键字前缀。默认情况下,列表为
marker_traits(unsafe Send, unsafe Sync, UnwindSafe, RefUnwindSafe)
。有关宏如何与超特性交互的更多信息,请参阅超特性部分。
示例
trait SafeTrait {} unsafe trait UnsafeTrait {} #[thin_trait_object( marker_traits( SafeTrait, // `unsafe` keyword here ensures that "unsafe code" is required // to produce UB by implementing the trait unsafe UnsafeTrait, ) )] trait MyTrait: SafeTrait + UnsafeTrait {}
-
= <true/false>
——指定生成的vtable是否还应包含size
和align
字段,分别存储存储类型的尺寸和首选对齐方式。默认设置为false
以提高兼容性。示例
#[thin_trait_object( store_layout = true )]
与FFI一起使用
宏的主要重点之一是FFI,这就是为什么与FFI一起使用宏既简单又自然。
use thin_trait_object::*;
use std::ffi::c_void;
#[thin_trait_object(drop_abi = "C")]
trait Foo {
extern "C" fn say_hello(&self);
}
impl Foo for String {
extern "C" fn say_hello(&self) {
println!("Hello from \"{}\"", self);
}
}
extern "C" {
fn eater_of_foo(foo: *mut c_void);
fn creator_of_foo() -> *mut c_void;
}
let foo = BoxedFoo::new("Hello World!".to_string());
unsafe {
// Will transfer ownership to the C side.
eater_of_foo(foo.into_raw() as *mut c_void);
}
// Acquire ownership of a different implementation from the C side.
let foo = unsafe { BoxedFoo::from_raw(creator_of_foo() as *mut ()) };
foo.say_hello();
C端将执行
#include <stdio.h>
typedef void (*vtable_say_hello)(void*);
typedef void (*vtable_drop)(void*);
typedef struct foo_vtable {
vtable_say_hello say_hello;
vtable_drop drop;
} foo_vtable;
void eater_of_foo(void* foo) {
// The first field is a pointer to the vtable, so we have to first
// extract that pointer and then dereference the function pointers.
foo_vtable* vtable = *((foo_vtable**)foo);
// Have to provide the pointer twice, firstly for
// lookup and then to provide the &self reference.
vtable.say_hello(foo);
// Don't forget about manual memory management — the C side owns the trait object now.
vtable.drop(foo);
}
void* creator_of_foo(void) {
// Allocate space for one pointer, the pointer to the vtable.
void* allocation = malloc(sizeof(foo_vtable*));
void* vtable_pointer = &custom_vtable;
// Put the pointer into the allocation.
memcpy(allocation, &vtable_pointer, sizeof(foo_vtable*));
return allocation;
}
static foo_vtable custom_vtable {
// Using C11 designated initializers, consult your local C expert for
// ways to do this on an old compiler.
.say_hello = &impl_say_hello,
.drop = &impl_drop
};
void impl_say_hello(void* self) {
puts("Hello from C!");
}
void impl_drop(void* self) {
free(self);
}
超特性
考虑这种情况
use thin_trait_object::*;
trait A {
fn a(&self);
}
#[thin_trait_object]
trait B: A {
fn b(&self);
}
这将会编译失败,因为宏将尝试为生成的瘦特性对象结构BoxedB
实现B
,这将失败,因为BoxedB
没有实现A
。为了修复这个问题,那必须手动完成。
#[thin_trait_object]
trait B: A {
fn b(&self);
#[doc(hidden)]
fn _thunk_a(&self) {
self.a(); // Redirect to the method from the A trait implementation
}
}
impl A for BoxedB<'_> {
fn a(&self) {
// Redirect to the hidden thunk, which will use the actual implementation of the method
self._thunk_a();
}
}
这是必要的,因为宏无法访问 A
,因此不知道需要将其方法添加到虚函数表中。有点巧妙,但仅使用过程宏没有更干净的方法来做这件事。如果您有任何改进此模式的建议,请提出问题,说明您提出的解决方案或创建一个PR。
输出参考
以下是一个宏发出的所有内容的完整列表
-
该特质的本身,以及所有其他属性。
-
一个虚拟调度表的struct定义。
名称可以通过
vtable(...)
配置选项进行自定义(参见 配置宏 部分);默认名称是{trait name}Vtable
,例如,对于名为Foo
的特质,名称为FooVtable
。虚拟调度表定义如下
#[repr(C)] // Can be customized via configuration options #[derive(Copy, Clone, Debug, Hash)] struct FooVtable { // One field for every method in the trait drop: unsafe fn(::core::ffi::c_void), // ABI can be customized via configuration options }
除了
drop
之外的其他字段,名称与相应的特质方法相同。签名几乎相同,但有两个区别&self
或&mut self
(如果存在),被替换为*mut ::core::ffi::c_void
;- 如果特质方法上没有
'unsafe
,则自动添加,因为作为第一个参数传递的指针永远不会经过验证。
-
一个薄的特质对象struct定义。
名称可以通过
trait_object(...)
配置选项进行自定义(参见 配置宏 部分);默认名称是Boxed{trait name}
,例如,对于名为Foo
的特质,名称为BoxedFoo
。虚拟调度表定义如下
#[repr(transparent)] struct BoxedFoo<'inner>( ::core::ptr::NonNull<{vtable name}>, ::core::marker::PhantomData<&'inner ()>, );
如果特质有一个
'static
生命周期限制,则不会发出'inner
生命周期参数,因为所有可能的包含实现都限制为'static
。以下方法和关联函数存在于boxed thin trait对象结构上
-
fn new<T: {trait name} + Sized + 'inner>(val: T) -> Self
从一个实现了特质的类型构建boxed thin trait对象。如果
'static
生命周期是基特质上的超特质之一,则'inner
限制被替换为'static
。 -
const unsafe fn from_raw(ptr: *mut ()) -> Self
直接从其vtable的原始指针创建thin trait对象。
安全性
由于其本质,此构造函数非常不安全,应尽可能避免使用。以下不变量必须得到遵守
- 指针不能为空,并且必须指向一个有效的薄特质对象,该对象符合其vtable的预期,并且不是未初始化的;
- vtable中的函数指针不能为空,并且必须指向具有正确ABI和签名的有效函数;
- 函数指针必须与隐含的安全协议一致,而不是更严格的一个:只有当传递给它们的虚表指针无效时,或者如果这些在特性本身中不安全时,如果违反了它们的声明中的安全协议,才会导致未定义行为(UB);
- 如果特性是不安全的,函数指针必须遵循特性对有效实现的协议;
- 该指针不是由
as_raw
返回的,该函数在一个未被放入ManuallyDrop
或由mem::forget
消耗的对象上被调用,否则当两者都被丢弃时将引发未定义行为。
-
const fn as_raw(&self) -> *mut ()
提取包含的特质对象的指针。
与
into_raw
不同,指针的所有权不会被释放,因此它将正常丢弃。除非通过mem::forget
或ManuallyDrop
移除原始副本,否则调用from_raw
然后丢弃将导致未定义行为。 -
fn into_raw(self) -> *mut ()
释放特质对象的所有权,返回包含的指针。调用者有责任稍后使用
from_raw
丢弃特质对象。对于不释放所有权的版本,请参阅
as_raw
。 -
fn vtable(&self) -> &{vtable name}
检索包含的特质对象的原始虚表。
-
依赖项
~1.5MB
~36K SLoC