扩展宿主:VS Code 如何隔离并与扩展通信
前置知识
- ›第 1 篇:架构与分层
- ›第 2 篇:启动与进程架构
- ›第 3 篇:DI 引擎与服务模式
- ›理解 IPC 和 RPC 的基本概念
扩展宿主:VS Code 如何隔离并与扩展通信
VS Code 的扩展生态系统是将一个文本编辑器蜕变为开发平台的核心能力。但如果把成千上万个第三方扩展都运行在编辑器主进程中,崩溃、卡死和安全漏洞将会接踵而至。为此,VS Code 将扩展运行在独立的扩展宿主进程中,通过一套庞大且类型完备的 RPC 协议与工作台进行通信。本文将深入剖析这一架构。
三种扩展宿主类型
ExtensionHostKind 枚举定义了三种扩展宿主环境:
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 子进程中,拥有完整的文件系统和网络访问权限,是最宽松的运行环境。在 manifest 中声明 "extensionKind": ["workspace"] 的扩展会在此环境中运行。
LocalWebWorker 面向轻量级、以 UI 为中心的扩展,这类扩展只需要浏览器 API。在 vscode.dev(Web 版 VS Code)上,这也是唯一可用的宿主类型。声明 "extensionKind": ["ui"] 的扩展会尽可能在此环境中运行。
Remote 将扩展宿主运行在完全不同的机器上。通过 SSH 连接时,Remote 扩展会在远端机器上启动一个无界面的 VS Code 服务器,由它负责启动扩展宿主进程。工作区扩展在远端运行并访问远程文件系统,而 UI 扩展则留在本地运行。
determineExtensionHostKinds() 中的分配逻辑会综合考量每个扩展声明的类型、本地或远程安装情况以及开发偏好,最终生成一份完整的映射关系。
协议契约:extHost.protocol.ts
工作台(主线程侧)与扩展宿主之间的通信,全部定义在一个单一的庞大文件中:src/vs/workbench/api/common/extHost.protocol.ts。这个文件将近 3,922 行,是整个代码库中最大的接口定义文件。
该文件采用成对接口的设计方式——每个 MainThread*Shape 接口(扩展宿主可调用的工作台方法)都对应一个 ExtHost*Shape 接口(工作台可调用的扩展宿主方法)。以下是部分示例:
| MainThread(工作台侧) | ExtHost(扩展宿主侧) |
|---|---|
MainThreadCommandsShape |
ExtHostCommandsShape |
MainThreadLanguageFeaturesShape |
ExtHostLanguageFeaturesShape |
MainThreadSCMShape |
ExtHostSCMShape |
MainThreadDebugServiceShape |
ExtHostDebugServiceShape |
MainThreadWebviewsShape |
ExtHostWebviewsShape |
文件末尾,两个对象负责注册所有代理标识符:
src/vs/workbench/api/common/extHost.protocol.ts#L3757-L3840
MainContext 对象注册了约 83 个工作台侧服务的代理标识符,ExtHostContext 对象注册了 60 余个扩展宿主侧处理器的代理标识符。两者合计定义了超过 140 个 RPC 接口,共同构成了扩展与工作台之间完整的通信契约。
提示: 实现新的扩展 API 时,从
extHost.protocol.ts开始是最好的切入点。先添加MainThread*Shape和ExtHost*Shape接口,在两个 context 对象中注册代理标识符,再分别实现两侧的逻辑。类型系统会帮你确保实现的完整性。
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
扩展宿主启动时会收到一个 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)、MessagePort(ipc.mp.ts)以及 WebSocket 之上。其上层的 RPC 协议层则在此基础上提供了自动序列化、取消传播,以及基于 RequestType/ResponseType 枚举协议的事件订阅管理。
构建 vscode.* 命名空间
扩展通过 import * as vscode from 'vscode' 引入的公共 API,是在运行时由 createApiFactoryAndRegisterActors() 动态构建的。
这个函数是整个 API 层的连接枢纽,它负责:
-
从扩展宿主的 DI 容器中解析约 30 个 ExtHost 服务实例,包括
extHostWorkspace、extHostConfiguration、extHostCommands、extHostTerminalService等。 -
将它们注册为 RPC 处理器,对每个实例调用
rpcProtocol.set(ExtHostContext.ExtHostCommands, extHostCommands),使工作台能够跨进程调用这些对象上的方法。 -
创建不受 DI 管理的额外 ExtHost 对象,如
ExtHostDocuments、ExtHostNotebookController等。 -
返回一个工厂函数,该函数接收扩展描述信息,为该扩展构建出一个量身定制的完整
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"]
返回的工厂函数在每次扩展激活时调用一次。正是这种针对每个扩展的独立调用,实现了 proposed API 的访问控制——工厂函数会在将不稳定 API 纳入命名空间之前,先检查该扩展是否具有相应访问权限。
Proposed API 与扩展信任机制
公开的 vscode.d.ts 类型定义是稳定的 API 契约。但 VS Code 还在 src/vscode-dts/vscode.proposed.*.d.ts 下维护着数十个 proposed API 文件。这些不稳定的 API 仅对以下情况开放:
- 在
product.json的extensionEnabledApiProposals中列出的扩展(微软及合作伙伴扩展)。 - 以
--enable-proposed-api标志在扩展开发模式下运行的扩展。
访问控制通过 checkProposedApiEnabled() 来实现,在 extHost.api.impl.ts 中每当访问 proposed API 时都会调用该函数:
// Example from the authentication namespace
checkProposedApiEnabled(extension, 'authLearnMore');
checkProposedApiEnabled(extension, 'authIssuers');
内置扩展(位于 extensions/ 目录下)是一类特殊的存在。它们随 VS Code 一起发布,可以访问内部 API,并随产品版本更新,而非通过扩展市场分发。这些扩展承载着核心功能:TypeScript 语言支持、Git 集成、Markdown 渲染、主题默认值等。
IExtensionHostInitData 接口定义了扩展宿主启动时收到的所有初始化数据:
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;
// ...
}
这份数据包含了完整的扩展列表(含激活事件)、工作区标识、日志级别以及远程连接信息。扩展宿主据此决定激活哪些扩展以及何时激活。
扩展与工作台之间的数据流
让我们通过一个具体示例来梳理整个流程:当扩展注册一个 DocumentSymbolProvider 时,调用链将跨越四道边界:
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
注册流程朝工作台方向流动,而实际的 provider 调用则朝扩展宿主方向流动。正是这种双向特性,决定了 MainContext 和 ExtHostContext 两套代理标识符同时存在的必要性。
下一篇
至此,我们已经完整梳理了服务基础设施:服务的声明与装配方式(第 3 篇)、进程间通信机制(本篇),以及扩展的接入方式。最后一篇文章将聚焦两大主要 UI 层——Monaco 编辑器引擎与工作台 shell——并探讨功能特性如何借助贡献系统组合成完整的 IDE 体验,将前几篇的内容融会贯通。