トップ回答者
C++/CLIによるアンマネージクラスのラップ

質問
-
こんちには.
C++で作ったアンマネージのクラスを以下のようなイメージでマネージのクラスでラップしています.
// アンマネージクラス class UnmngClass { public: void Method() { ... }; }; // マネージクラス ref class MngClass { private: UnmngClass* m_UnmngObj; MngClass() { m_UnmngObj = new UnmngClass(); } ~MngClass() { delete m_UnmngObj; } !MngClass() { delete m_UnmngObj; } public: void Method() { m_UnmngObj->Method(); }; };
ところが、アンマネージクラスの関数(UnmngClass::Method())を実行途中で
マネージクラスのファイナライザ(MngClass::!MngClass())が呼ばれて
アンマネージのクラスが解放されてしまうことがあります.
Webで調べてみると、アンマネージコードを実行しているときは
そのオブジェクトが使用中であっても、GCによって解放される(ファイナライザが呼ばれる)ので
それを防ぐためにはGC::KeepAlive()を使わなければならないというようなことがわかりました↓
http://msdn.microsoft.com/ja-jp/library/system.gc.keepalive(VS.71).aspx
http://msdn.microsoft.com/ja-jp/library/ms182300(VS.80).aspx
そうであれば、UnmngClassのメンバ関数を呼び出す全てMngClassのメソッドにKeepAlive()が必要になるのでしょうか?
また、ラップするような場合でなくても
C++/CLIであればマネージクラスの関数内で容易にアンマネージ関数を呼ぶことができますよね(例えば、printf()など).
このような場合でも全てKeepAliveが必要になるのでしょうか?
以上、よろしくお願いします.
回答
-
そのラップクラスに対する参照が残っていないのでは?
自身のメソッドを実行中であっても Finalize されることがあるのでご注意ください。参考記事
http://d.hatena.ne.jp/NyaRuRu/20060626/p4# 今回のコードは、マネージコードの手を離れて実行し続けるコードなんだろうか。
質問スレッドで解決した場合は、解決の参考になった投稿に対して「回答としてマーク」のボタンを押すことで、同じ問題に遭遇した別のユーザが役立つ投稿を見つけやすくなります。- 回答としてマーク 山本春海 2011年1月13日 4:56
-
mngObj->Method();
があるにも関わらず、先行してファイナライザ(MngClass::!MngClass())が実行されているのがわかります.Windows 7 (x86) 環境の Visual Studio 2008 SP1、Visual Studio 2010 でそれぞれ Release 構成でビルドし、エクスプローラから実行しましたが、同じ結果にはなりませんでした。(ファイナライザが先行することがない)
考えづらいのですが、環境によるんでしょうか?
参考までに環境をお聞かせください。Azulean様のご紹介頂いた記事では、本来カプセル化で隠蔽しなくてはならないハンドルを公開している設計が問題とも言える気がしますが
#私には、そう読めました
今回のパターンは、それとはまた問題が違う気もいたします.提起したかった問題の本質は「this が予想外に速く消されること」です。(注記:消される=ファイナライザが実行される)
ハンドル云々はあくまでイメージであり、問題の本質ではありません。
今回遭遇している問題は「this が予想外に速く消されること」と捉えていますので、同じことだと思います。
質問スレッドで解決した場合は、解決の参考になった投稿に対して「回答としてマーク」のボタンを押すことで、同じ問題に遭遇した別のユーザが役立つ投稿を見つけやすくなります。- 回答としてマーク 山本春海 2011年1月13日 4:56
-
>マネージクラスのメソッドの中でアンマネージメソッドだけを実行をしている場合は全て対象なのか…?
KeepAliveを入れておくのがよいでしょう。
>アンマネージメソッドとマネージメソッドの呼び出しが混在している場合はどうなのか…?
マネージオブジェクトがどこで参照されているか、で必要か否か決まります。私が一番最初に記述した内容で判断してください。良く分からなければ、まずはGCとJITの動作をMSDNでよく読んで理解してください。実際の動作検証も必要でしょう。
(私からは、パターンが多すぎて調べきれない、言及しきれない、という事情もあります。)
>はたまた、マネージコードのみの場合でも、何らかの条件で起こりうる問題なのか…?
私の紹介したLinkでは、起こりうると記述されています。C#で書かれていますが、事情はC++/CLIも同じでしょう。
いずれも、Stack風に記述すればよいですが、不都合でしょうか?
GC.KeepAlive以外で何か方法があれば良いのですが、現状見つかっていません。
Microsoftに問い合わせてみるのも手です。U.S. Forumで聞いてみるのもひとつの手です。(ただ、色好い回答があるかは疑問です)
ちなみに、一つ前のSample Codeで現象を確認したところ、私の環境では発生しませんでした。
- 回答としてマーク 山本春海 2011年1月13日 4:56
すべての返信
-
そのラップクラスに対する参照が残っていないのでは?
自身のメソッドを実行中であっても Finalize されることがあるのでご注意ください。参考記事
http://d.hatena.ne.jp/NyaRuRu/20060626/p4# 今回のコードは、マネージコードの手を離れて実行し続けるコードなんだろうか。
質問スレッドで解決した場合は、解決の参考になった投稿に対して「回答としてマーク」のボタンを押すことで、同じ問題に遭遇した別のユーザが役立つ投稿を見つけやすくなります。- 回答としてマーク 山本春海 2011年1月13日 4:56
-
>UnmngClassのメンバ関数を呼び出す全てMngClassのメソッドにKeepAlive()が必要になるのでしょうか?
>このような場合でも全てKeepAliveが必要になるのでしょうか?
そんなことはないです。
>そのオブジェクトが使用中であっても
まず、GCやJITはUnmanaged Codeレベルでの参照を感知しません。Managed Codeレベルでの参照しか感知しません。Unmanaged CodeでObjectが使用されている間、Source Code記述者がそのObjectが参照されていること(= GCの対象にならないこと)を保障する必要があります。従って、GCされるということは、そのObjectはManaged Codeレベルで使用中ではない、使用済みである、ということになります。
使用中かどうかは、そのObjectへの参照が残っているか否かで判断されます。例えば、以下のCaseではGC::KeepAliveの処理が必要になります。
ref class MyString { public: MyString() { Console::WriteLine("MyString"); str = new wchar_t[10]; str[0] = L'C'; str[1] = 0; } ~MyString() { Console::WriteLine("~MyString"); str[0] = L'~'; } !MyString() { Console::WriteLine("!MyString"); str[0] = L'!'; } const wchar_t* GetString() { return str; } private: wchar_t *str; };
※このClassはTestのために、意図的に文字列Bufferを解放しません。
int main(array<System::String ^> ^args) { MyString ^myStr = gcnew MyString; const wchar_t *str = myStr->GetString(); wprintf(L"%s\n",str); Console::WriteLine("Collect"); GC::Collect(); GC::WaitForPendingFinalizers(); Thread::Sleep( 5000 ); //(※1) myStr->GetString();以降myStrの参照が行われていないため、 //すでにFinalizerが実行されている可能性がある。 //この場合、GC::KeepAliveが必要 wprintf(L"%s\n",str); Thread::Sleep( 5000 ); //GC::KeepAlive(myStr); }
※1に対応するには、GC::KeepAliveを呼ぶ必要があります。
実行環境に依存しますので、いろいろとSource Codeに手を加えてRelease ModuleをExplorerから実行し、Testしてみてください。このCaseでは、最適化が影響しています。処理単位で参照がなくなった時点で、積極的にGCの対象にされます。GC::KeepAlive内部では特に何もしておらず、最適化を抑制するために参照を行っているだけです。興味があれば、ILDasmで確認してみてください。
私の環境ですと、※1の時点ですでにFinalizerが実行されていました。
>ところが、アンマネージクラスの関数(UnmngClass::Method())を実行途中で
>マネージクラスのファイナライザ(MngClass::!MngClass())が呼ばれて
>アンマネージのクラスが解放されてしまうことがあります
その現象が発生するMngClass利用側のSource Codeは、どのようになっていますか?
ところで、
>C++/CLIの仕様でデストラクタ(MngClass::~MngClass)は何度も呼ばれる可能性があり
これはどういう状況で起こるのでしょうか?
deleteを同じObjectに対し複数回呼んだ場合は分かりますが、このような単純なCoding Miss以外で起こりうる状況を知りたいです。
-
メソッドのローカル変数に保持されているということは、それだけでリファレンスカウント1とならないのでしょうか? なので myStr = null; をしない限り解放されないものだと思っていました。
# この部分、はっきりとは知らないので間違っていれば否定してください。C++/CLIのデストラクタはIDisposable::Dispose()メソッドになると思います。となると「オブジェクトの Dispose メソッドが複数回呼び出された場合、そのオブジェクトは最初の呼び出し以外は無視する必要があります 」なのではと思い書きました。
-
メソッドのローカル変数に保持されているということは、それだけでリファレンスカウント1とならないのでしょうか? なので myStr = null; をしない限り解放されないものだと思っていました。
上記の例の場合、JIT の最適化でローカル変数 myStr がなくなりますので、参照がない状態になります。
なお、デバッグ実行した場合は JIT の最適化が抑制されるのでローカル変数は残るかもしれません。
(デバッグ実行と、デバッグなしでの実行で逆アセンブルしたコードの差を確認すると見えてきます)# NyaRuRu さんの記事、その先の記事の流し読み、手元の実行結果を踏まえて判断しています。
質問スレッドで解決した場合は、解決の参考になった投稿に対して「回答としてマーク」のボタンを押すことで、同じ問題に遭遇した別のユーザが役立つ投稿を見つけやすくなります。 -
有難うございます。
>C++/CLIのデストラクタはIDisposable::Dispose()メソッドになると思います。...
そうですね。
C#の場合、結果的にCleanup処理の記述がDispose Methodに集約されるため、disposedのCheckが必要になります。
しかし、C++/CLIの場合、DestructorとFinalizerに分かれることと、DestructorとFinalizerはどちらかしか呼ばれないため、意図的に複数回直接呼び出さない限りdisposedのCheckが必要ない=1回しか呼ばれない、と理解しています。Source Code記述者による~CXxxや!CXxxの直接的な呼び出しや、複数回のdeleteはCoding上の不具合である、という前提での判断です。C++的な考え方に近いでしょうか。
ご提示されたLinkの英語版を参照しましたが、C++/CLI Sample Codeでもそのような前提に立った処理を記述しているように思えます。
確かにLink先には以下のように記述されていますね。
If an object's Dispose method is called more than once, the object must ignore all calls after the first one.
GC等のFrameworkによる呼び出しよりも、Source Code記述者による意図的な呼び出しに、より強い意味を置いているように思えます。
但し、経験的に判断しているところもありますので、これが正しいかどうか確信はありません。
従って、C++/CLIにおいてその状況が起こりうるSource Codeがどのようなものか想像がつかなかったため、質問をしました。或いは、例えば、C#からC++/CLIのClassを利用する場合、C#の仕様や文化からDisposeを複数呼び出されるような/呼び出すような状況があるのでしょうか。その場合は要注意ですね。
何れにせよ別の理由として、そのClassで扱っているUnmanaged Resourceを、部分的な成功の後処理の場合や失敗も想定し、NULL Check等を行い解放処理を行うことは必要になりますね。Unmanaged Resource解放専用のMethodを用意する場合もそうですね。
-
>それだけでリファレンスカウント1とならないのでしょうか?
結果的に”はい”になります。
この挙動はUnmanaged Codeの有無に関係しません。容易にManaged CodeとUnmanaged Codeが混在・呼び出しが出来るC++/CLIにおいて、特にその動作を期待したいのですが、残念ながらそうはならないです。ちょっとしたTrapですね。
Debugger経由ですとこの挙動が分からないですし、少し厄介な話です。
この挙動を簡単に制御できるような仕組みがあれば良いですね。
以下が参考になります。
[GC.KeepAlive Method]
http://msdn.microsoft.com/en-us/library/system.gc.keepalive(v=VS.80).aspx
[When do I need to use GC.KeepAlive?]
http://blogs.msdn.com/b/oldnewthing/archive/2010/08/13/10049634.aspx
C#ではusing keywordを使う方法が紹介されているので、C++/CLIではstack風に記述するのもひとつの手かと思います。(要検証です。)
-
佐祐理様、Azulean様、kozz様:
返信ありがとうございます.
レスポンスが遅くなって申し訳ありません.
>その現象が発生するMngClass利用側のSource Codeは、どのようになっていますか?
完全なサンプルコードを掲載させていただきます.
// アンマネージクラス class UnmngClass { public: UnmngClass() { printf("UnmngClass::UnmngClass()\n"); } ~UnmngClass() { printf("UnmngClass::~UnmngClass()\n"); } void Method() { printf("UnmngClass::Method()\n"); } }; // マネージクラス ref class MngClass { private: UnmngClass* m_UnmngObj; public: MngClass() { Console::WriteLine("MngClass::MngClass()"); m_UnmngObj = new UnmngClass(); } ~MngClass() { Console::WriteLine("MngClass::~MngClass()"); delete m_UnmngObj; } !MngClass() { Console::WriteLine("MngClass::!MngClass()"); delete m_UnmngObj; } void Method() { Console::WriteLine("MngClass::Method()"); m_UnmngObj->Method(); } }; int main(array<System::String ^> ^args) { MngClass^ mngObj = gcnew MngClass(); GC::Collect(); GC::WaitForPendingFinalizers(); mngObj->Method(); Console::WriteLine("Press Enter to Exit..."); Console::ReadLine(); return 0; }
これの実行結果は
MngClass::MngClass()
UnmngClass::UnmngClass()
MngClass::!MngClass()
UnmngClass::~UnmngClass()
MngClass::Method()
UnmngClass::Method()
Press Enter to Exit...となり、
mngObj->Method();
があるにも関わらず、先行してファイナライザ(MngClass::!MngClass())が実行されているのがわかります.
kozz様の言われているように、この現象はRelease版(/O2)で、VC++からではなくexeを直接起動したときのみ起こります.
続くコードで、マネージクラスのメソッド(MngClass::Method())を呼び出しているのにも関わらず
GC::Collect()で収集されてしまっているのは、やはりインライン展開で直接アンマネージ関数呼び出しに展開されているため…?
クラスの利用者に、この辺の問題を意識してKeepAlive()を入れろ、というのは酷ですし
クラスのどのメンバを、どの順番で呼び出すかは利用者次第ですので
やはり、全部のメソッドにKeepAlive()を埋め込んでいくしかないのかなぁ…というのが、本質問の発端です.
Azulean様のご紹介頂いた記事では、本来カプセル化で隠蔽しなくてはならないハンドルを公開している設計が問題とも言える気がしますが
#私には、そう読めました
今回のパターンは、それとはまた問題が違う気もいたします.
KeepAlive()を呼ぶのが基本だ、と言われればそれまでなのですが
今回のような実装はよくあるパターンと思いますし、かなりフェータルな問題とも思うのですが
その割には周知されていないように思いますし、ホントにこうするのが正しい方法なのか、腑に落ちていないという感じです.
以上
-
mngObj->Method();
があるにも関わらず、先行してファイナライザ(MngClass::!MngClass())が実行されているのがわかります.Windows 7 (x86) 環境の Visual Studio 2008 SP1、Visual Studio 2010 でそれぞれ Release 構成でビルドし、エクスプローラから実行しましたが、同じ結果にはなりませんでした。(ファイナライザが先行することがない)
考えづらいのですが、環境によるんでしょうか?
参考までに環境をお聞かせください。Azulean様のご紹介頂いた記事では、本来カプセル化で隠蔽しなくてはならないハンドルを公開している設計が問題とも言える気がしますが
#私には、そう読めました
今回のパターンは、それとはまた問題が違う気もいたします.提起したかった問題の本質は「this が予想外に速く消されること」です。(注記:消される=ファイナライザが実行される)
ハンドル云々はあくまでイメージであり、問題の本質ではありません。
今回遭遇している問題は「this が予想外に速く消されること」と捉えていますので、同じことだと思います。
質問スレッドで解決した場合は、解決の参考になった投稿に対して「回答としてマーク」のボタンを押すことで、同じ問題に遭遇した別のユーザが役立つ投稿を見つけやすくなります。- 回答としてマーク 山本春海 2011年1月13日 4:56
-
うわぁ…
Windows 7 (x64)、Visual Studio 2010で発生しました。アセンブリ(Assemblyクラスじゃなくて.asmの方)を出力させると確かに、ローカル変数に保存することなく、スタック上に残したまま処理していますね。
0000b 73 00 00 00 00 newobj ??0MngClass@@$$FQE$AAM@XZ
00010 28 00 00 00 00 call ?Collect@GC@System@@$$FSMXXZ
00015 28 00 00 00 00 call ?WaitForPendingFinalizers@GC@System@@$$FSMXXZ
0001a 28 00 00 00 00 call ?Method@MngClass@@$$FQE$AAMXXZmngObj->Method();を2行に増やすだけで
0000b 73 00 00 00 00 newobj ??0MngClass@@$$FQE$AAM@XZ
00010 0a stloc.0 ; mngObj$
00011 28 00 00 00 00 call ?Collect@GC@System@@$$FSMXXZ
00016 28 00 00 00 00 call ?WaitForPendingFinalizers@GC@System@@$$FSMXXZ
0001b 06 ldloc.0 ; mngObj$
0001c 28 00 00 00 00 call ?Method@MngClass@@$$FQE$AAMXXZ
00021 06 ldloc.0 ; mngObj$
00022 28 00 00 00 00 call ?Method@MngClass@@$$FQE$AAMXXZになるから、テスト目的の何もしないコードでもない限りはこうならないんでしょうけど。
あとC++らしい書き方としては
{
MngClass mngObj;
GC::Collect();
GC::WaitForPendingFinalizers();
mngObj.Method();
}とかすればいいし。
-
返信ありがとうございます.
>参考までに環境をお聞かせください。
メインの環境は、Windows 7 (x64) + VS 2005(SP2)ですが
Windows 7 (x64) + VS 2008(SP1)、Windows XP (x86) + VS 2005(SP2)でも同様の動作をします.
>今回遭遇している問題は「this が予想外に速く消されること」と捉えていますので、同じことだと思います。
仰るとおりです.
ただ、マネージコードでアンマネージハンドルを扱う場合に限定されるのなら、そのような設計を避けるか
アンマネージハンドルを扱う場合は注意しろ、で済むのですが
今回の場合、どんなときに注意すればよい(KeepActiveを入れればよい)のかがよくわかりません.
マネージクラスのメソッドの中でアンマネージメソッドだけを実行をしている場合は全て対象なのか…?
アンマネージメソッドとマネージメソッドの呼び出しが混在している場合はどうなのか…?
はたまた、マネージコードのみの場合でも、何らかの条件で起こりうる問題なのか…?
>テスト目的の何もしないコードでもない限りはこうならないんでしょうけど。
実際に処理を行っているものでも、同様の問題が発生しています.
ソースが大きいのと機密上の関係で、実際の処理は掲載できませんが
簡単に書くと、以下のような感じで例外が発生してしまいます.
// アンマネージクラス class UnmngClass { private: char* m_Buf; public: UnmngClass() { m_Buf = new char[100]; } ~UnmngClass() { delete[] m_Buf; m_Buf = NULL; } void Method() { char localBuf[100]; // ↓ここに達するより前に // MngClassのファイナライザ⇒UnmngClassのデストラクタが呼び出されて // それにより、m_BufがNULLとなるので、NullReferenceExceptionが発生する memcpy(localBuf, m_Buf, 100 * sizeof(char)); } };
-
マネージクラスのメソッドの中でアンマネージメソッドだけを実行をしている場合は全て対象なのか…?
アンマネージメソッドとマネージメソッドの呼び出しが混在している場合はどうなのか…?
はたまた、マネージコードのみの場合でも、何らかの条件で起こりうる問題なのか…?単純に new してすぐ使って、それでおしまいのコードだと発生する可能性があるということでは?
C# でも発生する事例をすでに紹介していますよね。>テスト目的の何もしないコードでもない限りはこうならないんでしょうけど。
実際に処理を行っているものでも、同様の問題が発生しています.ファイナライザを持つクラスなのに、デストラクタを実行させるための delete をしないんですか?
少なくとも、インスタンスメソッドを呼び出す、delete を呼び出すの 2 箇所で参照されるので、インライン化される危険性は低いと思ったのですが、違うのでしょうか?マネージクラスのファイナライザで解放はあくまで最終手段であって、お行儀よく delete(.NET 的には Dispose メソッド)を呼んでおくべきだと思います。
質問スレッドで解決した場合は、解決の参考になった投稿に対して「回答としてマーク」のボタンを押すことで、同じ問題に遭遇した別のユーザが役立つ投稿を見つけやすくなります。 -
>マネージクラスのメソッドの中でアンマネージメソッドだけを実行をしている場合は全て対象なのか…?
KeepAliveを入れておくのがよいでしょう。
>アンマネージメソッドとマネージメソッドの呼び出しが混在している場合はどうなのか…?
マネージオブジェクトがどこで参照されているか、で必要か否か決まります。私が一番最初に記述した内容で判断してください。良く分からなければ、まずはGCとJITの動作をMSDNでよく読んで理解してください。実際の動作検証も必要でしょう。
(私からは、パターンが多すぎて調べきれない、言及しきれない、という事情もあります。)
>はたまた、マネージコードのみの場合でも、何らかの条件で起こりうる問題なのか…?
私の紹介したLinkでは、起こりうると記述されています。C#で書かれていますが、事情はC++/CLIも同じでしょう。
いずれも、Stack風に記述すればよいですが、不都合でしょうか?
GC.KeepAlive以外で何か方法があれば良いのですが、現状見つかっていません。
Microsoftに問い合わせてみるのも手です。U.S. Forumで聞いてみるのもひとつの手です。(ただ、色好い回答があるかは疑問です)
ちなみに、一つ前のSample Codeで現象を確認したところ、私の環境では発生しませんでした。
- 回答としてマーク 山本春海 2011年1月13日 4:56