[易学易懂系列|rustlang语言|零基础|快速入门|(22)|宏Macro]

实用知识

宏Macro

我们今天来讲讲Rust中强大的宏Macro。

Rust的宏macro是实现元编程的强大工具。

宏主要作用为:

1.减少重复代码。

2.编写DSL(Domain-specific languages。

3.可变参数接口定义。

在Rust主要分两种宏:

  1. 声明式宏declarative macros (一般用macro_rules!定义)

  2. 过程式宏 procedural macros,像一个过程函数,更易懂。它主要功能是把属性代码作为输入,作用于现有代码,并生成新的代码,从而实现特定功能。过程式宏又分为三种:

    2.1自定义派生宏custom derive,主要用于给结构体或枚举增加派生属性#[derive]。

    2.2自定义属性(属性风格宏Attribute-like),主要用于给任意元素增加属性。

    2.3 函数宏(函数风格宏Function-like),主要用于AST抽象语法树的操作。

这里简单说明 一下,如果要理解宏,就要简单了解下编译原理,一般来说现代化的编译器的编译过程如下 :

编译过程

分词(词条流)→解析(抽象语法树)→简化(高级中间语言)→简化(中级中间语言)→转译(LLVM中间语言)→优化(机器码)

简单来说,编译器就像一个翻译者,它把程序代码,一步步翻译成机器码,让机器能理解并执行人类的代码。

先来看看声明式宏,macro_rules! 是使用递归和模式匹配、字符串替换的函数式风格定义宏。

我们来看看简单的宏定义:

// 宏名字为:`say_hello`.
macro_rules! say_hello {
// `()` 这里括号里没有任何表达式,表示这个宏不用接受任何参数.
() => {
// 宏将把上面的括号()内的表达式展开到这个大括号{}中的代码。
println!("Hello!");
};
} fn main() {
// 调用这个宏,将直接把这个宏展开为代码: `println!("Hello");`
say_hello!()
}

我们再来看看稍微有点复杂的宏定义:

macro_rules! create_function {
// 这个宏定义有两个参数:ident为标识符,主要用来标识变量或函数,表明宏展开为变量或函数;
//而$func_name则为宏代码中代码展开的参数名,这里为函数名.
($func_name:ident) => {
fn $func_name() {
//这里的宏 `stringify!`直接把函数名字转换成字符串。
println!("You called {:?}()", stringify!($func_name));
}
};
} macro_rules! print_result {
//这个宏把一个表达式,并把表达式的字符串和表达式结果一起打印出来。
//而标识符expr,主要用来标识表达式。
($expression:expr) => {
// `stringify!` 宏将把表达式转换成字符串形式
println!("{:?} = {:?}", stringify!($expression), $expression);
};
} fn main() {
// 用上面的宏create_function!分别创建foo和bar函数
create_function!(foo);
create_function!(bar); foo();//调用foo函数,即调用代码:println!("You called {:?}()", stringify!(foo));
bar();/调用foo函数,即调用代码:println!("You called {:?}()", stringify!(bar)); //用上面宏print_result!,将把代码展开为:
//println!("{:?} = {:?}", stringify!(1u32 + 1), 1u32 + 1);
print_result!(1u32 + 1); // 同样,将大括号的代码当成表达式参数传给上面的宏print_result!
print_result!({
let x = 1u32; x * x + 2 * x - 1
});
}

运行上面的代码,打印结果为:

You called "foo"()
You called "bar"()
"1u32 + 1" = 2
"{ let x = 1u32; x * x + 2 * x - 1 }" = 2

我们从上面的代码来一一分析,一般来说宏定义用:macro_rules! 开头,表明这是个宏。

基本形式为:() => { };

其中小括号为宏定义的参数:主要用来定义宏的参数,其中参数有个标识符,主要用来表明宏定义的表达式展开的参数类型。

有以下标识符:

  • item,语言项,比如模块、声明、函数定义、类型定义、结构体定义、impl实现等。

    block,代码块,由花括号限定的代码;

    stmt,语句,一般是指以分号结尾的代码;

    expr,表达式,会生成具体的值;

    pat,模式;

    ty,类型;

    ident,标识符;

    path,路径,比如foo、std::iter等;

    meta,元信息,包含在#[...]或者#![...]属性内的信息;

    tt,TokenTree的缩写,词条树;

    vis,可见性,比如pub;

    lifetime,生命周期参数。

代码重载

宏可以像模式匹配一样,可以根据参数不同,匹配不同的宏定义代码,如下例子:

/ `test!` 宏主要用来比较 `$left` 和 `$right`
// 它会根据你调用时的参数自动匹配不一样的代码:
macro_rules! test {
// 参数之间不一定要用逗号隔开
// 任何形式的模板代码都可以
($left:expr; and $right:expr) => {
println!("{:?} and {:?} is {:?}",
stringify!($left),
stringify!($right),
$left && $right)
};
// 每个left参数必须以分号结尾
($left:expr; or $right:expr) => {
println!("{:?} or {:?} is {:?}",
stringify!($left),
stringify!($right),
$left || $right)
};
} fn main() {
test!(1i32 + 1 == 2i32; and 2i32 * 2 == 4i32);
test!(true; or false);
}

运行上面代码,结果为:

"1i32 + 1 == 2i32" and "2i32 * 2 == 4i32" is true
"true" or "false" is true

重复调用

宏定义可以用加号+来表示,可以传入一个或多个参数;同样,也可以星号*来表示可以传入零个或多个参数。

我们看看简单例子:

// `min!` 宏主要用来求多个表达式结果中的最小值.
macro_rules! find_min {
//一个参数的情况:
($x:expr) => ($x);
// 参数`$x` 和至少一个参数 `$y,`的情况
($x:expr, $($y:expr),+) => (
// 调用标准库中的最小值求值函数 ,并且让`find_min!`作用于参数 `$y`和更多其它参数
std::cmp::min($x, find_min!($($y),+))
)
} fn main() {
println!("{}", find_min!(1u32));
println!("{}", find_min!(1u32 + 2, 2u32));
println!("{}", find_min!(5u32, 2u32 * 3, 4u32));
}

运行结果:

1
2
4

当然,宏最大的用法,就是DSL(Domain Specific Languages),即领域特定语言。我们来看看简单例子:

macro_rules! calculate {
(eval $e:expr) => {{
{
let val: usize = $e; // 强制类型转换成integer
println!("{} = {}", stringify!{$e}, val);//打印结果
}
}};
} fn main() {
calculate! {
eval 1 + 2 // 还好 `eval`不是Rust的关键词!
} calculate! {
eval (1 + 2) * (4 / 4)
}
}

结果为:

1 + 2 = 3
(1 + 2) * (4 / 4) = 3

可变参数接口

很多时候,我们的接口可能要适应一个或多个参数的情况,也就是可变参数接口。那这样的宏又如何实现呢?我们来扩展下上面的宏,代码如下:

macro_rules! calculate {
// The pattern for a single `eval`
(eval $e:expr) => {{
{
let val: usize = $e; // Force types to be integers
println!("{} = {}", stringify!{$e}, val);
}
}}; // Decompose multiple `eval`s recursively
(eval $e:expr, $(eval $es:expr),+) => {{
calculate! { eval $e }
calculate! { $(eval $es),+ }
}};
} fn main() {
calculate! { // Look ma! Variadic `calculate!`!
eval 1 + 2,
eval 3 + 4,
eval (2 * 3) + 1
}
}

运行结果为:

1 + 2 = 3
3 + 4 = 7
(2 * 3) + 1 = 7

导入导出

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

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

我们再来看看过程宏,过程宏有三个:1.自定义派生宏custom derive 2.自定义属性 3.函数宏

1.自定义派生宏custom derive

所谓自定义derive属性,即可自动为结构体或枚举类型进行语法扩展。

我们来看看例子。

先用命令生成目录:test_derive_macro

cargo new test_derive_macro

新建一个lib.rs文件和目录tests,并创建一个test.rs文件,目录结构如下:

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

我们先更新Cargo.toml ,完整代码如下:

[package]
name = "test_derive_macro"
version = "0.1.0"
authors = ["gyc567 <gyc567@126.com>"]
edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
#设置lib的类型,这个一定要加上
[lib]
proc_macro = true
[dependencies]

test.rs的代码:

#[macro_use]
extern crate test_derive_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()
}

