Rust之Macro

编译过程

整体流程:[源代码]->分词->[Tokens词条流]->解析->[AST]->语法分析,宏扩展→[高级中间语言HIR]->类型检查->[中级中间语言MIR]->转换->[LLVM IR]->LLVM->[目标文件]->链接->[可执行程序]

详细过程如下:

  1. 解析输入:将.rs文件作为输入并进行解析生成AST(抽象语法树);
  2. 名称解析,宏扩展和属性配置:解析完毕后处理AST,处理#[cfg]节点解析路径,扩展宏;
  3. 转为HIR:名称解析完毕后将AST转换为HIR(高级中间表示),HIR比AST处理的更多,但是他不负责解析Rust的语法,例如((1+2)+3)1+2+3在AST中会保留括号,虽然两者的含义相同但是会被解析成不同的树,但是在HIR中括号节点将会被删除,这两个表达式会以相同的方式表达;
  4. 类型检查以及后续分析:处理HIR的重要步骤就是类型检查,例如使用x.f时如果我们不知道x的类型就无法判断访问的哪个f字段,类型检查会创建TypeckTables其中包括表达式的类型,方法的解析方式;
  5. 转为MIR以及后置处理:完成类型检查后,将HIR转为MIR(中级中间表示)进行借用检查以及优化;
  6. 转为LLVM IR和优化:LLVM进行优化,从而生成许多.o文件;
  7. 链接: 最后将那些.o文件链接在一起。

Rust在预处理时,会加入以下代码:

#![feature(prelude_import)]
#[prelude_import]
use std::prelude::v1::*;
#[macro_use]
extern crate std;

从而自动导入标准库。

声明宏(Declarative Macro)

宏解析器将宏扩展的时机在解析过程中。

声明宏中可以捕获的类型:

  • item,语言项,比如模块、声明、函数定义、类型定义、结构体定义、impl实现等。
  • block,代码块,由花括号限定的代码;
  • stmt,语句,一般是指以分号结尾的代码;
  • expr,表达式,会生成具体的值;
  • pat,模式;
  • ty,类型;
  • ident,标识符;
  • path,路径,比如foo、std::iter等;
  • meta,元信息,包含在#[...]或者#![...]属性内的信息;
  • tt,TokenTree的缩写,词条树;
  • vis,可见性,比如pub;
  • lifetime,生命周期参数。

词法树的范围比表达式的范围广,比如匹配一个语句块时,就必须用tt。

重复匹配的模式是“$(...) sep rep”,具体的说明如下:

  • $(...),代表要把重复匹配的模式置于其中;
  • sep,代表分隔符,常用逗号、分号和=>。这个分隔符可以依据具体的情况忽略。
  • rep,代表控制重复次数的标记,目前支持两种:* 和 +,代表“重复零次及以上”和“重复一次及以上”。

展开宏:

cargo rustc -- -Z unstable-options --pretty=expanded

rustc -Z unstable-options --pretty=expanded main.rs
#[macro_export]
macro_rules! my_vec { 
  // x 为重复匹配到的表达式,“,”可以根据情况忽略
    ($($x: expr), *) => {
        {
            let mut temp_vec = Vec::new();
          // 重复匹配到的值在这里访问
            $(
                temp_vec.push($x);
            )*
            temp_vec
        }
    };
}

hashmap实现

递归调用

#![allow(unused)]
macro_rules! hashmap {
  // 利用递归调用消去最后键值对的结尾逗号
  // 如果是末尾仍有逗号的,会转换成($($key:expr => $value:expr),*)
    ($($key:expr => $value:expr,)*) =>
        {  hashmap!($($key => $value),*) };
    ($($key:expr => $value:expr),* ) => {
        {
            let mut _map = ::std::collections::HashMap::new();
            $(
                _map.insert($key, $value);
            )*
           _map
       }
   };
}
fn main(){
    let map = hashmap!{
        "a" => 1,
        "b" => 2,
        "c" => 3, 
    };
    assert_eq!(map["a"], 1);
}

第一层转换为hashmap ! ("a" => 1, "b" => 2, "c" => 3)

重复匹配规则

