2006年03月27日

動的なDLLインポート

連載が終わった途端放置気味。意味もなく五七五で始めてみました。

コメント欄でちょっと話の出たホットキーの記事を書こうと思っていたのですが、調べれば調べるほど(本筋とは関係のないところで)難しい話になっていったので、後回し。いつになるかは未定義です。こだわりさえ捨てればいいんですが。

今回のネタはまさにネタですが、動的にアンマネージド 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 
 

posted by Hongliang at 16:00| Comment(8) | TrackBack(1) | .NET | このブログの読者になる | 更新情報をチェックする
この記事へのコメント
http://www.kiraku-rmt.jp/】より便利になったサービスを、是非一度お試しください。
Posted by アラド戦記 rmt at 2011年11月29日 02:40
どうぞよろしくお願いいたします。
Posted by ネクソンポイント RMT at 2011年11月29日 14:22
より皆様にご利用いただけるように、多彩な決済が可能になりました。
Posted by Ragnarok rmt at 2011年11月29日 15:45
今【FF14】を最安値で販売しています。
Posted by rmt at 2011年12月01日 11:16
スタッフと一同にご利用を心より期待しております!【http://www.rmt-mab.com/gamebuy/lineage2.htm
Posted by アイオン rmt at 2011年12月01日 17:55
こんにちはいつも【http://www.rmt-roan.jp/gamebuy/Dofus.htm】ご愛顧本当にありがとうございます!
Posted by RMT 桜RMT 桜 at 2011年12月01日 22:05
いつも【http://www.rmt-link.jp/games/CABAL.htm】をご愛顧いただき、ありがとうございます!
Posted by RMT 桜RMT 桜 at 2011年12月02日 01:10
通貨信用販売サービス【http://www.sale-rmt.jp/gamebuy/TERA.htm
Posted by RMT 桜RMT 桜 at 2011年12月02日 09:07
コメントを書く
お名前:

メールアドレス:

ホームページアドレス:

コメント:

認証コード: [必須入力]


※画像の中の文字を半角で入力してください。
この記事へのトラックバックURL
http://blog.seesaa.jp/tb/15565763

この記事へのトラックバック

ASP.NET サーバー アプリケーションは使用できません
Excerpt: 昔つくったASP.NETのサイトを技術検証のため 動かすことになった。 ところが・・ こんなエラーがでて、うまく動かない。 しかも、昔の設定なんか忘れてます・・ イベント..
Weblog: 正しい選択
Tracked: 2010-08-10 19:20

ここ(hongliang.seesaa.net)で公開しているものについて、利用は自由に行って頂いて構いません。改変、再頒布もお好きになさって下さい。利用に対しこちらが何かを要求することはありません。

ただし、公開するものを使用、または参考したことによって何らかの損害等が生じた場合でも、私はいかなる責任も負いません。

あ、こんなのに使ったってコメントを頂ければ嬉しいです。

×

この広告は1年以上新しい記事の投稿がないブログに表示されております。