Skip to content

設計思想について ​

Pipe・WebSocket に続く3つ目の通信路として、v3.0.0 で WebView2(ICoreWebView2)を直接叩くモードを増設しました。

Pipe・WebSocket がどちらも「外にいるブラウザプロセス」を相手にするのに対し、WebView2 モードは「Excel自身のUserFormに埋め込んだブラウザ」を相手にします。デバッグポート--remote-debugging-portも匿名パイプ--remote-debugging-pipeも一切開かず、WebView2 SDK が公開する COM インターフェースを直接呼び出す、まったく別の経路です。

既存のCDPスタックへの繋ぎ込み方 ​

CDPCoreViaWebSocket.cls と同じ「CDPCore.cls に RaiseEvent で繋ぎ込む」拡張パターンを踏襲しています。実装対象は WebView2 SDK の3メソッドだけです(このツールは CDP フォーマットしか扱わないため、ナビゲーション制御や DOM 操作などの WebView2 本来の機能は一切実装しません)。

  • CallDevToolsProtocolMethodAsync → ICoreWebView2::CallDevToolsProtocolMethod
  • CallDevToolsProtocolMethodForSessionAsync → ICoreWebView2_11::CallDevToolsProtocolMethodForSession
  • GetDevToolsProtocolEventReceiver → ICoreWebView2::GetDevToolsProtocolEventReceiver

WebSocket 版は「バイト列の断片を都度 RaiseEvent で流す」設計でしたが、WebView2 は COM コールバック経由で「UTF-16 デコード済み・欠けのない完成 JSON 文字列」を1件ずつ届けてくれます。そのため CDPCoreViaWebView2 は RaiseEvent readWebView2CDP(RawJson As String) という、文字列1件そのままの、より単純な形のイベントを発火するだけで済みます。

CDPの外側にある機能(v3.1.0〜) ​

「CDPフォーマットしか扱わない」という上記の原則には、実は1つだけ穴がありました。拡張機能(Extensions)関連のCDPコマンドが、WebView2経路では丸ごとMethod not availableで弾かれるのです(Extensions.loadUnpacked等。おそらく内部的にPage単位のセッションとして実行されているためと推測しています)。

CDPだけでは拡張機能を扱えない以上、WebView2 SDK側の専用APIを直接叩くしかありません。そこでCDPCoreViaWebView2に、CDPを介さない拡張機能専用のメソッドを追加しました。

vb
Public Function AddBrowserExtension(extensionFolderPath As String) As String       ' 戻り値: インストールした拡張機能のID
Public Function GetBrowserExtensionIds() As Collection                             ' 各要素はDictionary(キー: "ID"/"Name"/"IsEnabled")
Public Function RemoveBrowserExtension(extensionId As String) As Boolean

この対応には、ICoreWebView2Environmentを生成する時点の設定(AreBrowserExtensionsEnabled等)を差し込む必要がありました。ところが、この設定を渡すICoreWebView2EnvironmentOptions自体もCOMインターフェースで、公式にはネイティブDLL経由でしかインスタンス化できません。そこでWebView2EnvOptions.clsが、このインターフェースそのものをVBA側のvtable偽造で丸ごとエミュレーションしています(Set_AreBrowserExtensionsEnabled等のプロパティで設定し、CDPCoreViaWebView2.EnvironmentOptions経由でOpenWebView2AndConnectCDP前にアクセスします)。詳しい使い方はWebView2モードでできることを参照してください。

ついでに全部載せた

ICoreWebView2EnvironmentOptionsのvtableを一度組み上げてしまうと、他の関連インターフェース(ICoreWebView2 / ICoreWebView2Controller / ICoreWebView2Environment / ICoreWebView2Settings / ICoreWebView2Profile)についても、コールバックが要らないスカラー値のプロパティであれば追加コストがほぼ無いことがわかりました。そこで、CDPで完結しないWebView2固有の見た目・挙動設定(ズーム倍率、ダウンロード先フォルダ、DevTools有効/無効等)も、まとめてプロパティメソッドとして公開しています。ただし、①完了コールバックが要るもの、②イベント系、③さらに別インターフェースへ依存するネスト系、の3種類は「あくまでCDP制御主体のツール」という線引きのため対象外です。

既知の制約 ​

WebView2独自のイベント購読モデル

CDP-over-Pipe / CDP-over-WebSocket は「ドメインを enable すれば、以後そのドメインの全イベントが自動で流れてくる」モデルです。しかし WebView2 の GetDevToolsProtocolEventReceiver は 「イベント名ごとに個別登録」 が必要なモデルです。この違いを隠さず、SubscribeCdpEvent / UnsubscribeCdpEvent という明示的な API として公開しています(一括購読の概念はWebView2側に無いため未対応。一括解除のみ UnsubscribeAllCdpEvents として提供)。

VBEでのリセットに注意

