5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

こちらの記事は Rustマクロ冬期講習アドベントカレンダー 1日目の記事です!大大大遅刻!!!! :bow: :bow:

1日目は「そもそもRustのマクロにはどのような種類があるのか?」を簡単にまとめたいと思います!

Rustのマクロは、定義方法で2種類、呼び出し方法で3種類に大まかに分けることができます。

以下は定義方法と呼び出し方法の組み合わせです。宣言マクロは関数風マクロしか定義できず、手続きマクロならば全てのマクロを定義できます。

呼び出し方法 →
↓定義方法
関数風マクロ
hoge!
属性風マクロ
#[hoge]
deriveマクロ
#[derive(Hoge)]
宣言マクロ
macro_rules!
:o: :x: :x:
手続きマクロ
[lib]
proc-macro = true
:o: :o: :o:

定義方法2分類

どのようにしてマクロを定義するかで、2種類あります。

  • 宣言マクロ (Declarative Macro, MBE)
  • 手続きマクロ (Procedural Macro)

例を出しつつ紹介していきます。

プレースホルダーがわかりやすいように関数名等を漢字かなで書いていますが、例であるためです。(以下は実は全て正しいRustコードとして扱われますが、)ASCII文字でないユニコード文字列を関数名等に使用するのは基本的には避けましょう。

1. 宣言マクロ (Declarative Macro, MBE)

1つ目は macro_rules! マクロを用いた定義方法です。

Rust
macro_rules! マクロ名 {
    // 呼び出しパターン
    // $メタ変数:フラグメント指定子 
    ( $メタ変数1:ident, $メタ変数2:expr ) => { // マクロ名!( a, b ) という呼び出しにマッチ
        // 置き換え内容をここに書く
        // 例↓
        let $メタ変数1 = $メタ変数2;
        println!("{} = {}", stringify!($メタ変数1), $メタ変数1);
    }; // <- セミコロンで終わる

    // 複数のパターンからなることもある
    ( $m:ident; $($n:tt)* ) => {}; // マクロ名!( a; ... ) のような呼び出しにマッチ
}

fn main() {
    マクロ名!(hoge, 10);
    // ↑↓ カッコの種類 (), [], {} は呼び出しに無関係
    マクロ名![hoge, 10];

    // 共に以下に展開される
    // let hoge = 10;
    // println!("{} = {}", stringify!(hoge), hoge);

    // 以下は2番目のパターンにマッチする
    マクロ名!{ fuga; , ;  + [無視] される; };
}

playground: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=28a2919e5e032ea1bfe0e24db3bb4fc9

ソースコード内に直接書くことができ、手軽に作成できます。一方で、置き換え前のパターンマッチ内の書き方に制約がある影響で高度なことは手続きマクロと比べればしにくくなっています。特に繰り返し構造は再帰を使うことで書けることもある一方、複雑になったり処理が重くなったりしがちです。

置換結果を例示することでマクロを作るという特徴より、宣言マクロ (Declarative Macro) や、例示マクロ (Macros By Example, MBE) と呼ばれます。

2. 手続きマクロ

2つ目はライブラリクレートの Cargo.toml にて [lib] proc-macro = true を指定することで作れるようになる手続きマクロです。

Cargo.toml
[package]
name = "proc_macro_example"
version = "0.1.0"
edition = "2021"

+ [lib]
+ proc-macro = true

[dependencies]
# 例では使用していないが、以下3つはほぼほぼ確実に使用する。
+ syn = { version = "2.0.90", features = ["full"] }
+ quote = "1.0.37"
+ proc-macro2 = "1.0.92"

宣言マクロと異なり、同一クレート内ではマクロが使えない1ので、マクロ専用にクレートを設ける必要があります。

lib.rs
use proc_macro::TokenStream;

// 入力をそのまま返す関数風マクロ!()

#[proc_macro]
pub fn 入力をそのまま返す関数風マクロ(input: TokenStream) -> TokenStream {
    input
}

