美文网首页Rust
关于rust的“宏”

关于rust的“宏”

作者: 神奇的考拉 | 来源:发表于2022-04-26 15:51 被阅读0次

    一、概述

    为了解决rust语法元素的扩展,并能复用现有的代码,在rust编写的程序中普遍使用.
    通过宏定义和宏调用或宏引用来简化代码的编写,以复用已有的代码来扩展语法元素:

    • 自定义语法元素

    有时语言层面定义的语法元素有“缺陷”或“不足”,甚至用已有的语法元素来描述一段逻辑看起来比较复杂或不够灵活等场景;我们可以通过来帮忙我们

    # 比如rust使用print宏来实现c语言中printf函数的功能
    print!("hello world");
    
    # 使用vec宏来简化vector的定义
    let v = vec!["a", "b", "c"];
    
    • 简化复用代码

    宏本身是具有自定义语法元素的“能力”,通过使用它能够简化和复用代码。

    # 比如给自定义的struct添加Clone和Debug trait, 在编译时会自动为其提供Clone和Debug的缺省实现代码
    #[derive(Clone, Debug)]
    struct StructDemo {
       // 省略代码
    }
    

    二、宏

    1.定义
    在rust中,宏就是一种代码复用的机制,它提供了基础语法,并允许开发者根据需要使用这些语法基础来自定义新的语法,然后方便其他的开发者调用或引用这些自定义的语法元素。
    宏由两部分构成:宏定义、宏调用或宏引用。

    2.分类
    主要是因为涉及到自定义语法及其定义和调用的复杂性,再加上复用代码的使用场景不同,将宏分为:声明宏和过程宏;
    不过这两类宏的定义方式、调用或引用方式可能各不相同,但其扩展语法元素和复用代码的目的还是一致的。

    3.宏对比(声明宏 vs 过程宏)

    定义
    • 声明宏
      在rust中,声明宏本质就是匹配规则 + 转译替换规则; 或者就是“代码模版按照匹配规则进行代码化替换”;
      声明宏通过给定一个宏名称,并为其指定多组输入匹配规则及对应的转译替换规则;
      调用声明宏时,就是传入一串代码片段,在编译期由编译期根据传入代码片段来匹配宏自身定义的匹配规则,再经过转译替换规则,将宏调用代码替换为转译后的代码;
      自定义的声明宏未必是一个有效的rust函数,但是需要确保其名称在宏扩展阶段能够被标识并被调用,匹配时是按照逐个规则“深度匹配的”,一旦不匹配就会出现“异常”;
      匹配规则和转移替换规则在调用时会进行分词和解析的,其内容是字面上可见的文字流。
    # 参数“hello world”作为一个分词后的文字量,传给println宏,并将其转移替换为对其他函数调用
    println!("hello world");
    
    • 过程宏
      相对来说过程宏就比较特殊: 定义一个过程宏首先确定其crate为proc-macro=true,然后定义一个传统的rust函数,并指定它的属性及其输入和输出;
      需要使用提供的proc_macroquotesyn等crate来实现过程宏的函数逻辑;
      过程宏根据定义的属性和输入及输出参数的不同,可分为:类似函数的过程宏、继承过程宏、属性过程宏, 不过他们的使用场景各不相同。
    声明宏和过程宏的区别
    • 1、定义方式
      声明宏通过macro_rules! macro_name {}来进行定义的,并实现匹配转译替换字面上的代码输入,宏定义的逻辑编译后存于当前的crate中;当前编译器在编译其他crate时使用到该crate的声明宏时,编译期会自动加载被调用的crate中宏逻辑来实现转译替换逻辑;

    过程宏则是通过定义一个特殊类型的crate<带有[lib] proc-macro=true标识>,在这个crate中定义符合特定属性和输入输出参数的传统rust函数,其中继承过程宏的函数名与过程宏名可不一样;当编译器编译这个特殊的proc-macro crate时,它会生成一个动态库,并导出相关过程宏对应的rust函数;

    • 2、调用方式
      当声明宏被定义和调用时,编译器会使用类型正则表达式的方式,来匹配字面上的代码并替换字面上的代码,生成新的字面上的代码,再进行解析生成语法树节点;
      当过程宏被编译时,编译器会将字面上的代码生成TokenStream对象,并动态搜索和加载过程宏对应的动态库,然后调用对应的过程宏rust函数,并将其输出的TokenStream对象转换为编译器内部的AST语法树节点;过程宏在定义和引用时,编译器会以调用动态库函数的方式来实现语法树节点的替换或新增的;

    • 与crate中其他语法元素的关系
      包含过程宏定义的crate不会被链接到使用它的crate中,往往只会通过被编译器动态调用,而这个crate往往不要包括需要链接开发者的lib或bin等其他rust语法元素中,比如fn/trait等;

    包含声明宏定义的crate可被包含在调用它的crate中,它可包含需要链接开发者的lib或bin中的所有rust语法元素,比如fn/trait等;

    4、宏与函数的区别
    声明宏没有对应Rust函数,语法上 MacroDefMacCall分别属于不同的ItemKind,类似Mod、Fn、Struct等也是一种ItemKind;

    过程宏则对应一个传统Rust函数,并带上不同的属性#[proc_macro]、#[proc_macro_derive(name_xx)]、#[proc_macro_attribute],不过其调用者往往来自于编译器等特定用途的程序,不会是普通开发者开发的程序;

    另外不管是声明宏调用还是过程宏的调用,往往发生在编译器编译阶段,而传统Rust函数的调用则发生在用户程序的运行阶段

    由于定义和实现过程宏的过程比较复杂,往往涉及到对proc_macro、quote、syn crate的了解和使用,所以定义一个过程宏,相对来讲比较复杂,但如能掌握它们抽象出来的概念的话,使用起来也会非常直接和明了

    三、示例

    1、声明宏

    • 语法定义
    // 
    macro_rules! macro_name {
      // 省略规则
      匹配规则   => {}
      .....
    }
    

    定义语法(部分)
    匹配规则是除了$、{}、()、[]之外的token组成的序列;
    转译替换规则是一个分隔的TokenTree;

    Syntax
    MacroRulesDefinition :
       macro_rules ! IDENTIFIER MacroRulesDef
    
    MacroRulesDef :
          ( MacroRules ) ;
       | [ MacroRules ] ;
       | { MacroRules }
    
    MacroRules :
       MacroRule ( ; MacroRule )* ;?
    
    MacroRule :
       MacroMatcher => MacroTranscriber
    
    MacroMatcher :
          ( MacroMatch* )
       | [ MacroMatch* ]
       | { MacroMatch* }
    
    MacroMatch :
          Token except $ and delimiters
       | MacroMatcher
       | $ IDENTIFIER : MacroFragSpec
       | $ ( MacroMatch+ ) MacroRepSep? MacroRepOp
    
    MacroFragSpec :
       block | expr | ident | item | lifetime | literal
       | meta | pat | path | stmt | tt | ty | vis
    
    MacroRepSep :
       Token except delimiters and repetition operators
    
    MacroRepOp :
       * | + | ?
    
    MacroTranscriber :
       DelimTokenTree
    

    匹配规则中包含meta变量用$标识来标示,其类型包括block、expr、ident、item、lifetime、literal、meta、pat、path、stmt、tt、ty、vis;
    简单示例如下:

    /// test宏定义了两组匹配和转译替换规则
        macro_rules! test_macro {
            ($left:expr; and $right:expr) => {
                println!("{:?} and {:?} is {:?}",
                         // $left变量的内容对应匹配上的语法片段的内容
                         stringify!($left),
                         // $right变量的内容对应匹配上的语法片段的内容
                         stringify!($right),
                         $left && $right)
            };
            ($left:expr; or $right:expr) => {
                println!("{:?} or {:?} is {:?}",
                         stringify!($left),
                         stringify!($right),
                         $left || $right)
            };
        }
    

    测试

    /// 传入的字面上的代码片段,解析后生成的语法片段,
    ///  - 在解析过程中进行简易分词和解析后生成一个语法片段(包含解析出来的不同类型及其对应的值)
    ///  - 与声明宏中定义的匹配规则包含的字面量token和meta变量类型等,按照从左到右一对一的方式进行匹配(匹配都是进行深度匹配的,一旦当前规则匹配过程出现问题,则不会再进行后续的规则匹配)
    ///  - 一旦提供的语法片段和某个声明宏定义的规则匹配了,那么对应类型的值绑定到meta变量中,即用$标示来代替;
    ///    再匹配后,进入转译替换阶段,直接读取对应的转译替换规则的内容,将其meta变量的内容用上一阶段绑定过来的值替换,完成处理后输出即可;
    /// 正好能匹配上第一个匹配规则;
    /// - 第一个匹配规则为
    ///  一个表达式类型语法片段和; and 和另一个表达式类型语法片段
    ///  其中;和and需要字面上一对一匹配;
    test_macro!(1i32 + 1 == 2i32; and 2i32 * 2 == 4i32);
    
    /// 下面传入的字面上的代码片段,解析后生成的语法片段,
    /// 正好能匹配上第二个匹配规则;
    /// - 第二个匹配规则为:
    /// 一个表达式类型语法片段和; or 和另一个表达式类型语法片段
    /// 其中;和or需要字面上一对一匹配;
    test_macro!(true; or false);
    

    在声明宏中为了简化表达重复具有相同类型的meta变量,就使用特别符号来描述相关规则

      • 代表任何数量的重复,数量可以是0个;
    • +代表任何数据的重复, 数量至少有1个;
    • ?代表可选的一个变量, 0或最多一个;
      大概的样式:(var: metatype),*,其中,可省略; $()代表一个分组;
    macro_rules! find_min {
            ($x:expr) => ($x);
            // $x语法表达式,后面跟上至少一个语法表达式$y
            ($x:expr, $($y:expr),+) => (
                // 将重复的匹配上的语法表达式$y至少一个或多个
                // 递归传给find_min宏,$x直接传给方法min
                std::cmp::min($x, find_min!($($y),+))
            )
        }
    

    样例验证

    println!("{}", find_min!(1u32));
    println!("{}", find_min!(1u32 + 2, 2u32));
    println!("{}", find_min!(5u32, 2u32 * 3, 4u32));
    

    当需要在声明宏的输出内容中引用宏定义所在的crate的标识,则需要使用$crate::ident_name来输出;
    声明宏调用时传入的字面上代码片段,分词解析后就不会有crate类型;而声明宏输出的内容包括的各种标识符,应在调用该声明宏的crate中找到其定义,否则宏输出编译会出错;

    // 宏thd_name会使用当前宏定义crate中的get_tag_from_thread_name方法
    #[macro_export]
    macro_rules! thd_name {
        ($name:expr) => {{
            $crate::get_tag_from_thread_name()
                .map(|tag| format!("{}::{}", $name, tag))
                .unwrap_or_else(|| $name.to_owned())
        }};
    }
    

    接下来看看声明宏的可见范围:
    声明宏的定义属于一个Item,其宏的定义可以在一个crate,而调用宏可以在另一个不同的crate中; 那么理论上可以存在于crate的mod中,或任何crate中可以出现Item的地方并被使用;但是由于历史原因和声明宏没有象其他Item的可见属性pub等,声明宏的可见范围及调用路径方式与传统Item不一样;
    其规则如下

    • 1.若没有使用带路径的方式来调用声明宏,则直接在当前代码块范围来匹配相关宏的名称,并进行调用,如果没有找到则从带有路径的范围中查找;
    • 2.若使用带路径的方式来调用声明宏,则直接从带路径的范围来查找,而不从当前字面范围来匹配查找;
    • 3.声明宏定义后的可见范围与let变量的可见范围类似,在它定义的代码块范围及子范围中可直接引用它或覆盖定义它;
    • 4.如想在大于定义它的代码块范围中使用它,则需要使用宏导出导入;
    use lazy_static::lazy_static;//带路径方式导入的宏
    macro_rules! lazy_static { //当前代码块范围定义的宏
        (lazy) => {};
    }
    // 没有带路径的调用方式,直接从当前代码块范围来找到当前定义的宏
    lazy_static!{lazy}
     // 带路径的调用方式,忽略当前代码块定义的宏,找到导入的宏
    self::lazy_static!{}
    
    /// src/lib.rs
    mod has_macro {
        // m!{} // 错误:当前代码块中宏没有定义.
        macro_rules! m {
            () => {};
        }
        m!{} // OK: 在当前代码块中已定义m宏.
        mod uses_macro;
    }
    // 错误: 当前代码块中并没有定义宏m,而是在其子mod has_macro块中有定义;
    // m!{} 
    
    /// 另一个src/has_macro/uses_macro.rs文件,被引用到has_macro mod中
    m!{} // OK: 宏m的定义在src/lib.rs中的has_macro mod中
    
    // 宏在mod代码块中的定义范围
    macro_rules! m {
        (1) => {};
    }
    
    m!(1);// 当前代码块范围有宏m定义
    
    mod inner {
        m!(1); // 当前代码块的父mod中有定义宏m,可以直接引用
        macro_rules! m { // 覆盖父mod中定义的宏m
            (2) => {};
        }
        // m!(1); // 错误: 没有匹配'1'的规则,原来的已被覆盖
        m!(2); // 当面代码块有定义宏m
    
        macro_rules! m {
            (3) => {};
        }
        m!(3); // 当面代码块有定义宏m,原来的已被覆盖
    }
    
    m!(1);//当面代码块有定义宏m
    
    // 宏在函数代码块中的定义范围
    fn foo() {
        // m!(); // 错误: 宏m在当前代码块没有定义.
        macro_rules! m {
            () => {};
        }
        m!();// 当前代码块范围有宏m定义
    }
    // m!(); // 错误: 宏m不在当前代码块范围中定义.
    

    使用导出#[macro_export]和导入#[macro_import]的用法,来“放大”声明宏的可见范围;
    一般说来,宏定义后没有带路径的调用方式,只有当一个宏定义时加上宏导出属性#[macro_export],即代表将其定义的代码块范围提升到crate级别范围;

    在一个宏被导出后,当前crate中的其他mod可以使用带路径的方式来调用它;

    在一个宏被导出后,其它crate可以使用宏导入属性#[macro_use]的方式,将其中导出宏名称导入到当前crate范围中;

    对于同一crate中不同的mod中定义的宏,可以使用#[macro_use]方式来提升定义宏的可见范围,而无须使用[macro_export];

    self::m!(); // OK:带路径的调用方式,会查找当前crate中导出的宏
    m!(); // OK: 不带路径的调用方式,会查找当前crate中导出的宏
    
    mod inner {
        // 子mod块范围使用带路径方式调用,在当前crate中可找到导出的宏
        super::m!();
        crate::m!();
    }
    
    mod mac {
        #[macro_export]
        // 子mod块范围中定义的宏m导出到当前crate中
        macro_rules! m {
            () => {};
        }
    }
    // 导入外部crate中的宏m或者使用#[macro_use]来导入其所有导出的宏.
    
    #[macro_use(m)]
    extern crate lazy_static;
    
    m!{} // 外部crate宏已导入到当前crate
    // self::m!{} // 错误: m没有在`self`中定义
    

    2、过程宏

    • 类似函数的过程宏
      其对应函数声明中的输入参数item是proc_macro crate中定义的TokenStream,由调用时传递过来的字面上的代码生成,它内部包含结构化的Token流,使用相关接口可以访问指定Token等;其对应函数声明中的输出是proc_macro crate中定义的TokenStream,字面上的代码串可以通过parse方法来生成;

    类似函数的过程宏,使用时类似声明宏调用方式,传入代码片段,调用后的输出结果会替换调用过程宏这个语法元素,类似声明宏调用;

    类似函数的过程宏的名称与对应的函数声明一致,它可应用在任何声明宏可被调用的地方;

    其示例如下:

    // 过程宏定义
    extern crate proc_macro;
    use proc_macro::TokenStream;
    // 过程宏输出的TokenStream中包含fn answer定义及实现
    #[proc_macro]
    pub fn make_answer(_item: TokenStream) -> TokenStream {
        "fn answer() -> u32 { 42 }".parse().unwrap()
    }
    
    // 过程宏调用
    extern crate proc_macro_examples;
    use proc_macro_examples::make_answer;
    make_answer!(); // 类似函数简易宏的调用
    fn main() {
        println!("{}", answer());// 直接调用过程宏输出的fn answer
    }
    
    • 继承过程宏
      其对应函数声明中的输入参数item是附加有指定过程宏属性的整个自定义类型Item对应的TokenStream,

    其对应函数声明中的输出是一个独立的Item对应的TokenStream,它与自定义类型Item属于同一个mod或block中;

    继承过程宏的使用是以属性#[derive(过程宏名)]的方式出现在struct、enum、union自定义类型声明中;

    继承过程宏的名称包含在对应函数的属性中,可与对应函数名不同;

    其示例如下:

    // 过程宏定义
    extern crate proc_macro;
    use proc_macro::TokenStream;
    // 定义一个属性过程宏名称为AnserFn的过程宏
    #[proc_macro_derive(AnswerFn)]
    pub fn derive_answer_fn(_item: TokenStream) -> TokenStream {
        "fn answer() -> u32 { 42 }".parse().unwrap()
    }
    
    // 过程宏引用
    extern crate proc_macro_examples;
    use proc_macro_examples::AnswerFn;
    // 将过程宏AnswerFn引用到struct声明定义中
    // 编译时触发过程宏对应函数调用,生成fn answer
    #[derive(AnswerFn)]
    struct Struct;
    
    fn main() {
        assert_eq!(42, answer());// 直接调用过程宏输出的fn answer
    }
    

    带自定义属性名称的过程宏,过程宏的函数实现可对_item中是否有自定义属性进行检查和判断等

    /// 定义一个属性过程宏名称为HelperAttr的过程宏,
    /// 并且支持输入的item定义中包含名称为helper的属性
    #[proc_macro_derive(HelperAttr, attributes(helper))]
    pub fn derive_helper_attr(_item: TokenStream) -> TokenStream {
        TokenStream::new()
    }
    
    #[derive(HelperAttr)]
    struct Struct {
        #[helper] // 与自定义attributes中的属性名helper对应
        field:()
    }
    

    4.属性过程宏

    其对应函数声明中的输入参数attr是指属性的内容对应的TokenStream;

    输入参数item,是指附加有指定属性过程宏的属性的自定义类型Item对应的TokenStream,但不包括属性部分,属性部分已在attr参数中体现;

    attr和item内部包含结构化的Token流,使用相关接口可以访问指定Token等;

    其对应函数声明中的输出是proc_macro crate中定义的TokenStream,字面上的代码串可以通过parse方法来生成;

    属性过程宏,使用属性#[属性过程宏名称]方式来引用过程宏,引用后的输出结果会替换引用过程宏这个语法元素,类似简易宏调用;

    属性过程宏的名称与对应的函数声明一致;

    其示例如下:

    /// 定义一个名称为show_streams的属性过程宏
    #[proc_macro_attribute]
    pub fn show_streams(attr: TokenStream, item: TokenStream)
        -> TokenStream {
        /// 调用宏时打印输出attr
        println!("attr: \"{}\"", attr.to_string());
        /// 调用宏时打印输出item
        println!("item: \"{}\"", item.to_string());
        /// 调用宏时输出原来输出的item
        item
    }
    
    /// 引用属性过程宏
    // src/lib.rs
    extern crate my_macro;
    use my_macro::show_streams;
    // Example: Basic function
    #[show_streams]
    fn invoke1() {}
    // out: attr: ""
    // out: item: "fn invoke1() { }"
    // Example: Attribute with input
    #[show_streams(bar)]
    fn invoke2() {}
    // out: attr: "bar"
    // out: item: "fn invoke2() {}"
    // Example: Multiple tokens in the input
    #[show_streams(multiple => tokens)]
    fn invoke3() {}
    // out: attr: "multiple => tokens"
    // out: item: "fn invoke3() {}"
    

    宏调试

    Rust中对宏的调用或引用,往往在编译器生成完整语法树阶段完成,宏调用的结果是否正常或有效,后面还会进行严格的类型和借用检查等;

    # 对单个 rs 文件
    rustc -Z unpretty=expanded hello.rs
    # 对项目里的二进制 rs 文件
    cargo rustc --bin hello -- -Z unpretty=expanded
    

    rust的宏

    相关文章

      网友评论

        本文标题:关于rust的“宏”

        本文链接:https://www.haomeiwen.com/subject/xygfyrtx.html