#unsized #box #no-std #async-trait

nightly no-std dyn-ptr

一个存储指针等类型的数据箱,除了 Self: Unsize<dyn Trait>

5 个版本

0.2.3 2023年3月4日
0.2.2 2023年3月4日
0.2.1 2023年3月3日
0.2.0 2023年3月3日
0.1.0 2023年3月1日

2030Rust 模式

27 每月下载量

MIT 许可证

12KB
167

如果 dyn 是已知大小呢?

在正常使用 async(无论是 std 还是 core)时,您可以连续编写

async fn async_things(...) -> T {}

// it is equivalent to:
fn async_things(...) -> impl Future<Output=T> {}

这是可以接受的,因为 impl 可以是任何类型,我们并不关心它在哪里以及它的内存是如何释放的。
直到你想要 `async trait'。想象一下,你有一个这样的特质

mod io {
    type Result<T> = ...;
}

pub trait AsyncRead {
    async fn read(&mut self, buf: &mut [u8]) -> io::Result<usize>;

    ...
}

之前你可能必须像这样重写 `read'

fn poll_read(
    self: Pin<&mut Self>,
    cx: &mut Context<'_>,
    buf: &mut [u8]
) -> Poll<io::Result<usize>>;

但现在你可以这样写

fn read(&mut self, buf: &mut [u8]) -> impl Future<Output=io::Result<usize>>;
// aka: async fn read(&mut self, buf: &mut [u8]) -> io::Result<usize>;

这为每个单独的实现创建了一个新的类型,实现了 Future,所以它不是 object-safety

error[E0038]: the trait `AsyncRead` cannot be made into an object
  --> src\main.rs:20:12
   |
20 |     let _: dyn AsyncRead = todo!();
   |            ^^^^^^^^^^^^^ `AsyncRead` cannot be made into an object
   |
note: for a trait to be "object safe" it needs to allow building a vtable to allow the call to be resolvable dynamically; for more information visit <https://doc.rust-lang.org/reference/items/traits.html#object-safety>
  --> src\main.rs:16:43
   |
15 | pub trait AsyncRead {
   |           --------- this trait cannot be made into an object...
16 |     fn read(&mut self, buf: &mut [u8]) -> impl Future<Output = io::Result<usize>>;
   |                                           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ...because method `read` references an `impl Trait` type in its return type
   = help: consider moving `read` to another trait

问题:核心中没有内存分配器

显然的解决方案是使用 Box<dyn Future>,但在 no-std 系统(其中仅提供 corealloc)中,Box 硬编码似乎是多余的,因为它需要分配器。

问题:&dyn Traits 不能替换 impl Traits

但如果有一个类型允许您保存与指针大小相同的大小,以及存储指向 drop 的指针的 Vtable,那么我们就会得到一种新的胖指针,它作为 T: 'a' ( &'a T' of `&dyn Trait')

基本用法

在今天的 rast 中,没有机制可以将 dyn Trait 传递给 impl Trait

fn print(x: impl Display) {
    println!({x});
}

并且我们可以轻松地在 'static 上下文中使用它

fn print_later(x: impl Display + Send + 'static) {
    thread::spawn(move || println!("{x}"));
}

但是,我们如何在这里通过值传递而不是引用传递来发送 &dyn Trait 呢?当然,使用 Box

fn print_later(x: Box<dyn Display + Send>) {
    thread::spawn(move || println!("{x}"));
}

然而,这总是需要内存分配,这在很多情况下(和在 no_std 中几乎总是)是没有意义的。

如果你只是存储 dyn Trait 会怎样呢?

但它需要多少空间?它是如何对齐的?你必须在编译时知道所有这些,这绝对不是关于 dyn 的。
那么,你如何选择大小呢?
让我们记住一个事实——Box<T> 是透明的指针。那么,如果你想象我们的新类型,它看起来可能就像这样

struct DynPtr<Dyn: ?Sized> {
    repr: PtrRepr,
    // is alias to `*const ()` not to be confused 
    drop: unsafe fn(*const ()),
}

这让你想起什么吗?这与在 std 中创建 vtables 的最流行方式非常相似。这是为 BoxDynPtr 的原始创建可能的样子

let ptr = DynPtr::<dyn Display> {
    repr: Box::into_raw(x) as PtrRepr,
    drop: |repr| unsafe {
        let _ = Box::from_raw(repr as *mut i32);
    },
};
(ptr.drop)(ptr.repr); // drop original box 

你可能注意到,from_raw/into_raw 可以推广到 mem::transmute 的情况,因为 Box 相似指针的 repr 类似于 *const ()
此外,这种方法还有一个优点,即实际上许多 future 都包含一个非常小的状态(与指针相等或更小),或者它的状态也被封装在 Arc-like 中。这意味着在一般情况下,你可以继续使用 Box,它也相当于一个指针,对于足够瘦的原始类型使用你自己的转换。

这就是 Dyn<dyn Trait> 的出现方式(这里的 dyn 是由于 Rust 2021 版本的要求的),它可以像 impl Trait 一样容易使用

fn print_later(x: Dyn<dyn Display + Send + 'static>) {
    thread::spawn(move || println!("{}", &*x)); // `Dyn` is not automatically delegate traits like `dyn`
}

// use `.do_dyn()` to coerce type into `Dyn<dyn ...>`
let x = & 12;
print_later(x.do_dyn());
print_later(12_usize.do_dyn());
print_later(Box::new(x).do_dyn());

无运行时依赖