Boxing and Unboxing in C++/CLI
C++/CLI のボックス化で見えにくい現象に遭遇したのでメモ。大昔に誰かが書いたコードがひどいだけですが。
こんなソースがありましたとさ。
//
// cppclrtest.cpp
//
#include <windows.h>
#pragma comment(lib, "ole32.lib")
using namespace System;
ref class BadClass {
private:
UInt32 ^mUint32;
public:
BadClass() {
mUint32 = gcnew UInt32;
}
UInt32 Run() {
// DebugBreak();
*mUint32 = CoInitialize(NULL);
if (mUint32 == (UInt32)0x80010106) {
*mUint32 = 42;
}
return *mUint32;
}
};
void RunTest(String ^Str) {
if (Str == "HOGEHOGE") {
Console::WriteLine(L"Matched.");
}
BadClass ^Class = gcnew BadClass();
Console::WriteLine(L"BadClass.Run returned {0:x8}", Class->Run());
}
int main(array<System::String ^> ^args) {
RunTest("piyopiyo");
return 0;
}
いろいろと突っ込みどころはありそうですが、少なくともコンパイル エラーも警告も出ません。手元の環境で実行するとこうなります。環境は Windows 8.1 x64 + Update 1 です。
D:\VSDev\Projects\cppclrtest\x64\Release> cppclrtest
BadClass.Run returned 80010106
CoInitialize が 80010106 を返してエラーになるようです。この現象自体は以下の KB に合致しそうなので何でもないのですが、コードをパッと見るとおかしい感じがします。なぜ if 文に引っかかって 42 にならないのでしょう。
BUG: “HRESULT - 0x80010106” error occurs when you run a managed Visual C++ application in Visual Studio .NET
http://support.microsoft.com/kb/824480
実は、BadClass.Run にある if 文における左辺は UInt32^ で、右辺は UInt32 となっていて型が違っています。なぜコンパイラーに怒られないかというと、右辺がボックス化されるからでしょう。しかし、2 つの UInt32 オブジェクトの比較になり、ともに同じ値 80010106 を持っているので比較結果が true になってもよさそうなものです。
Boxing (C++/CLI)
http://msdn.microsoft.com/en-us/library/hh875061.aspx
というわけでコメント アウトしておいた DebugBreak を有効にしてコンパイルされたコードを見てみます。
0:000> g
ModLoad: 00007ffb`c69f0000 00007ffb`c6a24000 C:\Windows\system32\IMM32.DLL
...
ModLoad: 00007ffb`c4e10000 00007ffb`c4ec7000 C:\Windows\system32\OLEAUT32.dll
(2494.103c): Break instruction exception - code 80000003 (first chance)
KERNELBASE!DebugBreak+0x2:
00007ffb`c46e9e3a cc int 3
0:000> .loadby sos clr
0:000> !clrstack
c0000005 Exception in C:\Windows\Microsoft.NET\Framework64\v4.0.30319\sos.clrstack debugger extension.
PC: 00007ffb`9e42d3f3 VA: 00000000`00000000 R/W: 0 Parameter: 00000000`00000000
0:000> !clrstack
OS Thread Id: 0x103c (0)
Child SP IP Call Site
000000907648e8a8 00007ffbc46e9e3a [InlinedCallFrame: 000000907648e8a8] <Module>.DebugBreak()
000000907648e8a8 00007ffb5c42253a [InlinedCallFrame: 000000907648e8a8] <Module>.DebugBreak()
000000907648e880 00007ffb5c42253a DomainBoundILStubClass.IL_STUB_PInvoke()
000000907648e940 00007ffb5c42242b <Module>.RunTest(System.String)
000000907648e9a0 00007ffb5c4220d5 <Module>.mainCRTStartupStrArray(System.String[])
000000907648ed00 00007ffbbba3a8b3 [GCFrame: 000000907648ed00]
0:000> gu
00007ffb`5c42253a 488b4540 mov rax,qword ptr [rbp+40h] ss:00000090`7648e8e0=0000009076674360
0:000> gu
00007ffb`5c42242b 488b17 mov rdx,qword ptr [rdi] ds:00000090`00003748=0000009000003758
0:000> !U .
Normal JIT generated code
<Module>.RunTest(System.String)
Begin 00007ffb5c422370, size 148
00007ffb`5c422370 53 push rbx
00007ffb`5c422371 55 push rbp
...
00007ffb`5c422413 e898ee605f call clr!JIT_WriteBarrier (00007ffb`bba312b0)
00007ffb`5c422418 48bd5033001090000000 mov rbp,9010003350h
00007ffb`5c422422 488b6d00 mov rbp,qword ptr [rbp]
00007ffb`5c422426 e855a5ecff call 00007ffb`5c2ec980 (<Module>.DebugBreak(), mdToken: 0000000006000056)
>>> 00007ffb`5c42242b 488b17 mov rdx,qword ptr [rdi]
00007ffb`5c42242e 488d0dc302015e lea rcx,[mscorlib_ni+0x7026f8 (00007ffb`ba4326f8)]
00007ffb`5c422435 e8a68d615f call clr!JIT_Unbox (00007ffb`bba3b1e0)
00007ffb`5c42243a 488bd8 mov rbx,rax
00007ffb`5c42243d 33c9 xor ecx,ecx
00007ffb`5c42243f e84ca5ecff call 00007ffb`5c2ec990 (<Module>.CoInitialize(Void*), mdToken: 0000000006000057)
00007ffb`5c422444 8903 mov dword ptr [rbx],eax
00007ffb`5c422446 488b1f mov rbx,qword ptr [rdi]
00007ffb`5c422449 c744242406010180 mov dword ptr [rsp+24h],80010106h
00007ffb`5c422451 488d542424 lea rdx,[rsp+24h]
00007ffb`5c422456 488d0d9b02015e lea rcx,[mscorlib_ni+0x7026f8 (00007ffb`ba4326f8)]
00007ffb`5c42245d e8eeff605f call clr!JIT_BoxFastMP_InlineGetThread (00007ffb`bba32450)
00007ffb`5c422462 483bd8 cmp rbx,rax
00007ffb`5c422465 7515 jne 00007ffb`5c42247c
00007ffb`5c422467 488bd3 mov rdx,rbx
00007ffb`5c42246a 488d0d8702015e lea rcx,[mscorlib_ni+0x7026f8 (00007ffb`ba4326f8)]
00007ffb`5c422471 e86a8d615f call clr!JIT_Unbox (00007ffb`bba3b1e0)
00007ffb`5c422476 c7002a000000 mov dword ptr [rax],2Ah
00007ffb`5c42247c 488b5608 mov rdx,qword ptr [rsi+8]
00007ffb`5c422480 488d0d7102015e lea rcx,[mscorlib_ni+0x7026f8 (00007ffb`ba4326f8)]
00007ffb`5c422487 e8548d615f call clr!JIT_Unbox (00007ffb`bba3b1e0)
00007ffb`5c42248c 8b08 mov ecx,dword ptr [rax]
00007ffb`5c42248e 894c2420 mov dword ptr [rsp+20h],ecx
00007ffb`5c422492 488d542420 lea rdx,[rsp+20h]
00007ffb`5c422497 488d0d5a02015e lea rcx,[mscorlib_ni+0x7026f8 (00007ffb`ba4326f8)]
00007ffb`5c42249e e8adff605f call clr!JIT_BoxFastMP_InlineGetThread (00007ffb`bba32450)
00007ffb`5c4224a3 488bd0 mov rdx,rax
00007ffb`5c4224a6 488bcd mov rcx,rbp
00007ffb`5c4224a9 e85257e65d call mscorlib_ni+0x557c00 (00007ffb`ba287c00) (System.Console.WriteLine(System.String, System.Object), mdToken: 00000000060009a4)
00007ffb`5c4224ae 90 nop
00007ffb`5c4224af 4883c438 add rsp,38h
00007ffb`5c4224b3 5f pop rdi
00007ffb`5c4224b4 5e pop rsi
00007ffb`5c4224b5 5d pop rbp
00007ffb`5c4224b6 5b pop rbx
00007ffb`5c4224b7 c3 ret
0:000> g 00007ffb`5c422444
00007ffb`5c422444 8903 mov dword ptr [rbx],eax ds:00000090`00003760=00000000
0:000> r eax
eax=80010106
0:000> g 00007ffb`5c422462
00007ffb`5c422462 483bd8 cmp rbx,rax
0:000> !do @rbx
Name: System.UInt32
MethodTable: 00007ffbba4326f8
EEClass: 00007ffbb9e02488
Size: 24(0x18) bytes
File: C:\Windows\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
Fields:
MT Field Offset Type VT Attr Value Name
00007ffbba4326f8 4000634 8 System.UInt32 1 instance 2147549446 m_value
0:000> !do @rax
Name: System.UInt32
MethodTable: 00007ffbba4326f8
EEClass: 00007ffbb9e02488
Size: 24(0x18) bytes
File: C:\Windows\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
Fields:
MT Field Offset Type VT Attr Value Name
00007ffbba4326f8 4000634 8 System.UInt32 1 instance 2147549446 m_value
0:000> p
00007ffb`5c422465 7515 jne 00007ffb`5c42247c [br=1]
0:000>
00007ffb`5c42247c 488b5608 mov rdx,qword ptr [rsi+8] ds:00000090`00003748=0000009000003758
0:000> g
ntdll!NtTerminateProcess+0xa:
00007ffb`c71bae4a c3 ret
0:000> q
quit:
どうやら BadClass.Run がインライン展開されたようです。言い古されたネタでしょうが、デバッグする立場からすると厄介なものです。また、C++/CLI の特徴ですが、DebugBreak の呼び出しは <Module>.DebugBreak になっていて、KERNELBASE!DebugBreak にはヒットしないはずです。
さて、CoInitialize の後、青字で示した部分は戻り値を BadClass.mUint32 に代入する部分で、赤字がボックス化です。clr!JIT_BoxFastMP_InlineGetThread とかいうそれらしき関数でボックス化が行われるのでしょうかね。00007ffb`5c422462 の cmp で、左辺の mUint32 である rbx と、rax レジスターを比較しているところを見ると、JIT_BoxFastMP_InlineGetThread はボックス化したオブジェクトを戻り値として返すようです。
それぞれを !do コマンドで見ると、確かに System.UInt32 のオブジェクトになっていて、m_value の値は 2147549446 (=0x80010106) になっています。しかし、cmp はレジスタの値を比べているだけなので、つまりオブジェクト ハンドルの 64bit 値をそのまま比べているだけです。これでは if の結果は false になって当然ですね。
実際に遭遇したコードはもう少し規模が大きいわけですが、何といっても意味が分からないのは UInt32 のハンドルをクラスに持たせているところ。値を直接持たせた方がいいと思うのだが、ハンドル化するメリットが不明。あと、生成されたコードから分かるように、ボックス化/アンボックス化に伴う関数呼び出しが多発するのが無駄。特にボックス化はヒープにメモリを確保するのでコストが高いのは言うまでもないことです。