すべてのCOMコールバックは、機械語で書かれたサンク(後述)を経由します。次の状態は、それぞれ危険度が異なります(v3.2.0でさらに精査)。

  • 安全: .navigate等の同期メソッド呼び出しが完了し、普通のVBAコード行でブレーク/ステップ実行が静止している状態。この状態のままブラウザ側からコールバックを後で受け取っても、通常はクラッシュしません
  • 危険①: .navigate等、内部で完了まで待つ同期メソッドの呼び出し中そのものでブレークすると、ネイティブ呼び出しの真っ最中にVBEのデバッガが割り込む形になり、クラッシュする可能性があります
  • 危険②: 静止ブレイク中でも、ページ操作やJS遷移等をきっかけに、購読中のCDPイベントや保留中のコマンド完了通知が実際に届くと、機械語サンク経由でVBAへ再入しようとしてクラッシュする可能性があります
  • 最も危険: 上記①②いずれの状態でも、VBEでリセット操作を行うのはほぼ確実にクラッシュします(サンク領域やハンドラオブジェクトが解放され、ブラウザ側が保持中の生ポインタが無効化されるため)

どうしてもリセットが必要な場合は、まず処理を安全な静止状態まで進め、イミディエイトウィンドウからUnsubscribeAllCdpEventsで購読を解除してから行ってください。

UnsubscribeAllCdpEventsを呼ぶと内部トラッキングも止まる(v3.2.0〜)

接続成功時、このツールはPage.frameStartedLoading等、WaitEventsや生存確認に必要なCDPイベントを内部で自動購読しています(WaitEventsを参照)。デバッグ目的でUnsubscribeAllCdpEventsを呼ぶと、これらの内部トラッキングも道連れで止まります。以後WaitEvents等が正しく機能しなくなるため、デバッグが終わったら再接続するか、必要なイベントを再購読してください。

なお、OnCdpMethodCompleted / OnCdpEventReceived の中からCDPの同期待ちメソッド(ExecuteCDP等)を呼ぶと再入デッドロックになりますが、このツールはCOMスレッド側で受け取ったレスポンスをバッファに格納して即座に抜ける設計にしているため、利用者側でこれを意識する必要はありません。

クラッシュ調査には外部デバッグログビュアーが便利(v3.2.0〜)

上記のようなクラッシュは、発生した瞬間にイミディエイトウィンドウの内容ごと失われがちです。VBAプロジェクトの条件付きコンパイル引数に exportDbgMsg = 1 を追加すると、すべてのログが OutputDebugStringW 経由でも出力されるようになり、DebugView(インストール不要)等の外部ビュアーでExcelクラッシュ後も直前のログを確認できます。日常利用では不要な、調査用のオプトイン機能です。

複数タブは「別ウィンドウ」として開きます

CDPBrowser.newTab(内部的にはTarget.createTarget)は、WebView2モードでも他のtransportと同じように使えます。ただし、WebView2のICoreWebView2は1インスタンス=1ページのため、新しく作られたタブをUserFormの中に埋め込む仕組みまでは用意していません。そのため、2つ目以降のタブは独立した新規ウィンドウとして立ち上がります。

タブ(=ウィンドウ)をまたいだCDPコマンドの送受信自体は、ICoreWebView2_11::CallDevToolsProtocolMethodForSession(SendCommandCDPがsessionIdの有無で自動的に切り分けます)で正しくルーティングされるため、CDPContextを複数持って並行操作すること自体は可能です。「UserFormの中に複数タブ分のビューを並べて表示する」機能が無いだけです。

低レイヤの実装:機械語サンクとvtable ​

WebView2はIUnknownベースのCOMオブジェクトで、VBAのObject変数(IDispatchベース)としては直接扱えません。関数の呼び出しにはDispCallFunc(vtableのインデックスを直接指定して実行するWindows API)を使い、コールバックを受け取るには、AddressOfで取得した関数ポインタをメモリ上に構造体として詰め込み、「COMオブジェクトのフリをしたデータ」を構築する(vtable偽造)必要があります。

移植元へのクレジット

この機械語サンク・vtable呼び出し・SAFEARRAYメモリプリミティブの心臓部(WebView2Thunks.bas)は、WebView2-For-Excel-VBA(作者:たーぼー(インコ) 氏、MIT License)の Wv2Thunks.bas を、バイト列やオフセット値を一切変更せずそのまま移植したものです。

このツール向けに追加/変更したのはCDP専用の薄い層と、v3.1.0で追加した拡張機能対応です。

  • HandlerKind に、CDP用の種別(HK_EnvironmentCompleted / HK_ControllerCompleted / HK_CdpMethodCompleted / HK_CdpEventReceived)を追加
  • CallDevToolsProtocolMethodCompletedHandler と DevToolsProtocolEventReceivedEventHandler の実IIDを、内部のIIDテーブルへ追加
  • v3.1.0:ICoreWebView2EnvironmentOptionsエミュレーション(WebView2EnvOptions.cls)用に、プロパティの数だけget_/put_ハンドラ種別を追加

低レベルの死闘(QueryInterfaceがE_NOINTERFACEを返す、vtableオフセットが1つずれるだけでクラッシュする等)をすでに乗り越えてくれていた先人の実装があったからこそ、CDP側は安心して「その上に何を乗せるか」だけに集中できました。改めて感謝します。

関連 ​

Excel VBA × CDP / WebDriver BiDi