読者です 読者をやめる 読者になる 読者になる
ようこそ。睡眠不足なプログラマのチラ裏です。

動的にDLL(ライブラリ)をロードしてアセンブリを作って、P/Invokeなdelegateを生成してみよう。

プログラミング C# C#2.0 C#3.0

C#で動的にDLL(ライブラリ)をロードしてアセンブリを作って、P/Invokeなdelegateを生成

動的にDLL(ライブラリ)をロードするというシナリオは、意外と要求されることが多いのではないかと思います。
利用する関数があらかじめ明確になっているのであれば、そのまま直実装でも構わないのですが、
それが明確になっていないような場合、あるいは都度動的に変化するというような場合では、一筋縄にはいきません。
そんな場合は、P/Invokeなdelegateについても動的に生成することを検討してみるとよさそうです。


具体的な方法を簡単に説明します。まずWinAPIのLoadLibrary関数でDLL(ライブラリ)をロードします。
続きまして、アセンブリを動的に生成し、またそのアセンブリに動的にクラスを作成します。
その動的に作成したクラスについて、読み込んだDLLから関数ポインタを積んだメソッドを
ILGeneratorを用いて作成します。動的に作成したクラスのインスタンスを生成し、
対象である関数ポインタを積んだメソッドのデリゲートをDelegate.CreateDelegateを用いて生成します。
という感じで、やること自体は非常にシンプルです。


ただ、WinAPIのLoadLibrary関数でDLL(ライブラリ)をロードした場合、FreeLibrary関数で後始末が必要となります。
必ず正しく後始末をしたいので、IDisposableインターフェイスを実装することにします。
usingステートメントも使えて便利さ100万倍ですね。


では、以下サンプルコードです。

using System;
using System.Collections.Generic;
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.InteropServices;
using System.Security;
using System.Security.Permissions;
using System.Threading;

namespace ClassLibrary1
{
    /// <summary> 
    /// PlatformInvokeMethodGenerator
    /// 
    /// DLL内の関数を動的に呼び出します。
    /// ただし、、Blittableでない型については自前で整形して渡してやる必要があります
    /// </summary> 
    [SecurityPermission(SecurityAction.Demand, Flags = SecurityPermissionFlag.UnmanagedCode)]
    public sealed class PlatformInvokeMethodGenerator : IDisposable
    {
        #region メンバ

        /// <summary>
        /// Disposeされたかどうか
        /// </summary>
        private bool _disposed;

        /// <summary>
        /// 利用DLL(ライブラリ)のポインタ
        /// </summary>
        private IntPtr _ipLib = IntPtr.Zero;

        #endregion
        
        #region Win32 API宣言

        /// <summary>
        /// 指定したDLLモジュールを、呼び出し側プロセスのアドレス空間に展開します。
        /// </summary>
        /// <param name="lpFileName">DLLモジュールのファイル パスを指定します。</param>
        /// <returns>
        /// 関数が成功すると、DLLモジュールを示すハンドルが返ります。失敗すると、0 が返ります。
        /// なお、不要になったモジュールハンドルは必ずFreeLibrary関数を使用してメモリから破棄して下さい。
        ///  
        /// ※拡張エラー情報は、GetLastError関数で取得できます。
        /// </returns>
        [SuppressUnmanagedCodeSecurity()]
        [DllImport("kernel32.dll", CharSet = CharSet.Auto)]
        private extern static IntPtr LoadLibrary(string lpFileName);

        /// <summary>
        /// 呼び出し側プロセスのアドレス空間にロードされているDLLモジュールを解放します。
        /// </summary>
        /// <param name="hModule">
        /// DLLモジュールを示すハンドルを指定します。
        /// 
        /// </param>
        /// <returns>
        /// 関数が成功すると、TRUE が返ります。失敗すると、FALSE が返ります。
        /// </returns>
        [SuppressUnmanagedCodeSecurity()]
        [DllImport("kernel32.dll", CharSet = CharSet.Auto)]
        private extern static bool FreeLibrary(IntPtr hModule);

        /// <summary>
        /// 呼び出し側プロセスのアドレス空間に展開されているDLLモジュール内から
        /// 指定した名前を持つプロシージャの先頭アドレスを取得します。
        /// </summary>
        /// <param name="hModule">
        /// モジュール ハンドルを指定します。
        /// </param>
        /// <param name="lpProcName">
        /// プロシージャ名を指定します。
        /// </param>
        /// <returns>
        /// 関数が成功すると、プロシージャの先頭アドレスが返ります。失敗すると、0 が返ります。
        /// なお、取得できるプロシージャは、エクスポート指定されている関数のみになります。
        /// </returns>
        [SuppressUnmanagedCodeSecurity()]
        [DllImport("kernel32.dll", CharSet = CharSet.Ansi)]
        private extern static IntPtr GetProcAddress(IntPtr hModule, string lpProcName);

        #endregion

        #region コンストラクタ

        /// <summary>
        /// コンストラクタ
        /// </summary>
        /// <param name="libName">
        /// 利用DLL(ライブラリ)名
        /// 
        /// "user32.dll","kernel32.dll"等
        /// </param>
        public PlatformInvokeMethodGenerator(string libName)
        {
            _ipLib = LoadLibrary(libName);
        }

        #endregion

        #region デストラクタ

