Deno Node 兼容层深入解析:internal_binding 如何模拟 Node.js 内部 C++ 绑定
Deno 之所以能够直接运行大量为 Node.js 编写的 npm 包,关键在于 ext/node/polyfills/internal_binding/ 目录下的一组"内部绑定(internal bindings)"模拟实现。本文以 internal_binding 目录 README 为核心,结合目录内的 TypeScript 实现、Rust 侧 ops 和消费方 polyfill 源码,讲清楚 Deno 是如何用纯 JS/TS(辅以少量 Rust FFI)复刻 Node.js 源码 src/ 目录中由 C++ 导出的那批内部模块,以及 uv、http_parser 等关键绑定的具体实现细节。
一、什么是 internal bindings:文档给出的核心定义
ext/node/polyfills/internal_binding/README.md 全文很短,但给出了这个目录的准确定位:
The modules in this directory implement (simulate) C++ bindings implemented in the
./src/directory of the Node.js repository. These bindings are created in the Node.js source code by usingNODE_MODULE_CONTEXT_AWARE_INTERNAL.
拆解开有三层含义:
- 来源:Node.js 仓库的
src/目录下存在一批 C++ 模块,它们通过NODE_MODULE_CONTEXT_AWARE_INTERNAL宏注册为"内部绑定",供 Node 自身的 JS 标准库(lib/目录下的内置模块)通过process.binding()/internalBinding()调用,而非暴露给普通用户代码。 - 目的:Node 内置模块(fs、net、tls、crypto、dgram 等)大量依赖这些绑定获取底层能力(libuv 句柄、errno 映射、HTTP 解析器、异步追踪上下文等)。要让这些内置模块在 Deno 里跑起来,就必须把绑定提供的"面(API surface)"复刻出来。
- 方式:由于 Deno 的 Node 兼容层运行在 V8 isolate 中、无法按 Node 的方式加载这套 C++ 扩展,
internal_binding/目录选择用 TypeScript 直接**实现(simulate)**这些模块的对外行为;真正需要 C 能力(如 HTTP 报文解析)的部分,则下沉到 Rust 侧通过 op/FFI 提供。
README 还提示读者参考 Node.js 仓库 src/README.md 了解内部绑定的注册机制——也就是说,本目录是 Node 内部机制的一份"镜像文档",而真正的证据全部落在下面这些源码里。
二、目录结构与绑定清单
ext/node/polyfills/internal_binding/ 目录下共有 30 个 TS/JS 文件,每个文件对应一个(或一组)Node 内部绑定模块:
| 文件 | 对应的 Node 绑定 | 实现形态 |
|---|---|---|
| async_wrap.ts | async_wrap |
异步追踪(AsyncResource)上下文 |
| block_list.ts | block_list |
底层 IP 封禁列表(文件头注释注明 "Mirrors Node's internalBinding('block_list')") |
| buffer.ts | buffer |
Buffer 相关常量与能力 |
| cares_wrap.ts | cares_wrap |
c-ares DNS 解析封装 |
| constants.ts | constants |
UV / UV_UDP 等常量集 |
| crypto.ts | crypto |
底层加密绑定面 |
| http_parser.ts | http_parser |
llhttp 解析器的 JS 门面(Rust 实现在 ext/node/ops/llhttp/binding.rs) |
| http2.ts | http2 |
nghttp2 常量与错误字符串(ext/node/lib.rs 注释提到用于镜像 Node 的 internalBinding('http2').nghttp2ErrorString()) |
| inspector.js | inspector |
Inspector 相关绑定面 |
| pipe_wrap.ts | pipe_wrap |
Unix 管道句柄封装 |
| stream_wrap.ts | stream_wrap |
libuv stream 句柄封装 |
| string_decoder.ts | string_decoder |
字符串解码器 |
| symbols.ts | symbols |
内置模块共享的 Symbol 集 |
| tcp_wrap.ts | tcp_wrap |
TCP 句柄封装 |
| tls_wrap.ts | tls_wrap |
文件头注释注明 "Mirrors Node's internalBinding('tls_wrap').wrap(handle, context, isServer)" |
| tty_wrap.ts | tty_wrap |
TTY 句柄封装 |
| types.ts | types |
基于 core 类型判定的对象类型检测 |
| udp_wrap.ts | udp_wrap |
UDP 句柄封装 |
| util.ts | util |
内部工具函数 |
| uv.ts | uv |
libuv 错误码/常量映射(约 600 行,见下文) |
| ares.ts | ares |
c-ares 常量 |
| 其余 | _libuv_winerror.ts、_listen.ts、_node.ts、_timingSafeEqual.ts、_utils.ts |
带下划线的私有辅助模块 |
| mod.ts | — | 绑定注册表与 getBinding() 入口 |
三、绑定注册表与 getBinding:Node 语义的复刻入口
mod.ts 是整个目录的调度中心。它先用 core.loadExtScript("ext:deno_node/internal_binding/...") 逐个加载上面列出的绑定脚本,再组装成一个 modules 注册表(第 86–144 行):
const modules = {
"async_wrap": asyncWrap,
"block_list": blockList,
buffer,
"cares_wrap": caresWrap,
constants,
crypto,
"http_parser": httpParser,
"http2": http2Binding,
inspector: inspectorBinding,
"pipe_wrap": pipeWrap,
"stream_wrap": streamWrap,
"string_decoder": stringDecoder,
symbols,
"tcp_wrap": tcpWrap,
"tty_wrap": ttyWrap,
types,
"udp_wrap": udpWrap,
util,
uv,
// …其余条目
};
export type BindingName = keyof typeof modules;
export function getBinding(name: BindingName) {
const mod = modules[name];
if (!mod) {
throw new Error(`No such module: ${name}`);
}
return mod;
}
两个值得注意的设计:
- 类型层面的完整性:
BindingName = keyof typeof modules让调用方获得编译期约束;运行时getBinding对未知名字抛出No such module: <name>,与 Node 中internalBinding查询失败的行为保持一致。 - 空对象占位策略:注册表里还有一批值为
{}的条目,如config、contextify、credentials、errors、fs、fs_dir、fs_event_wrap、heap_utils、icu、js_stream、messaging、module_wrap、native_module、natives、options、os、process_methods、report、serdes、signal_wrap、spawn_sync、task_queue、tls_wrap、trace_events、url、v8、worker、zlib。从源码结构看,这些绑定尚未被 Node 兼容层真正需要,Deno 选择注册空对象而非让它们抛错,保证依赖它们存在性的内置模块代码至少可以走到运行时再按需补全。 - 小型内联实现:注册表里还有两处直接内联的极简实现,例如
timers.getLibuvNow()返回MathFloor(performance.now()),用 Web Performance 时间线模拟 libuv 的"当前时刻";performance.observerCounts则是一个长度为 9 的全零数组,按 Node 的 observer 条目类型索引初始化。
四、uv 绑定深读:跨平台 errno 映射与只读常量
uv.ts 是目录中体量最大的文件之一,它对应 Node src/uv.cc 导出的 uv 绑定。文件头部注释交代了移植背景:
In Node these values are coming from libuv…… Since there is no easy way to port code from libuv and these maps are changing very rarely, we simply extract them from Node and store here.
也就是说,Deno 没有把 libuv 的 C 头文件重新编译一遍,而是直接从 Node 运行期抽取了错误码表,以静态数据形式内嵌进 TS 源码。
4.1 五套平台错误码表
文件内依次定义了 codeToErrorWindows、codeToErrorDarwin、codeToErrorLinux、codeToErrorFreebsd、codeToErrorOpenBSD 五份表,每份都是 [errno, [错误名, 描述]] 的三元组数组。同一语义在不同平台上的数值不同,例如 ECONNREFUSED 在 Linux 上是 -111、在 Darwin 上是 -61、在 Windows 上是 -4078;而 EAI_* 系列(c-ares 解析错误)各平台共用 -30xx 段。反向表 errorToCodeXxx 则由 ArrayPrototypeMap 从正向表自动翻转生成,保证两份表永不失配。
4.2 为什么必须用"真 Map"
构造 errorMap / codeMap 时(第 508–541 行)有一个细节非常讲究,源码注释写得很直白:
// Must be a real Map (not SafeMap): it is returned to userland via
// getErrorMap() / process.binding("uv").getErrorMap() and must pass
// `instanceof Map` (SafeMap's prototype chain does not include Map).
// deno-lint-ignore deno-internal/prefer-primordials
const errorMap = new Map<number, [string, string]>(
osType === "windows" ? codeToErrorWindows
: osType === "darwin" ? codeToErrorDarwin
: osType === "linux" ? codeToErrorLinux
: osType === "android" ? codeToErrorLinux
: osType === "freebsd" ? codeToErrorFreebsd
: osType === "openbsd" ? codeToErrorOpenBSD
: unreachable(),
);
Deno 内部代码一般强制使用原型链被切断的 "primordial" 安全对象(SafeMap),但这份表会直接返回给用户代码——Node 生态里存在 errorMap instanceof Map 之类的写法,SafeMap 的原型链不含 Map,会检测失败。因此这里显式豁免 lint 规则(deno-lint-ignore deno-internal/prefer-primordials),改用真 Map。平台选择依据 core.loadExtScript("ext:deno_node/_util/os.ts") 提供的 osType,Android 复用 Linux 表,未知平台直接 unreachable() 抛错。
对外暴露的 API 为 errname(errno)、getErrorMessage(errno)、getErrorMap()、getCodeMap() 以及 mapSysErrnoToUvErrno()——后者在 Windows 上先经 _libuv_winerror.ts 的 uvTranslateSysError 把 Win32 错误码翻译成标准 errno 名再查表,其它平台直接取负。这些函数被 _utils.ts、internal/errors.ts、fs.ts 等内置模块广泛使用,是 Deno 中 Node 风格错误对象(err.code === "ENOENT")背后的数据来源。
4.3 UV_* 常量的"冻结"
mod.ts 在把 uv.ts 的结果挂到注册表前,还做了一次浅拷贝:
// Mutable shallow copy so callers can replace properties (e.g. wrap
// `errname` with a deprecation warning when --pending-deprecation is set).
// Match Node's C++ binding: UV_* error code constants are read-only and
// non-deletable. See `Initialize` in `src/uv.cc`.
const uv: Record<string, unknown> = {};
for (const key of new SafeArrayIterator(ObjectKeys(uvNamespace))) {
const value = (uvNamespace as Record<string, unknown>)[key];
if (StringPrototypeStartsWith(key, "UV_")) {
ObjectDefineProperty(uv, key, {
__proto__: null,
value,
writable: false,
enumerable: true,
configurable: false,
});
} else {
uv[key] = value;
}
}
这里再次体现了"镜像 Node 行为"的原则:Node 的 src/uv.cc::Initialize 把 UV_* 错误码常量定义为只读、不可删除的属性,Deno 就用 ObjectDefineProperty 逐一定义出 writable: false, configurable: false 的属性来复现;而函数类属性(如 errname)保持可写,以便 Node 在 --pending-deprecation 场景下对其打补丁——这段注释直接说明了对齐动机。
五、http_parser 绑定:JS 门面 + Rust/FFI 底座的协作
uv 绑定是纯数据模拟,而 http_parser 绑定展示了另一类实现路径——Rust 侧真正的 FFI 绑定。ext/node/ops/llhttp/binding.rs 的模块注释开门见山:
CppGC-based HTTPParser binding for
internalBinding('http_parser'). This exposes llhttp to JavaScript matching Node.js's nativeHTTPParserclass.
从源码结构看,其协作方式是:
- Rust 侧维护
llhttp_t解析器实例(Inner结构体,含llhttp_settings_t、头部缓冲、max_header_size等状态),并把 JS 回调存为 parser 对象上的索引属性,常量K_ON_MESSAGE_BEGIN=0 … K_ON_EXECUTE=5与 JS 端 http_parser.ts 中的索引一一对应(注释明确"must match the constants in http_parser.ts"); execute()期间llhttp_t.data指向栈上分配的ExecuteContext,持有Inner状态与 v8PinScope的原始指针,C 回调据此同步调用回 JS;- 头部累计到
MAX_HEADER_PAIRS = 32对时先经kOnHeaders回调分批刷回 JS,与 Node 的批量解析行为保持一致("matches Node.js")。
也就是说,internal_binding/http_parser.ts 提供的是 Node 语义下的类与方法签名,重活(逐字节状态机解析)由 Rust + llhttp 完成。类似地,ext/node/lib.rs 中的注释表明 http2 绑定也镜像了 Node internalBinding('http2').nghttp2ErrorString() 一类能力。
六、types 绑定:把 V8 类型判定暴露给 JS
types.ts 是一个小巧而关键的绑定。它从 core(Deno 内核的 JS 侧 API)解构出约 30 个类型判定函数并原样导出:
const { isAnyArrayBuffer, isArgumentsObject, isArrayBuffer, isAsyncFunction,
isBigIntObject, isBooleanObject, isBoxedPrimitive, isDataView, isDate,
isGeneratorFunction, isGeneratorObject, isMap, isMapIterator,
isModuleNamespaceObject, isNativeError, isNumberObject, isPromise,
isProxy, isRegExp, isSet, isSetIterator, isSharedArrayBuffer,
isStringObject, isSymbolObject, isTypedArray, isWeakMap, isWeakSet, } = core;
Node 侧 internalBinding('types') 提供的是 V8 原语级的 IsArrayBuffer、IsPromise 等判定能力,供 assert、buffer 等内置模块做精确的对象类型识别。Deno 直接把内核 core 已具备的同族判定函数转发出去(文件顶部保留了 "Adapted from Node.js" 的署名注释),属于典型的"能力对齐型"绑定。其消费方之一 assert.ts 就在模块头部加载了它。
七、两种消费路径:getBinding 之外的大头
理解 internal_binding/ 目录时容易忽略的一点是:大多数内置模块并不走 getBinding() 注册表,而是用 core.loadExtScript 直接按文件加载绑定脚本。例如:
- _http_common.js 直接
loadExtScript("ext:deno_node/internal_binding/http_parser.ts"); - _tls_wrap.js 依次加载
tcp_wrap.ts、pipe_wrap.ts、tls_wrap.ts、symbols.ts; - dgram.ts 加载
udp_wrap.ts、util.ts、constants.ts; - dns.ts 加载
ares.ts与cares_wrap.ts; - internal/buffer.mjs 加载
string_decoder.ts、buffer.ts、_utils.ts、util.ts; - constants.ts、crypto.ts、_brotli.js、_fs/_fs_constants.ts 等则大量引用
constants.ts与uv.ts中的常量表。
这种"文件级直接引用 + 注册表兜底"的双轨结构可以推断出如下分工:loadExtScript 直接加载服务于性能与模块初始化顺序(内置模块在启动快照中即可依赖具体文件),而 mod.ts 的注册表服务于那些按名字动态查询绑定的第三方/兼容代码路径——即 Node 的 process.binding() / internalBinding() 语义。
后者在 Deno 中由 ext/node/polyfills/internal/test/binding.ts 实现:
const lazyBindingMod = core.createLazyLoader(
"ext:deno_node/internal_binding/mod.ts",
);
function internalBinding(name) {
emitBindingWarning();
return lazyBindingMod().getBinding(name);
}
这里还有两个细节:createLazyLoader 使注册表首次被查询时才真正加载(避免拖慢启动);而每次调用都会触发一次 process.emitWarning("These APIs are for internal testing only. Do not use them.", "internal/test/binding")——这与 Node 自身对该 API 的定位一致:内部测试用,不保证稳定。
八、小结:一份"以文档为纲、以源码为证"的对照表
回到 README 给出的那句核心定义,仓库源码为它提供了完整的实现证据链:
| README 的论断 | 仓库中的证据 |
|---|---|
模拟 Node src/ 中的 C++ 绑定 |
mod.ts 的 modules 注册表 + getBinding,条目名与 Node 内部绑定名一一对应 |
这些绑定由 NODE_MODULE_CONTEXT_AWARE_INTERNAL 创建 |
ext/node/lib.rs、block_list.ts、tls_wrap.ts 等文件注释逐处标注 "Mirrors Node's internalBinding('...')",并在 binding.ts 中保留 internalBinding() 调用面 |
详见 Node src/ 文档 |
uv.ts 头部注释逐条列出移植来源(src/uv.cc、deps/uv),llhttp/binding.rs 注释对齐 Node 的 HTTPParser 类行为 |
适用前提与限制也需要说清:这些绑定服务于 Deno 的 Node 兼容层(内置模块移植与 npm 包运行),internalBinding 本身被官方定位为内部测试 API;注册表中值为 {} 的条目表示对应绑定尚未实现,遇到依赖它们的深层 Node 特性时可能需要回退到 Node 运行环境。对读者而言,想排查某个 Node 内置模块在 Deno 下的兼容性差异,最有效的起点就是按本文第六节的方法,从该内置模块的 polyfill 文件出发,找到它 loadExtScript 的具体 internal_binding/* 文件,再对照 Node 同名绑定核对行为差异。
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