Unwinding

Rustのエラーハンドリングには階層的なスキームが存在します。

  • もし何かが、明確な理由があって欠如しうる場合、Optionが使われます
  • もし何かおかしなことが起こった際に合理的な対処方法がある場合、Resultが使われます
  • もし何かおかしなことが起こった際に合理的な対処方法がない場合、そのスレッドはpanicします
  • もし何か破滅的な出来事が起こった場合、プログラムはabortします

大抵の状況では圧倒的にOptionとResultが好まれます。というのもAPIのユーザーの 裁量次第でpanicやabortさせることも可能だからです。panicはスレッドの正常処理を 停止し、stackをunwind、全ての関数が即座にreturnしたかのようにデストラクタ を呼び出します。

バージョン1.0以降のRustはpanic時に2種類の対処法を用いるようになりました。 大昔、Rustは今よりもErlangによく似ていました。Erlangと同様、Rustには軽量のタスク が存在し、タスクが続行不可能な状態に陥った際にはタスクが自分自身をpanicによって killすることを意図して設計されていました。JavaやC++の例外と違い、panicはいかなる 場合においてもcatchすることはできませんでした。panicをcatchできるのはタスクの オーナーのみであり、その時点で適切にハンドリングされるか、そのタスク (訳注: オーナーとなるタスク)自体がpanicするかのどちらかでした。

この一連の流れの中では、タスクのデスクトラクタが呼ばれなかった場合にメモリー及び その他のシステムリソースがリークを起こす可能性があったため、unwindingが重要でした。 タスクは通常の実行中にも死ぬ可能性があると想定されていたため、Rustのこういった 特徴は長期間実行されるシステムを作る上でとても不適切でした。

Rustが現在の形に近づく過程で、より抽象化を少なくしたいという時流に押された スタイルのプログラミングが確立していき、その過程で軽量のタスクは重量級の OSスレッドに駆逐・統一されました (訳注: いわゆるグリーンスレッドとネイティブスレッドの話)。しかしながら Rust1.0の時点ではpanicはその親スレッドによってのみ補足が可能という仕様であった ため、 panicの補足時にOSのスレッドを丸ごとunwindしてしまう必要 があったのです!不幸なことにこれはゼロコスト抽象化というRustの思想と 真っ向からぶつかってしまいました。

一応 catch_panic というunstableなAPIが存在し、これによってスレッドをspawn することなくpanicを補足することはできます。

訳注: その後 recover -> catch_unwind と変更され、Rust1.9でstableになりました。

とはいえあくまでこれは代替手段として用いることを推奨します。現在のRustのunwind は「unwindしない」ケースに偏った最適化をしています。unwindが発生しないとわかって いれば、プログラムがunwindの準備をするためのランタイムコストも無くなるためです。 結果として、実際にはJavaのような言語よりもunwindのコストは高くなっています。 したがって通常の状況ではunwindしないようなプログラムの作成を心がけるべきです。 非常に大きな問題の発生時やプログラミングエラーに対してのみpanicすべきです。

Rustのunwindの取り扱い方針は、他の言語のそれと根本から同等になるように設計されて はいません。したがって他の言語で発生したunwindががRustに波及したり、逆にRustから 多言語に波及したりといった動作は未定義となっています。 FFIの構築時には絶対に全てのpanicを境界部でキャッチしなくてはなりません。 キャッチの結果どのように対処するかはプログラマ次第ですが、とにかく何かを しなくてはなりません。そうしなければ、良くてアプリケーションがクラッシュ・炎上します。 最悪のケースではアプリケーションがクラッシュ・炎上しません。完全にボロボロの状態 のまま走り続けます。