Rustの世界では、エラーが起きた時、しばしばコンパイラが友人のように寄り添ってくれます。しかし、ときにはコンパイラも頼りにならない状況に遭遇します。そう、スタックオーバーフローがその一つです。これは、プログラムが自らの終わりを告げる前に無限の深淵に落ちるかのような状況です。今日はそんなスリリングなバグとの戦いについて、実体験をもとにお話しします。
スタックオーバーフローを引き起こすコード
さて、私が書いたRustのコードは以下の通りです。見た目は無害そうですが、内に秘めた力は想像以上です。
fn main() {
let user_1 = User::default();
println!("user_1: {}", user_1.username);
}
struct User {
username: String,
email: String,
}
impl Default for User {
fn default() -> Self {
Self {
username: String::new(),
..Default::default() // ここで、あるべきではない再帰の旅が始まる...
}
}
}
この小さなコードをcargo run
と共に実行すると、ターミナルは次のような不穏なメッセージを投げ返してきました。
$ cargo run
...
thread 'main' has overflowed its stack
fatal runtime error: stack overflow
zsh: abort cargo run
デバッグの挑戦
興味深いことに、スタックオーバーフローは私たちのスタックトレースを直接見ることを拒絶します。通常、エラーメッセージは犯人を指し示す手がかりを含んでいますが、スタックオーバーフローは犯人を隠します。このミステリーを解決するためには、backtrace-on-stack-overflow
という名のルーペを使う必要があります。
スタックトレースの活用
まずは、この便利なクレートを私たちのプロジェクトに招待します。
cargo add backtrace-on-stack-overflow
そして、main
関数の序章に、以下の一行を加えることで、スタックオーバーフローが起きた際のバックストーリーを明らかにします。
unsafe { backtrace_on_stack_overflow::enable() };
再度の試行
これで準備は完了です。再びcargo run
を叩いてみると、今度は犯人の足跡が明らかになります。
$ cargo run
...
Stack Overflow:
0: backtrace::backtrace::libunwind::trace
at /Users/mei/.cargo/registry/src/github.com-1ecc6299db9ec823/backtrace-0.3.69/src/backtrace/libunwind.rs:93:5
backtrace::backtrace::trace_unsynchronized
at /Users/mei/.cargo/registry/src/github.com-1ecc6299db9ec823/backtrace-0.3.69/src/backtrace/mod.rs:66:5
1: backtrace::backtrace::trace
at /Users/mei/.cargo/registry/src/github.com-1ecc6299db9ec823/backtrace-0.3.69/src/backtrace/mod.rs:53:14
2: backtrace::capture::Backtrace::create
at /Users/mei/.cargo/registry/src/github.com-1ecc6299db9ec823/backtrace-0.3.69/src/capture.rs:176:9
# これ以降、無限にループしてしまいます。
ここから先のスタックトレースを注意深く眺めると、Default::default()
が再帰的に自分自身を呼び出しているのが分かります。これがスタックオーバーフローの真犯人です。..Default::default()
が User
構造体の他のフィールドに対するデフォルト値の生成を試みるとき、User
自体のデフォルト実装を再帰的に呼び出してしまい、結果として無限ループに陥ってしまっていたのです。
解決への道
バグの正体が明らかになったので、修正は単純です。User
構造体のデフォルト実装を修正して、再帰的な呼び出しをなくすだけです。
impl Default for User {
fn default() -> Self {
Self {
username: String::new(),
email: Default::default(), // StringのDefault値
}
}
}
これで、User::default()
を呼び出したときに再帰的な問題が解消され、User
のインスタンスを安全に作成できるようになります。プログラムを再実行すると、今度はエラーなく実行されます。
学び
Rustにおいても再帰は慎重に扱うべきであるということ、そしてバグに直面したときには、適切なツールを使って深堀りすることの大切さです。
そして何より、エラーメッセージはただのガイドラインであり、時にはさらに深く掘り下げて真実を探究する勇気も必要だということを思い出させてくれました。プログラマーとしてのスキルを磨く旅は、このようなバグとの格闘を通じて続いていくのです。
~~ fin ~~
参考文献