macro_rules! hashmap {
    ($($key:expr => $value:expr),* $(,)*) => {
        {
            let mut _map = ::std::collections::HashMap::new();
            $(
                _map.insert($key, $value);
            )*
            _map
        }
   };
}
fn main(){
    let map = hashmap!{
        "a" => 1,
        "b" => 2,
        "c" => 3, 
    };
    assert_eq!(map["a"], 1);
}

预分配空间

macro_rules! unit {
    ($($x:tt)*) => (());
}
macro_rules! count {
    ($($key:expr),*) => (<[()]>::len(&[$(unit!($key)),*]));
}
macro_rules! hashmap {
    ($($key:expr => $value:expr),* $(,)*) => {
        {
           let _cap = count!($($key),*);
           let mut _map 
               = ::std::collections::HashMap::with_capacity(_cap);
           $(
               _map.insert($key, $value);
           )*
           _map
       }
   };
}

消除外部宏

#![feature(trace_macros)]
macro_rules! hashmap {
    (@unit $($x:tt)*) => (());
    (@count $($rest:expr),*) => 
        (<[()]>::len(&[$(hashmap!(@unit $rest)),*]));
    ($($key:expr => $value:expr),* $(,)*) => {
        {
            let _cap = hashmap!(@count $($key),*);
            let mut _map = 
                ::std::collections::HashMap::with_capacity(_cap);
           $(
               _map.insert($key, $value);
           )*
           _map
       }
   };
}
fn main(){
   trace_macros!(true);
   let map = hashmap!{
       "a" => 1,
       "b" => 2,
       "c" => 3, 
   };
   assert_eq!(map["a"], 1);
}

#![feature(trace_macros)]在nightly版本下可以跟踪宏展开,在需要展开宏的地方使用trace_macros!(true);打开跟踪。

导入/导出

#[macro_export]表示下面的宏定义对其他包也是可见的。#[macro_use]可以导入宏。

在宏定义中使用$crate,可以在被导出时,让编译器根据上下文推断包名,避免依赖问题。

过程宏

过程宏的Cargo.toml中,设置lib类型:

[lib]
proc_macro = true

自定义派生属性

derive属性,自动为结构体或枚举类型进行语法扩展。可以使用TDD(测试驱动开发)的方式来开发。

包结构:

|-Cargo.toml
|-src
    |- lib.rs
|-tests
    |- test.rs

先写test.rs

#[macro_use]
extern crate my_proc_macro;

#[derive(A)]
struct A;
#[test]
fn test_derive_a(){
    assert_eq!("hello from impl A".to_string(), A.a());
}

再写lib.rs

extern crate proc_macro;
use self::proc_macro::TokenStream;

// 自定义派生属性
#[proc_macro_derive(A)]
pub fn derive(input: TokenStream) -> TokenStream {
    let _input = input.to_string();
    assert!(_input.contains("struct A;"));
    r#"
        impl A {
            fn a(&self) -> String{
               format!("hello from impl A")
           }
       }
   "#.parse().unwrap()
}

自定义属性

可以说自定义派生属性是自定义属性的特例。例如条件编译属性#[cfg()]和测试属性#[test]都是自定义属性。

test.rs

use my_proc_macro::attr_with_args;
#[attr_with_args("Hello, Rust!")]
fn foo(){}
#[test]
fn test_foo(){
    assert_eq!(foo(), "Hello, Rust!");
}

lib.rs

extern crate proc_macro;
use self::proc_macro::TokenStream;

#[proc_macro_attribute]
pub fn attr_with_args(args: TokenStream, input: TokenStream)
                      -> TokenStream {
    let args = args.to_string();
    let _input = input.to_string();
    format!("fn foo() -> &'static str {% raw %}{{ {} }}{% endraw %}", args)
        .parse().unwrap()
}

