属性マクロの展開順序は、上のマクロから先に適用されます!
そのため、影響度が少ない・展開結果の予想がつきやすい属性マクロを先に付けるのがよさそうです!
こんにチュア!本記事は hooqアドベントカレンダー 14日目の記事です!
hooqはメソッドを ? 演算子にフックする属性マクロです。本アドカレではhooqの使い方を始め、hooqマクロを作成するにあたり得た知識、Rustのエラーハンドリング・エラーロギング周りの話をまとめています。
最近うっかり重めのハンズオン記事を2記事も書いてヘロヘロになってしまったので、14, 16, 17日目の記事は比較的簡単なものにしようと思います1。
属性マクロ複数付ける時、順番わからん!
非同期関数なので #[tokio::main] を、そして雑にエラートレースを表示させたいから #[hooq(anyhow)] を付けたいとしましょう。
#[hooq(anyhow)]
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// ...
}
アレ?この順番で良いのでしょうか...?
#[tokio::main]
#[hooq(anyhow)]
async fn main() -> anyhow::Result<()> {
// ...
}
それともこっちが正しいのでしょうか...?どの順で書くべきかもやもやします...
このもやもやをはっきり言葉にするとこうなります。
―「属性マクロは上についている方、下についている方どちらから先に展開されるのか?」―
...
...
...
これってトリビアになりませんか?
というわけで調査してみました!
ちなみに先に答えを言ってしまうのですが、 「上から下に」 適用されます。以下は検証ログ的な感じです。
公式ドキュメントを確認
見つけられると思ったのですが見つけられませんでした!!!!!
フォーラムに質問はあり、そこに回答はついていました。
先頭に「実行順は決まらない」という答えがついていたのですが多分これ最初に発動した属性マクロが後続の属性マクロを取り除いてしまう可能性を指している...?属性マクロが適切に属性を扱っていれば、最初の回答をミスリードと言っている後続の回答の通り上から下で合っているはずです。
検証して確認
というわけで、これはもう直接コードを書いて確かめるのが早そうですね。検証用の属性マクロを用意しました!
modify_name マクロ 構成
GitHubの方を読んでくれればよいのですが大した分量ではないためここにも記しておきます。
属性マクロ全般の作り方はぜひ拙著を読んで!: Rust 属性風マクロを軽くハンズオン #Rust - Qiita
[package]
name = "modify-name"
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 = ["full"] }
use proc_macro::TokenStream;
use quote::ToTokens;
#[proc_macro_attribute]
pub fn modify_name(attr: TokenStream, item: TokenStream) -> TokenStream {
let add_ident = syn::parse_macro_input!(attr as syn::Ident);
let mut f = syn::parse_macro_input!(item as syn::ItemFn);
f.sig.ident = syn::Ident::new(
&format!("{}_{}", f.sig.ident, add_ident),
f.sig.ident.span(),
);
f.into_token_stream().into()
}
属性マクロ #[modify_name(aaa)] を関数 fn bbb() {} に付けると、 fn bbb_aaa() {} という名前の関数に変わります。この属性マクロは付与対象関数の名前以外は一切触らず、他の属性マクロや不活性属性もちゃんと保存するので今回の検証には十分なマクロでしょう。
use modify_name::modify_name;
#[modify_name(aaa)]
fn bbb() {}
fn main() {
bbb_aaa();
}
cargo-expandで展開すると fn bbb_aaa() {} が生成されていることが確認できます。
$ cargo expand --bin modify-name
Checking modify-name v0.1.0 (/home/namn/workspace/qiita_adv_articles_2025/programs/adv14/modify-name)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.04s
#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use modify_name::modify_name;
fn bbb_aaa() {}
fn main() {
bbb_aaa();
}
検証結果は...!?
ではいよいよ実験...!次のプログラムはどうなるのか...?!
use modify_name::modify_name;
#[modify_name(outer)]
#[modify_name(inner)]
fn func() {}
fn main() {
func_outer_inner();
}
これで無事コンパイルが通ります! cargo expand の結果は...?
$ cargo expand --bin modify-name
Checking modify-name v0.1.0 (/home/namn/workspace/qiita_adv_articles_2025/programs/adv14/modify-name)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.17s
#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use modify_name::modify_name;
fn func_outer_inner() {}
fn main() {
func_outer_inner();
}
関数名は func_outer_inner になっている...!どんどん後ろに指定した名前を連ねていくマクロなので、 func に近いほど先に実行されているはずです。
先に outer がついていることからわかる通り、 属性マクロは上から下に 実行されていくということがわかりました!
まとめ・所感
良いトリビアを提供できたでしょうか...?
そして冒頭のコードについては、 #[tokio::main] と #[hooq(anyhow)] の付与順についてはより展開結果が単純な #[hooq(anyhow)] を持ってくるのが正解というか読みやすいでしょう。
#[hooq::hooq(anyhow)]
#[tokio::main]
async fn main() -> anyhow::Result<()> {
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
println!("Hello, world!");
Ok(())
}
一応cargo expand
$ cargo expand
Checking hooq_tokio_pg v0.1.0 (/home/namn/workspace/qiita_adv_articles_2025/programs/adv14/hooq_tokio_pg)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.08s
#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
#[allow(unused)]
use ::anyhow::Context as _;
fn main() -> anyhow::Result<()> {
let body = async {
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
{
::std::io::_print(format_args!("Hello, world!\n"));
};
Ok(())
};
#[allow(
clippy::expect_used,
clippy::diverging_sub_expression,
clippy::needless_return,
clippy::unwrap_in_result
)]
{
return tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.expect("Failed building the Runtime")
.block_on(body);
}
}
ちなみに今回の場合だと tokio を先に持っていくとコンパイルエラーになってしまいました。
hooqマクロは多分ですが他のマクロと併用したいという場合が特に多いんじゃないかと思います。そのためhooq作者として今回こちらについてまとめられたのは地味に良かったですね。
ここまで読んでいただきありがとうございました!
-
15日は月曜日で、月曜日は記事のインプレッションが稼ぎやすいという"噂"があるのでちょっと面白めの記事にしたいと考えてます。まぁ変わらないでしょうけど... ↩