マクロ

本全体を通じてprintln!のようなマクロを使用してきましたが、マクロがなんなのかや、 どう動いているのかということは完全には探究していませんでした。 Rustにおいて、マクロという用語はある機能の集合のことを指します:macro_rules!を使った 宣言的 (declarative) マクロと、3種類の 手続き的 (procedural) マクロ:

  • 構造体とenumにderive属性を使ったときに追加されるコードを指定する、カスタムの#[derive]マクロ
  • 任意の要素に使えるカスタムの属性を定義する、属性風のマクロ
  • 関数のように見えるが、引数として指定されたトークンに対して作用する関数風のマクロ

です。

それぞれについて一つずつ話していきますが、その前にまず、どうして関数がすでにあるのにマクロなんてものが必要なのか見てみましょう。

マクロと関数の違い

基本的に、マクロは、他のコードを記述するコードを書く術であり、これはメタプログラミングとして知られています。 付録Cで、derive属性を議論し、これは、色々なトレイトの実装を生成してくれるのでした。 また、本を通してprintln!vec!マクロを使用してきました。これらのマクロは全て、展開され、 手で書いたよりも多くのコードを生成します。

メタプログラミングは、書いて管理しなければならないコード量を減らすのに有用で、これは、関数の役目の一つでもあります。 ですが、マクロには関数にはない追加の力があります。

関数シグニチャは、関数の引数の数と型を宣言しなければなりません。一方、マクロは可変長の引数を取れます: println!("hello")のように1引数で呼んだり、println!("hello {}", name)のように2引数で呼んだりできるのです。 また、マクロは、コンパイラがコードの意味を解釈する前に展開されるので、例えば、 与えられた型にトレイトを実装できます。関数ではできません。何故なら、関数は実行時に呼ばれ、 トレイトはコンパイル時に実装される必要があるからです。

関数ではなくマクロを実装する欠点は、Rustコードを記述するRustコードを書いているので、 関数定義よりもマクロ定義は複雑になることです。この間接性のために、マクロ定義は一般的に、 関数定義よりも、読みにくく、わかりにくく、管理しづらいです。

マクロと関数にはもう一つ、重要な違いがあります: ファイル内で呼び出すにマクロは定義したりスコープに導入しなければなりませんが、 一方で関数はどこにでも定義でき、どこでも呼び出せます。

一般的なメタプログラミングのためにmacro_rules!で宣言的なマクロ

Rustにおいて、最もよく使用される形態のマクロは、宣言的マクロです。これらは時として、 例によるマクロmacro_rules!マクロ、あるいはただ単にマクロとも称されます。 核となるのは、宣言的マクロは、Rustのmatch式に似た何かを書けるということです。第6章で議論したように、 match式は、式を取り、式の結果の値をパターンと比較し、それからマッチしたパターンに紐づいたコードを実行する制御構造です。 マクロも、あるコードと紐付けられたパターンと値を比較します。ここで、値とは マクロに渡されたリテラルのRustのソースコードそのもののこと。パターンがそのソースコードの構造と比較されます。 各パターンに紐づいたコードは、それがマッチしたときに、マクロに渡されたコードを置き換えます。これは全て、コンパイル時に起きます。

マクロを定義するには、macro_rules!構文を使用します。vec!マクロが定義されている方法を見て、 macro_rules!を使用する方法を探究しましょう。vec!マクロを使用して特定の値で新しいベクタを生成する方法は、 第8章で講義しました。例えば、以下のマクロは、3つの整数を持つ新しいベクタを生成します:


#![allow(unused)]
fn main() {
let v: Vec<u32> = vec![1, 2, 3];
}

また、vec!マクロを使用して2整数のベクタや、5つの文字列スライスのベクタなども生成できます。 同じことを関数を使って行うことはできません。予め、値の数や型がわかっていないからです。

リスト19-28で(いささ)か簡略化されたvec!マクロの定義を見かけましょう。

ファイル名: src/lib.rs


#![allow(unused)]
fn main() {
#[macro_export]
macro_rules! vec {
    ( $( $x:expr ),* ) => {
        {
            let mut temp_vec = Vec::new();
            $(
                temp_vec.push($x);
            )*
            temp_vec
        }
    };
}
}

