宏的扩充

宏是一种捷径,在类型检查之前,甚至在编译之前扩充成Rust代码。
调用宏的时候用圆括号或方括号或花括号都是一样的。如果用花括号后边的分号可以省略。习惯上assert,print这类的用圆括号,vec用方括号,macro_rules用花括号。

因为宏的扩充在编译开始之前,甚至在扫描其他代码之前,所以宏的定义必须在宏的调用之前。

匹配

image.png
宏的扩充很像是模式匹配。匹配的是token:数字,命名,符号等;注释和空白符除外。
pr2e_2102.png
这个匹配中,$left:expr告诉Rust去匹配一个表达式,并赋给$left。想正则一样只有一些符号能匹配一些特殊的情况,除此之外想逗号只能简单逐字匹配。

在上面的例子里匹配到的表达式被是$left和$right然后被变量left_val和right_val取代,应为匹配的表达式会在扩充时替换成匹配到的表达式,left_val和right_val是表达式返回的值,如果不替换成值而用

  1. if !($left == $right) {
  2. panic!("assertion failed: `(left == right)` \
  3. (left: `{:?}`, right: `{:?}`)", $left, $right)
  4. }

那第三行的$left, $right还会被替换成原表达式,那这个表达式就会运行两次。

宏里如果不使用引用也会获取值的所有权。

重复

  1. macro_rules! vec {
  2. ($elem:expr ; $n:expr) => {
  3. ::std::vec::from_elem($elem, $n)
  4. };
  5. ( $( $x:expr ),* ) => {
  6. <[_]>::into_vec(Box::new([ $( $x ),* ]))
  7. };
  8. ( $( $x:expr ),+ ,) => {
  9. vec![ $( $x ),* ]
  10. };
  11. }

其中第二个匹配$( $x:expr ),*,就像正则一样*表示0或多个重复,但,不是匹配的对象而是表示重复是用,分割的,而且如果末尾有多余的,还匹配不到。里面的$x:expr表示每个重复的表达式。有多种重复的形式的匹配:

Pattern Meaning
$( … )* Match 0 or more times with no separator
$( … ),* Match 0 or more times, separated by commas
$( … );* Match 0 or more times, separated by semicolons
$( … )+ Match 1 or more times with no separator
$( … ),+ Match 1 or more times, separated by commas
$( … );+ Match 1 or more times, separated by semicolons
$( … )? Match 0 or 1 times with no separator
$( … ),? Match 0 or 1 times, separated by commas
$( … );? Match 0 or 1 times, separated by semicolons

除了用作匹配传入的表达式,还可以用来生成重复的表达式,<[_]>::into_vec(Box::new([ $( $x ),* ]))表示匹配到的$x以逗号分割重复。
这里intovec是数组的方法,这行代码是首先创建一个数组,用box把这个数组放到堆,然后用数组的into_vec方法把这个数组转成向量,`[]中的_`是类型推断。

内建的宏

这些宏的功能是无法自己实现的,他们是写在rustc里的。

file!(), line!(), column!()

返回文件路径,当前行,列。如果被多个宏层层调用,返回的是最顶层的那个宏的位置。column返回的是c所在的列。

stringify!(…tokens…)

返回token本身的字符串,assert用这个来返回错误的代码。而且它里面调用别的宏不会被扩展。

concat!(str0, str1, …)

拼接字符串。

cfg!(…)

返回编译配置是否是指定的值。

env!(“VAR_NAME”) , option_env!(“VAR_NAME”)

返回编译时该变量的值。

include!(“file.rs”)

扩展成指定文件的代码。

include_str!(“file.txt”)

将文件的内容扩展成一个’static str。

include_bytes!(“file.dat”)

扩展成’static [u8]。

todo!(), unimplemented!()

和panic功能一样,只是意图不一样。

matches!(value, pattern)

判断value是不是匹配pattern。

宏的debug

但这几个功能只有nightly能用。

使用cargo build —verbose可以查看rustc编译的命令。在编译命令里传入-Z unstable-options —pretty expanded可以查看扩展后的代码。

