0%

Macro in Rust

寫了一堆CC++的文章,是時候換換口味了
Macro在Rust也有,不過不同於C/C++的Texture Replace
Rust的Macro強大的不得了,順便也跟C++的template做個比較

Declarative Macros

從min開始

C macro版的min或是C++ templaate版的就不提供了,寫到不想寫了
直接看Rust的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
macro_rules! min {
($a:ident, $b:ident) => {
if ($a < $b) {
$a
} else {
$b
}
}
}
fn main() {
let a = 2u32;
let b = 3u32;
println!("{}", min!(a, b));
}

這樣看起來沒什麼特別的
那如果多加一個變數呢

min version2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
macro_rules! min {
($a:ident, $b:ident) => {
if ($a < $b) {
$a
} else {
$b
}
};
($a:ident, $b:ident, $c:ident) => {
if ($a < $b) {
if ($a < $c) {
$a
} else {
$c
}
} else {
if ($b < $c) {
$b
} else {
$c
}
}
}
}
fn main() {
let a = 3u32;
let b = 2u32;
let c = 1u32;
println!("{}", min!(a, b, c));
}

同樣的macro,可以有兩種不同的使用方式意
C語言的marco板本長這樣

1
2
3
4
5
#define min_2(a, b) ((a) < (b)) ? (a) : (b)
#define min_3(a, b, c) ((a) < (b)) ? ((a) < (c)) ? (a) : (c) : ((b) < (c)) ? (b) : (c)
#define GET_MACRO(_1,_2,_3,NAME,...) NAME
#define min(...) GET_MACRO(__VA_ARGS__, min_3, min_2)(__VA_ARGS__)
printf("%d\n", min(3, 2, 1));

看起來就是一堆亂七八糟拼湊的組合怪
來看看Template版

1
2
3
4
5
6
7
8
9
10
template <typename T>
T min(T a, T b)
{
return (a < b) ? a : b;
}
template <typename T>
T min(T a, T b, T c)
{
return (a < b) ? (a < c) ? a : c : (b < c) ? b : c;
}

憑藉於Function overloading,可讀性高很多,唯一比較麻煩的是要寫兩次template function declaration

min version3

來個varadic個版本,先寫個看起來沒問題,實際上編譯不過的

1
2
3
4
5
6
7
8
9
10
11
macro_rules! min {
($a:ident) => { $a };
($a:ident, $($b:ident),+) => {
let minV = min!($($b),+)
if ($a < minV) {
$a
} else {
minV
}
};
}

後來發現Rust Macro裡面不能有local variable,只能改成這樣

1
2
3
4
5
6
macro_rules! min {
($a:ident) => { $a };
($a:ident, $($b:ident),+) => {
std::cmp::min($a, min!($($b),+))
};
}

之後又發現一點和C/C++ preprocessor不同的地方,由於他是直接對AST做操作,所以得到的Token要自己Parse
所以做個實驗,參數之間分隔用;取代,,這樣是合法的

1
2
3
4
5
6
7
8
9
10
11
12
macro_rules! min {
($a:ident) => { $a };
($a:ident; $($b:ident);+) => {
std::cmp::min($a, min!($($b);+))
};
}
fn main() {
let a = 3u32;
let b = 2u32;
let c = 1u32;
println!("{}", min!(a; b; c));
}

不過沒辦法用local variable有點可惜,
Marco版的,我寫不出來,直接看Variadic Template的版本

1
2
3
4
5
6
7
8
9
10
template <typename T, typename... Args>
T min(const T& first, const Args&... args)
{
if constexpr (sizeof...(Args) == 0) {
return first;
} else {
const auto minV = min(args...);
return (first < minV) ? first : minV;
}
}