我们在当前工程目录下运行:cargo test

显示测试通过 。

我们再来看看独立crate工程的例子(这个官方案例代码更全面):

我们先用命令创建一个工程:hello_macro:

$ cargo new hello_macro --lib

我在当前工程下的src/lib.rs,定义一个公共特征,写入如下代码:

pub trait HelloMacro {
fn hello_macro();
}

然后我们在当前工程目录创建另一个工程:hello_macro_derive,用如下命令:

$ cargo new hello_macro_derive --lib

修改在这个工程目录下文件:hello_macro_derive/Cargo.toml,主要增加 两个库syn和quote:

[lib]
proc-macro = true [dependencies]
syn = "0.14.4"
quote = "0.6.3"

然后在文件:hello_macro_derive/src/lib.rs,写入如下代码:

extern crate proc_macro;

use crate::proc_macro::TokenStream;
use quote::quote;
use syn; #[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
// Construct a representation of Rust code as a syntax tree
// that we can manipulate
//从Rust代码构造出我们可以操作的语法树ast
//文档:https://docs.rs/syn/0.14.4/syn/struct.DeriveInput.html
let ast: syn::DeriveInput = syn::parse(input).unwrap(); // 实现特征方法 Build the trait implementation
impl_hello_macro(&ast)
} // 实现特征方法
fn impl_hello_macro(ast: &syn::DeriveInput) -> TokenStream {
let name = &ast.ident;
let gen = quote! {//主要用来替换相关字符串,生成特定代码,
//文档:https://docs.rs/quote/1.0.2/quote/
impl HelloMacro for #name {
fn hello_macro() {
println!("Hello, Macro! My name is {}", stringify!(#name));
}
}
};
gen.into()
}

然后在目录hello_macro,运行命令:

cargo build

现在退出当前工程,当前工程的上一级目录下创建一个新工程,用命令:

cargo new pancakes

更新工程文件:pancakes/Cargo.toml文件下的依赖:

[dependencies]
hello_macro = { path = "../hello_macro" }
hello_macro_derive = { path = "../hello_macro/hello_macro_derive" }

更新main文件pancakes/src/main.rs,完整代码如下 :

use hello_macro::HelloMacro;
use hello_macro_derive::HelloMacro;
// struct Pancakes; #[derive(HelloMacro)]
struct Pancakes; fn main() {
Pancakes::hello_macro();
}

运行以上代码,正常情况下,会打印正确结果:

Hello, Macro! My name is Pancakes

我们再来看看后两种过程宏:

2.自定义属性(属性风格宏Attribute-like)

我们来看看代码例子,首先生成目录:

cargo new test_macro_attribute

然后到工程目录test_macro_attribute下直接生成lib crate目录,用命令:

cargo new my_macro --lib

生成的目录结构为:

test_macro_attribute
├── Cargo.toml
├── my_macro
│ ├── Cargo.toml
│ ├── src
│ │ └── lib.rs
└── src
└── main.rs

然后我们在当着工程目录test_macro_attribute下的Cargo.toml文件,新增一行,如下:

[package]
name = "test_macro_attribute"
version = "0.1.0"
authors = ["gyc567 <gyc567@126.com>"]
edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies]
#新增一行,自定义宏lib依赖,这里就是:my_macro
my_macro = { path = "my_macro" }

