この記事の主張
-
#[xxx]のような属性マクロを利用して挿入されたトークン内では、line!()マクロ はつねに#[xxx]がある行を指し、トークンが挿入される行を指さない - 挿入行の番号は、関連するトークンのSpanから
proc_macro::Span::lineを呼ぶことで得られる
こんにチュア!本記事は hooqアドベントカレンダー 4日目の記事です!
hooqはメソッドを ? 演算子にフックする属性マクロです。
hooq属性マクロでは、フック対象が存在する行数を表す $line メタ変数を設けています。
「メタ変数を用意しなくても、 line!() マクロ ではダメなのですか?」と以前問われたことがありました。
今回はhooqメイキング記事の一つとして、「 line!() ではダメなのです!」ということを紹介したいと思います。
実験してみる
理屈がどうこうというよりは事実として正確な行数を得られなかったので、再現手順を示したいと思います。
というわけでデモ用の属性マクロを用意しましょう!
拙著 Rust 属性風マクロを軽くハンズオン の属性マクロ dbg_stmts を使います。
ハンズオン内容をすべて実装する必要はないです。とりあえず各ファイルを次のようにしてください。
[package]
name = "dbg_stmts"
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", "extra-traits"] }
mod impls;
use proc_macro::TokenStream;
use quote::ToTokens;
use syn::{ItemFn, parse_macro_input};
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()
}
use syn::{Block, parse_quote};
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();
}
ここで [dbg_stmts] を [L{}] に変え、ここに line!() が入るようにしてみます!
use syn::{Block, parse_quote};
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)); },
+ parse_quote! { println!("[L{}] {}", line!(), stringify!(#stmt)); },
stmt.clone(),
]
})
.collect();
}
main関数の方もハンズオンに従い用意します。
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 run
Compiling dbg_stmts v0.1.0 (/home/namn/workspace/qiita_adv_articles_2025/programs/dbg_stmts)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.38s
Running `target/debug/dbg_stmts`
[L3] let hoge = 10;
[L3] let fuga = hoge + 20;
[L3] let bar = vec! [hoge, fuga];
[L3] println! ("{:?}", bar);
[10, 30]
[L3] 100
全部 [L3] 、つまり3行目扱いされてしまいました!3行目は #[dbg_stmts] が付与されている行です。そうです。 属性マクロにおいて、 line!() は属性マクロ自体の付与行を指す のです!
line!() マクロ部分のproc_macro::Span を上書きしてみたら変わるかも?と思って色々試してみましたが、どの方法をとっても置き換わる行数は変わらず 3 でした。どうも、 line!() マクロが参照するSpan情報は手続きマクロから利用できるSpanとは別物らしいです。
正確な行数は、各 stmt の Span についてproc_macro::Span::lineによって得ることができます。
use syn::{Block, parse_quote, spanned::Spanned};
pub fn insert_println_between_stmts(block: &mut Block) {
block.stmts = block
.stmts
.iter()
.flat_map(|stmt| {
+ let span = stmt.span();
+ let line = span.unwrap().line();
vec![
- parse_quote! { println!("[L{}] {}", line!(), stringify!(#stmt)); },
+ parse_quote! { println!("[L{}] {}", #line, stringify!(#stmt)); },
stmt.clone(),
]
})
.collect();
}
実行結果は期待通りになります!
$ cargo run
Compiling dbg_stmts v0.1.0 (/home/namn/workspace/qiita_adv_articles_2025/programs/dbg_stmts)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.45s
Running `target/debug/dbg_stmts`
[L6] let hoge = 10;
[L7] let fuga = hoge + 20;
[L8] let bar = vec! [hoge, fuga];
[L9] println! ("{:?}", bar);
[10, 30]
[L10] 100
proc_macro::Span::line は1.88にて安定化
proc_macro::Span::lineは、実はhooqマクロを作り始めたころは安定化されていませんでした。そのため行数表示は、他の代替手段を模索したりnightlyでの提供などを考えていました。しかしnightlyで行数を得る場合マクロを利用するユーザー側のcargoもnightlyで実行しなければならず、実用面では難しいものがありました。
あまりにも安定化してほしくて、エイプリールフールの時にネタにしたぐらいでした。
その願いが届いたとでも言うのでしょうか...?
なんとRust 1.88で安定化されました! ![]()
Rust 1.88はLet chainsが導入されたバージョンなのですが、それが霞むぐらい嬉しかったことを覚えています。
MSRVは1.88になってしまいましたが、hooqは無事フルstableで提供できるようになったわけです...!
まとめ・所感
というわけで、でもないですが、hooqのように正確な $line メタ変数を提供している属性マクロは他にはないんじゃないかと思います...!似たようなことをしたい人の参考になれば幸いです。
ここまで読んでいただきありがとうございました!