C#からネイティブDLLを呼び出す場合のVSからのデバッグのジレンマを解決する
「C#を使う最大のメリットって、やっぱり、Visual Studioですよね!」って自信を持って言いたいですね。
という心境ではあるんですが、私の仕事はどっちかというとC++よりなので、どうしても、DllImportはお友達という側面があります。そうすると、プログラム実行時に、
AnyCPUなアセンブリ.exe ネイティブC++.dll
みたいな感じになって、要は、AnyCPUなアセンブリがネイティブC++ちゃんを呼び出す構図になるんですが、この構成、64-bitの環境で開発をしていたりすると相当なジレンマを抱えることになります。
普通に実行する場合には、x64環境なので、「ネイティブC++.dll」さんは、64-bit版を配置しておくべきなんですが、VS上からデバッグしようとすると、win32(32-bit版)を置いておかないと行けなかったり、あるいは、「ネイティブC++.dll」さんのデバッグ版は激遅なので、デバッグ中でも普通はリリース版を置いておきたかったりと、様々な面倒なシチュエーションに出くわすことになります。
で、これを解決する方法を考えたよという話です。
簡単に言うと、
AnyCPUなアセンブリ.exe win32/ ネイティブC++.dll x64/ ネイティブC++.dll
というディレクトリ構成にしてしまえという。「AnyCPUなアセンブリ.exe」は、賢いので、自分が実行されている環境に応じて、どっちをロードするか見てくれるという。
で、どうするかというと、次みたいなコードをアプリに取り込んでしまえという。
これは、MainWindow.xaml.csとかの例ですが、もっと早い段階がよければ、そこでやっても良いですね:
static MainWindow() { SetDllDirectory(""); #if DEBUG if (setDllLocation(true)) return; #endif setDllLocation(false); } /// <summary> /// Controlling DLL directory which native DLLs are loaded from. /// </summary> /// <param name="debug">Whether this is debug mode or not.</param> /// <returns><c>true</c> if DLL directory is correctly set; otherwise <c>false</c>.</returns> static bool setDllLocation(bool debug) { var appDir = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location); if (debug) appDir = Path.Combine(appDir, "debug"); var dllDir = Path.Combine(appDir, IntPtr.Size == 8 ? "x64" : "win32"); if (Directory.Exists(dllDir)) { Debug.WriteLine(string.Format("Set DLL Location ({0}): {1}", debug ? "Debug" : "Release", dllDir)); SetDllDirectory(dllDir); return true; } return false; } [DllImport("Kernel32.dll")] static extern bool SetDllDirectory(string lpPathName);
やってることの内容としては、 SetDllDirectory APIを呼び出して、DLLをロードするディレクトリを事前に変更しておこうと。
C++だと、delayloadがらみを調整して同じような事が出来ますが、C#でのDllImportは完全に実行時にバインディングを解決することになるので、かなりこういう芸当がやりやすいです。
で、上のコードにはオマケがあって、デバッグ版では、さらに、
AnyCPUなアセンブリ.exe debug/ win32/ ネイティブC++.dll x64/ ネイティブC++.dll
みたいなレイアウトにしておけば、デバッグ版のDLLを見つけてロードしてくれると。
いずれにしても、デバッグ時にDLLをコピーしたり移動したりの右往左往がかなり軽減されるという意味では開発が楽になると思います。
Xamarin iOSのP/Invokeでコールバックを使うときの制限
C#でコードが書きたいという一心で、Xamarin iOSを使おうと考えているんですが、基本的に僕の各コードは、C/C++で基本的なAPIセットを用意して、それのラッパーをC#で書き、さらにそのラッパーを呼び出すコードをC#で書く or 書いて貰うという感じになります。
typedef int (*MyCallback)(); int MyFunction(MyCallback callback);
なんていうやる気のないコールバックが用意されていて、普通にC++やっていると、これじゃコンテキストわたせねぇというような場合でも、
[UnmanagedFunctionPointer(CallingConvention.Cdecl)] delegate int MyCallbackDelegate(); [DllImport("MyDll.dll")] public static extern int MyFunction(MyCallback callback);
なんて定義をしてやれば、何でも渡し放題になるところですね。
MyFunction(() => { /*何でも出来る!*/ });
みたいにラムダすら渡せるので、何にも困らない。C++だとbindなんて使ったところで、本質的に関数オブジェクトと関数は別物なので、いわゆるサンクとかトランポリンという汚い方法を使わざるを得ません。
Xamarin iOSでのP/Invokeに関する制約
ところが、Xamarin iOSでは、
Xamarin iOS Limitations 2.2. Reverse Callbacksには、
In standard Mono it is possible to pass C# delegate instances to unmanaged code in lieu of a function pointer. The runtime would typically transform those function pointers into a small thunk that allows unmanaged code to call back into managed code.
In Mono these bridges are implemented by the Just-in-Time compiler. When using the ahead-of-time compiler required by the iPhone there are two important limitations at this point:
- You must flag all of your callback methods with the MonoPInvokeCallbackAttribute
- The methods have to be static methods, there is no support for instance methods.
などという記述があります。要は、JITじゃないからいろいろ出来ないことが多くて、
- コールバックされる側のメソッドに、MonoPInvokeCallbackAttributeを付けろ
- コールバックされる側のメソッドは、staticじゃないといけません
という制限がありますと。
別に、MonoPInvokeCallbackなんていう属性を付けないと行けないなんていうのは付ければ良いかぁという話ではあるんですが、staticメソッドしか呼び出せないって事は、結局、C#でもコンテキスト渡せないやんけっていう・・・。
まともな設計のライブラリなら何とかなる
もちろん、C++側のライブラリが、
typedef int (*MyCallback2)(void* context); int MyFunction2(void* context, MyCallback2 callback);
となっているならば問題はありません。これだと、C#側は、
[UnmanagedFunctionPointer(CallingConvention.Cdecl)] delegate int MyCallback2Delegate(IntPtr context); [DllImport("MyDll.dll")] public static extern int MyFunction2(IntPtr context, MyCallback2Delegate callback);
となるわけですが、例えば、Xamarin iOSを無視するなら、呼び出す側のコードは、
MyFunction2(IntPtr.Zero, context => { /*contextは使わない。何でも出来る!*/ });
みたいに、やっぱり、contextは無視できます。でも、Xamarin iOSを相手にするとこれはダメなコードになります。どうするかといえば、GCHandleを使って、コンテキストをIntPtrの形で渡すわけですね。
一方で、本当は、C#では上に書いたように、contextなんて要らないわけです。C#的には、delegateがコンテキストを暗黙的に持ってくれるので、delegateだけ渡せば十分。他は要らない。
なので、最終的に、
public static int MyFunction2(Func<int> callback)
みたいな感じで見えるメソッドにする方が便利です。
public static int MyFunction2(Func<int> callback) { // FIXME:潜在的な例外については無視・・・ var gcHandle = GCHandle.Alloc(callback, GCHandleType.Normal); var ret = MyFunction2(GCHandle.ToIntPtr(gcHandle), myCallback2); gcHandle.Free(); return ret; } [MonoPInvokeCallback(typeof(MyCallback2Delegate))] private static int myCallback2(IntPtr context) { var gcHandle = GCHandle.FromIntPtr(context); var callback = (Func<int>)gcHandle.Target; return callback(); }
これだとmyCallback2は、staticなメソッドなのでXamarin iOSでも問題はありません。まぁ、他の環境のことを考えると、#ifで、
#if XAMARIN_IOS public static int MyFunction2(Func<int> callback) { // FIXME:潜在的な例外については無視・・・ var gcHandle = GCHandle.Alloc(callback, GCHandleType.Normal); var ret = MyFunction2(GCHandle.ToIntPtr(gcHandle), myCallback2); gcHandle.Free(); return ret; } [MonoPInvokeCallback(typeof(MyCallback2Delegate))] private static int myCallback2(IntPtr context) { var gcHandle = GCHandle.FromIntPtr(context); var callback = (Func<int>)gcHandle.Target; return callback(); } #else public static int MyFunction2(Func<int> callback) { return MyFunction2(IntPtr.Zero, context => callback()); } #endif
とでもしておけば良いですね。で、元々のP/Invokeによるエントリは誤爆されることを防ぐためにprivateなりinternalなりにして置けば、C#側のプログラマは好きにやれば良いという感じになります。ハッピーです。
ndk-buildを使わないでプログラムをビルドする
普段は、ndk-buildでビルドをしているのが楽だし、便利なんですが、時に、外部のライブラリがconfigureとか使ってて、自前でAndroid.mkとかを作るのが面倒なことがあります。というか、試しに自前で作ってみようとしたけど、新旧の情報が錯綜しており、また、C++を使った場合のリンク方法がさっぱりとか、とりあえず、root化した端末でコマンドラインベースのコードを動かしてみたいけど・・・な場合にどうやれば良いのか全く分かりませんでした。
結局、当たり前ながら、じゃぁ、ndk-buildは何をやってるんでございましょうということになるんですが、これを見てみるために凄く簡単なサンプルを作ってみます。
sample/
jni/
Application.mk
Android.mk
sample.cpp
というディレクトリ構造で、
# Application.mk APP_ABI := armeabi-v7a # RTTI、例外を使うためにstatic GNU STLを利用する APP_STL := gnustl_static
# Android.mk LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE := sample LOCAL_CFLAGS := LOCAL_SRC_FILES += sample.cpp LOCAL_STATIC_LIBRARIES += gnustl_static include $(BUILD_EXECUTABLE)
// sample.cpp #include <iostream> #include <vector> int main(int argc, char* argv[]) { // 単にSTLが使えているかどうかをチェックしたい std::vector<int> v(256); std::cout << "Hello, world." << v.size() << std::endl; }
っていうのを作って、jni/ディレクトリで
ndk-build -n
とやると(ちなみに、/opt/android-ndk-r7にAndroid NDK r7をインストールしている)、
rm -f /somewhere/sample/libs/armeabi/lib*.so /somewhere/sample/libs/armeabi-v7a/lib*.so /somewhere/sample/libs/x86/lib*.so rm -f /somewhere/sample/libs/armeabi/gdbserver /somewhere/sample/libs/armeabi-v7a/gdbserver /somewhere/sample/libs/x86/gdbserver rm -f /somewhere/sample/libs/armeabi/gdb.setup /somewhere/sample/libs/armeabi-v7a/gdb.setup /somewhere/sample/libs/x86/gdb.setup mkdir -p /somewhere/sample/obj/local/armeabi-v7a/objs/helloworld/ echo """Compile++ thumb"" : helloworld <= sample.cpp" ccache /opt/android-ndk-r7/toolchains/arm-linux-androideabi-4.4.3/prebuilt/darwin-x86/bin/arm-linux-androideabi-g++ -MMD -MP -MF /somewhere/sample/obj/local/armeabi-v7a/objs/helloworld/sample.o.d -fpic -ffunction-sections -funwind-tables -fstack-protector -D__ARM_ARCH_5__ -D__ARM_ARCH_5T__ -D__ARM_ARCH_5E__ -D__ARM_ARCH_5TE__ -Wno-psabi -march=armv7-a -mfloat-abi=softfp -mfpu=vfp -fno-exceptions -fno-rtti -mthumb -Os -fomit-frame-pointer -fno-strict-aliasing -finline-limit=64 -I/opt/android-ndk-r7/sources/cxx-stl/gnu-libstdc++/include -I/opt/android-ndk-r7/sources/cxx-stl/gnu-libstdc++/libs/armeabi-v7a/include -I/opt/android-ndk-r7/sources/cxx-stl/gnu-libstdc++/include -I/opt/android-ndk-r7/sources/cxx-stl/gnu-libstdc++/libs/armeabi-v7a/include -I/somewhere/sample/jni -DANDROID -Wa,--noexecstack -fexceptions -frtti -fexceptions -frtti -O2 -DNDEBUG -g -fexceptions -frtti -I/opt/android-ndk-r7/platforms/android-3/arch-arm/usr/include -c /somewhere/sample/jni/sample.cpp -o /somewhere/sample/obj/local/armeabi-v7a/objs/helloworld/sample.o mkdir -p /somewhere/sample/obj/local/armeabi-v7a/ echo "Prebuilt : libgnustl_static.a <= <NDK>/sources/cxx-stl/gnu-libstdc++/libs/armeabi-v7a/" cp -f /opt/android-ndk-r7/sources/cxx-stl/gnu-libstdc++/libs/armeabi-v7a/libgnustl_static.a /somewhere/sample/obj/local/armeabi-v7a/libgnustl_static.a mkdir -p /somewhere/sample/obj/local/armeabi-v7a/ echo "Executable : helloworld" /opt/android-ndk-r7/toolchains/arm-linux-androideabi-4.4.3/prebuilt/darwin-x86/bin/arm-linux-androideabi-g++ -Wl,--gc-sections -Wl,-z,nocopyreloc --sysroot=/opt/android-ndk-r7/platforms/android-3/arch-arm /somewhere/sample/obj/local/armeabi-v7a/objs/helloworld/sample.o /somewhere/sample/obj/local/armeabi-v7a/libgnustl_static.a /somewhere/sample/obj/local/armeabi-v7a/libgnustl_static.a /opt/android-ndk-r7/toolchains/arm-linux-androideabi-4.4.3/prebuilt/darwin-x86/bin/../lib/gcc/arm-linux-androideabi/4.4.3/libgcc.a -Wl,--fix-cortex-a8 -Wl,--no-undefined -Wl,-z,noexecstack -lc -lm -o /somewhere/sample/obj/local/armeabi-v7a/helloworld echo "Install : helloworld => libs/armeabi-v7a/helloworld" mkdir -p /somewhere/sample/libs/armeabi-v7a install -p /somewhere/sample/obj/local/armeabi-v7a/helloworld /somewhere/sample/libs/armeabi-v7a/helloworld /opt/android-ndk-r7/toolchains/arm-linux-androideabi-4.4.3/prebuilt/darwin-x86/bin/arm-linux-androideabi-strip --strip-unneeded /somewhere/sample/libs/armeabi-v7a/helloworld
という感じのことをやっている事が分かる。
つまり、まとめると、コンパイル、リンク、リンク後処理として、
# コンパイル /opt/android-ndk-r7/toolchains/arm-linux-androideabi-4.4.3/prebuilt/darwin-x86/bin/arm-linux-androideabi-g++ \ -MMD -MP -MF \ /somewhere/sample/obj/local/armeabi-v7a/objs/helloworld/sample.o.d \ -fpic \ -ffunction-sections -funwind-tables -fstack-protector \ -D__ARM_ARCH_5__ -D__ARM_ARCH_5T__ -D__ARM_ARCH_5E__ -D__ARM_ARCH_5TE__ \ -Wno-psabi \ -march=armv7-a -mfloat-abi=softfp -mfpu=vfp \ -fno-exceptions -fno-rtti \ -mthumb \ -Os \ -fomit-frame-pointer \ -fno-strict-aliasing \ -finline-limit=64 \ -I/opt/android-ndk-r7/sources/cxx-stl/gnu-libstdc++/include \ -I/opt/android-ndk-r7/sources/cxx-stl/gnu-libstdc++/libs/armeabi-v7a/include \ -I/somewhere/sample/jni \ -DANDROID \ -Wa,--noexecstack \ -O2 -DNDEBUG -g \ -I/opt/android-ndk-r7/platforms/android-3/arch-arm/usr/include \ -c /somewhere/sample/jni/sample.cpp \ -o /somewhere/sample/obj/local/armeabi-v7a/objs/helloworld/sample.o
# リンク /opt/android-ndk-r7/toolchains/arm-linux-androideabi-4.4.3/prebuilt/darwin-x86/bin/arm-linux-androideabi-g++ \ -Wl,--gc-sections \ -Wl,-z,nocopyreloc \ --sysroot=/opt/android-ndk-r7/platforms/android-3/arch-arm \ /somewhere/sample/obj/local/armeabi-v7a/objs/helloworld/sample.o \ /somewhere/sample/obj/local/armeabi-v7a/libgnustl_static.a \ /opt/android-ndk-r7/toolchains/arm-linux-androideabi-4.4.3/prebuilt/darwin-x86/lib/gcc/arm-linux-androideabi/4.4.3/libgcc.a \ -Wl,--fix-cortex-a8 \ -Wl,--no-undefined \ -Wl,-z,noexecstack \ -lc -lm \ -o /somewhere/sample/obj/local/armeabi-v7a/helloworld
# デバッグ情報の除去 /opt/android-ndk-r7/toolchains/arm-linux-androideabi-4.4.3/prebuilt/darwin-x86/bin/arm-linux-androideabi-strip \ --strip-unneeded \ /somewhere/sample/libs/armeabi-v7a/helloworld
ということが分かる(一部、重複など簡略化済み)。
あとは、この辺を配慮した上でconfigure様に、CC/CXX/LD/CFLAGS/LDFLAGSなどを設定してあげれば良い感じみたいだ。
Qt を試したけど玉砕した。
注意
下の作業をしたけど、あんまりうまくいきませんでした。玉砕です。
ただいま、いろいろ試行錯誤中ですが、自分用にメモとして書いておきます。
Qt のダウンロード
今回は、 http://qt.nokia.com/ から、Qt 4.7.0 (qt-sdk-win-opensource-2010.05.exe) をダウンロードした。LGPLが適用できるので、DLL版を使っている限りはソースを公開できない商用アプリでも利用できる。
Qt のインストール
あんまり考えたくないので、基本、デフォルトのままで。Visual Studio を使いたいので、逆に言えば、mingw版はいらないといえばいらないけど、なんかのためにインストールしておく(つまり、デフォルトのまま)。
Qt のビルド
基本的には、C:\Qt\2010.05\qt で作業を行う。コマンドプロンプトを開き、
cd C:\Qt\2010.05\qt
で Qt のディレクトリに移動。まぁ、直接、このディレクトリでコマンドプロンプトを開いてもかまいませんけど。
この後、Visual Studio のツール群を使えるように、vcbarsXXX.batを呼び出す。Visual Studio 2010 の x64 (64-bit) 環境なら、
cmd /K "%VS100COMNTOOLS%..\..\VC\vcvarsall.bat" amd64
Visual Studio 2010 の x86 (32-bit) 環境なら、
cmd /K "%VS100COMNTOOLS%vsvars32.bat"
configure
ライブラリ出力先をカスタマイズしたかったけど、オプションが分からないのでそっちは諦める。というか、後でどうにかする。
configure -debug-and-release -opensource -shared -exceptions
make
nmake を使いたいところですが、我が Core i7 マシンの SMT 8 論理コアを生かすために、jom を利用する(というか、そうしないと死ぬほど時間がかかるらしい。十数時間という単位で)。とはいえ、ダウンロードとかする必要はない。C:\Qt\2010.05\bin にすでにバイナリが存在していたので、それをそのまま使う。
..\bin\jom
エラーが出る
変なエラーが出た。
api\qscriptextensionplugin.h(43): Error: Undefined interface
適当にググった感じだと、2つのゴミファイルを削除すればどうにかなる模様。
del src/script/tmp/moc/debug_shared/mocinclude.tmp del src/script/tmp/moc/release_shared/mocinclude.tmp
あとは何度か失敗するけど、jomを何度かやってるとうまくいく。理由はよく分からないけど、タイミングの問題??
インストールされるファイル群
注意しないといけないのは、これで作成されるlib/dll/pdbなどのファイル群は、C:\Qt\2010.05\qt\lib にそのまんま配置されてしまうこと。32-bitの開発しかしないとか、64-bitの開発しかしないというのであればそのまんまでも良いけど、共存しないといけないのであれば、x64 (64-bit)の方のバイナリをx64とか、amd64といったサブディレクトリに移動する。僕の場合は、各Visual Studioとの兼ね合いもあるので、vs2010-x64のようなサブディレクトリを作成し、そっちにライブラリを置くことにする。
mkdir lib\vs2010-x64 move lib\*.lib lib\vs2010-x64 move lib\*.dll lib\vs2010-x64 move lib\*.exp lib\vs2010-x64 move lib\*.ilk lib\vs2010-x64 move lib\*.pdb lib\vs2010-x64
Visual Studio/cl/link でリンクをするときには、このディレクトリを指定しないといけなくなることだけは注意。
プログラムのコンパイル・ビルド
qmake (頓挫中)
sample.cpp とかをつくって、そのディレクトリで、
set PATH=C:\Qt\2010.05\qt\bin;%PATH% qmake -project qmake -tp vc sample.pro -o sample.vcproj
なんてことをやっても、できる vcproj がなんかおかしい。よく分からない。
かといって、nmakeを使うぐらいなら、vs2010の意味が半減する。ということで今回はすんなりと諦めることにする。
仕方がないので手作業で(ただいま試行錯誤中)
まぁ、基本的に、ヘッダ、ライブラリ、プリプロセッサ関連の設定が主か。あとは、なぜか、/Zc:wchar_t- が指定されている(wchar_tがビルトインタイプじゃなく、unsigned shortと同じ扱いになる)ので、場合によっては、これに会わせる必要があるのかな?みたいな。
とりあえず、いろいろ問題があるようなので、もうちょっと試行錯誤してみる。
C++とC# unsafeで速度差を計測してみる
何となく、unsafeコードの速度が気になったので、.NET Framework 4.0上でのC#コードと、Visual C++ 2010のコードの速度差を計測してみました。
ちなみに、このコードは、いわゆる画像処理の速度差を計測することを前提とするものなので、w/h/strideといった画像処理っぽいコードになっています。また、コードが厳密には一緒ではないことは、言語の特性にも大いに依存し、言語の特性を無視してまで最適化を行うことは趣旨に反すると思うので、そこは、C++とC#の味だとして続けることにします。
C#版(speedtest.cs)。
// Compile: csc /o /unsafe speedtest.cs using System; using System.Diagnostics; class SpeedTest { static void test1() { int w = 4321; int stride = (w + 3) & ~3; int h = 6789; var a = new byte[stride * h]; for (int y = 0; y < h; y++) { int offset = y * stride; for (int x = 0; x < w; x++) { a[x + offset] = (byte)(x ^ y); } } } static void test2() { int w = 4321; int stride = (w + 3) & ~3; int h = 6789; var a = new byte[stride * h]; unsafe { fixed (byte* p0 = a) { for (int y = 0; y < h; y++) { byte* p = p0 + y * stride; for (int x = 0; x < w; x++) { p[x] = (byte)(x ^ y); } } } } } static void time(Action action, int count = 100) { var tw = new Stopwatch(); tw.Start(); for (int i = 0; i < count; i++) action(); tw.Stop(); Console.WriteLine(tw.ElapsedMilliseconds); } static void Main(string[] args) { time(test1); time(test2); } }
C++版(speedtest.cpp)。
// Compile: cl /MD /Ox speedtest.cpp #include <stdio.h> #include <windows.h> typedef unsigned char byte; static void test2() { int w = 4321; int stride = (w + 3) & ~3; int h = 6789; byte* a = new byte[stride * h]; byte* p0 = a; for (int y = 0; y < h; y++) { byte* p = p0 + y * stride; for (int x = 0; x < w; x++) { p[x] = (byte)(x ^ y); } } delete[] a; } void time(void (*action)(), int count = 100) { DWORD start = GetTickCount(); for (int i = 0; i < count; i++) action(); printf("%u\n", GetTickCount() - start); } int main() { time(test2); }
C#版(AnyCPU on Windows 7 x64)の結果。
6663 5436
Unsafeコード、つまりポインタ版が、1.2倍程度速いという結果。逆に言うと、配列の範囲チェックはこの程度の影響しかないとも言える。
さて、C++版(x64 on Windows 7 x64)の結果。
5210
ん。C++版は、C#ポインタ版に比べても4%程度しか速くない!
思ったよりもC#のコードはかなり速いということか。差がこの程度ということは、メモリ管理とかの差もあんまり問題にならないということだろう。
C#でunsafeコードが使えないコンテキストでも、C++に比べて、130%程度の処理時間にしかならない。
ライブラリの充実度や、コードのポータビリティ(Silverlightなど)を考えると、この程度のトレードオフならば許容できると思う。