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を使用する場合はこれらを利用することができそうです。
しかし、いざ利用しようとなると以下の問題が出てきます。
- Marshal.GetDelegateForFunctionPointerをMethodInfo.Invoke経由で呼び出そうとしてもポインターアドレスを渡すことができない(※)
- 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の作成
コールバック関数を用いる方法は比較的シンプルですが、以下の問題があります。
- コールバックに追加情報を渡すことができない(どのDelegate経由で呼び出されたか判定できない)
- 型指定に気を遣う必要がある(誤ると予期しない動作になる)
これらを解決できるのが次に紹介する「クラスのメソッドを呼び出すDelegateを作成する」方法です。大まかな手順は以下の通りです。
- ラッパークラスを用意し、そのクラスでDelegateのハンドルを行う
- そのハンドルするメソッド内でラップ元のクラス(COMオブジェクト)のメソッドを呼び出す
こちらもC#などによるラッパークラスを利用することになりますが、Delegate.CreateDelegateに渡すメソッドとDelegate型の引数の数を一致させる必要があるため、以下のC#(擬似)コードのような回りくどい方法を用いて作成します。
(※ 前述のVBによる「CreateDelegateWrapper」生成コードを以下のように書き換えます。)
なお、このコードでは「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」ステートメントを利用することができます。
- 対象オブジェクトに「DispId」が「-4」(DISPID_NEWENUM)であるプロパティーまたはメソッドが存在する
- そのプロパティー(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自体は初期化できるものの、現時点で(マネージコードなしに)ネイティブのみで利用できることを確認しているものは以下の通りです。
- アセンブリの「実行」(エントリーポイントの呼び出し)
- アセンブリ内のスタティックメソッドを関数ポインターとして取得・呼び出し(ICLRRuntimeHost2::CreateDelegate を利用)
- アセンブリ内の型に対応するインスタンスのプロパティーを利用(スタティックメソッドから取得したインスタンスに対し ICustomPropertyProvider(ABI::Windows::UI::Xaml::Data::ICustomPropertyProvider) に変換して利用)
- (アセンブリ内の型に対応するインスタンスが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向けに作成しており、以下の手順で利用可能です。
- ここにある「CLRHost.cls」と「ExitHandler.bas」をインポート
- タイプライブラリ「Common Runtime Language Execution Engine」と「mscorlib.dll」をプロジェクトにインポート
- 参考: タイプ ライブラリへの参照の設定
- 「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系でも利用できると思われます。ただし動作未確認ですのでご注意ください。