リスト19-28: vec!マクロ定義の簡略化されたバージョン

標準ライブラリのvec!マクロの実際の定義は、予め正確なメモリ量を確保するコードを含みます。 その最適化コードは、ここでは簡略化のために含みません。

#[macro_export]注釈は、マクロを定義しているクレートがスコープに持ち込まれたなら、無条件でこのマクロが利用可能になるべきということを示しています。 この注釈がなければ、このマクロはスコープに導入されることができません。

それから、macro_rules!でマクロ定義と定義しているマクロの名前をビックリマークなしで始めています。 名前はこの場合vecであり、マクロ定義の本体を意味する波括弧が続いています。

vec!本体の構造は、match式の構造に類似しています。ここではパターン( $( $x:expr ),* )の1つのアーム、 =>とこのパターンに紐づくコードのブロックが続きます。パターンが合致すれば、紐づいたコードのブロックが発されます。 これがこのマクロの唯一のパターンであることを踏まえると、合致する合法的な方法は一つしかありません; それ以外は、全部エラーになるでしょう。より複雑なマクロには、2つ以上のアームがあるでしょう。

マクロ定義で合法なパターン記法は、第18章で講義したパターン記法とは異なります。というのも、 マクロのパターンは値ではなく、Rustコードの構造に対してマッチされるからです。リスト19-28のパターンの部品がどんな意味か見ていきましょう; マクロパターン記法全ては参考文献をご覧ください。

まず、1組のカッコがパターン全体を囲んでいます。次にドル記号($)、そして1組のカッコが続き、 このかっこは、置き換えるコードで使用するためにかっこ内でパターンにマッチする値をキャプチャします。 $()の内部には、$x:exprがあり、これは任意のRust式にマッチし、その式に$xという名前を与えます。

$()に続くカンマは、$()にキャプチャされるコードにマッチするコードの後に、区別を意味するリテラルのカンマ文字が現れるという選択肢もあることを示唆しています。 *は、パターンが*の前にあるもの0個以上にマッチすることを指定しています。

このマクロをvec![1, 2, 3];と呼び出すと、$xパターンは、3つの式123で3回マッチします。

さて、このアームに紐づくコードの本体のパターンに目を向けましょう: $()*部分内部のtemp_vec.push()コードは、 パターンがマッチした回数に応じて0回以上パターン内で$()にマッチする箇所ごとに生成されます。 $xはマッチした式それぞれに置き換えられます。このマクロをvec![1, 2, 3];と呼び出すと、 このマクロ呼び出しを置き換え、生成されるコードは以下のようになるでしょう:

{
    let mut temp_vec = Vec::new();
    temp_vec.push(1);
    temp_vec.push(2);
    temp_vec.push(3);
    temp_vec
}

任意の型のあらゆる数の引数を取り、指定した要素を含むベクタを生成するコードを生成できるマクロを定義しました。

macro_rules!には、いくつかの奇妙なコーナーケースがあります。 将来、Rustには別種の宣言的マクロが登場する予定です。これは、同じように働くけれども、それらのコーナーケースのうちいくらかを修正します。 そのアップデート以降、macro_rules!は事実上非推奨 (deprecated) となる予定です。 この事実と、ほとんどのRustプログラマーはマクロを書くよりも使うことが多いということを考えて、macro_rules!についてはこれ以上語らないことにします。 もしマクロの書き方についてもっと知りたければ、オンラインのドキュメントや、“The Little Book of Rust Macros”のようなその他のリソースを参照してください。

属性からコードを生成する手続き的マクロ

2つ目のマクロの形は、手続き的マクロと呼ばれ、より関数のように働きます(そして一種の手続きです)。 宣言的マクロがパターンマッチングを行い、マッチしたコードを他のコードで置き換えていたのとは違い、 手続き的マクロは、コードを入力として受け取り、そのコードに対して作用し、出力としてコードを生成します。

3種の手続き的マクロ (カスタムのderiveマクロ, 属性風マクロ、関数風マクロ)はみな同じような挙動をします。

手続き的マクロを作る際は、その定義はそれ専用の特殊なクレート内に置かれる必要があります。 これは複雑な技術的理由によるものであり、将来的には解消したいです。 手続き的マクロを使うとListing 19-29のコードのようになります。some_attributeがそのマクロを使うためのプレースホールダーです。

