|
宣言的マクロはmacro_rules!マクロで定義する。
macro_rules! マクロ名 { (引数のパターン) => { 生成する値 }; … }
なお、宣言的マクロはグローバルな位置(モジュール直下)で定義する。(implブロック内では定義できない)
列挙型のインスタンスを生成する宣言的マクロを作ってみる。
pub enum MyEnum { Message(String), }
この列挙子ではメッセージをStringで保持するが、文字列リテラルを渡そうとすると一手間かかる。(直接&strを渡せないから)
fn main() { let _e = MyEnum::Message("test".to_string()); // いちいちto_string()するのがウザい }
そこで、&strを渡せるマクロを作ってみる。
macro_rules! my_enum_message { ($message:expr) => { MyEnum::Message($message.to_string()) }; }
宣言的マクロはmacro_rules!マクロで定義する。
ここで定義するマクロ名の後ろには「!
」を付けない。
マクロ定義本体はmatch式のような感じで、引数をパターンマッチさせる。
このマクロ内で使用する変数には「$」を付ける。
「($message:expr)
」は引数が1個の場合にマッチする。
引数2つだったら「($message1:expr, $message2:expr)
」のようになる。
引数名の後ろのexprは、式を受け取るという意味。
識別子(変数名や関数名等)を受け取るidentとか、型名を受け取るtyとか、他にも色々ある。
そして、「=>
」の後に、生成したい値を構築する。
fn main() { let _e = my_enum_message!("test"); let _e = my_enum_message!(123); }
(残念ながら)マクロでは渡された式の型をチェックできないので、マクロ展開後のソースコードにエラーが無ければ通る。
この例だと、"test".to_string()
も123.to_string()
もコンパイルが通るので、マクロとしてもエラーにならない。
Rustでは、関数やメソッド呼び出しの引数の末尾にカンマがあっても良い。(末尾カンマは無視される)
fn main() { my_func(1); my_func(2,); // 末尾カンマOK } fn my_func(n: i32) { println!("{}", n); }
しかし上記で作ったマクロの呼び出しでは、末尾カンマがあるとエラーになる。
let _e = my_enum_message!("test",); // error: no rules expected the token `,`
macro_rules!マクロによる引数のパターンマッチでは、カンマがあるかどうかも判断対象になる。
したがって、末尾カンマを許容するには、引数パターンにカンマを含めてやればいい。
以下の2通りの方法が考えられる。
macro_rules! my_enum_message {
($message:expr) => { // 末尾カンマが無いパターン
MyEnum::Message($message.to_string())
};
($message:expr,) => { // 末尾カンマが有るパターン
MyEnum::Message($message.to_string())
};
}
macro_rules! my_enum_message { ($message:expr $(,)?) => { // カンマが0個または1個 MyEnum::Message($message.to_string()) }; }
Rustでは、関数やメソッドのオーバーロード(同名の引数違い)を定義することは出来ない。
しかしmacro_rules!マクロでは引数のパターンマッチが出来るので、異なる引数が受け取れる。
つまり、宣言的マクロではオーバーロードを定義するのと同じ状態に出来る。
#[derive(Debug)] struct MyStruct { value1: i32, value2: Option<i32>, }
macro_rules! new_my_struct { ($value:expr) => { // 引数が1つのパターン MyStruct { value1: $value, value2: None, } }; ($value1:expr, $value2:expr) => { // 引数が2つのパターン MyStruct { value1: $value1, value2: Some($value2), } }; }
fn main() { let s1 = new_my_struct!(123); println!("s1={:?}", s1); // MyStruct { value1: 123, value2: None } let s2 = new_my_struct!(123, 456); println!("s2={:?}", s2); // MyStruct { value1: 123, value2: Some(456) } }
macro_rules!マクロで定義された宣言的マクロは、デフォルトでは同一モジュール内(マクロを定義した場所より下)でしか使用できない。
他モジュールで使用できるようにする為には、マクロをエクスポートする必要がある。
(通常の構造体や列挙型等はuse文によってモジュールを指定すれば使えるようになるが、宣言的マクロはそういう仕組みではない)
// my_structモジュール #[derive(Debug)] pub struct MyStruct1 { value: i32, } impl MyStruct1 { pub fn new(value: i32) -> MyStruct1 { MyStruct1 { value } } }
// my_macroモジュール #[macro_export] macro_rules! new_my_struct1 { ($value:expr) => { $crate::my_struct::MyStruct1::new($value) }; }
マクロ定義に#[macro_export]
属性を付けることによってエクスポートする。
これにより、ルートモジュールのマクロとして、他モジュールから使用できるようになる。
(このマクロを定義している場所はmy_macroモジュールなのだが、エクスポートされたマクロは、そのモジュールに属している扱いにならないようだ)
エクスポートされたマクロはどのモジュールで展開されるか分からない。
内部で使用する構造体等をモジュールの相対パスで書いてしまうと、展開された場所からは異なるものを指してしまう(あるいは見つからない)ことになってしまう。
そこで、$crate(自分のクレートのルートを表す)を使って、絶対パスでモジュールを記述しておく。
mod my_macro; mod my_struct; mod sub; fn main() { let s = new_my_struct1!(123); println!("main={:?}", s); sub::print_my_struct1(); }
main.rsはルートモジュールなので、(use文でマクロを宣言しなくても)エクスポートされたマクロを使用できる。
use crate::new_my_struct1; pub(crate) fn print_my_struct1() { let s = new_my_struct1!(456); println!("sub={:?}", s); }
ルートモジュールでないモジュールでは、use文でインポートしないとエクスポートされたマクロを使えない。
use文で宣言する際は、(マクロが定義されているモジュールではなく)ルートモジュール直下にあるものとして宣言する。