💎

セバスチャンマクロを作って学ぶRustの手続きマクロ

2022/06/27に公開

皆様、ごきげんよう。
わたくし、Rustaceanお嬢様の白山風露と申しますわ[1]
本日はセバスチャン[2]マクロの作成を通して、皆様と一緒にRustの手続きマクロの作り方を学んでいこうと思いますの。

手続きマクロとは何ですの?

皆様の中にはそんな疑問を抱いている方もいらっしゃるのではないかしら?
手続きマクロは、英語の"Procedural macro"の訳語ですわ。
こちらはRustのバージョン1.15.0で安定化された、以前は"Macros 1.1"なんて呼ばれていた機能ですの。

手続きマクロが1.1なのですから、以前にもRustにはマクロが存在しておりまして、macro_rules!マクロ[3]を使って定義するのがそれになりますわ。
macro_rules!を使ったマクロ定義はトークン列をパターンマッチして置き換えルールを記述するものですが、手続きマクロはトークン列を受け取ってトークン列を返す関数を定義することで実装することができますわ。つまり、コンパイル中に自作の関数でソースコードの一部をある程度自由に置き換えることができるのですわ。

手続きマクロ用クレートの作り方を説明いたしますわ。

手続きマクロを作るには、手続きマクロ用クレートを作る必要がありますわ。
手続きマクロ用クレートはライブラリクレートの一種ですが、手続きマクロ以外の定義を公開することができませんの。ですから、既存のライブラリに手続きマクロを追加したくなった場合などは、ワークスペース機能を使って手続きマクロ用クレートを追加する必要がありますわ。

手続きマクロ用クレートは、Cargo.toml[lib]proc-macro = trueと書くことで作成できますわ。
手続きマクロ用クレートの中ではproc_macroクレート様と、

  • #[proc_macro_derive]
  • #[proc_macro_attribute]
  • #[proc_macro]

の3つの属性が使えるようになりますの。これらは手続きマクロ用クレート以外からは呼び出すことができない特殊なクレートと属性になりますわ。

proc_macroクレート様でできることと、それぞれの属性は以下で説明いたしますわ。

手続きマクロにも種類がございますのよ。

手続きマクロには

  • カスタム#[derive]マクロ
  • 属性風マクロ
  • 関数風マクロ

の3種類がございますわ。
これらは定義方法は大体同じなのですが、使う場所が異なりますわ。

カスタム#[derive]マクロの説明ですわ。

カスタム#[derive]マクロは#[derive(Debug)]などと同じように、自作のトレイトに対して自動でトレイト境界を定義するために使われますわ。
例えばserdeクレート様にはSerializeDeserializeのカスタム#[derive]マクロが定義されていますの[4]。JSONなどの形式で入出力を行うためにお世話になった方も多いのではございませんかしら?

カスタム#[derive]マクロは#[proc_macro_derive]属性を付けて、proc_macro::TokenStreamを受け取り、proc_macro::TokenStreamを返す関数として定義いたしますわ。

// crate my_macro

use proc_macro::TokenStream;

#[proc_macro_derive(MyTrait)]
pub fn my_trait_derive(tokens: TokenStream) -> TokenStream {
    // ...
}

このように定義すると、外側からはuse my_macro::MyTrait;と書いて#[derive(MyTrait)]のように使うことができますわ。
なお、先ほど述べましたように、手続きマクロ用クレートからは手続きマクロ以外を公開することができませんので、trait MyTraitは別のライブラリクレート内で定義しておく必要がありますわ。

カスタム#[derive]マクロは入力としてはstructenumの定義をトークン列として受け取るのですが、この後説明する他の手続きマクロと異なりまして、入力となるソースコード部分を置き換えるのではなく、出力部分がソースコードに追加される形になりますわ。ですので、カスタム#[derive]マクロを使ってstructenumの定義そのものを書き換えることはできませんわ。

ヘルパー属性についてですわ。

