Skip to content

Latest commit

 

History

History
383 lines (292 loc) · 15.4 KB

macro.md

File metadata and controls

383 lines (292 loc) · 15.4 KB

Table of Contents generated with DocToc

macro 宏

元编程可以让开发者将原生语言写的代码作为数据输入,经过自定义的逻辑,重新输出为新的代码并作为整体代码的一部分。 这个过程一般在编译时期完成(对于编译型语言来说),所以让人觉得这是一种神奇的 “黑魔法“

宏就两大类:对代码模板做简单替换的声明宏(declarative macro)、可以深度定制和生成代码的过程宏(procedural macro)。

宏调用有三种等价的形式:marco!(xx), macro![xxx], macro!{xx}。惯例是:

  • 函数传参调用场景使用 () 形式,如 println!();
  • 字面量初始化使用 [] 形式,如 vec![0; 4];

声明宏(declarative macro)

声明宏可以用 macro_rules! 来描述,比如像 vec![]、println!、以及 info!,它们都是声明宏。

声明式宏类似于 match 匹配。它可以将表达式的结果与多个模式进行匹配。一旦匹配成功,那么该模式相关联的代码将被展开。和 match 不同的是,宏里的值是一段 rust 源代码。所有这些都发生在编译期,并没有运行期的性能损耗

#[cfg(all(not(no_global_oom_handling), not(test)))]
#[macro_export]
#[stable(feature = "rust1", since = "1.0.0")]
#[rustc_diagnostic_item = "vec_macro"]
#[allow_internal_unstable(rustc_attrs, liballoc_internals)]
macro_rules! vec {
    () => (
        $crate::__rust_force_expr!($crate::vec::Vec::new())
    );
    // 匹配到以 ; 分隔的两个表达式,; 左边的表达式的值将被捕获匹配到 $elem,; 右边的表达式的值将被捕获匹配到 $n
    ($elem:expr; $n:expr) => (
        $crate::__rust_force_expr!($crate::vec::from_elem($elem, $n))
    );
    ($($x:expr),+ $(,)?) => (
        $crate::__rust_force_expr!(<[_]>::into_vec(
            // This rustc_box is not required, but it produces a dramatic improvement in compile
            // time when constructing arrays with many elements.
            #[rustc_box]
            $crate::boxed::Box::new([$($x),+])
        ))
    );
}
  • $crate 是一个特殊的元变量,用来指代当前 crate
  • 条件捕获的参数使用 $ 开头的标识符来声明
  • #[macro_export]标签是用来声明:只要 use 了这个crate,就可以使用该宏。同时包含被 export 出的宏的模块,在声明时必须放在前面,否则靠前的模块里找不到这些宏

macro_rules! 的基本结构

macro_rules! $ name {
  $ rule0;
  $ rule1;
  //...
  $ ruleN;
}

每一条 rule 其实就是模式匹配和代码扩展生成:

( $matcher ) => { $expansion };

类似 vec![0; 10] 的功能时,0; 10 ,其中 ; 左边是元素初始值 0,; 右边是个数 10。那么匹配似乎可以为:

($elem ; $n) => { ... }

描述是不精确的,我们还需要加上捕获方式,即捕获的是一个表达式

($elem:expr ; $n:expr) => { ... }
let v = vec![1, 2, 3];

先看 $matcher 部分,即 1, 2, 3。像这种需要匹配一系列 token 的模式,我们需要使用宏里的重复匹配模式。比如要想匹配 1,2,3,可以写成:

( $ ( $ elem:expr ), * ) => { ... }

即 $(...),* 模式,而 (...) 则是和上节中变量捕获的方式是一样的,即 $elem:expr。, 表示为分隔符,* 表示匹配 0 或者多次.

为参数明确类型,哪些类型可用也整理在这里了:

  • item,比如一个函数、结构体、模块等。
  • block,代码块。比如一系列由花括号包裹的表达式和语句。
  • stmt,语句。比如一个赋值语句。
  • pat,模式。
  • expr,表达式。刚才的例子使用过了。ty,类型。比如 Vec。
  • ident,标识符。比如一个变量名。path,路径。比如:foo、::std::mem::replace、transmute::<_, int>。meta,元数据。一般是在 #[...] 和 #![...] 属性内部的数据。
  • tt,单个的 token 树(一个独立的 token 或一系列在匹配完整的定界符 ()、[] 或 {} 中的 token)。
  • vis,可能为空的一个 Visibility 修饰符。比如 pub、pub(crate)

声明宏的卫生性 hygiene

