LabEx における Rust マクロの探索

RustRustBeginner
今すぐ練習

This tutorial is from open-source community. Access the source code

💡 このチュートリアルは英語版からAIによって翻訳されています。原文を確認するには、 ここをクリックしてください

はじめに

マクロへようこそ。この実験は、Rust Bookの一部です。LabEx で Rust のスキルを練習することができます。

この実験では、Rust におけるマクロの概念を探り、macro_rules!を使った宣言的マクロと、3 種類の手続き型マクロ:カスタム#[derive]マクロ、属性のようなマクロ、および関数のようなマクロについて学びます。

マクロ

この本を通じて、println!のようなマクロを使用してきましたが、マクロが何であり、どのように機能するかについては、まだ十分に掘り下げていません。「マクロ」という用語は、Rust の機能のグループを指します。macro_rules!を使った「宣言的」マクロと、3 種類の「手続き型」マクロです。

  • 構造体と列挙体に対して使用されるderive属性に追加されるコードを指定するカスタム#[derive]マクロ
  • 任意の項目で使用可能なカスタム属性を定義する属性のようなマクロ
  • 関数呼び出しのように見えるが、引数として指定されたトークンに対して操作する関数のようなマクロ

これらの各項目について順に説明しますが、最初に、関数が既にあるのになぜマクロが必要なのか見てみましょう。

マクロと関数の違い

基本的に、マクロは他のコードを書くコードを書く方法であり、これは「メタプログラミング」と呼ばれます。付録 C では、derive属性について説明しており、これはあなたに対してさまざまなトレイトの実装を生成します。また、本書を通じてprintln!vec!マクロも使用してきました。これらのマクロはすべて、手動で書いたコードよりも多くのコードを生成するように「展開」されます。

メタプログラミングは、書かなければならないコード量と保守コストを削減するのに役立ち、これは関数の役割の 1 つでもあります。ただし、マクロには関数にはない追加の機能がいくつかあります。

関数のシグネチャは、関数が持つパラメータの数と型を宣言する必要があります。一方、マクロは可変数のパラメータを取ることができます。つまり、1 つの引数でprintln!("hello")を呼び出したり、2 つの引数でprintln!("hello {}", name)を呼び出したりすることができます。また、マクロはコンパイラがコードの意味を解釈する前に展開されるため、例えば、特定の型に対してトレイトを実装することができます。関数はできません。なぜなら、関数は実行時に呼び出され、トレイトはコンパイル時に実装する必要があるからです。

関数の代わりにマクロを実装する欠点は、マクロ定義が関数定義よりも複雑であるということです。なぜなら、あなたが Rust コードを書いているので、それが Rust コードを書くからです。この間接性のため、マクロ定義は一般的に関数定義よりも読みにくく、理解しにくく、保守しにくい傾向があります。

マクロと関数のもう 1 つの重要な違いは、関数はどこで定義してもどこで呼び出してもよいのに対し、マクロはファイル内で呼び出す前に定義するか、スコープに持ち込む必要があるということです。

一般的なメタプログラミング用の macro_rules! を使った宣言的マクロ

Rust で最も広く使われるマクロの形式は「宣言的マクロ」です。これは、「例によるマクロ」、「macro_rules! マクロ」、または単に「マクロ」とも呼ばれます。その核心は、宣言的マクロが Rust の match 式に似たものを書けるようにすることです。第 6 章で説明したように、match 式は制御構造であり、式を取り、その式の結果の値をパターンと比較し、その後、一致するパターンに関連付けられたコードを実行します。マクロもまた、特定のコードに関連付けられたパターンと値を比較します。この場合、値はマクロに渡されるリテラルの Rust ソースコードであり、パターンはそのソースコードの構造と比較され、各パターンに関連付けられたコードは一致したときに、マクロに渡されたコードを置き換えます。これらのすべてはコンパイル時に行われます。

マクロを定義するには、macro_rules! 構文を使用します。vec! マクロがどのように定義されているかを見て、macro_rules! をどのように使用するかを探りましょう。第 8 章では、特定の値を持つ新しいベクトルを作成するために vec! マクロをどのように使用できるかを説明しました。たとえば、次のマクロは 3 つの整数を含む新しいベクトルを作成します。

let v: Vec<u32> = vec![1, 2, 3];

