// //v:20 //dbg- /t:library /doc:DynamicDllImporter.xml using System; using System.Collections; using System.ComponentModel; using System.Runtime.InteropServices; using System.Reflection; using System.Reflection.Emit; using System.Text; using System.Text.RegularExpressions; using Interop = System.Runtime.InteropServices; namespace HongliangSoft.Utilities.Unmanaged { ///インターフェイスを元に、アンマネージド DLL の関数を呼び出すメソッドを持つインスタンスを動的に作成する静的メソッドを提供します。 ///user32.dll の EnumWindows 関数と GetWindowThreadProcessId 関数を使用して、トップレベルウィンドウのウィンドウハンドルとプロセス ID を列挙するサンプルコードです。 /// /// using System; /// using System.Runtime.InteropServices; /// public interface IUser { /// int GetWindowThreadProcessId(IntPtr window, out int processId); /// bool EnumWindows([MarshalAs(UnmanagedType.FunctionPtr)] EnumWindowsHandler callback, IntPtr lParam); /// } /// public delegate EnumWindowsHandler(IntPtr window, IntPtr lParam); /// /// public class EntryPoint { /// public static void Main() { /// // インポート関数を実装する IUser のインスタンスを取得します。 /// user = (IUser)DynamicDllImporter.Import(typeof(IUser), "user32.dll"); /// // .NET 2.0 では次の書き方が可能です。 /// // user = DynamicDllImporter.Import<IUser>("user32.dll"); /// user.EnumWindows(new EnumWindowsHandler(OnWindowEnumerated), IntPtr.Zero); /// } /// private static IUser user; /// private static bool OnWindowEnumerated(IntPtr window, IntPtr lParam) { /// int processId; /// user.GetWindowThreadProcessId(window, out processId); /// Console.WriteLine("{0} : {1}", window, processId); /// return true; /// } /// } /// /// public sealed class DynamicDllImporter { private DynamicDllImporter() {} private static readonly ModuleBuilder module; static DynamicDllImporter() { AssemblyName name = new AssemblyName(); name.Name = "DynamicDllImporter"; AssemblyBuilder assem = AppDomain.CurrentDomain.DefineDynamicAssembly(name, AssemblyBuilderAccess.Run); module = assem.DefineDynamicModule("DynamicDllImporter"); } #if !V10 && !V11 /// ///型パラメータのインターフェイスに従って、指定したアンマネージド DLL ファイルの関数をインポートするメソッドを提供するインスタンスを作成します。 /// ///インポートする関数の雛形を持つインターフェイス型。 ///インポートするアンマネージド DLL のパス。カレントディレクトリまたはパスが通っているディレクトリに存在する DLL では、DLL の名前だけで構いません。拡張子が .DLL の場合、省略できます。 ///作成したインスタンス。 /// に null が指定されました。 ///以下のいずれかの原因が考えられます。 /// /// にインターフェイスではない型が指定されました。 /// が外部のアセンブリから参照可能ではありません。 /// にプロパティまたはイベントが定義されています。 /// に空文字列が指定されました。 /// /// public static TDeclared Import(string dllName) { return (TDeclared)Import(typeof(TDeclared), dllName); } /// ///型パラメータのインターフェイスに従って、指定したアンマネージド DLL ファイルの関数をインポートするメソッドを提供するインスタンスを作成します。 /// ///インポートする関数の雛形を持つインターフェイス型。 ///インポートするアンマネージド DLL のパス。カレントディレクトリまたはパスが通っているディレクトリに存在する DLL では、DLL の名前だけで構いません。拡張子が .DLL の場合、省略できます。 ///各関数に適用する属性を指定するための 。 ///作成したインスタンス。 /// のいずれかに null が指定されました。 ///以下のいずれかの原因が考えられます。 /// /// にインターフェイスではない型が指定されました。 /// が外部のアセンブリから参照可能ではありません。 /// にプロパティまたはイベントが定義されています。 /// に空文字列が指定されました。 /// /// public static TDeclared Import(string dllName, ImportInformation attr) { return (TDeclared)Import(typeof(TDeclared), dllName, attr); } #endif /// ///インターフェイスに従って、指定したアンマネージド DLL ファイルの関数をインポートするメソッドを提供するインスタンスを作成します。 /// ///インポートする関数の名前、シグネチャ、属性を定義したインターフェイスの型。 ///インポートするアンマネージド DLL のパス。カレントディレクトリまたはパスが通っているディレクトリに存在する DLL では、DLL の名前だけで構いません。拡張子が .DLL の場合、省略できます。 ///作成したインスタンス。 /// のいずれかに null が指定されました。 ///以下のいずれかの原因が考えられます。 /// /// にインターフェイスではない型が指定されました。 /// が外部のアセンブリから参照可能ではありません。 /// にプロパティまたはイベントが定義されています。 /// に空文字列が指定されました。 /// /// public static object Import(Type declaredInterface, string dllName) { return Import(declaredInterface, dllName, new ImportInformation()); } /// ///型パラメータのインターフェイスに従って、指定したアンマネージド DLL ファイルの関数をインポートするメソッドを提供するインスタンスを作成します。 /// ///インポートする関数の名前、シグネチャ、属性を定義したインターフェイスの型。 ///インポートするアンマネージド DLL のパス。カレントディレクトリまたはパスが通っているディレクトリに存在する DLL では、DLL の名前だけで構いません。拡張子が .DLL の場合、省略できます。 ///各関数に適用する属性を指定するための 。 ///作成したインスタンス。 /// のいずれかに null が指定されました。 ///以下のいずれかの原因が考えられます。 /// /// にインターフェイスではない型が指定されました。 /// が外部のアセンブリから参照可能ではありません。 /// にプロパティまたはイベントが定義されています。 /// に空文字列が指定されました。 /// /// public static object Import(Type declaredInterface, string dllName, ImportInformation attr) { if (declaredInterface == null) throw new ArgumentNullException("declaredInterface", "インターフェイスを null にすることはできません。"); if (! (declaredInterface.IsInterface)) throw new ArgumentException("インターフェイスを指定してください。", "declaredInterface"); if (! IsAccessibleFromOuterAssembly(declaredInterface)) throw new ArgumentException("使用するインターフェイスは外部アセンブリから参照できなければなりません。", "declaredInterface"); if (declaredInterface.GetProperties().Length > 0) throw new ArgumentException("使用するインターフェイスにプロパティが存在します。"); if (declaredInterface.GetEvents().Length > 0) throw new ArgumentException("使用するインターフェイスにイベントが存在します。"); if (dllName == null) throw new ArgumentNullException("dllName", "DLL の名前を null にすることはできません。"); if (dllName == "") throw new ArgumentException("DLL の名前を 空文字列にすることはできません。"); if (attr == null) throw new ArgumentNullException("attr", "属性を null にすることはできません。"); // 作成する型の名前。適当。かぶらないように時間を含む string typeName = string.Format("{0}<{1}>({2})", declaredInterface.Name, dllName.Split('.')[0], DateTime.Now.ToString("HHmmssfffffff")); TypeBuilder type = module.DefineType(typeName, TypeAttributes.Public, null, new Type[]{declaredInterface}); //エントリ名の置換を正規表現で行う場合のRegexオブジェクト Regex replacer = null; if (attr.Pattern != null && attr.ReplacedByRegex) replacer = new Regex(attr.Pattern); //DllImport 属性の CustomAttributeBuilder 作成用 //DllImportAttribute のコンストラクタ引数。DLL名: [DllImport("dllName")] object[] ctorParam = new object[]{dllName}; Type dllimport = typeof(DllImportAttribute); ConstructorInfo ctor = dllimport.GetConstructor(new Type[]{typeof(string)}); FieldInfo[] fieldInfos = dllimport.GetFields(); //DllImport 属性ビルダ用の各種フィールドを設定 object[] fieldValues = new object[fieldInfos.Length]; for (int i = 0; i < fieldInfos.Length; i++) { fieldValues[i] = attr.FindField(fieldInfos[i].Name); } //インターフェイスの各メソッドを実装する foreach (MethodInfo baseMethod in declaredInterface.GetMethods()) { //インターフェイスのメソッド名=実装するメソッド名と、関数のエントリポイント string methodName = baseMethod.Name, entryName = methodName; //必要に応じてエントリポイントの名前を置換 if (attr.Pattern != null) entryName = (replacer == null) ? entryName.Replace(attr.Pattern, attr.Replacement) : replacer.Replace(entryName, attr.Replacement); ParameterInfo[] paramInfos = baseMethod.GetParameters(); //メソッドの引数の型の配列 Type[] paramTypes = GetParameterTypes(paramInfos); //static extern なメソッドの定義 //名前は任意だが、かぶることがないように記号を含ませてみる。 MethodBuilder implMethod = type.DefineMethod(methodName + "(dllimport)", MethodAttributes.Private | MethodAttributes.PinvokeImpl | MethodAttributes.HideBySig | MethodAttributes.Static, baseMethod.ReturnType, paramTypes); #if !V10 && !V11 //.NET 2.0 からは、返値の属性をDefineParameter(0, ...)で定義するParameterBuilderで設定できるようになった。ヘルプの DefineParameter には書かれてないけど。 //.NET 1.x では返値の属性を設定できない。設計ミスと思われる ParameterBuilder retparamBuilder = implMethod.DefineParameter(0, baseMethod.ReturnParameter.Attributes, null); AddCustomAttributes(retparamBuilder, baseMethod.ReturnTypeCustomAttributes); #endif //各パラメータの属性を設定 for (int i = 0; i < paramInfos.Length; i++) { //DefineParameter第一引数は1スタート。0は.NET2.0で返値の意味になった ParameterBuilder paramBuilder = implMethod.DefineParameter(i + 1, paramInfos[i].Attributes, "arg" + (i + 1).ToString()); AddCustomAttributes(paramBuilder, paramInfos[i]); } //DllImport 属性の EntryPoint フィールドを設定 //fieldInfosの3番目にEntryPointがあったら fieldValuesの3番目に値を入れる、が必要 //ちなみにこの属性の他のフィールドはforeach以前に設定済み for (int i = 0; i < fieldInfos.Length; i++) { if (fieldInfos[i].Name == "EntryPoint") { fieldValues[i] = entryName; break; } } //DllImport 属性のビルダを作成、セット CustomAttributeBuilder attrBuilder = new CustomAttributeBuilder(ctor, ctorParam, fieldInfos, fieldValues); implMethod.SetCustomAttribute(attrBuilder); //インターフェイスメソッドの実装メソッドの定義 MethodBuilder derivMethod = type.DefineMethod(methodName, MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.Final | MethodAttributes.NewSlot | MethodAttributes.HideBySig, baseMethod.ReturnType, paramTypes); //ILは、上で定義した static extern メソッドを呼び出してその結果を返すだけ ILGenerator il = derivMethod.GetILGenerator(); //引数を読み込む for (int i = 1; i <= paramTypes.Length; i++) il.Emit(OpCodes.Ldarg_S, (byte)i); //返値がvoidの場合でも、スタックに積まれないのでRetでOK il.Emit(OpCodes.Call, implMethod); il.Emit(OpCodes.Ret); //インターフェイスメソッドの実装であることを宣言 type.DefineMethodOverride(derivMethod, baseMethod); } return Activator.CreateInstance(type.CreateType()); } //あるパラメータのカスタム属性をコピーする。完全な自動化は無理。 private static void AddCustomAttributes(ParameterBuilder paramBuilder, ICustomAttributeProvider parameter) { //元となるパラメータのカスタム属性のインスタンス配列を取得 //このままコピーできたらいいのにね…… object[] attrs = parameter.GetCustomAttributes(false); foreach (object attr in attrs) { Type attrType = attr.GetType(); BindingFlags flag = BindingFlags.Public | BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly; //属性のプロパティとフィールドを取得 PropertyInfo[] props = attrType.GetProperties(flag); FieldInfo[] fields = attrType.GetFields(flag); //コンストラクタを取得。基本的に一番引数が少ないのを使用する ConstructorInfo[] ctors = attrType.GetConstructors(); ConstructorInfo ctor = ctors[0]; int min = ctor.GetParameters().Length; for (int i = 1; i < ctors.Length; i++) { int length = ctors[i].GetParameters().Length; if (length < min) { ctor = ctors[i]; min = length; } } Type[] paramTypes = GetParameterTypes(ctor.GetParameters()); object[] param = new object[paramTypes.Length]; //全てを機械的に処理するのは無理なので、ある程度決め撃ち //MashalAsみたいな、コンストラクタ引数を読みとり専用プロパティValueで公開するの向け PropertyInfo valueProperty = attrType.GetProperty("Value"); if (valueProperty != null && param.Length == 1 && paramTypes[0].Equals(valueProperty.PropertyType)) { param[0] = valueProperty.GetValue(attr, null); } //基本的にはこっち。全てをデフォルト値で指定する else { for (int i = 0; i < param.Length; i++) { //参照型はほっといてもnullが入るが、値型は妥当な初期値を入れとく必要がある if (paramTypes[i].IsSubclassOf(typeof(ValueType))) param[i] = Activator.CreateInstance(paramTypes[i]); } } //読み書きどちらも可能なプロパティだけ取得設定する ArrayList accessible = new ArrayList(); foreach (PropertyInfo prop in props) if (prop.CanRead && prop.CanWrite) accessible.Add(prop); props = (PropertyInfo[])accessible.ToArray(typeof(PropertyInfo)); object[] propValues = new object[props.Length]; for (int i = 0; i < props.Length; i++) { //引数付きプロパティはどうしようもないので無視 if (props[i].GetIndexParameters().Length > 0) continue; propValues[i] = props[i].GetValue(attr, null); } //フィールドの取得/設定 object[] fieldValues = new object[fields.Length]; for (int i = 0; i < fields.Length; i++) { fieldValues[i] = fields[i].GetValue(attr); } //カスタム属性の作成とセット paramBuilder.SetCustomAttribute(new CustomAttributeBuilder(ctor, param, props, propValues, fields, fieldValues)); } } //ParameterInfo配列から、それぞれのパラメータの型の配列を取得 private static Type[] GetParameterTypes(ParameterInfo[] parameters) { Type[] paramTypes = new Type[parameters.Length]; for (int i = 0; i < paramTypes.Length; i++) paramTypes[i] = parameters[i].ParameterType; return paramTypes; } private static bool IsAccessibleFromOuterAssembly(Type type) { //名前空間直下でPublicならアクセス可能 if (type.IsPublic) return true; //ネストクラスの場合、いずれにせよネスト内でPublicでなければアクセス不能 //非ネストクラスの場合、Publicでない=internalなのでアクセス不能 if (!type.IsNestedPublic) return false; //ネスト内でPublicなネストクラスの場合、自分を定義するクラスが外部アセンブリからアクセス可能かどうか確認する return IsAccessibleFromOuterAssembly(type.DeclaringType); } } ///各メソッドに付加する属性などの、インポート時に適用される追加情報を表します。 /// /// このクラスは、それぞれのアンマネージド関数に適用される DllImportAttribute 属性に追加する各種フィールドの設定のほか、既定の名前以外のエントリポイントに変更することにも使われます。 /// ///統合アーカイバプロジェクトの API 仕様にしたがった cab32.dll と unlha32.dll をそれぞれ使用し、引数に与えられたファイルの解凍を行うサンプルコードです。cab32.dll と unlha32.dll は実行ファイルと同じディレクトリかシステムディレクトリにインストール済みであることが前提です。 /// /// using System; /// using System.Runtime.InteropServices; /// public interface IDecompressor { /// int DllName(IntPtr window, string commandLine, IntPtr output, int size); /// bool DllNameCheckArchive(string fileName, int mode); /// } /// /// public class EntryPoint { /// public static void Main(string[] args) { /// if (args.Length == 0) /// return; /// ImportInformation attr = new ImportInformation(); /// attr.CharSet = CharSet.Ansi; /// attr.SetReplacePattern("DllName", "Cab", false); /// IDecompressor dec = (IDecompressor)DynamicDllImporter.Import(typeof(IDecompressor), "cab32.dll", attr); /// foreach (string file in args) { /// if (dec.DllNameCheckArchive(file, 1)) { /// dec.DllName(IntPtr.Zero, "-x \"" + file + "\" *.*", IntPtr.Zero, 0); /// } /// } /// attr.SetReplacePattern("DllName", "Unlha", false); /// dec = (IDecompressor)DynamicDllImporter.Import(typeof(IDecompressor), "unlha32.dll", attr); /// foreach (string file in args) { /// if (dec.DllNameCheckArchive(file, 1)) { /// dec.DllName(IntPtr.Zero, "x \"" + file + "\" *.*", IntPtr.Zero, 0); /// } /// } /// } /// } /// /// public class ImportInformation { /// ///置換パターンを設定します。 /// ///置換するパターンを表す文字列。この値が null または の場合、置換を行いません。 ///置換後の文字列。 ///置換を正規表現を用いて行う場合は true 。 /// /// このメソッドを使用することで、DLL によって名前が異なる、共通シグネチャを持つアンマネージド関数を一括でインポートすることが可能になります。 /// に null または 以外の値を指定した場合、元となるインターフェイスの各メソッド名に対して実際に呼び出す DLL 関数の名前を置き換えることができます。 /// 例えばインターフェイスに MethodA、MethodB が定義されているとき、 に "Method" 、 に "Get" が指定された場合、実際に呼び出す DLL 内の関数のエントリポイントは、それぞれ GetA 、GetB と言うことになります。 /// に true を指定することで、置換に正規表現を用いることもできます。このとき は正規表現文字列を表し、 はマッチしたものに対するメタ文字列を受け入れられる置換文字列を表すことになります。置換には を使用します。置換動作の詳細はそちらを参照してください。 /// /// が null または空文字列()以外の時に、 に null を指定しました。 public void SetReplacePattern(string pattern, string replacement, bool byRegex) { if (pattern == null || pattern == "") { this.pattern = null; this.replacement = null; } else { if (replacement == null) throw new ArgumentNullException("replacement", "置換後の文字列を null にすることはできません。"); this.pattern = pattern; this.replacement = replacement; this.byRegex = byRegex; } } ///置換するパターンを表す文字列を取得します。 ///置換パターンを表す文字列。null の場合、置換を行いません。 public string Pattern { get { return this.pattern; } } ///置換後の文字列を表す文字列を取得します。 ///置換後の文字列。 が null の時はこの値も null になります。 public string Replacement { get { return this.replacement; } } ///置換に正規表現を用いるかどうかを表す値を取得します。 ///正規表現を用いる場合は true 。 public bool ReplacedByRegex { get { return this.byRegex; } } ///関数とのマーシャリングに使用する文字セットを取得または設定します。 ///マーシャリング時に使われる文字セットを表す 列挙体の値。既定値は です。 ///不正な が指定されました。 /// public Interop.CharSet CharSet { get { return this.charSet; } set { if (!Enum.IsDefined(typeof(Interop.CharSet), value)) throw new InvalidEnumArgumentException("value", (int)value, typeof(Interop.CharSet)); this.charSet = value; } } ///アンマネージド関数の呼び出し規約を取得または設定します。 ///関数の呼び出し規約を表す 列挙体の値。既定値は です。 ///不正な が指定されました。 /// public CallingConvention CallingConvention { get { return this.callingConvention; } set { if (!Enum.IsDefined(typeof(Interop.CallingConvention), value)) throw new InvalidEnumArgumentException("value", (int)value, typeof(Interop.CallingConvention)); this.callingConvention = value; } } ///シグネチャがアンマネージドのエントリポイントを直接変換したものかどうかを表す値を取得または設定します。 ///直接変換したものの場合は true 。既定値は true です。 /// public bool PreserveSig { get { return this.preserveSig; } set { this.preserveSig = value; } } ///呼び出し元に戻る前に、関数が Win32API の SetLastError 関数を呼び出すかどうかを取得または設定します。 ///関数が SetLastError を呼び出す場合は true 。既定値は false です。 /// public bool SetLastError { get { return this.setLastError; } set { this.setLastError = value; } } ///指定された名前以外のエントリポイント名を DLL 関数から検索させるかどうかを取得または設定します。 ///検索させない場合は true 。既定値は false です。 /// public bool ExactSpelling { get { return this.exactSpelling; } set { this.exactSpelling = value; } } private string pattern; private string replacement; private bool byRegex; private CharSet charSet = CharSet.Ansi; private CallingConvention callingConvention = CallingConvention.Winapi; private bool preserveSig = true; private bool setLastError = false; private bool exactSpelling = false; #if !V10 ///ANSI 文字に変換するときの、最適マッピング動作を取得または設定します。 ///最適マッピングを行う場合は true 。既定値は true です。 /// public bool BestFitMapping { get { return this.bestFitMapping; } set { this.bestFitMapping = value; } } ///マップできない Unicode 文字 (ANSI の "?" に変換される文字) が見つかったときに、例外を投げるかどうかを取得または指定します。 ///マップできないときに例外を投げる場合は true 。既定値は false です。 /// public bool ThrowOnUnmappableChar { get { return this.throwOnUnmappableChar; } set { this.throwOnUnmappableChar = value; } } private bool bestFitMapping = true; private bool throwOnUnmappableChar = false; #endif /// ///指定した名前のフィールドの値を返します。大文字小文字の区別を無視します。 /// ///検索するフィールドの名前。 ///このインスタンスの該当するフィールドの値。該当するフィールドが存在しなかった場合、null 。 internal object FindField(string field) { BindingFlags flag = BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.IgnoreCase; FieldInfo thisField = typeof(ImportInformation).GetField(field, flag); return (thisField == null) ? null : thisField.GetValue(this); } } }