宏的卫生性,其实说的就是宏在上下文工作不影响或不受周围环境的影响。或者换句话来说,就是宏的调用是没有 side effect。对于 macro_rules!, 它是部分卫生的(partially hygienic)。我们目前阶段可以不用太关注 macro_rules! 在哪些场景是 “不卫生” 的,而是了解一下 macro_rules! 是如何在大多数场景做到 “卫生” 的

#![feature(prelude_import)]
#[prelude_import]
use std::prelude::rust_2021::*;
#[macro_use]
extern crate std;
use chapter15_macro::make_local;
fn main() {
    let local = 42;
    let local = 0;
    match (&local, &42) {
        (left_val, right_val) => {
            if !(*left_val == *right_val) {
                let kind = ::core::panicking::AssertKind::Eq;
                ::core::panicking::assert_failed(
                    kind,
                    &*left_val,
                    &*right_val,
                    ::core::option::Option::None,
                );
            }
        }
    };
}

Rust 看来,macro_rules 中的 local 和 main() 里的 local 分别有着不同的颜色,所以不会将其混淆

过程宏(procedural macro)

过程宏分为三种:

  • 函数宏(function-like macro):custom!(…) 看起来像函数的宏,但在编译期进行处理。比如 sqlx 里的 query 宏,它内部展开出一个 expand_query 函数宏。你可能想象不到,看上去一个简单的 query 处理,内部有多么庞大的代码结构。
  • 属性宏(attribute-like macro):#[CustomAttribute]可以在其他代码块上添加属性,为代码块提供更多功能。比如 rocket 的 get / put 等路由属性,#[tokio::main] 来引入 runtime。
  • 派生宏(derive macro 可推导宏):为 derive 属性添加新的功能,一般用来为 struct/enum/union 实现特定的 trait。这是我们平时使用最多的宏,比如 #[derive(Debug)] 为我们的数据结构提供 Debug trait 的实现、#[derive(Serialize, Deserialize)]为我们的数据结构提供 serde 相关 trait 的实现

它更像函数,他接受一些代码作为参数输入,然后对他们进行加工,生成新的代码,他不是在做声明式宏那样的模式匹配

不能在原始的crate中直接写过程式宏,需要把过程式宏放到一个单独的crate中(以后可能会消除这种约定)。定义过程式宏的方法如下

use proc_macro;

#[some_attribute] // # proc_macro_derive  proc_macro_attribute  proc_macro
pub fn some_name(input: TokenStream) -> TokenStream {}

在单独的 crate package 中定义过程宏的原因:

proc macro 定义需要先被编译器编译为 host 架构类型,后续编译使用它的代码时,编译器才能 dlopen 和执行它们来为 target 架构生成代码; 非过程宏 crate 需要被编译成 target 架构类型,然后才能被和其它 target 架构的动态库链接;

派生宏(derive macro 可推导宏)

派生宏可以自动生成实现特定trait的代码,减少手动实现的繁琐性。

// 为数据类型派生方法的示例
#[derive(Debug)]
pub struct User {
    username: String,
    first_name: String,
    last_name: String,
}

#[derive(Debug)]是一个派生宏,它告诉Rust编译器为Person结构体自动生成Debug trait的实现.

impl core::fmt::Debug for User {
    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
        f.debug_struct(
            "User"
        )
            .field("username", &self.username)
            .field("first_name", &self.first_name)
            .field("last_name", &self.last_name)
            .finish()
    }
}

属性宏(attribute macro)

在Rust中,属性宏是一种特殊的宏,它允许开发者在代码上方添加自定义的属性,并在编译期间对代码进行处理。 属性宏除了数据类型外,通常还应用于代码块,如函数、impl 块、内联块等。它们通常用于以某种方式转换目标代码,或使用附加信息注解它。

这些宏最常见的用例是修改函数以添加额外的功能或逻辑。例如,你可以轻松编写一个属性宏:

  • 记录所有输入和输出参数
  • 记录函数的总运行时间
  • 统计函数调用次数
  • 向任何结构体添加预定义的附加字段

属性宏使用proc_macro_attribute属性来定义

extern crate proc_macro;

use proc_macro::TokenStream;

#[proc_macro_attribute]
pub fn attribute_macro(attr: TokenStream, item: TokenStream) -> TokenStream {
    // 宏的处理逻辑
    // ...
}

使用proc_macro_attribute属性来定义了一个名为attribute_macro的属性宏。 属性宏接受两个TokenStream参数:attr表示属性的输入,item表示应用该属性的代码块。在宏的处理逻辑中,我们可以根据attr和item对代码进行定制化处理,并返回一个TokenStream作为输出。

第三方实现 -->tokio

#[tokio::main] 转换

