こんにチュア!本記事は hooqアドベントカレンダー2025 1日目の記事です!初日から1日遅刻して申し訳ないです
ドキュメント作成で手間取ってしまい...
Rustのエラーロギングをお手軽に行うための、 hooq という属性マクロを作りました!
GitHub: https://github.com/anotherhollow1125/hooq
チュートリアル: https://anotherhollow1125.github.io/hooq/latest/ja/index.html
このhooqアドベントカレンダーは折角作ったhooqクレートを宣伝するために25記事投稿しようという狂気の個人カレンダーです。hooqの便利な使い方や、Rustのエラーハンドリング周りの話、あるいはたまにhooqと関係ない属性マクロ作成に用いたテクニック等を紹介していく予定です!
hooqマクロとは?
hooqマクロの機能自体はとてもシンプルで、「 ? 演算子・ return ・末尾式にメソッドをフックできるマクロ」です! Question に HOOk するので hooq と名付けました。
READMEに記載の例がわかりやすいと思います。
use hooq::hooq;
#[hooq]
#[hooq::method(.map(|v| v * 2))]
fn double(s: &str) -> Result<u32, Box<dyn std::error::Error>> {
let res = s.parse::<u32>()?;
Ok(res)
}
fn double_expanded(s: &str) -> Result<u32, Box<dyn std::error::Error>> {
let res = s.parse::<u32>().map(|v| v * 2)?;
Ok(res)
}
fn main() {
assert_eq!(double("21").unwrap(), double_expanded("21").unwrap());
}
今回の場合hooqマクロは ? を見つけると、 .map(|v| v * 2) を挿入するので、 double 関数と double_expanded 関数が同一の処理になっています!
hooqマクロはどういう時に便利か?
このマクロを作ろうと思い立ったきっかけは自分のエラーデバッグ方法にありました。たくさん ? があるソースコードで考えてみます。
fn main() -> anyhow::Result<()> {
let i = "10".parse::<u32>()?;
let j = "20".parse::<u64>()?;
let k: u32 = j.try_into()?;
let m = i.checked_sub(k).ok_or_else(|| anyhow::anyhow!("checked_sub failed."))?;
println!("{m}");
Ok(())
}
Error: checked_sub failed.
Playground: https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=1a0651968bbbf62681b8284e7a4b0051
Rustで小さいツールを作ることが多い筆者は、あまりこのエラー出力が好みではありません。今回はたまたまわかりやすい位置でエラーになってくれましたが、64行目や65行目でエラーが起きていた時などを考えてみるとどうでしょうか...? エラーが何行目で起きたかがすぐにわからない というのはコードが増えるほど、関数のネストが深まるほど深刻な問題になっていきます。
この問題の解決策の一つとして、anyhow クレートにはエラー時に"コンテキスト"という形で情報をスタックできる .with_context(...) というとても便利なメソッドがあります!ここで line!() マクロを使うと行数を取得できる のでエラー発生箇所がわからないという問題が解決されます!
「予めエラーになる箇所を予想する」というエスパーはできないので、かつての筆者はこれを ? の前に 全部 自分で挿入していました。
use anyhow::Context;
fn main() -> anyhow::Result<()> {
let i = "10".parse::<u32>()
.with_context(|| format!("@{}", line!()))?;
let j = "20".parse::<u64>()
.with_context(|| format!("@{}", line!()))?;
let k: u32 = j.try_into()
.with_context(|| format!("@{}", line!()))?;
let m = i.checked_sub(k) // with_context は Option型にも付けられる!
.with_context(|| format!("@{}", line!()))?;
println!("{m}");
Ok(())
}
Error: @11
Playground: https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=64e37ee5c5802291f46afc5e2c8b424c
11行目で発生しているということはわかったけど...いや!読みにくい!!!!高々エラーの位置を楽に知るためだけにこんなに何度も .with_context(...) を書きたくない!
このようなボイラープレートの削減に便利なRustの道具は何か? そう、マクロです 。
というわけで、 .with_context(...) を自動挿入してくれる便利マクロを作りました!
use hooq::hooq;
#[hooq(anyhow)]
fn main() -> anyhow::Result<()> {
let i = "10".parse::<u32>()?;
let j = "20".parse::<u64>()?;
let k: u32 = j.try_into()?;
let m = i.checked_sub(k)?;
println!("{m}");
Ok(())
}
Error: [src/main.rs:8:29]
8> i.checked_sub(k)?
|
このようにソースコードの可読性を損なわずにいい感じの出力がなされます!
cargo expand コマンドで展開してみると、hooqが各行に .with_context(...) を挿入していることがわかります。
use hooq::hooq;
#[allow(unused)]
use ::anyhow::Context as _;
fn main() -> anyhow::Result<()> {
let i = "10"
.parse::<u32>()
.with_context(|| {
let path = "src/main.rs";
let line = 5usize;
let col = 32usize;
let expr = " 5> \"10\".parse::<u32>()?\n |";
::alloc::__export::must_use({
::alloc::fmt::format(
format_args!("[{0}:{1}:{2}]\n{3}", path, line, col, expr),
)
})
})?;
let j = "20"
.parse::<u64>()
.with_context(|| {
let path = "src/main.rs";
let line = 6usize;
let col = 32usize;
let expr = " 6> \"20\".parse::<u64>()?\n |";
::alloc::__export::must_use({
::alloc::fmt::format(
format_args!("[{0}:{1}:{2}]\n{3}", path, line, col, expr),
)
})
})?;
let k: u32 = j
.try_into()
.with_context(|| {
let path = "src/main.rs";
let line = 7usize;
let col = 30usize;
let expr = " 7> j.try_into()?\n |";
::alloc::__export::must_use({
::alloc::fmt::format(
format_args!("[{0}:{1}:{2}]\n{3}", path, line, col, expr),
)
})
})?;
let m = i
.checked_sub(k)
.with_context(|| {
let path = "src/main.rs";
let line = 8usize;
let col = 29usize;
let expr = " 8> i.checked_sub(k)?\n |";
::alloc::__export::must_use({
::alloc::fmt::format(
format_args!("[{0}:{1}:{2}]\n{3}", path, line, col, expr),
)
})
})?;
{
::std::io::_print(format_args!("{0}\n", m));
};
Ok(())
}
正直Rustのエラーロギングのベストプラクティスがわからない
「そのようなことをせずとも、Rustで『実行時エラーが発生した行数を知る方法』は他にもあるだろう」と思われた読者がいるかもしれません。
他に筆者が認識していたのは下記2つですが なんとなく 使いたいと思えない方法だったのもhooqを作ったきっかけだったりします。
-
std::backtrace::Backtraceを始めとしたバックトレース技術- 環境変数
RUST_LIB_BACKTRACE=1を付けないといけない -
thiserrorで型を定義する際に、フィールドに処理フロー的要素(それも、通常のRustの処理フローとは異なる)を含めなければならない
- 環境変数
-
tracingクレートのSpanTrace(あるいは#[tracing::instrument(err)])の利用- 純粋に
tracingのことをよくわかっていなかった - 「エラーの行数を得る目的に対しては過剰なのではないか」という気持ちがあった
- 純粋に
しっかり利用していた上で文句を言っていたわけではないので本当はこれらの方法を選択するべきだったかもしれません。しかし .with_context(...) でのデバッグに慣れすぎたあまり、これらに取り組む前に気づいたらhooqを作ってしまっていた、というわけです。
筆者はRustのエラーロギングのベストプラクティスを知らないままずっとRustを使ってきたのでした...そのため「いやhooqは良くない。こっちのエラーロギングの方が良いでしょう」という一家言がある方は優しく教えてくれたら嬉しいです1!
まとめ
「残り24記事も何を書くんだ?」という話ですが、hooqのカレンダーであると見せかけてその他のエラーロギング手法の学習・整理もちゃっかりやってみようかなと思ってたりします!
とにもかくにも、筆者にとっても読者の皆さんにとっても、hooqがRustのエラーロギングについて考え直すきっかけになってくれていたら嬉しいです。
ここまで読んでいただきありがとうございました ![]()
-
ちなみにここで紹介したBacktrace、SpanTraceはcolor-eyre というクレートを使うことでいい感じに出力することが可能です。hooqは eyreクレート とも一緒に使えるので、別な機会に3つの手法を贅沢に使う例を見せたいと思います。 ↩