Deno 的 WebIDL 转换层:deno_webidl 扩展如何将 ECMAScript 值转换为 Web 标准类型
deno_webidl 是 Deno 运行时中负责 WebIDL(Web IDL)类型转换的基础设施 crate,它提供了一套从 ECMAScript 值到 WebIDL 类型的转换(conversions)机制,是所有 Web 平台 API(如 fetch、TextDecoder、WebSocket)做参数强制转换(coercion)与品牌校验的底层支撑。读完本篇,你将理解 Deno 如何通过一个纯 JS 扩展(00_webidl.js)实现 WebIDL 转换器注册表、字典/序列/记录/枚举等复合类型转换、接口品牌机制(branding),并能看懂这些转换器在 ext/webidl 中的完整源码脉络与性能优化取舍。
项目定位:WebIDL 转换在 Deno 架构中的位置
WebIDL(Web Interface Definition Language)是 W3C/WHATWG 用来描述 Web API 接口签名的语言。浏览器中"传一个非法参数会抛出特定 TypeError"的行为,本质上就是运行时在执行 WebIDL 规范定义的转换规则。Deno 要让自己的 fetch()、TextDecoder、WebSocket 等内置对象与 Web 标准行为逐字节对齐,就必须有一套等价的转换实现。
ext/webidl/README.md 对该 crate 的定位非常精炼:
This crate implements WebIDL for Deno. It consists of infrastructure to do ECMA -> WebIDL conversions.
从源码头部注释可以看到,这份实现移植自 jsdom 社区的 webidl-conversions(Domenic Denicola 著,BSD-2-Clause 许可),见 00_webidl.js 的前几行注释。移植到 Deno 后还加入了大量针对 Deno 场景的扩展:Node 兼容的错误码、async sequence 支持、面向 hot path 的字典转换优化等。
crate 的 Rust 侧极其轻薄——lib.rs 只有一行核心内容:
deno_core::extension!(deno_webidl, lazy_loaded_js = ["00_webidl.js"],);
这意味着 deno_webidl 是一个典型的 deno_core 扩展:Rust 侧仅负责把 00_webidl.js 作为懒加载脚本(lazy_loaded_js)注册进运行时,所有的类型转换逻辑都以 JavaScript 实现,在运行时首次 loadExtScript 时才求值。整个 crate 的依赖只有 deno_core,另有一个 dev-dependency deno_bench_util 用于基准测试(见 Cargo.toml)。
接入方式:从 JS 与 Rust 两侧加载扩展
README 给出了标准的接入方法,这里完整保留并结合仓库中的真实用法加以说明。
JS 侧:loadExtScript 加载并挂载品牌符号
按照 README 的用法示例:
import { core } from "ext:core/mod.js";
const webidl = core.loadExtScript("ext:deno_webidl/00_webidl.js");
Object.defineProperty(globalThis, webidl.brand, {
value: webidl.brand,
enumerable: false,
configurable: true,
writable: true,
});
core.loadExtScript 会执行 00_webidl.js(一个 IIFE),并返回其返回值——即 assertBranded、converters、createDictionaryConverter、brand 等一整套 API(见 00_webidl.js 末尾的 return 对象)。脚本还通过 __bootstrap 解构拿到 core、internals 与 primordials(第 10 行),全程只使用 Primordials 化的内置方法,避免被用户代码篡改后的 Object、Array 等影响转换正确性——这是 Deno 内置 JS 的通用安全模式。
仓库中多处运行时脚本实际采用相同模式加载该扩展,例如 runtime/js/11_workers.js、runtime/js/97_navigator_user_agent_data.js、runtime/js/98_global_scope_shared.js 中都有:
const webidl = core.loadExtScript("ext:deno_webidl/00_webidl.js");
Rust 侧:注册进 RuntimeOptions
README 同时说明,Rust 侧需要在 RuntimeOptions 的 extensions 字段中提供 deno_webidl::deno_webidl::init()。基准测试代码 benches/dict.rs 展示了这个注册过程的最小可运行形态:
fn setup() -> Vec<Extension> {
deno_core::extension!(
deno_webidl_bench,
esm_entry_point = "ext:deno_webidl_bench/setup.js",
esm = ["ext:deno_webidl_bench/setup.js" = "benches/dict.js"]
);
vec![deno_webidl::deno_webidl::init(), deno_webidl_bench::init()]
}
deno_webidl::deno_webidl::init() 由 deno_core::extension! 宏从 lib.rs 生成。在完整的 Deno 运行时中,该扩展已被编入运行时依赖(runtime/Cargo.toml 声明了 deno_webidl.workspace = true),因此最终用户无需手动接线,只有构建自定义运行时(如 deno compile 或嵌入式场景)时才需要显式注册。
另外,脚本在加载时还会向 internals 挂载一份只读子集(00_webidl.js):
internals.webidlBrand = brand;
internals.webidl = ObjectFreeze({
assertBranded,
configureInterface,
createBranded,
illegalConstructor,
requiredArguments,
converters: ObjectFreeze({
DOMString: converters["DOMString"],
}),
});
注释里说明了动机:deno desktop 的初始化脚本是 post-bootstrap classic script,无法 import 该模块,所以经由 internals 暴露少量工具;同时刻意不暴露完整的 converters 注册表,因为它"是整个进程中所有 web API 参数强制转换的活注册表",暴露出去会允许任何有 internals 访问权的代码改写全部 API 的参数转换行为。这是一个值得注意的安全边界设计。
标量类型转换器:converters 注册表
converters 是一个以 WebIDL 类型名为键的函数注册表(第 319 行 起),完整类型声明见 internal.d.ts。它的调用约定统一为:
converters.<类型名>(v, prefix?, context?, opts?) => 转换后的值
prefix:错误消息前缀,通常是接口/方法名(如"TextDecoder.decode");context:出错时描述出错值位置的上下文(如参数名、成员路径);opts:按类型不同的选项对象(见下文),缺省时使用一个共享的冻结空对象EMPTY_OPTS(第 99-103 行)以避免每次调用分配{ __proto__: null }——因为转换器对opts只读不写,共享冻结对象是安全的。
整数类型:enforceRange 与 clamp
byte、octet、short、unsigned short、long、unsigned long 全部由工厂函数 createIntegerConversion(bitLength, typeOpts) 生成(第 329-341 行),行为遵循 WebIDL 规范的整数转换算法:
- 先做
toNumber(bigint直接抛TypeError: Cannot convert a BigInt value to a number)并把-0规范化为0; opts.enforceRange为真时:值必须有限,取integerPart后若超出[lowerBound, upperBound]抛 TypeError(消息形如is outside the accepted range of -128 to 127, inclusive);opts.clamp为真且值非 NaN 时:先夹取到边界,再用evenRound做"四舍六入五成双"(banker's rounding)取整;- 默认路径:非有限值或 0 直接返回 0;在界内则原样返回;越界则做
modulo(2^bitLength)回绕,符号类型还需要减掉 2^bitLength。
其中 evenRound(第 153-173 行)对 .5 边界值选择偶数整,modulo 则按 ECMA-262 的模定义实现(结果符号与除数一致)。
64 位的 long long / unsigned long long 使用单独的 createLongLongConversion(第 270-317 行),因为它无法用 Math.pow(2, 64) 精确表示:非 enforceRange 路径会先转 BigInt,经 BigIntAsIntN(64, x) / BigIntAsUintN(64, x) 按位截断后再转回 Number。internal.d.ts 中也明确标注了这一点:
Note this is truncated to a JS number (53 bit precision).
所有整数转换器共享的选项(internal.d.ts):
| 选项 | 含义 |
|---|---|
enforceRange |
值超出该类型可接受范围时抛 TypeError |
clamp |
把值夹取(clamp)到可接受范围内再取整 |
浮点、字符串与其他标量
float/double:非有限值抛TypeError: ... is not a finite floating-point value;float会经MathFround压缩到 f32,压缩后溢出仍抛错;unrestricted float/unrestricted double则允许 Infinity 与 NaN 通过;DOMString:字符串原样返回;null在opts.treatNullAsEmptyString时返回"";symbol显式抛TypeError: Cannot convert a Symbol value to a string(注释说明这是为了对齐 Node 与 V8 原生消息);其余值走String(V);ByteString:先按 DOMString 转换,再逐字符检查charCodeAt > 255,含非字节码点抛is not a valid ByteString;USVString:在 DOMString 之上调用StringPrototypeToWellFormed,把孤立代理项替换为 U+FFFD,保证字符串 well-formed;object:WebIDLType(V)不是Object则抛is not an object;- 类型判定函数
type(V)(第 124-151 行)返回Null / Undefined / Boolean / Number / String / Symbol / BigInt / Object八个 WebIDL 语义类型,注意function归入Object。
二进制数据类型:ArrayBuffer 族与 allowShared
ArrayBuffer、DataView、八种 TypedArray(Int8Array 到 Float64Array,Float16Array 留有 TODO)、ArrayBufferView、BufferSource 转换器通过 core.isArrayBuffer、core.isDataView、core.isSharedArrayBuffer、core.isTypedArray 做真实类型判断(第 549-683 行),而非 instanceof,因此跨 realm 的值也能正确识别。
它们的选项(internal.d.ts):
| 选项 | 含义 |
|---|---|
allowShared |
是否允许 SharedArrayBuffer(不仅 ArrayBuffer) |
典型规则:allowShared: false(默认)时,DataView/TypedArray 若 backed by SharedArrayBuffer 会抛 is backed by a SharedArrayBuffer, which is not allowed。错误消息中还附带了 Node 兼容错误码:例如 ArrayBufferView 转换失败时 err.code 为 "ERR_INVALID_ARG_TYPE"(第 613 行),使 node:* 消费方观察到的 err.code 与 Node 行为一致——这是 Deno 做 Node 兼容性的一个缩影。
此外还有两个时间戳别名:DOMTimeStamp 即 unsigned long long,DOMHighResTimeStamp 即 double(第 685-686 行),以及 Function / VoidFunction(都检查 typeof V === "function")和一组常用组合类型的预置转换器,如 converters["sequence<ByteString>"]、converters["record<USVString, USVString>"]、converters["Promise<undefined>"](第 692-730 行)。
复合类型工厂转换器
converters 解决标量问题后,WebIDL 的复合类型(序列、记录、字典、枚举、Promise、async sequence)由一组工厂函数生成,它们的签名同样是 (V, prefix?, context?, opts?) => T,返回的仍是转换器,可无限嵌套组合。完整声明见 internal.d.ts。
可空与序列
createNullableConverter(converter):V 为 null 或 undefined 时返回 null,否则委托内层转换器。对应 WebIDL 的 T?。
createSequenceConverter(converter) 实现 sequence<T>:要求 V 是 Object 且可通过 SymbolIterator 迭代(不要求是数组,任何可迭代对象都行);逐个调用内层转换器,且把索引注入 context(`${context}, index ${array.length}`),所以报错时能定位到"第几个元素"。若迭代器 next() 不返回对象则抛 TypeError。
记录与字典
createRecordConverter(keyConverter, valueConverter) 实现 record<K, V>(第 1205-1243 行):结果对象无原型({ __proto__: null }),遍历 V 的可枚举自有属性,键值各自过转换器。实现区分快慢两条路径:非 Proxy 对象直接 for...in + ObjectHasOwn;Proxy 对象(注释提到来自 WPT 测试场景)走 ReflectOwnKeys + ObjectGetOwnPropertyDescriptor 检查 enumerable。
createDictionaryConverter(name, ...dictionaries) 是整套实现中优化最重的部分,下一节专门展开。
枚举、Promise 与回调
createEnumConverter(name, values):把值强转字符串后查 SafeSet,不在枚举内抛:
The provided value 'xxx' is not a valid enum value of type <name>
并附 err.code = "ERR_INVALID_ARG_VALUE"。
createPromiseConverter(converter):支持 thenable——typeof V?.then === "function" 时先 PromiseResolve(V) 再转换;否则同步转换后包成已 fulfilled 的 Promise。
invokeCallbackFunction(callable, args, thisArg, returnValueConverter, prefix, returnsPromise) 负责回调函数调用:用 ReflectApply 调用户回调、把返回值过 returnValueConverter;若 returnsPromise 为真,异常不直接抛出而是返回 rejected Promise——这正是 WebIDL Promise<T> 返回值的语义(异步方法出错表现为 Promise rejection 而非同步异常)。
async sequence:Deno 特色扩展
WebIDL 规范的 async_sequence<T> 允许同步可迭代对象或异步可迭代对象传给接受流式参数的 API。Deno 的实现包括:
isAsyncSequence(v):用GetMethod语义检查@@asyncIterator或@@iterator是否可用(方法存在但不可调用时抛 TypeError);createAsyncSequenceConverter(converter)(第 1078-1203 行):返回的"async sequence 对象"带有value、object、method、type: "sync" | "async"字段与open(context?)方法;open()按规范执行 "open the async sequence":取得迭代器、必要时用手工实现的%AsyncFromSyncIterator%(createAsyncFromSyncIterator,避免走yield*以保 primordials 安全)包装同步迭代器、并对每个值调用内层转换器;- 结果对象本身实现了
@@asyncIterator,因此可以直接for await (const x of converter(value))消费。
返回对象的形态在 internal.d.ts 中声明为:
interface ConvertedAsyncSequence<V, T> extends AsyncIterable<T> {
value: V;
object: V;
method: (...args: any[]) => any;
type: "sync" | "async";
open(context?: string): AsyncIterableIterator<T>;
}
这一形态(同时暴露 .open() 与 @@asyncIterator)便于既按 WebIDL 规范的 async sequence 抽象消费,又兼容 JS 原生的 for await...of。
字典转换器的热路径优化
benches/dict.js 用一个 TextDecodeOptions 字典(单个 stream: boolean 成员、默认 false)对照了 createDictionaryConverter 生成物与手写转换器 handwrittenConverter 的性能,benches/dict.rs 分别对 undefined 与 {} 两种输入跑了四组基准。这个基准的存在说明字典转换是热点路径,createDictionaryConverter(第 766-905 行)的实现也围绕此做了多层优化:
- 成员表构建期预处理:把多个继承层的成员合并(支持字典继承
...dictionaries)、按 key 排序、预生成每个成员的 context 字符串`'key' of 'DictName',避免热路径上拼接; - 默认值的快慢分类:值为 null/number/boolean/string/bigint/undefined 的默认值在构建期即过转换器求值,存进
primitiveDefaultKeys/primitiveDefaultValues两个平行数组;引用类型默认值则定义成 getter 属性(每次访问都重新生成,防止跨调用共享可变对象),并置nonPrimitiveDefaults非空; - undefined/null 输入的快路径:输入为
undefined或null时(调用方不传 options 对象的常见情况),若没有引用类型默认值,直接循环拷贝两个平行数组到{ __proto__: null },跳过ObjectAssign——后者需要遍历可枚举自有属性并触发 getter;若存在引用类型默认值,则退回ObjectAssign(idlDict, defaultValues)让 getter 触发重建; - 必需成员检查:
V为 nullish 但字典含required成员时抛can not be converted to a dictionary(附ERR_INVALID_ARG_TYPE);普通成员在值为undefined时依次检查required(抛错)或默认值; - 转换结果统一为
{ __proto__: null }的裸对象,成员键经过转换器处理,context 里带上'member' of 'DictName'便于报错定位。
字典成员的形状(internal.d.ts):
type Dictionary = DictionaryMember[];
interface DictionaryMember {
key: string;
converter: (v, prefix?, context?, opts?) => any;
defaultValue?: any;
required?: boolean;
}
品牌机制:assertBranded、createBranded 与接口校验
WebIDL 规定接口实例的方法必须在该接口的实例上调用(this 绑定检查),且接口参数必须检查"是某接口类型"。Deno 用一个全局唯一的品牌符号实现:
const brand = Symbol("[[webidl.brand]]");
createBranded(Type):以Type.prototype创建对象并打上t[brand] = brand(第 1291-1295 行)。所有接口实例都由它构造,这也是 README 用法示例中要把webidl.brand挂到globalThis的原因——脚本内部创建的实例靠它互相识别;assertBranded(self, prototype, interfaceName?):检查prototype是self的原型链成员且self[brand] === brand,否则抛TypeError: Illegal invocation(未提供接口名时)或Value of "this" must be of type <name>,并附err.code = "ERR_INVALID_THIS"——与 Node 的ERR_INVALID_THIS约定一致;createInterfaceConverter(name, prototype):生成检查ObjectPrototypeIsPrototypeOf(prototype, V) && V[brand] === brand的转换器,用于校验"参数必须是某接口实例",失败时抛is not of type <name>。品牌符号的存在使该检查无法被伪造一个同原型链的 plain object 绕过;illegalConstructor():抛TypeError: Illegal constructor(附ERR_ILLEGAL_CONSTRUCTOR),供不可new的接口使用;requiredArguments(length, required, prefix, argNames?):检查实际参数数,提供argNames时生成 Node 风格的The "a" and "b" arguments must be specified(第 732-764 行)。
所有错误统一经 makeException(ErrorType, message, prefix, context, code?) 构造(第 105-115 行),消息格式固定为 `${prefix}: ` + `${context or "Value"}` + ` ` + message,可选 code 用于挂 Node 兼容错误码。
接口整形工具:configureInterface 与 mixinPairIterable
除转换器外,该扩展还提供把 plain JS 对象"整形"为符合 WebIDL 暴露规则的接口的工具:
configureInterface(interface_)(第 1504-1514 行):对构造函数与其 prototype 上的自有属性统一改写描述符——函数值属性变为 enumerable/writable/configurable,accessor 属性变为 enumerable/configurable(constructor 与 prototype 跳过),并把 prototype[Symbol.toStringTag] 设为接口名。这对应 WebIDL 规定"接口的操作必须可枚举,而浏览器环境中的 constructor 等保持原有描述符"。
mixinPairIterable(name, prototype, dataSymbol, keyKey, valueKey)(第 1340-1502 行):为 key/value 对列表结构(典型如 URLSearchParams)混入 entries / keys / values / forEach 与 @@iterator:
- 内部以
{ target, kind, index }记录迭代状态,kind为"key"/"value"/"key+value"; forEach(idlCallback, thisArg = undefined)严格按 WebIDL 语义把回调bind到调用方提供的thisArg(缺省为undefined而非globalThis),回调不是函数时抛The "callback" argument must of type function...(ERR_INVALID_ARG_TYPE);- 迭代器原型上实现了
SymbolFor("Deno.privateCustomInspect"),使util.inspect输出URLSearchParams Iterator { 'a', 'b' }这类 Node 风格的 inspector 输出,长内容按breakLength折行; - 方法刻意使用对象方法简写定义,使函数不暴露自有
prototype属性——注释说明这与 Node 的 class 方法行为一致,否则某些 Node 测试中Object.hasOwn(value, "prototype")断言会失败。
setlikeObjectWrap(objPrototype, readonly)(第 1545-1645 行)则实现 WebIDL 的 setlike 语义:在原型上定义 size、@@iterator、entries/keys/values/forEach/has,非只读时再加 add/delete/clear,全部委托到 this[setlikeInner]() 返回的内部 Set,其中 setlikeInner = SymbolFor("setlike_set")。
类型声明与测试、基准
- internal.d.ts:以
declare module "ext:deno_webidl/00_webidl.js"形式给出整个扩展模块的 TypeScript 声明,覆盖了makeException、converters(含long long53 位精度提示等注释)、各*ConverterOpts接口、字典/枚举/接口/记录/异步序列工厂函数、brand、type()等,是其他内置 JS 对该模块使用时的类型来源; - benches/dict.js + benches/dict.rs:字典转换器相对手写基线(
handwrittenConverter)在undefined/{}输入下的基准,并带一个 sanity check 块验证TextDecodeOptions(undefined)得到{ stream: false }。运行方式为 crate 的标准 cargo bench([dev-dependencies]引入deno_bench_util,[[bench]] name = "dict" harness = false)。
小结
deno_webidl 以极轻的 Rust 扩展壳(一行 deno_core::extension!)加一个约 1700 行的 JS 文件,构成了 Deno 全部 Web API 的类型转换层:converters 注册表负责标量与二进制类型,create*Converter 工厂负责复合类型,brand 体系负责实例身份与 this 校验,configureInterface / mixinPairIterable / setlikeObjectWrap 负责把 JS 实现整形为 WebIDL 暴露形态。理解这一层,就读懂了 Deno 中每一个内置 API 的参数报错消息、错误码与默认值行为从何而来;而 internal.d.ts 中的完整类型声明与 benches/dict.rs 的基准代码,则是继续深入该模块时最直接的入口。
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