#[proc_macro_attribute]
pub fn main(args: TokenStream, item: TokenStream) -> TokenStream {
    entry::main(args.into(), item.into(), true).into()
}
//  tokio-macros-2.4.0/src/entry.rs
fn parse_knobs(mut input: ItemFn, is_test: bool, config: FinalConfig) -> TokenStream {
    // ...

    let mut rt = match config.flavor {
        RuntimeFlavor::CurrentThread => quote_spanned! {last_stmt_start_span=>
            #crate_path::runtime::Builder::new_current_thread()
        },
        RuntimeFlavor::Threaded => quote_spanned! {last_stmt_start_span=>
            #crate_path::runtime::Builder::new_multi_thread()
        },
    };
    // ... 

    let generated_attrs = if is_test {
        quote! {
            #[::core::prelude::v1::test]
        }
    } else {
        quote! {}
    };

    let body_ident = quote! { body };
    let last_block = quote_spanned! {last_stmt_end_span=>
        #[allow(clippy::expect_used, clippy::diverging_sub_expression)]
        {
            return #rt
                .enable_all()
                .build()
                .expect("Failed building the Runtime")
                .block_on(#body_ident);
        }
    };

    let body = input.body();

    // .. 

    input.into_tokens(generated_attrs, body, last_block)
}

类函数宏(function-like macro)

类函数宏可以让我们定义像函数那样调用的宏

其他编程语言常见的元编程方式

  • Go 的 ast 包和 go generate 机制:Go 没有显示提供元编程的相应机制,转而提供了一些相对不那么优雅的机制来实现类似于元编程的效果。比如 ast 包可以暴露 Go 程序的语法树,从而让开发者可在编译时期对源代码进行修改或者根据模版生成其他类型代码。
  • C++ 的 Template 编程:据说 C++ 的 Template 编程是图灵完备的,可在编译时期完成很多让人瞠目结舌的逻辑。由于 C++ 的 Template 编程非常复杂且难以掌握,所以易用性非常差。
  • C 语言的宏:这估计是大多数程序员对于宏的最初体验。个人觉得, C 语言中的宏本质上是发生在预处理过程的文本替换,是一种非常简单原始的元编程机制。而正是这种原始能力,导致 C 语言的宏结合编译器的各种扩展充满了各种奇技淫巧,可读性和可调试性都非常差,而且稍不小心就很容易写出错误的宏

工具

cargo-expand

https://github.com/dtolnay/cargo-expand

一个Rust cargo子命令扩展,通过简单的 cargo expand 命令,你可以获取当前项目中所有源码经过宏展开后的结果

# 直接安装 cargo-expand 插件
$ cargo install cargo-expand

syn--语法解析器。将输入的 token 流解析为 Rust AST

syn 是一个对 TokenStream 解析的库,它提供了丰富的数据结构,对语法树中遇到的各种 Rust 语法都有支持。

比如一个 Struct 结构,在 TokenStream 中,看到的就是一系列 TokenTree,而通过 syn 解析后,struct 的各种属性以及它的各个字段,都有明确的类型。这样,我们可以很方便地通过模式匹配来选择合适的类型进行对应的处理。

ast_struct! {
    /// Data structure sent to a `proc_macro_derive` macro.
    ///
    /// *This type is available only if Syn is built with the `"derive"` feature.*
    #[cfg_attr(doc_cfg, doc(cfg(feature = "derive")))]
    pub struct DeriveInput {
        /// Attributes tagged on the whole struct or enum.
        pub attrs: Vec<Attribute>,

        /// 可见性说明符 of the struct or enum.
        pub vis: Visibility,

        /// 标识符(名称) of the struct or enum.
        pub ident: Ident,

        /// 泛型参数的信息,包括生命周期.
        pub generics: Generics,

        /// Data within the struct or enum.
        pub data: Data,
    }
}

quote

quote 是一个帮助我们执行 syn 反向操作的库。它帮助我们将 Rust 源代码转换为可以从宏输出的 token 流

proc-macro2

标准库中有一个 proc-macro,但它提供的类型不能存在于过程宏之外。proc-macro2是一个标准库的包装器,使所有的内部类型在宏的上下文之外也能使用。 这允许 syn 和 quote 不仅用于过程宏,还可以在普通 Rust 代码中使用,如果你有这样的需求的话。而且,如果我们想要对我们的宏或其扩展进行单元测试,这将被广泛使用

darling

它有助于解析和处理宏的参数,否则由于需要从语法树中手动解析它,这将是一个繁琐的过程。darling 为我们提供了类似 serde 的能力,可以将输入参数树自动解析为我们的参数结构体。它还帮助我们处理无效参数、必需参数等错误。

参考