Read OSS

Extension Host:VS Code が拡張機能をどのように分離し、通信するか

上級

前提知識

  • 第 1 回:アーキテクチャとレイヤリング
  • 第 2 回:起動とプロセスアーキテクチャ
  • 第 3 回:DI エンジンとサービスパターン
  • IPC および RPC の基本概念の理解

Extension Host:VS Code が拡張機能をどのように分離し、通信するか

VS Code の拡張機能エコシステムは、テキストエディタをプラットフォームへと変貌させた機能です。しかし、何千ものサードパーティ拡張機能をエディタのプロセス内で直接実行すれば、クラッシュやフリーズ、セキュリティ上の脆弱性を招く恐れがあります。そこで VS Code は、拡張機能を独立した Extension Host プロセスで動作させ、型付きの RPC プロトコルを通じて Workbench と通信する設計を採用しています。本記事では、そのアーキテクチャを詳しく解説します。

3 種類の Extension Host

ExtensionHostKind enum は、Extension Host の実行環境として 3 種類を定義しています。

export const enum ExtensionHostKind {
    LocalProcess = 1,   // Node.js child process
    LocalWebWorker = 2, // Web Worker
    Remote = 3          // SSH/container/WSL
}
graph TD
    subgraph "Desktop VS Code"
        RENDERER["Renderer Process<br/>(Workbench UI)"]
        LP["<b>LocalProcess</b><br/>Node.js child process<br/>Full Node API access"]
        LW["<b>LocalWebWorker</b><br/>Web Worker<br/>Browser APIs only"]
    end
    
    subgraph "Remote Machine"
        SERVER["Remote Server"]
        REMOTE["<b>Remote</b><br/>Node.js process<br/>on SSH/container/WSL"]
    end
    
    RENDERER <-->|"Electron IPC /<br/>MessagePort"| LP
    RENDERER <-->|"Worker<br/>postMessage"| LW
    RENDERER <-->|"WebSocket"| SERVER
    SERVER <-->|"Node IPC"| REMOTE
    
    style LP fill:#e3f2fd
    style LW fill:#e8f5e9
    style REMOTE fill:#fff3e0

LocalProcess はデスクトップ版 VS Code のデフォルト環境です。拡張機能は Node.js の子プロセスとして起動し、ファイルシステムやネットワークへの完全なアクセスが可能な、最も自由度の高い実行環境です。マニフェストで "extensionKind": ["workspace"] を宣言している拡張機能はここで動作します。

LocalWebWorker は、ブラウザ API だけを必要とする軽量な UI 向け拡張機能に使われます。また、Web 版 VS Code(vscode.dev)では唯一の選択肢です。"extensionKind": ["ui"] を宣言している拡張機能は、可能な場合にこの環境で実行されます。

Remote は、拡張機能を完全に別のマシン上で動かすための環境です。SSH 接続時には、Remote 拡張機能が対象マシン上でヘッドレスの VS Code サーバーを起動し、そのサーバーが独自の Extension Host を立ち上げます。ワークスペース拡張機能はリモートのファイルシステムにアクセスしながらリモート側で実行され、UI 拡張機能はローカルに留まります。

determineExtensionHostKinds() の割り当てロジックは、各拡張機能が宣言した種別、ローカル・リモートどちらにインストールされているか、そして開発時の設定を考慮したうえで、最終的なマッピングを決定します。

プロトコル契約:extHost.protocol.ts

Workbench(メインスレッド側)と Extension Host の間の通信は、単一の巨大なファイル src/vs/workbench/api/common/extHost.protocol.ts で定義されています。約 3,922 行にのぼるこのファイルは、コードベース全体でも最大のインターフェース定義ファイルです。

このファイルはペアになったインターフェースを定義しています。Extension Host から Workbench 側を呼び出す MainThread*Shape に対して、Workbench から Extension Host を呼び出す ExtHost*Shape が 1 対 1 で対応します。いくつか例を挙げます。

MainThread(Workbench) ExtHost(Extension Host)
MainThreadCommandsShape ExtHostCommandsShape
MainThreadLanguageFeaturesShape ExtHostLanguageFeaturesShape
MainThreadSCMShape ExtHostSCMShape
MainThreadDebugServiceShape ExtHostDebugServiceShape
MainThreadWebviewsShape ExtHostWebviewsShape

ファイルの末尾には、すべてのプロキシ識別子を登録する 2 つのオブジェクトがあります。