ファイル名: src/lib.rs

use proc_macro;

#[some_attribute]
pub fn some_name(input: TokenStream) -> TokenStream {
}

Listing 19-29: 手続き的マクロの使用例

手続き的マクロを定義する関数はTokenStreamを入力として受け取り、TokenStreamを出力として生成します。 TokenStream型はRustに内蔵されているproc_macroクレートで定義されており、トークンの列を表します。 ここがマクロの一番重要なところなのですが、マクロが作用するソースコードは、入力のTokenStreamへと変換され、マクロが生成するコードが出力のTokenStreamなのです。 この関数には属性もつけられていますが、これはどの種類の手続き的マクロを作っているのかを指定します。 同じクレート内に複数の種類の手続き的マクロを持つことも可能です。

様々な種類の手続き的マクロを見てみましょう。カスタムのderiveマクロから始めて、そのあと他の種類との小さな相違点を説明します。

カスタムのderive マクロの書き方

hello_macroという名前のクレートを作成してみましょう。 このクレートは、hello_macroという関連関数が1つあるHelloMacroというトレイトを定義します。 クレートの使用者に使用者の型にHelloMacroトレイトを実装することを強制するのではなく、 使用者が型を#[derive(HelloMacro)]で注釈してhello_macro関数の既定の実装を得られるように、 手続き的マクロを提供します。既定の実装は、Hello, Macro! My name is TypeName!(訳注: こんにちは、マクロ!僕の名前はTypeNameだよ!)と出力し、 ここでTypeNameはこのトレイトが定義されている型の名前です。言い換えると、他のプログラマに我々のクレートを使用して、 リスト19-30のようなコードを書けるようにするクレートを記述します。

ファイル名: src/main.rs

use hello_macro::HelloMacro;
use hello_macro_derive::HelloMacro;

#[derive(HelloMacro)]
struct Pancakes;

fn main() {
    Pancakes::hello_macro();
}

リスト19-30: 我々の手続き的マクロを使用した時にクレートの使用者が書けるようになるコード

このコードは完成したら、Hello, Macro! My name is Pancakes!(Pancakes: ホットケーキ)と出力します。最初の手順は、 新しいライブラリクレートを作成することです。このように:

$ cargo new hello_macro --lib

次にHelloMacroトレイトと関連関数を定義します:

ファイル名: src/lib.rs


#![allow(unused)]
fn main() {
pub trait HelloMacro {
    fn hello_macro();
}
}

トレイトと関数があります。この時点でクレートの使用者は、以下のように、 このトレイトを実装して所望の機能を達成できるでしょう。

use hello_macro::HelloMacro;

struct Pancakes;

impl HelloMacro for Pancakes {
    fn hello_macro() {
        println!("Hello, Macro! My name is Pancakes!");
    }
}

fn main() {
    Pancakes::hello_macro();
}

しかしながら、使用者は、hello_macroを使用したい型それぞれに実装ブロックを記述する必要があります; この作業をしなくても済むようにしたいです。

さらに、まだhello_macro関数にトレイトが実装されている型の名前を出力する既定の実装を提供することはできません: Rustにはリフレクションの能力がないので、型の名前を実行時に検索することができないのです。 コンパイル時にコード生成するマクロが必要です。

注釈: リフレクションとは、実行時に型名や関数の中身などを取得する機能のことです。 言語によって提供されていたりいなかったりしますが、実行時にメタデータがないと取得できないので、 RustやC++のようなアセンブリコードに翻訳され、パフォーマンスを要求される高級言語では、提供されないのが一般的と思われます。

次の手順は、手続き的マクロを定義することです。これを執筆している時点では、手続き的マクロは、 独自のクレートに存在する必要があります。最終的には、この制限は持ち上げられる可能性があります。 クレートとマクロクレートを構成する慣習は以下の通りです: fooというクレートに対して、 カスタムのderive手続き的マクロクレートはfoo_deriveと呼ばれます。hello_macroプロジェクト内に、 hello_macro_deriveと呼ばれる新しいクレートを開始しましょう:

$ cargo new hello_macro_derive --lib

