#proc-macro #reflection #procedural #parameters #bounds #lifetime #robust

reflect

“我以为Rust没有反射?”纪念全新的定义过程宏的方法

14个版本

0.0.14 2024年2月26日
0.0.13 2023年7月21日
0.0.11 2023年3月18日
0.0.9 2022年4月30日
0.0.3 2018年5月27日

过程宏 中排名 124

Download history 4/week @ 2024-04-27 149/week @ 2024-05-04 87/week @ 2024-05-11 82/week @ 2024-05-18 55/week @ 2024-05-25 71/week @ 2024-06-01 86/week @ 2024-06-08 31/week @ 2024-06-15 62/week @ 2024-06-22 77/week @ 2024-06-29 98/week @ 2024-07-06 103/week @ 2024-07-13 45/week @ 2024-07-20 135/week @ 2024-07-27 530/week @ 2024-08-03 344/week @ 2024-08-10

每月下载量 1,142

MIT/Apache

99KB
2K SLoC

我以为Rust没有反射...?

该crate通过类似于编译时反射的编程模型来探索如何处理自定义 derive 宏的80%用例。

动机

我的现有库 synquote 以一种非常通用的方式处理过程宏的问题空间,适用于大约95%的用例。然而,这种通用性伴随着相对较低抽象层次的代价。宏的作者需要负责放置每个单个尖括号、生命周期、类型参数、特性和幻数。涉及大量的领域知识,并且很少有人能够可靠地使用这种方法生成健壮的宏。

这里探索的设计集中在消除所有边缘案例——也就是说,如果你的宏对于最基本的情况有效,那么它也会在所有复杂的情况下有效。

编程模型

我们的想法是暴露出一个看起来很无聊、简单的运行时反射 API,就像如果你使用过Java中的反射Go中的反射 一样可能会认识到的。

宏作者使用此API将宏的逻辑表达为,使用类似于reflect::Value的类型来检索函数参数、访问数据结构的字段以及调用函数等。重要的是,在此模型中不存在泛型类型或幻数数据。一切只是一个具有运行时概念上单态化的类型的reflect::Value

同时,库正在跟踪控制流和函数调用,以构建一个完整通用且健壮的作者宏的过程实现。生成的代码将具有所有正确位置的角度符号、生命周期、界限和幻数类型,而无需宏作者考虑任何这些。

反射API仅仅是定义过程宏的一种手段。库将其全部消除,并生成干净的无任何实际运行时反射的Rust源代码。请注意,这并不是关于编译器优化的声明——我们不依赖于Rust编译器对糟糕的生成代码进行英雄般的优化。实际上,通过反射API编写的源代码将与经验丰富的宏作者仅仅使用synquote所生成的代码相同。

从调用宏的人的角度来看,宏的调用方式与不带反射的传统方式编写的方式完全相同,他们的代码编译速度和性能都完全一样。优势在于宏作者,对于他们来说,开发和维护一个健壮的宏变得大大简化。

演示

本项目包含了一个用于定义自定义 derive 的编译时反射API的概念验证。

tests/debug/目录展示了为具有命名字段的 struct 定义#[derive(Debug)]的可行可编译实现。相应的测试用例显示了为具有两个字段的Point struct 派生Debug时生成的代码;它与不带反射的手写derive(Debug)宏为相同数据结构生成的代码等效。

宏实现开始于运行时所需类型和函数的DSL声明。

reflect::library! {
    extern crate std {
        mod fmt {
            type Formatter;
            type Result;
            type DebugStruct;

            trait Debug {
                fn fmt(&self, &mut Formatter) -> Result;
            }

            impl Formatter {
                fn debug_struct(&mut self, &str) -> DebugStruct;
            }

            impl DebugStruct {
                fn field(&mut self, &str, &Debug) -> &mut DebugStruct;
                fn finish(&mut self) -> Result;
            }
        }
    }
}

如果需要使用标准库外的类型,这里可能会有额外的extern crate块。例如,Serde的#[derive(Serialize)]宏想要列出serde crate、SerializeSerializer类型以及它们在运行时可能被调用的任何方法。

在宏实现的其余部分,所有类型信息都是基于在此库声明中给出的签名进行静态推断的。

接下来,宏的入口点是一个普通的proc_macro_derive函数,就像任何其他方式定义的 derive 宏一样。

再次强调,反射API只是一个定义过程宏的手段。尽管下面看起来可能不同,这里所写的所有内容都是在编译时执行的。reflect库会输出到一个输出TokenStream的生成代码,这个代码会被编译到宏用户的crate中。这个令牌流中不包含任何运行时反射的痕迹。

use proc_macro::TokenStream;

// Macro that is called when someone writes derive(MyDebug) on a data structure.
// It returns a fragment of Rust source code (TokenStream) containing an
// implementation of Debug for the input data structure. The macro uses
// compile-time reflection internally, but the generated Debug impl is exactly
// as if this macro were handwritten without reflection.
#[proc_macro_derive(MyDebug)]
pub fn derive(input: TokenStream) -> TokenStream {
    // Feed the tokens describing the data structure into the reflection library
    // for parsing and analysis. We provide a callback that describes what trait
    // impl(s) the reflection library will need to generate code for.
    reflect::derive(input, |ex| {
        // Instruct the library to generate an impl of Debug for the derive
        // macro's target type / Self type.
        ex.make_trait_impl(RUNTIME::std::fmt::Debug, ex.target_type(), |block| {
            // Instruct the library to compile debug_fmt (a function shown
            // below) into the source code for the impl's Debug::fmt method.
            block.make_function(RUNTIME::std::fmt::Debug::fmt, debug_fmt);
        });
    })
}