src/vs/workbench/api/common/extHost.protocol.ts#L3757-L3840

MainContext オブジェクトは Workbench 側のサービス向けに約 83 個のプロキシ識別子を登録し、ExtHostContext オブジェクトは Extension Host 側のハンドラー向けに 60 個以上を登録しています。合わせると 140 を超える RPC インターフェースが定義されており、これが拡張機能と Workbench の通信面全体を構成しています。

ヒント: 新しい拡張機能 API を実装する際は、まず extHost.protocol.ts から始めましょう。MainThread*ShapeExtHost*Shape インターフェースを追加し、両方のコンテキストオブジェクトにプロキシ識別子を登録してから、両側の実装を進めます。型定義が実装の完全性を保証してくれます。

RPC プロキシパターンと IPC トランスポート

createProxyIdentifier<T>() 関数は、RPC システムがプロキシオブジェクトを自動生成する際に使う型付きのマーカーを作成します。処理の流れは次のとおりです。

sequenceDiagram
    participant Ext as Extension Code
    participant EH as ExtHostCommands
    participant RPC as RPC Protocol
    participant MT as MainThreadCommands

    Ext->>EH: vscode.commands.executeCommand('myCmd')
    EH->>RPC: proxy.MainThreadCommands.$executeCommand('myCmd')
    Note over RPC: Serialize to MessagePort/IPC
    RPC->>MT: $executeCommand('myCmd')
    MT->>MT: Execute command in workbench
    MT-->>RPC: Return result
    RPC-->>EH: Deserialized result
    EH-->>Ext: Promise resolves

Extension Host の起動時には IExtHostRpcService(RPC プロトコルのインスタンス)が渡されます。各側は rpcProtocol.set(identifier, handler) で受信コールを処理するオブジェクトを登録し、rpcProtocol.getProxy(identifier) でメソッド呼び出しを相手側に転送するプロキシを取得します。

内部のトランスポートは IChannel/IServerChannel インターフェースで抽象化されています。

export interface IChannel {
    call<T>(command: string, arg?: any, cancellationToken?: CancellationToken): Promise<T>;
    listen<T>(event: string, arg?: any): Event<T>;
}

export interface IServerChannel<TContext = string> {
    call<T>(ctx: TContext, command: string, arg?: any, cancellationToken?: CancellationToken): Promise<T>;
    listen<T>(ctx: TContext, event: string, arg?: any): Event<T>;
}

IChannel はトランスポートに依存しない抽象化レイヤーです。同じインターフェースが Electron IPC(ipc.electron.ts)、Node 名前付きパイプ(ipc.net.ts)、MessagePortipc.mp.ts)、WebSocket のいずれでも機能します。その上に位置する RPC プロトコルレイヤーが、自動シリアライズ、キャンセル伝播、RequestType/ResponseType enum プロトコルによるイベントサブスクリプション管理を担います。

vscode.* 名前空間の構築

拡張機能が import * as vscode from 'vscode' としてインポートする公開 API は、createApiFactoryAndRegisterActors() によってランタイムに組み立てられます。

この関数はすべてを繋ぐ配線ハブです。具体的には以下の処理を行います。

  1. Extension Host の DI コンテナから約 30 個の ExtHost サービスインスタンスを解決しますextHostWorkspaceextHostConfigurationextHostCommandsextHostTerminalService などです。

  2. それぞれを RPC ハンドラーとして登録するrpcProtocol.set(ExtHostContext.ExtHostCommands, extHostCommands) のように呼び出し、Workbench がプロセス境界を越えてメソッドを呼び出せるようにする。

  3. DI で管理されていない追加の ExtHost オブジェクトを生成しますExtHostDocumentsExtHostNotebookController などが該当します。

  4. ファクトリ関数を返します — この関数は拡張機能の定義を受け取り、その拡張機能のパーミッションに合わせた完全な typeof vscode オブジェクトを構築します。

flowchart TD
    A["createApiFactoryAndRegisterActors()"] --> B["Resolve ~30 ExtHost<br/>services from DI"]
    B --> C["Register as RPC handlers<br/>rpcProtocol.set()"]
    C --> D["Create additional<br/>ExtHost objects"]
    D --> E["Return factory function"]
    E --> F["factory(extension)"]
    F --> G["Check proposed API access"]
    G --> H["Assemble vscode.* namespace"]
    H --> I["Extension receives<br/>typed API object"]

