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の関数は、名前が修飾されてしまったり、引数をスタックに積む順序が逆になったりと、このままでは使えません・・・
もし、役に立ちそうでしたらコピペして持って行って下さい。(^^;
(追記) 参照型が渡せないので役立たずかも。文字列と配列が使えないのは痛いよなぁ・・・