連載が終わった途端放置気味。意味もなく五七五で始めてみました。
コメント欄でちょっと話の出たホットキーの記事を書こうと思っていたのですが、調べれば調べるほど(本筋とは関係のないところで)難しい話になっていったので、後回し。いつになるかは未定義です。こだわりさえ捨てればいいんですが。
今回のネタはまさにネタですが、動的にアンマネージド 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 {publicsealedclass DynamicDllImporter {private DynamicDllImporter() {}privatestaticreadonly 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 && !V11publicstatic TDeclared Import<TDeclared>(string dllName) {return (TDeclared)Import(typeof(TDeclared), dllName); }publicstatic TDeclared Import<TDeclared>(string dllName, ImportInformation attr) {return (TDeclared)Import(typeof(TDeclared), dllName, attr); }#endifpublicstaticobject Import(Type declaredInterface,string dllName) {return Import(declaredInterface, dllName,new ImportInformation()); }publicstaticobject Import(Type declaredInterface,string dllName, ImportInformation attr) {if (declaredInterface ==null)thrownew ArgumentNullException( "declaredInterface", "インターフェイスをnull にすることはできません。");if (! (declaredInterface.IsInterface))thrownew ArgumentException( "インターフェイスを指定してください。", "declaredInterface");if (! IsAccessibleFromOuterAssembly(declaredInterface))thrownew ArgumentException( "使用するインターフェイスは" + "外部アセンブリから参照できなければなりません。", "declaredInterface");if (declaredInterface.GetProperties().Length > 0)thrownew ArgumentException( "使用するインターフェイスにプロパティが存在します。");if (declaredInterface.GetEvents().Length > 0)thrownew ArgumentException( "使用するインターフェイスにイベントが存在します。");if (dllName ==null)thrownew ArgumentNullException( "dllName", "DLL の名前をnull にすることはできません。");if (dllName == "")thrownew ArgumentException( "DLL の名前を 空文字列にすることはできません。");if (attr ==null)thrownew 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 =newobject[]{dllName}; Type dllimport =typeof(DllImportAttribute); ConstructorInfo ctor = dllimport.GetConstructor(new Type[]{typeof(string)}); FieldInfo[] fieldInfos = dllimport.GetFields();// DllImport 属性ビルダ用の各種フィールドを設定object[] fieldValues =newobject[fieldInfos.Length];for (int i = 0; i < fieldInfos.Length; i++) { fieldValues[i] = attr.FindField(fieldInfos[i].Name); }// インターフェイスの各メソッドを実装するforeach (MethodInfo baseMethodin 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()); }// あるパラメータのカスタム属性をコピーする。完全な自動化は無理。privatestaticvoid AddCustomAttributes( ParameterBuilder paramBuilder, ICustomAttributeProvider parameter) {// 元となるパラメータのカスタム属性のインスタンス配列を取得// このままコピーできたらいいのにね……object[] attrs = parameter.GetCustomAttributes(false);foreach (object attrin 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 =newobject[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 propin props)if (prop.CanRead && prop.CanWrite) accessible.Add(prop); props = (PropertyInfo[])accessible.ToArray(typeof(PropertyInfo));object[] propValues =newobject[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 =newobject[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配列から、それぞれのパラメータの型の配列を取得privatestatic 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; }privatestaticbool IsAccessibleFromOuterAssembly(Type type) {// 名前空間直下でPublicならアクセス可能if (type.IsPublic)returntrue;// ネストクラスの場合、ネスト内でPublicでなければ結局アクセス不能// 非ネストクラスの場合、Publicでない=internalなのでアクセス不能if (!type.IsNestedPublic)returnfalse;// ネスト内でPublicなネストクラスの場合、自分を定義するクラスが// 外部アセンブリからアクセス可能かどうか確認するreturn IsAccessibleFromOuterAssembly(type.DeclaringType); } }publicclass ImportInformation {publicvoid SetReplacePattern(string pattern,string replacement,bool byRegex) {if (pattern ==null || pattern == "") {this.pattern =null;this.replacement =null; }else {if (replacement ==null)thrownew ArgumentNullException( "replacement", "置換後の文字列をnull にすることはできません。");this.pattern = pattern;this.replacement = replacement;this.byRegex = byRegex; } }publicstring Pattern {get {returnthis.pattern; } }publicstring Replacement {get {returnthis.replacement; } }publicbool ReplacedByRegex {get {returnthis.byRegex; } }public Interop.CharSet CharSet {get {returnthis.charSet; }set {if (!Enum.IsDefined(typeof(Interop.CharSet),value))thrownew InvalidEnumArgumentException( "value", (int)value,typeof(Interop.CharSet));this.charSet =value; } }public CallingConvention CallingConvention {get {returnthis.callingConvention; }set {if (!Enum.IsDefined(typeof(Interop.CallingConvention),value))thrownew InvalidEnumArgumentException( "value", (int)value,typeof(Interop.CallingConvention));this.callingConvention =value; } }publicbool PreserveSig {get {returnthis.preserveSig; }set {this.preserveSig =value; } }publicbool SetLastError {get {returnthis.setLastError; }set {this.setLastError =value; } }publicbool ExactSpelling {get {returnthis.exactSpelling; }set {this.exactSpelling =value; } }privatestring pattern;privatestring replacement;privatebool byRegex;private CharSet charSet = CharSet.Ansi;private CallingConvention callingConvention = CallingConvention.Winapi;privatebool preserveSig =true;privatebool setLastError =false;privatebool exactSpelling =false;#if !V10publicbool BestFitMapping {get {returnthis.bestFitMapping; }set {this.bestFitMapping =value; } }publicbool ThrowOnUnmappableChar {get {returnthis.throwOnUnmappableChar; }set {this.throwOnUnmappableChar =value; } }privatebool bestFitMapping =true;privatebool throwOnUnmappableChar =false;#endifinternalobject 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.NETImports SystemImports System.CollectionsImports System.ComponentModelImports System.Runtime.InteropServicesImports System.ReflectionImports System.Reflection.EmitImports System.TextImports System.Text.RegularExpressionsImports Interop = System.Runtime.InteropServicesNamespace HongliangSoft.Utilities.ReflectionPublicNotInheritableClass DynamicDllImporterPrivateSubNew()End SubPrivateSharedReadOnly _moduleAs ModuleBuilderSharedSubNew()Dim nameAs AssemblyName =New AssemblyName() name.Name = "DynamicDllImporter"Dim assemAs AssemblyBuilder = _ AppDomain.CurrentDomain.DefineDynamicAssembly( _ name, AssemblyBuilderAccess.Run) _module = assem.DefineDynamicModule("DynamicDllImporter")End Sub#IfNot(V = 10OrElse V = 11)PublicSharedFunction Import(Of TDeclared)( _ByVal dllNameAsString)As TDeclaredReturnDirectCast(Import(GetType(TDeclared), dllName), TDeclared)End FunctionPublicSharedFunction Import(Of TDeclared)( _ByVal dllNameAsString,ByVal attrAs ImportInformation) _As TDeclaredReturnDirectCast(Import(GetType(TDeclared), dllName, attr), TDeclared)End Function#End IfPublicSharedFunction Import( _ByVal declaredInterfaceAs Type,ByVal dllNameAsString)AsObjectReturn Import(declaredInterface, dllName,New ImportInformation())End FunctionPublicSharedFunction Import( _ByVal declaredInterfaceAs Type,ByVal dllNameAsString, _ByVal attrAs ImportInformation)AsObjectIf declaredInterfaceIsNothingThenThrowNew ArgumentNullException( _ "declaredInterface", _ "インターフェイスを null にすることはできません。")End IfIfNot(declaredInterface.IsInterface)ThenThrowNew ArgumentException( _ "インターフェイスを指定してください。", "declaredInterface")End IfIfNot(IsAccessibleFromOuterAssembly(declaredInterface))ThenThrowNew ArgumentException( _ "使用するインターフェイスは外部アセンブリから" _ & "参照できなければなりません。", _ "declaredInterface")End IfIf declaredInterface.GetProperties().Length > 0ThenThrowNew ArgumentException( _ "使用するインターフェイスにプロパティが存在します。")End IfIf declaredInterface.GetEvents().Length > 0ThenThrowNew ArgumentException( _ "使用するインターフェイスにイベントが存在します。")End IfIf dllNameIsNothingThenThrowNew ArgumentNullException( _ "dllName", "DLL の名前を null にすることはできません。")End IfIf dllName = ""ThenThrowNew ArgumentException( _ "DLL の名前を 空文字列にすることはできません。")End IfIf attrIsNothingThenThrowNew ArgumentNullException( _ "attr", "属性を null にすることはできません。")End IfDim iAsInteger' 作成する型の名前。適当。かぶらないように時間を含むDim _typeNameAsString =String.Format( _ "{0}<{1}>({2})", _ declaredInterface.Name, _ dllName.Split(Chr(46)), _ DateTime.Now.ToString("HHmmssfffffff"))Dim _typeAs TypeBuilder = _module.DefineType( _ _typeName, TypeAttributes.Public,Nothing, _New Type(){declaredInterface})' エントリ名の置換を正規表現で行う場合のRegexオブジェクトDim replacerAs Regex =NothingIfNot(attr.PatternIsNothing)AndAlso attr.ReplacedByRegexThen replacer =New Regex(attr.Pattern)End If' DllImport 属性の CustomAttributeBuilder 作成用' DllImportAttribute のコンストラクタ引数Dim ctorParamAsObject() =NewObject(){dllName}Dim dllimportAs Type =GetType(DllImportAttribute)Dim ctorAs ConstructorInfo = dllimport.GetConstructor( _New Type(){GetType(String)})Dim fieldInfosAs FieldInfo() = dllimport.GetFields()' DllImport 属性ビルダ用の各種フィールドを設定Dim fieldValuesAsObject() =NewObject(fieldInfos.Length - 1){}For i = 0To fieldInfos.Length - 1 fieldValues(i) = attr.FindField(fieldInfos(i).Name)NextDim baseMethodAs MethodInfo' インターフェイスの各メソッドを実装するFor Each baseMethodIn declaredInterface.GetMethods()' インターフェイスのメソッド名=実装するメソッド名と、' 関数のエントリポイントDim methodNameAsString = baseMethod.NameDim entryNameAsString = methodName' 必要に応じてエントリポイントの名前を置換IfNot(attr.PatternIsNothing)ThenIf (replacerIsNothing)Then entryName = entryName.Replace(attr.Pattern, attr.Replacement)Else entryName = replacer.Replace(entryName, attr.Replacement)End IfEnd IfDim paramInfosAs ParameterInfo() = baseMethod.GetParameters()' メソッドの引数の型の配列Dim paramTypesAs Type() = GetParameterTypes(paramInfos)' Shared dllimport なメソッドの定義' 名前は任意だが、かぶることがないように記号を含ませてみる。Dim implMethodAs MethodBuilder = _type.DefineMethod( _ methodName & "(dllimport)", _ MethodAttributes.PrivateOr MethodAttributes.PinvokeImpl _Or MethodAttributes.HideBySigOr MethodAttributes.Static, _ baseMethod.ReturnType, paramTypes)#IfNot(V = 10OrElse V = 11)' .NET 2.0 からは、返値の属性をDefineParameter(0, ...)で定義する' ParameterBuilderで設定できるようになった。' ヘルプの DefineParameter には書かれてないけど。' .NET 1.x では返値の属性を設定できない。設計ミスと思われるDim retparamBuilderAs ParameterBuilder _ = implMethod.DefineParameter( _ 0, baseMethod.ReturnParameter.Attributes,Nothing) AddCustomAttributes(retparamBuilder, _ baseMethod.ReturnTypeCustomAttributes)#End If' 各パラメータの属性を設定For i = 0To paramInfos.Length - 1' DefineParameter第一引数は1スタート。' 0は.NET2.0で返値の意味になったDim paramBuilderAs 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 = 0To fieldInfos.Length - 1If fieldInfos(i).Name = "EntryPoint"Then fieldValues(i) = entryNameExit ForEnd IfNext' DllImport 属性のビルダを作成、セットDim attrBuilderAs CustomAttributeBuilder _ =New CustomAttributeBuilder( _ ctor, ctorParam, fieldInfos, fieldValues) implMethod.SetCustomAttribute(attrBuilder)' インターフェイスメソッドの実装メソッドの定義Dim derivMethodAs MethodBuilder = _type.DefineMethod( _ methodName, _ MethodAttributes.PublicOr MethodAttributes.Virtual _Or MethodAttributes.FinalOr MethodAttributes.NewSlot _Or MethodAttributes.HideBySig, _ baseMethod.ReturnType, paramTypes)' ILは、上で定義した Shared dllimport メソッドを呼び出して' その結果を返すだけDim ilAs ILGenerator = derivMethod.GetILGenerator()' 引数を読み込むFor i = 1To 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)NextReturn Activator.CreateInstance(_type.CreateType())End Function' あるパラメータのカスタム属性をコピーする。完全な自動化は無理。PrivateSharedSub AddCustomAttributes( _ByVal paramBuilderAs ParameterBuilder, _ByVal parameterAs ICustomAttributeProvider)Dim iAsInteger' 元となるパラメータのカスタム属性のインスタンス配列を取得' このままコピーできたらいいのにね……Dim attrsAsObject() = parameter.GetCustomAttributes(False)Dim attrAsObjectFor Each attrIn attrsDim attrTypeAs Type = attr.GetType()Dim flagAs BindingFlags _ = BindingFlags.PublicOr BindingFlags.Instance _Or BindingFlags.PublicOr BindingFlags.DeclaredOnly' 属性のプロパティとフィールドを取得Dim propsAs PropertyInfo() = attrType.GetProperties(flag)Dim fieldsAs FieldInfo() = attrType.GetFields(flag)' コンストラクタを取得。基本的に一番引数が少ないのを使用するDim ctorsAs ConstructorInfo() = attrType.GetConstructors()Dim ctorAs ConstructorInfo = ctors(0)Dim minAsInteger = ctor.GetParameters().LengthFor i = 1To ctors.Length - 1Dim lengthAsInteger = ctors(i).GetParameters().LengthIf length < minThen ctor = ctors(i) min = lengthEnd IfNextDim paramTypesAs Type() = GetParameterTypes(ctor.GetParameters())Dim paramAsObject() =NewObject(paramTypes.Length - 1){}' 全てを機械的に処理するのは無理なので、ある程度決め撃ち' MashalAsみたいな、コンストラクタ引数を' 読みとり専用プロパティValueで公開するの向けDim valuePropertyAs PropertyInfo = attrType.GetProperty("Value")If (Not(valuePropertyIsNothing)AndAlso param.Length = 1AndAlso _ paramTypes(0).Equals(valueProperty.PropertyType))Then param(0) = valueProperty.GetValue(attr,Nothing)' 基本的にはこっち。全てをデフォルト値で指定するElseFor i = 0To param.Length - 1' 参照型はほっといてもnullが入るが、' 値型は妥当な初期値を入れとく必要があるIf paramTypes(i).IsSubclassOf(GetType(ValueType))Then param(i) = Activator.CreateInstance(paramTypes(i))End IfNextEnd If' 読み書きどちらも可能なプロパティだけ取得設定するDim accessibleAsNew ArrayList()Dim propAs PropertyInfoFor Each propIn propsIf prop.CanReadAndAlso prop.CanWriteThen accessible.Add(prop)End IfNext props =DirectCast(accessible.ToArray(GetType(PropertyInfo)), _ PropertyInfo())Dim propValuesAsObject() =NewObject(props.Length - 1){}For i = 0To props.Length - 1' 引数付きプロパティはどうしようもないので無視If props(i).GetIndexParameters().Length > 0ThenContinueForEnd If propValues(i) = props(i).GetValue(attr,Nothing)Next' フィールドの取得/設定Dim fieldValuesAsObject() =NewObject(fields.Length - 1){}For i = 0To fields.Length - 1 fieldValues(i) = fields(i).GetValue(attr)Next' カスタム属性の作成とセット paramBuilder.SetCustomAttribute( _New CustomAttributeBuilder(ctor, param, _ props, propValues, _ fields, fieldValues))NextEnd Sub' ParameterInfo配列から、それぞれのパラメータの型の配列を取得PrivateSharedFunction GetParameterTypes( _ByVal parametersAs ParameterInfo())As Type()Dim paramTypesAs Type() =New Type(parameters.Length - 1){}Dim iAsIntegerFor i = 0To paramTypes.Length - 1 paramTypes(i) = parameters(i).ParameterTypeNextReturn paramTypesEnd FunctionPrivateSharedFunction IsAccessibleFromOuterAssembly( _ByVal typeAs Type)AsBoolean' 名前空間直下でPublicならアクセス可能If type.IsPublicThenReturnTrueEnd If' ネストクラスの場合、ネスト内でPublicでなければ結局アクセス不能' 非ネストクラスの場合、Publicでない=internalなのでアクセス不能IfNot(type.IsNestedPublic)ThenReturnFalseEnd If' ネスト内でPublicなネストクラスの場合、自分を定義するクラスが' 外部アセンブリからアクセス可能かどうか確認するReturn IsAccessibleFromOuterAssembly(type.DeclaringType)End FunctionEnd ClassPublicClass ImportInformationPublicSub SetReplacePattern( _ByVal patternAsString,ByVal replacementAsString, _ByVal byRegexAsBoolean)If patternIsNothingOrElse pattern = ""ThenMe.m_pattern =NothingMe.m_replacement =NothingElseIf replacementIsNothingThenThrowNew ArgumentNullException( _ "replacement", _ "置換後の文字列をNothing にすることはできません。")End IfMe.m_pattern = patternMe.m_replacement = replacementMe.m_byRegex = byRegexEnd IfEnd SubPublicReadOnlyProperty Pattern()AsStringGetReturnMe.m_patternEnd GetEnd PropertyPublicReadOnly
ここ(hongliang.seesaa.net)で公開しているものについて、利用は自由に行って頂いて構いません。改変、再頒布もお好きになさって下さい。利用に対しこちらが何かを要求することはありません。
ただし、公開するものを使用、または参考したことによって何らかの損害等が生じた場合でも、私はいかなる責任も負いません。
あ、こんなのに使ったってコメントを頂ければ嬉しいです。
この広告は90日以上新しい記事の投稿がないブログに表示されております。