src/main.rs代码如下:

#[macro_use]
extern crate my_macro; #[log_entry_and_exit(hello, "world")]
fn this_will_be_destroyed() -> i32 {
42
} fn main() {
dummy()
}

my_macro/Cargo.toml文件,新增一行宏的属性定义,完整代码:

[package]
name = "my_macro"
version = "0.1.0"
authors = ["gyc567 <gyc567@126.com>"]
edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
#新增一行,定义为宏的lib
proc-macro = true
[dependencies]

my_macro/src/lib.rs代码如下 :

extern crate proc_macro;

use proc_macro::*;

#[proc_macro_attribute]
pub fn log_entry_and_exit(args: TokenStream, input: TokenStream) -> TokenStream {
let x = format!(
r#"
fn dummy() {{
println!("entering");
println!("args tokens: {{}}", {args});
println!("input tokens: {{}}", {input});
println!("exiting");
}}
"#,
args = args.into_iter().count(),
input = input.into_iter().count(),
); x.parse().expect("Generated invalid tokens")
}

保存好代码,直接用cargo run运行,结果为:

entering
args tokens: 3
input tokens: 7
exiting

我们再来看看函数宏,在my_macro/src/lib.rs新增如下代码:

#[proc_macro]
pub fn hw(input: TokenStream) -> TokenStream {
// r#"println!("Hello, World!----in pm");"#.parse().unwrap()
println!("in pm ----{:?}", input); TokenStream::new()
}

