|
宣言的マクロは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文で宣言する際は、(マクロが定義されているモジュールではなく)ルートモジュール直下にあるものとして宣言する。