Programming Field

VBからCLR(.NET)を利用する その3[発展編]

「VBからCLR(.NET)を利用する その2[呼び出し編]」にて、VB6.0やVBAの環境からCLR(Common Language Runtime: 共通言語ランタイム)におけるライブラリのクラスやメソッドの呼び出しを行えるようになりました。しかしこのままではVB→ライブラリ、というある意味一方向の利用になっており、機能によってはDelegate/イベントハンドラーを利用した逆方向の処理を提供・要求しているものもあります。ここではVB上でDelegateのインスタンスを作成し、Delegateやイベントハンドラーを提供・要求する機能を利用する方法を紹介します。

また、それ以外にもVB特有のIEnumerableの利用方法や、その他補足等を紹介します。

※ 引き続き、VBのコードは64ビット対応のある「VBA 7.0」を前提に記述します。また、準備編および呼び出し編で紹介したコードを一部前提として記述していますのでご注意ください。

Delegate利用の準備

CLR/.NETにおいてDelegate(デリゲート)は「メソッドのシグネチャー(Signature)を定義する型」であり、C/C++における関数ポインター型に近い概念です。Delegateおよびその派生型のデータは特定の(任意の)メソッドを呼び出すための情報を内部に保持しており、Delegateに対する呼び出し行為が行われるとそのメソッドが実行されます。Delegate型に設定できるデータは引数や戻り値が一致していればどのメソッド(スタティック/インスタンス問いません)でも構いません。例として、イベントハンドラーはDelegateを使って実現されています。

Delegate型のインスタンスの作成は、C#やVB.NETなどでは言語レベルでその処理をサポートしているためあまり気にする必要がありませんが、フレームワークの機能を用いて作成する場合はDelegate.CreateDelegateメソッドの各オーバーロードを利用することができます。また、相互運用としてMarshal.GetDelegateForFunctionPointerメソッドが提供されており、VBからDelegateを使用する場合はこれらを利用することができそうです。

しかし、いざ利用しようとなると以下の問題が出てきます。

  1. Marshal.GetDelegateForFunctionPointerをMethodInfo.Invoke経由で呼び出そうとしてもポインターアドレスを渡すことができない(※)
  2. Delegate.CreateDelegateにVB内のコードに対応するTypeとメソッド名(またはMethodInfo)を渡したくても分からない

※ COM→CLRへのデータ受け渡し時、(データはすべてVariant型となった後に)Variant型はCLR向けの適切なデータ型に変換されますが、「Default Marshaling for Objects」の「Marshaling Variant to Object」にある一覧を見るとSystem.IntPtr型に変換されるパスがなく、System.IntPtr型はコンストラクターの利用以外で数値型から変換することができないため、このような問題にあたります。

そこで、これらを解決する方法として、少々強引ですが「CLRの実行コードを動的に生成してそれをラッパーとして利用する」という方法を用います。

実行コードの動的生成

CLRでは、CodeDomProviderクラスの派生クラスを用いることで、ランタイム処理において動的にソースコードをコンパイルして実行コードを生成することができます。.NET FrameworkではC#用に「CSharpCodeProviderクラス」、VB.NET用に「VBCodeProviderクラス」などがそれぞれ提供されており、対応する言語に応じたソースコードから実行コードを得ることができます。

これらはCLR上で利用できるため、以下のようにすることでVBからも利用することができます

[VBA 7.0]

' domain の環境下で code に指定されたC#コードをコンパイルし、
' (code 内に定義されたクラスなどを含む)生成されたアセンブリを返す
' - RefAssemblyName には code 内で参照する追加アセンブリ名を指定
Public Function ExecuteCSharpCode(ByVal domain As mscorlib.AppDomain, _
    ByVal code As String, _
    ParamArray RefAssemblyName() As Variant) As mscorlib.Assembly
    ' CodeProviderを提供しているアセンブリ「System」を読み込む
    Dim asmSys As mscorlib.Assembly
    Set asmSys = domain.Load_2("System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")

    ' コンパイルオプションを指定するためのオブジェクトを作成
    Dim objParams As Object
    ' (プロパティーを直接利用するために mscorlib.Object 型経由でVBの Object 型に変換)
    Set objParams = ToObject(asmSys.CreateInstance("System.CodeDom.Compiler.CompilerParameters"))
    ' ファイルではなくメモリ上に実行コードを生成させるようにする
    objParams.GenerateInMemory = True

    ' RefAssemblyName で指定されたアセンブリ名を
    ' ReferencedAssemblies の配列に追加していく
    Dim v As Variant
    Dim oRefAsms As Object
    Set oRefAsms = ToObject(objParams.ReferencedAssemblies)
    For Each v In RefAssemblyName
        Call oRefAsms.Add(CStr(v))
    Next v

    ' C#のコードをコンパイルする CSharpCodeProvider のインスタンスを作成
    Dim objProvider As Object
    Set objProvider = ToObject(asmSys.CreateInstance("Microsoft.CSharp.CSharpCodeProvider"))

    ' ソースコードをコンパイル
    Dim vCodes(0) As String
    vCodes(0) = code
    Dim objResults As Object
    Set objResults = ToObject(objProvider.CompileAssemblyFromSource(objParams, vCodes))

    ' エラーの有無を確認
    Dim oErrors As Object
    Set oErrors = ToObject(objResults.Errors)
    If oErrors.HasErrors Then
        ' エラーがあった場合はそのまま関数を抜ける
        ' (デバッグ用にイミディエイトウィンドウにログを出力する)
        ' ※ 後述の ToEnumerable を利用すると For Each で列挙できます
        Dim oError As Object
        Dim c As Long, i As Long
        c = oErrors.Count - 1
        For i = 0 To c
            Set oError = ToObject(oErrors.Item(i))
            Debug.Print oError.ErrorText; " [Line="; oError.Line; "]"
        Next i
        Exit Function
    End If
    ' エラーが無かった場合は生成されたアセンブリを返す
    Set ExecuteCSharpCode = objResults.CompiledAssembly
End Function

※ ソースコードからのアセンブリの生成は前述の通りVB.NETなども利用できますが、ここではC#のコードをコンパイルすることとし、以降もC#を利用します。

この「ExecuteCSharpCode関数」は以下のように利用します。

[VBA 7.0]

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

    Dim asm As mscorlib.Assembly
    ' ソースコードをコンパイルしてアセンブリを生成
    ' (※ 「vbCrLf」やインデントは不要ですが、見やすくすることと、
    ' コンパイルエラー時に行を分かりやすくするために加えています)
    Set asm = ExecuteCSharpCode(domain, _
        "class TestClass" + vbCrLf + _
        "{" + vbCrLf + _
        "    public int Multiply(int x, int y)" + vbCrLf + _
        "    {" + vbCrLf + _
        "        return x * y;" + vbCrLf + _
        "    }" + vbCrLf + _
        "}")
    ' (エラー時は asm が Nothing になりますが、ソースコードが誤っていなければ
    '  基本的にエラーは起きないはずなのでチェックしていません)

    ' 生成されたアセンブリに含まれる「TestClass」のインスタンスを作成
    ' (クラスは既定では「ComVisible(true)」なので Object 型に変換して利用可能)
    Dim objTest As Object
    Set objTest = ToObject(asm.CreateInstance("TestClass"))
    ' インスタンスメソッド「TestClass.Multiply」を呼び出し
    Debug.Print objTest.Multiply(7, 8)