また、vec! マクロを使って、2 つの整数のベクトルや 5 つの文字列スライスのベクトルを作成することもできます。事前に値の数や型を知らないため、同じことを関数で行うことはできません。

リスト 19-28 は、vec! マクロのわずかに簡略化された定義を示しています。

ファイル名:src/lib.rs

1 #[macro_export]
2 macro_rules! vec {
  3 ( $( $x:expr ),* ) => {
        {
            let mut temp_vec = Vec::new();
          4 $(
              5 temp_vec.push(6 $x);
            )*
          7 temp_vec
        }
    };
}

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

注:標準ライブラリにおける vec! マクロの実際の定義には、事前に正しい量のメモリを割り当てるコードが含まれています。このコードは最適化であり、この例を簡単にするためにここには含めていません。

#[macro_export] アノテーション [1] は、マクロが定義されているクレートがスコープに入るときに、このマクロが利用可能になるようにすることを示しています。このアノテーションがないと、マクロをスコープに持ち込むことができません。

次に、マクロ定義を macro_rules! と定義するマクロの名前(感嘆符を付けない)で始めます [2]。この場合の名前は vec で、マクロ定義の本体を表す波括弧が続きます。

vec! の本体の構造は、match 式の構造に似ています。ここでは、( $( $x:expr ),* ) というパターンを持つ 1 つのアームがあり、その後に => とこのパターンに関連付けられたコードブロックが続きます [3]。パターンが一致すると、関連付けられたコードブロックが生成されます。このマクロにはこれが唯一のパターンなので、一致する有効な方法は 1 つだけで、他のパターンはエラーになります。より複雑なマクロには複数のアームがあります。

マクロ定義における有効なパターン構文は、第 18 章で説明したパターン構文とは異なります。なぜなら、マクロパターンは値ではなく Rust コード構造と照合されるからです。リスト 19-28 のパターンの部分が何を意味するか見てみましょう。完全なマクロパターン構文については、https://doc.rust-lang.org/reference/macros-by-example.html の Rust リファレンスを参照してください。

まず、全体的なパターンを囲むために一組の丸括弧を使用します。マクロシステム内の変数を宣言するには、ドル記号 ($) を使用します。この変数は、パターンに一致する Rust コードを含みます。ドル記号は、これが通常の Rust 変数とは異なるマクロ変数であることを明確に示しています。次に、丸括弧のセットが続きます。これは、置換コードで使用するために、丸括弧内のパターンに一致する値をキャプチャします。$() の中には $x:expr があり、これは任意の Rust 式と一致し、式に $x という名前を付けます。

$() の後に続くコンマは、$() 内のコードに一致するコードの後に、リテラルのコンマ区切り文字が任意に現れることを示しています。* は、* の前にあるものと 0 回以上一致することを指定します。

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

次に、このアームに関連付けられたコードの本体のパターンを見てみましょう。$()* 内の temp_vec.push() [5] は、パターンが一致する回数に応じて、パターン内の $() と一致する各部分に対して 0 回以上生成されます。$x [6] は、一致した各式に置き換えられます。このマクロを vec![1, 2, 3]; で呼び出すと、このマクロ呼び出しを置き換える生成されるコードは次のようになります。

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

任意の型の任意の数の引数を取り、指定された要素を含むベクトルを作成するコードを生成できるマクロを定義しました。

マクロを書く方法について詳しく学ぶには、オンラインドキュメントや、Daniel Keep によって始まり Lukas Wirth によって続けられている https://veykril.github.io/tlborm の「The Little Book of Rust Macros」などの他のリソースを参照してください。

属性からコードを生成するための手続き型マクロ

マクロの 2 番目の形式は手続き型マクロで、これはより関数に似た働きをします(手続きの一種です)。「手続き型マクロ」は、宣言的マクロのようにパターンと照合してコードを他のコードで置き換えるのではなく、入力としてコードを一部受け取り、そのコードに対して操作を行い、出力としてコードを生成します。3 種類の手続き型マクロは、カスタムderive、属性のような、および関数のようなもので、すべて同様の方法で機能します。

手続き型マクロを作成する際、定義は特別なクレートタイプを持つ独自のクレートに存在しなければなりません。これは将来的に解消したい複雑な技術的理由によるものです。リスト 19-29 では、特定のマクロ種類を使用するためのプレースホルダであるsome_attributeがある場合の手続き型マクロの定義方法を示しています。

ファイル名:src/lib.rs

use proc_macro::TokenStream;

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

