Programming Field

VBからCLR(.NET)を利用する その2[呼び出し編]

「VBからCLR(.NET)を利用する その1[準備編]」にて、VB6.0やVBAの環境からCLR(Common Language Runtime: 共通言語ランタイム)、あるいは.NETのライブラリを利用するための環境構築を行いました。ここでは、そのCLRを利用してアセンブリのロードや各種機能の利用を行う方法を紹介します。

※ 引き続き、VBのコードは64ビット対応のある「VBA 7.0」を前提に記述します。また、準備編で紹介したコードを前提として記述していますのでご注意ください。
※ ここからは基本的にCLR/.NETのリフレクションを使った処理になりますが、一部VB特有の解決方法をしている場合があります。

アセンブリのロード

CLR/.NETにおいて「アセンブリ」はWindowsにおけるEXEやDLLのようなものであり、複数のクラスやインターフェイス、および実行コードなどをまとめたバイナリーデータの単位となっています。CLRの上で何かしらの処理を実現したい場合は、1つ以上のアセンブリを読み込んでそれらに含まれる機能を利用していくことになります。

シンプルにアセンブリをロードするには、AppDomainクラスの「Load(String)」メソッドを使うことができますが、このメソッドはCOMオブジェクトとしては「Load_2」という名前で公開されています。このメソッドでSystem.Reflection.Assemblyクラス(COMオブジェクト上はmscorlib.Assembly)のインスタンスを得ることができます。

[VBA 7.0]

    ' ※ 「host」は「mscoree.CorRuntimeHost」のオブジェクト
    ' (「準備編」で取得方法を説明しています)

    ' 実行環境用ドメインの作成
    Dim app As IUnknown
    Call host.CreateDomain("VB2CLRTest", Nothing, app)
    Dim domain As mscorlib.AppDomain
    Set domain = app
    ' アセンブリのロード
    Dim asmCore As mscorlib.Assembly
    Set asmCore = domain.Load_2("mscorlib")

なお、初期状態では「mscorlib」のみがロードされており、最後の行のアセンブリ読み込みは改めて読み込みが行われるわけではありませんが、このコードは「mscorlib」のAssemblyのインスタンスを取得するという目的も兼ねています。

「mscorlib」以外のアセンブリをロードする場合は、このメソッドにアセンブリ名をStrong Name形式で指定します。

※ 後述するスタティックメソッドの呼び出し方法を用い、Assembly.LoadWithPartialName(String)メソッドを使うことで部分的な名前でアセンブリを読み込むことができますが、現在非推奨となっています。

[VBA 7.0]

    Dim asmXml As mscorlib.Assembly
    Set asmXml = domain.Load_2("System.Xml, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")

クラスインスタンスの作成

アセンブリがロード出来たら、そのアセンブリに属するクラスのインスタンスを作成することができます。コンストラクターのパラメーターを要求しないクラスであれば、AssemblyクラスのCreateInstanceメソッドを用いるのが容易です。(COMオブジェクト上は「CreateInstance」の名前になります。)

[VBA 7.0]

    ' 「System.Random」のインスタンスを作成する例
    ' (System.Random は mscorlib.Random として公開されています)
    Dim objRandom As mscorlib.Random
    Set objRandom = asmCore.CreateInstance("System.Random")

※ 1つ以上のパラメーターを受け取るクラスの場合は、System.Type(mscorlib.Type)のインスタンスを取得し、Reflectionの機能を用いてコンストラクターの情報(System.Reflection.ConstructorInfo)を取得、呼び出しを行います。

インスタンスメソッドの呼び出し

クラスのインスタンスを作成しただけでは、多くの場合ほぼ何も処理をしたことにならないため、クラスが持つメソッドの呼び出しを行います。

メソッドの呼び出しは、クラスのCOMオブジェクトとしての扱いによって主に以下の3パターンに分かれます。

  1. COM定義がある - System.Typeクラス(mscorlib.Type)、System.Reflection.Assemblyクラス(mscorlib.Assembly)など
  2. COM定義はないけれどもIDispatchのサポートがある - System.Randomクラス(mscorlib.Random)、System.Windows.Forms.Formクラスなど
  3. COM非対応 - System.Xml.XmlDocumentなど