以下看起来像是一个执行运行时反射的函数。它接收类型为reflect::Value的函数参数,并且可以传递它们,提取它们的字段,检查属性,调用方法等。

use reflect::*;

// This function will get compiled into Debug::fmt, which has this signature:
//
//     fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result
//
fn debug_fmt(f: MakeFunction) -> Value {
    let receiver: reflect::Value = f.arg(0);  // this is `self`
    let formatter: reflect::Value = f.arg(1);

    // The input value may be any of unit struct, tuple struct, ordinary braced
    // struct, or enum.
    match receiver.data() {
        Data::Struct(receiver) => match receiver {
            Struct::Unit(receiver) => unimplemented!(),
            Struct::Tuple(receiver) => unimplemented!(),
            Struct::Struct(receiver) => {
                /* implemented below */
            }
        },
        // For an enum, the active variant of the enum may be any of unit
        // variant, tuple variant, or struct variant.
        Data::Enum(receiver) => receiver.match_variant(|variant| match variant {
            Variant::Unit(variant) => unimplemented!(),
            Variant::Tuple(variant) => unimplemented!(),
            Variant::Struct(variant) => unimplemented!(),
        }),
    }
}

在具有命名字段的struct的情况下,我们使用反射遍历struct的字段,并调用标准库的Formatter API的方法,将每个字段的值追加到调试输出中。

有关运行时应该执行的操作,请参考标准库API文档中的DebugStruct示例代码。

RUNTIME::开头的路径是指上面提到的library! { ... }片段中声明的库签名。

let builder = RUNTIME::std::fmt::Formatter::debug_struct
    .INVOKE(formatter, type_name)
    .reference_mut();

for field in receiver.fields() {
    RUNTIME::std::fmt::DebugStruct::field.INVOKE(
        builder,
        field.get_name(),
        field.get_value(),
    );
}

RUNTIME::std::fmt::DebugStruct::finish.INVOKE(builder)

反射库能够跟踪reflect::Value对象从一个INVOKE流向另一个INVOKE,并且包含一个编译器,可以将这种数据流以健壮的方式编译成强类型Rust源代码。在本演示中,当对具有两个字段的括号结构调用Debug derive宏时,反射库会生成一个类似下面的trait impl:

#[derive(MyDebug)]
struct Point {
    x: i32,
    y: i32,
}

下面是生成的代码:

// expands to:
impl ::std::fmt::Debug for Point {
    fn fmt(&self, _arg1: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {
        match *self {
            Point { x: ref _v0, y: ref _v1 } => {
                let mut _v2 = ::std::fmt::Formatter::debug_struct(_arg1, "Point");
                let _ = ::std::fmt::DebugStruct::field(&mut _v2, "x", _v0);
                let _ = ::std::fmt::DebugStruct::field(&mut _v2, "y", _v1);
                let _v3 = ::std::fmt::DebugStruct::finish(&mut _v2);
                _v3
            }
        }
    }
}

这个生成的代码最终会在运行时执行。请注意,这里没有反射。事实上,这与标准库内建的derive(Debug)宏为相同的数据结构生成的代码几乎完全相同。

健壮性和出错的原因

我上面提到,仅仅使用synquote实现健壮的宏是非常具有挑战性的。

我喜欢的例子是取一个struct字段,并将其临时包装在一个新的struct中。这是一个从serde_derive处理serialize_with属性中得到的实际用例。从概念上讲

let input: DeriveInput = syn::parse(...).unwrap();

// Pull out one of the field types.
let type_of_field_x: syn::Type = /* ... */;

quote! {
    // Very not robust.
    struct Wrapper<'a> {
        x: &'a #type_of_field_x,
    }

    Wrapper { x: &self.x }
}

使quote!部分生成所有可能的type_of_field_x值的可编译代码是极其复杂的。宏作者需要考虑并处理以下所有内容,以使这一功能可靠地运行:

  • type_of_field_x使用的生命周期参数;
  • type_of_field_x使用的类型参数;
  • type_of_field_x使用的关联类型;
  • input上的where子句,它约束了上述任何一个;
  • 类似地,input的类型参数上的trait界限;
  • 影响input其他任何字段的where子句或界限;
  • 需要剥离的input上的类型参数默认值。

相比之下,reflect库将能够以更少的思考从宏作者那里得到正确的结果。可能只需要像这样简单:

let wrapper: reflect::Type = reflect::new_struct_type();

wrapper.instantiate(vec![input.get_field("x").reference()])

剩余工作

目前,该概念验证只能生成勉强运行的代码,用于我们的简单 Debug 派生。要生成在存在生命周期和泛型参数的情况下以及涉及更复杂类型的库签名中的健壮代码,需要做更多的工作来完善 reflect 库。

关键在于,所有剩余的工作都应在不触及我们的 Debug 派生代码的情况下完成。 reflect 的承诺是,如果宏对于最基本的案例有效(上面的代码已经做到了),那么它也将在所有边缘情况下有效。从现在起,将简单的反射样式的 reflect::Value 对象操作编译成完全通用和健壮的过程宏的责任就落在了 reflect 的身上。


许可证

根据您的选择,许可协议为 Apache License, Version 2.0MIT 许可证
除非您明确指出,否则根据 Apache-2.0 许可证的定义,您有意提交的任何贡献,旨在包含在此软件包中,将按照上述双重许可方式,不附加任何额外条款或条件。

依赖关系

约 250-690KB
~16K SLoC