返されたファクトリ関数は、拡張機能のアクティベーションごとに 1 回呼び出されます。この「拡張機能ごとの呼び出し」という設計がproposed API のゲーティングを可能にしています。ファクトリは不安定な API へのアクセス権を拡張機能ごとに確認してから、名前空間に含めるかどうかを決定します。

Proposed API と拡張機能の信頼

公開されている vscode.d.ts の型定義が安定した API 契約です。しかし VS Code は src/vscode-dts/vscode.proposed.*.d.ts 以下に多数の proposed API ファイルも管理しています。これらの不安定な API を利用できるのは、次のいずれかに該当する拡張機能のみです。

  1. product.jsonextensionEnabledApiProposals に列挙された拡張機能(Microsoft およびパートナー拡張機能)
  2. --enable-proposed-api フラグ付きで Extension Development モードで動作している拡張機能

このゲーティングは checkProposedApiEnabled() によって強制されており、proposed API にアクセスする箇所では extHost.api.impl.ts 全体にわたって呼び出されています。

// Example from the authentication namespace
checkProposedApiEnabled(extension, 'authLearnMore');
checkProposedApiEnabled(extension, 'authIssuers');

extensions/ ディレクトリに含まれるビルトイン拡張機能は特別な存在です。VS Code に同梱されており、内部 API へのアクセス権を持ち、マーケットプレイスではなく製品のリリースサイクルに合わせて更新されます。TypeScript 言語サポート、Git 連携、Markdown レンダリング、テーマのデフォルト設定など、重要な機能を担っています。

IExtensionHostInitData インターフェースは、起動時に Extension Host へ送信されるすべての情報を定義しています。

export interface IExtensionHostInitData {
    version: string;
    quality: string | undefined;
    commit?: string;
    parentPid: number | 0;
    environment: IEnvironment;
    workspace?: IStaticWorkspaceData | null;
    extensions: IExtensionDescriptionSnapshot;
    telemetryInfo: { ... };
    logLevel: LogLevel;
    remote: { isRemote: boolean; authority: string | undefined; ... };
    uiKind: UIKind;
    // ...
}

このペイロードには、アクティベーションイベントを含む完全な拡張機能リスト、ワークスペースの識別情報、ログレベル、リモート接続データが含まれています。Extension Host はこの情報をもとに、どの拡張機能をいつアクティベートするかを判断します。

拡張機能と Workbench のデータフロー

具体的な例として、拡張機能が DocumentSymbolProvider を登録する流れを追ってみましょう。この処理は 4 つの境界をまたいで進みます。

sequenceDiagram
    participant Ext as Extension
    participant API as vscode.languages
    participant EH as ExtHostLanguageFeatures
    participant RPC as RPC Protocol  
    participant MT as MainThreadLanguageFeatures
    participant EditorModel as Editor Model

    Ext->>API: registerDocumentSymbolProvider(selector, provider)
    API->>EH: $registerDocumentSymbolProvider(handle, selector)
    EH->>RPC: proxy.$registerDocumentSymbolProvider(handle, selector)
    RPC->>MT: $registerDocumentSymbolProvider(handle, selector)
    MT->>EditorModel: Register provider adapter
    
    Note over EditorModel: User opens a file...
    
    EditorModel->>MT: Request symbols
    MT->>RPC: $provideDocumentSymbols(handle, uri)
    RPC->>EH: $provideDocumentSymbols(handle, uri)
    EH->>Ext: provider.provideDocumentSymbols(document)
    Ext-->>EH: SymbolInformation[]
    EH-->>RPC: Serialized symbols
    RPC-->>MT: Deserialized symbols
    MT-->>EditorModel: Display in outline

登録はWorkbench へ向かって流れます。実際のプロバイダー呼び出しは Extension Host へ向かって流れます。MainContextExtHostContext の両方にプロキシ識別子が存在するのは、この双方向の性質によるものです。

次回予告

ここまでの記事で、サービスインフラ全体を網羅しました。サービスの宣言と配線(第 3 回)、プロセス間通信(本記事)、そして拡張機能の接続方法について解説してきました。最終回では、Monaco エディタエンジンと Workbench シェルという 2 つの主要な UI レイヤーに加え、コントリビューションシステムを通じて完全な IDE 体験を構成する仕組みを取り上げ、全体像をまとめます。