#[proc_macro_derive]属性は第一引数に公開される名前を指定するのですが、第二引数にattributes(my_trait)などのようにヘルパー属性の名前を指定できますわ。

#[proc_macro_derive(MyTrait, attributes(my_derive_attr))]
pub fn my_trait_derive(tokens: TokenStream) -> TokenStream {
    // ...
}

このようにいたしますと、以下のように書いたとき、

#[derive(MyTrait)]
struct MyStruct {
    #[my_derive_attr]
    i: i32,
}

#[my_derive_attr]属性がマクロ呼び出し前の構文解析でエラーにならなくなりますの。もちろん構文解析でエラーにならないだけで、属性をどのように解釈するかは手続きマクロの実装次第ですので、手続きマクロがエラーを出せばエラーになることもありますわ。
例えばserdeクレート様で#[derive(Serialize)]などを使うと#[serde]属性が使えるようになるのはこの機能を使っておりますわ。

属性風マクロの説明ですわ。

属性風マクロはRust組み込みの属性(例えば#[cfg]#[test]など)のように、任意のアイテム[5]に対して付けることができる属性を定義できる機能ですわ。

属性風マクロは入力として受け取ったトークン列を出力のトークン列で置き換えますわ。ですので、カスタム#[derive]マクロよりも柔軟ですのよ。

属性風マクロは以下のように#[proc_macro_attribute]属性を使用して定義できますわ。

#[proc_macro_attribute]
pub fn my_attr(attrs: TokenStream, item: TokenStream) -> TokenStream {
    // ...
}

カスタム#[derive]マクロと異なり、入力が2つになっていますわね。
これは属性が引数を受け取ることができるためでして、

#[my_attr(a, b, c)]
fn my_fn() {}

などと書きますと、a, b, cの部分がattrs引数に、fn my_fn() {}の部分がitem引数に渡されることになりますわ。

関数風マクロの説明ですわ。

関数風マクロは、println!などのmacro_rules!を使って定義されたマクロと同じように使えるマクロですわ。

関数風マクロを定義には#[proc_macro]属性を使いますわ。属性が引数をとることもありませんし、入力も引数1つの一番シンプルな形ですのよ。

#[proc_macro]
pub fn my_macro(tokens: TokenStream) -> TokenStream {
    // ...
}

カスタム#[derive]マクロはstructenumに付ける形ですし、属性風マクロはアイテムに付ける形になりますので、必然的にマクロに渡されるトークン列はRustの文法的に正しくなければなりませんでしたわ。
一方で、関数風マクロは、括弧の対応や使える文字に制限はございますが、Rustとは文法的にかけ離れたトークン列でも入力に受け付けることができますわ[6]

今回、セバスチャンマクロはこの関数風マクロを使って作っていきますわ。

クレートの初期化をいたしますわ。

それでは皆様、まずはマクロ用クレートを作りましょう。そうですわね……、ここはお嬢様らしくクレート名はreijouといたしましょうか。
クレートを作るには以下のコマンドを実行しますわ。

cargo new --lib reijou

これでとりあえずライブラリクレートが作られましたわ。一旦コミットを行いましょう。
この状態ではまだ普通のライブラリクレートですわ。そこで、Cargo.tomlを開いて、以下の内容を追加しますわよ。

[lib]
proc-macro = true

ここでcargo buildをいたしますと、以下のようなエラーが出てくるのではなくて?

error: `proc-macro` crate types currently cannot export any items other than functions tagged with `#[proc_macro]`, `#[proc_macro_derive]`, or `#[proc_macro_attribute]`
 --> src\lib.rs:1:1
  |
1 | pub fn add(left: usize, right: usize) -> usize {
  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

error: could not compile `reijou` due to previous error

先ほども申しました通り、手続きマクロ用クレートからはマクロ以外を公開することができませんので、cargo newで生成されたテンプレートのままではエラーになってしまうのですわ。
ですので、add関数は削除いたしまして、セバスチャンマクロを追加いたしますわ。

use proc_macro::TokenStream;

#[proc_macro]
pub fn セバスチャン(tokens: TokenStream) -> TokenStream {
    todo!()
}

add関数を削除したことでテストもビルドに失敗するようになってしまいましたので、こちらも変更しますわ。

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_セバスチャン() {
        todo!()
    }
}

とりあえずこんな感じにいたしますわ。警告は出ますものの、ビルドには成功するようになりましたわね。中身がtodo!()になっているので当然テストが通らないのですが、ここで一旦コミットいたしますわ。

テストができるようにいたしますわ。

さて、ここで残念なお話があるのですが、proc_macro::TokenStreamを利用したコードはテストができませんの。
先ほどコミットした状態からtodo!()を消しまして、試しに入力をそのまま返すように変更してみますわ。

#[proc_macro]
pub fn セバスチャン(tokens: TokenStream) -> TokenStream {
    tokens
}

TokenStream::new()で空のTokenStreamを作れますから、

#[test]
fn test_セバスチャン() {
    セバスチャン(TokenStream::new());
}

といたしますと、一旦テストが通りそうに思えます。ところが、これでcargo testを実行いたしますと、

running 1 test
test tests::test_セバスチャン ... FAILED

failures:

---- tests::test_セバスチャン stdout ----
thread 'tests::test_セバスチャン' panicked at 'procedural macro API is used outside of a procedural macro', library\proc_macro\src\bridge\client.rs:350:17
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
    tests::test_セバスチャン

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

error: test failed, to rerun pass '--lib'

このようにテストに失敗するのですわ。
エラーメッセージに出ておりますように[7]、手続きマクロのAPIは手続きマクロの外では使えませんわ。この場合テストコードも手続きマクロの外ということになってしまいますの。

しかしこれではあまりに不便ですわ。エレガントなテストはRustaceanお嬢様の嗜みですのよ。
ですが心配はいりませんわ。手続きマクロの内部からしか使えないproc_macroクレート様に代わって、proc_macroクレート様とほぼ同じAPIが実装されたproc-macro2クレート様というライブラリクレートが存在しますの。
proc_macro2::TokenStreamproc_macro::TokenStreamと相互に変換が可能でして、普通のライブラリクレートなのでテストから呼び出してもpanicを起こしませんの。
ですので、#[proc_macro]属性を付けた関数はエントリーポイントとしてproc_macro::TokenStreamproc_macro2::TokenStreamの変換のみを行い、それ以外の処理はproc_macro2::TokenStreamを受け取る実装関数に転送してしまえば、そちらをテストすることができるようになりますわ。

proc-macro2クレート様に加えてsynクレート様とquoteクレート様は手続きマクロの作成にとても便利なクレートですので、合わせてこの3つをCargo.tomldependenciesに書き加えますわ。

[dependencies]
proc-macro2 = "1.0.40"
syn = "1.0.98"
quote = "1.0.20"

バージョンはこの記事の執筆時点の最新版ですわ。皆様は適宜バージョンを読み替えてくださいな。

新しくセバスチャン_impl関数を用意しまして、セバスチャン関数から呼び出すようにいたしましょう。

use proc_macro2::TokenStream;

fn セバスチャン_impl(tokens: TokenStream) -> TokenStream {
    tokens
}

#[proc_macro]
pub fn セバスチャン(tokens: proc_macro::TokenStream) -> proc_macro::TokenStream {
    セバスチャン_impl(tokens.into()).into()
}

テストコードもセバスチャン_implを呼び出すようにいたしますわ。

#[test]
fn test_セバスチャン() {
    セバスチャン_impl(TokenStream::new());
}

これでcargo testが通ることを確認してくださいまし。
確認いたしましたら、ここでコミットをいたしますわ。

テストを書きますわ。

ここまで来ましたら、ようやくマクロの中身の実装に入れますわ。ですがここはやはり、テストファーストで行きたいところですわね。TDDは乙女の嗜みですわ。

テストを書くにあたって、pretty_assertionsクレート様を導入させていただきますわ。pretty_assertions::assert_eq!を使用いたしますと、テストに失敗した時に、差分をdiff形式で表示してくださいますわ。Cargo.tomlに以下を追加しますわ。

[dev-dependencies]
pretty_assertions = "1.2.1"

例によってバージョンは適宜読み替えてくださいませ。

そういえば、皆様にはセバスチャンマクロがどのような機能を目指しているか説明していませんでしたわ。セバスチャンマクロとは、セバスチャンに説明することでRustのコードが書けるマクロですわ。具体的に言うと、

セバスチャン! {
    わたくし std::env::args 様を使わせていただきますわ.
}

と書きますと、

use std::env::args;

に置き換えられる、といった機能を目指しておりますの。

今申しましたことをそのままテストにいたしますわ。
つまり、わたくし std::env::args 様を使わせていただきますわ.TokenStreamにしたものをセバスチャン_implに渡しまして、結果がuse std::env::args;TokenStreamにしたものと一致すれば良いので、そのようなテストを書くのですわ。

先ほどproc-macro2クレート様と一緒に追加したquoteクレート様にはquote!マクロがございまして、これを利用いたしますと

quote!{
    わたくし std::env::args 様を使わせていただきますわ.
}

のように書くことでTokenStreamを生成できるのですわ。
TokenStreamPartialEqを実装しておりませんので、そのままだと比較できませんわ。そこで、to_stringを呼んでからassert_eq!に渡すことになりますわ。
つまり、以下のようになりますわね。

use quote::quote;

#[test]
fn test_セバスチャン() {
    assert_eq!{
        セバスチャン_impl(quote!{
            わたくし std::env::args 様を使わせていただきますわ.
        }).to_string(),
        quote!{
            use std::env::args;
        }.to_string()
    };
}

さて、セバスチャン_implは今のところただ入力をそのまま返すだけの関数になっていますわ。ですからこのテストは当然落ちるはずですわ。試しにcargo testを実行してみますと、

テストに失敗しましたわ~~~!!!!

このようにカラフルなdiffが表示されるはずですわ。これでテストに失敗した時にどこが原因か一目瞭然になるのですわ。

ここらへんで一旦でコミットを作りますわ。

マクロの中身の実装ですわ。

それでは、ようやくマクロの中身を実装して、このテストが通るようにしていきますわよ。
TokenStreamを解析するにはsynクレート様を使いますわ。
そうそう、今回使わせていただきますsynクレート様の一部の機能はfeaturesで指定しておかないと使えませんので、Cargo.tomlをこんな風に書き換えますわ。

-syn = "1.0.98"
+syn = { version = "1.0.98", features = ["full"] }

synクレート様にはparse::Parserというトレイトがありまして、Fn(syn::parse::ParseStream) -> syn::Result<TokenStream>型に対して実装がありますわ。つまり、ParseStreamを受け取ってResult<TokenStream>を返す関数を実装いたしますと、その関数に対してParserトレイトのメソッドを呼び出すことができるのですわ。

ですので、まずこのようにセバスチャン_parse関数を定義いたしまして、

use syn::{
    parse::{Parser, ParseStream},
    Error, Result,
};

fn セバスチャン_parse(input: ParseStream) -> Result<TokenStream> {
    todo!()
}

セバスチャン_impl関数からParser::parse2メソッドを呼び出しますわ。

fn セバスチャン_impl(tokens: TokenStream) -> TokenStream {
    セバスチャン_parse.parse2(tokens)
        .unwrap_or_else(Error::into_compile_error)
}

セバスチャン_parse関数がinput: ParseStreamを受け取っているのが重要でして、こちらを利用いたしますとトークンの先読みや部分的なパースなどができるようになるのですわ。syn::parse::ParseStreamは定義を見ていただければ、

pub type ParseStream<'a> = &'a ParseBuffer<'a>;

となっておりますことからsyn::parse::ParseBuffer型の参照のエイリアスでして、ParseBufferのメソッドを利用することになりますわ。

また、戻り値がResultになりますので、エラーが発生した時は?演算子で抜けることができるのも利点ですわね。戻り値がErrだった時は、セバスチャン_impl側でsyn::Error::into_compile_errorによってコンパイルエラーに変換いたしますわ。

さて、今の目標はわたくし std::env::args 様を使わせていただきますわ.という入力をuse std::env::args;に変換することですわ。ですので、入力の要素がRustのトークンとしては何になるかを考える必要がありますのよ。
まず、わたくしはRustのトークンとしては識別子になりますわ。識別子はproc_macro2::Ident型で表現されますわ。
ParseBuffer::parseを利用すると、syn::parse::Parseトレイト境界が実装されている型をパースすることができるようになりますわ。proc_macro2で用意されている構文要素にはそれぞれParseトレイトが実装されていますので、

use proc_macro2::Ident;
let ident: Ident = input.parse()?;

といたしますとIdentが一つパースされ、inputの内部状態がパースが終わった位置に進みますわ[8]

Identをパースして、実際は識別子以外のトークンがあった場合はエラーになりますわ。ですが、Identは識別子全般をパースいたしますので、わたくし以外の識別子でもパースできてしまいますわ。
そこで、Identわたくしでなかった場合はコンパイルエラーにしてしまおうと思いますわ。IdentにはToStringトレイト境界が実装されておりますので、ident.to_string() == "わたくし"で判定できますわね。

if ident.to_string() == "わたくし" {
    todo!()
} else {
    return Err(Error::new(
        ident.span(),
        format!("予期せぬ識別子ですわ~!: {}", ident),
    ));
}

エラー処理の部分は今後も同じようなことを何度も書くことになりそうですので、関数に切り出しておくと良さそうですわ。

fn unexpected_ident<T>(ident: &Ident) -> Result<T> {
    Err(Error::new(
        ident.span(),
        format!("予期せぬ識別子ですわ~!: {}", ident),
    ))
}

わたくし以外の識別子がコンパイルエラーを引き起こすことを確認するためにテストを追加してみますわ。

#[test]
fn test_unexpected_ident() {
    assert_eq! {
        セバスチャン_impl(quote!{
            わたし
        }).to_string(),
        quote!{
            compile_error!{ "予期せぬ識別子ですわ~!: わたし" }
        }.to_string()
    };
}

cargo test test_unexpected_identを実行して通ることを確認いたしましょう。

わたくしがパースできましたら、続いてはstd::env::argsに該当する部分をパースしますわ。こちらはRustの構文要素としてはUseTreeと定義されております部分で、syn::UseTree型で表現されますわ。

let tree: UseTree = input.parse()?;

UseTreeの後ろには様を使わせていただきますわという識別子と.トークンが続く必要がありますわ。識別子はわたくしの時と同じようにチェックいたしますが、少し汎用的にするために以下のような関数を作りますわ。

fn expect_ident(input: ParseStream, name: &str) -> Result<Ident> {
    let ident: Ident = input.parse()?;
    if ident.to_string() == name {
        Ok(ident)
    } else {
        unexpected_ident(&ident)
    }
}

このようにしておきますと、

expect_ident(input, "様を使わせていただきますわ")?;

といたしますことで特定の識別子が来ることを確認できますわ。

続いて、.トークンの確認ですわ。使える文字種の関係でこの位置にを登場させることができないのでその代わりですわね。の場合も代わりに,を使いますわ。わたくしは句読点は「。、」派ではございますけど、論文など「.,」を使って書かれることもありますから問題ありませんわね。

.トークンはsyn::token::Dot型で表されるのですが、synクレート様はsyn::Token!マクロを使ってToken![.]と表現することをご推奨なさっておいでですので、その通りにいたしますわ。

input.parse::<Token![.]>()?;

. トークンがこの位置に存在することが確かめられればよいので、戻り値は使いませんわ。

さて、後はtreeを使ってTokenStreamを作りましょう。TokenStreamの作成には、先ほどテストを書く時にも使ったquote!マクロを使用いたしますわ。
quote!マクロにはmacro_rules!と似たような書き方で、変数に束縛されたToTokensトレイト境界が実装された型を埋め込むことができる機能がございますの。macro_rules!$を使うのに対して、quote!マクロでは#を使うのが大きな違いですわね。

ここでは、

quote!{
    use #tree;
}

と書くことでtreeに束縛されたUseTreeの値をTokenStreamの中に展開できますわ。
ここまでをまとめると、以下のようなコードになりますわ。

use proc_macro2::{Ident, TokenStream};
use quote::quote;
use syn::{
    parse::{ParseStream, Parser},
    Error, Result, Token, UseTree,
};

fn unexpected_ident<T>(ident: &Ident) -> Result<T> {
    Err(Error::new(
        ident.span(),
        format!("予期せぬ識別子ですわ~!: {}", ident),
    ))
}

fn expect_ident(input: ParseStream, name: &str) -> Result<Ident> {
    let ident: Ident = input.parse()?;
    if ident.to_string() == name {
        Ok(ident)
    } else {
        unexpected_ident(&ident)
    }
}

fn セバスチャン_parse(input: ParseStream) -> Result<TokenStream> {
    let ident: Ident = input.parse()?;
    if ident.to_string() == "わたくし" {
        let tree: UseTree = input.parse()?;
        expect_ident(input, "様を使わせていただきますわ")?;
        input.parse::<Token![.]>()?;
        Ok(quote! {
            use #tree;
        })
    } else {
        return unexpected_ident(&ident);
    }
}

fn セバスチャン_impl(tokens: TokenStream) -> TokenStream {
    セバスチャン_parse
        .parse2(tokens)
        .unwrap_or_else(Error::into_compile_error)
}

そしてこの時点でテストが通るようになっているはずですわ。cargo testを実行してテストが通ることを確認いたしましょう。

テストが通りましたら、コミットをいたしますわ。

試しに使用してみますわ。

ここまでくればセバスチャンマクロを使ってみることができるようになりましたわ。
exemples/example1.rsというファイルを作成しまして、Cargo.tomlに以下を追加いたしますわ。

[[example]]
name = "example1"
path = "examples/example1.rs"

examples/example1.rsの中身はこんな感じにいたしますわ。

use reijou::セバスチャン;

セバスチャン! {
    わたくし std::env::args 様を使わせていただきますわ.
}

fn main() {
    for arg in args() {
        println!("{}", arg)
    }
}
use reijou::セバスチャン;

の部分を

セバスチャン! {
    わたくし reijou::セバスチャン 様を使わせていただきますわ.
}

にできないのは少し残念ですわね。でもわたくしがセバスチャンのことを様付けで呼ぶのも少し妙な気がいたしますから、これで良いのかもしれませんわ。

コードの内容的には、単純に受け取った引数を一行ずつ出力するだけですわ。cargo run --example example1で実行できますわ。

また一旦コミットいたしましょう。

機能を増やしますわ。

皆様もここまでで基本的な手続きマクロの作り方を理解できたと思いますが、これだけだと少し機能的に寂しいですわ。
そこで、簡単な関数定義をできるよう機能を拡張しようと思いますわ。

目標は、

セバスチャン! {
    こちらの f 様は,
    a: i32 と b: &str をお受け取りになって,
    std::io::Result<()> をお返しになり,
    以下のことをなさいますのよ. {
        writeln!(std::io::stdout(), "a: {}", a)?;
        writeln!(std::io::stdout(), "b: {}", b)?;
        Ok(())
    }
}

を、

fn f(a: i32, b: &str) -> std::io::Result<()> {
    writeln!(std::io::stdout(), "a: {}", a)?;
    writeln!(std::io::stdout(), "b: {}", b)?;
    Ok(())
}

に置き換えできるようにすることですわ。

テストを追加しますわ。

実装をはじめる前に、テストを追加いたしましょう。テストファーストはRustaceanお嬢様の嗜みですわ。
先ほどと同じように、上で申しましたことをそのままテストの内容にいたしましょう。

#[test]
fn test_fn() {
    assert_eq! {
        セバスチャン_impl(quote!{
            こちらの f 様は,
            a: i32 と b: &str をお受け取りになって,
            std::io::Result<()> をお返しになり,
            以下のことをなさいますのよ. {
                writeln!(std::io::stdout(), "a: {}", a)?;
                writeln!(std::io::stdout(), "b: {}", b)?;
                Ok(())
            }
        }).to_string(),
        quote!{
            fn f(a: i32, b: &str) -> std::io::Result<()> {
                writeln!(std::io::stdout(), "a: {}", a)?;
                writeln!(std::io::stdout(), "b: {}", b)?;
                Ok(())
            }
        }.to_string()
    };
}

そういえば、 先ほどのtest_セバスチャンというテスト名はテストが増えてくるとあまり適切ではなく思えますわね。test_useとでも変更しましょうか。

またテストが失敗するようになりましたが、ここで一旦コミットいたしますわ。

追加機能の実装ですわ。

テストを追加いたしましたので、追加機能を実装していきたいところですが、そうですわね……、ここは皆様も実装してみませんこと?

皆様はhttps://github.com/kazatsuyu/reijouリポジトリのbefore-fnタグから実装をはじめてみてくださいませ。
わたくしの実装結果はこちらのコミットに置いておきますわ。

更なる機能追加もできれば良いですわね。

これで簡単な関数は作れるようになりましたが、まだ機能を追加しようと思えばできますわ。
たとえば、関数本体のブロックは何も手を加えていないので、こちらももっとお嬢様らしくエレガントに記述できるようにする、ですとか、structenumなども定義できるようにする、などですわ。
(とはいえ、まあ正直に言ってしまえばこれはジョークライブラリですので、あまりこだわっても不毛だと思いますわ)

おわりにですわ。

いかがだったかしら?
皆様がこれで手続きマクロを作れるようになりますと、わたくしも嬉しく思いますわ。

最後になりましたが、今回の記事の発想の元になりましたのは、@yaito3014様の以下のツイートのツリーですわ。一部の文言など勝手ながら参考にさせていただきましたの。感謝いたしますわ。

https://twitter.com/yaito3014/status/1539110456753025025

ここまでお読みになって下さりありがとうございますわ。
それでは皆様、ごきげんよう。

脚注
  1. わたくしはお嬢様ですわ。どなたが何をおっしゃろうともお嬢様なのですわ。 ↩︎

  2. もちろん当家の執事の名前ですわ。 ↩︎

  3. 余談ですが、macro_rules!をマクロと言ってしまっていいのか少し疑問がありますわ。通常のマクロ呼び出しとも文法的に異なりますし。とは言え公式様のRust By Exampleでも"macro_rules! macro"と呼んでいらっしゃるのでそれに倣うことにいたしますわ。 ↩︎

  4. 使えるようにするにはCargo.toml[dependencies.serde]features = ["derive"]と書く必要がありますわ。 ↩︎

  5. アイテムはRustの構文要素のカテゴリの一つですわ。クレートの外に公開できるstructenumfnなどが該当しますの。式に付けることはできませんので、ブロックなどに付けることができる#[cfg]よりは制限がありますわね。 ↩︎

  6. 強力な表現力を得られます一方、知らないマクロを見た時に「これはなんですの~!!!」となってしまいますわね。 ↩︎

  7. 'procedural macro API is used outside of a procedural macro'の部分ですわ。 ↩︎

  8. inputにはmutが付いていないのに内部状態が変わるのは少し不思議ですわね。わたくし、そのうち内部構造をきちんと調べてみたいですわ。 ↩︎

Discussion