COM定義がある場合

VBのプロジェクトに参照を追加し、「オブジェクト ブラウザー」で目的のクラスを表示した際に目的のメソッドが表示される場合、多くのケースでは通常のCOMクラスと同じようにメソッド呼び出しをすることができます。前述のAssemblyクラスのメソッド呼び出しがそれに該当しています。

[VBA 7.0]

    ' 「System.Random」の型情報を取得する例
    Dim typeRandom As mscorlib.Type
    Set typeRandom = asmCore.GetType_2("System.Random")

IDispatchのサポートがある場合

「オブジェクト ブラウザー」で目的のクラスを表示しても何もメソッドが表示されないケースがあります。「クラスインスタンスの作成」で作成したSystem.Randomクラスに対応する「mscorlib.Random」もその一例です。

System.Randomクラスもそうですが、COMには公開されているクラスの場合、IDispatchインターフェイスを継承していることがあります。この場合、(通常のVBオブジェクトと同じですが)入力候補に表示されないメソッドをそのまま記述することで呼び出しを行うことができます。

[VBA 7.0]

    ' 「System.Random.Next()」を呼び出す例
    Dim i As Integer
    For i = 1 To 10
        Debug.Print objRandom.Next()
    Next i

※ 「メソッドが見つからない」エラーとなる場合は、一旦 mscorlib.Object 型にSetしてからVBのObject型変数にSetし、その変数を使って呼び出しを行います。(nonextensible 属性が付いている場合に該当します。)
※ 引数の数が誤っている場合、基本的にはCOMのエラーが発生します。(状況によっては異常終了する場合がありますが条件は未確認です。)

また、VBのCallByName関数を用いることもできます。

[VBA 7.0]

    ' 「System.Random.Next(Int32, Int32)」を呼び出す例
    ' (COMからは「Next_2」という名前で呼び出し可能)
    Dim i As Integer
    For i = 1 To 10
        Debug.Print CallByName(objRandom, "Next_2", VbMethod, CLng(10), CLng(20))
    Next i

※ オーバーロードされたメソッドは上記の例のようにCOMからは「_2」「_3」などといった接尾辞を名前に付ける必要がありますが、どのオーバーロードがどの番号に対応しているかを確認するのは難しいため、次の「COM非対応の場合」の方法を用いる方が確実である場合があります。(基本的にはアセンブリ内部でのメソッド定義順になっていると思われますが未確認です。)

COMまたはVBで非対応の場合

以上の方法でインスタンスのメソッドを呼び出すことができない場合は、CLRのリフレクション機能を用いることで呼び出すことができます。具体的には、インスタンスに対応する型のSystem.Typeインスタンスを取得し、GetMethods(BindingFlags)メソッドで一覧を取得して目的のメソッドを探します。見つかった場合、そのMethodInfoクラス(MethodBaseクラス)のインスタンスにあるInvoke(object, object[])メソッド(COM上ではInvoke_3)を利用してインスタンスに対するメソッドを呼び出します。

※ オーバーロードされていないメソッドの場合はGetMethod(string)メソッドを利用することもできます。オーバーロードされたメソッドの場合名前だけでは判定できないため、GetMethodsメソッドでパラメーター数・型情報を元に検索します。
※ GetMethod(string, Type[])メソッド(COM上ではGetMethod_5)はVBから直接呼び出すことができません。ただし毎回GetMethodsで検索するのは煩雑であるため、「TypeクラスのGetMethod(string, Type[])メソッド」のMethodInfoをここで紹介する方法で事前に取得しておき、これを使って呼び出すという手はあります。
※ 引数なしのGetMethods()メソッドを利用しても構いませんが、BindingFlags引数があると多少絞り込みができる分探しやすくなります。

メソッドの検索を行うコードはそこそこのボリュームになるため、以下のような関数を定義してそれを利用することとします。

※ 「mscorlib.Object」経由で「VBのObject」に変換するコードが途中にありますが、この変換を行うのは、ほとんどのCLRクラスがCOM的には直接IUnknownまたはIDispatchを継承しているもののIDispatchの各メソッドを正しく実装していないことが多く、その一方で、mscorlib.Objectに変換した場合は仮想関数テーブルが変わって呼び出されるIDispatchメソッドが厳密に変わり、mscorlib.Objectが提供?しているIDispatchメソッドであれば呼び出すことができるためです。そして、それをVBのObject型変数に変換することでVB上から意図通りにIDispatchメソッドを利用することができるようになります。

