1
0

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マクロ冬期講習Advent Calendar 2024

Day 20

Rust 属性風マクロを軽くハンズオン

Last updated at Posted at 2024-12-25

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

前回は関数風マクロについて軽いハンズオンを行いました。今回は属性風マクロに取り組みます!

属性風マクロの定義・構造

Cargo.toml に、 [lib] proc-macro = true という項目を追加した上で、 src/lib.rs に、

src/lib.rs
use proc_macro::TokenStream;

#[proc_macro_attribute]
pub fn マクロ名(attr: TokenStream, item: TokenStream) -> TokenStream {
    // ...
}

というように #[proc_macro_attribute] を付けた関数を定義すると、 #[マクロ名(アトリビュート)] みたいに呼び出せる属性風マクロが定義されます。

三種の神器クレート syn quote proc-macro2 を使って入力の attr: proc_macro::TokenStream, item: proc_macro::TokenStream を加工していくことで出力を作ります。

attr#[マクロ名(アトリビュート)] のうちの アトリビュート にあたる部分で、 item がこのマクロを付与した対象(アイテム)になります。

なお、 属性風マクロが何も出力しないと、入力されたアイテムはコードから除かれることになります。その分柔軟な書き方が可能というわけです。

属性風マクロ「 dbg_stmts

本マクロを付与した関数のブロック内にある各文の実行前に、文の内容をデバッグ出力するマクロを作ってみようと思います1

Rust
#[dbg_stmts]
fn hoge() {
    let hoge = 10;
    let fuga = hoge + 20;
    let bar = vec![hoge, fuga];
    println!("{:?}", bar);
}

こちらが以下と同等になるように展開されることを目指します!

Rust
fn hoge() {
    println!("[dbg_stmt] {}", "let hoge = 10;");
    let hoge = 10;
    println!("[dbg_stmt] {}", "let fuga = hoge + 20;");
    let fuga = hoge + 20;
    println!("[dbg_stmt] {}", "let bar = vec![hoge, fuga];");
    let bar = vec![hoge, fuga];
    println!("[dbg_stmt] {}", r#"println!("{:?}", bar);"#);
    println!("{:?}", bar);
}

なお、属性風マクロにオプションを渡せるようにします!

例: #[dbg_stmts(sep = "===")]

オプション 機能
sep 出力行間にセパレータを出力する設定。デフォルトでは出力なし
fmt 各行の println! に渡すフォーマットの指定

今回想定しているマクロの最終的な使用方法全体像です。

Rust
#[dbg_stmts(
    sep = "セパレータ",
    fmt = "カスタムフォーマット: {}",
)]
fn hoge() {
    文1;
    文2;
    ...
    文n;
    ...
}

// ↓ 展開

fn hoge() {
    println!("カスタムフォーマット: {}", "文1");
    println!("セパレータ");
    文1;
    println!("セパレータ");
    println!("カスタムフォーマット: {}", "文2");
    println!("セパレータ");
    文2;
    println!("セパレータ");

    ...

    println!("カスタムフォーマット: {}", "文n");
    println!("セパレータ"); // 文n 開始前のセパレータ
    文n; // ここでの標準出力はセパレータで囲まれる
    println!("セパレータ"); // 文n 実行後のセパレータ

    ...
}

手順 1. 骨組みを作る

RTAと同様に準備します!

cargo new --lib dbg_stmts
Cargo.toml
[package]
name = "dbg_stmts"
version = "0.1.0"
edition = "2021"

+[lib]
+proc-macro = true

[dependencies]

前回同様、 src/lib.rs はシンプルに保ち実装は src/impls.rs で行うディレクトリ構造にします。

ディレクトリ構造
.
├── Cargo.lock
├── Cargo.toml
├── src
│   ├── impls.rs
│   ├── lib.rs
│   └── main.rs # マクロの動作確認用
└── target

ここまでは前回の関数風マクロとほとんど同じですが、マクロの宣言方法のみ異なります。

src/lib.rs
mod impls;

use proc_macro::TokenStream;

#[proc_macro_attribute]
pub fn dbg_stmts(attr: TokenStream, item: TokenStream) -> TokenStream {
    todo!()
}

