OITA: Oika's Information Technological Activities

@oika 情報技術的活動日誌。

C# WeakEventManagerがFatalExecutionEngineError(0x80131623)を投げるケース

「弱いイベント」(Weak Event)パターンで
WeakEventManagerを継承したクラスが以下のようなエラーを投げて
びっくりしたのでメモ。

ランタイムの重大なエラーが発生しました。エラーのアドレスは 0xXXXXXXXX、スレッド 0xXXXX です。エラー コードは 0x80131623 です。これは CLR のバグであるか、またはユーザー コードのアンセーフまたは確認不可能な部分にバグがある可能性があります。このバグの一般的な原因には、スタックが壊れる可能性のある COM-interop または PInvoke のユーザー マーシャリング エラーが含まれています。

※この記事では.NET 4.0のWeak Eventパターン実装について書いてます。
.NET 4.5ではWeak Event周りが強化されていて
書き方もちょっと変わってるはずなので、参考にならないかも。

Weak Eventパターンって何よって人には、詳しくはこのへんを見ていただければですが、
簡単にいうと、イベント購読によるメモリリークを防ぐための書き方です。
リスナオブジェクトへの参照がなくなったときに
イベント購読が自動的に破棄される便利なAPIを使います。

はじめに、(.NET 4.0までの)シンプルな実装例を。
まず、Hogeイベントを持っているクラス。
HogeEventArgsは適当なEventArgsのサブクラスです。

class HogeEventSource {  
    public event EventHandler<HogeEventArgs> Hoge;  
  
    //Hogeイベントを発生させる  
    public void RaiseHoge() {  
        var h = Hoge;  
        if (h != null) h(this, new HogeEventArgs());  
    }  
}  

そして、このHogeイベントを弱参照で利用するために
WeakEventManagerを継承したクラスを作る。

class HogeEventManager : WeakEventManager {  
    //現在のインスタンスを取得するプロパティ  
    private static HogeEventManager CurrentManager {  
        get {  
            var mng = GetCurrentManager(typeof(HogeEventManager)) as HogeEventManager;  
            if (mng == null) {  
                mng = new HogeEventManager();  
                SetCurrentManager(typeof(HogeEventManager), mng);  
            }  
            return mng;  
        }  
    }  
  
    protected override void StartListening(object source) {  
        (source as HogeEventSource).Hoge += OnHoge;  
    }  
  
    protected override void StopListening(object source) {  
        (source as HogeEventSource).Hoge -= OnHoge;  
    }  
  
    private void OnHoge(object sender, HogeEventArgs e) {  
        DeliverEvent(sender, e);  
    }  
  
    //ハンドラを登録するための公開メソッド  
    public static void AddListener(HogeEventSource source, HogeEventListener listener) {  
        CurrentManager.ProtectedAddListener(source, listener);  
    }  
}  

このHogeイベントを購読するクラスではIWeakEventListenerインタフェースを実装する。 

class HogeEventListener : IWeakEventListener {  
    //Hogeイベントのハンドラを登録する  
    public void RegisterHogeHandler(HogeEventSource eventSource) {  
        HogeEventManager.AddListener(eventSource, this);  
    }  
  
    //全てのWeakEventを受け取るハンドラ  
    public bool ReceiveWeakEvent(Type managerType, object sender, EventArgs e) {  
        //WeakEventManagerの型でふるい分けする  
        if (managerType == typeof(HogeEventManager)) {  
            OnHoge(sender, (HogeEventArgs)e);  
            return true;  
        }  
  
        //知らない型のときはfalseを返す  
        return false;  
    }  
  
    //Hogeイベント受信時の処理  
    private void OnHoge(object sender, HogeEventArgs e) {  
        Console.WriteLine("Hogeイベントが発生しました。");  
    }  
}  

これで準備完了。以下のように使います。

    static void Main(string[] args) {  
        var source = new HogeEventSource();  
        var listener = new HogeEventListener();  
        listener.RegisterHogeHandler(source);  
  
        source.RaiseHoge();  //これは受信される  
  
        //リスナの参照を削除  
        listener = null;  
        GC.Collect();  
        GC.WaitForPendingFinalizers();  
        GC.Collect();  
  
        source.RaiseHoge();  //これは受信されない  
    }  

ここからが本題。
リスナクラスでReceiveWeakEventメソッドを実装するときに、
メソッドからfalseを返すと、冒頭の例外が発生するらしい。

まあ普通はReceiveWeakEventメソッドからfalseを返すケースは
コーディングミスでしかないと思うのでそれは良いのだけど、
いきなりこの例外が出たときに、これが原因になるって情報が
全然見つけられなかったのでハマった。

結局ググって見つかったのは以下。
WeakEventManager throws FatalExecutionEngineError when IWeakEventListener.ReceiveWeakEvent method returns false
「仕様です」との回答のようだ。

そうだと分かっていれば別に良いのだけど、
この例外がリスナのReceiveWeakEventメソッドからでなくて
WeakEventManagerのDeliverEventメソッドから発生してるように見えるので
とても分かりにくかった。

個人的には、ReceiveWeakEventからはfalseを返さないで
代わりにArgumentExceptionとか、NotSupportedExceptionとか?を
自分で投げるようにしておくほうが分かりやすいかなと思ったり。