連載が終わった途端放置気味。意味もなく五七五で始めてみました。
コメント欄でちょっと話の出たホットキーの記事を書こうと思っていたのですが、調べれば調べるほど(本筋とは関係のないところで)難しい話になっていったので、後回し。いつになるかは未定義です。こだわりさえ捨てればいいんですが。
今回のネタはまさにネタですが、動的にアンマネージド DLL をインポートするってどうよ、と言う話です。
アンマネージド関数のインポートは通常 DllImport 属性(あるいは VB.NET なら Declare 構文)を使いますが、これを使用してインポートするには DLL 名とエントリポイントを静的に決定しておかなければなりません。これでは例えば統合アーカイバプロジェクトの DLL を使うにも、別の DLL に対しては別の関数を使わざるを得ず、あまり嬉しくない状況です。
動的にインポートする主要な手段は、とくに .NET 1.1 まででは Managed C++ を使い、そちらで LoadLibrary と GetProcAddress を呼び出すというものでした。この場合、Managed C++ の DLL はその関数の呼び出しをマネージドのメソッドにラップして C#/VB.NET に公開することになります。
.NET 2.0 では Marshal クラスに GetDelegateForFunctionPointer メソッドが追加され、C#/VB.NET 単独でも LoadLibrary と GetProcAddress さえ 静的インポートしてやればアンマネージド関数の動的インポートが可能になりました(.NET 1.1 まででは GetProcAddress で取得した関数ポインタを C#/VB.NET から扱うすべがなかったのです)。
そこで今回紹介するソリューションが、「動的に DllImport 属性そのものを作っちまえ」、です。もちろん属性はアセンブリが作られた時に決定する静的なもので、後から付けたり消したりできるものではありません。ではどう解決するか。動的にアセンブリごと作っちゃいましょう。
.NET には動的にアセンブリを作成する手段が備わっています。一つは CodeDom による動的コンパイル。文字列やソースファイルから実行時にコンパイラを使ってアセンブリを作成します。もう一つはリフレクションを使った、メソッドの実装は IL(中間言語)使ってしこしこ手書きというローレベルな動的アセンブリ定義です。今回はこの後者の方を使ってみたいと思います。コンパイラ使うだけあって動的コンパイルは結構作成時のコストが大きいので。
まず構成を考えましょう。動的にアセンブリ(と言うか型)を作成するのは良いとして、そこで定義された DllImport な静的メソッドにどうやってアクセスすべきでしょうか。メソッド名を指定してリフレクションで実行する手段もありますが、これはできれば避けておきたいところ。いくら動的とは言え静的にできる部分は静的にしたいものです。手段は二つ考えられます。一つはデリゲートインスタンスに突っ込むこと。もう一つはインスタンスメソッドから呼び出すようにすること。
デリゲートを使う手法は、インポートする関数が一つだけの時は便利ですが、複数インポートする時は管理が面倒になりがちです。基本的に目標を統合アーカイバプロジェクト仕様の各 DLL を動的インポートするところにおいていますので、複数の DLL からそれぞれ複数の関数をインポートする前提で話を進めるべきです。となると、インスタンスメソッドから呼び出す手法と言うことになります。この場合、このインスタンスメソッドをインターフェイスの実装と言うことにすれば、呼び出しも簡単です。
となると次はインターフェイスの構造を考えなければなりませんね。まず、エントリポイントとなる関数名はそのままメソッドの名前にすればいいでしょう。引数・返値はそのまま定義してもらえばいいですし、もし引数に属性が付加されるのならそれらは全ての DLL で共通になると仮定して、このインターフェイスで宣言するメソッドそのものに付けてもらえばいいでしょう。DllImport 属性そのものはどうしましょうか。もちろんインターフェイスで宣言したメソッドに付けてもらうわけにはいきません。そもそもこの属性は同じ DLL からインポートする場合は EntryPoint 以外は大抵共通になるものでしょう。となるとこのインターフェイス全体に対して一つの設定を使い回せばいいと言うことになります。それを属性で与える手もありますが、わざわざそんなことせずともアセンブリ作成時に設定をまとめたクラスを引数に渡してやれば済む話ですね。
さて、統合アーカイバプロジェクト仕様に対応すると言うことになるとこのままでは問題があります。各関数のエントリポイントが、DLL ごとにバラバラと言うことです。例えばアーカイブの適合検査をする関数が、 unlha32.dll なら UnlhaCheckArchive に、cab32.dl なら CabCheckArchive になります。これではそのままインターフェイスで宣言したメソッド名をエントリポイントにするわけにはいきません。しかしこれらの関数名にはしっかり命名規約が存在し、各 DLL ごとに固有の冠詞+機能ごとの固定名(適合検査なら CheckArchive 、バージョン取得なら GetVersion)ということになっています。ということは、インターフェイスで宣言するメソッド名のうち一部を仮定の名前とし、エントリポイントを設定する際に実際の名前に置換する、という手段を執れば解決できそうです。
さて、これで利用者から見た仕様は決定しました。まず利用者は事前にインターフェイスを定義します。インターフェイスにはインポートする関数の名前を付けたメソッドを定義します。オーバーロードしても構いません。次に、動的インポートメソッドを呼び出します。引数には使用するインターフェイス、インポートする対象の DLL 名、それに必要であれば追加情報を格納するクラスのインスタンスを与えます。動的インポートメソッドは、引数に与えたインターフェイスを実装したインスタンスを返します。利用者はこのインターフェイスを使って任意の(インターフェイスのメソッドでラップされた)アンマネージド関数を使用することができるようになります。
と言うことで、今回のソースコード(XML ドキュメントコメント付き)へのリンクです。
ここからは技術的なお話です。
まずは動的アセンブリの概要を簡単に(あれ、馬から落馬?)紹介しておきましょう。リフレクションを使って動的アセンブリを作成するのには、System.AppDomain クラスの DefineDynamicAssembly メソッドが入り口になります。これで作られた AssemblyBuilder の DefineDynamicModule メソッドで ModuleBuilder を、さらにその ModuleBuilder から DefineType で TypeBuilder を作成します。後はこの TypeBuilder から、メソッドやプロパティ、フィールドを動的に定義していきます。例えばメソッドは、名前や疑似カスタム属性(アクセス修飾子や静的か否かなどの情報です)、引数・返値の型やらを指定し TypeBuilder.DefineMethod を使って形を定義し、GetILGenerator で取得できる ILGenerator に対して実装である IL コードを書き込んでいきます。SetCustomAttribute でカスタム属性の設定もできます。
.NET 1.0/1.1 では、何故かメソッドの返値に対して属性を設定できません。これは明らかに設計ミスだと思います。幸い .NET 2.0 で可能になりましたが、それも変則的な手法を使っていて(DefineParameter、つまりパラメータ定義用のメソッドを流用しているのです)、おまけにこのメソッドに対するヘルプにはそんなこと一言も触れて無いどころか例外が出るように書いています(.NET 1.0/1.1 のときののまま変更を忘れてたんでしょう)。別のところに載っていたサンプルで使っていたので気づきましたが。
一通り作り終わったら、仕上げに TypeBuilder.CreateType で型の作成を完了して実際に使用可能な型を取得します。この型のインスタンスは Activator.CreateInstance などを使えば作成できます。作成する型には実装するインターフェイスや基底クラスなどを持たせることもできるので(当然のことながらそれらが他のアセンブリから見えることが前提ですが)、その場合はこの作成したインスタンスをそれらにキャストして使用することも可能になります。と言うか普通はそう言う使い方がメインになるはずです。今回の実装もそうですね。
ちなみに、今回はやってませんが、ここで作成したアセンブリはファイルに DLL として保存することもできます。
さて、それでは今回作ったクラス・DynamicDllImporter の解説。ソースを片手に読んだほうが良いかも知れません。
実装は全て静的メソッドで行っています。特に動的である意味がないですし、単純なファクトリですし。とは言え毎回アセンブリごと作るのも無駄なので、ModuleBuilder の作成までは静的コンストラクタにやらせます。これはフィールドに置いて、実際に型を作成するときに使います。コンストラクタを private にしているのは、.NET 2.0 であれば static クラスにするところですね。
中心的なメソッドである Import にはオーバーロードが 2 つあり、また .NET 2.0 用に同名のジェネリックメソッドが 2 つ定義されています。ジェネリックメソッドの方は単に非ジェネリックのメソッドを呼び出し、返値をキャストして返すだけですが、これのおかげで呼び出し側でインターフェイスにキャストする必要が無くなってちょっとだけ便利です。更に非ジェネリックのオーバーロードの内、インターフェイス型と DLL 名だけを受け取るメソッドの方はデフォルトの ImportInformation インスタンスを作成してもう一つのオーバーロードに渡すだけですので、実質一つと言うことになります。
さてその Import メソッドですが、まあやってることはそんなに特別なことではありません。動的型作成そのものが特別なことかも知れませんけど。適当に型名を付けて作成し、インターフェイスで定義されているメソッドをループで回して、それぞれ対応するインターフェイスの実装メソッドとそれが呼び出す DllImport な静的メソッドを作成します。DllImport な静的メソッドは、インターフェイスの実装メソッドと同名にするわけにはいかないのでちょっと追加。DllImport の EntryPoint フィールドで実際にインポートする関数名を指定できますので、この静的メソッドの名前にはさして意味はありません。インターフェイスの実装メソッドには IL を使って実装を書く必要がありますが、これはたいしたことじゃありません。引数を読み出してそのまま DllImport 静的メソッドに渡すだけです。値渡しか参照渡しかを悩む余地すらありません。
実は IL 的にはメソッド名などに記号が含まれていても問題なかったりします。今回も関数名に (dllimport) なんて括弧付きのを追加しています。型名にもそんな名前を付けてたり。大抵の言語ではそんな名前付けたらコンパイラが撥ねますけどね。
パラメータ(.NET 2.0 では返値も)の属性設定は長くなるので別メソッドで行っています。このメソッドの引数のうち、第一引数は属性を付加する先の ParameterBuilder を表し、第二引数は付加する属性の実体を持っている ICustomAttributeProvider で、これは要するにインターフェイスで宣言されている各メソッドにおける、パラメータ(または返値)を指します。このパラメータに付加されている属性を、第一引数の ParameterBuilder に付加させるのが目的な訳ですね。
さて、コードのコメントにも書いていますが、属性の設定コピーの完全自動化は不可能です。Attribute インスタンスを渡して「はいこれで属性作ってね」で済めば天国だったのですが、残念ながらそんなわけにも行かず、CustomAttributeBuilder を作る必要が出てきます。
まず、パラメータには複数属性が付いている可能性がありますので(In と Out と MarshalAs とか。実は In と Out は疑似カスタム属性の一種なんですが、普通に GetCustomAttributes でも取得できる特殊な属性です)、GetCustomAttributes で属性インスタンスの配列を取得し、これを元に一つずつ処理します。
CustomAttributeBuilder には、属性クラスの使用するコンストラクタを表す ConstructorInfo 、そのコンストラクタに渡す引数、あとその他設定する フィールドの FieldInfo 配列&その値の配列、とプロパティの PropertyInfo 配列&その値の配列、を渡す必要があります(後ろ二つはオプションですが)。コンストラクタ引数を取らない属性は全く問題なくそれで終了ですが、問題は引数を取るコンストラクタしかなかった場合です。一つの方法として、コンストラクタでは全てをデフォルト値で設定し、プロパティやフィールドを設定することで初期化するという手が考えつきます。しかしこれはうまくありません。MarshalAs 属性を見ればすぐ分かりますが、コンストラクタ引数で渡す値はプロパティでは get しかできないので、「後から設定」はできないのです。ここに自動化は破綻します。ですので、属性によって適用方法を変えなければなりません。今回は、コンストラクタ引数が一つしか取らず、Value プロパティを持っていて、そのプロパティとコンストラクタ引数の型が一致している場合において、その Value プロパティの値をコンストラクタ引数に与える、という手段を執っています。一見汎用性がありそうですが、実際のところ MarshalAs 狙い撃ちです。ですので、指定する属性によってはこの部分で失敗する可能性があります。In、Out、MarshalAs さえできればマーシャリングにはそう不自由しないとは思いますが。あと、プロパティとフィールドはそんなに難しくもないでしょう。プロパティは取得と設定両方できるものだけにする必要があるってくらいです。
それからおまけのようにヘルパメソッドが二つ定義されていますが、まあ見れば分かるとおりです。
追加情報クラスについては特に書くことはありません。
現在このクラスには DLL アンロードの機能を付けていません。.NET において読み込んだアセンブリをアンロードするにはアプリケーションドメインごとアンロードする必要がありますが、このクラスに実装するとすると、Import メソッドを呼び出すたびにアプリケーションドメインから作る必要があるので二の足を踏んでしまいます。
なお、このクラスは恐らく相当量不完全な部分が含まれているはずですので、こんな例外が出たみたいな情報がありましたらお知らせください。
// C# 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.Reflection { 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 public static TDeclared Import<TDeclared>(string dllName) { return (TDeclared)Import(typeof(TDeclared), dllName); } public static TDeclared Import<TDeclared>(string dllName, ImportInformation attr) { return (TDeclared)Import(typeof(TDeclared), dllName, attr); } #endif public static object Import(Type declaredInterface, string dllName) { return Import(declaredInterface, dllName, new ImportInformation()); } 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 のコンストラクタ引数 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); } } public class ImportInformation { 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; } } public string Pattern { get { return this.pattern; } } public string Replacement { get { return this.replacement; } } 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; } } public bool PreserveSig { get { return this.preserveSig; } set { this.preserveSig = value; } } public bool SetLastError { get { return this.setLastError; } set { this.setLastError = value; } } 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 public bool BestFitMapping { get { return this.bestFitMapping; } set { this.bestFitMapping = value; } } public bool ThrowOnUnmappableChar { get { return this.throwOnUnmappableChar; } set { this.throwOnUnmappableChar = value; } } private bool bestFitMapping = true; private bool throwOnUnmappableChar = false; #endif 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); } } } ' VB.NET Imports System Imports System.Collections Imports System.ComponentModel Imports System.Runtime.InteropServices Imports System.Reflection Imports System.Reflection.Emit Imports System.Text Imports System.Text.RegularExpressions Imports Interop = System.Runtime.InteropServices Namespace HongliangSoft.Utilities.Reflection Public NotInheritable Class DynamicDllImporter Private Sub New() End Sub Private Shared ReadOnly _module As ModuleBuilder Shared Sub New() Dim name As AssemblyName = New AssemblyName() name.Name = "DynamicDllImporter" Dim assem As AssemblyBuilder = _ AppDomain.CurrentDomain.DefineDynamicAssembly( _ name, AssemblyBuilderAccess.Run) _module = assem.DefineDynamicModule("DynamicDllImporter") End Sub #If Not(V = 10 OrElse V = 11) Public Shared Function Import(Of TDeclared)( _ ByVal dllName As String) As TDeclared Return DirectCast(Import(GetType(TDeclared), dllName), TDeclared) End Function Public Shared Function Import(Of TDeclared)( _ ByVal dllName As String, ByVal attr As ImportInformation) _ As TDeclared Return DirectCast(Import(GetType(TDeclared), dllName, attr), TDeclared) End Function #End If Public Shared Function Import( _ ByVal declaredInterface As Type, ByVal dllName As String) As Object Return Import(declaredInterface, dllName, New ImportInformation()) End Function Public Shared Function Import( _ ByVal declaredInterface As Type, ByVal dllName As String, _ ByVal attr As ImportInformation) As Object If declaredInterface Is Nothing Then Throw New ArgumentNullException( _ "declaredInterface", _ "インターフェイスを null にすることはできません。") End If If Not(declaredInterface.IsInterface) Then Throw New ArgumentException( _ "インターフェイスを指定してください。", "declaredInterface") End If If Not(IsAccessibleFromOuterAssembly(declaredInterface)) Then Throw New ArgumentException( _ "使用するインターフェイスは外部アセンブリから" _ & "参照できなければなりません。", _ "declaredInterface") End If If declaredInterface.GetProperties().Length > 0 Then Throw New ArgumentException( _ "使用するインターフェイスにプロパティが存在します。") End If If declaredInterface.GetEvents().Length > 0 Then Throw New ArgumentException( _ "使用するインターフェイスにイベントが存在します。") End If If dllName Is Nothing Then Throw New ArgumentNullException( _ "dllName", "DLL の名前を null にすることはできません。") End If If dllName = "" Then Throw New ArgumentException( _ "DLL の名前を 空文字列にすることはできません。") End If If attr Is Nothing Then Throw New ArgumentNullException( _ "attr", "属性を null にすることはできません。") End If Dim i As Integer ' 作成する型の名前。適当。かぶらないように時間を含む Dim _typeName As String = String.Format( _ "{0}<{1}>({2})", _ declaredInterface.Name, _ dllName.Split(Chr(46)), _ DateTime.Now.ToString("HHmmssfffffff")) Dim _type As TypeBuilder = _module.DefineType( _ _typeName, TypeAttributes.Public, Nothing, _ New Type(){declaredInterface}) ' エントリ名の置換を正規表現で行う場合のRegexオブジェクト Dim replacer As Regex = Nothing If Not(attr.Pattern Is Nothing) AndAlso attr.ReplacedByRegex Then replacer = New Regex(attr.Pattern) End If ' DllImport 属性の CustomAttributeBuilder 作成用 ' DllImportAttribute のコンストラクタ引数 Dim ctorParam As Object() = New Object(){dllName} Dim dllimport As Type = GetType(DllImportAttribute) Dim ctor As ConstructorInfo = dllimport.GetConstructor( _ New Type(){GetType(String)}) Dim fieldInfos As FieldInfo() = dllimport.GetFields() ' DllImport 属性ビルダ用の各種フィールドを設定 Dim fieldValues As Object() = New Object(fieldInfos.Length - 1){} For i = 0 To fieldInfos.Length - 1 fieldValues(i) = attr.FindField(fieldInfos(i).Name) Next Dim baseMethod As MethodInfo ' インターフェイスの各メソッドを実装する For Each baseMethod In declaredInterface.GetMethods() ' インターフェイスのメソッド名=実装するメソッド名と、 ' 関数のエントリポイント Dim methodName As String = baseMethod.Name Dim entryName As String = methodName ' 必要に応じてエントリポイントの名前を置換 If Not(attr.Pattern Is Nothing) Then If (replacer Is Nothing) Then entryName = entryName.Replace(attr.Pattern, attr.Replacement) Else entryName = replacer.Replace(entryName, attr.Replacement) End If End If Dim paramInfos As ParameterInfo() = baseMethod.GetParameters() ' メソッドの引数の型の配列 Dim paramTypes As Type() = GetParameterTypes(paramInfos) ' Shared dllimport なメソッドの定義 ' 名前は任意だが、かぶることがないように記号を含ませてみる。 Dim implMethod As MethodBuilder = _type.DefineMethod( _ methodName & "(dllimport)", _ MethodAttributes.Private Or MethodAttributes.PinvokeImpl _ Or MethodAttributes.HideBySig Or MethodAttributes.Static, _ baseMethod.ReturnType, paramTypes) #If Not(V = 10 OrElse V = 11) ' .NET 2.0 からは、返値の属性をDefineParameter(0, ...)で定義する ' ParameterBuilderで設定できるようになった。 ' ヘルプの DefineParameter には書かれてないけど。 ' .NET 1.x では返値の属性を設定できない。設計ミスと思われる Dim retparamBuilder As ParameterBuilder _ = implMethod.DefineParameter( _ 0, baseMethod.ReturnParameter.Attributes, Nothing) AddCustomAttributes(retparamBuilder, _ baseMethod.ReturnTypeCustomAttributes) #End If ' 各パラメータの属性を設定 For i = 0 To paramInfos.Length - 1 ' DefineParameter第一引数は1スタート。 ' 0は.NET2.0で返値の意味になった Dim paramBuilder As ParameterBuilder _ = implMethod.DefineParameter( _ i + 1, paramInfos(i).Attributes, _ "arg" & (i + 1).ToString()) AddCustomAttributes(paramBuilder, paramInfos(i)) Next ' DllImport 属性の EntryPoint フィールドを設定 ' fieldInfosの3番目にEntryPointがあったら ' fieldValuesの3番目に値を入れる、が必要 ' ちなみにこの属性の他のフィールドはforeach以前に設定済み For i = 0 To fieldInfos.Length - 1 If fieldInfos(i).Name = "EntryPoint" Then fieldValues(i) = entryName Exit For End If Next ' DllImport 属性のビルダを作成、セット Dim attrBuilder As CustomAttributeBuilder _ = New CustomAttributeBuilder( _ ctor, ctorParam, fieldInfos, fieldValues) implMethod.SetCustomAttribute(attrBuilder) ' インターフェイスメソッドの実装メソッドの定義 Dim derivMethod As MethodBuilder = _type.DefineMethod( _ methodName, _ MethodAttributes.Public Or MethodAttributes.Virtual _ Or MethodAttributes.Final Or MethodAttributes.NewSlot _ Or MethodAttributes.HideBySig, _ baseMethod.ReturnType, paramTypes) ' ILは、上で定義した Shared dllimport メソッドを呼び出して ' その結果を返すだけ Dim il As ILGenerator = derivMethod.GetILGenerator() ' 引数を読み込む For i = 1 To paramTypes.Length il.Emit(OpCodes.Ldarg_S, CByte(i)) Next ' 返値がvoidの場合でも、スタックに積まれないのでRetでOK il.Emit(OpCodes.Call, implMethod) il.Emit(OpCodes.Ret) ' インターフェイスメソッドの実装であることを宣言 _type.DefineMethodOverride(derivMethod, baseMethod) Next Return Activator.CreateInstance(_type.CreateType()) End Function ' あるパラメータのカスタム属性をコピーする。完全な自動化は無理。 Private Shared Sub AddCustomAttributes( _ ByVal paramBuilder As ParameterBuilder, _ ByVal parameter As ICustomAttributeProvider) Dim i As Integer ' 元となるパラメータのカスタム属性のインスタンス配列を取得 ' このままコピーできたらいいのにね…… Dim attrs As Object() = parameter.GetCustomAttributes(False) Dim attr As Object For Each attr In attrs Dim attrType As Type = attr.GetType() Dim flag As BindingFlags _ = BindingFlags.Public Or BindingFlags.Instance _ Or BindingFlags.Public Or BindingFlags.DeclaredOnly ' 属性のプロパティとフィールドを取得 Dim props As PropertyInfo() = attrType.GetProperties(flag) Dim fields As FieldInfo() = attrType.GetFields(flag) ' コンストラクタを取得。基本的に一番引数が少ないのを使用する Dim ctors As ConstructorInfo() = attrType.GetConstructors() Dim ctor As ConstructorInfo = ctors(0) Dim min As Integer = ctor.GetParameters().Length For i = 1 To ctors.Length - 1 Dim length As Integer = ctors(i).GetParameters().Length If length < min Then ctor = ctors(i) min = length End If Next Dim paramTypes As Type() = GetParameterTypes(ctor.GetParameters()) Dim param As Object() = New Object(paramTypes.Length - 1){} ' 全てを機械的に処理するのは無理なので、ある程度決め撃ち ' MashalAsみたいな、コンストラクタ引数を ' 読みとり専用プロパティValueで公開するの向け Dim valueProperty As PropertyInfo = attrType.GetProperty("Value") If (Not(valueProperty Is Nothing) AndAlso param.Length = 1 AndAlso _ paramTypes(0).Equals(valueProperty.PropertyType)) Then param(0) = valueProperty.GetValue(attr, Nothing) ' 基本的にはこっち。全てをデフォルト値で指定する Else For i = 0 To param.Length - 1 ' 参照型はほっといてもnullが入るが、 ' 値型は妥当な初期値を入れとく必要がある If paramTypes(i).IsSubclassOf(GetType(ValueType)) Then param(i) = Activator.CreateInstance(paramTypes(i)) End If Next End If ' 読み書きどちらも可能なプロパティだけ取得設定する Dim accessible As New ArrayList() Dim prop As PropertyInfo For Each prop In props If prop.CanRead AndAlso prop.CanWrite Then accessible.Add(prop) End If Next props = DirectCast(accessible.ToArray(GetType(PropertyInfo)), _ PropertyInfo()) Dim propValues As Object() = New Object(props.Length - 1){} For i = 0 To props.Length - 1 ' 引数付きプロパティはどうしようもないので無視 If props(i).GetIndexParameters().Length > 0 Then Continue For End If propValues(i) = props(i).GetValue(attr, Nothing) Next ' フィールドの取得/設定 Dim fieldValues As Object() = New Object(fields.Length - 1){} For i = 0 To fields.Length - 1 fieldValues(i) = fields(i).GetValue(attr) Next ' カスタム属性の作成とセット paramBuilder.SetCustomAttribute( _ New CustomAttributeBuilder(ctor, param, _ props, propValues, _ fields, fieldValues)) Next End Sub ' ParameterInfo配列から、それぞれのパラメータの型の配列を取得 Private Shared Function GetParameterTypes( _ ByVal parameters As ParameterInfo()) As Type() Dim paramTypes As Type() = New Type(parameters.Length - 1){} Dim i As Integer For i = 0 To paramTypes.Length - 1 paramTypes(i) = parameters(i).ParameterType Next Return paramTypes End Function Private Shared Function IsAccessibleFromOuterAssembly( _ ByVal type As Type) As Boolean ' 名前空間直下でPublicならアクセス可能 If type.IsPublic Then Return True End If ' ネストクラスの場合、ネスト内でPublicでなければ結局アクセス不能 ' 非ネストクラスの場合、Publicでない=internalなのでアクセス不能 If Not(type.IsNestedPublic) Then Return False End If ' ネスト内でPublicなネストクラスの場合、自分を定義するクラスが ' 外部アセンブリからアクセス可能かどうか確認する Return IsAccessibleFromOuterAssembly(type.DeclaringType) End Function End Class Public Class ImportInformation Public Sub SetReplacePattern( _ ByVal pattern As String, ByVal replacement As String, _ ByVal byRegex As Boolean) If pattern Is Nothing OrElse pattern = "" Then Me.m_pattern = Nothing Me.m_replacement = Nothing Else If replacement Is Nothing Then Throw New ArgumentNullException( _ "replacement", _ "置換後の文字列を Nothing にすることはできません。") End If Me.m_pattern = pattern Me.m_replacement = replacement Me.m_byRegex = byRegex End If End Sub Public ReadOnly Property Pattern() As String Get Return Me.m_pattern End Get End Property Public ReadOnly