関数風マクロは引数一つでしたが、属性風マクロは「属性部分(#[macro(...)])」と「アイテム部分(fn hoge() {...}の部分)」の2つの引数を持ちます!ただし今回アイテム部分は関数のみ受け取ることにします。

続き。三種の神器を cargo add しておきます。

cargo add proc-macro2 quote
cargo add syn --features full --features extra-traits

手順 2. 入力をパースする

属性部分は追加機能なので後ほど取り組むとして、目的の機能である行間出力を先に実装します!

Rust
#[dbg_stmts]
fn hoge() {
    ;
    ;
    ...
}

アイテム部分の inputsyn::ItemFn として受け取れればそれでよいので、前回と異なり入力用の構造体は特に設けないことにします。(後で属性部分をパースするために結局用意しますが...)

Rust
let func = parse_macro_input!(input as ItemFn);

一旦関数をそのまま出力するように書いておきます。

src/lib.rs
mod impls;

use proc_macro::TokenStream;
use quote::ToTokens;
use syn::{parse_macro_input, ItemFn};

#[proc_macro_attribute]
pub fn dbg_stmts(_attr: TokenStream, item: TokenStream) -> TokenStream {
    let func = parse_macro_input!(item as ItemFn);

    // 行間出力を加える処理をここで行う

    func.into_token_stream().into()
}

手順 3. 出力を考える

syn::ItemFn を受け取り、 syn::ItemFn を出力する、でも良いのですが、 可変参照を受け取り中身を書き換えるほうが( Span の再設定が要らないなど)色々と楽なので、今回は後者で行こうと思います!

改変対象である syn::ItemFn は次のような定義になっています。

Rust
pub struct ItemFn {
    pub attrs: Vec<Attribute>,
    pub vis: Visibility,
    pub sig: Signature,
    pub block: Box<Block>,
}
フィールド名 説明
attrs Vec<Attribute> 本マクロ以外に関数に付与されている属性情報
vis Visibility pubpub(crate) などの可視性
sig Signature async fn func(arg: T) -> T みたいな関数のシグネチャ部分
block Box<Block> {} の中身。関数の処理内容。 今回の改変対象

そして改変対象の syn::Block の定義は以下です。

Rust
pub struct Block {
    pub brace_token: Brace,
    pub stmts: Vec<Stmt>,
}
フィールド名 説明
brace_token Brace {} のカッコの部分。主にもとの Span を保存する目的で設けられています(多分)。
stmts Vec<Stmt> ブロックを構成する文のリストです。 今回の改変対象

&mut func.block&mut Block を得て、 stmts に新しい Vec<Stmt> を入れ直す形で改変します。書き下す処理のイメージです。

src/impls.rs
use syn::{parse_quote, Block};

// insert_println_between_stmts(&mut func.block) として呼び出す

pub fn insert_println_between_stmts(block: &mut Block) {
    block.stmts = /* Vec<Stmt>書き換え */;
}

では早速いよいよ(?) /* Vec<Stmt>書き換え */ の部分を実装していきます。

Rust
block.stmts = block
    .stmts
    .iter()
    // .map系処理で Stmt ごとに新たなパーツを生成
    .collect();

map系処理の部分で .map(|stmt| stmtからTokenStream生成) というように書いても良いのですが、それでは最終的な結果は Vec<Stmt> ではなく TokenStream になってしまいます。

そこでクロージャ部分は |stmt| vec![追加のデバッグ出力文, stmt] となるようにし、二重配列になるので flat_map を使うことで Vec<Stmt> でコレクトするようにします。

Rust
block.stmts = block
    .stmts
    .iter()
    .flat_map(|stmt| {
        vec![
            /* 追加のデバッグ出力文 */,
            stmt.clone(),
        ]
    })
    .collect();

あとは /* 追加のデバッグ出力文 */ に出力したいデバッグ文を書くだけです!

ここは宣言マクロに似た感じでそのままテンプレートを書きたいので、 syn::parse_quote! マクロを利用します。このマクロの結果は TokenStream ではなく型推論の上で syn::Stmt になります。

Rust
parse_quote! { println!("[dbg_stmts] {}", stringify!(#stmt)); },

parse_quote! の中身の println! では、デバッグ出力したい文部分を stringify! マクロで囲んでいます。トークン木を渡して出力させるとダブルクォート " で囲んでそのまま文字列にしてくれるとても便利なマクロです!

ここまでで全体像は次のようになります。

src/impls.rs
use syn::{parse_quote, Block};

pub fn insert_println_between_stmts(block: &mut Block) {
    block.stmts = block
        .stmts
        .iter()
        .flat_map(|stmt| {
            vec![
                parse_quote! { println!("[dbg_stmts] {}", stringify!(#stmt)); },
                stmt.clone(),
            ]
        })
        .collect();
}
src/lib.rs
mod impls;

use proc_macro::TokenStream;
use quote::ToTokens;
use syn::{parse_macro_input, ItemFn};

use impls::insert_println_between_stmts;

#[proc_macro_attribute]
pub fn dbg_stmts(_attr: TokenStream, item: TokenStream) -> TokenStream {
    let mut func = parse_macro_input!(item as ItemFn);

    insert_println_between_stmts(&mut func.block);

    func.into_token_stream().into()
}

呼び出し側は次のように呼び出せます。

src/main.rs
use dbg_stmts::dbg_stmts;

#[dbg_stmts]
#[allow(unused)]
fn hoge() -> u32 {
    let hoge = 10;
    let fuga = hoge + 20;
    let bar = vec![hoge, fuga];
    println!("{:?}", bar);
    100
}

fn main() {
    let _ = hoge();
}

cargo expand した結果は次のようになり、

cargo expand
$ cargo expand --bin dbg_stmts
    Checking dbg_stmts v0.1.0 (/path/to/dbg_stmts)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.07s

#![feature(prelude_import)]
#[prelude_import]
use std::prelude::rust_2021::*;
#[macro_use]
extern crate std;
use dbg_stmts::dbg_stmts;
#[allow(unused)]
fn hoge() -> u32 {
    {
        ::std::io::_print(format_args!("[dbg_stmts] {0}\n", "let hoge = 10;"));
    };
    let hoge = 10;
    {
        ::std::io::_print(format_args!("[dbg_stmts] {0}\n", "let fuga = hoge + 20;"));
    };
    let fuga = hoge + 20;
    {
        ::std::io::_print(
            format_args!("[dbg_stmts] {0}\n", "let bar = vec! [hoge, fuga];"),
        );
    };
    let bar = <[_]>::into_vec(#[rustc_box] ::alloc::boxed::Box::new([hoge, fuga]));
    {
        ::std::io::_print(
            format_args!("[dbg_stmts] {0}\n", "println! (\"{:?}\", bar);"),
        );
    };
    {
        ::std::io::_print(format_args!("{0:?}\n", bar));
    };
    {
        ::std::io::_print(format_args!("[dbg_stmts] {0}\n", "100"));
    };
    100
}
fn main() {
    let _ = hoge();
}

出力結果は次のとおりになりました。とりあえず基本機能は完成したようです!

出力結果
$ cargo run -q
[dbg_stmts] let hoge = 10;
[dbg_stmts] let fuga = hoge + 20;
[dbg_stmts] let bar = vec! [hoge, fuga];
[dbg_stmts] println! ("{:?}", bar);
[10, 30]
[dbg_stmts] 100

手順 4. 属性(アトリビュート)にあるオプションの実装

基本機能ができたので、ここからは sepfmt といった属性オプション 2 を受け取りマクロの出力結果を調整する機能を作りこんでいきます!

Rust
#[dbg_stmts(
    // ↓ 受け取れるようにする
    sep = "セパレータ",
    fmt = "カスタムフォーマット: {}",
)]
fn hoge() {
    文1;
    文2;
    ...
}

第一引数の attr: TokenStream で属性オプション部分を受け取れます。 dbg! マクロを利用してどんなトークン木を受け取れているか確認してみます。

Rust
#[proc_macro_attribute]
pub fn dbg_stmts(attr: TokenStream, /* <- 属性部分の入力! */ item: TokenStream) -> TokenStream {
+    dbg!(&attr);

    let mut func = parse_macro_input!(item as ItemFn);

    insert_println_between_stmts(&mut func.block);

    func.into_token_stream().into()
}
[src/lib.rs:11:5] &attr = TokenStream [
    Ident {
        ident: "sep",
        span: #0 bytes(86..89),
    },
    Punct {
        ch: '=',
        spacing: Alone,
        span: #0 bytes(90..91),
    },
    Literal {
        kind: Str,
        symbol: "セパレータ",
        suffix: None,
        span: #0 bytes(92..109),
    },
    Punct {
        ch: ',',
        spacing: Alone,
        span: #0 bytes(109..110),
    },
    Ident {
        ident: "fmt",
        span: #0 bytes(115..118),
    },
    Punct {
        ch: '=',
        spacing: Alone,
        span: #0 bytes(119..120),
    },
    Literal {
        kind: Str,
        symbol: "カスタムフォーマット: {}",
        suffix: None,
        span: #0 bytes(121..157),
    },
    Punct {
        ch: ',',
        spacing: Alone,
        span: #0 bytes(157..158),
    },
]

#[dbg_stmts( ... )]... にあたる部分をそのままトークン木として受け取れています。これをこちらで用意した以下の構造体にパースできるようにします。

Rust
#[derive(Debug)]
pub struct DbgStmtsOption {
    pub sep: Option<LitStr>,
    pub fmt: LitStr,
}

セパレータ sepOption 型にし、 Some の時だけ出力することにします。出力フォーマット fmt も指定に関してはオプションですが、出力には必ず必要なものなので unwrap_or を利用して得ることにします。

parse_macro_input!(attr as DbgStmtsOption) として DbgStmtsOption を得たいので、 syn::ParseDbgStmtsOption にimplします。

src/impls.rs
impl Parse for DbgStmtsOption {
    fn parse(input: ParseStream) -> Result<Self> {
        todo!()
    }
}

次の方針で実装していきます。

  • 1: , で区切られて sep = "..."fmt = "..." が入ってきます。そのため parse_terminated を利用して MetaNameValue でイイカンジに受けます
  • 2: 得た MetaNameValue ごとにその value"..." 部分を得て、必要なものを保存します
    • 2.1: 各 value について parse_quote! マクロを使うことで LitStr として "..." を得ます
      • この方法だとパースに失敗した場合(例えば sep = "..."sep = 0 みたいに間違っている場合)にマクロがパニックしてしまうので、発展で丁寧なエラーハンドリングに直します
    • 2.2: sepfmt の時にそれぞれの変数に LitStr 型の値を保存します
  • 3: DbgStmtsOption 構造体にキャプチャしたオプションをまとめて、返します
    • 3.1: unwrap_or を利用し fmt 未指定の時は規定値として LitStr::new("[dbg_stmt] {}", Span::mixed_site()) を作り返します
src/impls.rs
use proc_macro2::Span;
use syn::{
    parse::{Parse, ParseStream},
    parse_quote, Block, Error, Expr, ExprLit, Lit, LitStr, MetaNameValue, Result, Token,
};

// ...省略...

impl Parse for DbgStmtsOption {
    fn parse(input: ParseStream) -> Result<Self> {
        // 1
        // path1 = "value1", path2 = "value2", ... を vec![path = "value"] のようなイテレータとして扱いたい
        let meta_name_value_list = input.parse_terminated(MetaNameValue::parse, Token![,])?;

        // 2
        let mut sep = None;
        let mut fmt = None;
        // path = "value" を一つずつ
        for MetaNameValue { path, value, .. } in meta_name_value_list {
            // 2.1
            let lit: LitStr = parse_quote!( #value ); // "value" のパース

            // 2.2
            match path {
                p if p.is_ident("sep") => sep = Some(lit),
                p if p.is_ident("fmt") => fmt = Some(lit),
                _ => (),
            }
        }

        // 3
        Ok(Self {
            sep,
            // 3.1
            fmt: fmt.unwrap_or(LitStr::new("[dbg_stmt] {}", Span::mixed_site())),
        })
    }
}

insert_println_between_stmts も大幅に書き換えていきます。

  • 1: DbgStmtsOption を受け取れるようにします
    • かっこつけて分解構文を使ってこの時点で sepfmt に分けています
  • 2: 関数の最後が式で終わっている場合最後のセパレータを出力すると式の評価値を返せなくなります。これを防止するため、 .peekable() メソッドで最後かどうかを確認して最後の文の後にはセパレータを出力しないようにします
    • 2.1: 伴い、flat_map を使用していた箇所で while let を使うようにしています
    • 2.2, 2.3: sepSome の時だけセパレータを出力するようにしています
    • 2.3: 最後でない( stmts.peek().is_some() が真の)時だけセパレータを出力するようにしています
  • 3: println!("[dbg_stmts] {}", stringify!(#stmt)); とハードコードしていた箇所について、println!(#fmt, stringify!(#stmt)); として fmt を設定するようにしています
    • fmtLitStr なのでダブルクォートで囲む必要はありません
src/impls.rs
use proc_macro2::Span;
use syn::{
    parse::{Parse, ParseStream},
    parse_quote, Block, Error, Expr, ExprLit, Lit, LitStr, MetaNameValue, Result, Token,
};

pub fn insert_println_between_stmts(
    block: &mut Block,
    // 1
    DbgStmtsOption { sep, fmt }: DbgStmtsOption,
) {
    // 2
    let mut stmts = block.stmts.iter().peekable();

    let mut res = Vec::new();

    // 2.1
    while let Some(stmt) = stmts.next() {
        let mut v = Vec::with_capacity(4);

        // 3
        v.push(parse_quote! { println!(#fmt, stringify!(#stmt)); });

        // 2.2 sepオプション指定時だけ設定
        if let Some(sep) = &sep {
            v.push(parse_quote! { println!(#sep); });
        }

        v.push(stmt.clone());

        // 2.3 sepオプション指定時だけ&末尾以外で設定
        if let (true, Some(sep)) = (stmts.peek().is_some(), &sep) {
            v.push(parse_quote! { println!(#sep); });
        }

        res.extend(v);
    }

    block.stmts = res;
}

// ...省略...

最後に呼び出し側の src/lib.rs も修正します。

src/lib.rs
mod impls;

use proc_macro::TokenStream;
use quote::ToTokens;
use syn::{parse_macro_input, ItemFn};

use impls::{insert_println_between_stmts, DbgStmtsOption};

#[proc_macro_attribute]
pub fn dbg_stmts(attr: TokenStream, item: TokenStream) -> TokenStream {
    let mut func = parse_macro_input!(item as ItemFn);
    let option = parse_macro_input!(attr as DbgStmtsOption);

    insert_println_between_stmts(&mut func.block, option);

    func.into_token_stream().into()
}

これでオプションを受け取れるようになりました!

src/main.rs
use dbg_stmts::dbg_stmts;

#[dbg_stmts(
    sep = "セパレータ",
    fmt = "カスタムフォーマット: {}",
)]
#[allow(unused)]
fn hoge() -> u32 {
    let hoge = 10;
    let fuga = hoge + 20;
    let bar = vec![hoge, fuga];
    println!("{:?}", bar);
    100
}

fn main() {
    let _ = hoge();
}

cargo expand の結果は長いので今回は実行時標準出力のみ確認しておきます。

$ cargo run -q
カスタムフォーマット: let hoge = 10;
セパレータ
セパレータ
カスタムフォーマット: let fuga = hoge + 20;
セパレータ
セパレータ
カスタムフォーマット: let bar = vec! [hoge, fuga];
セパレータ
セパレータ
カスタムフォーマット: println! ("{:?}", bar);
セパレータ
[10, 30]
セパレータ
カスタムフォーマット: 100
セパレータ

[10, 30] の出力や最後のセパレータが1つない等より想定通りであることを確認できました!

発展 正しいエラーハンドリング

ParseDbgStmtsOption に実装する際、 "..." 部分のパースについて記述が煩雑になる関係で 説明が面倒だったので 一旦 parse_quote を利用して LitStr に変換していました。

src/impls.rs
impl Parse for DbgStmtsOption {
    fn parse(input: ParseStream) -> Result<Self> {
        // ...省略...

        // path1 = "value1", path2 = "value2", ...
        for MetaNameValue { path, value, .. } in meta_name_value_list {
            let lit: LitStr = parse_quote!( #value ); // "value" のパース

            // ...省略...
        }

        // ...省略...
    }
}

しかし parse_quote変換が失敗しないことを前提にしたマクロで、外からの入力をそのまま渡す目的のものではありません。

parse_quote でのパニックに限らず、マクロがパニックすると、以下の画像のようにマクロに渡したアイテム全体に赤線が引かれてしまいます。

コンパイルエラー.png

詳細は Rust手続きマクロ エラーハンドリング手法 にて解説していますが、 Parse トレイトの parse メソッドに関してはパニックさせず syn::Result を返すようにすれば適切なエラーハンドリングができるようになります。

src/impls.rs
use proc_macro2::Span;
use syn::{
    parse::{Parse, ParseStream},
    parse_quote, Block, Error, Expr, ExprLit, Lit, LitStr, MetaNameValue, Result, Token,
};

impl Parse for DbgStmtsOption {
    fn parse(input: ParseStream) -> Result<Self> {
        let meta_name_value_list = input.parse_terminated(MetaNameValue::parse, Token![,])?;

        let mut sep = None;
        let mut fmt = None;
        for MetaNameValue { path, value, .. } in meta_name_value_list {
-            let lit: LitStr = parse_quote!( #value );
+            let Expr::Lit(ExprLit {
+                lit: Lit::Str(lit), ..
+            }) = value
+            else {
+                return Err(Error::new_spanned(value, "expected string literal"));
+            };

            match path {
                p if p.is_ident("sep") => sep = Some(lit),
                p if p.is_ident("fmt") => fmt = Some(lit),
                _ => (),
            }
        }

        Ok(Self {
            sep,
            fmt: fmt.unwrap_or(LitStr::new("[dbg_stmt] {}", Span::mixed_site())),
        })
    }
}

詳細を解説します。 MetaNameValue で分解した value は今列挙体 syn::Expr になっています。ここから内側の値をパターンマッチで取っていき、 LitStr までマッチさせて変数 lit に代入させています。

列挙体なので論駁可能であり、そのため let-else 文を利用しています。失敗した時には syn::Error を返します。

Rust
let Expr::Lit(ExprLit {
   lit: Lit::Str(lit), ..
}) = value
else {
   return Err(Error::new_spanned(value, "expected string literal"));
};

Error::new_spanned の部分が今回のミソで、マクロがエラーとなった原因箇所を赤下線でハイライトさせるために、エラーの該当箇所の情報( proc_macro::Span )を持つ value を第一引数に渡しています。

これで、マクロはパニックせずコンパイルエラー要因となる箇所を正しく提示できるようになります!

コンパイルエラー2.png

まとめ・所感

最終的な全体像は次の通りになります!

src/lib.rs
mod impls;

use proc_macro::TokenStream;
use quote::ToTokens;
use syn::{parse_macro_input, ItemFn};

use impls::{insert_println_between_stmts, DbgStmtsOption};

#[proc_macro_attribute]
pub fn dbg_stmts(attr: TokenStream, item: TokenStream) -> TokenStream {
    let mut func = parse_macro_input!(item as ItemFn);
    let option = parse_macro_input!(attr as DbgStmtsOption);

    insert_println_between_stmts(&mut func.block, option);

    func.into_token_stream().into()
}
src/impls.rs
use proc_macro2::Span;
use syn::{
    parse::{Parse, ParseStream},
    parse_quote, Block, Error, Expr, ExprLit, Lit, LitStr, MetaNameValue, Result, Token,
};

pub fn insert_println_between_stmts(
    block: &mut Block,
    DbgStmtsOption { sep, fmt }: DbgStmtsOption,
) {
    let mut stmts = block.stmts.iter().peekable();

    let mut res = Vec::new();

    while let Some(stmt) = stmts.next() {
        let mut v = Vec::with_capacity(4);

        v.push(parse_quote! { println!(#fmt, stringify!(#stmt)); });

        if let Some(sep) = &sep {
            v.push(parse_quote! { println!(#sep); });
        }

        v.push(stmt.clone());

        if let (true, Some(sep)) = (stmts.peek().is_some(), &sep) {
            v.push(parse_quote! { println!(#sep); });
        }

        res.extend(v);
    }

    block.stmts = res;
}

#[derive(Debug)]
pub struct DbgStmtsOption {
    pub sep: Option<LitStr>,
    pub fmt: LitStr,
}

impl Parse for DbgStmtsOption {
    fn parse(input: ParseStream) -> Result<Self> {
        let meta_name_value_list = input.parse_terminated(MetaNameValue::parse, Token![,])?;

        let mut sep = None;
        let mut fmt = None;
        for MetaNameValue { path, value, .. } in meta_name_value_list {
            let Expr::Lit(ExprLit {
                lit: Lit::Str(lit), ..
            }) = value
            else {
                return Err(Error::new_spanned(value, "expected string literal"));
            };

            match path {
                p if p.is_ident("sep") => sep = Some(lit),
                p if p.is_ident("fmt") => fmt = Some(lit),
                _ => (),
            }
        }

        Ok(Self {
            sep,
            fmt: fmt.unwrap_or(LitStr::new("[dbg_stmt] {}", Span::mixed_site())),
        })
    }
}
src/main.rs
use dbg_stmts::dbg_stmts;

#[dbg_stmts(sep = "=========", fmt = "### {} ###")]
#[allow(unused)]
fn hoge() -> u32 {
    let hoge = 10;
    let fuga = hoge + 20;
    let bar = vec![hoge, fuga];
    println!("{:?}", bar);
    100
}

fn main() {
    let _ = hoge();
}

そして cargo expand の結果は次のようになり(長いので折りたたんでおきました)、

cargo expand
cargo expand
$ cargo expand --bin dbg_stmts
    Checking dbg_stmts v0.1.0 (/path/to/dbg_stmts)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.07s

#![feature(prelude_import)]
#[prelude_import]
use std::prelude::rust_2021::*;
#[macro_use]
extern crate std;
use dbg_stmts::dbg_stmts;
#[allow(unused)]
fn hoge() -> u32 {
    {
        ::std::io::_print(format_args!("### {0} ###\n", "let hoge = 10;"));
    };
    {
        ::std::io::_print(format_args!("=========\n"));
    };
    let hoge = 10;
    {
        ::std::io::_print(format_args!("=========\n"));
    };
    {
        ::std::io::_print(format_args!("### {0} ###\n", "let fuga = hoge + 20;"));
    };
    {
        ::std::io::_print(format_args!("=========\n"));
    };
    let fuga = hoge + 20;
    {
        ::std::io::_print(format_args!("=========\n"));
    };
    {
        ::std::io::_print(format_args!("### {0} ###\n", "let bar = vec! [hoge, fuga];"));
    };
    {
        ::std::io::_print(format_args!("=========\n"));
    };
    let bar = <[_]>::into_vec(#[rustc_box] ::alloc::boxed::Box::new([hoge, fuga]));
    {
        ::std::io::_print(format_args!("=========\n"));
    };
    {
        ::std::io::_print(format_args!("### {0} ###\n", "println! (\"{:?}\", bar);"));
    };
    {
        ::std::io::_print(format_args!("=========\n"));
    };
    {
        ::std::io::_print(format_args!("{0:?}\n", bar));
    };
    {
        ::std::io::_print(format_args!("=========\n"));
    };
    {
        ::std::io::_print(format_args!("### {0} ###\n", "100"));
    };
    {
        ::std::io::_print(format_args!("=========\n"));
    };
    100
}
fn main() {
    let _ = hoge();
}

実行結果では次のとおり次に実行される文を確認できるようになりました!

実行結果
$ cargo run -q
### let hoge = 10; ###
=========
=========
### let fuga = hoge + 20; ###
=========
=========
### let bar = vec! [hoge, fuga]; ###
=========
=========
### println! ("{:?}", bar); ###
=========
[10, 30]
=========
### 100 ###
=========

関数マクロと同様に、

  • syn クレートを利用して入力をパース
  • 入力となる関数 func のフィールドを書き換えることで結果を生成する
  • 属性オプション部分も同じようにパースして利用する
  • syn::Error を使った正しいエラーハンドリングをする

という手順を解説し、文の間にデバッグ出力を行う属性風マクロを作りました!

アイテムにしか付けられないという制約はありますが、今回見せたような、受け取ったアイテムの可読性を維持しつつちょっとした記述を追加するという場合に、属性風マクロは重宝しますね!

次回は属性風マクロと似ているのだけども雰囲気が違うderiveマクロに取り組みます!

  1. println! マクロを利用しているので、 print_stmts のほうが妥当だったかもしれないですね...もともとは dbg! マクロを使おうかなと思っていた名残でこのマクロ名になりました。

  2. この部分を属性やアトリビュートと呼んでしまうと #[dbg_stmts] の部分はなんなんだとなってしまうためこのような呼称にしました。フラグメント指定子では meta とされるのでメタ部分と呼んでもよさそうですが、「属性オプション」の方が伝わりやすいと思います。

1
0
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
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?