        /// <summary>
        /// デストラクタ
        /// </summary>
        ~PlatformInvokeMethodGenerator()
        {
            //アンマネージリソースの解放
            this.Dispose(false);
        }

        #endregion

        #region メソッド
        /// <summary> 
        /// 指定されたデリゲートスタイルで動的にAPI関数実行メソッドを生成する 
        /// </summary> 
        /// <param name="methodType">デリゲートのタイプを指定</param> 
        /// <param name="methodName">DLL内に定義されている関数名</param> 
        /// <returns>関数実行メソッドに関連付けられたデリゲート</returns> 
        public MulticastDelegate Generate(string methodName, Type methodType)
        {
            this.ThrowExceptionIfDisposed();

            if (methodType.BaseType != typeof(MulticastDelegate))
                throw new ArgumentException("TypeはSystm.MulticastDelegate型である必要があります。");

            AssemblyName asmName = new AssemblyName("PlatformInvokeMethodGenerator");
            AssemblyBuilder asmb = Thread.GetDomain().DefineDynamicAssembly(asmName, AssemblyBuilderAccess.Run);
            ModuleBuilder modb = asmb.DefineDynamicModule("PInvokeModule");
            TypeBuilder typb = modb.DefineType("PInvokeType", TypeAttributes.Public);

            MethodInfo mi = methodType.GetMethod("Invoke");
            var pi = mi.GetParameters();

            var pList = new List<Type>();
            foreach (var item in pi)
                pList.Add(item.ParameterType);
            var pTypes = pList.ToArray();

            MethodBuilder metb = typb.DefineMethod("Generated" + methodName,
                                                    MethodAttributes.Public,
                                                    mi.ReturnType, pTypes);

            ILGenerator ilg = metb.GetILGenerator();
            // 関数に送られるすべての引数を積む 
            for (int i = 1; i <= pTypes.Length; i++)
                ilg.Emit(OpCodes.Ldarg, i);

            // 関数ポインタを積む 
            ilg.Emit(OpCodes.Ldc_I4, (int)GetProcAddress(_ipLib, methodName));
            ilg.EmitCalli(OpCodes.Calli, CallingConvention.StdCall, mi.ReturnType, pTypes);
            ilg.Emit(OpCodes.Ret);

            var o = Activator.CreateInstance(typb.CreateType());
            return (MulticastDelegate)Delegate.CreateDelegate(methodType, o, "Generated" + methodName);
        }

        /// <summary>
        /// Dispose
        /// </summary>
        /// <param name="disposing"></param>
        private void Dispose(bool disposing)
        {
            lock (this)
            {
                if (this._disposed)
                {
                    return;
                }

                this._disposed = true;

                if (disposing) {}

                // アンマネージリソースの解放
                if (this._ipLib != IntPtr.Zero)
                {
                    //呼び出し側プロセスのアドレス空間にロードされているDLLモジュールを解放
                    FreeLibrary(_ipLib);
                    this._ipLib = IntPtr.Zero;
                }
            }
        }

        /// <summary>
        /// Dispose済みの場合ObjectDisposedExceptionをthrow
        /// クラスがシールクラスの場合ははprivateメソッドとする
        /// </summary>
        private void ThrowExceptionIfDisposed()
        {
            if (this._disposed)
                throw new ObjectDisposedException(this.GetType().ToString());
        }

        #endregion

        #region IDisposable メンバ

        /// <summary>
        /// Dispose
        /// </summary>
        public void Dispose()
        {
            //マネージリソースおよびアンマネージリソースの解放
            this.Dispose(true);
            
            //ガベコレから、このオブジェクトのデストラクタを対象外とする
            GC.SuppressFinalize(this);
        }

        #endregion
    }
}


ちなみに、「.dll」拡張子ではないファイル名を指定してWinAPI LoadLibrary関数を呼び出した場合、
パスの中にピリオド「.」が含まれていると、LoadLibrary関数が指定したファイルを正しく見つけられないことが以前ありました。
http://support.microsoft.com/kb/324673/ja
最新の状態にWindowsUpdateをしているPCであれば、気にする必要はないでしょう。



お試しコード
Beep音でしょっぱいチャルメラを鳴らしてみます。

using ClassLibrary1;

namespace ConsoleApplication1
{
    sealed class Program
    {
        private delegate bool BeepDelegate(uint dwFreq, uint dwDuration);
        static void Main()
        {
            using (var pimg = new PlatformInvokeMethodGenerator("kernel32.dll"))
            {
                var Beep = pimg.Generate("Beep", typeof(BeepDelegate)) as BeepDelegate;

                //Beep音でチャルメラを♪
                Beep(262, 375);  
                Beep(294, 375);  
                Beep(330, 700);  
                Beep(294, 405);  
                Beep(262, 255);  
                System.Threading.Thread.Sleep(475);
                Beep(262, 305);  
                Beep(294, 310);  
                Beep(330, 305);  
                Beep(294, 300);  
                Beep(262, 255);  
                Beep(294, 775);  
            }
        }
    }
}

実行すると、対象のデリゲートが正しく動的に生成されて、意図した通りにしょっぱいチャルメラが鳴りましたね。
このあたりの動的な実装に関してはC#4.0であれば、もっと簡単にいけそうな感じですね、たぶん。


関連記事:特定のインターフェイスを持つプラグインを取得