Rust中的过程宏

2022-06-27 13:19:37 浏览数 (1)

过程宏是rust里的强大的武器,非常值得学习rust的人去掌握。但过程宏的编写有点难度,且文档也不太详细,最近也专门学习了下过程宏,算是有点收获,写下一点东西。

Rust 吉祥物是只螃蟹,Ferris,这可以理解,但是它为什么被煮了啊?都变红了。

网友们的回答很高赞,因为编译时发热太高了.....,很简单,因为螃蟹要蒸熟了吃才 Safe 

因为你只有熟了后才能感觉到rust的强大。

在Rust中,一般常见的是按如下方式定义的宏:

代码语言:javascript复制
 macro_rules! sqr {
    ($x:expr) => {$x * $x}
}

fn main() {
    println!("{}", sqr!(1   1));
}

将得到正确的答案4。这是因为Rust的宏展开发生在语法分析阶段,此时编译器知道sqr!宏中的x变量是一个表达式(用x:expr标记),所以在展开后它知道如何正确处理,会将其展开为((1 1) * (1 1))。

然而这只是书本上常见的宏的简单用法。宏的强大远不至此,在一些开源库和框架中常见一些高级用法。当然也有点儿黑魔法的味道了,但封装后确实可以简化代码,方便使用。学习曲线陡峭是有的,由于本文介绍的重点是过程宏,因此涉及普通宏的内容便不多赘述,有兴趣者可参考官方文档上的介绍。

对于宏编程,Rust中提供了几种过程宏的库操作支持,即: 1、Syn 它是基于TokenStream的一种语法分析过程,它并不很强大,需要自定义扩展一些宏,比如Rust中的函数和闭包等。 2、Quote 应用比较简单,适合使用。 3、proc_macro2 更好的proc_macro更方便的接口,能和syn、quote一起更好的配合应用。

什么是过程宏?

过程宏(Procedure Macro)是Rust中的一种特殊形式的宏,它将提供比普通宏更强大的功能。方便起见,本文将Rust中由macro_rules!定义的宏称为规则宏以示区分。

过程宏分为三种:

  • 派生宏(Derive macro):用于结构体(struct)、枚举(enum)、联合(union)类型,可为其实现函数或特征(Trait)。
  • 属性宏(Attribute macro):用在结构体、字段、函数等地方,为其指定属性等功能。如标准库中的#[inline]、#[derive(...)]等都是属性宏。
  • 函数式宏(Function-like macro):用法与普通的规则宏类似,但功能更加强大,可实现任意语法树层面的转换功能。

过程宏的定义与使用方法

派生宏

派生宏的定义方法如下:

代码语言:javascript复制
#[proc_macro_derive(Builder)]
fn derive_builder(input: TokenStream) -> TokenStream {
    let _ = input;

    unimplemented!()
}

其使用方法如下:

代码语言:javascript复制
#[derive(Builder)]
struct Command {
    // ...
}

属性宏

属性宏的定义方法如下:

代码语言:javascript复制
#[proc_macro_attribute]
fn sorted(args: TokenStream, input: TokenStream) -> TokenStream {
    let _ = args;
    let _ = input;

    unimplemented!()
}

使用方法如下:

代码语言:javascript复制
#[sorted]
enum Letter {
    A,
    B,
    C,
    // ...
}

函数式宏

函数式宏的定义方法如下:

代码语言:javascript复制
#[proc_macro]
pub fn seq(input: TokenStream) -> TokenStream {
    let _ = input;

    unimplemented!()
}

使用方法如下:

代码语言:javascript复制
seq! { n in 0..10 {
    /* ... */
}}

过程宏的原理

以上三种过程宏的定义方法已全部介绍。可以发现,它的定义方式与普通函数无异,只不过其函数调用发生在编译阶段而已。下面以较为常见的派生宏为例,介绍过程宏的原理。

回顾刚才的定义:

代码语言:javascript复制
#[proc_macro_derive(Builder)]
fn derive_builder(input: TokenStream) -> TokenStream {
    let _ = input;

    unimplemented!()
}