// #[入力をそのまま返す属性風マクロ(_attr)]
// fn item() {
//     // ...,
// }

#[proc_macro_attribute]
pub fn 入力をそのまま返す属性風マクロ(
    _attr: TokenStream,
    item: TokenStream,
) -> TokenStream {
    item
}

// #[入力をそのまま返すderive風マクロ]
// struct Item {
//   // ...
// }

#[proc_macro_derive(何もしないderive風マクロ)]
pub fn 何もしないderive風マクロの実装_名前は異なって良い(
    _item: TokenStream,
) -> TokenStream {
    TokenStream::new()
}
呼び出す側のmain.rs
use proc_macro_example::{
    何もしないderive風マクロ, 入力をそのまま返す属性風マクロ, 入力をそのまま返す関数風マクロ,
};

fn main() {
    入力をそのまま返す関数風マクロ!( let _なにか入力: (); );

    #[入力をそのまま返す属性風マクロ(今回はメタは無視される)]
    fn _hoge() {}

    #[derive(何もしないderive風マクロ)]
    struct _Hoge;
}

// ↓ 展開結果

fn main() {
    let _ = "何か入力";
    fn _hoge() {}
    struct _Hoge;
}

proc_macro::TokenStream を受け取り、 proc_macro::TokenStream を吐き出す手続きプログラム」を作成し、そのプログラムをマクロを使用しているソースコードのコンパイル時に実行するのが手続きマクロの正体になります。

宣言マクロと比べると、「どのようになるか」ではなくて「どのような流れ(手続き)でRustソースコードを書き換えるか」を記述して作成することになるので、手続きマクロ (Procedual Macro) と呼ばれています。

宣言マクロより煩雑な分、より高度な処理が行いやすかったり、置き換え前部分の記述を柔軟にできたりします。

呼び出し方法3分類

使用方法・記述方法で3種類に分類されます。

  • 関数風マクロ (Function-Like Macro)
  • 属性風マクロ (Attribute Macro)
  • deriveマクロ

同じく例を出しつつ紹介していきます。

1. 関数風マクロ

println("...") ではなくて println!("{}", ...) みたいに ! (エクスクラメーションマーク) をつけて呼び出すおなじみのマクロです。

Rust
fn main() {
    println!("{} + {} = {}", 10, 20, 10 + 20);
    let v = vec![1, 2, 3, 4];

    // () ではなく [] でも {} でもよく、決まっていない
    // マクロごとに慣例で決めている節がある
    let v2 = vec! ( 1, 2, 3, 4 ) ;
    println! {
        "{:?} == {:?} = {}",
        v,
        v2,
        v == v2
    };
}

playground: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=52c7eff6ebf89b148690b4097cef22e9

例で書いた通り、関数風マクロの ! の後ろにくるカッコはなんでも大丈夫です。

カッコで囲まれた範囲はトークン木 tt として扱われるため、関数風マクロはマッチで書くと $name:ident ! $token_tree:tt のような構造であるといえます(少し嘘)。

ちょっとした検証
Rust
macro_rules! is_func_macro {
    ($macro_name:ident ! $token_tree:tt) => { true };
    ($other:tt) => { false };
}

fn main() {
    println!("{}", is_func_macro!(vec![1, 2, 3])); // true
    println!("{}", is_func_macro!(10)); // false
    println!("{}", is_func_macro!(vec! 10)); // true // これは望んだ結果ではないです
}

playground: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=d78badde1b12c1b7c2e1920142d8f26c

$name:ident ! $token_tree:tt だと vec! 10 みたいなやつも当てはまってしまうのですがこれは関数風マクロとしては不正です。宣言マクロにおけるフラグメント指定子の限界ですね...

トークンツリーの中でも、複数のトークンから構成される「グループ」という種類のもので、グループを作成するためにデリミタとして [] () {} が使われます(そのためカッコはどれでもよい)。proc_macro2::Group をはじめとして手続きマクロ作成用クレートには対応する構造体があります。

