workerd 中的 Pyodide:把 Python 运行时“编译进” JavaScript 引擎的构建与加载机制
2026/9/16 19:37:33 网站建设 项目流程

workerd 中的 Pyodide:把 Python 运行时“编译进” JavaScript 引擎的构建与加载机制

【免费下载链接】workerdThe JavaScript / Wasm runtime that powers Cloudflare Workers项目地址: https://gitcode.com/GitHub_Trending/wo/workerd

本文围绕 src/pyodide/README.md 展开,讲解 workerd(驱动 Cloudflare Workers 的 JavaScript / Wasm 运行时)是如何将 Pyodide 打包成 bundle 嵌入自身、如何裁剪 Pyodide 的包加载机制、以及“为什么要这么做、接下来要怎么做”的设计权衡;并结合 src/pyodide/BUILD.bazel、src/workerd/api/pyodide/pyodide.h 与 samples/pyodide/ 的源码和示例,说明从构建产物到运行时加载的完整链路,以及如何在本地跑通一个 Python Worker。

一、这个目录在做什么

README 开篇给出的定位非常直接:src/pyodide/存放“生成被构建进 workerd 的 Pyodide bundle”所需的全部代码。它由三部分协作完成:

  1. src/pyodide/目录本身:包含生成 bundle 的构建代码(Bazel 规则、打包工具、内嵌的 TypeScript/Python 运行时模块)。从源码结构看,该目录还承担运行时层的角色——AGENTS.md 将其描述为 “Python Workers runtime layer”,其中的模块会以pyodide-internal:*的 BUILTIN 模块形式注册。
  2. build/BUILD.pyodide:目前只做一件事——exports_files(glob(["pyodide/*"])),即把 Pyodide 官方发布产物(下载后落盘到build/pyodide/)暴露给 workerd 的构建系统。
  3. src/workerd/api/pyodide/pyodide.h:C++ 侧的接入点,在开启相应 compatibility flag 时,把 Pyodide bundle 加入 module registry,让 V8 isolate 能够加载。

README 同时说明了一个重要前提:该功能需要experimental兼容性开关才能启用。而在当前仓库的示例 samples/pyodide/config.capnp 中,实际使用的是专门的 Python Workers 开关:

const mainWorker :Workerd.Worker = ( modules = [ (name = "worker.py", pythonModule = embed "./worker.py"), ], compatibilityDate = "2023-12-18", compatibilityFlags = ["python_workers", "python_workers_20250116"], );

python_workers是功能总开关,python_workers_20250116是绑定到特定 Python 快照发布(release)的开关。这一点与 src/pyodide/pyodide_extra.capnp 中的 schema 对应:每个PythonSnapshotRelease常量携带pyodide版本号、packages包 bundle 日期、backport回退计数、baselineSnapshotHash(基线内存快照的 sha256)以及flagName/fieldName等字段——也就是说,“开关 → 特定 Pyodide/Python 快照版本”的映射是在构建期生成进 capnp schema 的,而不是运行时动态决定的。

二、为什么要把 Pyodide 编译进 workerd(设计权衡)

README 专门用一节标题提出疑问:“Do we really want to be compiling this stuff into workerd?”(我们真的想把这套东西编译进 workerd 吗?),并给出了明确的结论与理由。这一节值得完整理解,因为它决定了当前架构的取舍:

升级主版本风险高。Pyodide 每升一个大版本,都可能带来脚本层面的破坏:每个 Pyodide 版本都绑定了一个特定的 Python 版本和一份特定的包 lockfile(锁定所有包的版本与依赖信息)。任何在新 Python 或新版包上会出问题的脚本,都会在升级时一起出问题。

因此必须允许用户固定(pin)旧版本。而在开始支持多个 Pyodide 版本之前,合理的做法是:在运行时动态获取用户所需的 Pyodide 版本,而不是把一大堆“可能用不上”的代码都塞进二进制里撑大体积。同理,用户使用的 Python 包将来也大概率需要动态获取。

