首页
/ Deno 的 WebIDL 转换层:deno_webidl 扩展如何将 ECMAScript 值转换为 Web 标准类型

Deno 的 WebIDL 转换层:deno_webidl 扩展如何将 ECMAScript 值转换为 Web 标准类型

2026-09-04 17:03:34作者:昌雅子Ethen

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()TextDecoderWebSocket 等内置对象与 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),并返回其返回值——即 assertBrandedconverterscreateDictionaryConverterbrand 等一整套 API(见 00_webidl.js 末尾的 return 对象)。脚本还通过 __bootstrap 解构拿到 coreinternalsprimordials第 10 行),全程只使用 Primordials 化的内置方法,避免被用户代码篡改后的 ObjectArray 等影响转换正确性——这是 Deno 内置 JS 的通用安全模式。

仓库中多处运行时脚本实际采用相同模式加载该扩展,例如 runtime/js/11_workers.jsruntime/js/97_navigator_user_agent_data.jsruntime/js/98_global_scope_shared.js 中都有:

const webidl = core.loadExtScript("ext:deno_webidl/00_webidl.js");

Rust 侧:注册进 RuntimeOptions

README 同时说明,Rust 侧需要在 RuntimeOptionsextensions 字段中提供 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

byteoctetshortunsigned shortlongunsigned long 全部由工厂函数 createIntegerConversion(bitLength, typeOpts) 生成(第 329-341 行),行为遵循 WebIDL 规范的整数转换算法:

  1. 先做 toNumberbigint 直接抛 TypeError: Cannot convert a BigInt value to a number)并把 -0 规范化为 0
  2. opts.enforceRange 为真时:值必须有限,取 integerPart 后若超出 [lowerBound, upperBound] 抛 TypeError(消息形如 is outside the accepted range of -128 to 127, inclusive);
  3. opts.clamp 为真且值非 NaN 时:先夹取到边界,再用 evenRound 做"四舍六入五成双"(banker's rounding)取整;
  4. 默认路径:非有限值或 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 valuefloat 会经 MathFround 压缩到 f32,压缩后溢出仍抛错;unrestricted float / unrestricted double 则允许 Infinity 与 NaN 通过;
  • DOMString:字符串原样返回;nullopts.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:WebIDL Type(V) 不是 Object 则抛 is not an object
  • 类型判定函数 type(V)第 124-151 行)返回 Null / Undefined / Boolean / Number / String / Symbol / BigInt / Object 八个 WebIDL 语义类型,注意 function 归入 Object

二进制数据类型:ArrayBuffer 族与 allowShared

ArrayBufferDataView、八种 TypedArray(Int8ArrayFloat64ArrayFloat16Array 留有 TODO)、ArrayBufferViewBufferSource 转换器通过 core.isArrayBuffercore.isDataViewcore.isSharedArrayBuffercore.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 兼容性的一个缩影。

此外还有两个时间戳别名:DOMTimeStampunsigned long longDOMHighResTimeStampdouble第 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)Vnullundefined 时返回 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 对象"带有 valueobjectmethodtype: "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 行)的实现也围绕此做了多层优化:

  1. 成员表构建期预处理:把多个继承层的成员合并(支持字典继承 ...dictionaries)、按 key 排序、预生成每个成员的 context 字符串 `'key' of 'DictName',避免热路径上拼接;
  2. 默认值的快慢分类:值为 null/number/boolean/string/bigint/undefined 的默认值在构建期即过转换器求值,存进 primitiveDefaultKeys / primitiveDefaultValues 两个平行数组;引用类型默认值则定义成 getter 属性(每次访问都重新生成,防止跨调用共享可变对象),并置 nonPrimitiveDefaults 非空;
  3. undefined/null 输入的快路径:输入为 undefinednull 时(调用方不传 options 对象的常见情况),若没有引用类型默认值,直接循环拷贝两个平行数组到 { __proto__: null },跳过 ObjectAssign——后者需要遍历可枚举自有属性并触发 getter;若存在引用类型默认值,则退回 ObjectAssign(idlDict, defaultValues) 让 getter 触发重建;
  4. 必需成员检查V 为 nullish 但字典含 required 成员时抛 can not be converted to a dictionary(附 ERR_INVALID_ARG_TYPE);普通成员在值为 undefined 时依次检查 required(抛错)或默认值;
  5. 转换结果统一为 { __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?):检查 prototypeself 的原型链成员且 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/configurableconstructorprototype 跳过),并把 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@@iteratorentries/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 声明,覆盖了 makeExceptionconverters(含 long long 53 位精度提示等注释)、各 *ConverterOpts 接口、字典/枚举/接口/记录/异步序列工厂函数、brandtype() 等,是其他内置 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 的基准代码,则是继续深入该模块时最直接的入口。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
903
1.82 K
docsdocs
暂无描述
Markdown
888
5.78 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
527
590
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.51 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.33 K
1.45 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384
flutter_flutterflutter_flutter
本仓库是 Flutter SDK 与 Flutter Engine 的 OpenHarmony 适配版本,由 CPF-Flutter 团队维护。开发者可使用熟悉的 Flutter 技术栈开发 OpenHarmony 应用,3.35.7 及以后的适配版本可基于本仓库源码构建支持 OpenHarmony 的 Flutter Engine。
Dart
1.17 K
341