定義方法で触れた通り、関数風マクロは宣言マクロ・手続きマクロ両方で作成することが可能です。

Q. macro_rules! は関数風マクロなの?

macro_rules! マクロ名 { (マッチ) => {展開後}; }

マクロを定義できるこの文法ですが、実は例外的なものでこれ自体は関数風マクロとは呼び難いものです。

特に構造としてみると macro_rules ! $name:ident $def:tt のようになっており、関数風マクロのそれとは異なっています。

参考: https://veykril.github.io/tlborm/syntax-extensions/ast.html

2. 属性風マクロ

フラグメント指定子 item (あるいは syn::Item ) に分類されるもの、すなわち関数 fn hoge() {} やモジュール mod fuga {}impl Bar {}struct Baz {} ...などの構文要素に対して、真上に付けることで使用できるマクロです。

Rust
use tokio::time::{sleep, Duration};

#[tokio::main]
async fn main() {
    sleep(Duration::from_secs(1)).await;

    println!("Hello, world!");
}

#[macro] という形が基本形で、他にも3パターンほどあります。

ref: https://docs.rs/syn/latest/syn/enum.Meta.html

  • #[macro] : Path形式
  • #[macro(...)] : List形式。 () の内側にさらにメタのリストが続きます
  • #[macro = "..."] : NameValue形式

属性風マクロは、マクロを付与したアイテムに対して、メタ情報(例えば #[macro(Meta)]Meta に来る部分) を補助的に使いながら書き換えを行い、Rustコードを出力します。

前述の通り、手続きマクロでないと作成できません。

3. deriveマクロ

属性風マクロと似ていますが、構造体 struct ... {} と列挙体 enum ... {} のみに付与できるマクロです。

慣例的には構造体・列挙体に「自動でトレイトを付与する」ために使われますが、あくまでも慣例で対応するトレイトが絶対存在しなければならないわけではありません。

Rust
use serde::{Deserialize, Serialize};

#[derive(Deserialize, Serialize)]
#[serde(rename_all = "PascalCase")]
struct Hoge {
    a: u32,
    #[serde(rename = "hoge")]
    b: String,
}

deriveマクロには関連する属性(アトリビュート)を設定できます。上述の例では serde(rename = ...) 等がこれに当たります。属性風マクロに似た書き方がされますが、deriveマクロの処理に活用されるのみでありこれ自体は属性風マクロではありません。

属性風マクロ同様、手続きマクロでないと作成できません。

属性風マクロは「置き換え」 deriveマクロは「追加」

属性風マクロでも構造体は受け取れますし、トレイトの実装が可能です。そして逆にderiveマクロだからと言って属性風マクロでもできる処理ができないわけではありません。

大きな違いは、手続きマクロの入力が出力されるか否かです。

  • 属性風マクロ: 「置き換え」を目的としているマクロなので、入力として与えられたアイテムは、属性風マクロが出力しない限りソースコードからは失われます。
  • deriveマクロ: トレイト実装など、構造体・列挙体への実装の「追加」を目的としているマクロなので、 出力に入力で得られる構造体・列挙体それ自体を含める必要はありません
    • トレイトの付与が目的の場合 impl ブロックだけ出力すれば大丈夫です。

どちらのマクロで作るべきか迷ったら、この仕様を基準にすると良さそうです。

まとめ・所感

Rustのマクロは色んな形で呼ばれたり macro_rules! 以外の方法でも定義できたりするしでなんか沢山の種類がありそうですが、実際にまとめてみるとそこまで何種類もあるわけではないことがわかりました! 特に、手続きマクロでマクロを作成できるようになれば、どの種類のマクロも作れるということがわかります。手続きマクロのハウツーはアドカレの今後の記事で書いていきたいと思っています!

ここまで読んでいただきありがとうございました。

  1. ただし、マクロを定義したライブラリクレートに設けた実行向けファイル(main.rsなど)では使用可能です。

5
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?