これにより、C#のソースコードを動的にコンパイルしてVB上で利用することが可能になったため、VBのみでは記述できなかった処理が記述できるようになります。これを利用し、Delegateを利用しようとして障害になっていた点の解決を行います。

Delegateインスタンスの作成

Delegateインスタンスの作成について、コールバックとして関数が呼び出されるようなDelegateを作成する方法と、クラスのメソッドが呼び出されるようなDelegateを作成する方法をそれぞれ紹介します。

コールバック関数が呼び出されるDelegateの作成

※ ここでは関数ポインターを扱っています。COMを経由した型チェック等が行われなくなるため、記述を間違えると異常終了する可能性がありますのでご注意ください。

前述の通り、関数ポインターからDelegateを作成するためにはMarshal.GetDelegateForFunctionPointerメソッドを利用します。VBからは直接利用できそうにありませんが、C#のコードではSystem.IntPtr型の変数を扱うことができるため、C#のコード経由で呼び出します。

C#のソースコードは以下のようになります。

[C#]

using System.Runtime.InteropServices;
class CreateDelegateWrapper
{
    // 受け取った引数を Marshal.GetDelegateForFunctionPointer に渡すラッパー
    public Delegate CreateDelegateFromFunctionPointer(Type delegateType, object objPtr)
    {
        IntPtr ip;
        // int や Int64 の場合は IntPtr のコンストラクターに渡して変換する
        if (objPtr is int)
        {
            ip = new IntPtr((int)objPtr);
        }
        else if (objPtr is Int64)
        {
            ip = new IntPtr((Int64)objPtr);
        }
        else
        {
            ip = (IntPtr)objPtr;
        }
        return Marshal.GetDelegateForFunctionPointer(ip, delegateType);
    }
}

これをアセンブリに変換し、オブジェクトを取得しておきます。

[VBA 7.0]

    ' コンパイルするソースコード
    Dim code As String
    code = "using System.Runtime.InteropServices;" + vbCrLf + _
        "class CreateDelegateWrapper" + vbCrLf + _
        "{" + vbCrLf + _
        "    public Delegate CreateDelegateFromFunctionPointer(Type delegateType, object objPtr)" + vbCrLf + _
        "    {" + vbCrLf + _
        "        IntPtr ip;" + vbCrLf + _
        "        if (objPtr is int)" + vbCrLf + _
        "        {" + vbCrLf + _
        "            ip = new IntPtr((int)objPtr);" + vbCrLf + _
        "        }" + vbCrLf + _
        "        else if (objPtr is Int64)" + vbCrLf + _
        "        {" + vbCrLf + _
        "            ip = new IntPtr((Int64)objPtr);" + vbCrLf + _
        "        }" + vbCrLf + _
        "        else" + vbCrLf + _
        "        {" + vbCrLf + _
        "            ip = (IntPtr)objPtr;" + vbCrLf + _
        "        }" + vbCrLf + _
        "        return Marshal.GetDelegateForFunctionPointer(ip, delegateType);" + vbCrLf + _
        "    }" + vbCrLf + _
        "}"
    ' アセンブリを生成しラッパーオブジェクトを取得
    Dim asmWrapper As mscorlib.Assembly
    Set asmWrapper = ExecuteCSharpCode(code)
    Dim objWrapper As Object
    Set objWrapper = ToObject(asmWrapper.CreateInstance("CreateDelegateWrapper"))

これを使ってDelegateを作成することができます。ここでは例として、System.Windows.Forms.Formクラスのイベントをハンドルする処理を紹介します。

[VBA 7.0]

    Dim asmCore As mscorlib.Assembly
    Set asmCore = domain.Load_2("mscorlib.dll")

    ' アセンブリ「System.Windows.Forms」を読み込み
    Dim asmForm As mscorlib.Assembly
    Set asmForm = domain.Load_2("System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")
    If Not asmForm Is Nothing Then
        ' 「System.Windows.Forms.Form」インスタンスを作成
        ' (「System.Windows.Forms.Form」はCOM対応あり)
        Dim objForm As Object
        Set objForm = ToObject(asmForm.CreateInstance("System.Windows.Forms.Form"))
        objForm.Text = "VB Form"
        objForm.Width = 800
        objForm.Height = 600

        ' MyClickHandler をコールバックとして利用して
        ' 「System.EventHandler」のDelegateインスタンスを作成
        Dim delgClick As mscorlib.Object
        Set delgClick = objWrapper.CreateDelegateFromFunctionPointer(asmCore.GetType_2("System.EventHandler"), AddressOf MyClickHandler)
        ' 「Click」イベントに追加
        Call objForm.add_Click(delgClick)
        ' フォームを表示
        Call objForm.Show
        ' フォームが閉じられるまでイベントループを回す
        Do
            ' (フォームが操作できるように DoEvents を置く)
            DoEvents
        Loop While objForm.Visible
        Call objForm.remove_Click(delgClick)
        ' フォームを破棄
        Call objForm.Dispose
        Set objForm = Nothing
    End If
.
.
.
' フォームがクリックされたときに実行される処理
' ※ sender はCLR上では object であり、COM向けにはVariantに変換されるのでVariant型として記述
'    (Args は System.EventArgs クラスなので mscorlib.Object とする)
Public Sub MyClickHandler(ByVal sender As Variant, ByVal Args As mscorlib.Object)
    ' ログを出力しつつ Form のインスタンスに対して何かしら操作を行ってみる
    Dim o As Object
    Set o = ToObject(sender)
    Debug.Print TypeName(o); " Clicked"
    o.Text = o.Text + "!"
End Sub

※ Delegateに変換する関数における引数の型は、COM相互運用による型変換を考慮した型に合わせる必要があります。例として「object」はソースコード内のコメントの通りVariant型とする必要があり、「mscorlib.Object」などとすると不正なアクセスによる異常終了のもととなってしまいます。

クラスのメソッドが呼び出されるDelegateの作成

コールバック関数を用いる方法は比較的シンプルですが、以下の問題があります。

  1. コールバックに追加情報を渡すことができない(どのDelegate経由で呼び出されたか判定できない)
  2. 型指定に気を遣う必要がある(誤ると予期しない動作になる)

これらを解決できるのが次に紹介する「クラスのメソッドを呼び出すDelegateを作成する」方法です。大まかな手順は以下の通りです。

  1. ラッパークラスを用意し、そのクラスでDelegateのハンドルを行う
  2. そのハンドルするメソッド内でラップ元のクラス(COMオブジェクト)のメソッドを呼び出す

こちらもC#などによるラッパークラスを利用することになりますが、Delegate.CreateDelegateに渡すメソッドとDelegate型の引数の数を一致させる必要があるため、以下のC#(擬似)コードのような回りくどい方法を用いて作成します。

コードを展開

[C#]

partial class CreateDelegateWrapper
{
    // target オブジェクトにあるメソッド「methodName」を呼び出す
    // Delegateのインスタンスを、delegateType の型を利用して作成
    public Delegate CreateDelegate(Type delegateType, object target, string methodName)
    {
        // 下記の VariableDelegateWrapper の処理を呼び出す
        return VariableDelegateWrapper.CreateDelegate(delegateType, target, methodName);
    }
}

class VariableDelegateWrapper
{
    private object _target;
    private Type _typeTarget;
    private string _methodName;

    // (実際にDelegateのインスタンスを作成するメソッド)
    public static Delegate CreateDelegate(Type delegateType, object target, string methodName)
    {
        // Delegateの型かどうかチェック
        if (!delegateType.IsSubclassOf(typeof(Delegate)))
        {
            throw new ArgumentException("Invalid 'delegateType'");
        }
        // Delegateの型にはInvokeメソッドがあるのでそれを取得
        var miInvoke = delegateType.GetMethod("Invoke");
        if (miInvoke == null)
        {
            throw new ArgumentException("Invalid 'delegateType'");
        }
        // Delegateの実際の引数を取得
        var c = miInvoke.GetParameters().Length;
        // (19個より多くの引数をサポートする場合は「MethodXX」などを追加してここを変更)
        if (c > 19)
        {
            throw new NotSupportedException("Parameter count of delegate is too large (maximum support = 19)");
        }
        // 呼び出し先クラスインスタンスの型情報を取得
        var targetType = target.GetType("Invoke");
        // ラッパーオブジェクトを作成
        var wrapper = new VariableDelegateWrapper(target, targetType, methodName);
        // ラッパーオブジェクト内のメソッドをDelegateの直接の呼び出し先にする
        var miWrapper = typeof(VariableDelegateWrapper).GetMethod(
            (miInvoke.ReturnType == typeof(void) ? "VMethod" : "Method") + c.ToString(),
            BindingFlags.NonPublic | BindingFlags.Instance
            );
        // Delegateを作成
        return Delegate.CreateDelegate(delegateType, wrapper, miWrapper);
    }

    // ラッパーオブジェクトのコンストラクター
    private VariableDelegateWrapper(object target, Type targetType, string methodName)
    {
        _target = target;
        _typeTarget = targetType;
        _methodName = methodName;
    }

    // 引数を配列として受け取るラッパーメソッド
    // (MethodXX / VMethodXX から呼び出す共通処理)
    private object Method(object[] args)
    {
        // ラップ元のオブジェクトに対して _methodName のメソッドを呼び出す
        // (COMオブジェクトの場合は IDispatch::Invoke 相当の処理が行われる)
        // ※ 型の変換はCLR→COM(→VB)の処理に委ねる
        return _typeTarget.InvokeMember(_methodName,
            BindingFlags.InvokeMethod | BindingFlags.OptionalParamBinding,
            null, _target, args);
    }
    // 引数の数が0~19個・戻り値ありのラッパーメソッド
    private object Method0() { return Method(new object[] { }); }
    private object Method1(object p1) { return Method(new object[] { p1 }); }
    private object Method2(object p1, object p2) { return Method(new object[] { p1, p2 }); }
    private object Method3(object p1, object p2, object p3) { return Method(new object[] { p1, p2, p3 }); }
    private object Method4(object p1, object p2, object p3, object p4) { return Method(new object[] { p1, p2, p3, p4 }); }
    private object Method5(object p1, object p2, object p3, object p4, object p5) { return Method(new object[] { p1, p2, p3, p4, p5 }); }
    private object Method6(object p1, object p2, object p3, object p4, object p5, object p6) { return Method(new object[] { p1, p2, p3, p4, p5, p6 }); }
    private object Method7(object p1, object p2, object p3, object p4, object p5, object p6, object p7) { return Method(new object[] { p1, p2, p3, p4, p5, p6, p7 }); }
    private object Method8(object p1, object p2, object p3, object p4, object p5, object p6, object p7, object p8) { return Method(new object[] { p1, p2, p3, p4, p5, p6, p7, p8 }); }
    private object Method9(object p1, object p2, object p3, object p4, object p5, object p6, object p7, object p8, object p9) { return Method(new object[] { p1, p2, p3, p4, p5, p6, p7, p8, p9 }); }
    private object Method10(object p1, object p2, object p3, object p4, object p5, object p6, object p7, object p8, object p9, object p10) { return Method(new object[] { p1, p2, p3, p4, p5, p6, p7, p8, p9, p10 }); }
    private object Method11(object p1, object p2, object p3, object p4, object p5, object p6, object p7, object p8, object p9, object p10, object p11) { return Method(new object[] { p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11 }); }
    private object Method12(object p1, object p2, object p3, object p4, object p5, object p6, object p7, object p8, object p9, object p10, object p11, object p12) { return Method(new object[] { p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12 }); }
    private object Method13(object p1, object p2, object p3, object p4, object p5, object p6, object p7, object p8, object p9, object p10, object p11, object p12, object p13) { return Method(new object[] { p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13 }); }
    private object Method14(object p1, object p2, object p3, object p4, object p5, object p6, object p7, object p8, object p9, object p10, object p11, object p12, object p13, object p14) { return Method(new object[] { p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14 }); }
    private object Method15(object p1, object p2, object p3, object p4, object p5, object p6, object p7, object p8, object p9, object p10, object p11, object p12, object p13, object p14, object p15) { return Method(new object[] { p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15 }); }
    private object Method16(object p1, object p2, object p3, object p4, object p5, object p6, object p7, object p8, object p9, object p10, object p11, object p12, object p13, object p14, object p15, object p16) { return Method(new object[] { p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15, p16 }); }
    private object Method17(object p1, object p2, object p3, object p4, object p5, object p6, object p7, object p8, object p9, object p10, object p11, object p12, object p13, object p14, object p15, object p16, object p17) { return Method(new object[] { p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15, p16, p17 }); }
    private object Method18(object p1, object p2, object p3, object p4, object p5, object p6, object p7, object p8, object p9, object p10, object p11, object p12, object p13, object p14, object p15, object p16, object p17, object p18) { return Method(new object[] { p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15, p16, p17, p18 }); }
    private object Method19(object p1, object p2, object p3, object p4, object p5, object p6, object p7, object p8, object p9, object p10, object p11, object p12, object p13, object p14, object p15, object p16, object p17, object p18, object p19) { return Method(new object[] { p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15, p16, p17, p18, p19 }); }
    // 引数の数が0~19個・戻り値なしのラッパーメソッド
    private void VMethod0() { Method(new object[] { }); }
    private void VMethod1(object p1) { Method(new object[] { p1 }); }
    private void VMethod2(object p1, object p2) { Method(new object[] { p1, p2 }); }
    private void VMethod3(object p1, object p2, object p3) { Method(new object[] { p1, p2, p3 }); }
    private void VMethod4(object p1, object p2, object p3, object p4) { Method(new object[] { p1, p2, p3, p4 }); }
    private void VMethod5(object p1, object p2, object p3, object p4, object p5) { Method(new object[] { p1, p2, p3, p4, p5 }); }
    private void VMethod6(object p1, object p2, object p3, object p4, object p5, object p6) { Method(new object[] { p1, p2, p3, p4, p5, p6 }); }
    private void VMethod7(object p1, object p2, object p3, object p4, object p5, object p6, object p7) { Method(new object[] { p1, p2, p3, p4, p5, p6, p7 }); }
    private void VMethod8(object p1, object p2, object p3, object p4, object p5, object p6, object p7, object p8) { Method(new object[] { p1, p2, p3, p4, p5, p6, p7, p8 }); }
    private void VMethod9(object p1, object p2, object p3, object p4, object p5, object p6, object p7, object p8, object p9) { Method(new object[] { p1, p2, p3, p4, p5, p6, p7, p8, p9 }); }
    private void VMethod10(object p1, object p2, object p3, object p4, object p5, object p6, object p7, object p8, object p9, object p10) { Method(new object[] { p1, p2, p3, p4, p5, p6, p7, p8, p9, p10 }); }
    private void VMethod11(object p1, object p2, object p3, object p4, object p5, object p6, object p7, object p8, object p9, object p10, object p11) { Method(new object[] { p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11 }); }
    private void VMethod12(object p1, object p2, object p3, object p4, object p5, object p6, object p7, object p8, object p9, object p10, object p11, object p12) { Method(new object[] { p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12 }); }
    private void VMethod13(object p1, object p2, object p3, object p4, object p5, object p6, object p7, object p8, object p9, object p10, object p11, object p12, object p13) { Method(new object[] { p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13 }); }
    private void VMethod14(object p1, object p2, object p3, object p4, object p5, object p6, object p7, object p8, object p9, object p10, object p11, object p12, object p13, object p14) { Method(new object[] { p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14 }); }
    private void VMethod15(object p1, object p2, object p3, object p4, object p5, object p6, object p7, object p8, object p9, object p10, object p11, object p12, object p13, object p14, object p15) { Method(new object[] { p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15 }); }
    private void VMethod16(object p1, object p2, object p3, object p4, object p5, object p6, object p7, object p8, object p9, object p10, object p11, object p12, object p13, object p14, object p15, object p16) { Method(new object[] { p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15, p16 }); }
    private void VMethod17(object p1, object p2, object p3, object p4, object p5, object p6, object p7, object p8, object p9, object p10, object p11, object p12, object p13, object p14, object p15, object p16, object p17) { Method(new object[] { p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15, p16, p17 }); }
    private void VMethod18(object p1, object p2, object p3, object p4, object p5, object p6, object p7, object p8, object p9, object p10, object p11, object p12, object p13, object p14, object p15, object p16, object p17, object p18) { Method(new object[] { p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15, p16, p17, p18 }); }
    private void VMethod19(object p1, object p2, object p3, object p4, object p5, object p6, object p7, object p8, object p9, object p10, object p11, object p12, object p13, object p14, object p15, object p16, object p17, object p18, object p19) { Method(new object[] { p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15, p16, p17, p18, p19 }); }
}

(※ 前述のVBによる「CreateDelegateWrapper」生成コードを以下のように書き換えます。)

コードを展開

[VBA 7.0]

    ' コンパイルするソースコード
    Dim code As String
    ' ※ 長すぎて「_」の最大連続行数をオーバーするため、
    '    コードを分割しています。
    code = "using System.Runtime.InteropServices;" + vbCrLf
    ' class CreateDelegateWrapper
    code = code + "class CreateDelegateWrapper" + vbCrLf + _
        "{" + vbCrLf
    ' 受け取った引数を Marshal.GetDelegateForFunctionPointer に渡すラッパー
    ' Function CreateDelegateFromFunctionPointer(delegateType As mscorlib.Type, objPtr As Variant) As mscorlib.Delegate
    code = code + "    public Delegate CreateDelegateFromFunctionPointer(Type delegateType, object objPtr)" + vbCrLf + _
        "    {" + vbCrLf + _
        "        IntPtr ip;" + vbCrLf + _
        "        // int や Int64 の場合は IntPtr のコンストラクターに渡して変換する" + vbCrLf + _
        "        if (objPtr is int)" + vbCrLf + _
        "        {" + vbCrLf + _
        "            ip = new IntPtr((int)objPtr);" + vbCrLf + _
        "        }" + vbCrLf + _
        "        else if (objPtr is Int64)" + vbCrLf + _
        "        {" + vbCrLf + _
        "            ip = new IntPtr((Int64)objPtr);" + vbCrLf + _
        "        }" + vbCrLf + _
        "        else" + vbCrLf + _
        "        {" + vbCrLf + _
        "            ip = (IntPtr)objPtr;" + vbCrLf + _
        "        }" + vbCrLf + _
        "        return Marshal.GetDelegateForFunctionPointer(ip, delegateType);" + vbCrLf + _
        "    }" + vbCrLf
    ' target オブジェクトにあるメソッド「methodName」を呼び出す
    ' Delegateのインスタンスを、delegateType の型を利用して作成
    ' Function CreateDelegate(delegateType As mscorlib.Type, target As Variant, methodName As String) As mscorlib.Delegate
    code = code + "    public Delegate CreateDelegate(Type delegateType, object target, string methodName)" + vbCrLf + _
        "    {" + vbCrLf + _
        "        // 下記の VariableDelegateWrapper の処理を呼び出す" + vbCrLf + _
        "        return VariableDelegateWrapper.CreateDelegate(delegateType, target, methodName);" + vbCrLf + _
        "    }" + vbCrLf + _
        "}" + vbCrLf
    ' end of class CreateDelegateWrapper
    ' class VariableDelegateWrapper
    code = code + "class VariableDelegateWrapper" + vbCrLf + _
        "{" + vbCrLf + _
        "    private object _target;" + vbCrLf + _
        "    private Type _typeTarget;" + vbCrLf + _
        "    private string _methodName;" + vbCrLf
    ' (実際にDelegateのインスタンスを作成するメソッド)
    code = code + "    public static Delegate CreateDelegate(Type delegateType, object target, string methodName)" + vbCrLf + _
        "    {" + vbCrLf + _
        "        // Delegateの型かどうかチェック" + vbCrLf + _
        "        if (!delegateType.IsSubclassOf(typeof(Delegate)))" + vbCrLf + _
        "        {" + vbCrLf + _
        "            throw new ArgumentException(""Invalid 'delegateType'"");" + vbCrLf + _
        "        }" + vbCrLf + _
        "        // Delegateの型にはInvokeメソッドがあるのでそれを取得" + vbCrLf + _
        "        var miInvoke = delegateType.GetMethod(""Invoke"");" + vbCrLf + _
        "        if (miInvoke == null)" + vbCrLf + _
        "        {" + vbCrLf + _
        "            throw new ArgumentException(""Invalid 'delegateType'"");" + vbCrLf + _
        "        }" + vbCrLf
    code = code + _
        "        // Delegateの実際の引数を取得" + vbCrLf + _
        "        var c = miInvoke.GetParameters().Length;" + vbCrLf + _
        "        // (19個より多くの引数をサポートする場合は「MethodXX」などを追加してここを変更)" + vbCrLf + _
        "        if (c > 19)" + vbCrLf + _
        "        {" + vbCrLf + _
        "            throw new NotSupportedException(""Parameter count of delegate is too large (maximum support = 19)"");" + vbCrLf + _
        "        }" + vbCrLf + _
        "        // 呼び出し先クラスインスタンスの型情報を取得" + vbCrLf + _
        "        var targetType = target.GetType();" + vbCrLf + _
        "        // ラッパーオブジェクトを作成" + vbCrLf + _
        "        var wrapper = new VariableDelegateWrapper(target, targetType, methodName);" + vbCrLf + _
        "        // ラッパーオブジェクト内のメソッドをDelegateの直接の呼び出し先にする" + vbCrLf + _
        "        var miWrapper = typeof(VariableDelegateWrapper).GetMethod(" + vbCrLf + _
        "            (miInvoke.ReturnType == typeof(void) ? ""VMethod"" : ""Method"") + c.ToString()," + vbCrLf + _
        "            BindingFlags.NonPublic | BindingFlags.Instance" + vbCrLf + _
        "            );" + vbCrLf + _
        "        // Delegateを作成" + vbCrLf + _
        "        return Delegate.CreateDelegate(delegateType, wrapper, miWrapper);" + vbCrLf + _
        "    }" + vbCrLf
    ' ラッパーオブジェクトのコンストラクター
    code = code + "    private VariableDelegateWrapper(object target, Type targetType, string methodName)" + vbCrLf + _
        "    {" + vbCrLf + _
        "        _target = target;" + vbCrLf + _
        "        _typeTarget = targetType;" + vbCrLf + _
        "        _methodName = methodName;" + vbCrLf + _
        "    }" + vbCrLf + _
        "" + vbCrLf
    ' 引数を配列として受け取るラッパーメソッド
    ' (MethodXX / VMethodXX から呼び出す共通処理)
    code = code + "    private object Method(object[] args)" + vbCrLf + _
        "    {" + vbCrLf + _
        "        // ラップ元のオブジェクトに対して _methodName のメソッドを呼び出す" + vbCrLf + _
        "        // (COMオブジェクトの場合は IDispatch::Invoke 相当の処理が行われる)" + vbCrLf + _
        "        // ※ 型の変換はCLR→COM(→VB)の処理に委ねる" + vbCrLf + _
        "        return _typeTarget.InvokeMember(_methodName," + vbCrLf + _
        "            BindingFlags.InvokeMethod | BindingFlags.OptionalParamBinding," + vbCrLf + _
        "            null, _target, args);" + vbCrLf + _
        "    }" + vbCrLf
    ' 引数の数が0~19個・戻り値ありのラッパーメソッド
    code = code + "    private object Method0() { return Method(new object[] { }); }" + vbCrLf + _
        "    private object Method1(object p1) { return Method(new object[] { p1 }); }" + vbCrLf + _
        "    private object Method2(object p1, object p2) { return Method(new object[] { p1, p2 }); }" + vbCrLf + _
        "    private object Method3(object p1, object p2, object p3) { return Method(new object[] { p1, p2, p3 }); }" + vbCrLf + _
        "    private object Method4(object p1, object p2, object p3, object p4) { return Method(new object[] { p1, p2, p3, p4 }); }" + vbCrLf + _
        "    private object Method5(object p1, object p2, object p3, object p4, object p5) { return Method(new object[] { p1, p2, p3, p4, p5 }); }" + vbCrLf + _
        "    private object Method6(object p1, object p2, object p3, object p4, object p5, object p6) { return Method(new object[] { p1, p2, p3, p4, p5, p6 }); }" + vbCrLf + _
        "    private object Method7(object p1, object p2, object p3, object p4, object p5, object p6, object p7) { return Method(new object[] { p1, p2, p3, p4, p5, p6, p7 }); }" + vbCrLf + _
        "    private object Method8(object p1, object p2, object p3, object p4, object p5, object p6, object p7, object p8) { return Method(new object[] { p1, p2, p3, p4, p5, p6, p7, p8 }); }" + vbCrLf + _
        "    private object Method9(object p1, object p2, object p3, object p4, object p5, object p6, object p7, object p8, object p9) { return Method(new object[] { p1, p2, p3, p4, p5, p6, p7, p8, p9 }); }" + vbCrLf + _
        "    private object Method10(object p1, object p2, object p3, object p4, object p5, object p6, object p7, object p8, object p9, object p10) { return Method(new object[] { p1, p2, p3, p4, p5, p6, p7, p8, p9, p10 }); }" + vbCrLf + _
        "    private object Method11(object p1, object p2, object p3, object p4, object p5, object p6, object p7, object p8, object p9, object p10, object p11) { return Method(new object[] { p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11 }); }" + vbCrLf + _
        "    private object Method12(object p1, object p2, object p3, object p4, object p5, object p6, object p7, object p8, object p9, object p10, object p11, object p12) { return Method(new object[] { p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12 }); }" + vbCrLf + _
        "    private object Method13(object p1, object p2, object p3, object p4, object p5, object p6, object p7, object p8, object p9, object p10, object p11, object p12, object p13) { return Method(new object[] { p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13 }); }" + vbCrLf + _
        "    private object Method14(object p1, object p2, object p3, object p4, object p5, object p6, object p7, object p8, object p9, object p10, object p11, object p12, object p13, object p14) { return Method(new object[] { p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14 }); }" + vbCrLf + _
        "    private object Method15(object p1, object p2, object p3, object p4, object p5, object p6, object p7, object p8, object p9, object p10, object p11, object p12, object p13, object p14, object p15) { return Method(new object[] { p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15 }); }" + vbCrLf + _
        "    private object Method16(object p1, object p2, object p3, object p4, object p5, object p6, object p7, object p8, object p9, object p10, object p11, object p12, object p13, object p14, object p15, object p16) { return Method(new object[] { p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15, p16 }); }" + vbCrLf + _
        "    private object Method17(object p1, object p2, object p3, object p4, object p5, object p6, object p7, object p8, object p9, object p10, object p11, object p12, object p13, object p14, object p15, object p16, object p17) { return Method(new object[] { p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15, p16, p17 }); }" + vbCrLf + _
        "    private object Method18(object p1, object p2, object p3, object p4, object p5, object p6, object p7, object p8, object p9, object p10, object p11, object p12, object p13, object p14, object p15, object p16, object p17, object p18) { return Method(new object[] { p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15, p16, p17, p18 }); }" + vbCrLf + _
        "    private object Method19(object p1, object p2, object p3, object p4, object p5, object p6, object p7, object p8, object p9, object p10, object p11, object p12, object p13, object p14, object p15, object p16, object p17, object p18, object p19) { return Method(new object[] { p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15, p16, p17, p18, p19 }); }" + vbCrLf
    ' 引数の数が0~19個・戻り値なしのラッパーメソッド
    code = code + "    private void VMethod0() { Method(new object[] { }); }" + vbCrLf + _
        "    private void VMethod1(object p1) { Method(new object[] { p1 }); }" + vbCrLf + _
        "    private void VMethod2(object p1, object p2) { Method(new object[] { p1, p2 }); }" + vbCrLf + _
        "    private void VMethod3(object p1, object p2, object p3) { Method(new object[] { p1, p2, p3 }); }" + vbCrLf + _
        "    private void VMethod4(object p1, object p2, object p3, object p4) { Method(new object[] { p1, p2, p3, p4 }); }" + vbCrLf + _
        "    private void VMethod5(object p1, object p2, object p3, object p4, object p5) { Method(new object[] { p1, p2, p3, p4, p5 }); }" + vbCrLf + _
        "    private void VMethod6(object p1, object p2, object p3, object p4, object p5, object p6) { Method(new object[] { p1, p2, p3, p4, p5, p6 }); }" + vbCrLf + _
        "    private void VMethod7(object p1, object p2, object p3, object p4, object p5, object p6, object p7) { Method(new object[] { p1, p2, p3, p4, p5, p6, p7 }); }" + vbCrLf + _
        "    private void VMethod8(object p1, object p2, object p3, object p4, object p5, object p6, object p7, object p8) { Method(new object[] { p1, p2, p3, p4, p5, p6, p7, p8 }); }" + vbCrLf + _
        "    private void VMethod9(object p1, object p2, object p3, object p4, object p5, object p6, object p7, object p8, object p9) { Method(new object[] { p1, p2, p3, p4, p5, p6, p7, p8, p9 }); }" + vbCrLf + _
        "    private void VMethod10(object p1, object p2, object p3, object p4, object p5, object p6, object p7, object p8, object p9, object p10) { Method(new object[] { p1, p2, p3, p4, p5, p6, p7, p8, p9, p10 }); }" + vbCrLf + _
        "    private void VMethod11(object p1, object p2, object p3, object p4, object p5, object p6, object p7, object p8, object p9, object p10, object p11) { Method(new object[] { p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11 }); }" + vbCrLf + _
        "    private void VMethod12(object p1, object p2, object p3, object p4, object p5, object p6, object p7, object p8, object p9, object p10, object p11, object p12) { Method(new object[] { p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12 }); }" + vbCrLf + _
        "    private void VMethod13(object p1, object p2, object p3, object p4, object p5, object p6, object p7, object p8, object p9, object p10, object p11, object p12, object p13) { Method(new object[] { p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13 }); }" + vbCrLf + _
        "    private void VMethod14(object p1, object p2, object p3, object p4, object p5, object p6, object p7, object p8, object p9, object p10, object p11, object p12, object p13, object p14) { Method(new object[] { p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14 }); }" + vbCrLf + _
        "    private void VMethod15(object p1, object p2, object p3, object p4, object p5, object p6, object p7, object p8, object p9, object p10, object p11, object p12, object p13, object p14, object p15) { Method(new object[] { p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15 }); }" + vbCrLf + _
        "    private void VMethod16(object p1, object p2, object p3, object p4, object p5, object p6, object p7, object p8, object p9, object p10, object p11, object p12, object p13, object p14, object p15, object p16) { Method(new object[] { p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15, p16 }); }" + vbCrLf + _
        "    private void VMethod17(object p1, object p2, object p3, object p4, object p5, object p6, object p7, object p8, object p9, object p10, object p11, object p12, object p13, object p14, object p15, object p16, object p17) { Method(new object[] { p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15, p16, p17 }); }" + vbCrLf + _
        "    private void VMethod18(object p1, object p2, object p3, object p4, object p5, object p6, object p7, object p8, object p9, object p10, object p11, object p12, object p13, object p14, object p15, object p16, object p17, object p18) { Method(new object[] { p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15, p16, p17, p18 }); }" + vbCrLf + _
        "    private void VMethod19(object p1, object p2, object p3, object p4, object p5, object p6, object p7, object p8, object p9, object p10, object p11, object p12, object p13, object p14, object p15, object p16, object p17, object p18, object p19) { Method(new object[] { p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15, p16, p17, p18, p19 }); }" + vbCrLf
    code = code + "}" + vbCrLf
    ' end of class VariableDelegateWrapper

    ' アセンブリを生成しラッパーオブジェクトを取得
    Dim asmWrapper As mscorlib.Assembly
    Set asmWrapper = ExecuteCSharpCode(code)
    (以下略)

なお、このコードでは「CreateDelegateWrapper.CreateDelegate」の第2引数にVBのクラスオブジェクトをそのまま渡すことができます。InvokeMemberメソッドがIDispatch::GetIDsOfNames→IDispatch::Invoke相当の処理を行うため、(IDispatchに標準で対応する)VBのクラスオブジェクト内にあるメソッドをCLRコードから呼び出すことが可能になっています。

※ ここでは渡されたオブジェクトにメソッドが存在するかどうかのチェックを行っていません。InvokeMemberメソッドで名前を指定してメソッドを呼び出すことは可能であるものの、GetMethodメソッドなどを使った存在確認を行うことができないため、チェックをしたい場合は、CLRから直接IDispatch::GetIDsOfNamesを呼び出すか、VB側で(GetIDsOfNamesを呼び出して)存在チェックを行うかする必要があります。

この「CreateDelegateWrapper.CreateDelegate」を使ってイベントのハンドルを行う例は以下の通りです。

[ファイル: HandlerClass.cls(一部)、クラス名: HandlerClass]

[VBA 7.0]

' クラスインスタンス用のデータ
Public MyData As String

' イベントハンドル用のメソッド
Public Sub MyClickHandler(ByVal sender As mscorlib.Object, ByVal Args As mscorlib.Object)
    ' ログを出力しつつ Form のインスタンスに対して何かしら操作を行ってみる
    Dim o As Object
    Set o = sender
    Debug.Print TypeName(o); " Clicked"
    o.Text = o.Text + MyData + "!"
End Sub

[ファイル: TestModule.bas(一部)]

[VBA 7.0]

    Dim asmCore As mscorlib.Assembly
    Set asmCore = domain.Load_2("mscorlib.dll")

    ' アセンブリ「System.Windows.Forms」を読み込み
    Dim asmForm As mscorlib.Assembly
    Set asmForm = domain.Load_2("System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")
    If Not asmForm Is Nothing Then
        ' 「System.Windows.Forms.Form」インスタンスを作成
        ' (「System.Windows.Forms.Form」はCOM対応あり)
        Dim objForm As Object
        Set objForm = ToObject(asmForm.CreateInstance("System.Windows.Forms.Form"))
        objForm.Text = "VB Form"
        objForm.Width = 800
        objForm.Height = 600

        ' HandlerClass のインスタンスを作成し、インスタンス別の値を設定
        Dim objHandler As HandlerClass
        Set objHandler = New HandlerClass
        objHandler.MyData = "My Handler Data"
        ' objHandler.MyClickHandler をコールバックとして利用して
        ' 「System.EventHandler」のDelegateインスタンスを作成
        Dim delgClick As mscorlib.Object
        Set delgClick = objWrapper.CreateDelegate(asmCore.GetType_2("System.EventHandler"), _
            objHandler, "MyClickHandler")
        ' 「Click」イベントに追加
        Call objForm.add_Click(delgClick)
        ' フォームを表示
        Call objForm.Show
        ' フォームが閉じられるまでイベントループを回す
        Do
            ' (フォームが操作できるように DoEvents を置く)
            DoEvents
        Loop While objForm.Visible
        Call objForm.remove_Click(delgClick)
        ' フォームを破棄
        Call objForm.Dispose
        Set objForm = Nothing
    End If

※ コールバック関数による方法と異なり、HandlerClass.MyClickHandler の第1引数の型は Variant である必要はありません。
※ HandlerClassの「Instancing」設定は「Private」のままでも利用できます。また、上記実行コードがクラス内であれば、そのクラス自身にメソッドを定義して利用することもできます。

その他

動的生成コードを利用したシンプルなメソッド呼び出しなど(InvokeMember, GetType)

Delegateのインスタンスを作成するために動的にコードを生成する手法を紹介しましたが、この動的生成を利用すると、「呼び出し編」で記述していたメソッド呼び出しがよりシンプルに行うことができます。

TypeクラスにはInvokeMemberメソッドがありますが、このメソッドはオーバーロードされたメソッドに対して引数を元に自動的に呼び出し先の判定を行ってくれるため、多くの場合で簡単にメソッド呼び出しを記述できるようになります。

なお、メソッド呼び出しとは直接関係はありませんが、mscorlib.Object のGetTypeメソッドはCOM非対応のアセンブリ/クラスのインスタンスに対しては利用することができないため、これもラップしておくと型情報を利用しやすくなります。

C#で記述すると以下のようになります。

[C#]

    // Type.InvokeMember をほぼそのままラップするメソッド
    // (スタティックメソッドに対しても利用可能)
    public object MyInvokeMember(Type targetType, string methodName, BindingFlags bindingFlags, object obj, object[] methodArgs)
    {
        return targetType.InvokeMember(methodName, bindingFlags, null, obj, methodArgs);
    }
    // obj に対してメソッド「methodName」を呼び出すメソッド
    // (MyInvokeMember の簡易版)
    public object MyInvokeInstanceMember(object obj, string methodName, object[] methodArgs)
    {
        return obj.GetType().InvokeMember(methodName,
            BindingFlags.Public | BindingFlags.Instance | BindingFlags.InvokeMethod,
            null, obj, methodArgs);
    }
    // obj の「GetType」を呼び出すメソッド(COM非対応のオブジェクトに対しても利用可能)
    public Type MyGetType(object obj) => obj.GetType();
    

例として、System.Random.Next(Int32, Int32)は「MyInvokeInstanceMember」を経由して以下のように呼び出すことができます。

※ 上記コードのVBによる記述は省略します。これらのメソッドを objWrapper オブジェクトで利用できるものと仮定します。

[VBA 7.0]

    ' 「System.Random」のインスタンスを作成する例
    ' (System.Random は mscorlib.Random として公開されています)
    Dim objRandom As mscorlib.Random
    Set objRandom = asmCore.CreateInstance("System.Random")
    ' 「System.Random.Next(Int32, Int32)」を呼び出す例
    Dim i As Integer
    For i = 1 To 10
        Debug.Print objWrapper.MyInvokeInstanceMember(objRandom, "Next", Array(CLng(10), CLng(20)))
    Next i

※ 「object[]」の引数に対しては Variant 型の配列を指定します。VBでは Variant 型の配列データを作るのにArray関数を利用することができます。

Enum値の利用

CLRにおけるEnum値はSystem.Enumクラスから派生した値型のクラスを型とするデータとして扱われます。文字列からEnum値に変換することが可能であり、その場合はEnumクラスのスタティックメソッドであるEnum.Parseメソッドを利用することができますが、メソッド呼び出し時は基本的にはEnum系の引数に数値をそのまま渡すことで対応するEnum値に変換されます。

ただし、前述したType.InvokeMemberメソッドを利用したメソッド呼び出しの場合は、適したメソッドの検索に引数として渡されたデータの型が使用されますが、このときEnum系の引数を想定して数値型のデータを渡すと、Enum系の型と数値型とは基本的には一致しない型の扱いとなるため、メソッドの検索に失敗しエラーとなります。Enum系の引数を取るメソッドの場合は「呼び出し編」でも行っているような、MethodInfo経由でのメソッド呼び出しを行う必要があります。

※ Enum.ParseメソッドやEnum.ToObjectメソッドの戻り値はCLR上ではEnum系のデータとなりますが、COMに変換される際に数値型に置き換わります。そのこともあり、COMからCLRに「Enum系の型を持つデータ」を渡すことは出来ません。

IEnumerableの利用

System.Collection.IEnumerableインターフェイスはCLRにおいていわゆる「foreach」処理を可能にするために多くのコレクション系クラスが継承しているインターフェイスです。

C#やVB.NETなどでは対応する構文により、IEnumerableを継承したクラスのオブジェクトに対して簡単にリピート処理を行うことができます。一方、VB6.0(VBA含む)では、以下の条件がそろっているとそのオブジェクトに対して「For Each」ステートメントを利用することができます。

  1. 対象オブジェクトに「DispId」が「-4」(DISPID_NEWENUM)であるプロパティーまたはメソッドが存在する
  2. そのプロパティー(Get)またはメソッドが「IEnumVARIANT」インターフェイスを返す、またはそれをサポートする

幸い(?)、CLRの「IEnumerableインターフェイス」は、COMから見た場合(mscorlib.IEnumerable)にはGetEnumeratorメソッドが「DispId = -4」の「戻り値が IEnumVARIANT」であるメソッドとして扱われ、上記の2条件を満たすことになります。従って、対象のデータを明示的に mscorlib.IEnumerable の型に変換することでVBでもFor Eachステートメントを利用することができます。

[VBA 7.0]

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

    ' 「System.Text.RegularExpressions」があるアセンブリ「System」をロード
    Dim asmSys As mscorlib.Assembly
    Set asmSys = domain.Load_2("System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")
    ' 「System.Text.RegularExpressions.Regex」のオブジェクトを作成
    ' (引数には文字列「([0-9])+」を指定)
    ' ※ CreateInstance_3 はそのままでは呼び出せないので一旦 Object に変換
    Dim cobjRegex As mscorlib.Object
    Set cobjRegex = ToObject(asmSys).CreateInstance_3("System.Text.RegularExpressions.Regex", _
        False, BindingFlags_Public Or BindingFlags_Instance Or BindingFlags_CreateInstance, Nothing, _
        Array("([0-9])+"), Nothing, Array())
    ' 「Matches」メソッドを文字列「10 20 50 1234 98765」の引数で呼び出す
    Dim cobjColl As mscorlib.Object
    Set cobjColl = host.CLRInvokeMethod(cobjRegex, "Matches", "10 20 50 1234 98765")
    ' 戻り値は MatchCollection で IEnumerable なので
    ' IEnumerable に明示的に変換した上でFor Each文でリピートする
    ' ※ 列挙データは Variant で受ける必要があります
    Dim vMatch As Variant
    For Each vMatch In ToEnumerable(cobjColl)
        Dim cobjMatch As mscorlib.Object
        Set cobjMatch = vMatch
        ' cobjMatch のプロパティー「Value」の値を取得
        ' Object 型に変換して直接利用するとオートメーションエラーとなるため、
        ' 前述の「MyInvokeMember」メソッド経由で取得を行う
        ' (BindingFlags に GetProperty を使うことで呼び出し可能)
        Dim tMatch As mscorlib.Type
        Set tMatch = objWrapper.MyGetType(cobjMatch)
        Debug.Print "Matches: "; objWrapper.MyInvokeMember(tMatch, _
            "Value", BindingFlags_GetProperty Or BindingFlags_Instance Or BindingFlags_Public, _
            cobjMatch, Array())
    Next vMatch

※ プロパティーを直接利用できない場合、Type.GetPropertyメソッド・PropertyInfoオブジェクト経由で利用することもできますが、上記では簡単のため MyInvokeMember を利用しています。

.NET(旧.NET Core)の利用?

.NET(旧.NET Core)はオープンソースのCLR実装であり、Windowsに限らないクロスプラットフォームで利用可能なものとして作られています。Windowsにおいては、.NET Coreは.NET Frameworkとは異なるものであり、ネイティブアプリケーションからのCLRの初期化方法や利用方法が大きく異なります。

.NETのCLR自体は初期化できるものの、現時点で(マネージコードなしに)ネイティブのみで利用できることを確認しているものは以下の通りです。

  1. アセンブリの「実行」(エントリーポイントの呼び出し)
  2. アセンブリ内のスタティックメソッドを関数ポインターとして取得・呼び出し(ICLRRuntimeHost2::CreateDelegate を利用)
  3. アセンブリ内の型に対応するインスタンスのプロパティーを利用(スタティックメソッドから取得したインスタンスに対し ICustomPropertyProvider(ABI::Windows::UI::Xaml::Data::ICustomPropertyProvider) に変換して利用)
  4. (アセンブリ内の型に対応するインスタンスがCOMのサポートをしている場合、IDispatch経由でのメソッドの利用)

※ 2点目の関数ポインターは戻り値が HRESULT ではなく元のメソッドの戻り値そのままとなっており、オブジェクトは IUnknown のオブジェクトとなります。また、文字列はいわゆるUnicode文字列ではなくマルチバイト文字列で利用します(UTF-8 と思われますが厳密には未確認)。
※ 4点目は.NETの実装に基づく推測であり、実挙動は確認できていません。

注意点として、.NETで用意されているアセンブリのほとんどはCOMのサポートを行っていないため、VBを含むネイティブ処理からのインスタンスメソッドの呼び出しができない状態になっています。そのため、別途マネージコードで実装されたラッパーライブラリ(アセンブリ)を作成してそのライブラリ経由で.NET Coreの機能を利用するしかないと考えられます。

(2023/02/12 更新) 実際にやってみた例を公開しています → VBから.NET(旧.NET Core)を利用してみる

まとめ

VBでCLRの実行コードを動的生成できることにより、Delegateの利用などをVBからもできるようになります。必ずしも簡単に利用できるというわけではないため、他に代替手段がなくどうしてもCLR/.NETの機能を利用したいときに使うのが適していると思われますが、ネイティブでカバーしたりCOM相互運用を可能にする形でアセンブリを作り直したりする必要が無いため、VB6系・VBAのプログラムにCLR/.NETの機能を取り入れて機能を実現することが可能になります。

補足: サポートクラス「CLRHost」

VBからCLRを利用しやすくするためのサポートクラス「CLRHost」のソースコードをGitHubの「vb2clr」プロジェクトとして公開しました。VBA 7.0向けに作成しており、以下の手順で利用可能です。

  1. ここにある「CLRHost.cls」と「ExitHandler.bas」をインポート
  2. タイプライブラリ「Common Runtime Language Execution Engine」と「mscorlib.dll」をプロジェクトにインポート
  3. 「CLRHost」クラスを利用したソースコードを記述

README.mdには英語ながら簡単な説明と単純なサンプルコードを掲載していますので、こちらもあわせてご覧ください。

注意点は以下の通りです。

  • CLRHostクラスのインスタンスを利用し終わった場合は、必ずインスタンスを解放するか「Terminate」メソッドを呼び出すようにしてください。これを適切に行わなかった場合、プロセス上にCLRが残り続けることで予期しない動作が起こる可能性があります。
  • 「Initialize」メソッドの「TerminateOnExit」に「True」を指定した場合は、デバッガーの中断状態(ソースコードのデバッグ中断中)でアプリケーションの停止を行わないでください。これは、中断状態のまま停止を行うと ExitHandler モジュール内の処理が実行できずにアプリケーション(VBAの場合はExcel全体なども含む)が異常終了してしまう可能性があるためです。

なお、ライセンスは「修正BSDライセンス」(3条項BSDライセンス; BSD-3-Clause)としています。

※ 公開しているソースコードはVBA 7.0向けですが、「PtrSafe」や「LongPtr」周りの記述を修正することでVB 6.0系でも利用できると思われます。ただし動作未確認ですのでご注意ください。