リスト 19-29: 手続き型マクロの定義の例

手続き型マクロを定義する関数は、入力としてTokenStreamを取り、出力としてTokenStreamを生成します。TokenStream型は、Rust に含まれるproc_macroクレートによって定義され、トークンのシーケンスを表します。これがマクロの核心です。マクロが操作するソースコードが入力TokenStreamを構成し、マクロが生成するコードが出力TokenStreamです。また、関数には、作成している手続き型マクロの種類を指定する属性が付けられています。同じクレートに複数種類の手続き型マクロを持つことができます。

さて、手続き型マクロの種類を見てみましょう。まずはカスタムderiveマクロから始めて、他の形式が異なる小さな違いを説明します。

カスタム derive マクロを書く方法

hello_macro という名前のクレートを作成しましょう。このクレートには、HelloMacro という名前のトレイトが定義されており、そのトレイトには hello_macro という名前の 1 つの関連付けられた関数があります。ユーザーに対してそれぞれの型に対して HelloMacro トレイトを実装させるのではなく、手続き型マクロを提供して、ユーザーが #[derive(HelloMacro)] で型を注釈付けすることで、hello_macro 関数のデフォルトの実装を取得できるようにします。デフォルトの実装では、Hello, Macro! My name is 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! と出力します。最初のステップは、新しいライブラリクレートを作成することです。次のようにします。

cargo new hello_macro --lib

次に、HelloMacro トレイトとその関連付けられた関数を定義します。

ファイル名:src/lib.rs

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 にはリフレクション機能がないため、実行時に型の名前を参照することができません。コンパイル時にコードを生成するために、マクロが必要です。

次のステップは、手続き型マクロを定義することです。この記事を書いている時点で、手続き型マクロは独自のクレートに存在する必要があります。最終的に、この制限は解除されるかもしれません。クレートとマクロクレートを構成する規則は次の通りです。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

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

#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
    // Rust コードを構文木として表現し、操作できるようにする
    let ast = syn::parse(input).unwrap();

    // トレイトの実装を構築する
    impl_hello_macro(&ast)
}

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

hello_macro_derive 関数は、TokenStream を解析する責任があり、impl_hello_macro 関数は、構文木を変換する責任があります。これにより、手続き型マクロを書くのが便利になります。外側の関数(この場合は hello_macro_derive)のコードは、見たり作成したりするほとんどの手続き型マクロクレートで同じになります。内側の関数(この場合は impl_hello_macro)の本体で指定するコードは、手続き型マクロの目的に応じて異なります。

私たちは 3 つの新しいクレートを紹介しました。proc_macrosynhttps://crates.io/crates/syn から入手可能)、および quotehttps://crates.io/crates/quote から入手可能)です。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 という名前を指定したからできます。これは、ほとんどの手続き型マクロが従う規則です。

hello_macro_derive 関数はまず、inputTokenStream から、その後で解釈して操作できるデータ構造に変換します。ここが syn が登場するところです。synparse 関数は TokenStream を取り、解析された Rust コードを表す DeriveInput 構造体を返します。リスト 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
            )
        }
    )
}

リスト 19-32: リスト 19-30 のマクロ属性を持つコードを解析するときに得られる DeriveInput インスタンス

この構造体のフィールドは、解析した Rust コードが Pancakesident(識別子、つまり名前)を持つユニット構造体であることを示しています。この構造体には、さまざまな種類の Rust コードを記述するためのさらに多くのフィールドがあります。詳細については、https://docs.rs/syn/1.0/syn/struct.DeriveInput.htmlDeriveInputsyn ドキュメントを参照してください。

すぐに、新しい Rust コードを生成する impl_hello_macro 関数を定義します。しかし、その前に、derive マクロの出力も TokenStream であることに注意してください。返された TokenStream は、クレートのユーザーが書いたコードに追加されます。したがって、彼らがクレートをコンパイルするとき、修正された TokenStream に含まれる追加の機能を得ることができます。

syn::parse 関数の呼び出しがここで失敗した場合に、hello_macro_derive 関数がパニックするように unwrap を呼んでいることに気付いたかもしれません。手続き型マクロがエラーでパニックする必要があるのは、proc_macro_derive 関数が手続き型マクロ API に準拠するために TokenStream を返さなければならないからです。この例では unwrap を使って簡略化していますが、本番コードでは、panic! または expect を使って何が問題だったかに関するより具体的なエラーメッセージを提供する必要があります。

