TL;DR
コンパイル時に関数を実行して構文を拡張する Rust のマクロ。トークン列(TokenStream)を受け取り、新しいトークン列を返す。関数的マクロ、derive マクロ、属性マクロの 3 種類がある。
仕組み
The Rust Reference では、手続き的マクロを「AST から別の AST への関数のようなもの」と説明している。ただし、トークン列のほうがコンパイラにとってもマクロにとっても長く安定したインタフェースになるので、実際には AST のノードではなくトークン列を扱う。
3 種類のマクロは定義に付ける属性で区別する。
- 関数的マクロ:
custom!(...)の形で呼ぶ。#[proc_macro]で定義する - derive マクロ:
#[derive(CustomDerive)]の形で使う。#[proc_macro_derive]で定義し、出力は入力の要素の後ろに追加される - 属性マクロ:
#[CustomAttribute]の形で使う。#[proc_macro_attribute]で定義し、出力は付けた要素を置き換える
手続き的マクロは、クレートの種類が proc-macro のクレートのルートで定義する必要がある。定義したクレートの中では使えず、別のクレートから import して使う。Cargo では、マニフェストに次のように書く。
[lib]
proc-macro = true例
The Rust Reference の関数的マクロの例では、answer 関数を生成している。
#[proc_macro]
pub fn make_answer(_item: TokenStream) -> TokenStream {
"fn answer() -> u32 { 42 }".parse().unwrap()
}注意点
手続き的マクロはコンパイル中に実行され、コンパイラと同じ資源にアクセスできる。そのため、Cargo のビルドスクリプトと同じセキュリティ上の懸念がある。また、無限ループはコンパイラが検知できず、コンパイルが止まってしまう。
手続き的マクロは健全なマクロではなく、出力したトークン列がその場に直接書かれたかのように振る舞う。名前の衝突を避けるには、::std::option::Option のような絶対パスや、__internal_foo のような独特な名前を使う。