在引号的括号要用{% raw %}{{{% endraw %}转义,最内部的{}是给args占位的。

Bang宏/类函数宏

test.rs

#![feature(proc_macro_hygiene)]
use my_proc_macro::hashmap;
#[test]
fn test_hashmap(){
    let hm = hashmap!{ "a":1,"b":2,};
    assert_eq!(hm["a"],1);
    let hm = hashmap!{"a"=>1,"b"=>2,"c"=>3};
    assert_eq!(hm["c"],3);
}

lib.rs

#[proc_macro]
pub fn hashmap(input: TokenStream) -> TokenStream {
    // 转换input为字符串
    let _input = input.to_string();
    // 将input字符串结尾的逗号去掉,否则在下面迭代中将报错
    let input = _input.trim_end_matches(',');
    // 用split将字符串分割为slice,然后用map去处理
    // 为了支持「"a" : 1」或 「"a" => 1」这样的语法
    let input: Vec<String> = input.split(",").map(|n| {
        let mut data = if n.contains(":") {  n.split(":") }
                       else { n.split(" => ") };
        let (key, value) =
           (data.next().unwrap(), data.next().unwrap());
       format!("hm.insert({}, {})", key, value)
    }).collect();
    let count: usize = input.len();
    let tokens = format!("
        {% raw %}{{
        let mut hm =
            ::std::collections::HashMap::with_capacity({});
            {}
            hm
        }}{% endraw %}", count,
        input.iter().map(|n| format!("{};", n)).collect::<String>()
    );
    // parse函数会将字符串转为Result<TokenStream>
    tokens.parse().unwrap()
}

第三方包

使用syn,quoteproc_macro可以实现自定义派生属性功能。

以下代码实现了派生属性New

extern crate proc_macro;
use {
    syn::{Token, DeriveInput, parse_macro_input},
    quote::*,
    proc_macro2,
    self::proc_macro::TokenStream,
};

#[proc_macro_derive(New)]
pub fn derive(input: TokenStream) -> TokenStream {
    let ast = parse_macro_input!(input as DeriveInput);
    let result = match ast.data {
        syn::Data::Struct(ref s) => new_for_struct(&ast, &s.fields),
        _ => panic!("doesn't work with unions yet"),
    };
    result.into()
}

fn new_for_struct(ast: &syn::DeriveInput,fields: &syn::Fields) -> proc_macro2::TokenStream
{
    match *fields {
        syn::Fields::Named(ref fields) => {
            new_impl(&ast, Some(&fields.named), true)
        },
        syn::Fields::Unit => {
            new_impl(&ast, None, false)
        },
        syn::Fields::Unnamed(ref fields) => {
            new_impl(&ast, Some(&fields.unnamed), false)
        },
    }
}

fn new_impl(ast: &syn::DeriveInput,
            fields: Option<&syn::punctuated::Punctuated<syn::Field, Token![,]>>,
            named: bool) -> proc_macro2::TokenStream
{
    let struct_name = &ast.ident;

    let unit = fields.is_none();
    let empty = Default::default();

    let fields: Vec<_> = fields.unwrap_or(&empty)
        .iter()
        .enumerate()
        .map(|(i, f)| FieldExt::new(f, i, named)).collect();

    let args = fields.iter().map(|f| f.as_arg());
    let inits = fields.iter().map(|f| f.as_init());

    let inits = if unit {
        quote!()
    } else if named {
        quote![ { #(#inits),* } ]
    } else {
        quote![ ( #(#inits),* ) ]
    };


    let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl();
    let (new, doc) = (
        syn::Ident::new("new", proc_macro2::Span::call_site()),
        format!("Constructs a new `{}`.", struct_name)
    );
    quote! {
        impl #impl_generics #struct_name #ty_generics #where_clause {
            #[doc = #doc]
            pub fn #new(#(#args),*) -> Self {
                #struct_name #inits
            }
        }
    }
}

struct FieldExt<'a> {
    ty: &'a syn::Type,
    ident: syn::Ident,
    named: bool,
}
impl<'a> FieldExt<'a> {
    pub fn new(field: &'a syn::Field, idx: usize, named: bool) -> FieldExt<'a> {
        FieldExt {
            ty: &field.ty,
            ident: if named {
                field.ident.clone().unwrap()
            } else {
                syn::Ident::new(&format!("f{}", idx), proc_macro2::Span::call_site())
            },
            named: named,
        }
    }
    pub fn as_arg(&self) -> proc_macro2::TokenStream {
        let f_name = &self.ident;
        let ty = &self.ty;
        quote!(#f_name: #ty)
    }

    pub fn as_init(&self) -> proc_macro2::TokenStream {
        let f_name = &self.ident;
        let init =  quote!(#f_name);
        if self.named {
            quote!(#f_name: #init)
        } else {
            quote!(#init)
        }
    }
}

参考

使用Rust开发编译系统(C以及Rust编译的过程)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260

推荐阅读更多精彩内容