2つのクレートは緊密に関係しているので、hello_macroクレートのディレクトリ内に手続き的マクロクレートを作成しています。 hello_macroのトレイト定義を変更したら、hello_macro_deriveの手続き的マクロの実装も変更しなければならないでしょう。 2つのクレートは個別に公開される必要があり、これらのクレートを使用するプログラマは、 両方を依存に追加し、スコープに導入する必要があるでしょう。hello_macroクレートに依存として、 hello_macro_deriveを使用させ、手続き的マクロのコードを再エクスポートすることもできるかもしれませんが、 このようなプロジェクトの構造にすることで、プログラマがderive機能を使用したくなくても、hello_macroを使用することが可能になります。

hello_macro_deriveクレートを手続き的マクロクレートとして宣言する必要があります。 また、すぐにわかるように、synquoteクレートの機能も必要になるので、依存として追加する必要があります。 以下をhello_macro_deriveCargo.tomlファイルに追加してください:

ファイル名: hello_macro_derive/Cargo.toml

[lib]
proc-macro = true

[dependencies]
syn = "1.0"
quote = "1.0"

手続き的マクロの定義を開始するために、hello_macro_deriveクレートのsrc/lib.rsファイルにリスト19-31のコードを配置してください。 impl_hello_macro関数の定義を追加するまでこのコードはコンパイルできないことに注意してください。

ファイル名: hello_macro_derive/src/lib.rs

extern crate proc_macro;

use proc_macro::TokenStream;
use quote::quote;
use syn;

#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
    // 操作可能な構文木としてのRustコードの表現を構築する
    // Construct a representation of Rust code as a syntax tree
    // that we can manipulate
    let ast = syn::parse(input).unwrap();

    // トレイトの実装内容を構築
    // Build the trait implementation
    impl_hello_macro(&ast)
}

リスト19-31: Rustコードを処理するためにほとんどの手続き的マクロクレートに必要になるコード

TokenStreamをパースする役割を持つhello_macro_derive関数と、構文木を変換する役割を持つimpl_hello_macro関数にコードを分割したことに注目してください:これにより手続き的マクロを書くのがより簡単になります。 外側の関数(今回だとhello_macro_derive)のコードは、あなたが見かけたり作ったりするであろうほとんどすべての手続き的マクロのクレートで同じです。 内側の関数(今回だとimpl_hello_macro)の内部に書き込まれるコードは、手続き的マクロの目的によって異なってくるでしょう。

3つの新しいクレートを導入しました: proc_macrosynquoteです。proc_macroクレートは、 Rustに付随してくるので、Cargo.tomlの依存に追加する必要はありませんでした。proc_macroクレートはコンパイラのAPIで、私達のコードからRustのコードを読んだり操作したりすることを可能にします。

synクレートは、文字列からRustコードを構文解析し、 処理を行えるデータ構造にします。quoteクレートは、synデータ構造を取り、Rustコードに変換し直します。 これらのクレートにより、扱いたい可能性のあるあらゆる種類のRustコードを構文解析するのがはるかに単純になります: Rustコードの完全なパーサを書くのは、単純な作業ではないのです。

hello_macro_derive関数は、ライブラリの使用者が型に#[derive(HelloMacro)]を指定した時に呼び出されます。 それが可能な理由は、ここでhello_macro_derive関数をproc_macro_deriveで注釈し、トレイト名に一致するHelloMacroを指定したからです; これは、ほとんどの手続き的マクロが倣う慣習です。

この関数はまず、TokenStreamからのinputをデータ構造に変換し、解釈したり操作したりできるようにします。 ここでsynが登場します。 synparse関数はTokenStreamを受け取り、パースされたRustのコードを表現するDeriveInput構造体を返します。 Listing 19-32はstruct Pancakes;という文字列をパースすることで得られるDeriveInput構造体の関係ある部分を表しています。

DeriveInput {
    // --snip--

    ident: Ident {
        ident: "Pancakes",
        span: #0 bytes(95..103)
    },
    data: Struct(
        DataStruct {
            struct_token: Struct,
            fields: Unit,
            semi_token: Some(
                Semi
            )
        }
    )
}

Listing 19-32: このマクロを使った属性を持つListing 19-30のコードをパースしたときに得られるDeriveInputインスタンス