首先,#[proc_macro_derive(Builder)]表明derive_builder是一个派生宏,Builder表示它将作用的地方。比如定义如下结构体

代码语言:javascript复制
#[derive(Builder)]
struct Command {
    // ...
}

就会触发以上派生宏执行。至于其中的Builder具体代表什么含义,本期暂不展开,后面再详细介绍。

fn derive_builder(input: TokenStream) -> TokenStream函数头部表明该函数将接受一个TokenStream对象作为输入,并返回一个TokenStream 对象。

要理解TokenStream,需要一些简单的编译原理知识。编译器在编译一段程序时,会首先将输入的文本转换成一系列的Token(标识符、关键字、符号、字面量等),同时忽略注释(文档注释除外)与空白字符等。

例如println!("Hello world");这句代码将被转换成标识符println 、叹号! 、圆括号( 、字面量"Hello world" 、圆括号) 、分号; 几个Token。

TokenStream顾名思义,是Rust中对一系列连续的Token的抽象。在宏展开的过程中,遇到派生宏时,会将整个结构体(或enumunion)展开成TokenStream作为派生宏函数的输入,然后将其输出的TokenStream附加到结构体后面,再继续作语法分析。

构建过程宏的必要设置

构建过程宏,要在cargo.toml里面设置一些参数,这是必须的。一般来说,过程宏必须是一个库,或者作为工程的子库,不能单独作为一个源文件存在,至少目前不行。

代码语言:javascript复制
[lib]
proc-macro = true
path = "src/lib.rs"

而编写过程宏,在stable版本里,我们需要借助三个crate:

  • syn,这个是用来解析语法树(AST)的。各种语法构成
  • quote,解析语法树,生成rust代码,从而实现你想要的新功能。
  • proc_macro(std) 和 proc_macro2(3rd-party)

但在nightly版本里,以上的这些crate都不需要了,不依赖第三方crate,还有就是语法上是稍微有些不同,大部分是一样的。但这篇文章只讲stable rust里的过程宏,如果想了解nightly rust的过程宏,可以去看maud 和Rocket,前者是一个HTML模板引擎,大量使用了过程宏,模板都是编译时生成,所以性能非常高,而后者是一个web framework,rust各种黑魔法使用的集大成者。

使用举例

proc-macro(function-like,类函数宏)

这种过程宏和标准宏很类似,只是构建过程不太一样,使用方式还是一样的。标准语法是这样的。

代码语言:javascript复制
#[proc_macro]
pub fn my_proc_macro(input: TokenStream) -> TokenStream{
    // ...
}

可以看出函数式的过程宏只接受一个形参,而且必须是pub的。 简单写一个例子,参照官网文档的,只是稍微改了一点点。

代码语言:javascript复制
#[proc_macro]
pub fn my_proc_macro(ident: TokenStream) -> TokenStream {
    let new_func_name = format!("test_{}", ident.to_string());
    let concated_ident = Ident::new(&new_func_name, Span::call_site()); // 创建新的ident,函数名

    let expanded = quote! {
        // 不能直接这样写trait bound,T: Debug
        // 会报错,找不到Debug trait,最好给出full path
        fn #concated_ident<T: std::fmt::Debug>(t: T) {
            println!("{:?}", t);
        }
    };
    expanded.into()
}

使用情形如下。

代码语言:javascript复制
use your_crate_name::my_proc_macro;
// ...
my_proc_macro!(hello)!; // 函数test_hello就生成了,可见性在调用之后
// ...
test_hello("hello, proc-macro");
test_hello(10);

可以看出,写一个函数式的过程宏还是不那么复杂的。

proc_macro_derive(Derive mode macros, 继承宏)

继承宏的函数签名和前者有些类似:

代码语言:javascript复制
#[proc_macro_derive(MyDerive)]
pub fn my_proc_macro_derive(input: TokenStream) -> TokenStream{
    // ...
}

不过不同的是,引入属性有些不同。

代码语言:javascript复制
#[proc_macro_derive(MyDerive)]