[VBA 7.0]

' PickupMethodByParamType: 型「t」に含まれる名前「Name」のメソッドを検索します。
'   BindingFlags: 絞り込み条件
'   Types: メソッドの引数に対応する型を表す mscorlib.Type のインスタンス。
'          対応する引数の数だけ指定します。
'          (※ ParamArray の引数は Variant である必要があります)
'   戻り値: mscorlib.MethodInfo のインスタンス、または見つからない場合 Nothing
Public Function PickupMethodByParamType(ByVal t As mscorlib.Type, _
    ByVal Name As String, ByVal BindingFlags As mscorlib.BindingFlags, _
    ParamArray Types() As Variant) As mscorlib.MethodInfo
    Dim tlb As Long, tub As Long
    Dim actualTypeCount As Long
    Dim i As Long
    tlb = LBound(Types)
    tub = UBound(Types)
    actualTypeCount = 0
    ' 可変長引数で指定された Type インスタンスの数を計算
    For i = tlb To tub
        ' Types(i) が Type インスタンスかどうかを判定
        ' (13 は VT_UNKNOWN、mscorlib.Type は IUnknown ベースの型のため念のためチェック)
        If VarType(Types(i)) = 13 And TypeOf Types(i) Is mscorlib.Type Then
            actualTypeCount = actualTypeCount + 1
        End If
    Next i

    Dim mis() As mscorlib.MethodInfo, mi As mscorlib.MethodInfo
    Dim j As Long, k As Long, Matched As Boolean
    ' BindingFlags で対応するメソッドの一覧を取得(配列が返ります)
    mis = t.GetMethods(BindingFlags)
    ' それぞれのメソッドに対し、メソッド名と引数の型をチェック
    For i = LBound(mis) To UBound(mis)
        Set mi = mis(i)
        ' メソッド名を Option Compare ステートメント設定に応じた比較方法で判定
        ' (大文字・小文字の区別を制御したい場合は StrComp 関数を用います。)
        If mi.Name = Name Then
            Dim p() As mscorlib.ParameterInfo
            ' 引数情報を取得(配列が返ります)
            p = mi.GetParameters()
            ' 引数の数の一致性を確認
            If UBound(p) - LBound(p) + 1 = actualTypeCount Then
                Matched = True
                k = LBound(p)
                For j = tlb To tub
                    If VarType(Types(j)) = 13 And TypeOf Types(j) Is mscorlib.Type Then
                        Dim cobjPI As mscorlib.Object
                        Dim o As Object
                        Dim tP As mscorlib.Type
                        ' ParameterInfo クラスの ParameterType プロパティーを見るため、
                        ' mscorlib.Object 経由で VB の Object 型に変換
                        Set cobjPI = p(k)
                        Set o = cobjPI
                        Set tP = o.ParameterType
                        ' 型が一致しない引数が一つでもあった場合は不一致とする
                        If Not tP.Equals(Types(j)) Then
                            Matched = False
                            Exit For
                        End If
                        k = k + 1
                    End If
                Next j
                ' 一致した場合は見つかったものとしてループを抜ける
                If Matched Then Exit For
            End If
        End If
        Set mi = Nothing
    Next i
    Set PickupMethodByParamType = mi
End Function

実際の利用例は以下の通りです。