この構造体のフィールドは、構文解析したRustコードがPancakesというident(識別子、つまり名前)のユニット構造体であることを示しています。 この構造体にはRustコードのあらゆる部分を記述するフィールドがもっと多くあります; DeriveInputsynドキュメンテーションで詳細を確認してください。

まもなくimpl_hello_macro関数を定義し、そこにインクルードしたい新しいRustコードを構築します。 でもその前に、私達のderiveマクロのための出力もまたTokenStreamであることに注目してください。 返されたTokenStreamをクレートの使用者が書いたコードに追加しているので、クレートをコンパイルすると、 我々が修正したTokenStreamで提供している追加の機能を得られます。

ここで、unwrapを呼び出すことで、syn::parse関数が失敗したときにhello_macro_derive関数をパニックさせていることにお気付きかもしれません。 エラー時にパニックするのは、手続き的マクロコードでは必要なことです。何故なら、 proc_macro_derive関数は、手続き的マクロのAPIに従うために、Resultではなく TokenStreamを返さなければならないからです。この例については、unwrapを使用して簡略化することを選択しました; プロダクションコードでは、panic!expectを使用して何が間違っていたのかより具体的なエラーメッセージを提供すべきです。

今や、TokenStreamからの注釈されたRustコードをDeriveInputインスタンスに変換するコードができたので、 Listing 19-33のように、注釈された型にHelloMacroトレイトを実装するコードを生成しましょう:

ファイル名: hello_macro_derive/src/lib.rs

extern crate proc_macro;

use proc_macro::TokenStream;
use quote::quote;
use syn;

#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
    // Construct a representation of Rust code as a syntax tree
    // that we can manipulate
    let ast = syn::parse(input).unwrap();

    // Build the trait implementation
    impl_hello_macro(&ast)
}