回到工程目录下的main.rs,新增一行调用代码:

#[macro_use]
extern crate my_macro; #[log_entry_and_exit(hello, "world")]
fn this_will_be_destroyed() -> i32 {
42
}
my_macro::hw!();//新增一行调用代码,注意,一定要放在这里才不报错
fn main() {
dummy()
}

cargo run的最终运行结果为:

PS E:\code\rustProject\test_macro_attribute> cargo run

Compiling test_macro_attribute v0.1.0 (E:\code\rustProject\test_macro_attribute)

in pm ----TokenStream []

Finished dev [unoptimized + debuginfo] target(s) in 0.75s

Running target\debug\test_macro_attribute.exe

entering

args tokens: 3

input tokens: 7

exiting

我们发现,新增的代码:my_macro::hw!();

在编译的时候打印相关信息,说明它是在操作在编译期。

新增代码一定要写在main函数上面,否则报错:error[E0658]: procedural macros cannot be expanded to statements。见:https://stackoverflow.com/questions/54174361/cannot-call-a-function-like-procedural-macro-cannot-be-expanded-to-statements

这可能主要是因为现在的宏还在进化和完善中,还没有最终稳定下来。

所以,在Rust,宏是很强大的工具,但要用好,要好好深入掌握相关知识,才能写更好的代码。

本篇宏的专题,先到这里吧,以后有更好的案例,我会慢慢加入进来。

以上,希望对你有用。

如果遇到什么问题,欢迎加入:rust新手群,在这里我可以提供一些简单的帮助,加微信:360369487,注明:博客园+rust

参考文章:

https://doc.rust-lang.org/stable/rust-by-example/macros.html

https://danielkeep.github.io/tlborm/book/index.html

https://xr1s.me/2018/12/08/introduction-to-rust-proc-macro/

https://tinkering.xyz/introduction-to-proc-macros/

https://github.com/dtolnay/proc-macro-workshop#attribute-macro-sorted

https://blog.x5ff.xyz/blog/easy-programming-with-rust-macros/

https://stackoverflow.com/questions/52585719/how-do-i-create-a-proc-macro-attribute?noredirect=1

https://stackoverflow.com/questions/54174361/cannot-call-a-function-like-procedural-macro-cannot-be-expanded-to-statements