现状是“最快跑通”的方案。README 明确承认:“The present approach is just the fastest way to get something working.”(当前方案只是最快能跑起来的方式。)换言之,把 Pyodide 静态编入 workerd 是一个有意识的、暂时的权衡:先保证功能可用,再逐步演进到多版本 + 动态下载。

仓库中的构建配置印证了这一演进方向:src/pyodide/BUILD.bazel 顶部从 build/python_metadata.bzl 加载BUNDLE_VERSION_INFO,并用BUNDLE_VERSION_INFO["development"]["real_pyodide_version"]作为开发版本名,通过alias生成pyodide.capnp.binpyodide等重定向目标——“多版本并存、按版本选择 bundle”的框架已经搭好,只是当前还只有一个开发版本在生效。

三、Pyodide bundle 到底装了什么

README 的 “What's happening here?” 一节说明了 Pyodide 官方发行物的组成,以及 workerd 只取其中一部分:

Pyodide 的发行物包括:

  1. 主“emscripten 二进制”:pyodide.asm.jspyodide.asm.wasm
  2. 加载器pyodide.js
  3. Python + Pyodide 标准库python_stdlib.zip
  4. 包 lockfilepyodide-lock.json,包含发布中构建的所有包的版本与依赖信息;
  5. ……其他东西。

workerd 目前只取pyodide.asm.jspyodide.asm.wasmpython_stdlib.zip,并且通过不配置 lockfile 彻底禁用了包加载——因为 workerd 最终需要自己的包管理机制,而且这个机制“很可能发生在配置阶段(configuration time)”。

关于加载器的替换,README 提到src/pyodide/python.js作为pyodide.js的简化替代:它去掉了包加载器和大部分配置,但增加了**内存快照(memory snapshot)**支持。团队希望把内存快照支持上游回 Pyodide,但“维护一个自己的精简加载器未来大概率仍有价值”。需要指出的是,README 中写的python.js在当前的仓库结构里已经演进为 src/pyodide/internal/python.ts——从 AGENTS.md 的组件表可以确认,internal/python.ts是“核心桥:Emscripten 初始化、Pyodide 引导、快照编排”,并配套internal/snapshot.ts(基线/专用快照的收集与恢复)、internal/setupPackages.tsinternal/loadPackage.ts(包挂载、sys.path、vendor 目录)等模块。

3.1 版本与 lockfile 在仓库中的落地

“每个 Pyodide 版本绑定一份 lockfile”这一约束在仓库中体现得非常具体:src/pyodide/python-lock/ 目录按“日期 + 修订号”命名提交了两份 checked-in 的包 lockfile:

  • pyodide-lock_20240829.4.json(2024-08-29 发布,第 4 次修订)
  • pyodide-lock_20250808.json(2025-08-08 发布)

BUILD.bazel 通过exports_files(glob(["python-lock/*.json"]))将它们导出,注释说明其用途是供pyodidemodule extension(build/deps/dep_pyodide.bzl)读取,以便下载 stdlib wheels——“同一 Pyodide 版本 + 不同包修订”的双坐标(pyodide版本号 ×packages日期)与上面PythonSnapshotReleaseschema 的字段一一对应,也解释了为什么 lockfile 文件名要同时携带日期和修订号。

3.2 构建产物如何被“嵌入”

helpers.bzl 中的pyodide_extra()pyodide_static()python_bundles()三个宏是 bundle 构建的核心:

  • python_packages.capnp 定义 stdlib wheel 文件的嵌入 schema,pack_python_packages.py 是一个构建期工具,把 stdlib wheels 解包、编码进一个PythonPackagescapnp 消息,最终嵌入 Pyodide bundle(这一流程在 BUILD.bazel 的注释中被直接写明)。
  • 输出的pyodide.capnp.bin是一个自描述的 capnp 消息;对于无法本地构建它的 runner(例如交叉编译),仓库还提供了一个 alias pyodide.capnp.bin_cross,在prebuilt_binaries_arm64配置下直接重定向到预编译产物bin.arm64/tmp/pyodide/pyodide.capnp.bin.aarch64-linux-gnu

C++ 侧的 pyodide.h 则定义了运行时读取这些嵌入数据的整套接口,可以按职责分为几组:

组件职责
PyodideBundleManager按版本存取 Pyodide bundle 数据(setPyodideBundleData/getPyodideBundle),bundle 以进程级kj::Directory/消息形式持有
EmbeddedPackagesReader从 bundle 中定位嵌入的python_packages数据模块,getFiles()一次性返回文件布局,每个条目携带指向“已解压字节”的ReadOnlyBuffer读取器,避免逐文件的 JS↔C++ 往返拷贝
PyodideMetadataReader向 Python 运行时暴露 worker 元数据:主模块、模块文件内容(getNames/getSizes/read)、Pyodide/包版本、lockfile、内存快照(hasMemorySnapshot/readMemorySnapshot)、compat flags 等
ArtifactBundler+MemorySnapshotResult验证器(validator)在部署验证时收集“初始化完成后的 Python 解释器状态”快照,用于加速冷启动;区分“基线/通用快照”与“专用快照”
DiskCache仅本地开发用:把跨 workerd 重启获取的 wheel 等数据落到磁盘缓存
SimplePythonLimiterPython 专属的启动期 CPU 时限:beginStartup/finishStartup之间超限即抛 TypeError(头文件注释中注明目前采用“启动结束后再检查”的事后策略)
WorkerFatalReporter把 fatal error 上报给 request observer(Runtime Analytics)

此外,pyodide.h 末尾声明了 bundle 完整性校验接口:computePyodideBundleIntegrity计算 subresource-integrity 风格的sha256-<base644>校验和,verifyPyodideBundleIntegrity在运行时校验下载的 Pyodide bundle 与 release 元数据中的integrity字段一致(pyodide_extra.capnpintegrity字段的注释也印证了这一点);只有本地构建的 “dev” bundle 跳过校验,其他 bundle 若缺失预期校验和本身即为错误。这与第二节“将来支持多版本 Pyodide、动态下载”的规划直接衔接——动态下载的前提首先是可校验。

四、从源码结构看运行时加载链路

把上述组件串起来,可以推断出当前 Python Worker 的加载链路(以“从源码结构看”表述,逐环节有文件可依):

  1. 入口:Python 模块不能直接作为 ES6 入口,因此 python-entrypoint.js 作为 ES6 入口被当作用户 bundle 的一部分嵌入。它只做两件事:从pyodide:python-entrypoint-helper(一个 BUILTIN 模块,见 python-entrypoint-helper.ts)引入initPythoncreateImportProxy等,并在设置好“动态 import JS 模块”的代理后才调用initPython()。文件头注释解释了为何要这一层间接:BUILTIN 模块不能导入 INTERNAL 模块、也不能直接看到用户模块,入口文件恰好处于唯一能同时看到两者的位置。
  2. 桥接:internal/python.ts 负责 Emscripten 初始化与 Pyodide 引导,从PyodideMetadataReader读取 worker 模块、从EmbeddedPackagesReader获取 stdlib 文件布局并构建只读文件系统(由 internal/tarfs.ts 提供),再执行用户模块。
  3. 快照:internal/snapshot.ts 完成内存快照的收集与恢复;PyodideMetadataReader携带memorySnapshotArtifactBundler携带 validator 阶段产出的快照,二者共同实现“验证期生成、运行期恢复”的冷启动加速。
  4. 约束:internal/pool/emscriptenSetup.ts 运行在“vanilla V8 isolate”中,不能导入 C++ 扩展模块(见 AGENTS.md 的醒目提示),这是 Emscripten 池化与 workerd 自身 isolate 隔离边界之间的实现约束。

测试方面,AGENTS.md 说明 Python Worker 的测试位于src/workerd/server/tests/python/,由py_wd_test.bzl宏展开:它处理%PYTHON_FEATURE_FLAGS模板替换、多 Pyodide 版本变体、快照的生成/加载,以及每个版本的 compat flag 隔离;测试默认size="enormous",每个测试会按支持的每个 Pyodide 版本生成一个变体。

五、动手:运行 samples/pyodide 示例

README 指引“Seesamples/pyodide/for an example using this in its current state.”,示例只有两个文件,非常小,适合本地快速验证整条链路。

