—— 从 IE ActiveX 到 Chromium Renderer 注入的非标准通信机制
关键词:window.external、Chromium、V8 注入、Renderer、IPC、非标准 API、浏览器内核
适合人群:浏览器内核工程师 / 客户端工程师 / Hybrid 架构开发者
在 Chromium 体系下谈 JS 与 C++ 通信,如果只讲 postMessage、Extension、Mojo,那其实永远绕不开一个“历史包袱级别”的机制:
window.external
它不是 Web 标准
不属于 Blink API
甚至在官方文档中几乎“被忽略”
但在 国内浏览器 / 企业定制 / 老业务系统 中,它却真实存在了 十几年,并且至今仍在大量代码中“活着”。
这篇文章,我们不从“怎么用”讲起,而是:
站在浏览器内核工程师的视角,完整拆解 window.external 的设计动机、实现路径、架构定位、安全问题,以及它为什么注定被淘汰。
一、window.external 的历史背景与设计动机
1️⃣ window.external 从哪里来?
window.external 并不是 Chromium 发明的。
它的源头可以追溯到 IE / Trident 内核时代,本质上是 ActiveX / COM 的 JavaScript 暴露形式。
在 IE 中,宿主程序(如 Outlook、Office、企业客户端)可以:
-
向网页注入 COM 对象
-
JS 通过
window.external.xxx()直接调用本地能力
经典 IE 时代代码示例:
window.external.AddFavorite(url, title); window.external.RunShellCommand("calc.exe");
这在当年是完全合法、甚至被鼓励的做法。
设计初衷非常明确:
让网页成为“壳”,本地程序才是“核心”。
2️⃣ 为什么 Chromium 也实现了 window.external?
如果你从“Web 标准洁癖”的角度看 Chromium,会觉得这很奇怪:
Chromium 一向强调安全、沙箱、最小权限
那为什么还要保留这种“野蛮接口”?
答案只有一句话:
历史 + 现实 + 商业
在国内浏览器生态中:
-
大量 老业务系统
-
政企内部系统
-
PC 客户端 + Web 混合架构
严重依赖以下模式:
window.external.invoke(...) window.external.CallNative(...)
这些系统往往:
-
不可能重写
-
不可能升级成 Extension
-
不可能接受严格权限模型
所以对很多厂商来说:
window.external 是“兼容成本最低”的宿主通信方案
二、window.external 在 Chromium 架构中的真实定位
1️⃣ 它不属于 Web API
这是理解 window.external 的第一道门槛。
它不在:
-
Blink Web IDL
-
DOM API
-
标准 JS Runtime
它属于一个完全不同的类别:
宿主注入(Host Object Injection)
也就是说:
这是 C++ 主动向 V8 Context 注入的“外挂对象”
2️⃣ 进程层级关系(极其关键)
window.external 的存在位置,决定了它的能力边界。
关键结论:
-
window.external一定存在于 Renderer -
但 真正的能力永远在 Browser / Native
window.external 本身不是能力提供者,而是:
JS → C++ → IPC → Native 的“入口点”
三、window.external 的总体实现模型
一句话抽象模型:
在 Renderer 中注册一个 V8 Object → JS 调用 → C++ 捕获 → IPC → Browser 执行
我们可以拆解为 6 个阶段:
-
Renderer 创建 C++ 扩展对象
-
注入到 window
-
绑定 JS 方法
-
JS 调用进入 V8 回调
-
参数序列化
-
IPC 发往 Browser
这是一个非常典型的“非标准通信路径”。
四、关键实现路径(Chromium 源码级)
下面这部分,是内核工程师真正关心的地方。
1️⃣ 注入时机:DidClearWindowObject
window.external 的注入不是随便找个地方做的。
唯一正确、稳定的注入时机是:
void ChromeRenderFrameObserver::DidClearWindowObject() { InstallExternalObject(render_frame()); }
DidClearWindowObject 的含义是:
-
新的 JS Context 被创建
-
页面导航 / 刷新 / iframe 初始化完成
这是 V8 Context 生命周期中唯一安全的注入点。
2️⃣ 创建 V8 ObjectTemplate
v8::Local<v8::ObjectTemplate> external = v8::ObjectTemplate::New(isolate); external->Set( v8::String::NewFromUtf8Literal(isolate, "invoke"), v8::FunctionTemplate::New(isolate, InvokeCallback));
这里发生了三件非常重要的事:
| 行为 | 说明 |
|---|---|
| ObjectTemplate | JS 对象“蓝图” |
| FunctionTemplate | JS ↔ C++ 绑定 |
| isolate | 绑定当前 V8 实例 |
3️⃣ 挂载到 window.external
v8::Local<v8::Object> external_obj = external->NewInstance(context).ToLocalChecked(); context->Global()->Set( context, v8::String::NewFromUtf8Literal(isolate, "external"), external_obj);
这一行执行完之后:
window.external // 出现在 JS 世界中
从此开始,JS 与宿主之间的“禁忌之门”被打开。
五、JS → C++ 调用链深度拆解
1️⃣ JS 侧调用方式
window.external.invoke( "ReadFile", JSON.stringify({ path: "c:\\test.txt" }) );
注意这里的 JSON.stringify,这是一个非常典型的坑。
2️⃣ 进入 V8 FunctionCallback
void InvokeCallback( const v8::FunctionCallbackInfo<v8::Value>& args) { v8::Isolate* isolate = args.GetIsolate(); }
此时你已经处于:
Renderer 进程 + V8 调用栈
3️⃣ 参数解析(工程实践中的坑王)
std::string method; std::string json; if (args.Length() >= 2) { method = V8ToString(args[0]); json = V8ToString(args[1]); }
⚠️ 常见错误:
window.external.invoke("ReadFile", { path: "c:\\a.txt" });
C++ 中你拿到的将是:
[object Object]
所以JSON 是事实上的“协议层”。
4️⃣ IPC 发送
Send(new ExtensionHostMsg_ExternalCall( routing_id(), method, json ));
从这一刻起:
-
Renderer 不再持有能力
-
所有风险、权限、系统操作都在 Browser
六、Browser 侧处理模型
1️⃣ IPC 接收
bool BrowserMessageFilter::OnMessageReceived( const IPC::Message& message) { IPC_MESSAGE_HANDLER(ExternalCall, OnExternalCall) }
2️⃣ Native 分发
void OnExternalCall( const std::string& method, const std::string& json) { if (method == "ReadFile") { HandleReadFile(json); } }
3️⃣ 执行系统能力
这一层已经完全脱离 Web 世界:
-
文件系统
-
注册表
-
登录态
-
本地设备
-
系统服务
这是一个彻底绕过 Web 安全模型的执行环境
七、返回值与回调模型设计
1️⃣ 同步返回(不推荐)
args.GetReturnValue().Set( v8::String::NewFromUtf8(isolate, result.c_str()) );
❌ 问题非常明显:
-
阻塞 Renderer
-
无法跨进程等待
-
极易造成卡顿
2️⃣ 异步回调(主流方案)
JS:
window.external.invoke("ReadFile", json, function(result) { console.log(result); });
C++:
-
保存 callback id
-
Browser 执行完成
-
反向 IPC
-
Renderer 执行 JS 回调
八、安全与设计缺陷(它为什么必然被淘汰)
1️⃣ 完全绕过 Web 安全模型
| 机制 | 是否生效 |
|---|---|
| Same-Origin | ❌ |
| CSP | ❌ |
| Permissions | ❌ |
只要 JS 能执行:
window.external.invoke("DeleteFile", "C:\\Windows");
2️⃣ 无 Schema、无约束
-
参数是字符串
-
无类型系统
-
无接口描述
-
无权限分级
3️⃣ 生命周期极其混乱
-
iframe reload
-
页面 crash
-
Native 仍在执行
最终结果:
callback 丢失,资源泄漏,难以维护
九、window.external 的现实定位
| 维度 | 评价 |
|---|---|
| 易用性 | ⭐⭐⭐⭐⭐ |
| 安全性 | ⭐ |
| 可维护性 | ⭐ |
| 现代化 | ⭐ |
| 历史兼容 | ⭐⭐⭐⭐⭐ |
十、工程师总结一句话
window.external 是一个“把浏览器当 ActiveX 用”的时代产物。
它简单、直接、暴力
能快速解决问题
但注定无法支撑:
-
Chromium 的安全模型
-
多进程架构
-
长期可维护性
它不是“错”,只是不属于这个时代。
十一、JS 是如何运行在 V8 里的?
—— 理解 window.external / JS ↔ C++ 通信之前必须补上的“底层真相”
在解释 window.external、Mojo、Extension、IPC 之前,有一个问题如果没有想清楚,后面的所有机制都会显得“像魔法”:
JavaScript 明明是一门脚本语言,它到底是怎么运行起来的?
又为什么能直接调用 C++?
这一章,我们完全不从 API 讲,而是从 V8 与 Chromium 内核视角,把 JS 的“运行本质”彻底拆开。
11.1 一个必须先接受的结论
先给出一个很多前端从未被明确告诉过的事实:
JavaScript 并不是自己在运行,而是 V8 这个 C++ 程序在“执行 JS 代码”。
也就是说:
-
JS 不是操作系统调度的进程
-
不是虚拟机里独立跑的程序
-
而是 C++ 主动加载、解析、编译并执行的一段文本
这一点,是理解 JS ↔ C++ 一切通信机制的起点。
11.2 V8 在 Chromium 中的真实角色
从工程角度看,V8 的定位非常清晰:
-
它是一个 用 C++ 编写的 JavaScript 引擎
-
提供:
-
JS 语法解析器(Parser)
-
解释器(Ignition)
-
JIT 编译器(TurboFan)
-
垃圾回收器(GC)
-
对象与原型系统
-
在 Chromium 的 Renderer 进程中,真实结构是:
Chromium Renderer(C++) └── Blink(DOM / Web API) └── V8(执行 JS)
JS 永远寄生在宿主程序中运行,它不能脱离宿主独立存在。
11.3 在 V8 眼中,JS 代码是什么?
当你写下这样一段 JS:
console.log("hello");
在 V8 看来,它的本质只是:
"console.log(\"hello\")"
也就是说:
JS 源码在进入 V8 之前,只是一段字符串
11.4 JS 是如何“被运行”的?
在 Chromium Renderer 中,JS 的执行流程(高度抽象后)如下:
// 1. 创建 V8 Isolate(一个 JS 虚拟机实例) v8::Isolate* isolate = v8::Isolate::New(...); // 2. 创建 Context(一个 JS 世界) v8::Local<v8::Context> context = v8::Context::New(isolate); // 3. 进入这个 JS 世界 v8::Context::Scope context_scope(context); // 4. 编译 JS 源码 v8::Local<v8::Script> script = v8::Script::Compile(context, source).ToLocalChecked(); // 5. 执行 JS script->Run(context);
这就是 JS“运行”的全部秘密。
没有线程魔法
没有语言特权
👉 JS 的每一行代码,都是 C++ 主动调用 Run() 执行的
11.5 V8 Context:JS 的“世界”是什么?
v8::Context 可以理解为:
一个完整的 JS 世界(Realm)
它包含:
-
全局对象(window / global)
-
变量作用域
-
原型链
-
内建对象(Array / Object / Function)
几个关键事实:
-
页面刷新 → Context 销毁
-
iframe → 新 Context
-
同一个页面可以有多个 Context
11.6 window 是谁创建的?
这是一个非常颠覆直觉但极其重要的事实:
window 不是 JS 创建的。
真实流程是:
C++ 创建 window 对象 ↓ 挂载到 V8 Context.global ↓ JS 才能访问 window
JS 只能使用宿主提供的对象,而不能反向定义它们。
这也是为什么:
-
JS 无法创建 document
-
JS 无法创建 location
-
JS 无法创建 window.external
11.7 JS 调用函数时,V8 内部发生了什么?
当 JS 执行:
foo(1, 2);
V8 会经历三个阶段:
1️⃣ 解析源码,生成 AST
2️⃣ 生成字节码(Ignition)
3️⃣ 执行字节码 / JIT 编译后执行
11.8 如果 foo 是 JS 函数
function foo(a, b) { return a + b; }
-
V8 执行 JS 字节码
-
完全在 JS 引擎内部完成
11.9 如果 foo 是 C++ 函数(关键转折点)
如果 C++ 这样注册了一个函数:
v8::FunctionTemplate::New(isolate, FooCallback);
并把它挂到 JS 世界:
global->Set(context, "foo", foo_function);
JS 调用:
foo(1, 2);
此时真实调用链是:
JS 调用 foo ↓ V8 发现这是 Host Function ↓ 直接调用 FooCallback(C++)
👉 JS 并没有“跨语言调用”,只是 V8 在执行 C++ 回调。
11.10 JS 为什么能“感知” C++ 的存在?
因为 V8 提供了两种核心能力:
1️⃣ Host Object
C++ 可以创建一个“像 JS 一样”的对象
2️⃣ Host Function
C++ 可以把一个函数挂进 JS 世界
JS 根本不知道背后是 C++,它只看到:
window.external.invoke(...)
11.11 JS 以为自己是“独立运行”的原因
这是浏览器刻意营造的“抽象幻觉”。
JS 看到的是:
| JS API | 背后真实实现 |
|---|---|
| window | C++ |
| document | Blink |
| fetch | Network Service |
| setTimeout | Browser Timer |
| console | DevTools |
👉 JS 只是“接口层”,能力永远在 C++。
11.12 回到 window.external:一切突然都合理了
现在再回看 window.external:
-
它不是 JS 特权
-
不是魔法
-
只是:
-
C++ 创建对象
-
注入到 V8 Context
-
JS 调用触发 C++ 回调
-
再通过 IPC 调到 Browser
-
window.external 的“危险”,不是它能通信,而是它通信得太直接。

4497

被折叠的 条评论
为什么被折叠?