打开#![feature(log_syntax)],使用log_syntax!() 打印宏。
打开#![feature(trace_macros)],trace_macros!(true) 来打印后面的宏。

实现

expr是用来匹配rust表达式的片段,还有别的片段类型。

Fragment type Matches (with examples) Can be followed by…
expr An expression:
2 + 2, “udon”, x.len()
=> , ;
stmt An expression or declaration, not including any trailing semicolon
(hard to use; try expr or block instead)
=> , ;
ty A type:
String, Vec, (&str, bool), dyn Read + Send
=> , ; = | { [ : > as where
path A path (discussed):
ferns,::std::sync::mpsc
=> , ; = | { [ : > as where
pat A pattern (discussed):
_,Some(ref x)
=> , = | if in
item An item (discussed):
struct Point { x: f64, y: f64 },mod ferns;
Anything
block A block (discussed):
{ s += “ok\n”; true }
Anything
meta The body of an attribute (discussed):
inline,derive(Copy, Clone),doc=”3D models.”
Anything
ident An identifier:
std, Json, longish_variable_name
Anything
literal A literal value:
1024, “Hello, world!”, 1_000_000f64
Anything
lifetime A lifetime:
‘a, ‘item, ‘static
Anything
vis A visibility specifier:
pub, pub(crate), pub(in module::submodule)
Anything
tt A token tree (see text):
;, >=, {}, [0 1 (+ 0 1)]
Anything

第三列的可以在后面的字符是按Rust语法规定的。比如expr后面只可能出现那几个字符。

宏可以像函数一个递归调用自己,默认的递归极限是64,可以用#![recursion_limit = “256”] 修改。

如果在宏里声明了变量,有可能会和外面的变量名冲突,这样扩展成代码的时候似乎会有问题,但Rust有hygiene机制。但这样也会有问题:

  1. macro_rules! setup_req {
  2. () => {
  3. let req = ServerRequest::new(server_socket.session());
  4. }
  5. }
  6. fn handle_http_request(server_socket: &ServerSocket) {
  7. setup_req!(); // declares `req`, uses `server_socket`
  8. ... // code that uses `req`
  9. }

如果想要用宏来做一个本地变量的复制是不能直接用的,因为hygiene会防止本地变量的冲突。可以把两个变量传进宏进行操作

  1. macro_rules! setup_req {
  2. ($req:ident, $server_socket:ident) => {
  3. let $req = ServerRequest::new($server_socket.session());
  4. }
  5. }
  6. fn handle_http_request(server_socket: &ServerSocket) {
  7. setup_req!(req, server_socket);
  8. ... // code that uses `req`
  9. }

引用

使用#[macro_use] mod json;引用别的模块定义的宏。引用之后,子模块也可以使用这些宏。或者在宏的定义上加上#[macro_export] 。
如果宏里用到的模块在宏调用的地方没有引入,宏是不能运行的。因此实际上export的宏是不能有本地依赖的。所以宏里依赖的项都要使用绝对路径。$crate是宏里的一个特殊片段,用来表示项目的跟模块。

避免语法错误

  1. macro_rules! complain {
  2. ($msg:expr) => {
  3. println!("Complaint filed: {}", $msg);
  4. };
  5. (user : $userid:tt , $msg:expr) => {
  6. println!("Complaint from user {}: {}", $userid, $msg);
  7. };
  8. }
  9. complain!(user: "jimb", "the AI lab's chatbots keep picking on me");

这个是会报错的,因为在第一个匹配里,rust会把输入的参数当做一个语法错误。虽然他是复合第二个匹配的。但在宏里遇到语法错误是不算匹配失败的,而是fatal error。
一般的解决办法是:

  1. 将每个规则都设置的规范

    1. macro_rules! complain {
    2. (msg : $msg:expr) => {
    3. println!("Complaint filed: {}", $msg);
    4. };
    5. (user : $userid:tt , msg : $msg:expr) => {
    6. println!("Complaint from user {}: {}", $userid, $msg);
    7. };
    8. }
  2. 将更具体的规则放在前面。