ま、そんなところで。

ニッチな技術系メモとか、車輪を再発明してみたりとか.

(続)Unmanagedの C++ から Managedのクラスを使う

の続き.

ManagedクラスとUnmanagedクラスの生存期間差の問題

Unmanaged C++からManagedクラスを使用する場合、ただManagedクラスでUnmanaged C++クラスをWrapしてやるだけではダメなケースがあります.
Unmanaged クラスのポインタをVisitorやObserverパターンで使うケースです.

// 
// SomeListenerのManagedWrapper
//
ref class ListenerWrap {
public:
    // コンストラクタ
    ListenerWrap(SomeListener* cb)
    : cb_(cb)
    {

    }
    // デストラクタ
    ~ListenerWrap()
    {
        // ファイナライザへ委譲
        // C++CLIの場合, GC::SuppressFinalize(this)は無条件で行われるので呼び出しは不要.
        this->!ListenerWrap();
    }
    // ファイナライザ
    !ListenerWrap()
    {
        // ポインタを使った終了処理など.
        if (cb_) {
            cb_->close();  // Unmanaged オブジェクトの cb_ の後始末(★ ここが問題)
            cb_ = nullptr;   // ListenerWrapとSomeListenerポインタの関連付けを解除.
        }
    }

    virtual void Exec(int param);
protected:
    SomeListener* cb_;
};
// 問題になるコード・・・
void SomeClass::DoWithCallBack(int param, SomeListener* cb)
{
    // 未定義動作の危険!!
    // cbWrapはスコープアウトによってgc対象になるだけ. ファイナライザ実行まで行われるとは限らない.
    // GCによってcbWrapがFinalizeされるより前に cb が無効ポインタになってしまう可能性がある.

    ListenerWrap^ cbWrap = gcnew ListenerWrap(cb);
    someManagedObject_->Exec(param, cbWrap);
}

Unmanagedクラスのローカル変数は スコープアウトで削除 されますが、Managedクラスのハンドルは スコープアウトでGC対象になるだけ です.

分かりやすい例だと下記のようなコードです.

void DoSomethingClearSample()
{
    // localCbのほうが先に無効になる.
    // cbWrapのファイナライザはlocalCbが削除された後.
    SomeListener localCb;
    ListenerWrap^ cbWrap = gcnew ListenerWrap(&localCb);
             :
             :
}

この生存期間の僅かなギャップにより、Unmanagedクラスのポインタを保持するManagedクラスがFinalizerでUnmanagedクラスのポインタを使って何らかの終了処理を行うような場合に無効ポインタへのアクセスを引き起こすことになります.

C#のusingステートメントC++標準ライブラリのstd::unique_ptrのように、スコープアウトしたときに確実にDisposeしたりdeleteしたりする仕組みが欲しいなぁ、と思っていたところ、ちゃんと用意されてました.

こういうむず痒いところもさりげなーく手当てしてるのがすごい.
さすがMS.

msclr::auto_gcroot<T>テンプレートクラス

前の記事 の gcroot<T> は、UnmanagedクラスがManagedクラスのハンドルを保持するための補助的Unmanagedテンプレートクラスでした.

こちらはその機能を発展したものになります.
Unmanagedクラスのメンバに保持できるのは同じですが、こちらは デストラクタで保持しているManagedクラスのハンドルをdeleteしてくれます.

マネージドクラスのハンドルを delete するということは、IDisposable の Dispose メソッド呼び出しと同じですから、これを使うと Unmanagedのオブジェクトがdeleteされるタイミングで保持しているManagedのハンドルもDisposeさせる仕組みができます.

メンバメソッドも標準ライブラリのスマートポインタ std::unique_ptr<T> とほぼ同じで、C++の標準ライブラリと同じ感覚で使えるように設計されてました.
これは嬉しい.

// auto_gcrootヘッダをincludeします.
#include <msclr/auto_gcroot.h> 
             
SomeWrap^ someWrap = gcnew SomeWrap();
if (someWrap)
{
    // someWrapはスコープアウトでdispose.
    msclr::auto_gcroot<SomeWrap^> wrapped = someWrap;
    SomeWrap^ rawHandle = wrapped.get(); // rawハンドルの取得
               :
    wrapped->SomeFunc(); // -> でメソッド呼び出し
               :
    someWrap = nullptr;
    // someWrap.reset(other); // resetも同じ
    // someWrap.release(); // releaseもあります!
}

Managedクラスハンドルの生存期間をスコープで管理できるようになれば、生存期間のズレによる問題は容易に解決できますね.

void SomeClass::DoWithCallBack(int param, SomeListener* cb)
{
    // auto_gcrootで、スコープアウトのタイミングで確実にdelete(=Dispose)する.
    // このスコープ内ならcbが有効であることは確実なので、安全にcbの終了処理とnullptr化が行える.
    msclr::auto_gcroot<ListenerWrap^> cbWrap = gcnew ListenerWrap(cb);
    someManagedObject_->Exec(param, cbWrap);
}

これでcbポインタが無効になってからfinalizerが呼ばれることはなくなりました.

スコープ利用に特化した msclr::auto_handle<T> テンプレート

auto_gcroot<T> と似たようなクラスで auto_handle<T> ってのもあります.

これは、auto_gcroot<T>のManagedクラス版.

auto_handle<T> はManagedな参照(ref)型テンプレートクラスなので、Unmanagedクラスのメンバとして保持できないという違いがあるものの、機能としてはauto_gcroot<T> とほぼ同等です.

#include <msclr/auto_gcroot.h> 
#include <msclr/auto_handle.h> // auto_handle.hが別にあります.
             
{
    msclr::auto_gcroot<SomeWrap^>  wrapped1 = gcnew SomeWrap();
    // auto_handleにはハンドルのoperator=がないので、ハンドルのsetはコンストラクタかresetを利用する.
    msclr::auto_handle<SomeWrap> wrapped2( gcnew SomeWrap() ); // 割安
               :
               :
}

auto_gcrootがUnmanagedクラスのメンバとして保持することを想定したものであるのに対して、こちらはスコープ利用を想定したものといえそうです.
ちょうど、C++CLIC#のusingステートメント相当の処理を実現するためのもの 、といった位置づけでしょうか.

スコープ利用に限っては auto_gcroot<T> も auto_handle<T> と同じ作用をするのですが、内部処理としてGCHandle型のAllocとFreeが行われない分だけ auto_handle<T> のほうが処理コスト的に割安な感じです.

ということで、先の例は auto_handle のほうが最適な選択と言えますね.
書き換えると以下のような感じになりました.

void SomeClass::DoWithCallBack(int param, SomeListener* cb)
{
    // auto_handleで、スコープアウトのタイミングで確実にdelete(=Dispose)する.
    msclr::auto_handle<ListenerWrap> cbWrap;
    cbWrap.reset( gcnew ListenerWrap(cb) );
    someManagedObject_->Exec(param, cbWrap);
}

auto_gcroot も auto_handle も地味に便利なんですが、あまり使用例は見かけないですね・・.
ヘッダのコードを見るとそんな難しいものでもなさそうなので、大体の人は同等のクラスを自作しちゃってるのかもしれません.


参考情報

C++ Support Library | Microsoft Docs
auto_gcroot Class | Microsoft Docs
auto_handle | Microsoft Docs
unmanagedクラスにmanagedなメンバを持たせる - schima.hatenablog.com