64bit Windows が本格的に普及する前にネイティブ DLL を P/Invoke する .NET アプリがやっておくべきこと

以前からいろいろ調べたり質問したりしてた、ネイティブ DLL を P/Invoke する .NET アプリを 64bit Windows でも動作させる方法がようやく納得できる形で見つかったので、備忘録を兼ねてメモしておく。

P/Invoke したいネイティブDLL

  • x86用DLLとx64用DLLが用意されているネイティブDLL
  • System.Data.SQLite ※正確にはネイティブDLLではなく、ネイティブDLLを P/Invoke するアセンブリ

QA@ITでした質問

参考にしたサイト

なぜ必要か

まだまだ 32bit のサポートが必要だが、現在販売されている個人向けPCは 64bit 向けが大半を占めている。サーバーOSに至っては、 2008R2から 64bit しか用意されていない。

結論

  1. プロジェクトはターゲットプラットフォームを AnyCPU にする。
  2. x86用ネイティブDLLをx86ディレクトリに配置し、ビルドアクションを「コンテンツ」に設定する。さらに出力ディレクトリにコピーするようにしておく。
  3. x64用ネイティブDLLをx64ディレクトリに配置し、同様に設定する。
  4. SetDllDirectory でDLL検索パスを切り分ける。
using System;
using System.IO;
using System.Runtime.InteropServices;
namespace Hoge {
    /// <summary>
    /// ネイティブDLLのロード先ディレクトリを追加するユーティリティクラスです。
    /// </summary>
    public static class DllSearchPath {
        [DllImport("kernel32.dll", SetLastError = true)]
        static extern bool SetDllDirectory(string lpPathName);
        /// <summary>
        /// ネイティブDLLのロード先ディレクトリを追加します。
        /// </summary>
        /// <param name="path">ロード先ディレクトリ</param>
        public static void Add(string path) {
            SetDllDirectory(path);
        }
        /// <summary>
        /// x86とx64のネイティブDLLのロード先ディレクトリを追加します。
        /// </summary>
        /// <param name="x86path">x86用ロード先ディレクトリ</param>
        /// <param name="x64path">x64用ロード先ディレクトリ</param>
        public static void Add(string x86path, string x64path) {
            int bitWidth = IntPtr.Size * 8;
            switch (bitWidth) {
                case 64:
                    Add(x64path);
                    break;
                case 32:
                    Add(x86path);
                    break;
                default:
                    throw new NotSupportedException();
            }
        }
    }
}

#Program.cs
DllSearchPath.Add("path/to/x86", "path/to/x64");

ちなみに、 x86向けネイティブDLLしか用意されていない場合、ターゲットプラットフォームを x86 にしておく方が無難だろう。

動作原理

AnyCPU に設定しておけば、64bit Windows では 64bit プロセスとして動作し、32bit Windows では 32bit プロセスとして動作する。
DllSearchPath.Add で 32bit 向けパスと 64bit 向けパスをあらかじめ指定しておけば、自動的にプロセスを判断してロードするDLL
が配置されるディレクトリを決定してくれる。

この方法であれば、わざわざプロジェクトを x86 向けと x64 向けに2つ用意する必要はなくなる。

System.Data.SQLite

参考URLにあるとおり、

  • 32bit ・・・ Precompiled Statically-Linked Binaries for 32-bit Windows
  • 64bit ・・・ Precompiled Statically-Linked Binaries for 64-bit Windows

をダウンロードしておけばいい。選定理由はこんな感じ。

  • Visual C++ 2008 SP1 runtime はstaticリンクの方が無難。当然ランタイム分容量が増えるが、今更気にするサイズではないだろう。
  • mixed-mode assembly はネイティブDLLをアセンブリに含んでしまうので、ターゲットプラットフォームが x86 または x64 になってしまう。参照アセンブリは静的に決まるため、ロードするネイティブDLLを動的に切り分けられない。