2
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】好きな位置に好きな属性(#[attr])を付ける方法【ハンズオン付】

Posted at

要は↓以下に示すコードみたいなことができるか気になってますね?カスタムの属性アトリビュートでは できません

Rust (当然コンパイルエラー)
fn run(input: Vec<&'static str>) {
    let input_1 = input.clone();

    let mut res_1 = Vec::new();

    #[hoge] // <- なんかよくわからない属性つけたい!
    for item in input_1 {
        if let Ok(n) = item.parse::<usize>() {
            res_1.push(n);
        }
    }

    #[hoge::fuga] // <- なんかよくわからない属性つけたい!
    let res_2: Vec<_> = input
        .into_iter()
        .filter_map(|n| n.parse::<usize>().ok())
        .collect();
        
    assert_eq!(res_1, res_2);
}

関数の頭なら#[tokio::main]のように 属性マクロ を付けることができます。あるいは #[cfg(feature = ...)] のような組み込みの属性ならばある程度の制約はありますが色々な位置につけることができます。

しかしながら、 好きな位置好きな属性 を置けるわけないじゃないですか :sweat_smile: そんなの聞いたことがありません。

...
...
...
...

ほんとだもん!本当に属性あったんだもん!ウソじゃないもん!

1?好きな位置に好きな属性を挿入している例を見たことがあるって...?どこで?

rstestでの例

rstestはパラメータ化テストを楽に行うためのクレートおよびその 属性マクロ です。

7行目の #[files("files/*.txt")] path: PathBuf が見慣れない記述です!こんな位置での 謎の属性 #[files("files/*.txt")] はRustの文法上許されないはず ですが、ここではそれが許されています。

hooqでの例

hooq? 演算子と式の間にメソッドをフックしてくれるクレートおよびその 属性マクロ です。

4行目の #[hooq::method(...)] も謎ですし、何より 8行目に #[hooq::skip_all] という謎の属性 がついています。Rustの文法上こんなところに属性をつけることはできません。


両者の共通点はなんでしょうか?そうです、 属性マクロ がアイテムの先頭についています!

属性マクロが付与されたアイテム内では好きな位置に好きな属性を付ける方法があります。マクロの助けを借りることで付与できるこのような属性を 不活性属性 (inert attributes)といいます。

より正確に言えば不活性属性は「属性処理の結果自分自身を取り除かない属性」です。言い換えると誰かに取り除いてもらう必要がある = マクロの助けを借りる必要がある、という感じです。 最終的にマクロが取り除いてくれる場合に限り 、不活性属性はRustの文法上も合法2なのです!

ちなみに詳しい人ならderiveマクロには同様の機構(不活性属性)がすでについていることを知っているかもしれません。有名な例はthiserrorです。

Rust
#[derive(thiserror::Error, Debug)] // deriveマクロ
enum MyError {
    #[error("Parse Error: {0}")] // 以下、errorやfromは不活性属性
    ParseError(String),
    #[error("DB Error: {0}")]
    DbError(String),
    #[error("Something Wrong: {0}")]
    Other(#[from] anyhow::Error),
}

不活性属性を伴うderiveマクロの作成方法は拙著などを参照いただけると幸いです :bow:

しかし、 属性マクロで不活性属性を利用する 手段は明示されたドキュメントを見たことがありません!ということで今回の記事でまとめてみました。

真タイトル: 属性マクロで不活性属性を活用する

というわけで、改めましてこんにチュア!本記事は hooqアドベントカレンダー 13日目の記事です!

hooqはメソッドを ? 演算子にフックする属性マクロです。

冒頭の一例で紹介した通り、 #[hooq] 属性マクロは付与対象の中で #[hooq::skip_all] を始めとした不活性属性を扱うことができます!

Rust
use hooq::hooq;

#[hooq]
#[hooq::method(.inspect_err(|_: &String| {}))]
fn main() -> Result<(), String> {
    let n = Ok(42)?;

    // Option型向けの?ではフックせずスキップする
    #[hooq::skip_all]
    let f = |n: Option<usize>| {
        let n = n?;

        Some(n)
    };

    let _res = f(Some(n)).ok_or_else(|| "None!".to_string())?;

    Ok(())
}

この不活性属性 #[hooq::skip_all] が付与できるというのは地味なこだわりです。というのも例の場合に限らずコンパイルエラーを抑えるためにフックを行いたくないシーンは頻出と考えられ、あった方が確実に良い機能であるためです3

hooq実装時に得た属性マクロでの不活性属性の取り回し方4を、本記事にてハンズオンで紹介します!

時間測定属性マクロ timing ハンズオン!

他言語でも見たことあるような、時間測定用にラップされた関数を用意してくれる属性マクロを作ってみます!

Rust
#[timing(run_with_time)]
fn run(input: Vec<&'static str>) {
    let input_1 = input.clone();

    let mut res_1 = Vec::new();

    #[timing::span]
    for item in input_1 {
        if let Ok(n) = item.parse::<usize>() {
            res_1.push(n);
        }
    }

    #[timing::span]
    let res_2: Vec<_> = input
        .into_iter()
        .filter_map(|n| n.parse::<usize>().ok())
        .collect();

    assert_eq!(res_1, res_2);
}

次の意味で属性マクロ/不活性属性を付けました!

  • #[timing(run_with_time)]: 属性マクロ本体。 (run_with_time) のようにすることで、時間測定用のコードを挿入したバージョンの関数の名前を指定可能にする
  • #[timing::span]: 属性マクロにより取り除く必要がある不活性属性。この不活性属性がついている文の前後で時間を取得し、この部分でかかっている時間を表示

展開後は以下のような関数が生成されることを目指します!(行番号は仮です)

Rust
fn run_with_time(input: Vec<&'static str>) {
    let now = ::std::time::Instant::now();

    let input_1 = input.clone();

    let mut res_1 = Vec::new();

    let span_start_time = now.elapsed();
    for item in input_1 {
        if let Ok(n) = item.parse::<usize>() {
            res_1.push(n);
        }
    }
    let span_end_time = now.elapsed();
    println!(
        "[{}:{}]\n\tstart:\t{:>12?}\n\tend:\t{:>12?}\n\ttime:\t{:>12?}", "src/main.rs",
        8_usize,
        span_start_time,
        span_end_time,
        span_end_time - span_start_time,
    );

    let span_start_time = now.elapsed();
    let res_2: Vec<_> = input
        .into_iter()
        .filter_map(|n| n.parse::<usize>().ok())
        .collect();
    let span_end_time = now.elapsed();
    println!(
        "[{}:{}]\n\tstart:\t{:>12?}\n\tend:\t{:>12?}\n\ttime:\t{:>12?}", "src/main.rs",
        15_usize,
        span_start_time,
        span_end_time,
        span_end_time - span_start_time,
    );
    
    assert_eq!(res_1, res_2);

    println!("[{}:{}]\n\ttotal:\t{:>12?}", "src/main.rs", 20_usize, now.elapsed());
    // ↑ 最後の式が () ではない場合も考えると変わる可能性あり
}

Playground: https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=662e8e9c3bf82bfd27f4832a7746c863

実行結果例
[src/main.rs:8]
	start:	      5.42µs
	end:	      7.73µs
	time:	      2.31µs
[src/main.rs:15]
	start:	    36.152µs
	end:	    37.352µs
	time:	       1.2µs
[src/main.rs:20]
	total:	    43.212µs

この手のマクロはいくらあっても良いですからね、作ってみましょう!

ハンズオンの完成品を予めおいておきます↓

本文中を読んでよくわからない点があればこちら参照ください。

属性マクロ骨組み作成

いつも通り属性マクロを作成しましょう。手順の説明と入力部分の解説だけし、その他の詳細は拙著に代えさせていただきます :bow:

まずはライブラリクレートを作成します。ついでに三種の神器も入れます。

cargo new timing-macro --lib
cd timing-macro
cargo add quote proc-macro2
cargo add syn --features full --features extra-traits --features visit-mut

synクレートで extra-traits featureと visit-mut featureも入れている点に注意です。前者はデバッグ用、後者はトラバーサルを楽にする目的です。トラバーサルについては後ほど説明します。

Cargo.toml にマクロクレートであることを示すべく [lib] proc-macro = true を入れます。

Cargo.toml
[package]
name = "timing-macro"
version = "0.1.0"
edition = "2024"

+[lib]
+proc-macro = true

[dependencies]
proc-macro2 = "1.0.103"
quote = "1.0.42"
syn = { version = "2.0.111", features = ["extra-traits", "full", "visit-mut"] }

src/lib.rs にマクロAPIの骨組みを用意します。

src/lib.rs
use proc_macro::TokenStream;

#[proc_macro_attribute]
pub fn timing(_attr: TokenStream, _item: TokenStream) -> TokenStream {
    todo!()
}

src/impls.rs に実装の骨組みを用意します5

src/impls.rs
use proc_macro2::TokenStream;

pub fn timing(_timing_fn_name: syn::Ident, _original_fn: syn::ItemFn) -> TokenStream {
    todo!()
}

用意した impls::timing を src/lib.rs で利用するようにします。ここでマクロへの入力のパースも済ませてしまいます。

src/lib.rs
use proc_macro::TokenStream;

mod impls;

#[proc_macro_attribute]
pub fn timing(attr: TokenStream, item: TokenStream) -> TokenStream {
    let timing_fn_name = syn::parse_macro_input!(attr as syn::Ident);
    let original_fn = syn::parse_macro_input!(item as syn::ItemFn);

    impls::timing(timing_fn_name, original_fn).into()
}

attr には (run_with_time)run_with_time が入ってきます。今回これは関数名として取り扱いたいので syn::Ident へとパースさせます。

item には関数本体が入ってきます。今回このアイテムは関数以外の対象を取らないこととするため syn::ItemFn へとパースさせます。

impls::timing には追加で出力する関数の名と、関数本体を渡します。

属性マクロ全体の流れ

impls::timing の中を実装していきます!まずは処理の流れを考えます。

  • 「元の関数(以降 original_fn )」と「時間経過表示機能を付けた関数(以降 timing_fn )」の分として、関数本体を複製します。
    • ここで timing_fn の名前を変更します。
  • original_fn からは単に不活性属性を取り除きます。
  • timing_fn では不活性属性情報を元に時間測定用の処理を挿入します。
  • 両者を合わせて出力します。

これをこのまま以下のように書き起こします。

src/impls.rs
use proc_macro2::TokenStream;

pub fn timing(timing_fn_name: syn::Ident, mut original_fn: syn::ItemFn) -> TokenStream {
    let mut timing_fn = original_fn.clone();
    timing_fn.sig.ident = timing_fn_name;

    rid_inert_attributes_from_stmts(&mut original_fn);
    insert_timing(&mut timing_fn);

    quote::quote! {
        #timing_fn

        #original_fn
    }
}

fn rid_inert_attributes_from_stmts(_f: &mut syn::ItemFn) {
    todo!()
}

fn insert_timing(_f: &mut syn::ItemFn) {
    todo!()
}

残りの todo!() を埋めていきましょう!ここからが本番です。

元となる関数から不活性属性を取り除く

「属性マクロなら不活性属性を扱える」ことの根拠ですが、それはsynクレートが提供する構文要素を見てみることでわかります。

syn::Local ( let xxx = yyy; )を例に出すとこんな感じです。

Rust
pub struct Local {
    pub attrs: Vec<Attribute>,
    pub let_token: Let,
    pub pat: Pat,
    pub init: Option<LocalInit>,
    pub semi_token: Semi,
}

この attrs がまさしく属性の配列で、ここに付与された不活性属性が含まれています!

出力結果がコンパイルエラーとならないようにするには、この属性集合の中から我々のマクロのために付与された不活性属性( #[timing::span] )を取り除く必要があります。本節ではこの取り除き方を解説します。

文から不活性属性を取り除く操作自体は insert_timing でも利用するので、共通処理として rid_attrs モジュールに切り出しましょう。

src/impls.rs
use proc_macro2::TokenStream;

+mod rid_attrs;

pub fn timing(timing_fn_name: syn::Ident, original_fn: syn::ItemFn) -> TokenStream {
    // ...
}

// ...

src/impls/rid_attrs.rs では、次の関数を作成します。

  • 文から属性配列を得る関数 fn find_attrs(stmt: &mut syn::Stmt) -> Option<&mut Vec<syn::Attribute>>
    • rid_timing_span 内で利用
    • 後ほど取り除く操作をしたいので配列の可変参照を得る
    • 構文の特性上属性がない場合もあるので Option
  • 文から対象の属性(timing::span)を取り出す関数 pub fn rid_timing_span(stmt: &mut syn::Stmt) -> Option<syn::Attribute>
    • こちらは親モジュールから利用したいので公開
    • 対象属性が存在したかを呼び出し元に返す。セマンティクス的にブーリアンより Option の方が筆者は好きなのでそうしている
src/impls/rid_attrs.rs
pub fn rid_timing_span(stmt: &mut syn::Stmt) -> Option<syn::Attribute> {
    todo!()
}

fn find_attrs(stmt: &mut syn::Stmt) -> Option<&mut Vec<syn::Attribute>> {
    todo!()
}

属性配列の可変参照を得る

筆者が知らないだけで探したら楽に取り出せるクレートがあるのかもしれませんが、今のところ属性配列の取得は 力業です 。なぜならば、 syn::Stmt を始めとした構文は列挙体になっており、各バリアントにアクセスして初めて属性配列にアクセスできるためです。そしてこの方式のため 式から属性配列を得る分岐がエグい です...orz

とりあえず match 式で無理やり書きます!

src/impls/rid_attrs.rs
fn find_attrs(stmt: &mut syn::Stmt) -> Option<&mut Vec<syn::Attribute>> {
    match stmt {
        // 今回は計測対象ではないのでサボる
        syn::Stmt::Item(_) => None,
        // 以降取得対象
        syn::Stmt::Macro(syn::StmtMacro { attrs, .. })
        | syn::Stmt::Local(syn::Local { attrs, .. }) => Some(attrs),
        // 以降取得対象
        syn::Stmt::Local(syn::Local { attrs, .. }) => Some(attrs),
        syn::Stmt::Expr(expr, _semi) => match expr {
            syn::Expr::Array(syn::ExprArray { attrs, .. })
            | syn::Expr::Assign(syn::ExprAssign { attrs, .. })
            | syn::Expr::Async(syn::ExprAsync { attrs, .. })
            // 残りすべての syn::Expr::Xxx(syn::Xxx { attrs, .. })
            // さすがに省略
            | syn::Expr::While(syn::ExprWhile { attrs, .. })
            | syn::Expr::Yield(syn::ExprYield { attrs, .. }) => Some(attrs),
            syn::Expr::Verbatim(_) | _ => None,
        },
    }
}
省略なし版
Rust
fn find_attrs(stmt: &mut syn::Stmt) -> Option<&mut Vec<syn::Attribute>> {
    match stmt {
        // 今回は計測対象ではないのでサボる
        syn::Stmt::Item(_) => None,
        // 以降取得対象
        syn::Stmt::Macro(syn::StmtMacro { attrs, .. })
        | syn::Stmt::Local(syn::Local { attrs, .. }) => Some(attrs),
        syn::Stmt::Expr(expr, _semi) => match expr {
            syn::Expr::Array(syn::ExprArray { attrs, .. })
            | syn::Expr::Assign(syn::ExprAssign { attrs, .. })
            | syn::Expr::Async(syn::ExprAsync { attrs, .. })
            | syn::Expr::Await(syn::ExprAwait { attrs, .. })
            | syn::Expr::Binary(syn::ExprBinary { attrs, .. })
            | syn::Expr::Block(syn::ExprBlock { attrs, .. })
            | syn::Expr::Break(syn::ExprBreak { attrs, .. })
            | syn::Expr::Call(syn::ExprCall { attrs, .. })
            | syn::Expr::Cast(syn::ExprCast { attrs, .. })
            | syn::Expr::Closure(syn::ExprClosure { attrs, .. })
            | syn::Expr::Const(syn::ExprConst { attrs, .. })
            | syn::Expr::Continue(syn::ExprContinue { attrs, .. })
            | syn::Expr::Field(syn::ExprField { attrs, .. })
            | syn::Expr::ForLoop(syn::ExprForLoop { attrs, .. })
            | syn::Expr::Group(syn::ExprGroup { attrs, .. })
            | syn::Expr::If(syn::ExprIf { attrs, .. })
            | syn::Expr::Index(syn::ExprIndex { attrs, .. })
            | syn::Expr::Infer(syn::ExprInfer { attrs, .. })
            | syn::Expr::Let(syn::ExprLet { attrs, .. })
            | syn::Expr::Lit(syn::ExprLit { attrs, .. })
            | syn::Expr::Loop(syn::ExprLoop { attrs, .. })
            | syn::Expr::Macro(syn::ExprMacro { attrs, .. })
            | syn::Expr::Match(syn::ExprMatch { attrs, .. })
            | syn::Expr::MethodCall(syn::ExprMethodCall { attrs, .. })
            | syn::Expr::Paren(syn::ExprParen { attrs, .. })
            | syn::Expr::Path(syn::ExprPath { attrs, .. })
            | syn::Expr::Range(syn::ExprRange { attrs, .. })
            | syn::Expr::RawAddr(syn::ExprRawAddr { attrs, .. })
            | syn::Expr::Reference(syn::ExprReference { attrs, .. })
            | syn::Expr::Repeat(syn::ExprRepeat { attrs, .. })
            | syn::Expr::Return(syn::ExprReturn { attrs, .. })
            | syn::Expr::Struct(syn::ExprStruct { attrs, .. })
            | syn::Expr::Try(syn::ExprTry { attrs, .. })
            | syn::Expr::TryBlock(syn::ExprTryBlock { attrs, .. })
            | syn::Expr::Tuple(syn::ExprTuple { attrs, .. })
            | syn::Expr::Unary(syn::ExprUnary { attrs, .. })
            | syn::Expr::Unsafe(syn::ExprUnsafe { attrs, .. })
            | syn::Expr::While(syn::ExprWhile { attrs, .. })
            | syn::Expr::Yield(syn::ExprYield { attrs, .. }) => Some(attrs),
            syn::Expr::Verbatim(_) | _ => None,
        },
    }
}

syn::Item についてサボっていることから察しがつくかもしれませんが、自分の属性マクロの機能と関係ない箇所に来る属性配列の取得はサボっています。そのためタイトルの一部である「 好きな位置に 」はやっぱり嘘です。ごめんなさい。とはいえ言いたかったのは頑張ればもっと属性付与可能箇所が増やせるということですね。それは示せているんじゃないかと思います。

属性を取り除く

属性配列の可変参照さえ得られれば、あとは今回我々のマクロが対象としている属性を取り出すのみです!

自分のマクロに関係がない属性を誤って除去しないように気を付ける必要があります。

今回は分岐が単純なので filter_map で取り除きます。 #[timing::span]timing::spansyn::Meta::Pathバリアントに当たるので、こちらで取り出しを行い、対象属性であるかを Path の一致により確認して一致していたら取り出して None を、一致していなければ本体を Some に包んで返すようにします。

Rust
pub fn rid_timing_span(stmt: &mut syn::Stmt) -> Option<syn::Attribute> {
    // timing_span = #[timing::span]
    let timing_span = syn::parse_quote!(timing::span);
    let mut target_attr: Option<syn::Attribute> = None;

    let attrs = find_attrs(stmt)?;

    let new_attrs = attrs
        .clone()
        .into_iter()
        .filter_map(|attr| {
            if let syn::Meta::Path(path) = &attr.meta
                && path == &timing_span
            {
                target_attr = Some(attr);
                None
            } else {
                Some(attr)
            }
        })
        .collect();

    *attrs = new_attrs;

    target_attr
}
完成した src/impls/rid_attrs.rs 全体
src/impls/rid_attrs.rs
pub fn rid_timing_span(stmt: &mut syn::Stmt) -> Option<syn::Attribute> {
    // timing_span = #[timing::span]
    let timing_span = syn::parse_quote!(timing::span);
    let mut target_attr: Option<syn::Attribute> = None;

    let attrs = find_attrs(stmt)?;

    let new_attrs = attrs
        .clone()
        .into_iter()
        .filter_map(|attr| {
            if let syn::Meta::Path(path) = &attr.meta
                && path == &timing_span
            {
                target_attr = Some(attr);
                None
            } else {
                Some(attr)
            }
        })
        .collect();

    *attrs = new_attrs;

    target_attr
}

fn find_attrs(stmt: &mut syn::Stmt) -> Option<&mut Vec<syn::Attribute>> {
    match stmt {
        // 今回は計測対象ではないのでサボる
        syn::Stmt::Item(_) => None,
        // 以降取得対象
        syn::Stmt::Macro(syn::StmtMacro { attrs, .. })
        | syn::Stmt::Local(syn::Local { attrs, .. }) => Some(attrs),
        syn::Stmt::Expr(expr, _semi) => match expr {
            syn::Expr::Array(syn::ExprArray { attrs, .. })
            | syn::Expr::Assign(syn::ExprAssign { attrs, .. })
            | syn::Expr::Async(syn::ExprAsync { attrs, .. })
            | syn::Expr::Await(syn::ExprAwait { attrs, .. })
            | syn::Expr::Binary(syn::ExprBinary { attrs, .. })
            | syn::Expr::Block(syn::ExprBlock { attrs, .. })
            | syn::Expr::Break(syn::ExprBreak { attrs, .. })
            | syn::Expr::Call(syn::ExprCall { attrs, .. })
            | syn::Expr::Cast(syn::ExprCast { attrs, .. })
            | syn::Expr::Closure(syn::ExprClosure { attrs, .. })
            | syn::Expr::Const(syn::ExprConst { attrs, .. })
            | syn::Expr::Continue(syn::ExprContinue { attrs, .. })
            | syn::Expr::Field(syn::ExprField { attrs, .. })
            | syn::Expr::ForLoop(syn::ExprForLoop { attrs, .. })
            | syn::Expr::Group(syn::ExprGroup { attrs, .. })
            | syn::Expr::If(syn::ExprIf { attrs, .. })
            | syn::Expr::Index(syn::ExprIndex { attrs, .. })
            | syn::Expr::Infer(syn::ExprInfer { attrs, .. })
            | syn::Expr::Let(syn::ExprLet { attrs, .. })
            | syn::Expr::Lit(syn::ExprLit { attrs, .. })
            | syn::Expr::Loop(syn::ExprLoop { attrs, .. })
            | syn::Expr::Macro(syn::ExprMacro { attrs, .. })
            | syn::Expr::Match(syn::ExprMatch { attrs, .. })
            | syn::Expr::MethodCall(syn::ExprMethodCall { attrs, .. })
            | syn::Expr::Paren(syn::ExprParen { attrs, .. })
            | syn::Expr::Path(syn::ExprPath { attrs, .. })
            | syn::Expr::Range(syn::ExprRange { attrs, .. })
            | syn::Expr::RawAddr(syn::ExprRawAddr { attrs, .. })
            | syn::Expr::Reference(syn::ExprReference { attrs, .. })
            | syn::Expr::Repeat(syn::ExprRepeat { attrs, .. })
            | syn::Expr::Return(syn::ExprReturn { attrs, .. })
            | syn::Expr::Struct(syn::ExprStruct { attrs, .. })
            | syn::Expr::Try(syn::ExprTry { attrs, .. })
            | syn::Expr::TryBlock(syn::ExprTryBlock { attrs, .. })
            | syn::Expr::Tuple(syn::ExprTuple { attrs, .. })
            | syn::Expr::Unary(syn::ExprUnary { attrs, .. })
            | syn::Expr::Unsafe(syn::ExprUnsafe { attrs, .. })
            | syn::Expr::While(syn::ExprWhile { attrs, .. })
            | syn::Expr::Yield(syn::ExprYield { attrs, .. }) => Some(attrs),
            syn::Expr::Verbatim(_) | _ => None,
        },
    }
}

syn::visit_mut で文から属性を取り除く

後は関数内に含まれる文( syn::Stmt )を調べて、属性がついていたら取り除くのみです!

文としてブロックがありそのブロックも文を持つ時など、ネストしている時でも適切に取り除いてもらうために、 syn::visit_mut モジュールを利用します。

syn::visit_mut モジュールについては 昨日の記事 にて解説したのでこちらを読んでいただけると幸いです。

今回は関数中に存在するすべての文から属性を取り除ければ良いので、 visit_stmt_mut を利用します。

src/impls.rs
fn rid_inert_attributes_from_stmts(f: &mut syn::ItemFn) {
    struct RidAttrsVisitor;

    impl VisitMut for RidAttrsVisitor {
        fn visit_stmt_mut(&mut self, stmt: &mut syn::Stmt) {
            let _ = rid_attrs::rid_timing_span(stmt);
            syn::visit_mut::visit_stmt_mut(self, stmt);
        }
    }

    let mut visitor = RidAttrsVisitor;
    visitor.visit_item_fn_mut(f);
}

ここまでの動作確認

ここまでの実装が正しいか、つまり属性が適切に取り除かれるかを確認します。まだ実装していない部分が影響を及ぼさないよう、追加の関数が表示されないようにします。

一応ここまでの src/impls.rs 全体を載せます。

src/impls.rs
use proc_macro2::TokenStream;
use syn::visit_mut::VisitMut;

mod rid_attrs;

pub fn timing(timing_fn_name: syn::Ident, mut original_fn: syn::ItemFn) -> TokenStream {
    let mut timing_fn = original_fn.clone();
    timing_fn.sig.ident = timing_fn_name;

    rid_inert_attributes_from_stmts(&mut original_fn);
    // insert_timing(&mut timing_fn);

    // 一旦 #timing_fn を取り除いておく
    quote::quote! {
        #original_fn
    }
}

fn rid_inert_attributes_from_stmts(f: &mut syn::ItemFn) {
    struct RidAttrsVisitor;

    impl VisitMut for RidAttrsVisitor {
        fn visit_stmt_mut(&mut self, stmt: &mut syn::Stmt) {
            let _ = rid_attrs::rid_timing_span(stmt);
            syn::visit_mut::visit_stmt_mut(self, stmt);
        }
    }

    let mut visitor = RidAttrsVisitor;
    visitor.visit_item_fn_mut(f);
}

fn insert_timing(_f: &mut syn::ItemFn) {
    todo!()
}

手続きマクロクレートは src/lib.rs にマクロではない関数を書いたりはできませんが、実はライブラリクレートでも src/main.rs を書くことができ、さらには実行が可能だったりします。

そのためここをプレイグラウンドとして最初に掲載した計測対象のプログラムを載せてみましょう。

src/main.rs
use timing_macro::timing;

#[timing(run_with_time)]
fn run(input: Vec<&'static str>) {
    let input_1 = input.clone();

    let mut res_1 = Vec::new();

    #[timing::span]
    for item in input_1 {
        if let Ok(n) = item.parse::<usize>() {
            res_1.push(n);
        }
    }

    #[timing::span]
    let res_2: Vec<_> = input
        .into_iter()
        .filter_map(|n| n.parse::<usize>().ok())
        .collect();

    assert_eq!(res_1, res_2);
}

fn main() {
    let v = vec!["1", "a", "5", "2", "b"];

    run(v);
}

コンパイルエラーがでなければ6、属性が適切に取り除かれているのでおkです!

他の属性に影響がないことを確認する

必須ではありませんが一応他の属性に影響がないことを確認します。

src/main.rs
use timing_macro::timing;

#[timing(run_with_time)]
fn run(input: Vec<&'static str>) {
    let input_1 = input.clone();

    let mut res_1 = Vec::new();

    #[timing::span]
    for item in input_1 {
        if let Ok(n) = item.parse::<usize>() {
            res_1.push(n);
        }
    }

    #[timing::span]
    let res_2: Vec<_> = input
        .into_iter()
        .filter_map(|n| n.parse::<usize>().ok())
        .collect();

    #[cfg(test)]
    #[timing::span]
    println!("test output: {:?}", res_1);

    #[timing::span]
    #[cfg(test)]
    println!("test output: {:?}", res_2);

    assert_eq!(res_1, res_2);
}

fn main() {
    let v = vec!["1", "a", "5", "2", "b"];

    run(v);
}

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

    #[test]
    fn test_run() {
        let v = vec!["10", "20", "c", "30", "d"];
        run(v);
    }
}

普通に実行した時は何も出ず、テスト時にしかるべき出力が出ていれば問題ないでしょう。

ここで、 rid_timing_spanfilter_map 内返り値を Some(attr) から None にしたときに何が起きるか観察してみると面白そうです。後ほども確認しますが、ここでもcargo-expandなどを活用してみると良いでしょう。

cargo expand --bin timing-macro

時間計測機能付き関数を生成する

ここまで来れば後は本命の時間測定機能付き関数を生成すればよいだけです!本節長くなってしまいましたが内容はここまでほど難しくはない(はず)のでお付き合いいただければと思います。

また syn::visit_mut を利用します。先ほどは各文だけ見ればよかったですが、今回はブロック内の文の数が増えるため visit_block_mut の方を利用します。

src/impls.rs
fn insert_timing(f: &mut syn::ItemFn) {
    struct InsertTimingVisitor {
        is_top: bool,
    };

    impl VisitMut for InsertTimingVisitor {
        fn visit_block_mut(&mut self, block: &mut syn::Block) {
            let is_top = self.is_top;
            self.is_top = false;
            syn::visit_mut::visit_block_mut(self, block);

            // todo
        }
    }

    let mut visitor = InsertTimingVisitor { is_top: true };
    visitor.visit_item_fn_mut(f);
}

関数直下のブロックの時のみ時間測定の初期化と最終結果を出したいので、そのフラグを加えています。またシンプルに考えるため、先に再帰部分を済ませておくことにします。先に再帰を済ませることにすると、内側から外側へと置換が起きます。

todoを埋めていきましょう。 syn::Block には stmts というフィールドがあります。今回は元となる stmts を使いつつ、新しい new_stmts を作りそちらに差し替えることにします。

最初の部分を作ります。関数直下のブロックの場合は let now = ::std::time::Instant::now(); を差し込みます。

visit_block_mut内部 続き
// ...

let mut new_stmts = Vec::new();

if is_top {
    let start_stmt: syn::Stmt = syn::parse_quote! {
        let now = ::std::time::Instant::now();
    };

    new_stmts.push(start_stmt);
}

// ...

途中の文を for で処理していきます。後述しますが末尾式かの判定が必要なのでインデックスで判定できるように書いておきます7

#[timing::span] の有無で条件分岐し、もしある場合は区間時間を出力する処理も加えます。

visit_block_mut続き
let stmts_last_index = block.stmts.len().saturating_sub(1);
for (idx, mut stmt) in block.stmts.iter().cloned().enumerate() {
    let timing_span = rid_attrs::rid_timing_span(&mut stmt);

    let Some(_) = timing_span else {
        // 属性がついていなければそのまま挿入し次へ
        new_stmts.push(stmt);
        continue;
    };

    // 以降はtiming_span属性付きの場合

    // ...
}

#[timing::span] 属性付きの場合を書いていきます。後ほどエッジケース検証で詳細を解説しますが、「 #[timing::span] が付与されている & 最後の式である」場合は一旦評価値を退避させる必要があります。最後かどうかの判定8および syn::Stmt::Expr バリアントでセミコロン ; の有無を調べることで判断します。

visit_block_mut for文内続き
use syn::spanned::Spanned; // ファイル上部に加えてください

// ファイル名・出力行数をここで確保
let span = stmt.span().unwrap();
let file = span.file();
let line = span.line();

match stmt {
    // 最後の式の時
    stmt @ syn::Stmt::Expr(_, None) if idx == stmts_last_index => {
        insert_timing_to_tail_expr(&mut new_stmts, stmt, file, line)
    }
    // 通常時
    stmt => insert_timing_to_stmt(&mut new_stmts, stmt, file, line),
}

表示に使う fileline をここで確保しておきます。行数表示APIは最近安定化したばかりでその影響で proc_macro2 のそれよりも proc_macro::Span の方が信用できるので、 .unwrap() でこちらにアクセスしています9

流石に長くなってきたのでここからの中身の追加は別関数に切り出します。通常時から作りこみます。ゴリゴリ書いていくだけです!

src/impls.rs
/// ```ignore
/// #[timing::span]
/// println!("beep");
/// ```
///
/// ↓
///
/// ```ignore
/// let span_start_time = now.elapsed();
/// println!("beep");
/// let span_end_time = now.elapsed();
/// println!(...);
/// ```
fn insert_timing_to_stmt(
    new_stmts: &mut Vec<syn::Stmt>,
    original_stmt: syn::Stmt,
    file: String,
    line: usize,
) {
    let span_start_time: syn::Stmt = syn::parse_quote! {
        let span_start_time = now.elapsed();
    };
    new_stmts.push(span_start_time);

    new_stmts.push(original_stmt);

    let span_end_time: syn::Stmt = syn::parse_quote! {
        let span_end_time = now.elapsed();
    };
    new_stmts.push(span_end_time);

    let println: syn::Stmt = syn::parse_quote! {
        println!(
            "[{}:{}]\n\tstart:\t{:>12?}\n\tend:\t{:>12?}\n\ttime:\t{:>12?}",
            #file,
            #line,
            span_start_time,
            span_end_time,
            span_end_time - span_start_time,
        );
    };
    new_stmts.push(println);
}

最後の式である場合は少しだけ内容を変えます!

src/impls.rs
/// ```ignore
/// #[timing::span]
/// func(val)
/// ```
///
/// ↓
///
/// ```ignore
/// let span_start_time = now.elapsed();
/// let tmp = func(val);
/// let span_end_time = now.elapsed();
/// println!(...);
/// tmp
/// ```
fn insert_timing_to_tail_expr(
    new_stmts: &mut Vec<syn::Stmt>,
    original_stmt: syn::Stmt,
    file: String,
    line: usize,
) {
    let span_start_time: syn::Stmt = syn::parse_quote! {
        let span_start_time = now.elapsed();
    };
    new_stmts.push(span_start_time);

    // let tmp = 式; で一回退避
    let tmp_binding: syn::Stmt = syn::parse_quote! {
        let tmp = #original_stmt;
    };
    new_stmts.push(tmp_binding);

    let span_end_time: syn::Stmt = syn::parse_quote! {
        let span_end_time = now.elapsed();
    };
    new_stmts.push(span_end_time);

    let println: syn::Stmt = syn::parse_quote! {
        println!(
            "[{}:{}]\n\tstart:\t{:>12?}\n\tend:\t{:>12?}\n\ttime:\t{:>12?}",
            #file,
            #line,
            span_start_time,
            span_end_time,
            span_end_time - span_start_time,
        );
    };
    new_stmts.push(println);

    // tmp を置きなおすことで退避した値を戻す
    let return_tmp: syn::Expr = syn::parse_quote! {
        tmp
    };
    new_stmts.push(syn::Stmt::Expr(return_tmp, None));
}

最後に、関数全体でかかった時間を表示する機構を作ります。ここで、改めて最後の式によって分岐が生じます。

visit_block_mut for文の後
fn visit_block_mut(&mut self, block: &mut syn::Block) {
    // ...

    if is_top { // トップレベルの時だけトータルタイムを出す
        let last_stmt = new_stmts.pop(); // 最後の要素を取り出す

        // ファイル名・出力行数をここで確保
        let span = last_stmt.span().unwrap();
        let file = span.file();
        let line = span.line();

        match last_stmt {
            // 最後の文が末尾式の時
            // 退避させつつ挿入
            Some(stmt @ syn::Stmt::Expr(_, None)) => {
                insert_last_timing_to_tail_expr(&mut new_stmts, stmt, file, line)
            }
            // 最後の文がその他の文の時
            // 普通に後ろに挿入
            Some(stmt) => insert_last_timing_to_stmt(&mut new_stmts, stmt, file, line),
            None => {} // 空の時は何もしない
        }
    }

    // ここまでの内容で上書き!
    block.stmts = new_stmts;
}

末尾処理は以下のような感じです。

Rust
/// ```ignore
/// {
///     // ...
///     println!("beep");
/// }
/// ```
///
/// ↓
///
/// ```ignore
/// {
///     // ...
///     println!("beep");
///     println!(/* total time */);
/// }
/// ```
fn insert_last_timing_to_stmt(
    new_stmts: &mut Vec<syn::Stmt>,
    original_stmt: syn::Stmt,
    file: String,
    line: usize,
) {
    new_stmts.push(original_stmt);

    let total: syn::Stmt = syn::parse_quote! {
        println!("[{}:{}]\n\ttotal:\t{:>12?}", #file, #line, now.elapsed());
    };
    new_stmts.push(total);
}

/// ```ignore
/// {
///     // ...
///     func(val)
/// }
/// ```
///
/// ↓
///
/// ```ignore
/// {
///     // ...
///     let tmp = func(val);
///     println!(/* total time */);
///     tmp
/// }
/// ```
fn insert_last_timing_to_tail_expr(
    new_stmts: &mut Vec<syn::Stmt>,
    original_stmt: syn::Stmt,
    file: String,
    line: usize,
) {
    let tmp_binding: syn::Stmt = syn::parse_quote! {
        let tmp = #original_stmt;
    };
    new_stmts.push(tmp_binding);

    let total: syn::Stmt = syn::parse_quote! {
        println!("[{}:{}]\n\ttotal:\t{:>12?}", #file, #line, now.elapsed());
    };
    new_stmts.push(total);

    let return_tmp: syn::Expr = syn::parse_quote! {
        tmp
    };
    new_stmts.push(syn::Stmt::Expr(return_tmp, None));
}

これで完成です!src/impls.rs 全体は以下のようになります。(長いので折り畳み)

src/impls.rs 全体
src/impls.rs
use proc_macro2::TokenStream;
use syn::{spanned::Spanned, visit_mut::VisitMut};

mod rid_attrs;

pub fn timing(timing_fn_name: syn::Ident, mut original_fn: syn::ItemFn) -> TokenStream {
    let mut timing_fn = original_fn.clone();
    timing_fn.sig.ident = timing_fn_name;

    rid_inert_attributes_from_stmts(&mut original_fn);
    insert_timing(&mut timing_fn);

    quote::quote! {
        #original_fn

        #timing_fn
    }
}

fn rid_inert_attributes_from_stmts(f: &mut syn::ItemFn) {
    struct RidAttrsVisitor;

    impl VisitMut for RidAttrsVisitor {
        fn visit_stmt_mut(&mut self, stmt: &mut syn::Stmt) {
            let _ = rid_attrs::rid_timing_span(stmt);
            syn::visit_mut::visit_stmt_mut(self, stmt);
        }
    }

    let mut visitor = RidAttrsVisitor;
    visitor.visit_item_fn_mut(f);
}

fn insert_timing(f: &mut syn::ItemFn) {
    struct InsertTimingVisitor {
        is_top: bool,
    }

    impl VisitMut for InsertTimingVisitor {
        fn visit_block_mut(&mut self, block: &mut syn::Block) {
            let is_top = self.is_top;
            self.is_top = false;
            syn::visit_mut::visit_block_mut(self, block);

            let mut new_stmts = Vec::new();

            if is_top {
                let start_stmt: syn::Stmt = syn::parse_quote! {
                    let now = ::std::time::Instant::now();
                };

                new_stmts.push(start_stmt);
            }

            let stmts_last_index = block.stmts.len().saturating_sub(1);
            for (idx, mut stmt) in block.stmts.iter().cloned().enumerate() {
                let timing_span = rid_attrs::rid_timing_span(&mut stmt);

                let Some(_) = timing_span else {
                    // 属性がついていなければそのまま挿入し次へ
                    new_stmts.push(stmt);
                    continue;
                };

                // 以降はtiming_span属性付きの場合

                // ファイル名・出力行数をここで確保
                let span = stmt.span().unwrap();
                let file = span.file();
                let line = span.line();

                match stmt {
                    // 最後の式の時
                    stmt @ syn::Stmt::Expr(_, None) if idx == stmts_last_index => {
                        insert_timing_to_tail_expr(&mut new_stmts, stmt, file, line)
                    }
                    // 通常時
                    stmt => insert_timing_to_stmt(&mut new_stmts, stmt, file, line),
                }
            }

            if is_top {
                let last_stmt = new_stmts.pop();

                // ファイル名・出力行数をここで確保
                let span = last_stmt.span().unwrap();
                let file = span.file();
                let line = span.line();

                match last_stmt {
                    // 最後の文が末尾式の時
                    // 退避させつつ挿入
                    Some(stmt @ syn::Stmt::Expr(_, None)) => {
                        insert_last_timing_to_tail_expr(&mut new_stmts, stmt, file, line)
                    }
                    // 最後の文がその他の文の時
                    // 普通に後ろに挿入
                    Some(stmt) => insert_last_timing_to_stmt(&mut new_stmts, stmt, file, line),
                    None => {} // 空の時は何もしない
                }
            }

            block.stmts = new_stmts;
        }
    }

    let mut visitor = InsertTimingVisitor { is_top: true };
    visitor.visit_item_fn_mut(f);
}

/// ```ignore
/// #[timing::span]
/// println!("beep");
/// ```
///
/// ↓
///
/// ```ignore
/// let span_start_time = now.elapsed();
/// println!("beep");
/// let span_end_time = now.elapsed();
/// println!(...);
/// ```
fn insert_timing_to_stmt(
    new_stmts: &mut Vec<syn::Stmt>,
    original_stmt: syn::Stmt,
    file: String,
    line: usize,
) {
    let span_start_time: syn::Stmt = syn::parse_quote! {
        let span_start_time = now.elapsed();
    };
    new_stmts.push(span_start_time);

    new_stmts.push(original_stmt);

    let span_end_time: syn::Stmt = syn::parse_quote! {
        let span_end_time = now.elapsed();
    };
    new_stmts.push(span_end_time);

    let println: syn::Stmt = syn::parse_quote! {
        println!(
            "[{}:{}]\n\tstart:\t{:>12?}\n\tend:\t{:>12?}\n\ttime:\t{:>12?}",
            #file,
            #line,
            span_start_time,
            span_end_time,
            span_end_time - span_start_time,
        );
    };
    new_stmts.push(println);
}

/// ```ignore
/// #[timing::span]
/// func(val)
/// ```
///
/// ↓
///
/// ```ignore
/// let span_start_time = now.elapsed();
/// let tmp = func(val);
/// let span_end_time = now.elapsed();
/// println!(...);
/// tmp
/// ```
fn insert_timing_to_tail_expr(
    new_stmts: &mut Vec<syn::Stmt>,
    original_stmt: syn::Stmt,
    file: String,
    line: usize,
) {
    let span_start_time: syn::Stmt = syn::parse_quote! {
        let span_start_time = now.elapsed();
    };
    new_stmts.push(span_start_time);

    let tmp_binding: syn::Stmt = syn::parse_quote! {
        let tmp = #original_stmt;
    };
    new_stmts.push(tmp_binding);

    let span_end_time: syn::Stmt = syn::parse_quote! {
        let span_end_time = now.elapsed();
    };
    new_stmts.push(span_end_time);

    let println: syn::Stmt = syn::parse_quote! {
        println!(
            "[{}:{}]\n\tstart:\t{:>12?}\n\tend:\t{:>12?}\n\ttime:\t{:>12?}",
            #file,
            #line,
            span_start_time,
            span_end_time,
            span_end_time - span_start_time,
        );
    };
    new_stmts.push(println);

    let return_tmp: syn::Expr = syn::parse_quote! {
        tmp
    };
    new_stmts.push(syn::Stmt::Expr(return_tmp, None));
}

/// ```ignore
/// {
///     // ...
///     println!("beep");
/// }
/// ```
///
/// ↓
///
/// ```ignore
/// {
///     // ...
///     println!("beep");
///     println!(/* total time */);
/// }
/// ```
fn insert_last_timing_to_stmt(
    new_stmts: &mut Vec<syn::Stmt>,
    original_stmt: syn::Stmt,
    file: String,
    line: usize,
) {
    new_stmts.push(original_stmt);

    let total: syn::Stmt = syn::parse_quote! {
        println!("[{}:{}]\n\ttotal:\t{:>12?}", #file, #line, now.elapsed());
    };
    new_stmts.push(total);
}

/// ```ignore
/// {
///     // ...
///     func(val)
/// }
/// ```
///
/// ↓
///
/// ```ignore
/// {
///     // ...
///     let tmp = func(val);
///     println!(/* total time */);
///     tmp
/// }
/// ```
fn insert_last_timing_to_tail_expr(
    new_stmts: &mut Vec<syn::Stmt>,
    original_stmt: syn::Stmt,
    file: String,
    line: usize,
) {
    let tmp_binding: syn::Stmt = syn::parse_quote! {
        let tmp = #original_stmt;
    };
    new_stmts.push(tmp_binding);

    let total: syn::Stmt = syn::parse_quote! {
        println!("[{}:{}]\n\ttotal:\t{:>12?}", #file, #line, now.elapsed());
    };
    new_stmts.push(total);

    let return_tmp: syn::Expr = syn::parse_quote! {
        tmp
    };
    new_stmts.push(syn::Stmt::Expr(return_tmp, None));
}

検証

最後にこのマクロがうまく実装できたかを確かめましょう!

src/main.rs
use timing_macro::timing;

#[timing(run_with_time)]
fn run(input: Vec<&'static str>) {
    let input_1 = input.clone();

    let mut res_1 = Vec::new();

    #[timing::span]
    for item in input_1 {
        if let Ok(n) = item.parse::<usize>() {
            res_1.push(n);
        }
    }

    #[timing::span]
    let res_2: Vec<_> = input
        .into_iter()
        .filter_map(|n| n.parse::<usize>().ok())
        .collect();

    assert_eq!(res_1, res_2);
}

fn main() {
    let v = vec!["1", "a", "5", "2", "b"];

    run(v.clone());

    run_with_time(v);
}

以下、実行結果例です!

実行結果cargo run -q
[src/main.rs:10]
        start:         155ns
        end:           738ns
        time:          583ns
[src/main.rs:17]
        start:      36.037µs
        end:        37.271µs
        time:        1.234µs
[src/main.rs:22]
        total:      40.759µs

どうでも良いですが意外にも for の方がこの結果だと速いみたいですね。ヒープ確保分の差とか...?

終盤、末尾式かどうかで処理が嵩みました。この辺のエッジケースがしっかりと問題なく機能するかも調べましょう!

src/main.rs
use timing_macro::timing;

#[timing(run_with_time)]
fn run(input: Vec<&'static str>) -> Vec<usize> {
    let input_1 = input.clone();
    let input_2 = input.clone();
    let input_3 = input.clone();
    let input_4 = input.clone();

    let mut res_1 = Vec::new();

    #[timing::span]
    for item in input_1 {
        if let Ok(n) = item.parse::<usize>() {
            res_1.push(n);
        }
    }

    #[timing::span]
    let res_2: Vec<_> = input_2
        .into_iter()
        .filter_map(|n| n.parse::<usize>().ok())
        .collect();

    assert_eq!(res_1, res_2);

    let res_3 = {
        println!("for loop");
        // insert_timing_to_tail_expr が機能していることの確認
        #[timing::span]
        for_loop(input_3)
    };

    let res_4 = {
        println!("iter loop");
        #[timing::span]
        iter_loop(input_4)
    };

    assert_eq!(res_3, res_4);

    // insert_last_timing_to_tail_expr が機能していることの確認
    res_1
}

fn for_loop(input: Vec<&'static str>) -> Vec<usize> {
    let mut res = Vec::new();

    for item in input {
        if let Ok(n) = item.parse::<usize>() {
            res.push(n);
        }
    }

    res
}

fn iter_loop(input: Vec<&'static str>) -> Vec<usize> {
    input
        .into_iter()
        .filter_map(|n| n.parse::<usize>().ok())
        .collect()
}

fn main() {
    let v = vec!["1", "a", "5", "2", "b"];

    run(v.clone());

    run_with_time(v);
}
実行結果
cargo run -q
for loop
iter loop
[src/main.rs:13]
        start:         535ns
        end:         1.197µs
        time:          662ns
[src/main.rs:20]
        start:      31.603µs
        end:        32.918µs
        time:        1.315µs
for loop
[src/main.rs:31]
        start:      36.715µs
        end:        37.357µs
        time:          642ns
iter loop
[src/main.rs:37]
        start:      41.914µs
        end:        42.771µs
        time:          857ns
[src/main.rs:43]
        total:      45.604µs

実行結果は問題なさそうです。cargo-expand でマクロの展開結果も確認してみましょう!

cargo expand --bin timing-macro
    Checking timing-macro v0.1.0 (/home/namn/workspace/qiita_adv_articles_2025/programs/adv13/timing-macro)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.05s

#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use timing_macro::timing;
fn run(input: Vec<&'static str>) -> Vec<usize> {
    let input_1 = input.clone();
    let input_2 = input.clone();
    let input_3 = input.clone();
    let input_4 = input.clone();
    let mut res_1 = Vec::new();
    for item in input_1 {
        if let Ok(n) = item.parse::<usize>() {
            res_1.push(n);
        }
    }
    let res_2: Vec<_> = input_2
        .into_iter()
        .filter_map(|n| n.parse::<usize>().ok())
        .collect();
    match (&res_1, &res_2) {
        (left_val, right_val) => {
            if !(*left_val == *right_val) {
                let kind = ::core::panicking::AssertKind::Eq;
                ::core::panicking::assert_failed(
                    kind,
                    &*left_val,
                    &*right_val,
                    ::core::option::Option::None,
                );
            }
        }
    };
    let res_3 = {
        {
            ::std::io::_print(format_args!("for loop\n"));
        };
        for_loop(input_3)
    };
    let res_4 = {
        {
            ::std::io::_print(format_args!("iter loop\n"));
        };
        iter_loop(input_4)
    };
    match (&res_3, &res_4) {
        (left_val, right_val) => {
            if !(*left_val == *right_val) {
                let kind = ::core::panicking::AssertKind::Eq;
                ::core::panicking::assert_failed(
                    kind,
                    &*left_val,
                    &*right_val,
                    ::core::option::Option::None,
                );
            }
        }
    };
    res_1
}
fn run_with_time(input: Vec<&'static str>) -> Vec<usize> {
    let now = ::std::time::Instant::now();
    let input_1 = input.clone();
    let input_2 = input.clone();
    let input_3 = input.clone();
    let input_4 = input.clone();
    let mut res_1 = Vec::new();
    let span_start_time = now.elapsed();
    for item in input_1 {
        if let Ok(n) = item.parse::<usize>() {
            res_1.push(n);
        }
    }
    let span_end_time = now.elapsed();
    {
        ::std::io::_print(
            format_args!(
                "[{0}:{1}]\n\tstart:\t{2:>12?}\n\tend:\t{3:>12?}\n\ttime:\t{4:>12?}\n",
                "src/main.rs",
                13usize,
                span_start_time,
                span_end_time,
                span_end_time - span_start_time,
            ),
        );
    };
    let span_start_time = now.elapsed();
    let res_2: Vec<_> = input_2
        .into_iter()
        .filter_map(|n| n.parse::<usize>().ok())
        .collect();
    let span_end_time = now.elapsed();
    {
        ::std::io::_print(
            format_args!(
                "[{0}:{1}]\n\tstart:\t{2:>12?}\n\tend:\t{3:>12?}\n\ttime:\t{4:>12?}\n",
                "src/main.rs",
                20usize,
                span_start_time,
                span_end_time,
                span_end_time - span_start_time,
            ),
        );
    };
    match (&res_1, &res_2) {
        (left_val, right_val) => {
            if !(*left_val == *right_val) {
                let kind = ::core::panicking::AssertKind::Eq;
                ::core::panicking::assert_failed(
                    kind,
                    &*left_val,
                    &*right_val,
                    ::core::option::Option::None,
                );
            }
        }
    };
    let res_3 = {
        {
            ::std::io::_print(format_args!("for loop\n"));
        };
        let span_start_time = now.elapsed();
        let tmp = for_loop(input_3);
        let span_end_time = now.elapsed();
        {
            ::std::io::_print(
                format_args!(
                    "[{0}:{1}]\n\tstart:\t{2:>12?}\n\tend:\t{3:>12?}\n\ttime:\t{4:>12?}\n",
                    "src/main.rs",
                    31usize,
                    span_start_time,
                    span_end_time,
                    span_end_time - span_start_time,
                ),
            );
        };
        tmp
    };
    let res_4 = {
        {
            ::std::io::_print(format_args!("iter loop\n"));
        };
        let span_start_time = now.elapsed();
        let tmp = iter_loop(input_4);
        let span_end_time = now.elapsed();
        {
            ::std::io::_print(
                format_args!(
                    "[{0}:{1}]\n\tstart:\t{2:>12?}\n\tend:\t{3:>12?}\n\ttime:\t{4:>12?}\n",
                    "src/main.rs",
                    37usize,
                    span_start_time,
                    span_end_time,
                    span_end_time - span_start_time,
                ),
            );
        };
        tmp
    };
    match (&res_3, &res_4) {
        (left_val, right_val) => {
            if !(*left_val == *right_val) {
                let kind = ::core::panicking::AssertKind::Eq;
                ::core::panicking::assert_failed(
                    kind,
                    &*left_val,
                    &*right_val,
                    ::core::option::Option::None,
                );
            }
        }
    };
    let tmp = res_1;
    {
        ::std::io::_print(
            format_args!(
                "[{0}:{1}]\n\ttotal:\t{2:>12?}\n",
                "src/main.rs",
                43usize,
                now.elapsed(),
            ),
        );
    };
    tmp
}
fn for_loop(input: Vec<&'static str>) -> Vec<usize> {
    let mut res = Vec::new();
    for item in input {
        if let Ok(n) = item.parse::<usize>() {
            res.push(n);
        }
    }
    res
}
fn iter_loop(input: Vec<&'static str>) -> Vec<usize> {
    input.into_iter().filter_map(|n| n.parse::<usize>().ok()).collect()
}
fn main() {
    let v = <[_]>::into_vec(::alloc::boxed::box_new(["1", "a", "5", "2", "b"]));
    run(v.clone());
    run_with_time(v);
}

末尾式の時でも適切に処理されています、よさそうです!

残る課題

今回のハンズオンのために作ったので、このマクロにはまだ色々問題があるかと思います。明確なものとしては衛生性(Hygiene)が保たれていない点です。

次のコードをどこかに差し込んでください。型のエラーが出てしまうと思います。

Rust
#[timing::span]
let span_start_time = 42;

println!("{span_start_time}");

本来は syn::Ident::new を使い、 Span::mixed_site() を指定するべきだったのですが、ただでさえ長くなってしまっていたので今回ここはサボりました :sweat_smile: (言い訳)

他にも考慮漏れはあるかもしれないです。もし余裕がありましたら調べてみてください :bow:

まとめ

今回のハンズオンで作成したマクロのリポジトリを再掲しておきます:

ハンズオンと銘打っておきながら終盤は結構説明よりもソースコードが多くなってしまい申し訳ないです :sweat_smile:

中盤の不活性属性を除去するコードあたりでマクロ作成に慣れている人にとっては原理が伝わったのではないでしょうか...?構文中で不活性属性を使うのはそんなに難しくないことを示せたと思います。

その割に、 rstest以外で自由に不活性属性を利用している属性マクロを見たことがないんですよね... :thinking:

「別に不活性属性使いまくってもいいだろう」と思ってhooqを作ったわけですが、もしかしたら違法行為で筆者は近々Rust警察に逮捕されてしまうかもしれません :scream:

本当に合法なのか知りたいので、本記事を元に皆さんもぜひ不活性属性を活用する属性マクロを作って公開してみてほしいです。

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

  1. 見出しの元ネタはとなりのトトロのメイのセリフ

  2. トークン木ならなんでも受け取ってくれる関数風マクロと異なり、属性マクロやderiveマクロの適用先構文はある程度Rustの文法に従っている必要があります。この「ある程度」の差に位置するものが不活性属性なのでしょう。

  3. Option を自動検知してフックしないという手も考えられますが、マクロの限界というか具体的な型情報はこの時点では得られないので下手なことはできないんですよね...

  4. 余談。「属性マクロでも不活性属性を扱える」ということを教えてくれたのはこれまた冒頭で紹介したrstestでした。ただ扱えることはわかっていたのですがrstestクレートを深く読み込むようなことはしていなかったため方法はしばらくわかりませんでした。...ところがhooq実装の段になって、synクレートのドキュメントをじっと眺めているとできることに気づいたわけです!

  5. 普段は syn::Result にするのですが、今回のマクロではユーザー起因でのマクロ利用ミスによるコンパイルエラーは本マクロが関知しないところでのみ起きるため不要と判断し、決定的に決まるためそのまま proc_macro2::TokenStream を返しています。

  6. ちなみに筆者はちょっとしたミスをしていてここで一敗しました...確認、大事です

  7. こういう時 std::iter::Peekable を使いたくなりますが、今回はインデックスの方が素直でした...うむむ

  8. for 文の場合など、ブロックの途中でもセミコロンがつかない場合があるので末尾であることの判定は必要です(一敗)。ちなみに { let tmp = for _ in 0..10 {}; tmp } みたいなのは許されるようなのでなぜセミコロンがついていないかの解析まではしないことにします。

  9. ただこのようにした場合の欠点は、テストがしづらくなっているかもしれないという点です。本当はプロファイルとかで取得元を切り替えるとかした方が丁寧かもしれません。

2
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
2
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?