Unmanaged DLLの動的呼び出し

Unmanaged DLLをP/InvokeするにはDllImportを使えば良いのですが、実行時でなければDLL名が分からない場合もあります。しかし、DllImportを使うとDLL名毎にアセンブリを作成することになって不便です。C/C++の経験があればWin32APIのLoadLibraryは使えないかな?と考えるかもしれませんが、C#から関数ポインタを呼び出すのは結構面倒だったりします。そこでInterfaceを利用した、Unmanaged DLLを呼び出すヘルパークラスを作ってみました。

例えば、次のようなUnmanaged DLLを呼び出したいとします。

Dump of file calc.dll

File Type: DLL

  Section contains the following exports for calc.dll

    00000000 characteristics
    4082387B time date stamp Sun Apr 18 17:12:43 2004
        0.00 version
           1 ordinal base
           2 number of functions
           2 number of names

    ordinal hint RVA      name

          1    0 00001000 Add
          2    1 00001010 Sub

AddとSubの2つの関数が含まれます。このDLLをDllImportを使わずに呼び出すために次のようなヘルパークラスを用意します。

using System;
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.InteropServices;

public class UnmanagedMethodBuilder : IDisposable {
  [DllImport("kernel32")]
  public extern static IntPtr LoadLibrary(string lpLibFileName);

  [DllImport("kernel32")]
  public extern static bool FreeLibrary(IntPtr hLibModule);

  [DllImport("kernel32")]
  public extern static IntPtr GetProcAddress(IntPtr hModule,string lpProcName);

  private IntPtr hModule;
  private bool disposed = false;

  public UnmanagedMethodBuilder(string dll) {
    hModule = LoadLibrary(dll);
    if (hModule == IntPtr.Zero) 
      throw new System.IO.FileNotFoundException();
  }

  public Object Build(Type iface) {
    // アセンブリ名はインターフェース名の先頭に'_'を付ける
    AssemblyName asnm = new AssemblyName();
    asnm.Name = "_" + iface.Name;
    AssemblyBuilder ab = AppDomain.CurrentDomain.DefineDynamicAssembly(
      asnm, AssemblyBuilderAccess.Run);
    // モジュール名はアセンブリ名と同じ
    ModuleBuilder md = ab.DefineDynamicModule(asnm.Name);
    // クラス名はアセンブリ名と同じ
    TypeBuilder tb = md.DefineType(asnm.Name, TypeAttributes.Public);
    tb.AddInterfaceImplementation(iface);

    // インタフェースメソッドを実装
    foreach (MethodInfo mi in 
      iface.GetMethods(BindingFlags.Public | BindingFlags.Instance)) {
      ParameterInfo pis = mi.GetParameters();
      Type parameterTypes = new Type[pis.Length];
      for (int i = 0; i < pis.Length; ++i) 
        parameterTypes[i] = pis[i].ParameterType;  

      // _stdcallの場合は_XXX@YYY形式になるが未対応
      MethodBuilder mb = tb.DefineMethod(mi.Name, 
        MethodAttributes.Public | MethodAttributes.Virtual, 
        mi.ReturnType, parameterTypes);
      ILGenerator il = mb.GetILGenerator();
      // _stdcallの場合は、スタックの積み方は逆になるが未対応
      for (int i = parameterTypes.Length; i > 0; --i)
        il.Emit(OpCodes.Ldarg, i);
      il.Emit(OpCodes.Ldc_I4, (Int32)GetProcAddress(hModule, mb.Name));
      il.EmitCalli(OpCodes.Calli, CallingConvention.Cdecl, 
        mi.ReturnType, parameterTypes);
      il.Emit(OpCodes.Ret);

      tb.DefineMethodOverride(mb, mi);
    }
    return tb.CreateType().GetConstructor(Type.EmptyTypes).Invoke(null);
  }
  #region IDisposable メンバ

  public void Dispose() {
    if (!disposed) {
      FreeLibrary(hModule);
      hModule = IntPtr.Zero;
      disposed = true;
    }
  }

  #endregion
}

// DLLにある関数と同じシグネチャのメソッドを用意
public interface ICalc {
  int Add(int x, int y);
  int Sub(int x, int y);
}

class App {
  public static void Main() {
    using (UnmanagedMethodBuilder umb = new UnmanagedMethodBuilder("Calc.dll")) {
      int x = 10, y = 20;
      // ICalcメソッドと同名関数をDLLから呼び出す
      ICalc calc = umb.Build(typeof(ICalc)) as ICalc;
      Console.WriteLine("{0} + {1} = {2}", x, y, calc.Add(x, y));
      Console.WriteLine("{0} - {1} = {2}", x, y, calc.Sub(x, y));
    }
  }
}

呼び出したい関数と同名のメソッドをインタフェースに用意し、UnmanagedMethodBuilderに渡すとインタフェース経由でDLL関数が呼び出せるようになります。また、呼び出しに掛かるオーバーヘッドもDllImportの半分程度になるのでパフォーマンス向上も期待できそうです。ただ問題として呼び出し規約が_stdcallの関数は、名前が修飾されてしまったり、引数をスタックに積む順序が逆になったりと、このままでは使えません・・・

もし、役に立ちそうでしたらコピペして持って行って下さい。(^^;

(追記) 参照型が渡せないので役立たずかも。文字列と配列が使えないのは痛いよなぁ・・・