fn impl_hello_macro(ast: &syn::DeriveInput) -> TokenStream {
    let name = &ast.ident;
    let gen = quote! {
        impl HelloMacro for #name {
            fn hello_macro() {
                println!("Hello, Macro! My name is {}!", stringify!(#name));
            }
        }
    };
    gen.into()
}

Listing 19-33: パースされたRustコードを用いてHelloMacroトレイトを実装する

ast.identを使って、注釈された型の名前(識別子)を含むIdent構造体インスタンスを得ています。 Listing 19-32の構造体を見ると、impl_hello_macro関数をListing 19-30のコードに実行したときに私達の得るidentは、フィールドidentの値として"Pancakes"を持つだろうとわかります。 従って、Listing 19-33における変数nameは構造体Identのインスタンスをもちます。このインスタンスは、printされた時は文字列"Pancakes"、即ちListing 19-30の構造体の名前となります。

quote!マクロを使うことで、私達が返したいRustコードを定義することができます。 ただ、コンパイラが期待しているものはquote!マクロの実行結果とはちょっと違うものです。なので、TokenStreamに変換してやる必要があります。 マクロの出力する直接表現を受け取り、必要とされているTokenStream型の値を返すintoメソッドを呼ぶことでこれを行います。

このマクロはまた、非常にかっこいいテンプレート機構も提供してくれます; #nameと書くと、quote!は それをnameという変数の値と置き換えます。普通のマクロが動作するのと似た繰り返しさえ行えます。 本格的に入門したいなら、quoteクレートのdocをご確認ください。

手続き的マクロには使用者が注釈した型に対してHelloMacroトレイトの実装を生成してほしいですが、 これは#nameを使用することで得られます。トレイトの実装には1つの関数hello_macroがあり、 この本体に提供したい機能が含まれています: Hello, Macro! My name is、そして、注釈した型の名前を出力する機能です。

ここで使用したstringify!マクロは、言語に組み込まれています。1 + 2などのようなRustの式を取り、 コンパイル時に"1 + 2"のような文字列リテラルにその式を変換します。 これは、format!println!のような、式を評価し、そしてその結果をStringに変換するマクロとは異なります。 #name入力が文字通り出力されるべき式という可能性もあるので、stringify!を使用しています。 stringify!を使用すると、コンパイル時に#nameを文字列リテラルに変換することで、メモリ確保しなくても済みます。

この時点で、cargo buildhello_macrohello_macro_deriveの両方で成功するはずです。 これらのクレートをリスト19-30のコードにフックして、手続き的マクロが動くところを確認しましょう! cargo new pancakesであなたのプロジェクトのディレクトリ(訳注:これまでに作った2つのクレート内ではないということ)に新しいバイナリプロジェクトを作成してください。 hello_macrohello_macro_deriveを依存としてpancakesクレートのCargo.tomlに追加する必要があります。 自分のバージョンのhello_macrohello_macro_derivecrates.io に公開しているなら、 普通の依存になるでしょう; そうでなければ、以下のようにpath依存として指定すればよいです:

[dependencies]
hello_macro = { path = "../hello_macro" }
hello_macro_derive = { path = "../hello_macro/hello_macro_derive" }

リスト19-30のコードをsrc/main.rsに配置し、cargo runを実行してください: Hello, Macro! My name is Pancakesと出力するはずです。 手続き的マクロのHelloMacroトレイトの実装は、pancakesクレートが実装する必要なく、包含されました; #[derive(HelloMacro)]がトレイトの実装を追加したのです。

続いて、他の種類の手続き的マクロがカスタムのderiveマクロとどのように異なっているか見てみましょう。

属性風マクロ

属性風マクロはカスタムのderiveマクロと似ていますが、derive属性のためのコードを生成するのではなく、新しい属性を作ることができます。 また、属性風マクロはよりフレキシブルでもあります:deriveは構造体とenumにしか使えませんでしたが、属性は関数のような他の要素に対しても使えるのです。 属性風マクロを使った例を以下に示しています:webアプリケーションフレームワークを使っているときに、routeという関数につける属性名があるとします:

#[route(GET, "/")]
fn index() {

この#[route]属性はそのフレームワークによって手続き的マクロとして定義されたものなのでしょう。 マクロを定義する関数のシグネチャは以下のようになっているでしょう:

#[proc_macro_attribute]
pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream {

ここで、2つTokenStream型の引数がありますね。 1つ目は属性の中身:GET, "/"に対応しており、2つ目は属性が付けられた要素の中身に対応しています。今回だとfn index() {}と関数の本体の残りですね。

それ以外において、属性風マクロはカスタムのderiveマクロと同じ動きをします: クレートタイプとしてproc-macroを使ってクレートを作り、あなたのほしいコードを生成してくれる関数を実装すればよいです!

関数風マクロ

関数風マクロは、関数呼び出しのように見えるマクロを定義します。 これらは、macro_rules!マクロのように、関数よりフレキシブルです。 たとえば、これらは任意の数の引数を取ることができます。 しかし、一般的なメタプログラミングのためにmacro_rules!で宣言的なマクロで話したように、macro_rules!マクロはmatch風の構文を使ってのみ定義できたのでした。 関数風マクロは引数としてTokenStreamをとり、そのTokenStreamを定義に従って操作します。操作には、他の2つの手続き的マクロと同じように、Rustコードが使われます。 例えば、sql!マクロという関数風マクロで、以下のように呼び出されるものを考えてみましょう:

let sql = sql!(SELECT * FROM posts WHERE id=1);

このマクロは、中に入れられたSQL文をパースし、それが構文的に正しいことを確かめます。これはmacro_rules!マクロが可能とするよりも遥かに複雑な処理です。 sql!マクロは以下のように定義することができるでしょう:

#[proc_macro]
pub fn sql(input: TokenStream) -> TokenStream {

この定義はカスタムのderiveマクロのシグネチャと似ています:カッコの中のトークンを受け取り、生成したいコードを返すのです。

まとめ

ふう! あなたがいま手にしたRustの機能はあまり頻繁に使うものではありませんが、非常に特殊な状況ではその存在を思い出すことになるでしょう。 たくさんの難しいトピックを紹介しましたが、これは、もしあなたがエラー時の推奨メッセージや他の人のコードでそれらに遭遇した時、その概念と文法を理解できるようになっていてほしいからです。 この章を、解決策にたどり着くためのリファレンスとして活用してください。

次は、この本で話してきたすべてのことを実際に使って、もう一つプロジェクトをやってみましょう!

関連キーワード:  マクロ, macro, コード, 関数, クレート, derive, 定義, 手続き, TokenStream, パターン