ここで、注釈付きの Rust コードを TokenStream から DeriveInput インスタンスに変換するコードがあるので、注釈付きの型に対して HelloMacro トレイトを実装するコードを生成しましょう。リスト 19-33 を参照してください。

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

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()
}

リスト 19-33: 解析された Rust コードを使用して HelloMacro トレイトを実装する

ast.ident を使用して、注釈付きの型の名前(識別子)を含む Ident 構造体インスタンスを取得します。リスト 19-32 の構造体は、リスト 19-30 のコードに対して impl_hello_macro 関数を実行したとき、取得する identident フィールドに "Pancakes" の値を持つことを示しています。したがって、リスト 19-33 の name 変数は、Ident 構造体インスタンスを含み、これを印刷すると、リスト 19-30 の構造体の名前である文字列 "Pancakes" になります。

quote! マクロを使って、返す Rust コードを定義します。コンパイラは、quote! マクロの実行の直接の結果とは異なるものを期待しているため、TokenStream に変換する必要があります。これは、この中間表現を消費して、必要な TokenStream 型の値を返す into メソッドを呼び出すことで行います。

quote! マクロはまた、非常に便利なテンプレート機能も提供します。#name を入力すると、quote! はそれを name 変数の値で置き換えます。通常のマクロと同じように、繰り返し処理もできます。詳細な紹介については、https://docs.rs/quotequote クレートのドキュメントを参照してください。

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

ここで使用されている stringify! マクロは、Rust に組み込まれています。これは、Rust 式(たとえば 1 + 2)を取り、コンパイル時に式を文字列リテラル(たとえば "1 + 2")に変換します。これは、式を評価してから結果を String に変換する format!println! マクロとは異なります。#name 入力が文字通り出力する式である可能性があるため、stringify! を使用します。stringify! を使用することで、コンパイル時に #name を文字列リテラルに変換することで、割り当てを節約することもできます。

この時点で、hello_macrohello_macro_derive の両方で cargo build が正常に完了するはずです。この手続き型マクロを実際に使ってみましょう!リスト 19-30 のコードにこれらのクレートを接続します。cargo new pancakes を使って、project ディレクトリに新しいバイナリプロジェクトを作成します。pancakes クレートの Cargo.tomlhello_macrohello_macro_derive を依存関係として追加する必要があります。hello_macrohello_macro_derive のバージョンを https://crates.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 は構造体と列挙体にのみ機能しますが、属性は関数など、他の項目にも適用できます。属性のようなマクロを使用する例を示しましょう。Web アプリケーションフレームワークを使用する際に、route という名前の属性が関数に注釈付けされるとします。

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

この #[route] 属性は、フレームワークによって手続き型マクロとして定義されます。マクロ定義関数のシグネチャは次のようになります。

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

ここでは、TokenStream 型の 2 つのパラメータがあります。最初は属性の内容に対するもので、GET, "/" の部分です。2 番目は、属性が付けられている項目の本体です。この場合、fn index() {} と関数本体の残り部分です。

それ以外は、属性のようなマクロはカスタム derive マクロと同じように機能します。proc-macro クレートタイプのクレートを作成し、生成したいコードを生成する関数を実装します!

関数のようなマクロ

関数のようなマクロは、関数呼び出しのように見えるマクロを定義します。macro_rules! マクロと同様に、関数よりも柔軟です。たとえば、未知の数の引数を取ることができます。ただし、macro_rules! マクロは、「一般的なメタプログラミング用の macro_rules! を使った宣言的マクロ」で説明したマッチのような構文を使ってのみ定義できます。関数のようなマクロは TokenStream パラメータを取り、その定義は、他の 2 種類の手続き型マクロと同じように、Rust コードを使ってその TokenStream を操作します。関数のようなマクロの例は、次のように呼び出される sql! マクロです。

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

このマクロは、内部の SQL 文を解析し、構文的に正しいことを確認します。これは、macro_rules! マクロが行うことよりもはるかに複雑な処理です。sql! マクロは次のように定義されます。

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

この定義は、カスタム derive マクロのシグネチャに似ています。丸括弧内のトークンを受け取り、生成したかったコードを返します。

まとめ

おめでとうございます!あなたはマクロの実験を完了しました。あなたのスキルを向上させるために、LabEx でさらに多くの実験を行って練習することができます。