可以做更多的變化,不過Variadic Template最大的問題是我永遠記不住...到底要放哪這件事`
不過Rust真正厲害的是第二種Macro

Procedural Macros

基本上就是把輸入的TokenStream轉成另外的TokenStream的流程

分成三種

1
$ cargo new macro-demo --lib

Cargo.toml新增以下兩行

1
2
[lib]
proc-macro = true

Attribute macros

1
2
3
4
5
6
7
#[proc_macro_attribute]
fn sorted(args: TokenStream, input: TokenStream) -> TokenStream {
let _ = args;
let _ = input;

unimplemented!()
}

How to use atribute macro

1
2
3
4
5
6
#[sorted]
enum Letter {
A,
B,
C,
}

Function-like procedural macros

1
2
3
4
5
6
#[proc_macro]
pub fn seq(input: TokenStream) -> TokenStream {
let _ = input;

unimplemented!()
}

How to use function-like macro

1
2
3
seq! { n in 0..10 {
/* ... */
}}

Derive macro helper attributes

1
2
3
4
5
6
#[proc_macro_derive(Builder)]
fn derive_builder(input: TokenStream) -> TokenStream {
let _ = input;

unimplemented!()
}

How to use derived macro

1
2
3
4
#[derive(Builder)]
struct Command {
// ...
}

Caution

Procedural Macros不同於Declarative Macros,必須單獨是一個crate存在,目前IDE對Proc Macro的支持度不好,連Debug Proc Macro也很麻煩,最常使用的還是print大法

Simple example

從別人的範例中學來的,這邊實作一個Attribute macros

1
2
3
4
5
6
7
$ mkdir rust_proc_macro_demo && cd rust_proc_macro_demo
$ mkdir rust_proc_macro_guide && cd rust_proc_macro_guide
$ cargo init --bin
$ cd ..
$ mkdir proc_macro_define_crate && cd proc_macro_define_crate
$ cargo init --lib
$ cd ..

修改proc_macro_define_crate/Cargo.toml
加入

1
2
3
4
5
6
[lib]
proc-macro = true

[dependencies]
quote = "1"
syn = {features=["full","extra-traits"]}

接著修改rust_proc_macro_guide/Cargo.toml

1
2
[dependencies]
proc_macro_define_crate = {path="../proc_macro_define_crate"}

置換掉proc_macro_define_crate/src/lib.rs裡面的內容

1
2
3
4
5
6
7
8
use proc_macro::TokenStream;

#[proc_macro_attribute]
pub fn mytest_proc_macro(attr: TokenStream, item: TokenStream) -> TokenStream {
eprintln!("Attr {:#?}", attr);
eprintln!("Item {:#?}", item);
item
}

一樣將rust_proc_macro_guide/src/main.rs內部的內容換掉

1
2
3
4
5
6
use proc_macro_define_crate::mytest_proc_macro;

#[mytest_proc_macro(HungMingWu)]
fn foo(a:i32){
println!("hello world");
}

接著用cargo check檢查

1
2
$ cd rust_proc_macro_guide/
$ cargo check

可以看到類似這樣的輸出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
Attr TokenStream [
Ident {
ident: "HungMingWu",
span: #0 bytes(69..79),
},
]
Item TokenStream [
Ident {
ident: "fn",
span: #0 bytes(82..84),
},
Ident {
ident: "foo",
span: #0 bytes(85..88),
},
Group {
delimiter: Parenthesis,
stream: TokenStream [
Ident {
ident: "a",
span: #0 bytes(89..90),
},
Punct {
ch: ':',
spacing: Alone,
span: #0 bytes(90..91),
},
Ident {
ident: "i32",
span: #0 bytes(91..94),
},
],
span: #0 bytes(88..95),
},
Group {
delimiter: Brace,
stream: TokenStream [
Ident {
ident: "println",
span: #0 bytes(101..108),
},
Punct {
ch: '!',
spacing: Alone,
span: #0 bytes(108..109),
},
Group {
delimiter: Parenthesis,
stream: TokenStream [
Literal {
kind: Str,
symbol: "hello world",
suffix: None,
span: #0 bytes(110..123),
},
],
span: #0 bytes(109..124),
},
Punct {
ch: ';',
spacing: Alone,
span: #0 bytes(124..125),
},
],
span: #0 bytes(95..127),
},
]

這樣我們就能看出Attr和Item分別對應的TokenStream了

From TokenStream to Syntax Tree

有些時候,光看Lexer的TokenStream無助於解決問題,我們需要Syntax Tree
因此我們修改mytest_proc_macro

1
2
3
4
5
6
7
8
9
10
11
use proc_macro::TokenStream;
use syn::{parse_macro_input, AttributeArgs, Item};
use quote::quote;

#[proc_macro_attribute]
pub fn mytest_proc_macro(attr: TokenStream, item: TokenStream) -> TokenStream {
eprintln!("Attr {:#?}", parse_macro_input!(attr as AttributeArgs));
let body_ast = parse_macro_input!(item as Item);
eprintln!("Item {:#?}", body_ast);
quote!(#body_ast).into()
}

會跑出這樣的結果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
Attr [
Meta(
Path(
Path {
leading_colon: None,
segments: [
PathSegment {
ident: Ident {
ident: "HungMingWu",
span: #0 bytes(69..79),
},
arguments: None,
},
],
},
),
),
]
Item Fn(
ItemFn {
attrs: [],
vis: Inherited,
sig: Signature {
constness: None,
asyncness: None,
unsafety: None,
abi: None,
fn_token: Fn,
ident: Ident {
ident: "foo",
span: #0 bytes(85..88),
},
generics: Generics {
lt_token: None,
params: [],
gt_token: None,
where_clause: None,
},
paren_token: Paren,
inputs: [
Typed(
PatType {
attrs: [],
pat: Ident(
PatIdent {
attrs: [],
by_ref: None,
mutability: None,
ident: Ident {
ident: "a",
span: #0 bytes(89..90),
},
subpat: None,
},
),
colon_token: Colon,
ty: Path(
TypePath {
qself: None,
path: Path {
leading_colon: None,
segments: [
PathSegment {
ident: Ident {
ident: "i32",
span: #0 bytes(91..94),
},
arguments: None,
},
],
},
},
),
},
),
],
variadic: None,
output: Default,
},
block: Block {
brace_token: Brace,
stmts: [
Semi(
Macro(
ExprMacro {
attrs: [],
mac: Macro {
path: Path {
leading_colon: None,
segments: [
PathSegment {
ident: Ident {
ident: "println",
span: #0 bytes(101..108),
},
arguments: None,
},
],
},
bang_token: Bang,
delimiter: Paren(
Paren,
),
tokens: TokenStream [
Literal {
kind: Str,
symbol: "hello world",
suffix: None,
span: #0 bytes(110..123),
},
],
},
},
),
Semi,
),
],
},
},
)

Comparsion with C/C++

要達到類似的功能,除了X-Macros之外,我想不到類似的方法了
不過X-Marcos不僅醜,功能還有限,Debug更困難

Reference

Rust Macro 手册
Rust宏编程新手指南【Macro】
Rust 过程宏 101
The Little Book of Rust Macros
Rust Latam: procedural macros workshop
Macros in Rust: A tutorial with examples - LogRocket Blog
Overloading Macro on Number of Arguments