[易学易懂系列|rustlang语言|零基础|快速入门|(22)|宏Macro]的更多相关文章

  1. [易学易懂系列|rustlang语言|零基础|快速入门|(28)|实战5:实现BTC价格转换工具]

    [易学易懂系列|rustlang语言|零基础|快速入门|(28)|实战5:实现BTC价格转换工具] 项目实战 实战5:实现BTC价格转换工具 今天我们来开发一个简单的BTC实时价格转换工具. 我们首先 ...

  2. [易学易懂系列|rustlang语言|零基础|快速入门|(27)|实战4:从零实现BTC区块链]

    [易学易懂系列|rustlang语言|零基础|快速入门|(27)|实战4:从零实现BTC区块链] 项目实战 实战4:从零实现BTC区块链 我们今天来开发我们的BTC区块链系统. 简单来说,从数据结构的 ...

  3. [易学易懂系列|rustlang语言|零基础|快速入门|(26)|实战3:Http服务器(多线程版本)]

    [易学易懂系列|rustlang语言|零基础|快速入门|(26)|实战3:Http服务器(多线程版本)] 项目实战 实战3:Http服务器 我们今天来进一步开发我们的Http服务器,用多线程实现. 我 ...

  4. [易学易懂系列|rustlang语言|零基础|快速入门|(25)|实战2:命令行工具minigrep(2)]

    [易学易懂系列|rustlang语言|零基础|快速入门|(25)|实战2:命令行工具minigrep(2)] 项目实战 实战2:命令行工具minigrep 我们继续开发我们的minigrep. 我们现 ...

  5. [易学易懂系列|rustlang语言|零基础|快速入门|(24)|实战2:命令行工具minigrep(1)]

    [易学易懂系列|rustlang语言|零基础|快速入门|(24)|实战2:命令行工具minigrep(1)] 项目实战 实战2:命令行工具minigrep 有了昨天的基础,我们今天来开始另一个稍微有点 ...

  6. [易学易懂系列|rustlang语言|零基础|快速入门|(23)|实战1:猜数字游戏]

    [易学易懂系列|rustlang语言|零基础|快速入门|(23)|实战1:猜数字游戏] 项目实战 实战1:猜数字游戏 我们今天来来开始简单的项目实战. 第一个简单项目是猜数字游戏. 简单来说,系统给了 ...

  7. [易学易懂系列|rustlang语言|零基础|快速入门|(5)|生命周期Lifetime]

    [易学易懂系列|rustlang语言|零基础|快速入门|(5)] Lifetimes 我们继续谈谈生命周期(lifttime),我们还是拿代码来说话: fn main() { let mut a = ...

  8. [易学易懂系列|rustlang语言|零基础|快速入门|(21)|智能指针]

    [易学易懂系列|rustlang语言|零基础|快速入门|(21)|智能指针] 实用知识 智能指针 我们今天来讲讲Rust中的智能指针. 什么是指针? 在Rust,指针(普通指针),就是保存内存地址的值 ...

  9. [易学易懂系列|rustlang语言|零基础|快速入门|(20)|错误处理]

    [易学易懂系列|rustlang语言|零基础|快速入门|(20)|错误处理] 实用知识 错误处理 我们今天来讲讲Rust中的错误处理. 很多语言都有自己的错误处理方式,比如,java是异常处理机制. ...

随机推荐

  1. [Nowcoder212D]禁书目录_概率期望

    禁书目录 题目大意:清教需要定期给Index清除记忆,在此之前需要把当中的十万三千本禁书取出来......不幸的是,禁书一旦离开了Index就非常脆弱,具体来说,每一本禁书都有一个魔力值 ai ,其记 ...

  2. [转帖]查看Linux用的桌面是GNOME、KDE或者其他

    http://superuser.com/questions/96151/how-do-i-check-whether-i-am-using-kde-or-gnome KDE 基于QT做的 已经越来越 ...

  3. 如何利用swoole搭建一個簡易聊天室

    <?php class Chat { const HOST = '0.0.0.0';//ip地址 0.0.0.0代表接受所有ip的访问 const PART = 82;//端口号 private ...

  4. 从入门到自闭之Python--MySQL数据库的多表查询

    多表查询 连表: 内连接:所有不在条件匹配内的数据们都会被剔除连表 select * from 表名1,表名2 where 条件; select * from 表名1 inner join 表名2 o ...

  5. python — 进程

    目录 1. 进程 1.进程就是一个运行中的程序(是对正在运行程序的一个抽象). 2.程序和进程之间的区别: 程序只是一个文件 进程是这个文件被CPU运行起来了 程序是永久的,进程是暂时的. 3.进程- ...

  6. [经验分享] Docker网络解决方案-Weave部署记录

    前面说到了Flannel的部署,今天这里说下Docker跨主机容器间网络通信的另一个工具Weave的使用.当容器分布在多个不同的主机上时,这些容器之间的相互通信变得复杂起来.容器在不同主机之间都使用的 ...

  7. 怎样使用构造函数: Vue()?

    1. 新建一个 .html 文件 => 引入一个在线的 vue 库 => 写一个带 id 的 html 标签 => 写一个 script 标签, 这里的 vApp 是 Vue() 这 ...

  8. C#Linq之求和,平均值,最大值,最小值

    using System;using System.Collections.Generic;using System.Linq;using System.Text;using System.Threa ...

  9. 使用.netcore部署window服务完成过程(使用nssm,Topshelf)

    一,新建.netcore控制台应用程序.本文使用.netcore2.2版本,结构如下 二,negut引用Topshelf.Log4Net,Topshelf 三,代码如下:1>Program.cs ...

  10. 详解CSS居中布局技巧

    本文转自:https://zhuanlan.zhihu.com/p/25068655#showWechatShareTip一.水平居中元素: 1.通用方法,元素的宽高未知方式一:CSS3 transf ...