動的にアンマネージメソッドを呼び出す。


Description

通常、マネージアプリケーションからDLLなどを呼び出す(P/Invoke)ためには DllImport 属性を付加してコンパイル前にライブラリ名とエントリポイントを指定する必要があります。

しかし場合によっては実行中にDLL名やエントリポイントを決定し呼び出したいことがあります。

How to

.NET Framework 1.0 ではModuleBuilder.DefinePInvokeMethod メソッドを使うことで動的にP/Invokeメソッドを作ることができます。

.NET Framework 2.0 以降では Marshal.GetDelegateForFunctionPointer メソッドで関数ポインタをデリゲートに変換することで呼び出すことができます。

Sample Code (.NET 2.0 or later)

using System;
using System.Runtime.InteropServices;

class DynamicPInvokeTest
{
    /* MessageBeep のシグネチャとあわせたデリゲートを宣言しておく */
    delegate Boolean MessageBeepDelegate(UInt32 uType);

    [DllImport("kernel32.dll")]
    static extern IntPtr LoadLibrary(String lpFileName);
    [DllImport("kernel32.dll")]
    static extern IntPtr GetProcAddress(IntPtr hModule, String lpProcName);
    [DllImport("kernel32.dll")]
    static extern Boolean FreeLibrary(IntPtr hLibModule);
    
    static void Main(String[] args)
    {
        /* ライブラリをロードして MessageBeep への関数ポインタを取得 */
        IntPtr hLib = LoadLibrary("user32.dll");
        IntPtr hProc = GetProcAddress(hLib, "MessageBeep");
        /* 関数ポインタをデリゲートに変換する */
        MessageBeepDelegate msgBeepDelegate = Marshal.GetDelegateForFunctionPointer(hProc, typeof(MessageBeepDelegate))
                                                                               as MessageBeepDelegate;
        /* 呼び出す(音が鳴るはず) */
        msgBeepDelegate(0);
        /* ライブラリを開放 */
        FreeLibrary(hLib);
    }
}

Sample Code (.NET 1.x)

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

public class DynamicPInvokeTest
{
	public static Int32
	Main(String[] args)
	{
		/* アセンブリ/モジュールを作る */
		AssemblyName assemblyName = new AssemblyName();
		assemblyName.Name = "dynamicpinvokeasm";
		AssemblyBuilder asmBuild = AppDomain.CurrentDomain.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);
		ModuleBuilder modBuild = asmBuild.DefineDynamicModule("dynamicpinvokemodule");
		
		/* メソッドを定義する */
		MethodBuilder methodBuild = modBuild.DefinePInvokeMethod(
			/* エントリポイント/メソッド名 */
			"MessageBeep",
			/* ファイル名 */
			"User32.dll",
			MethodAttributes.HideBySig | MethodAttributes.PinvokeImpl | MethodAttributes.Static | MethodAttributes.Public,
			CallingConventions.Standard,
			/* 戻り値の型 */
			typeof(Boolean),
			/* 引数の型(の配列) 引数をとらない場合 new Type[0] */
			new Type[]{ typeof(Int32) },
			CallingConvention.Winapi,
			CharSet.Auto);
		
		methodBuild.SetImplementationFlags(
			MethodImplAttributes.PreserveSig | methodBuild.GetMethodImplementationFlags()
		);
		
		/* メソッドを作成(コレ以降は追加できない) */
		modBuild.CreateGlobalFunctions();

		/* 実行 */
		Boolean retval = (Boolean)modBuild.GetMethod("MessageBeep").Invoke(null, /* 引数 */ new Object[] {0});
		Console.WriteLine("MessageBeep-result: {0}", retval);
		
		return 0;
	}
}

Problem

.NET 1.x向け手法ではアプリケーションドメイン単位ならともかく、アセンブリはアンロードできないためDLLを読み込んだままになります。(定義時に内部でLoadLibraryを呼び出していると思われる)

そのため何かのプラグインなどを大量にロードした際、読み込んだままになりメモリを大量に消費して困るかもしれません。

対処方法としてはマネージドC++でラッパーDLLを作成する方法があります。また別なアプリケーションドメイン作成し、その中でプラグインの読み込み/呼び出しを行いアンロードするのでも大丈夫かもしれませんが、アプリケーションドメインをまたぐ手間がかかります。