AnyCPUなC#モジュールからx86/x64のDLLを呼び分ける方法まとめ
こんにちは、株式会社CFlatです。
C#には、実行環境が32ビットか64ビットかを問わず、適切なモードで実行できる、AnyCPUという仕組みが存在します。
ところがこの機能、裏を返せば実行時まで32ビットか64ビットかわからないという事でもあるわけですので、AnyCPUをサポートしていない言語……例えばCやC++/CLIで作ったDLLを使いたい場合、DllImportとの相性がすこぶる悪いことになります。というのもDllImportはコンパイル時にDLLの読み込みパスを指定するため、実行時に「32bitだったらこのDLL、64bitだったらこのDLL」とはできないのです。
このような場合、どのような解決策を使うにせよ、何らかの制約を強いられることになります。
そんなわけで、幾つかある解決策の長所短所をまとめてみました。
(1) 全部32bitで固定する
C#側を32bitに固定することで、常に32bitのDLLを使おう、という解決法です。大抵の場合、これでよいかと思います。
長所:一番単純で、最低限の変更で済む。
短所:64bitOSの長所(大量のメモリが使える、一部の処理が高速化する)を生かし切れない。
(2) C#側も32bitと64bitを別々に用意する
全部ひっくるめて、32bit版と64bit版として別々に作る方法です。
とはいえ、最初に起動するexeさえx86/x64でのビルドになっていれば、それ以降に読み込まれるAnyCPUのDLLもそれに応じて動作してくれるため、AnyCPUなDLLが大量にある場合もそれらは全部コピーで問題ありません。
長所:ユーザーに選択を任せることで、プログラム側で判断をする必要から解放される。
短所:ウェブ公開する場合などに、ユーザーは自分の環境に合わせたバージョンをDLして貰わねばならず、面倒をかける。
(3) DLLをSystemディレクトリに突っ込む
64bit版WindowsにはWOW64なる仕組みがあり、通常の64bit版のSystem32ディレクトリとは別に、32bit版のプログラムを動かすための32bit版System32ディレクトリがSysWOW64として用意されています。
だったらそこに別々にDLLを突っ込んじゃえばいいじゃん、っていう(斜め上)解決法。
長所:ソースコード側には一切修正が必要ない。
短所:Systemディレクトリを汚す必要がある。セキュリティ警告は出るわ、アンインストールも大変だわ……。
(4) インストーラを使う
インストーラを使えば、インストーラが実行時に実行環境を判別して、適切なDLLを配置してくれるように設定できるはずです。しかも不要な方のDLLは残らないので素敵。
長所:不要な方のDLLが残らない。
短所:インストーラを作る必要がある。レジストリにインストール情報が残る(残らないように設定できるインストーラコンパイラもある)。
この次辺りから、ソースコードを弄り始めます。(1)〜(4)をいずれも取りたくない場合というのがどれだけあるかは知りませんが、念のため紹介を。
(5) SetDllDirectory()を呼び出す
32bit版のDLLと64bit版のDLLを別のディレクトリに配置し、呼び分ける方法です。
DLLの検索場所を変更するAPI、SetDllDirectory()を用いて、実行時にそれぞれの置き場を指定します。
ソースコードはこんな感じ。
/// <summary> /// DllImport用に、x86用のDLLのあるディレクトリとx64用のDLLのあるディレクトリを設定するためのクラスです。 /// </summary> public static class NativeDllDir { /// <summary> /// DllImport用に、x86用のDLLのあるディレクトリとx64用のDLLのあるディレクトリを設定します。 /// </summary> /// <param name="x86DllDir">x86環境用のDLLを配置したディレクトリを指定します。指定しなければカレントディレクトリとなります。</param> /// <param name="x64DllDir">x64環境用のDLLを配置したディレクトリを指定します。指定しなければカレントディレクトリとなります。</param> /// <returns>設定に成功したらtrue。</returns> /// <exception cref="PlatformNotSupportedException">x86でもx64でもない場合の例外です。</exception> public static bool Set( string x86DllDir = null, string x64DllDir = null ) { // 既に設定されているものをリセット SetDllDirectory( null ); if ( IntPtr.Size == 8 ) { // 64bitっぽい return SetDllDirectory( string.IsNullOrEmpty( x64DllDir ) ? "." : x64DllDir ); } if ( IntPtr.Size == 4 ) { // 32bitっぽい return SetDllDirectory( string.IsNullOrEmpty( x86DllDir ) ? "." : x86DllDir ); } // いずれでもない throw new PlatformNotSupportedException(); } [System.Runtime.InteropServices.DllImport( "kernel32", SetLastError = true )] private static extern bool SetDllDirectory( string lpPathName ); } static class Program { /// <summary> /// アプリケーションのメイン エントリ ポイントです。 /// </summary> [STAThread] static void Main( string[] args ) { // アプリケーションのディレクトリ string MyPath = System.IO.Path.GetDirectoryName( System.Reflection.Assembly.GetEntryAssembly().Location ); NativeDllDir.Set( MyPath + @"\x86", MyPath + @"\x64" ); // TODO: この後でDllImportを定義しているクラスを使用 } }
長所:SetDllDirectory()の呼び出しコードを追加すればよいだけなので、実装が比較的容易。
短所:Windows XP SP1以降でのみ動作。それ以前ではSetDllDirectory()は使用できない。
(6) GetProcAddress()を自前で呼び出す
DllImportの中で勝手にやってくれるはずのLoadLibrary()/GetProcAddress()の処理を、自前で書いてやれば、動的にDLLを指定して解決できるでしょう。
具体的にはLoadLibrary()してGetProcAddress()して、Marshal.GetDelegateForFunctionPointer()してからすることでできますがソースコードは割愛。
なお肝心の呼び出される関数の方の宣言は、delegateの宣言とそのdelegate型の変数の宣言という形に分割する必要があります。(8)参照。
長所:Windows XP SP1より前でも使える。
短所:いろいろめんどくさい。
(7) じゃあ、DllImportAttributeを改造してやればいいんじゃない?
読み込むDLLの指定を動的に行なえるようなDllImportを作ってやればよさそうにも思えますが、残念ながら、DllImportAttributeはsealedなので継承できません。
短所:この方法では実現できない
(8) 諦めて、両方DllImportしてから考える
ソースコードは(6)よりは大分すっきりしますが……どうなんだろうこれ。
public class Dll { [DllImport(@"x86\hoge.dll", EntryPoint = "Hoge")] private static extern IntPtr Hoge32(); [DllImport(@"x64\hoge.dll", EntryPoint = "Hoge")] private static extern IntPtr Hoge64(); public delegate IntPtr HogeFunc(); public static readonly HogeFunc Hoge; static Dll() { if ( IntPtr.Size == 8 ) { // 64bitっぽい Hoge = Hoge64; } else if ( IntPtr.Size == 4 ) { // 32bitっぽい Hoge = Hoge32; } } }
長所:Windows XP SP1より前でも使える。(6)よりは多少マシ。
短所:正直、いろいろ中途半端だと思う。
以上、お好きな方法をお使い下さいませ。