Rustと例外
TechRustは言語仕様としては例外を持ちませんが、panic!
やDropの実装のためにC++の例外相当の機構を利用しています。
参考:
確かに、「panic時にも確実にDropを行う」はC++のtry~finallyに似ていますね。
単に似ているだけでなく、rfcs#2945を見るとC++の例外(およびそれに互換したもの)とRustのpanicが混ざるケースも考慮されているようです。
rust_eh_personality
rustcの吐く.llを見ると、@rust_eh_personality
という関数があることがわかります。
declare noundef i32 @rust_eh_personality(i32, i32 noundef, i64, %"unwind::libunwind::_Unwind_Exception"*, %"unwind::libunwind::_Unwind_Context"*) unnamed_addr #6
unstable bookを見ると、3種類の実装がある(あった)ことがわかります。
- eh_personality: libpanic_unwind/emcc.rs (EMCC)
- eh_personality: libpanic_unwind/gcc.rs (GNU)
- eh_personality: libpanic_unwind/seh.rs (SEH)
EMCCはemscripten(wasm環境)、SEHはWindows用、GNUがそれ以外だと思われます。
現在ではrustのリポジトリにlibpanic_unwindというモジュールはなく、代わりに以下があるようです。
各スタックフレームはpersonality functionへの参照を持ちます。unwind時、スタックフレーム毎にpersonality functionが呼び出されます。このときpersonality functionは発生した例外の情報を受け取り、「unwindを続けるか止めるか」を返します。
例えばC++でいうと、発生した例外が当該フレームでcatchされている場合は「unwindを止める」、されていない場合は「unwindを続ける」になります。
「unwindを止める」、つまりこのフレームが例外を処理することが確定したあと、unwinderはcleanup phaseに入り、personality functionをもう一度(別の用途で)呼び出します。cleanup phaseでは、personality functionはどのようなcleanup処理が必要かを決定します。
catch_unwind
Rustのpanicはあまりcatchするものではありませんが、本当に必要な場合はcatch_unwindで止めることができます。catch_unwindは引数として渡されたクロージャを実行し、そこでunwindが発生した場合にそれをcatchします。
catch_unwindの実装を見ると、最終的にintrinsics::r#try
を呼ぶことがわかります。intrinsicsはRustで記述できないほど低レベルな関数で、.llを見ると以下のような実装であることがわかります。
define internal i32 @__rust_try(ptr %0, ptr %1, ptr %2) unnamed_addr #3 personality ptr @rust_eh_personality {
entry-block:
invoke void %0(ptr %1)
to label %then unwind label %catch
then: ; preds = %entry-block
ret i32 0
catch: ; preds = %entry-block
%3 = landingpad { ptr, i32 }
catch ptr null
%4 = extractvalue { ptr, i32 } %3, 0
call void %2(ptr %1, ptr %4)
ret i32 1
}
Personality function
https://itanium-cxx-abi.github.io/cxx-abi/abi-eh.html を参考に、rust_eh_personality_implを読んでいきます。
unsafe extern "C" fn rust_eh_personality_impl(
version: c_int,
actions: uw::_Unwind_Action,
_exception_class: uw::_Unwind_Exception_Class,
exception_object: *mut uw::_Unwind_Exception,
context: *mut uw::_Unwind_Context,
) -> uw::_Unwind_Reason_Code {
- version: ABIのバージョン。1固定。
- actions: このPHがなんのために呼び出されたか。以下のビットマスク。
- static const _Unwind_Action _UA_SEARCH_PHASE = 1; (検索フェーズ。_URC_HANDLER_FOUNDまたは_URC_CONTINUE_UNWINDを返す)
- static const _Unwind_Action _UA_CLEANUP_PHASE = 2;(cleanupフェーズ。_URC_CONTINUE_UNWINDまたは_URC_INSTALL_CONTEXTを返す)
- static const _Unwind_Action _UA_HANDLER_FRAME = 4;
- 検索フェーズで「このフレームがcatchするよ」と伝えたとき、cleanupフェーズで当該フレームを処理するときにこのフラグが立つ。
- static const _Unwind_Action _UA_FORCE_UNWIND = 8;
- cleanupフェーズで使われる。このフラグが立っているとき、この例外をキャッチしてはいけない(longjmpやスレッドの終了時に使われる?)
- _exception_class: 例外の型を示す値。Rustでは不使用。
- exception_object: 例外オブジェクトへのポインタ。
- context: unwind context
if version != 1 {
return uw::_URC_FATAL_PHASE1_ERROR;
}
バージョンのチェック。
let eh_action = match find_eh_action(context) {
Ok(action) => action,
Err(_) => return uw::_URC_FATAL_PHASE1_ERROR,
};
find_eh_actionはこれ
検索フェーズ
if actions as i32 & uw::_UA_SEARCH_PHASE as i32 != 0 {
match eh_action {
EHAction::None | EHAction::Cleanup(_) => uw::_URC_CONTINUE_UNWIND,
EHAction::Catch(_) | EHAction::Filter(_) => uw::_URC_HANDLER_FOUND,
EHAction::Terminate => uw::_URC_FATAL_PHASE1_ERROR,
}
}
cleanupフェーズ
match eh_action {
EHAction::None => uw::_URC_CONTINUE_UNWIND,
// FORCE_UNWINDフラグが立っていたら何もせずunwindを続ける
EHAction::Filter(_) if actions as i32 & uw::_UA_FORCE_UNWIND as i32 != 0 => uw::_URC_CONTINUE_UNWIND,
EHAction::Cleanup(lpad) | EHAction::Catch(lpad) | EHAction::Filter(lpad) => {
uw::_Unwind_SetGR(
context,
UNWIND_DATA_REG.0,
exception_object as uintptr_t,
);
uw::_Unwind_SetGR(context, UNWIND_DATA_REG.1, 0);
// SetIPしてからINSTALL_CONTEXTを返すことでlandingpadに処理を進める
uw::_Unwind_SetIP(context, lpad);
uw::_URC_INSTALL_CONTEXT
}
EHAction::Terminate => uw::_URC_FATAL_PHASE2_ERROR,
}
}
}