Dyn Async Traits
原文 | 日期:2021-10-06 11:06 -0400
在前一篇文章中,我谈到了编译器是如何执行动态分发的 impl。
在这篇文章中,我想开始探索一种理论上的 future,其 impl 是由 Rust 程序员手动编写的。
这在一定程度上是一种思考练习,但它也是设计 future 的一个可能因素:如果我们能让程序员更好地控制 impl Trait for dyn Trait
,那么就可以启用许多用例。
示例
在这篇文章中, async fn
会有点让人分心,所以我们只使用一个简化的 Iterator
trait:
trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
正如在上一篇文章中所讨论的,编译器今天生成类似于以下内容的 impl:
impl<I> Iterator for dyn Iterator<Item = I> {
type Item = I;
fn next(&mut self) -> Option<I> {
type RuntimeType = ();
let data_pointer: *mut RuntimeType = self as *mut ();
let vtable: DynMetadata = ptr::metadata(self);
let fn_pointer: fn(*mut RuntimeType) -> Option<I> =
__get_next_fn_pointer__(vtable);
fn_pointer(data)
}
}
这段代码使用了来自 RFC 2580 的 API,以及少量的伪代码。看看它有什么作用:
提取数据指针
type RuntimeType = ();
let data_pointer: *mut RuntimeType = self as *mut ();
这里,self
是 &mut dyn Iterator<Item = I>
类型的宽指针。as
的规则规定,将宽指针转换为细指针会丢弃
metadata1,因此我们可以使用/滥用它来获取数据指针。
这里,我给指针的类型是 *mut RuntimeType
,这是 *mut()
的别名 —— 即指向某个对象的原始指针。
类型别名 RuntimeType
是为了表示“在运行时拥有的任何类型的数据”。使用 ()
来实现这一点是一种技巧;对它进行建模的"正确"方法应该是使用 existential type。
但由于 Rust 没有这些,没有必要的话,我不想添加它们,所以现在只使用这种类型别名。
我其实并不喜欢这些规则,它们已经折腾了我几次。我认为应该引入一个存取器函数 (accessor function),但我在 RFC 2580 中没有看到 —— 可能是我错过了它,或者它已经存在。
提取 vtable(或 DynMetadata
)
let vtable: DynMetadata = ptr::metadata(self);
RFC 2580 中增加了 ptr::metadata
函数。其目的是从宽指针中提取“元数据”。
该元数据的类型取决于所拥有的宽指针的类型:这由 Pointee
trait 决定2。对于 dyn
类型,元数据是 DynMetadata
,意思是“指向 vtable 的指针”。
在现在的 API 中,DynMetadata
非常有限:它允许你提取底层 RuntimeType
的大小/对齐,但不提供对内部实际函数指针的任何访问。
我希望这被称为“referent”。自从我几年前学会这个词以来,我就发现它是如此优雅。但我承认 Pointee
更显而易见,也更有趣,此外,“referent”似乎只是引用(我认为这是安全的东西,如 &
或 &mut
,区别于单纯的指针 pointers)。
从 vtable 中提取函数指针
let fn_pointer: fn(*mut RuntimeType) -> Option<I> =
__get_next_fn_pointer__(vtable);
现在来看看伪代码。无论如何,我们需要一种从 vtable 中取出函数指针的方法。
这种方法在运行时的工作方式是每个方法在 vtable 中都有一个被分配的偏移量,基本上是进行数组查找;有点像 vtable.methods()[0]
,其中 methods()
返回函数指针的数组 &[fn()]
。
问题是有很多“动态类型”:每个方法的签名将是不同的。此外,我们希望有一些自由来改变 vtable 的布局方式。例如,Charles Lew 正在进行的(而且很棒的)的
dyn upcasting 需要修改 vtable 布局,预计还会有进一步的修改,因为我们试图支持具有多个 trait 的 dyn
类型,比如 dyn Debug + Display
。
因此,现在,让我们将其保留为伪代码。一旦完成了示例的演练,我将返回到如何以向前兼容的方式对 __get_next_fn_pointer__
建模的问题。
需要指出的是,fn_pointer
的类型是一个 fn(*mut RuntimeType) -> Option<I>
。这里有两件有趣的事情:
- 参数的类型为
*mut RuntimeType
:使用类型别名表示该函数采用单指针(实际上,它是一个引用,但它们具有相同的布局)。这个指针应该指向的运行时数据与self
所指向的一样 —— 我们不知道它是什么,但只知道它们是相同的。这是因为self
把一个指向类型为RuntimeType
的数据的指针与一个期望引用RuntimeType
的一些函数的 vtable 放在一起。3 - 返回类型为
Option<I>
,其中I
是条目类型 (item type):这很有趣,因为虽然我们不是静态地知道Self
类型是什么,但知道Item
类型。事实上, 我们将为每一种条目生成一个不同副本,这使可以轻松地传递返回值。
如果你使用不安全代码将一个随机指针与一个不相关的 vtable 放在一起,那么这里将会非常有趣,因为没有运行时检查这些类型是否对齐。
调用该函数
fn_pointer(data)
代码中的最后一行非常简单:调用这个函数!它返回一个 Option<I>
,我们可以将其返回给调用者。
返回到伪代码
在那个想象中的 impl 中,我们依赖于一段伪代码:
let fn_pointer: fn(*mut RuntimeType) -> Option<I> =
__get_next_fn_pointer__(vtable);
那么,我们如何才能将 __get_next_fn_pointer__
从伪代码变成真正的代码呢?有两件事值得注意:
- 该函数的名称已经编码了我们想要的方法
next
,我们或许不想生成一组无限的getter
函数。 - 该函数的签名特定于我们想要的方法,因为它返回一个
fn
类型fn(*mut RuntimeType -> Option<I>
,该类型对next
的签名进行了编码(当然,带有修改过的 self 类型)。这似乎比仅仅返回必须由用户手动强制转换的泛型签名(如fn()
)要好得多;出错的可能性更小。
使用零大小的 fn
类型作为 API 的基础
解决这些问题的一种方法建立在 trait 体系之上。假设每个方法都有一个类型 A
,并且该类型实现了类似于 AssociatedFn
的 trait:
trait AssociatedFn {
// The type of the associated function, but as a `fn` pointer
// with the self type erased. This is the type that would be
// encoded in the vtable.
type FnPointer;
… // maybe other things
}
然后,定义一个通用的获取函数指针的函数,如下所示:
fn associated_fn<A>(vtable: DynMetadata) -> A::FnPtr
where
A: AssociatedFn
现在还不是 __get_next_fn_pointer__
,而是
type NextMethodType = /* type corresponding to the next method */;
let fn_pointer: fn(*mut RuntimeType) -> Option<I> =
associated_fn::<NextMethodType>(vtable);
这个 NextMethodType
是什么呢?我们如何获取 next
方法的类型?想必必须引入一些语法,比如 Iterator::Item
。
相关概念:零大小 fn
类型
这种关联函数类型的思想与 Rust 中已经存在的一个概念非常接近(但不完全相同):零大小函数类型。
正如你可能知道的,Rust 的函数实际上是唯一标识该函数的特殊的零大小类型。(目前)没有这种类型的语法,但你可以通过打印值的大小来观察它:
#![allow(unused)] fn main() { fn foo() { } // The type of `f` is not `fn()`. It is a special, zero-sized type that uniquely // identifies `foo` let f = foo; println!("{}", sizeof_value(&f)); // prints 0 // This type can be coerced to `fn()`, which is a function pointer let g: fn() = f; println!("{}", sizeof_value(&g)); // prints 8 }
也有出现在 impl 中的函数类型。例如,你可以在 vec::IntoIter<u32>
上获取表示 next
方法的类型实例,如下所示:
let x = <vec::IntoIter<u32> as Iterator>::next;
println!("{}", sizeof_value(&f)); // prints 0
零大小类型并不合适
现有的零大小类型不能用于我们的关联函数类型,原因有两个:
- 你不能说出他们的名字!可以通过添加语法来解决这个问题。
- trait 函数不存在独立于 impl 的零大小类型。
后一点很微妙4。在前面例子中,当我谈到从 impl 获取函数的类型时,你会注意到我给出了一个完全限定的函数名,它精确地指定了 Self
类型:
事实上,直到我写这篇博客文章我才看到它!
let x = <vec::IntoIter<u32> as Iterator>::next;
// ^^^^^^^^^^^^^^^^^^ the Self type
但我们在 impl 中想要的是编写不知道 Self 类型是什么的代码!因此,今天 Rust 类型系统中存在的这种类型并不完全是我们所需要的。但已经很接近了。
结论
显然,我还没有展示任何形式的最终设计,但我们已经看到了很多诱人的内容:
- 如今,编译器会生成一个
impl Iterator for dyn Iterator
,它从 vtable 中提取函数并神奇地调用它们。 - 但是,使用 RFC 2580 中的API,你几乎可以手工编写。我们缺少的是一种从 vtable 中提取函数指针的办法,而让这一点变得困难的是,需要一种办法来识别正在提取的函数。
- 我们有零大小的类型来表示今天的函数,但没有办法来命名它们,我们也没有只在 impl 中的 trait 中的函数的零大小类型。
当然,我在这里写的所有东西都是关于普通的函数。我们仍然需要返回到异步函数,这会增加一些额外的问题。下次见!