3
1

More than 3 years have passed since last update.

構造化例外処理(SEH)について with DirectX

Last updated at Posted at 2021-03-29

普通の例外

ゼロ徐算やアクセス違反などの低レベルな例外(SE)を取ることができません。クラッシュしてしまいます。

try {
  throw std::exception("hoge");
} catch (exception capture) {
  std::cout << capture.what() << std::endl;
} catch (...) {
  std::cout << "Unknown" << std::endl;
} finally {
  ...
}

構造化例外

これでSEを補足できるようになります。
ただし、関数内でtry-catchと併用はできません。
std::functionで主実装を外に出しておけば幾分かマシかもしれません。

_EXCEPTION_POINTERS* info = nullptr;
__try {
  auto hoge = 1 / ZERO; // 0徐算
  nullObject->Invoke(); // アクセス違反
} __except(info = GetExceptionInformation(), EXCEPTION_EXECUTE_HANDLER) {
  auto code = info->ExceptionRecord->ExceptionCode;
  std::cout << code << std::endl;
  throw exception(std::to_string(code)); // これで外側のtry-catchに伝播できる(※)
} __finally {
  ...
}

SEを自動で例外に変換するオプション

image.png

このオプションを点けておくことで、パフォーマンス上のペナルティと引き換えに、SE発生時に自動でC++の例外に変換してくれます。ただし、SEの中身は取れません。
また、Debugビルド時はオプションを無効にしていても変換してしまうようです。(お節介かな…)

try { ... }
catch(std::exception e) { ... }
catch(...){
  // ここに来る、「何が起こったか」は分からない
}

_set_se_translator

EHaを有効化したうえでこれを登録しておくことで、好きな例外の形式に変換できます。

_set_se_translator([](unsigned int code, _EXCEPTION_POINTERS* ep) -> void {
  throw exception(std::to_string(code));
});

try { ... }
catch(std::exception e) {
  // ここに来る、_set_se_translatorで、例外オブジェクト内に入れた内容が参照できる
} catch(...){ ... }

DirectXのSEH

DirectXはCOMなので、本来エラーはSEではなくHRESULTにて報告されます。
しかし、HRESULTを返さないメソッドや、COM ObjectのRelease時に問題が発生するとSEを飛ばすこともあるようです。

SEHブロック/関数内でthrowできない

これが本題です。
もしかするとCOM全般の話なのかもしれませんが、throwをしてもそれ自体がSEになり、呼び出し元の関数に制御が戻りません。

C++例外が使えないとなると、もう引数や戻り値でSEを伝えるしかありません。引数を操作できない_set_se_translatorは使い物になりません。(グローバル変数を使えばできなくはないですが…)

妥協案

上記の制約の中、私が思うコードの劣化を最小限に抑えつつ対応できる方法かなという書き方です。

void HandleStructuredException(std::function<void()> *callback, unsigned int &code) {
    __try { callback->operator()(); }
    __except (EXCEPTION_EXECUTE_HANDLER) { code = GetExceptionCode(); }
}

unsigned int seCode = 0;
std::function<void()> callback = [&]() -> void {
  // メイン処理
};
HandleStructuredException(&callback, seCode);
if(seCode != 0) throw std::exception(std::to_string(seCode).data());

GetExceptionCodeの代わりにGetExceptionInformationを使うことで、より詳細な(アドレスなど)を取ることもできますが、ポインタなのでDeepコピーが必要です。

callbackをポインタで渡していますが、SEHを実装する関数ではオブジェクトアンワインドというスコープ脱出時のデストラクタが使えないためです。
MSDN コンパイラエラー C2712

Widnowメッセージループ内外の例外伝搬(おまけ)

メッセージループ内では発生した例外をループ外に伝搬することはできません。より正確には、DispatchMessage → ... → WNDCLASSEX.lpfnWndProcと呼び出されますが、ここを例外で駆け上がることができません。

メッセージループでは複数のWindowがコンカレントに同居していることを考えればまぁ仕方ないような気もします。
こちらはMSG構造体のWHNDにWindowオブジェクトを紐づけておけば、インスタンスフィールドを使って例外の受け渡しができる分_set_se_translatorに比べれば幾分かマシな気がします。

int main(int argc, char* argc[]) {
  ...
  auto windowHandle = CreateWindow( ... );
  SetWindowLongPtr(windowHandle , GWLP_USERDATA, reinterpret_cast<LONG_PTR>(this));
  ...
  try {    
    MSG message;
    while (GetMessage(&message, NULL, 0, 0)) {
      auto window = reinterpret_cast<Window *>(GetWindowLongPtr(message.hwnd, GWLP_USERDATA));
      if(window != nullptr && window->Error() != nullptr) {
        throw *window->Error();
      }
      ...
    }
  } catch { ... }
  ...
}

MSDN WindowProc callback function

まとめ

そもそも、どこまでハンドリングすべきなのかという問題があります。
SEHしなきゃいけない時点でバグですし…ハンドルして回復できるとも限りません。
とはいえ、仮にできることが無かったとしてもクラッシュ前にごめんねダイアログを出したいものです。

世に出回るC++で描かれたWindowsアプリはこの辺りどうしているのでしょうかね。

参考

3
1
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1