samples/pyodide/config.capnp 声明了一个监听*:8080的 HTTP socket,绑定到名为main的 service;worker 配置如上节所示,pythonModule = embed "./worker.py"表明 workerd 把 Python 源文件作为模块嵌入,compatibilityFlags打开python_workerspython_workers_20250116

samples/pyodide/worker.py 是最小的 Python Worker:

from js import Response from workers import WorkerEntrypoint class Default(WorkerEntrypoint): def fetch(self, request): return Response.new("hello world") def test(): print("Hi there, this is a test")

可以看到两个关键 API:from js import Response(从 JS 侧导入 Web API)与from workers import WorkerEntrypoint(Python SDK 的入口基类,fetch是请求处理入口)。workers这个 Python SDK 包对应 src/pyodide/internal/workers-api/ 下冻结的 SDK 源码(src/workers/__init__.py_workers.py等);AGENTS.md同时说明该 SDK 现已迁至独立的 workers-py 项目并从 PyPI 安装,仓库中保留的是向后兼容版本。

运行方式(需要本地已构建出 workerd 可执行文件,例如通过 Bazel 构建//src/workerd/workerd:workerd目标后):

workerd serve samples/pyodide/config.capnp # 然后 curl http://localhost:8080 # 预期返回:hello world

注意示例配置没有显式写--experimental,因为当前 Python Workers 的开关已由python_workers*系列 compat flag 表达;README 中“需要experimental兼容性开关”的表述反映的是早期阶段的状态,阅读时应以仓库中当前配置为准。

六、下一步:自定义链接 Emscripten 二进制

README 最后一段给出了明确的性能优化方向,同样值得完整保留:

为了降低内存占用和启动时间,我们也希望链接我们自己版本的 Emscripten 二进制pyodide.asm.jspyodide.asm.wasm。由于 Emscripten 动态链接的限制,(Pyodide 官方产物)被静态链接进去的、很少用到的“垃圾”撑大了体积。希望下一个 Pyodide 版本的发布产物中包含所有编进 Pyodide 的静态库。那样我们就能在构建流程里做一个“修改版的最终链接步骤”,丢掉我们不需要的内容。

这段规划的工程含义是:当前 workerd 消费的是 Pyodide 的成品动态链接 wasm,无法裁剪;一旦上游把静态库暴露为发布产物,workerd 就能在 helpers.bzl 的构建规则里介入最终链接,按 Python Worker 实际用到的符号集做静态裁剪。这与第二节“多版本 + 动态获取”的路径互为表里:一个解决“体积/启动”,一个解决“版本/依赖”,最终让编译进二进制的只剩真正需要的部分。

七、小结

回到 src/pyodide/README.md 的全局信息:

  • 它做了什么:把 Pyodide 的 emscripten 二进制 + stdlib 打成一个 capnp bundle 编入 workerd,用自研精简加载器(internal/python.ts一族模块)替代官方pyodide.js,砍掉包加载器并新增内存快照;
  • 它为什么这么做:升级 Pyodide 大版本有破坏性风险,必须支持多版本 pin 与动态获取,而静态编入是“最快跑通”的过渡方案;
  • 它现在长什么样python-lock/双 lockfile、pyodide_extra.capnp的 release 元数据、pyodide.h的 bundle 管理/完整性校验/快照接口,以及 samples/pyodide/ 可运行的最小示例;
  • 它要去哪里:动态下载多版本 Pyodide bundle、配置期包管理、自定义最终链接以裁剪 emscripten 产物。

想继续深入时,建议按顺序阅读:src/pyodide/BUILD.bazel 与 src/pyodide/helpers.bzl(构建与 bundle 装配)、src/pyodide/pyodide_extra.capnp(release 元数据 schema)、src/workerd/api/pyodide/pyodide.h(C++ 运行时接口)、src/pyodide/internal/python.ts(运行时桥接实现),以及src/workerd/server/tests/python/下的测试。

【免费下载链接】workerdThe JavaScript / Wasm runtime that powers Cloudflare Workers项目地址: https://gitcode.com/GitHub_Trending/wo/workerd

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询