macro 可以简化重复代码编写任务,实现 DSL,编译时代码生成等。例如:
- 方便创建 Vec 的
vec![]
; - 为数据结构添加各种 trait 支持的 derive macro:
#[derive(Debug, Default, ...)]
; - 单元测试宏:
#[cfg(test)]
macro 还可以实现一些普通函数不支持的特性,如可变数量的参数(Rust 只允许在 extern block 中声明的函数的最后一个参数类型是 …, 如 args: …,),典型的是: println!()
。
macro 分为两类:
- 声明宏(macro_rules!):编译期间对代码模版做简单替换,比如 vec![]、println!() 等;
- 过程宏(Procedural Macros):编译期间生成代码,分为 function、attribute、deriver 三种类型。
在使用宏前,必须将宏定义引入到当前作用域中(也即先定义宏或使用 use 导入宏),然后才能使用宏。
- 函数则可以在任意位置定义并在任意位置使用。
宏调用有三种等价的形式:marco!(xx), marcro![xxx], macro!{xx}。惯例是:
- 函数传参调用场景使用 () 形式,如 println!();
- 字面量初始化使用 [] 形式,如 vec![0; 4];
1 声明宏 #
声明宏使用 macro_ruels!
宏来定义,内部使用一系列模式来对输入参数/代码进行匹配,然后生成代码,语法规则如下:
- 宏名称后面可以使用 (xx), [xx], {xx} 三种格式来定义 body,三种方式是等价的;
- body 中各 rule 使用分号分割;
- rule 格式:MacroMatcher => MacroTranscriber
- MacroMatcher 有三种等价格式: (xx), [xx], {xx};
- MacroMatch 两种格式:
$(IDENTIFIER): MacroFragSpec
:例如:$expression:expr
;$(IDENTIFIER) MacroRepSep?MacroRepOp
:表示重复匹配,其中 MacroRepSep 为可选的重复分隔符, MacroRepOp 为重复类型字符,例如:$($expression:expr),+
macro_rules! 示例:
macro_rules! say_what {
($expression:expr) => {
println!("You said: {}", $expression);
};
}
say_what!("I'm learning macros"); // 传入字符串字面量,匹配 expr 类型
macro_rules! create_function {
($func_name:ident) => {
fn $func_name() {
println!("You called {:?}()", stringify!($func_name));
}
};
}
create_function!(foo); // 传入标识符,匹配 ident 类型
create_function!(bar);
macro_rules! print_result {
($expression:expr) => {
println!("{:?} = {:?}", $expression, $expression);
};
}
print_result!(1u32 + 1); // 传入表达式
// 宏展开后的结果,可见:表达式被执行了两次,所以只是简单替换。
{
$crate::io::_print(builtin #format_args(
"{:?} = {:?}",
(1u32 + 1),
(1u32 + 1),
));
};
// block 也是表达式,也可以匹配 expr
print_result!({
let x = 1u32;
x * x + 2 * x - 1 // 该表达式的结果
});
$expression:expr
是一个捕获表达式,它匹配任何 Rust 表达式,并将其作为参数传递给宏。expr 的类型如下:
item
: an Itemblock
: 块表达式;stmt
: 语句(不含结尾的分号);pat_param
: a PatternNoTopAltpat
: at least any PatternNoTopAlt, and possibly more depending on editionexpr
: 表达式;ty
: 类型;ident
: 标识符;path
: a TypePath style pathtt
: a TokenTree (a single token or tokens in matching delimiters (), [], or {})meta
: 属性;lifetime
: 生命周期;vis
: 可见性;literal
: 字面量;
macro_rules! test {
($left:expr; and $right:expr) => {
println!("{:?} and {:?} is {:?}",
stringify!($left),
stringify!($right),
$left && $right)
};
($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);
}
使用 $($xx:yy),+
语法来重复匹配:
逗号 ,
为可选的分隔符;加号 +
指定重复类型,共有三种类型:- * — 0 次或任意次的重复;
-
- — 至少 1 次的重复;
- ? — 0 次或 1 次的重复;
在 body 中使用 $($xx),+
语法来获得重复匹配的值:
- $($xx),+ 的重复类型必须和匹配定义的一致,但是分隔符可以不一样,例如:匹配是 $( $i:ident ),* 时,可以使用 => { $( $i );* },但不可以是:=> { $i }, => { $( $( $i)* )* }, 和 => { $( $i )+ }
- 如果使用多个重复匹配变量,则它们的匹配持续必须一致;
macro_rules! find_min {
($x:expr) => ($x);
($x:expr, $($y:expr),+) => (
std::cmp::min($x, find_min!($($y),+))
)
}
fn main() {
println!("{}", find_min!(1));
println!("{}", find_min!(1 + 2, 2));
println!("{}", find_min!(5, 2 * 3, 4));
}
// 重复匹配
({ $($key:tt : $value:tt),* }) => {
{
let mut fields = Box::new(HashMap::new());
$(fields.insert($key.to_string(), json!($value)); )* // 展开为多条 insert 方法调用
Json::Object(fields)
}
};
( $( $i:ident ),* ; $( $j:ident ),* ) => (( $( ($i,$j) ),* ))
// OK
(a, b, c; d, e, f)
// is legal and expands to
((a,d), (b,e), (c,f))
// but error
(a, b, c; d, e)
定义宏时一般使用引用来捕获对象,否则调用该宏时会转移对象所有权:
macro_rules! assert_eq {
($left:expr, $right:expr) => ({ // 匹配对象,这里的 $left 只是匹配,不是实际的变量值(没有赋值的过程)
match (&$left, &$right) { // 对象引用
(left_val, right_val) => {
if !(left_val == right_val) {
panic!("assertion failed" /* ... */);
} }
} });
}
使用 macro 来减少重复的例子(DRY):
use std::ops::{Add, Mul, Sub};
macro_rules! assert_equal_len {
($a:expr, $b:expr, $func:ident, $op:tt) => {
assert!($a.len() == $b.len(),
"{:?}: dimension mismatch: {:?} {:?} {:?}",
stringify!($func),
($a.len(),),
stringify!($op),
($b.len(),));
};
}
macro_rules! op {
($func:ident, $bound:ident, $op:tt, $method:ident) => {
fn $func<T: $bound<T, Output=T> + Copy>(xs: &mut Vec<T>, ys: &Vec<T>) {
assert_equal_len!(xs, ys, $func, $op);
for (x, y) in xs.iter_mut().zip(ys.iter()) {
*x = $bound::$method(*x, *y);
// *x = x.$method(*y);
}
}
};
}
// Implement `add_assign`, `mul_assign`, and `sub_assign` functions.
op!(add_assign, Add, +=, add);
op!(mul_assign, Mul, *=, mul);
op!(sub_assign, Sub, -=, sub);
mod test {
use std::iter;
macro_rules! test {
($func:ident, $x:expr, $y:expr, $z:expr) => {
#[test]
fn $func() {
for size in 0usize..10 {
let mut x: Vec<_> = iter::repeat($x).take(size).collect();
let y: Vec<_> = iter::repeat($y).take(size).collect();
let z: Vec<_> = iter::repeat($z).take(size).collect();
super::$func(&mut x, &y);
assert_eq!(x, z);
}
}
};
}
test!(add_assign, 1u32, 2u32, 3u32);
test!(mul_assign, 2u32, 3u32, 6u32);
test!(sub_assign, 3u32, 2u32, 1u32);
}
使用 macro 实现 DSL:
macro_rules! calculate {
(eval $e:expr) => {
{
let val: usize = $e;
println!("{} = {}", stringify!{$e}, val);
}
};
}
fn main() {
calculate! {
eval 1 + 2 // eval 是原样匹配
}
calculate! {
eval (1 + 2) * (3 / 4)
}
}
使用 macro 实现可变参数(递归宏定义):
macro_rules! calculate {
(eval $e:expr) => {
{
let val: usize = $e;
println!("{} = {}", stringify!{$e}, val);
}
};
// 递归宏定义
(eval $e:expr, $(eval $es:expr),+) => {{
calculate! { eval $e }
calculate! { $(eval $es),+ }
}};
}
fn main() {
calculate! {
eval 1 + 2,
eval 3 + 4,
eval (2 * 3) + 1
}
}
2 macro_use/macro_export #
macro 的作用域 scope 有两种类型:textual scope 和 path-base scope:
- 只使用标识符来引用 macro 则是 textual scope;
- 使用 path 语法来引用 macro 则是 path-base;
// path-base:导入外部的 macro,该 macro 需要通过 #{macro_expose} 暴露。
use lazy_static::lazy_static;
// textual:上下文中重定义该 macro
macro_rules! lazy_static {
(lazy) => {};
}
// 使用上下文中定义的 macro 版本
lazy_static!{lazy}
// 使用 path-base 导入的 macro 版本
self::lazy_static!{}
textual scope 特点:必须先定义再使用,从于定义的位置开始生效:
- 父 module 定义的 macro 可以在子 module 中直接使用(这是 Rust module 的通用规则);
- 可以重复定义 macro,但最后的生效;
//// src/lib.rs
mod has_macro {
// 错误:m!宏未定义
// m!{}
macro_rules! m {
() => {};
}
// OK:宏已经在 scope 中定义,所以可以使用
m!{}
// 子 module 中可以使用 m 宏
mod uses_macro;
}
// 错误:m 不在作用域内
// m!{}
//// src/has_macro/uses_macro.rs
m!{} // OK
// 重复定义时,最后一次生效
macro_rules! m {
(1) => {};
}
m!(1);
mod inner {
m!(1);
macro_rules! m {
(2) => {};
}
// m!(1); // Error: no rule matches '1'
m!(2);
macro_rules! m {
(3) => {};
}
m!(3);
}
m!(1);
// 也可以在 fn 里定义 macro
fn foo() {
// m!(); // Error: m is not in scope.
macro_rules! m {
() => {};
}
m!();
}
// m!(); // Error: m is not in scope.
导入外部宏定义
: use path
或 #[macro_use] attr
use path:
// 使用 use 导入 crate 中的特定宏
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize)]
struct MyStruct {
field1: String,
field2: i32,
}
#[macro_use] attr
规则:
- 位于 mod 前时,将 module 中定义的所有 marco 在 module 外部生效(不支持指定 macro 列表);
- 位于 extern crate 前时,从其它 crate 导入
所有
macro 定义或指定的列表
;- 其它 crate 必须先使用
#[macro_export]
来导出 macro 后才能被 macro_use 导入; - 典型使用场景是自动引入标准库中导出的所有 macro。
- 其它 crate 必须先使用
// 将 mod 中定义的所有宏导出到 root crate,后续可以在所有子 module 中使用。
#[macro_use]
mod inner {
macro_rules! m {
() => {};
}
}
m!(); // root crate 可用
// 导入 crate 中所有宏
#[macro_use]
extern crate lazy_static;
// 导入 crate 中的 lazy_static 宏
#[macro_use(lazy_static)]
extern crate lazy_static;
#[macro_export] attr
可以 将 macro 定义导出到 crate root scope
,本 crate 可以使用 path-based
scope 的语法来使用它(未 export 时是 textural scoped,Rust 2018 开始支持该特性):
- #[macro_export] 标记的 macro 是 pub 的,其它 crate 可以直接使用 use 或 macro_use 来导入它们。
self::m!(); // OK
m!(); // OK
mod inner {
super::m!();
crate::m!();
}
mod mac {
#[macro_export]
macro_rules! m { // 将 m 导出到 root crate
() => {};
}
}
macro 相互引用问题:macro A 定义内部可以使用同 crate 定义的其他 macro B,但是如果在其他 crate 只导入 macro A 而未导入 macro B 的话,会导致扩展错误。
解决办法:在 macro A 内部使用绝对 path 来引用它使用的对象或类型。
$crate
是只能在 macro 定义中使用的一个变量,它是 macro 定义所在的 module 的 root crate,可以用来引用依赖的 macro B,同时 macro B 也必须被 macro_export:
#[macro_export]
macro_rules! helped {
// () => { helper!() } // This might lead to an error due to 'helper' not being in scope.
() => { $crate::helper!() }
}
#[macro_export]
macro_rules! helper {
() => { () }
}
// 由于 helped 被 macro_export,所以,其他 crate 可以使用 use 来直接导入使用。
use helper_macro::helped;
fn unit() {
helped!();
}
// 由于 $crate 是本 crate root,所以如果要引用其他定义,则需要使用包含中间 module 的完整引用路径。
// 同时引用的 item 也必须是 pub 的。
pub mod inner {
#[macro_export]
macro_rules! call_foo {
() => { $crate::inner::foo() }; // 必须使用完整路径
}
pub fn foo() {} // 必须是 pub 的
}
在使用 #[macro_export] 时可以传入 local_inner_macros 参数 ,这样会自动对内部调用的 macro 添加
$crate::
前缀:
#[macro_export(local_inner_macros)]
macro_rules! helped {
() => { helper!() } // Automatically converted to $crate::helper!().
}
#[macro_export]
macro_rules! helper {
() => { () }
}
3 过程宏 #
procedural macro 是 在编译时运行一个 func
来对 item 进行转换(从一个 AST 到另一个 AST),包括三种类型:
Function-like macros
: custom!(…)Derive macros
: #[derive(CustomDerive)]Attribute macros
: #[CustomAttribute]
函数宏(function macro): 函数宏看起来和普通函数类似,但接收和返回 Rust 代码 AST 的 Token:Stream:
// 声明 proc_macro 是外部 crate(由于 Cargo.toml 中定义了外部 crate dependences,所以一般情况下不
// 需要声明 extern crate,但是 proc_macro 是编译器带出的 crate 且没有在 dependences 中指定,所以需
// 要单独声明)
extern crate proc_macro;
use proc_macro::TokenStream;
#[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!(); // 利用函数宏生成一个 answer() 函数定义
fn main() {
println!("{}", answer());
}
derive 宏: 一般用来为 struct/enum/union 实现特定的 trait
:
- derive 只能用在自定义 struct/enum/union 类型上。
// 定义
extern crate proc_macro;
use proc_macro::TokenStream;
#[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; // 导入定义的 derive 宏
#[derive(AnswerFn)]
struct Struct;
fn main() {
assert_eq!(42, answer());
}
derive 宏可以包含一些 helper attr
:即在启用 derive macro 的情况下才生效的子 attribute。这些
helper attr 不需要通过 use 引入到作用域:
// 定义
#[proc_macro_derive(HelperAttr, attributes(helper))]
pub fn derive_helper_attr(_item: TokenStream) -> TokenStream {
TokenStream::new()
}
// 使用
#[derive(HelperAttr)]
struct Struct {
#[helper] // helper attr
field: ()
}
属性宏(attribute macros)): 属性宏类似于 derive 宏,但不仅限于实现 trait。
- derive 宏只能用于结构体或枚举类型,属性宏可以用在
任意类型
上,比如函数。 - attr macros 需要先显式导入后才能使用。
// 定义
// my-macro/src/lib.rs
#[proc_macro_attribute]
// attr 和 item 是属性宏的自定义参数
pub fn show_streams(attr: TokenStream, item: TokenStream) -> TokenStream {
println!("attr: \"{}\"", attr.to_string());
println!("item: \"{}\"", item.to_string());
item
}
// 使用
// src/lib.rs
extern crate my_macro;
use my_macro::show_streams; // 先导入 attr macro
#[show_streams]
fn invoke1() {}
// out: attr: ""
// out: item: "fn invoke1() {}"
// 给属性宏传参数,参数可以是各种语法格式
#[show_streams(bar)]
fn invoke2() {}
// out: attr: "bar"
// out: item: "fn invoke2() {}"
#[show_streams(multiple => tokens)]
fn invoke3() {}
// out: attr: "multiple => tokens"
// out: item: "fn invoke3() {}"
#[show_streams { delimiters }]
fn invoke4() {}
// out: attr: "delimiters"
// out: item: "fn invoke4() {}"
过程宏只能在单独的 proc-macro 类型的 crate package 中定义,包名以 derive 结尾:
- 编译器使用 Cargo.toml 中定义的
lib.proc-macro=true
来标识该 crate 是 proc-macro 类型; - 编译器提供的 crate type 类型: https://doc.rust-lang.org/reference/linkage.html
- RFC: https://rust-lang.github.io/rfcs/1566-proc-macros.html
在单独的 crate package 中定义过程宏的原因:
- proc macro 定义需要先被编译器编译为 host 架构类型,后续编译使用它的代码时,编译器才能 dlopen 和执行它们来为 target 架构生成代码;
- 非过程宏 crate 需要被边翼卫 target 架构类型,然后才能被和其它 target 架构的动态库链接;
由于 host 和 target 可能不一致,所以 proc macro 需要使用特殊的 proc-macro crate 来标识和单独编译。
修改 hello_macro/Cargo.toml 文件,在 src/main.rs 引用 hello_macro_derive 包的内容:
[dependencies]
hello_macro_derive = { path = "../hello_macro/hello_macro_derive" }
# 也可以使用下面的相对路径
# hello_macro_derive = { path = "./hello_macro_derive" }
定义过程宏: 在 hello_macro_derive/Cargo.toml 文件中添加以下内容:
[lib]
# 链接 rustc 工具链提供的 proc-macro 库 libproc_macro, 同时也表明该 crate 是 proc macro 类型。
proc-macro = true
[dependencies] # 定义过程宏依赖的包
syn = "1.0" # 解析 TokenStream 来生成语法树 AST
quote = "1.0" # 提供 quote!{} 宏来生成代码(如实现 trait)
在 hello_macro_derive/src/lib.rs 中添加如下代码:
use proc_macro::TokenStream;
use syn;
use syn::DeriveInput;
use quote::quote;
#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
// 基于 Input 构建 AST 语法树
let ast: DeriveInput = syn::parse(input).unwrap();
impl_hello_macro(&ast)
}
fn impl_hello_macro(ast: &syn:: DeriveInput) -> TokenStream {
// 获取结构体、枚举标识
let name = &ast.ident;
let gen = quote! {
// 为目标结构体或枚举自动实现 trait
impl HelloMacro for #name {
fn hello_macro() {
println!("Hello, Macro! My name is {}!", stringify!(#name));
}
}
};
gen.into()
}
3.1 proc macro 原理 #
Rust 编译器和 rust-analyzer 提供了 proc-macro server 服务,而编译自定义的 proc macro crate 生成的 dyn lib 则是 client 的角色,它调用 server 提供的服务来对 AST 进行转换:
- rust-analyzer 提供的 proc-macro server 是 rust-analyzer-proc-macro-srv,位于
~/.rustup/{toolchain}/libexec/rust-analyzer-proc-macro-srv
,用于 rust-analyzer 来对 proc macro 进行 expand 分析(也即获得 proc macro 展开后的代码). - Rust 编译器 rustc 提供 binary 内置的 proc-macro server,而这个 server 使用的是随 Rust 编译器一起安装的 proc_macro crate 提供的 libproc_macro 动态库;
参考:https://fasterthanli.me/articles/proc-macro-support-in-rust-analyzer-for-nightly-rustc-versions
# libproc_macro 库,供 Rust 编译器使用
zj@a:~/docs$ ls -l ~/.rustup/toolchains/nightly-x86_64-apple-darwin/lib/rustlib/x86_64-apple-darwin/lib/libproc_macro-ce17747687ef7ea0.rlib
-rw-r--r-- 1 zhangjun 4.5M 3 4 19:09 /Users/zhangjun/.rustup/toolchains/nightly-x86_64-apple-darwin/lib/rustlib/x86_64-apple-darwin/lib/libproc_macro-ce17747687ef7ea0.rlib
# rust-analyzer-proc-macro-srv
zj@a:~/docs$ ls -l ~/.rustup/toolchains/nightly-x86_64-apple-darwin/libexec/
total 1.4M
-rwxr-xr-x 1 zhangjun 1.4M 3 4 19:09 rust-analyzer-proc-macro-srv*
rust-analyzer 进程和 proc-macro server 之间使用 JSON 接口通信:
# libpm-e886d9f9eaf24619.so 是编译后的 proc-macro crate lib
$ echo '{"ListMacros":{"dylib_path":"target/debug/deps/libpm-e886d9f9eaf24619.so"}}' | ~/.cargo/bin/rust-analyzer proc-macro
{"ListMacros":{"Ok":[["do_thrice","FuncLike"]]}}
由于 rustc 内置的 proc_macro create 提供的 API 只能在 procedure macro 类型 crate 中使用, 而不能在 build.rs 和 main.rs 等场景中使用它们, 同时也不能用来进行测试。
社区引入了 proc-macro2 crate,它是 rustc 内置 proc_macro create 的封装(wrapper), 主要提供如下两个特性:
- Bring proc-macro-like functionality to other contexts like build.rs and main.rs.
- Make procedural macros unit testable.
proc-macro2 在 serde/tokio-marcros 等项目中广泛使用。
4 属性宏 attribute macro #
attribute macro 有两种形式:
#[outer_attribute]
:对紧接者的 item 有效;#![inner_attribute]
: 对 enclosing item 有效, 一般是 module 或 crate。
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
#![allow(unused_variables)] // module 有效
fn main() {
let x = 3;
}
attribute 可以带参数:
#[attribute = "value"]
#[attribute(key = "value")]
#[attribute(value)]
#[attribute(value, value2)]
#[attribute(value, value2, value3, value4, value5)]
nightly-only experimental API:需要使用 #![feature(API_NAME)]
来启用 nightly 实验性 APIs,同时需要安装 nightly toolchain:
#![feature(iter_next_chunk)]
let mut iter = "lorem".chars();
assert_eq!(iter.next_chunk().unwrap(), ['l', 'o']); // N is inferred as 2
assert_eq!(iter.next_chunk().unwrap(), ['r', 'e', 'm']); // N is inferred as 3
assert_eq!(iter.next_chunk::<4>().unwrap_err().as_slice(), &[]); // N is explicitly 4
let quote = "not all those who wander are lost";
let [first, second, third] = quote.split_whitespace().next_chunk().unwrap();
assert_eq!(first, "not");
assert_eq!(second, "all");
assert_eq!(third, "those");
#[allow(dead_code)]: 允许未使用的代码(如变量声明, 函数定义).
mod private_nested {
#[allow(dead_code)]
pub fn function() {
println!("called `my_mod::private_nested::function()`");
}
}
#![allow(unused_variables)]: 允许未使用的变量
#![allow(unused_variables)]
fn main() {
let x = 3;
}
#[cfg(…)] 条件编译:
// unix, windows, target_arch = "x86_64", target_os = "linux", feature = "robots" (自定义 feature);
#[cfg(target_os = "linux")]
fn are_you_on_linux() {
println!("You are running linux!");
}
#[cfg(not(target_os = "linux"))]
fn are_you_on_linux() {
println!("You are *not* running linux!");
}
fn main() {
are_you_on_linux();
println!("Are you sure?");
if cfg!(target_os = "linux") {
println!("Yes. It's definitely linux!");
} else {
println!("Yes. It's definitely *not* linux!");
}
}
// 传入自定义 feature,名为 some_condition
// rustc --cfg some_condition custom.rs && ./custom
#[cfg(some_condition)] // 根据 feature 来判断
fn conditional_function() {
println!("condition met!");
}
fn main() {
conditional_function();
}
#[test] 单元测试:
// 在 mod 前添加 cfg test attr, 表明这一个 module 中的代码只在 test 时编译使用, 可以避免编译器未使
// 用代码的警告.
#[cfg(test)]
mod tests
{
fn roughly_equal(a: f64, b: f64) -> bool { (a - b).abs() < 1e-6
}
#[test]
fn trig_works() {
use std::f64::consts::PI;
assert!(roughly_equal(PI.sin(), 0.0));
}
}
#[test]
#[allow(unconditional_panic, unused_must_use)]
#[should_panic(expected="divide by zero")]
fn test_divide_by_zero_error() {
1 / 0; // should panic!
}
5 宏染色 hygienic macro #
在定义宏时,可能会出现宏内部代码和宏上下文相互影响的情况:
// 宏 body 中定义了一个 fields 临时变量
({ $($key:tt : $value:tt),* }) => { {
let mut fields = Box::new(HashMap::new());
$( fields.insert($key.to_string(), json!($value)); )*
Json::Object(fields)
} };
// 在使用宏时,宏的参数中也使用了上下文中同名的变量
let fields = "Fields, W.C.";
let role = json!({
"name": "Larson E. Whipsnade",
"actor": fields
}
);
// 直接做宏展开时就有问题
let fields = "Fields, W.C.";
let role = {
let mut fields = Box::new(HashMap::new());
fields.insert("name".to_string(), Json::from("Larson E. Whipsnade"));
fields.insert("actor".to_string(), Json::from(fields));
Json::Object(fields)
};
解决办法:使用类似与颜色标记的方式将宏定义的代码和宏参数的变量区分开来,并对宏定义内部的变量自动重命名从而防止和宏参数内部的变量冲突。如果在宏定义代码中确实要使用外部的变量,则需要通过宏参数传递的形式传到宏内部。《— 被称为:hygienic macro
- hygienic 仅限于宏定义中的 local variable 和 arguments,对于宏定义中使用其他对象类型,如 Box/HashMap等,宏不会重命名。
hygienic 带来的问题:宏定义 body 不能直接使用上下文中的变量:
macro_rules! setup_req {
() => {
let req = ServerRequest::new(server_socket.session()); }
}
fn handle_http_request(server_socket: &ServerSocket) {
setup_req!(); // declares `req`, uses `server_socket`
// ...
// code that uses `req`
}
解决办法:将依赖的上下文变量通过参数的形式传递到宏定义中:
macro_rules! setup_req {
($req:ident, $server_socket:ident) => {
let $req = ServerRequest::new($server_socket.session());
}
}
fn handle_http_request(server_socket: &ServerSocket) {
setup_req!(req, server_socket);
// ...
// code that uses `req`
}
6 调试宏 #
使用 cargo build --verbose
来查看编译过程,比如 rustc 命令,然后将 rustc 命令复制出来,添加选项 -Z unstable-options -- pretty expanded
手动执行,这时会将 expanded code 打印出来,但是如果代码有语法错误,则不行;
使用 cargo-expand 工具命令。
开启 #![feature(log_syntax)] ,然后使用 log_syntax!() 来打印传入的值;
开启 #![feature(trace_macros)],然后在需要打印 macro 的地方插入 trace_macros!(true); ,结束后插入 trace_macros!(false);
对于 Emacs,安装了 eglot-x package 后,可以使用命令 M-x eglot-x-expand-macro 来展开宏;
7 常用宏举例 #
-
file!(), line!(), column!()
expands to a string literal: the current filename. line!() and column!() expand to u32 literals giving the current line and column (counting from 1). If one macro calls another, which calls another, all in different files, and the last macro calls file!(), line! (), or column!(), it will expand to indicate the location of the first macro call.
-
stringify!(…tokens…)
Expands to a string literal containing the given tokens. The assert! macro uses this to generate an error message that includes the code of the assertion. Macro calls in the argument are
not expanded
: stringify!(line!()) expands to the string “line!()”. Rust constructs the string from the tokens, so there are no line breaks or comments in the string. -
concat!(str0, str1, …)
Expands to a single string literal made by concatenating its arguments. Rust also defines these macros for querying the build environment:
-
cfg!(…)
Expands to a Boolean constant, true if the current build configuration matches the condition in parentheses.
-
env!(“VAR_NAME”)
Expands to a string: the value of the specified environment variable at compile time. If the variable doesn’t exist,
it’s a compilation error
. This would be fairly worthless except that Cargo sets several interesting environment variables when it compiles a crate. For example, to get your crate’s current version string, you can write: let version = env!(“CARGO_PKG_VERSION”); A full list of these environment variables is included in the Cargo documentation. -
option_env!(“VAR_NAME”)
This is the same as env! except that it returns an
Option<&'static str>
that is None if the specified variable is not set. -
include!(“file.rs”)
Expands to the contents of the specified file, which must be valid Rust code—either an expression or a sequence of items.
-
include_str!(“file.txt”)
Expands to a
&'static str
containing the text of the specified file. You can use it like this: const COMPOSITOR_SHADER: &str = include_str!("../resources/compositor.glsl"); If the file doesn’t exist or is not valid UTF-8, you’ll get a compilation error. -
include_bytes!(“file.dat”)
This is the same except the file is treated as binary data, not UTF-8 text. The result is a
&'static [u8]
. -
todo!(), unimplemented!()
These are equivalent to panic!(), but convey a different intent. unimplemented!() goes in if clauses, match arms, and other cases that are not yet handled. It
always panics
. todo!() is much the same, but conveys the idea that this code simply has yet to be written; some IDEs flag it for notice. -
matches!(value, pattern)
Compares a value to a pattern, and returns true if it matches, or false otherwise. It’s equivalent to writing: match value { pattern => true, _ => false } If you’re looking for an exercise in basic macro-writing, this is a good macro to replicate—especially since the real implementation, which you can see in the standard library documentation, is quite simple.