[VBA 7.0]

    ' 「System.Random.Next(Int32, Int32)」をリフレクション経由で呼び出す例
    Dim typeInt32 As mscorlib.Type
    Dim mi As mscorlib.MethodInfo
    ' 「Int32」型のTypeインスタンスを取得
    Set typeInt32 = asmCore.GetType_2("System.Int32")
    ' ・通常のインスタンスメソッドを探すので「Public」と「Instance」を指定
    ' ・引数は「Int32」が2つであるため、上記のインスタンスを2回指定する
    ' ・以下の「GetType_2」の呼び出しは、objRandom の場合は「objRandom.GetType()」で代用可能
    '   (インスタンスによっては <object>.GetType() が使えないケースもあります)
    Set mi = PickupMethodByParamType(asmCore.GetType_2("System.Random"), _
        "Next", BindingFlags_Public Or BindingFlags_Instance, _
        typeInt32, typeInt32)
    ' 「Invoke_3」は引数の型がVBでサポートされないので Object 経由で呼び出す
    Dim cobjMI As mscorlib.Object
    Dim objMI As Object
    Set cobjMI = mi
    Set objMI = cobjMI
    Dim i As Integer
    For i = 1 To 10
        ' Object 経由の場合「Invoke_3」の第2引数は VBA.Array 関数を使って指定する
        Debug.Print objMI.Invoke_3(objRandom, Array(CLng(10), CLng(20)))
    Next i

※ Invoke_3に渡す引数はVariantの配列になりますが、各要素は呼び出したいメソッドの型に応じたデータにしておく必要があります。なお、COM→.NET Frameworkの呼び出し時には.NET側の型に自動的に変換する機構が用意されています。詳しくはMicrosoft Docsの「Default Marshaling for Objects」の「Marshaling Variant to Object」(戻り値の変換は「Marshaling Object to Variant」)をご覧ください。

スタティックメソッドの呼び出し

スタティックメソッドの場合、VBでは厳密な意味でのスタティックメソッド呼び出し構文が存在せず、「<object>.<Method>()」の形式で呼び出すことができませんが、上記インスタンスメソッドの呼び出しにある「COMまたはVBで非対応の場合」と同様、リフレクションの機能を用いることで呼び出すことができます。

※ VBの「オブジェクト ブラウザー」における「グローバル」に表示されるプロパティーやメソッドは、COMの型定義で特別な定義がなされているクラスや「モジュール」のメンバーであり、クラスメンバーの場合は内部的にはインスタンスメソッドの呼び出し、「モジュール」メンバーの場合はDLL呼び出しになっています。

リフレクションの機能を用いる場合は以下のようになります。

[VBA 7.0]

' mscorlib.Object → VBのObjectに簡単に変換するためのラッパー関数
Private Function ToObject(ByVal obj As mscorlib.Object) As Object
    Set ToObject = obj
End Function

    ' 「System.Text.Encoding」の型情報を取得
    Dim tEncoding As mscorlib.Type
    Set tEncoding = asmCore.GetType_2("System.Text.Encoding")
    ' 「System.Text.Encoding.GetEncoding(string)」のメソッド情報を取得
    ' (GetEncoding はスタティックメソッド)
    Dim miGetEncoding As mscorlib.MethodInfo
    Set miGetEncoding = PickupMethodByParamType(tEncoding, "GetEncoding", _
        BindingFlags_Static Or BindingFlags_Public, _
        asmCore.GetType_2("System.String"))
    ' 「GetEncoding("UTF-8")」を呼び出し、戻り値を得る
    ' (スタティックメソッドの場合第1引数を Nothing とする)
    Dim cobjEncoding As mscorlib.Encoding
    Set cobjEncoding = ToObject(miGetEncoding).Invoke_3(Nothing, Array("UTF-8"))
    ' mscorlib.Encoding は IDispatch インスタンスのため
    ' System.Text.Encoding の持つプロパティーやメソッドをほぼそのまま使える
    Debug.Print cobjEncoding.CodePage, cobjEncoding.HeaderName, cobjEncoding.BodyName, cobjEncoding.WebName
    Dim by(0 To 2) As Byte
    by(0) = &HE3
    by(1) = &H81
    by(2) = &H82
    Debug.Print cobjEncoding.GetString(by)

まとめ

CLR/.NETのアセンブリをロードし、そこに定義されるクラスの機能を用いる場合は、COMのサポートが有効なアセンブリであればVBで一般的な手法とほぼ同じようにして利用することができますが、COMのサポートがない場合でもリフレクション機能を経由することでメソッド呼び出しを行うことができます。これにより、CLR/.NETのライブラリ側でのCOMサポートなしにそれらの機能をVBから利用できるようになります。

次回では、デリゲートの利用やその他発展的な利用方法(?)などを紹介したいと思います。