#traits #virtual #dyn

无std thin_trait_object

一种宽度为指针宽度的trait对象,同时也支持FFI,允许将trait传递给C ABI代码

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日

#312FFI

每月43次下载

MIT/Apache

89KB
1.5K SLoC

thin_trait_object

Crates.io Docs.rs Build Status

一种宽度为指针宽度的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是否还应包含sizealign字段,分别存储存储类型的尺寸和首选对齐方式。默认设置为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::forgetManuallyDrop移除原始副本,否则调用from_raw然后丢弃将导致未定义行为。

    • fn into_raw(self) -> *mut ()
      

      释放特质对象的所有权,返回包含的指针。调用者有责任稍后使用from_raw丢弃特质对象。

      对于不释放所有权的版本,请参阅as_raw

    • fn vtable(&self) -> &{vtable name}
      

      检索包含的特质对象的原始虚表。

依赖项

~1.5MB
~36K SLoC