Ladybird LibWeb 绑定生成器的 Realm 策略:实例成员默认使用接收者 Realm,静态成员与例外使用调用方 Realm
本篇技术指南基于 Ladybird 浏览器的官方策略文档 REALM_POLICY.md,系统讲解 LibWeb 绑定生成器如何为每一个 WebIDL 成员做出显式的 realm(JS 执行域)决策:实例操作/属性默认使用校验后的接收者 realm(this_object_realm),静态操作/构造函数/命名空间成员使用调用方 realm,唯一的全局 opt-out 是 [NeedsCallerRealm] 扩展属性。读完本篇,你将掌握该策略的完整规则集、共享生成入口 member_realm_expr() 的实现与调用点、VM 层对 TypeError 构造域的限定机制,以及 WindowProxy 接收者 realm 的特殊解析逻辑,能够为编写或审查 LibWeb IDL 与生成代码提供准确依据。
一、为什么每个绑定成员都需要一次显式的 realm 决策
在 WebIDL 绑定生成中,"realm"(JS 执行域,见 LibJS 的 JS::Realm)决定了 DOMException 包裹、返回值包装、Promise 创建/拒绝、wrapper 分配等行为发生在哪一个可观察域上。当页面存在多个执行域(主世界与隔离世界、跨 realm 传递的对象等)时,同一个对象上的成员调用可能在调用方 realm 与接收者所属 realm 之间产生不同的可观察行为——异常对象的身份、Promise 的原型链、instanceof 结果都会随 realm 选择而变化。
Ladybird 的策略文档开宗明义:生成的绑定代码对每个 IDL 成员只做一次显式的 realm 决策(Generated bindings make one explicit realm decision per IDL member)。这避免了生成器各发射器(emitter)中散落的临时策略,也保证了 DOMException 包裹、异常身份与返回值分配遵循同一个可观察 realm。
二、策略规则集:四类成员的 realm 决策
2.1 实例操作与属性:默认使用 this_object_realm
文档规定,实例操作(operations)和属性(attributes)默认使用校验后的接收者 realm,即 this_object_realm。该 realm 被用于:
- 实现调用(implementation calls);
- DOMException 包裹;
- JavaScript 返回值包装(wrapping);
- 对 promise 类型成员进行 promise 的创建与拒绝。
同时,生成的 pair-iterable、async-iterable、maplike、setlike 辅助代码也使用接收者 realm 来创建迭代器/容器以及包装值(对应发射器 iterables.py)。
2.2 静态操作、构造函数与命名空间成员:使用调用方 realm
静态操作(static operations)、构造函数(constructors)与命名空间成员(namespace members)没有接收者对象,因此直接使用调用方/当前 realm(realm)。
2.3 [RealmFreeConstructor]:不需要当前 realm 的自定义构造函数
[RealmFreeConstructor] 用于标记一个 [ImplementedInBindings] 构造函数,其自定义绑定辅助函数不需要当前 realm。注意一个精确的边界:生成的构造函数仍然使用调用方/当前 realm 来完成 WebIDL 转换、异常包裹、wrapper 分配与原型选择;只有对实现辅助函数的调用省略了 realm 参数。
2.4 [NeedsCallerRealm]:实例成员的唯一 opt-out
[NeedsCallerRealm] 是实例成员的唯一退出机制:当其 Web 可见结果或实现算法被规范指定为使用调用方/当前 realm 时,在 IDL 上标注该属性。文档明确了两条约束:
- 不存在 getter/setter 各自独立的 caller-realm 注解;
- 属性级别的
[NeedsCallerRealm]同时作用于生成的 getter 和 setter。
在仓库的 IDL 中可以看到大量真实用例。例如 History.idl 中 pushState/replaceState 标注了 [NeedsCallerRealm]:
undefined back();
undefined forward();
[NeedsCallerRealm] undefined pushState(any data, DOMString unused, optional USVString? url = null);
[NeedsCallerRealm] undefined replaceState(any data, DOMString unused, optional USVString? url = null);
Location.idl 则展示了属性级标注同时覆盖 getter/setter 的形态(href、origin、protocol、host、hostname、port、pathname、search、hash 等属性均带 [LegacyUnforgeable, NeedsCallerRealm])。
三、共享生成入口:realms.py 中的两个函数
策略文档指出,所有生成器代码应通过共享入口做出 realm 决策,而不应在个别发射器里临时拼写 realm 策略:
The shared generator entry point is
Meta/Generators/libweb_bindings/realms.py::member_realm_expr(). New generated member code should use that helper instead of spelling ad-hoc realm policy in individual emitters.
realms.py 全文只有两个函数,逻辑与文档一一对应:
def member_realm_expr(
member: Union[Attribute, Operation, SpecialOperation],
*,
interface: Interface,
is_static: bool = False,
) -> str:
"""Return the C++ realm expression used for member work.
See Meta/Generators/libweb_bindings/REALM_POLICY.md. Instance members
default to the validated receiver realm. Statics, constructors, namespaces,
and explicit caller-realm opt-outs use the caller realm.
"""
if is_static or interface.is_namespace:
return "realm"
if "NeedsCallerRealm" in member.extended_attributes:
return "realm"
return "this_object_realm"
def member_passes_realm_to_implementation(member: Union[Attribute, Operation, SpecialOperation]) -> bool:
return "NeedsCallerRealm" in member.extended_attributes
即:静态成员或命名空间接口返回 "realm"(调用方 realm);标注了 [NeedsCallerRealm] 的实例成员返回 "realm";其余实例成员返回 "this_object_realm"。
3.1 消费点:哪些发射器调用了 member_realm_expr
在生成器目录内检索 member_realm_expr / member_passes_realm_to_implementation 的调用点,可以看到策略被贯彻到各成员类型的发射器中:
| 发射器 | 用途 |
|---|---|
| operations.py | 普通操作与静态操作的 realm 参数(约 L451 使用 member_realm_expr;L83、L115 用 member_passes_realm_to_implementation 决定是否向实现传递 realm) |
| attributes.py | 属性 getter(L327)与 setter(L695)各自的 realm 参数 |
| named_and_indexed_properties.py | 命名/索引属性 getter(L758、L806)与操作(L879) |
| overload_resolution.py | 重载仲裁器中对 promise 型操作设置 promise_realm(L145) |
四、生成代码如何落地:invoke_first_available 与 realm 参数的两种形态
以 operations.py 中的 implementation_operation_call()(L93-L121)为例,可以直观看到 realm 决策如何变成 C++ 代码。该函数根据成员签名,通过 Web::Bindings::invoke_first_available 生成"尝试多个候选签名"的 lambda 链:
Web::Bindings::invoke_first_available(*idl_object,
& -> decltype(no_realm_call) { return no_realm_call; },
& -> decltype(realm_call) { return realm_call; },
...)
其中候选调用包括"不传 realm"、"传 realm.global_object()"、"传 realm"、"传 vm"等形式;当成员标注了 [NeedsCallerRealm] 时,member_passes_realm_to_implementation() 返回 true,生成器会交换"带 realm 调用"与"不带 realm 调用"的优先级(L115-L116),从而让实现辅助函数显式收到调用方 realm 参数。
对于 promise 型操作,overload_resolution.py 的 write_overload_arbiter()(L141-L156)还会按同一策略预置 promise_realm:若 member_realm_expr() 返回 "realm",直接取 *vm.current_realm();否则从 vm.this_value() 经 this_value_realm() 解析接收者 realm(this 为 nullish 时回退到全局对象所在 realm)。这正对应文档中"promise 创建/拒绝发生在选定 realm"的规则。
五、对 WebIDL 默认措辞的有意偏离:接收者 realm 包裹 DOMException
文档明确说明,LibWeb 有意偏离 WebIDL 规范关于实例成员 DOMException 包裹的默认措辞:LibWeb 在接收者 realm 中包裹 DOMException,使 wrapper 分配、异常身份与返回值分配遵循同一个可观察 realm。
生成器为操作与 setter 的转换块(conversion blocks)还将普通 JS TypeError 的构造作用域限定在选定的成员 realm 上,并有两个重要的边界:
- VM 在转换重新进入作者脚本(author script)时清除该 TypeError 覆盖,因此嵌套脚本仍在其自身 realm 中抛出异常;
- 该机制刻意比改变 VM 的当前 realm 或执行上下文更窄——目的是让转换失败获得统一的 realm 身份,而不是让用户脚本在另一个 realm 下执行。
这一设计说明该策略不是简单"切换全局执行域",而是对异常/错误对象构造路径的局部限定。
六、WindowProxy 接收者:this_value_realm() 的解析规则
对 WindowProxy 这类接收者,this_object_realm 由 Bindings::this_value_realm() 提供。规则是:
- 主世界(main-world)代理解析到当前激活的
[[Window]]的 realm; - 非主世界代理保持在其自身 realm。
仓库中的实现印证了这一规则。Wrappable.cpp 中 this_value_realm()(约 L214 起)的核心逻辑:
JS::Realm& this_value_realm(JS::Realm& fallback_realm, JS::Value this_value)
{
if (!this_value.is_object())
return fallback_realm;
auto& object = this_value.as_object();
if (auto* window_proxy = as_if<HTML::WindowProxy>(&object)) {
auto& proxy_realm = object.shape().realm();
if (!proxy_realm.host_defined())
return fallback_realm;
// A main-world WindowProxy is created once, in the initial about:blank
// realm, then reused as globalThis across later navigations. Do not use
// that stale shape realm for receiver-realm decisions: HTML defines a
// WindowProxy's relevant realm as the realm of its current [[Window]].
if (host_defined_wrapper_world(proxy_realm).is_main_world()) {
if (auto window = window_proxy->window())
return window->principal_realm();
}
}
return object.shape().realm();
}
源码头注释解释了动机:主世界 WindowProxy 在初始 about:blank realm 中创建一次,之后跨导航复用为 globalThis;其 shape realm 是"陈旧的",因此接收者 realm 决策必须使用当前 [[Window]] 的 realm(window->principal_realm())。而隔离世界的 WindowProxy 在自身世界 realm 中创建,保持在代理自身 realm,保证跨世界包装(cross-world wrapping)的隔离性。该函数声明于 Wrappable.h。
七、当前 [NeedsCallerRealm] 的退出类别
文档列出了目前使用 [NeedsCallerRealm] 的几类 API,理解这些类别有助于判断新 IDL 成员应使用哪个 realm:
- WebAssembly JS API 对象与命名空间函数:JS-API 算法在调用方 realm 中创建 ArrayBuffer、typed array、exports 对象与 promise;
- Promise / body / fetch / cache / credential / permission / serial / gamepad / media API:promise 能力(capability)与结果对象的分配对调用方可观察;
- 结构化序列化 API(
postMessage、History、Navigationstate):克隆/反序列化相对调用方/当前 realm 定义; - 以调用方 JS 回调/构造函数/对象作为策略输入的 API:
CustomElementRegistry、Trusted Types、keyframes、CSS 属性注册; - 显式保留为 caller-realm 行为的 WPT/规范豁免:包括
SourceBuffer.buffered与AbortSignal.abort()/timeout()。
仓库 IDL 中可检索到这些类别的实际标注,例如 History.idl、Navigation.idl、CustomElementRegistry.idl、MessagePort.idl、Location.idl、TrustedTypePolicyFactory.idl 等文件均包含 [NeedsCallerRealm]。
八、实践指引:为新 IDL 成员选择 realm 策略
结合上述规则与源码,在编写或审查 LibWeb IDL 及生成代码时可遵循以下决策路径:
- 默认不加任何扩展属性——实例操作/属性、迭代辅助代码将自动落在
this_object_realm,DOMException、返回值包装与 promise 创建均在接收者 realm 中进行; - 静态操作/构造函数/命名空间成员——无需(也不能)标注,生成器经
member_realm_expr()的is_static/interface.is_namespace分支自动使用调用方 realm; - 仅当规范算法明确要求调用方 realm 时,在成员上标注
[NeedsCallerRealm];对属性标注一次即同时作用于生成的 getter 与 setter; [ImplementedInBindings]且辅助函数不需要 realm 的构造函数,标注[RealmFreeConstructor];- 新增生成器发射器时,realm 表达式一律调用 realms.py 的
member_realm_expr(),不要在各发射器中自行拼写策略。
需要强调的是该策略的边界:它只影响绑定层生成代码中异常/错误对象构造与包装所选择的 realm,以及传给实现辅助函数的 realm 参数;它不改变 VM 的当前 realm 或执行上下文,作者脚本始终在其自身 realm 中执行,嵌套脚本的异常抛出行为不受转换块中的 TypeError 域覆盖影响。
小结
Ladybird 的 LibWeb 绑定 realm 策略可以概括为三条主线:实例成员 → 接收者 realm(this_object_realm)、静态/构造/命名空间 → 调用方 realm、[NeedsCallerRealm] 为实例成员的唯一 opt-out,并由 realms.py 的 member_realm_expr() 作为单一生成入口统一贯彻到操作、属性、命名/索引属性与重载仲裁等所有发射器中。配合 this_value_realm() 对 WindowProxy 主世界代理的特殊解析(跟随当前 [[Window]] realm)以及对转换块 TypeError 构造域的窄化限定,LibWeb 在不切换 VM 执行域的前提下,为所有绑定成员提供了一致且可观察的 realm 身份。该策略文档(REALM_POLICY.md)本身即作为 realms.py 内联注释的权威引用,是维护 LibWeb 绑定层时最直接的规范依据。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0622
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00