C# の未対処例外ダイアログを変更する

概要

この記事には、C# (.NET Framework) アプリケーションの未対処例外ダイアログを変更する方法について書いています。 ここでの「未対処例外ダイアログ」とは、 プログラムの実行中に例外が発生し、それがプログラム中で適切に対処 (プログラム的には catch)されなかった場合に表示されるダイアログを指します。 なお、この記事中では適切に対処されなかった例外を「未対処例外」と呼びます。

背景

.NET Framework では実行時に発生した例外がメインスレッドを抜けようとすると (catch されずに Main メソッドの上まで上がろうとすると) 標準の未対処例外ダイアログが表示され、 そのダイアログ上で簡単なエラーの内容確認やアプリケーションの継続および停止の判断ができます。 問答無用で「ご不便をおかけして申し訳ありません」と終了してしまう通常のシステムと比べれば、 これが非常に有用な機能である事は間違いありません。 しかし、標準の未対処例外ダイアログは場合により不適切な UI となりえます。 たとえば、ソフトウェア技術者でない人にはダイアログのメッセージを理解できないでしょう。 あるいは、発生した例外の内容を開発者のサーバへ送信する機能を持たせたい、 といった要望にも対応できません。 そこで、この未対処例外ダイアログを変更する方法を調べました。

.NET の例外機構について

普段はあまり意識しない事ですが、例外とスレッドは強く関係しています。 プログラムの実行中に例外が throw されて catch された場合、 制御の流れは throw の箇所から catch ブロックへと移動します。 このように例外機構は制御の流れを変化させるという意味で、 ループや条件分岐と同じ制御構文の一種と捉えられます。 制御構文の一種と考えればスレッドごとに例外が並行して発生しうる事が分かりやすいでしょう。 そして例外の発生がスレッドごとに起こりうるならば、 例外を最終的に「処理」する未対処例外ダイアログもスレッドごとに設定されていると予想できます。

Windows アプリケーションでの処理

まずは Windows アプリケーションのメインスレッドで未対処例外が発生した場合について説明します。 結論から言うと、この場合は次のイベントが発生します。

System.Windows.Forms.Application.ThreadException

このイベントにハンドラを登録すると、 未対処例外が発生した時にはそのハンドラが呼び出され、 標準のダイアログが表示されなくなります (おそらくイベントハンドラが一つも登録されていなければ標準ダイアログを出すのでしょう)。 もしアプリケーション独自の未対処例外ダイアログを表示したい場合は、 このイベントハンドラから独自のダイアログを表示します。 なお .NET Framework はハンドラ中で明示的にアプリケーションを終了させない限り、 制御を例外発生箇所に戻して処理を継続しようとします。 独自のダイアログにアプリケーションを即時停止するボタンを用意する場合は、 そのボタンが押された後で Application.Exit を呼ぶ等する必要がありますので注意して下さい。
(なお、これは厳密には「メインスレッドのメッセージループ中で」発生した場合です。 つまり Application. Run でメッセージループを回す前に未対処例外が発生した場合は該当せず、 それは次の節で説明する場合になります。)

コンソール アプリケーションでの処理

続いてコンソール アプリケーション場合(厳密には前節の場合以外)について説明します。 基本的には Windows アプリケーションの場合と同様にイベントハンドラを登録し、そこで処理します。 しかし Windows アプリケーションの場合とは違う点がいくつかあります。 一つ目は、標準でこの場合には Windows アプリケーションで表示されるような未対処例外ダイアログは表示されず、 例外発生後にアプリケーションを継続実行する事もできません。 二つ目は、ハンドラを設定する単位はスレッドではなく「アプリケーション ドメイン」になっている事です。

アプリケーション ドメインについての説明は次の節に譲ります。 ここでは、アプリケーション ドメインが複数のスレッドを含む単位という事だけ知っておけば問題ありません。 アプリケーション ドメインは複数のスレッドを含み、 すべてのスレッドはそれぞれがどこか一つのアプリケーション ドメインに含まれます(たぶん)。 したがって、あるスレッドで未対処例外が発生した場合、 その例外は必ずどこかのアプリケーション ドメインで発生しています。 では本題に進みましょう。 アプリケーション ドメインで発生した未対処例外を処理するには、 次のイベントをハンドリングします。

AppDomain.CurrentDomain.UnhandledException

このイベントにハンドラを登録すると、 未対処例外が発生した時にはそのハンドラが呼び出されるようになります。 もしアプリケーション独自の未対処例外ダイアログを表示したい場合は、 Windows アプリケーションでの場合と同じく、 このイベントハンドラから独自のダイアログを表示します。 ただし、このハンドラが呼ばれた後で制御を例外発生箇所に戻して処理を継続する事はできません。 これには、おそらく最終的に未対処例外をハンドリングするレベルがスレッドの上位 (アプリケーション ドメイン) である事が影響しているのではないかと私は勝手に予想しています。
(戻せる方法を知っている方、教えて下さい(苦笑))

アプリケーション ドメインとは

少し脱線して、アプリケーション ドメインについて簡単に説明します。 アプリケーション ドメインは .NET Framework で新たに導入された概念です。 現在、多くのシステムではプロセスごとに異なる仮想メモリ空間が割り当てられています。 これは暴走したプロセスの動作が他のプロセスに影響する事を防ぐためであり、 安全性(セキュリティ)向上が目的でした。 しかしどの世界にも共通するように安全性と利便性は相反します。 具体的にはプロセス間通信が非常にやりにくくなりました。 このような問題を解決するために、

プロセスは複数のスレッドを含む

という形の中にもう一層のレイヤーを加えて次のようにしました。

プロセスは複数のアプリケーションドメインを含み、
アプリケーションドメインは
複数のスレッドを含む

アプリケーションドメインはプロセスよりも小さく、 スレッドよりも大きい実行単位と言えます。 ざっくばらんに表現すると「プチ プロセス」といったところでしょう。 ドメイン間ではメモリ空間が共通なので通信は簡単ですが、 互いの領域を超える操作には必ず .NET Framework のセキュリティチェックが入るためセキュリティも高まります。 私は「スレッドで十分ではないのか」と考え込んでしまいましたが、 ユーザ定義スクリプトなど信用できないコードをプロセス内で実行する場合などは、 セキュリティチェックが入らないスレッドよりも アプリケーション ドメインの方が良い、という話もあるようです。 何にせよ、かなり大きな規模のアプリケーションで利用する機能のようではあります。

サンプルプログラム

興味のある方はどうぞ。
cs_exception_dialog.zip

役に立たない小話

おまけです。 System. Windows. Forms. Application. ThreadException イベントは通常のイベントと異なり、 複数のハンドラを追加すると最後に追加したハンドラだけが呼び出されます。 実際にはこれが問題になる事は無いと思いますが、 面白かったので書いておきました。 なお AppDomain. CurrentDomain. UnhandledException については通常のイベントと同様、 複数のハンドラを追加すれば追加した順番に呼び出されていきます。 まあ、実際には複数のハンドラを追加する事は無いでしょう(苦笑)。

参照

  1. 一色 政彦: 適切に処理されなかった例外をキャッチするには?; Insider.NET (@IT), 2005
  2. Microsoft: Application Domains Overview; MSDN Library, 200?
  3. 吉松 史彰: アプリケーション・ドメイン; Insider.NET (@IT), 2003