SharedArrayBuffer 与跨源隔离

结论:SharedArrayBuffer 只影响「交互式输入」和「Stop」,不是「能不能 Run」的前提。拿不到就自动降级,不再抛 ReferenceError,宿主页永不加头也能正常跑。

1. 它为什么存在

Pyodide 跑在 Web Worker 里,主线程要把用户输入送进去(input()), 还要能异步打断正在执行的脚本(Stop)。这两件事靠共享内存 + Atomics 实现, 而共享内存就是 SharedArrayBuffer(SAB)。

浏览器规定 SAB 只在跨源隔离(cross-origin isolated)的文档里存在。 隔离状态由宿主页自己的响应头决定:

Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: credentialless   # 或 require-corp

onlinepy.com 自身已带齐这两个头,所以独立打开永远是全功能。 但被第三方页面用裸 iframe 嵌入时,iframe 的隔离性服从父文档—— 父页没有这两个头,iframe 里就同样拿不到 SAB。 注意 allow="cross-origin-isolated" 这个 iframe 属性只在父页本身已跨源隔离时才生效,父页没头写它也没用。

2. 两种模式的区别

能力跨源隔离(有 SAB)未隔离(自动降级)
运行代码(Run)✅ 可用✅ 可用
标准输出 / 报错输出✅ 可用✅ 可用
交互式 input()✅ 可用⚠️ 返回空串,脚本继续跑完
Stop(中断执行)✅ 立即中断⚠️ 退化为「终止并重启 worker」
终端标题常规显示显示 non-interactive
控制台报错无无(不再抛 ReferenceError)
宿主页是否需要 COOP/COEP否(页面自身已带)否(自动降级)

一句话:降级后 IDE 依然能跑、能出结果,只是用不了 input() 和 Stop。终端标题会显示 non-interactive(鼠标悬停有说明),输入框不会再出现, 避免「闪一下但没法输入」的假象。

3. 它是怎么降级的

4. 你想怎么办

方案 A:什么也不做。 点 Run 就能跑,读者代码里没有 input() 就完全够用。

方案 B:给宿主页加头,换来完整功能。Netlify 建 public/_headers(与 _redirects 同目录):

/*
  Cross-Origin-Opener-Policy: same-origin
  Cross-Origin-Embedder-Policy: credentialless

注意:加了 COEP 后,页面上所有跨源资源(第三方 CDN、字体、图片)都要带Cross-Origin-Resource-Policy: cross-origin 或 Access-Control-Allow-Origin, 否则会被拦。站点接了第三方统计/广告且不好放行,就别加。

方案 C:免配置全功能。 改用 widget 气泡或 <onlinepy-ide> 自定义元素, 它们的面板本身就是跨源隔离的 iframe,父页零要求,input() 与 Stop 全可用。

<script src="https://onlinepy.com/embed.js"></script>
<onlinepy-ide code="print('Hello, World!')" features="run,output,theme"
  style="width:100%;height:500px"></onlinepy-ide>

5. 实测

把站点跑在一个刻意不返回 COOP/COEP 的本地服务上,复现「父页 + 裸 iframe」场景:

相关源码:src/hooks/usePyodide.ts(能力探测与降级)、public/pyodideWorker.js(非交互式 stdin)、src/components/IDE.tsx(non-interactive 提示)。
另见 首页 的嵌入说明与 Changelog。