proc_macro_derive表明了这是继承宏,还定义了新的继承宏的名字MyDerive。 熟悉rust编程的,都应该知道有个继承宏,一直用得到,就是Debug。这是标准库里的,可以帮助调试和显示。所以呢,这里就来实现一个类似功能的继承宏,暂时命名这个过程宏名字为Show。 这个例子稍微有点复杂。当然我觉得还是先看了官方文档的例子之后再来看我的例子会比较好些。

代码语言:javascript复制
#[proc_macro_derive(Show)]
pub fn derive_show(item: TokenStream) -> TokenStream {
    // 解析整个token tree
    let input = parse_macro_input!(item as DeriveInput);
    let struct_name = &input.ident; // 结构体名字

    // 提取结构体里的字段
    let expanded = match input.data {
        Data::Struct(DataStruct{ref fields,..}) => {
            if let Fields::Named(ref fields_name) = fields {
                // 结构体中可能是多个字段
                let get_selfs: Vec<_> = fields_name.named.iter().map(|field| {
                    let field_name = field.ident.as_ref().unwrap(); // 字段名字
                    quote! {
                        &self.#field_name
                    }
                }).collect();

            let implemented_show = quote! {
                // 下面就是Display trait的定义了
                // use std::fmt; // 不要这样import,因为std::fmt是全局的,无法做到卫生性(hygiene)
                // 编译器会报错重复import fmt当你多次使用Show之后
                impl std::fmt::Display for #struct_name {
                    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
                        // #(#get_self),*,这是多重匹配,生成的样子大概是这样:&self.a, &self.b, &self.c, ...
                        // 用法和标准宏有点像,关于多个匹配,可以看这个文档
                        // https://docs.rs/quote/1.0.0/quote/macro.quote.html
                        write!(f, "{} {:?}", stringify!(#struct_name), (#(#get_selfs),*))
                    }
                }
            };
            implemented_show
            
            } else {
                panic!("sorry, may it's a complicated struct.");
            }
        }
        _ => panic!("sorry, Show is not implemented for union or enum type.")
    };
    expanded.into()
}

使用情形:

代码语言:javascript复制
use your_crate_name::Show;
// ...
#[derive(Show)]
struct MySelf {
    name: String,
    age: u8,
}
// ...
let me = MySelf{name: "Jamie", age: 255};
println!("{}", me); // MySelf (Jamie, 255)

不过呢,继承宏还可以添加额外的属性,函数签名类似如下

代码语言:javascript复制
#[proc_macro_derive(MyDerive, attributes(my_attr)]
pub fn my_proc_macro_derive(input: TokenStream) -> TokenStream{
    // ...
}

这里增加了一个关键字attributes,并指定了属性的名字。详细情况可以看官方文档。示例代码里也有个例子,因为文章篇幅,我就不赘述了。

结语

过程宏确实是rust里的黑魔法,希望这篇文章能帮助到一些人了解并使用过程宏,体会到rust的强大。

实例代码可以在这里看到。所以的例子都是在rust版本1.32之下编写并通过编译的,最好使用最新的stable rust。当然nightly rust应该也可以编译过。

  • proc-macro example, using syn 1.0, quote 1.0, proc-macro2 1.0.
  • proc-macro example, using syn 0.15, quote 0.6, proc-macro2 0.4.

引用

如何编写一个过程宏(proc-macro) - Jamie's Blog

Rust过程宏入门(一)——过程宏简介 - 知乎

在 VSCode 下用 Markdown Preview Enhanced 愉快地写文档 - 知乎

https://github.com/dtolnay/proc-macro-workshop/

https://github.com/niuhuan/feign-rs

RUST网络客户端的基本技术说明-宏库_fpcc的博客-CSDN博客

Rust Cargo使用指南 | 第十六篇 | 发布到 crates.io - 知乎

Rust语言中文版_Rust中文教程_Rust开发中文手册[PDF]下载-UDN开源文档

rust学习笔记 · 看云

理解Rust的Result/Option/unwrap/?-阿里云开发者社区

Rust 错误处理 - Rust 基础教程 - 简单教程,简单编程

Rust语言中的Result:不得不处理的返回值 - 知乎

【译】Rust 的 Result 类型入门 - suhanyujie - 博客园

0 人点赞