AnyCPUのモジュールからx86とx64のモジュールを呼び分ける
概要
C#では、実行環境のアーキテクチャがx86 / AMD64に関わらず、適切なモードで実行できるAnyCPUという仕組みが存在する。
しかし、AnyCPUは実行時までx86 / AMD64のどちらで動作するかは不明である。
これは、C/C++言語やC++/CLI言語で開発されたネイティブライブラリを使用する場合に問題となる。
ネイティブライブラリはコンパイル時に特定のアーキテクチャ (x86またはAMD64) 向けにビルドされるため、実行環境のアーキテクチャと一致しなければ動作しない。
つまり、x86向けにビルドされたDLLは64ビットプロセスからは呼び出すことができず、逆にAMD64向けにビルドされたDLLは32ビットプロセスからは呼び出すことができない。
一方、C#の DllImport 属性は、コンパイル時にネイティブライブラリのパスと関数名を静的に指定する仕組みである。
この指定は実行ファイルに組み込まれるため、実行時に動的に変更することができない。
したがって、AnyCPUでビルドされたC#アプリケーションが実際に32ビットプロセスとして起動するか64ビットプロセスとして起動するかは実行時に決定されるにもかかわらず、
DllImport で指定するDLLのパスはコンパイル時に固定されてしまうという矛盾が生じる。
例えば、[DllImport("MyNativeLib.dll")] と記述した場合、この指定では単一のDLLパスしか指定できないため、
実行時のアーキテクチャに応じて異なるDLL (例:x86版とAMD64版) を自動的に選択することができない。
この制約により、AnyCPUとネイティブライブラリの組み合わせは相性が悪いと言える。
このような場合、どのような解決策を使うにせよ、何らかの制約を強いられることになる。
なお、.NET FrameworkにおけるAnyCPUには、AnyCPU と AnyCPU 32bitPreferred の2種類が存在する。
32bitPreferred は64ビットOS上でも32ビットプロセスとして実行される設定であり、ARM環境等での互換性を考慮した設定である。
全てのモジュールをx86に固定する方法
C#側の実行ファイルをx86に指定することにより、常にx86のライブラリを使用する方法である。
- メリット
- 最も簡単であり、最低限の変更で済む。
- デメリット
- 64ビットOSのメリット (多くのメモリが使用でき、一部の処理が高速化する) を活かせない。
C#側のモジュールもx86 / AMD64を別々に用意する方法
全てのモジュールに対して、x86 / AMD64を別々に開発する方法である。
最初に起動する実行ファイルがx86またはAMD64でのビルドになっている場合、それ以降に読み込まれるAnyCPUのライブラリもそれに応じて動作する。
そのため、AnyCPUのライブラリが多く存在する場合もそれらは全てコピーで問題ない。
- メリット
- ユーザに選択を任せることにより、プログラム側で判断をする必要から解放される。
- デメリット
- Webサイトに公開する場合等、ユーザは自分の環境に合わせたバージョンをダウンロードする必要がある。
DLLをSystemディレクトリに配置する方法
AMD64向けWindowsにはWOW64機能があり、AMD64のSystem32ディレクトリとは別に、x86のプログラムを動作させるためにSysWOW64ディレクトリが存在する。
これを利用して、該当するディレクトリに対して、別々にライブラリを配置する方法である。
- メリット
- ソースコードの修正が不要である。
- デメリット
- Systemディレクトリが汚れる。
- セキュリティ関連の警告が表示される。
- また、アンインストールも煩雑になる。
インストーラを使用する方法
インストーラを使用する場合、インストーラが実行時に実行環境を判別して適切なライブラリを配置するように設定可能なため、不要なライブラリは残らない。
- メリット
- 不要なライブラリが残らない。
- デメリット
- インストーラを作成する必要がある。
- レジストリにインストール情報が残る。
- x86向けライブラリおよびAMD64向けライブラリの両方を開発しなければならない。
SetDllDirectory関数を使用する方法
x86 / AMD64のライブラリを別のディレクトリに配置して呼び分ける方法である。
ライブラリの配置場所を変更するAPIであるSetDllDirectory関数を使用して、実行時にそれぞれのライブラリのパスを指定する。
/// <summary>
/// DllImport向けに、x86 / AMD64向けのライブラリを別々に設定するためのクラス
/// </summary>
public static class NativeDllDir
{
/// <summary>
/// DllImport向けに、x86 / AMD64のライブラリのあるディレクトリを設定する
/// </summary>
/// <param name="x86DllDir">x86 DLLを配置したディレクトリを指定する
/// 指定しなければカレントディレクトリとなる
/// </param>
/// <param name="x64DllDir">AMD64 DLLを配置したディレクトリを指定する
/// 指定しなければカレントディレクトリとなる
/// </param>
/// <returns>設定に成功した場合は true</returns>
/// <exception cref="PlatformNotSupportedException">x86でもAMD64でもない場合の例外</exception>
public static bool Set( string x86DllDir = null, string x64DllDir = null )
{
// 既に設定されているものをリセット
SetDllDirectory( null );
if ( IntPtr.Size == 8 )
{ // AMD64
return SetDllDirectory( string.IsNullOrEmpty( x64DllDir ) ? "." : x64DllDir );
}
else if ( IntPtr.Size == 4 )
{ // x86
return SetDllDirectory( string.IsNullOrEmpty( x86DllDir ) ? "." : x86DllDir );
}
else
{ // その他
throw new PlatformNotSupportedException();
}
}
[System.Runtime.InteropServices.DllImport( "kernel32", SetLastError = true )]
private static extern bool SetDllDirectory( string lpPathName );
}
static class Program
{
[STAThread]
static void Main( string[] args )
{
// exeファイルが存在するディレクトリパス
string MyPath = System.IO.Path.GetDirectoryName(
System.Reflection.Assembly.GetEntryAssembly().Location );
NativeDllDir.Set( MyPath + @"\x86", MyPath + @"\x64" );
// DllImportを定義しているクラスを使用
// ...略
}
}
- メリット
SetDllDirectory関数の呼び出しコードを追加するだけのため、実装が比較的容易である。
- デメリット
- Windows XP with Service Pack 1 (SP1) 以降、または、Windows Server 2003以降でのみ動作する。
- それ以前のOSの場合は、
SetDllDirectoryメソッドは使用できない。 - x86向け および AMD64向けのライブラリの両方を開発しなければならない。
- プロセス全体のDLL検索パスに影響を与えるため、他のライブラリの読み込みにも影響する可能性がある。
x86向けDLLとAMD64向けDLLの両方をDllImportする方法
SetDllDirectory メソッドを使用する方法より簡単に記述可能であるが、中途半端で使用しにくい。
- メリット
- Windows XP SP1より以前でも使用できる。
- デメリット
- x86向けDLLとAMD64向けライブラリの両方を開発する必要がある。
- また、呼び出す関数を全て記述する必要がある。
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 )
{
Hoge = Hoge64;
}
else if ( IntPtr.Size == 4 )
{
Hoge = Hoge32;
}
}
}
LoadLibrary / GetProcAddress 関数を使用した動的ロード方法
LoadLibrary 関数 と GetProcAddress 関数 を使用して、実行時にライブラリと関数を動的にロードする方法である。
public static class NativeLibraryLoader
{
[DllImport("kernel32", SetLastError = true, CharSet = CharSet.Unicode)]
private static extern IntPtr LoadLibrary(string lpFileName);
[DllImport("kernel32", SetLastError = true)]
private static extern IntPtr GetProcAddress(IntPtr hModule, string lpProcName);
[DllImport("kernel32", SetLastError = true)]
private static extern bool FreeLibrary(IntPtr hModule);
public static IntPtr LoadNativeLibrary(string basePath, string libraryName)
{
string path;
if (IntPtr.Size == 8)
{
path = Path.Combine(basePath, "x64", libraryName);
}
else
{
path = Path.Combine(basePath, "x86", libraryName);
}
return LoadLibrary(path);
}
}
- メリット
- 細かい制御が可能である。
- 必要なタイミングでライブラリをロード / アンロードできる。
- デメリット
- 実装が複雑になる。
- デリゲートを使用して関数ポインタを取得する必要がある。
- エラーハンドリングを適切に実装する必要がある。
.NET 5以降での改善
.NET 5以降 (.NET Core 3.0以降) では、NativeLibrary クラスが導入され、ネイティブライブラリの読み込みがより容易になった。
- メリット
- クロスプラットフォーム対応である。
RuntimeInformationにより、明示的なアーキテクチャ判定が可能である。NativeLibrary.TryLoadメソッド やNativeLibrary.Freeメソッド等の便利なメソッドが提供されている。
- デメリット
- .NET Frameworkでは使用できない。
using System.Runtime.InteropServices;
public static class NativeLibraryHelper
{
public static IntPtr LoadNativeLibrary(string basePath, string libraryName)
{
string path;
if (RuntimeInformation.ProcessArchitecture == Architecture.X64)
{
path = Path.Combine(basePath, "x64", libraryName);
}
else if (RuntimeInformation.ProcessArchitecture == Architecture.X86)
{
path = Path.Combine(basePath, "x86", libraryName);
}
else
{
throw new PlatformNotSupportedException();
}
return NativeLibrary.Load(path);
}
}