为Rust构建运行时反射系统(第3部分)

2021-01-10 13:20:17

欢迎来到本系列的第三期也是最后一部分,这是关于我们如何在Rust中实现运行时反射系统的系列。

到目前为止,我们已经展示了如何提出一个相当简单的Class和Instance模型来考虑运行时Rust类。在第1部分中,我们将它们用于类型检查,在第2部分中,我们增加了对从结构读取属性的支持。

在本文中,我们从属性获取器开始,然后扩展到方法调用。在某些方面,我们用于属性的相同技术在这里也同样适用。我们可以存储从方法名称到实现它们的函数的映射。但是,有一个曲线球:Rust Fn *特征。我们将经历错误的转弯,以及在此过程中积累的Rust知识小知识。

现在我们有了类,实例和属性,接下来的明显步骤是添加方法。

在oso策略中,可以调用带有和不带有参数的类和实例方法。

struct Cat; impl Cat {///一个类方法(注意缺少`self`)。 fn meow()-> String {" meowww" .to_string()} ///实例方法。 fn feed(& self,food:& str)->字符串{如果食物=="金枪鱼" {" purr" .to_string()}其他{Self :: meow()}}}

如果喂猫的结果与猫叫的结果不同,则表示输入的食物是猫的最爱食物。

让我们从采用零参数的方法的简单实现开始。实现零参数方法的方法与我们实现属性获取器的方式极为相似,并且实际上我们已经在第2部分中完成了此操作:

///类定义struct类{... ///从属性名称映射到属性查找方法:HashMap<&'静态str,InstanceMethod>} struct InstanceMethod(Arc< dyn Fn(& Instance)- > PolarValue);

同样,我们需要在ClassBuilder结构上添加一个方法,以允许我们注册新方法:

pub fn add_method< F,R>(mut self,名称:&' static str,f:F)->自身位置F:Fn(&T)-> R,R:crate :: ToPolar,{self.class.methods.insert(name,InstanceMethod :: new(f));自}

这基本上可以满足我们的需求! Polar并不关心方法的稀疏性(该方法接受多少个参数)-它会发送作为向量的无论多少参数。 Polar同时支持可变数量的参数(varargs)和关键字参数(kwargs)。只有具有该概念的宿主语言才支持后者,而Rust不支持。

但这还不是故事的结局。 AttributeGetter接口的关键部分是您可以为属性getter传入任何方法或闭包,并且AttributeGetter :: new方法可以透明地处理所有类型转换。这隐藏了用户的混乱且易于出错的详细信息,并保持界面清洁。

impl InstanceMethod {pub fn new< T,F,???(f:F)->自己所在的位置F:Fn(& T,???)F ::结果:ToPolarResult,T:静态,{Self(Arc :: new(move | receiver:& Instance,args:Vec src / main.rs:2:14 | 2 |其中F:Fn <(u32,),输出= u32>。 {| ^^^^^^^^^^^^^^^^^^^^^^^ help:使用括号符号代替:`Fn(u32)-> u32` | =注意:请参阅问题#29625< https://github.com/rust-lang/rust/issues/29625>欲获得更多信息

我们可以选择每晚使用Rust来获取这些功能,但是自己实施这些功能并不是一件容易的事。

在这里,作为作家,我试图公开我新创建的特征,使其完全符合我们的需求,并使其看起来就像我只是用手指按键盘来获得定义。

实际上,我们在这一特性上为此项目花费了大量的工程设计精力。我们犯了错误。我们编写了扔出的代码。主要原因是我们不了解Rust函数和特征解析系统的某些细微差别。与其将所有内容都写下来,不如向您展示我们尝试过的事情会更有趣。

事后看来,我们尝试的第一件事是有点贪心。为什么不直接跳写特质来精确封装我们需要的东西呢?

impl< F,T,R>用于F的方法F:Fn(&T)-> R,T:<静态,R:ToPolarValue,{fn invoke(&self;实例,实例,args:Vec< PolarValue>))> PolarValue {debug_assert!(args.is_empty());让接收者= instance.downcast ::< T>()。unwrap(); self(receiver).to_polar()}}

impl< F,T,R> F ^无约束类型参数的方法类型参数“ T”不受impl特性,自身类型或谓词F,T,R的约束。 F ^无约束类型参数的方法类型参数`R`不受impl特性,自身类型或谓词的约束

多数人可能会在Rust冒险中遇到此错误的某些变化!这里发生了什么? T和R对我来说看起来很拘束吗?它们就在F的定义内:Fn(& T)-> R.

我们的错误是将这些特征范围理解为:F是从& T到R的函数,而实际上,这是一个常规的旧特征,其特征本身的语法略有不同。一个功能可能实现多个这些特征范围。

//不做任何事情ident< T>(t:T)-> T {t} let _ =& dyn Fn(u32)-> u32; let _ =& dyn Fn(String)->串;

这是解决此问题的另一种方法。假设相反,我们决定为我们关心的所有类型全面实现Method特质:

impl< F>用于F的方法F:Fn(& u32)-> u32,{fn invoke(& self,instance:Instance,args:Vec< PolarValue>)-> PolarValue {debug_assert!(args.is_empty());让接收者= instance.downcast ::< u32>()。unwrap(); self(receiver).to_polar()}} impl< F> Fwhere F的方法:Fn(& String)->字符串,{fn invoke(& self,instance:Instance,args:Vec< PolarValue>)-> PolarValue {debug_assert!(args.is_empty());让接收者= instance.downcast ::< String>()。unwrap(); self(receiver).to_polar()}}

现在忽略这个想法有多糟糕,它甚至行不通!我们得到:

回头看看ident。一个函数同时实现Fn(String)->字符串和Fn(u32)-> u32特质。类似地,在上述情况下,实现Fn(& String)->字符串和Fn(& u32)-> u32将有两种可能的Method实现。因此我们得到了冲突的实现。

实际上,它的作用远不止于此。在任何两个功能特征边界上实施一揽子特征实现会导致实现冲突。即使不存在这样的功能:

特质测试{} impl< F:Fn()>测试F {} impl< F:Fn(String)->字符串>测试F {}

错误[E0119]:性状`Test`的实现冲突:| 4 | impl< F:Fn()>测试F {} | ------------------------这里的第一个实现5 | impl< F:Fn(String)->字符串>测试F {} | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^冲突的实现

有一天(或者今天,如果您每晚都在Rust上工作),Rust可能会让您为自己的类型实现Fn,支持可变函数(您的意思是Rust已经支持可变函数?),并且会带来各种乐趣诸如此类的事情。但是目前,我们不会采用这种方法走得更远。

这种方法特征看起来太方便了,无法完全摆脱并发症的第一个征兆。让我们尝试一些不同的东西。如果我们最初的错误是将Fn视为函数而不是特征,那么也许我们可以留意Rust文档的智慧并改用fn。

根据文档,我们可以将fn与常规函数和闭包一起使用!太好了,让我们这样做:

impl< T,R> fn(&T)->的方法R位置T:'静态,R:ToPolarValue,{fn invoke(&self; instance:Instance,args:Vec< PolarValue>)-> PolarValue {debug_assert!(args.is_empty());让接收者= instance.downcast()。unwrap(); self(receiver).to_polar()}}

let clone = |接收方:& String | ->字符串{receiver.clone()};让clone_method:Box< dyn方法> = Box :: new(clone); let instance = Instance :: new(" hello,world!" .to_string()); let result = clone_method.invoke(instance,vec![]);

错误[E0277]:特征绑定`[closure@src/main.rs:25:17:25:67]:方法不满足-> src / main.rs:26:41 | 26 | let clone_method:Box< dyn方法> = Box :: new(clone); | ^^^^^^^^^^^^^^^^^^特性未为`[closure@src/main.rs:25:17:25:67]实现” =注意:强制转换为对象类型“ dyn方法”

它对闭包无效吗?如果我们将clone更改为适当的函数fn clone(s:& u32)->,该怎么办?字符串{s.to_string()}

错误[E0277]:特征绑定为<' r> fn(&' r String)->字符串{main :: clone}:方法`不满足-> src / main.rs:26:41 | 26 | let clone_method:Box< dyn方法> = Box :: new(clone); |对于'for<' r>,未实现特征`Method'的特质^^^^^^^^^^^^^^^^^^^ fn(& r u32)->字符串{main :: clone}`| =注意:强制转换为对象类型“ dyn方法”

注意语法fn(& #r u32)->字符串{main :: clone}附加的block语句是重要的部分。每个函数项都有一个唯一的匿名类型,每个闭包也是如此。两者都可以强制为函数指针。但是,这种强制不会自动解决特质界限。

尤其难以发现此错误。乍一看,它看起来好像特性未正确实现。实际上,这里有关于错误消息的未解决问题:https://github.com/rust-lang/rust/issues/62385。

但是我们会将这项工作提交给我们图书馆的最终用户,这似乎是不可接受的

最终,我们回到了与不稳定的Rust Fn特性更相似的地方,该特性具有通用的Args,它是输入参数的元组。

pub trait Method T,Args =()>。 {输入结果; fn invoke(& self,接收者:& T,args:Args)->自我::结果;}

您可能仍然想知道为什么我们需要额外的接收者:& T参数?这是由于表达类型元组的方式受到限制。

我们想要具有的是单特征Function< Args>。 –其中Args是元组–然后将Method定义为trait Method< T,Args&gt ;: Function<(& T,... Args)> –其中... Args是某种元组扩展运算符。

诸如此类的问题尚待解决:Variadic泛型。但是到目前为止,对RFC的所有尝试都已被关闭或推迟。

因此,在此之前-或直到我们决定使用带有frunk的异构列表来实现它之前-我们将做很长的路要走。

我们快要完成了。我们只需要为每种可能数量的参数实现新特性即可。

impl< F,R,T,A,B>方法<接收器,(A,B)>对于F,F:Fn(& T,A,B)-> R,{类型Result = R; fn invoke(& self,接收者:& T,args:(A,B))-> Self :: Result(self)(receiver,args.0,args.1)}}

这很简单,但这也正是宏可以减轻痛苦的一种方式。最终版本如下:

macro_rules! tuple_impls {($($ name:ident)*)=> {impl< Fun,Res,Receiver,$($ name),*>方法<接收器,($($ name,)*)>对于其中乐趣的乐趣:Fn(& Receiver,$($ name),*)-> Res +发送+同步+'静态,{类型Result = Res; fn invoke(& self,接收者:& Receiver,args:($($ name,)*))-> Self :: Result {#[allow(non_snake_case)] let($($ name,)*)= args; (self)(接收者,$($ name,)*)}}};} tuple_impls! {} tuple_impls! {A} tuple_impls! {A B} tuple_impls! {A B C} // ..更多的宏调用随之///我们支持高达16tuple_impls的方法arities! {A B C D E F G H I J K L M N O P}

实际上,我们还需要几块。例如,我们如何从Vec< PolarValue>转到到(A,B,..)?更多特征和更多宏!

最终的结果正是我们一直在寻找的体验:您可以传入Rust闭包或函数:

事实证明,如果您在Rust的基础上构建一种语言,那么您很有可能最终实现此特征。例如:

在这里,类型检查cat:Cat使用符号Cat作为类标记,而在其他任何地方,Cat是绑定到Cat类的变量名。我们从相应的Python实现中借用了这种模式,在该实现中,我们将变量绑定到Cat的值,即< class' Cat'>。由于Python是动态的,并且我们不会像在Rust中那样经历相同的类注册步骤,因此它们都可以正常工作。

但是我们可以在此处应用相同的基本思想,并创建我们自己的元类。而且有效。

然后,要分派类方法,我们需要确保" instance方法" Class元类的其余输入参数(此处的接收器类型为Class),然后调用实际的class方法。

例如:在调用Cat.meow时,它解析为方法" meow"。在猫上。 Cat是一个常量,并且是Class的实例。所以,喵是oso :: host :: Class上的实例方法。而且我们在实例方法分派上有一个特殊的钩子,该钩子检查当前类是否为Class:

fn get_method(& self,名称:& str)-> Option< InstanceMethod> {如果self.type_id == TypeId :: of ::< Class>(){//通过查找类方法Some(InstanceMethod :: from_class_method(name.to_string()))重定向`Class`上的所有方法其他{self.instance_methods.get(name).cloned()}}

对于大多数用例,我们的实现都非常合适!您可以在此处查看最新版本的oso Rust板条箱。

但是我们可以在这里做更多的事情。首先,我们可以添加对其他方法变体的支持。我们在这里没有介绍,但是我们目前支持返回结果,选项和迭代器,最后一个需要使用特殊的add_iter_method。我们还可以支持异步方法。

此外,我们目前限制用户为每个名称定义一个方法,即使这不是Polar强制执行的限制。由于我们采用了上面的方法,要支持完全通用的方法(例如我们的ident T方法)将是一个挑战,但是我们可以为多种类型添加一个方法。如果需要,我们甚至可以添加对可变参数的支持!

在第1部分中,我们以Any特性为基础,依靠Rust对运行时动态类型的内置支持,开始了第1部分,这是我们类系统的基础。有了这个,我们可以做一些简单的运行时类型检查。 ✅

在第2部分中,我们研究了在运行时从结构中拉出属性。在这里,我们有一个可怕的选择:要求用户向其所有结构中添加#[repr(C)],以便我们可以执行~~暗咒语~~指针运算并动态读取字段?还是将属性访问器设为显式选择加入?后者似乎更像是Rust的方法,我们接受了。

到第三部分为止。我们基于属性获取器方法,将其转换为实例方法并允许输入。我们为最后的作品赢得了一些伤痕,可以证明这一点,并且我们为Rust提供了自己的运行时反射系统。

如果您有兴趣了解有关oso的更多信息以及我们如何使用Rust来构建我们的语言和开源策略引擎的信息,那么这里有一些方便的链接: