Repository navigation
feat: add GoJS wasm_exec.js host - #3030
Conversation
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
Code review: GoJS wasm_exec loader
This cleanly splits the GoJS WebAssembly host into a stable checked-in loader (targets/wasm_exec.js) and the emcc-emitted implementation (wasm_exec_app.js), moving the browser fs/process/path fallbacks out of host.c. The needsGoJSLoader predicate is well-guarded and thoroughly table-tested, the crosscompile LDFLAGS are clearly justified, and docs/comments are accurate. Security and documentation passes found no issues.
A few points below are worth addressing before merge. The most important is that the post-link rename of the driver output breaks artifact reporting for GoJS builds.
Artifact reporting is not aware of the GoJS rename (important — out of diff)
publishGoJSLoader renames the emcc driver output (*.mjs) to wasm_exec_app.js, then copies in wasm_exec.js. But CollectArtifacts runs after executeMainLink and still looks for the original .mjs:
internal/build/artifact_report.go:109-117statsemscriptenDriverOutput(conf, out.Out)(the.mjs). For a GoJS-o app.wasmbuild that.mjshas been renamed away, soos.Stathits theIsNotExistbranch and the glue is silently dropped — and the two sidecars the user actually needs to deploy (wasm_exec_app.js,wasm_exec.js) are never reported.- For a GoJS
-o app.mjsbuild,out.Outitself is the renamed-away.mjs, so the primaryadd(...)atinternal/build/artifact_report.go:103wouldos.Stata missing file and return a hardstat ... artifacterror when artifact/size reporting is active.
The Emscripten browser-host sidecar already has explicit report wiring (artifact_report.go:128-134); the new GoJS loader/implementation sidecars need equivalent handling so the rename doesn't drop or mis-report deployment artifacts. Please add a focused test for the GoJS reporting path once fixed.
Test coverage of the publish behavior
The three new tests only assert on static wasm_exec.js string contents and the pure path/predicate helpers. None exercise publishGoJSLoader's actual side effects (rename *.mjs → wasm_exec_app.js, copy the loader, no-op when needsGoJSLoader is false, missing LLGO_ROOT error). The sibling publishEmscriptenBrowserHostFrom threads root string as a parameter precisely so the copy is unit-testable without mutating the process environment — extracting an equivalent publishGoJSLoaderFrom(..., root string) seam (see the inline note at gojs_loader.go:51) would let a table test cover these paths, which CLAUDE.md's "features MUST include tests" expects.
Minor
targets/wasm_exec.js:95—loadSidecarusesdocument.write, which is ignored (or re-parses the document) whenwasm_exec.jsis loadedasync/deferor injected after parse. It's a one-time startup path anddocument.currentScriptis checked, so this is low severity, but adocument.head-appended<script>with anonloadgate, or at least a comment thatwasm_exec.jsmust be a parser-blocking classic script, would be more robust.
No separate inline comment is duplicated here; see the inline notes for the remaining points.
6805e2f to
a756192
Compare
LLGo WebAssembly build benchmarks
WebAssembly output sizes
LLGo WebAssembly build measurements
Compared with |
a756192 to
c43506d
Compare
|
All confirmed. This is managed mode, so I return the re-review as my final Markdown message (the platform publishes it). I will not create a PR review or GitHub comment, and I'll keep a clean final summary without a spinner, job link, or completion marker. Re-review: PR #3030 — GoJS wasm_exec loaderI re-reviewed the updated PR (head All three findings from my previous review are addressed and the threads are resolved:
The copy-then-remove ordering (keep the linked artifact until both sidecars succeed) and the artifact-report wiring for the GoJS sidecars both look correct. Security and documentation passes found no issues — the new sync-XHR+ A few minor, non-blocking polish items remain:
Note: I couldn't execute the test suite — this review environment lacks the LLVM/Emscripten toolchain Nice work on the revision — the collision handling and failure-ordering are notably more robust than the first pass. None of the above blocks merge. |
b2a9ae3 to
bad0466
Compare
| this._module = await this._factoryPromise; | ||
| this._module.callMain(this.argv.slice(1)); | ||
| return 0; |
There was a problem hiding this comment.
[P1] 等待 Go 程序真正退出后再完成 run()
callMain() 在 Asyncify 遇到 time.Sleep、定时器或异步 JS 回调时会先返回,不能用它的首次返回作为程序结束信号。使用实际构建的 Wasm,在 Node 和 Chrome 中运行 println("started"); time.Sleep(100*time.Millisecond); println("finished"),观察到 started → await go.run(instance) 返回 → finished;官方 Go 则在 finished 后才完成 run()。这会让调用方提前释放资源或开始下一轮执行。请将 run() 的 Promise 接到真实退出通知,并增加包含异步挂起的执行测试;仅把这里改成 await callMain(...) 也无法等待 Asyncify 的后续恢复。
There was a problem hiding this comment.
已修复。run() 在 callMain 返回后若 Asyncify 仍挂起会等到 currData 清空,再完成 Promise。TestGoJSLoaderJavaScript 里加了模拟 Sleep 的执行测试。
There was a problem hiding this comment.
已复核 652e6d458。使用最新编译器生成真实 Wasm,Node 和 Chrome 均观察到 started → finished → exit(0) → await go.run 完成,此项已修复。Asyncify/stackAlloc 也确实由当前 EXPORT_ALL 配置暴露。
| onExit: code => { | ||
| this.exit(code); | ||
| }, |
There was a problem hiding this comment.
[P2] 补齐 main 正常返回时的 go.exit(0) 通知
这里仅转发 Emscripten 的 onExit,但默认生成的 glue 中 noExitRuntime=true,正常返回的 main 不会触发该钩子;run() 的成功路径也没有调用 this.exit(0)。独立于异步提前返回问题,用一个完全同步、只打印 os.Args 后返回的程序,在 Node/Chrome 中设置 go.exit = code => records.push(code),执行完后 records 仍为空,官方 Go 会收到一次 0。依赖此回调收集退出状态或释放资源的宿主因此无法正常收尾。请覆盖同步正常返回、异步正常结束和显式 os.Exit,确保每条路径恰好通知一次。
There was a problem hiding this comment.
已修复。同步正常返回、Asyncify 结束后的正常返回,以及 ExitStatus(os.Exit)都走 _notifyExit,每种路径只通知一次 go.exit。
There was a problem hiding this comment.
已复核 652e6d458。真实产物的同步正常返回、Sleep 后正常结束、Sleep 后 os.Exit(7)、Sleep 后 panic 分别收到一次退出回调,退出码为 0/0/7/2,此项已修复。
| if (this.exited) { | ||
| this._resolveExitPromise(); | ||
| this._module = await this._factoryPromise; | ||
| this._module.callMain(this.argv.slice(1)); |
There was a problem hiding this comment.
[P2] 保留调用方设置的 argv[0]
this.argv.slice(1) 会丢掉调用方指定的程序名,而 Emscripten 的 callMain() 会自行在前面补入默认 thisProgram。实测在 new Go() 后设置 go.argv = ["custom-name", "hello"],Go 中读到的 os.Args 是 ["./this.program", "hello"],而不是官方 Go 的 ["custom-name", "hello"]。这会影响基于 os.Args[0] 的命令分发和程序名解析。请同时将首参数传递给 Emscripten 的程序名配置,并补一个实际执行的参数传递测试。
There was a problem hiding this comment.
已修复。改为用 _main(argc, argv) 传入完整 go.argv,不再丢掉 argv[0]。TestGoJSLoaderJavaScript 覆盖了 ["custom-name","hello"]。
There was a problem hiding this comment.
已复核 652e6d458。真实 Wasm 在 Node 和 Chrome 中均将 ["custom-name", "hello"] 完整传递到 os.Args,此项已修复。
| func goJSImplementationSidecar(output string) string { | ||
| return filepath.Join(filepath.Dir(output), goJSImplementationScriptName) |
There was a problem hiding this comment.
[P1] 避免不同 Wasm 程序覆盖同一个实现 sidecar
实现文件只取输出目录,所有程序都会发布到同一个 wasm_exec_app.js,但这份 glue 包含各自 Wasm 专用的 import/export 映射,不能像官方 wasm_exec.js 一样共用。实测依次构建 -o dist/first.wasm(读取 os.Args 的程序)和 -o dist/second.wasm(含 time.Sleep 的程序),两个构建都成功,但第二次会覆盖第一份 glue;随后用该目录的加载器实例化 first.wasm,直接得到 LinkError: Import #24 "a" "y": function import requires a callable。恢复第一份 glue 后同一 Wasm 又能正常运行。请给每个程序保留对应的实现文件并让加载器选择,或在确实只能一目录一程序时明确检测并拒绝跨程序覆盖;目前会静默破坏此前成功构建的产物,多 main 包输出到同一目录时也存在同样问题。
There was a problem hiding this comment.
已修复。单程序只写带 // llgo-wasm: 标记的 wasm_exec_app.js。同名重编覆盖该文件;同目录第二份程序不覆盖,改为写 stem.exec.js。加载第二份程序时请显式指定 glue,例如 new Go({ glue: "other.exec.js" }) 或先 <script src="other.exec.js">。
There was a problem hiding this comment.
已复核 652e6d458,同目录顺序构建两个新程序后,第一份 shared glue 得以保留,第二份 second.exec.js 预加载后通过 new Go() 可以运行。另在新版代码上提交了两条具体反馈,分别涉及无标记旧产物的原地重编仍默认选错 glue,以及显式 new Go({ glue }) 在预加载完成后仍无条件抛错。
cpunion
left a comment
There was a problem hiding this comment.
继续检查延迟加载分支,补充一个已在真实 Chrome 中验证的 CSP 兼容性问题。
| if (xhr.status !== 0 && (xhr.status < 200 || xhr.status >= 300)) { | ||
| throw new Error(`LLGo: failed to load ${name}: HTTP ${xhr.status}`); | ||
| } | ||
| (0, eval)(xhr.responseText); |
There was a problem hiding this comment.
[P2] 延迟加载不要依赖 CSP 禁止的字符串 eval
使用 defer/async 或解析后注入加载器时,这个分支会对配套脚本执行间接 eval,因此常见的 CSP script-src 'self' 'wasm-unsafe-eval' 仍会阻止初始化:wasm-unsafe-eval 只允许 Wasm 编译,不允许 JS 字符串执行。已在真实 Chrome 中复现:同源 <script defer src="wasm_exec.js"> 抛出 EvalError,后续 Go 构造器没有注册;相同产物不设 CSP 时能启动,使用普通解析期 script 标签时也能在同一 CSP 下启动。官方 Go 加载器不要求放开 'unsafe-eval'。请通过外部 script 的加载完成信号衔接初始化(或提供明确的预加载 API),避免让本 PR 新增的延迟加载路径要求放宽站点 CSP,并增加实际浏览器的 CSP 用例。
There was a problem hiding this comment.
已修复。动态加载改为插入经典 <script>,不再 eval。解析后注入 wasm_exec.js 时请先 await Go.ready 再 new Go()。测试断言加载器源码不含 eval(。
There was a problem hiding this comment.
已复核 652e6d458。真实 Chrome 中使用 defer 加载、await Go.ready 和 script-src 'self' 'wasm-unsafe-eval' 可以启动并正常退出,eval 导致的 CSP 问题已修复。
LLGo baseline benchmarks
String equality benchmarks
Program measurements
Core language and compiler benchmarks
Timer runtime benchmarks
Compared with |
| owner := goJSWasmMarker(existing) | ||
| if owner == goJSWasmModuleName(ctx, driverOutput) { | ||
| return shared, nil | ||
| } | ||
| return unique, nil |
There was a problem hiding this comment.
[P1] 处理上一版无标记 glue 的原地重编路径
bad04663f 及更早版本生成的 wasm_exec_app.js 没有 // llgo-wasm: 标记,所以升级后在原目录重编同一个 app.wasm 时,这里会保留旧 shared 文件,把新 glue 写到 app.exec.js。但新版默认加载器仍请求 wasm_exec_app.js,构建成功后会把新 Wasm 和旧 glue 配对。已用上一版的真实产物复现,修改程序后用 652e6d458 重编,默认启动报 RuntimeError: function signature mismatch,手动加载新生成的 app.exec.js 后同一份 Wasm 能正常运行。请让该迁移路径使用正确的新 glue,或者在无法判断旧文件归属时给出明确诊断,避免一次正常的原地重编留下默认无法启动的产物。现有 legacy 单测只检查旧文件保留和新文件生成,没有验证默认加载入口。
There was a problem hiding this comment.
已修复。没有 // llgo-wasm: 标记的旧 wasm_exec_app.js 会按当前程序覆盖默认入口,不再把新 glue 写到默认加载器读不到的 app.exec.js。同名重编后 new Go() 使用新 glue。
There was a problem hiding this comment.
已复核 d20b56984。用上一版没有标记的真实 glue 作为旧产物原地重编,默认 wasm_exec_app.js 被更新,new Go() 能启动新 Wasm 并正常结束,此项已修复。
| if (options && options.glue) { | ||
| const pending = loadSidecar(glue); | ||
| if (pending && typeof pending.then === "function") { | ||
| throw new Error("LLGo: preload " + glue + " before new Go({ glue })"); | ||
| } |
There was a problem hiding this comment.
[P2] 允许 glue 已预加载时成功构造 Go
传入 options.glue 后总会再次调用 loadSidecar(),而页面解析完成时它总是返回 Promise,因此这里必然抛出预加载错误,即使指定脚本已经成功加载。已在真实 Chrome 中依次执行 await Go.ready、通过 script.onload 等待 second.exec.js 加载完成、new Go({glue: "second.exec.js"}),仍得到 LLGo: preload second.exec.js before new Go({ glue })。同一页面将构造调用改成 new Go() 就能运行第二个程序。Node 中即使先将该 glue 注册为 globalThis.LLGoModule,带 options 的调用也会进入浏览器 URL 解析并报错。请记录预加载完成的 glue 或提供可等待的加载接口,让回复中新增的显式 glue 用法在预加载后能成功使用。
There was a problem hiding this comment.
已修复。会记录已加载的 glue,并检查页面上已有的 script 标签。预加载 second.exec.js 后 new Go({ glue: "second.exec.js" }) 不再抛错。另提供 Go.loadGlue(name)。Node 里若已有 LLGoModule 也不会再走浏览器 URL。
There was a problem hiding this comment.
已复核 d20b56984。真实 Chrome 中 await Go.loadGlue("second.exec.js") 后,new Go({glue: "second.exec.js"}) 能运行第二个程序,此项已修复。另补充了多程序切换与同一 glue 并发加载的独立问题。
There was a problem hiding this comment.
f1fbbaaee 的 Go.loadGlue 预加载入口验证通过,但外部 script 预加载后显式 new Go({glue}) 的用例再次失败,已在最新构造器代码旁补充 inline。最新测试只覆盖 Go.loadGlue 入口,需要保留外部预加载路径的回归用例。
| if err := tmp.Close(); err != nil { | ||
| return err | ||
| } | ||
| return os.Rename(tmpName, dst) |
There was a problem hiding this comment.
[P2] 发布实现脚本前设置可部署的读取权限
新 helper 用 os.CreateTemp 创建的文件默认是 0600,这里直接 rename,缺少原 copyFileAtomic 中的 tmp.Chmod(0o644)。实际用 652e6d458 构建后,wasm_exec.js 为 0644,但 wasm_exec_app.js 和第二个程序的 second.exec.js 都是 0600。因此将构建目录直接交给以其他用户运行的 nginx/静态服务器,或者通过保留权限的发布工具部署时,必要的实现脚本无法被服务器读取。请在发布前设置与其他 JS 部署产物一致的权限,并在发布测试中检查文件模式。
There was a problem hiding this comment.
已修复。writeFileAtomic 在 rename 前 Chmod(0o644),发布测试会检查实现脚本权限。
There was a problem hiding this comment.
已复核 d20b56984。实际构建得到的 wasm_exec_app.js 和 second.exec.js 权限均为 0644,此项已修复。新增的权限测试存在 Windows 可移植性问题,已在对应测试行单独反馈。
652e6d4 to
d20b569
Compare
| for (let i = 0; i < scripts.length; i++) { | ||
| const src = scripts[i].src || ""; | ||
| if (src === url || src.endsWith("/" + name)) { | ||
| return typeof global.LLGoModule === "function"; |
There was a problem hiding this comment.
[P1] 为指定 glue 恢复对应的模块工厂
页面上存在某个 script 标签,只能说明该脚本曾经被加载。这里返回 true 后,构造器仍读取唯一的 globalThis.LLGoModule,它可能已被另一个程序覆盖。已用本提交真实生成的 first/second 两份 Wasm 在 Chrome 中复现,先等待默认 glue,再 await Go.loadGlue("second.exec.js"),随后 await Go.loadGlue("wasm_exec_app.js") 和 new Go({glue: "wasm_exec_app.js"}),全局工厂仍是 second 的工厂,实例化 first.wasm 报 LinkError: Import #24 "a" "y": function import requires a callable。请按 glue URL 保存和选择对应 factory,或重新执行所请求的脚本,使已经加载过多个程序后的显式选择也能匹配正确的 Wasm。
There was a problem hiding this comment.
已修复。按 glue URL 保存 LLGoModule 工厂。Go.loadGlue("second.exec.js") 后再 Go.loadGlue("wasm_exec_app.js") 会切回第一份工厂,new Go({ glue }) 使用对应模块。
There was a problem hiding this comment.
已复核 f1fbbaaee。真实 Chrome 用例中,加载 second 后再选择默认 glue,第一份工厂得到恢复,first.wasm 可正常运行,此项已修复。
| function loadSidecar(name) { | ||
| if (glueAlreadyLoaded(name)) { | ||
| markGlueLoaded(name); | ||
| return; |
There was a problem hiding this comment.
[P2] 重复加载同一 glue 时等待正在进行的请求
第一次 Go.loadGlue(name) 会立即把 script 插入 DOM,第二次调用因此被 glueAlreadyLoaded() 判定为成功,并在 onload 之前返回。若默认程序的 LLGoModule 已存在,两个消费者同时请求第二份 glue 时,后一个消费者的 await 会提前完成并拿到旧工厂。已在真实 Chrome 中将 second.exec.js 的响应延迟 500ms,连续发起两个 Go.loadGlue("second.exec.js?slow=1"),等待第二次调用后第一请求仍未完成,LLGoModule 也仍是 first 的工厂。请缓存每个 URL 正在加载的 Promise,并让所有调用等待相同的成功或失败结果,只在实际执行成功后标记已加载。
There was a problem hiding this comment.
已修复。同一 glue URL 的 Go.loadGlue 共用一个 in-flight Promise,只在脚本 onload 成功后登记工厂。并发两次加载只会插入一个 script。
There was a problem hiding this comment.
已复核 f1fbbaaee。将同一 glue 响应延迟 500ms 并连续加载两次,第二次 await 会等到原请求完成,共用正确的工厂,此项已修复。
| if perm := info.Mode().Perm(); perm != 0o644 { | ||
| t.Fatalf("mode = %o, want 644", perm) |
There was a problem hiding this comment.
[P2] 按平台限制 POSIX 权限断言
这个无条件的 Perm() == 0644 检查,以及 TestPublishGoJSLoaderFrom 中新增的相同检查,会导致 Windows 上的 internal/build 测试失败。Go 在 Windows 上只用 Chmod 的 0200 位控制只读属性,可写普通文件的 Stat().Mode().Perm() 返回 0666,因此即使实现正确执行 Chmod(0644),也无法满足此断言。后面的 Mkdir(0500) 不可写目录测试同样依赖 POSIX 权限,Windows 的 Mkdir 忽略该 mode,以 root 运行时也不能据此保证创建文件失败。请给这些断言加适用平台和权限条件,保留其他平台的文件内容/发布行为测试。本项已核对 Go 的 Windows 实现,当前未在 Windows 执行测试。Go Windows Mode 实现,Chmod 文档。
There was a problem hiding this comment.
已修复。Perm() == 0644 和不可写目录测试仅在非 Windows 上断言。
There was a problem hiding this comment.
已复核 f1fbbaaee,两处 0644 断言和不可写目录用例都已排除 Windows,Windows 平台断言问题已修复。本次仅作源码检查,本机相关测试通过,未在 Windows 实机运行。
| if (blocking) { | ||
| document.write(`<script src="${source}"></script>`); | ||
| if (!captureFactory(name)) { | ||
| throw new Error(`LLGo: ${name} did not register LLGoModule`); |
There was a problem hiding this comment.
[P1] 等待 document.write 插入的脚本执行后再捕获工厂
普通解析期 <script src="wasm_exec.js"></script> 会进入这个分支。document.write 只是把子 script 插入解析流,该脚本会在当前 wasm_exec.js 执行结束后才运行,因此紧接着的 captureFactory 还看不到 LLGoModule,并在 Go 类注册前抛错。已将本提交的加载脚本与真实 Wasm/glue 放入普通静态页面,在 Chrome 复现 LLGo: wasm_exec_app.js did not register LLGoModule,随后 typeof Go 为 undefined。相同产物改用 defer 加载并等待 Go.ready 后能启动。若已有另一个工厂,这里还可能把旧工厂登记到新 URL。请在子脚本实际执行完成后捕获工厂,并增加普通解析期 script 标签的浏览器用例,保留默认的 Go 兼容加载入口。
There was a problem hiding this comment.
新合并实现 e30a1865a 已通过真实 Chrome 复核。普通解析期 script 标签加载后即可构造 new Go(),动态插入并等待 script.onload 后也能构造并运行,此前的 document.write 时序问题已解决。
| const glue = requestedGlue(options); | ||
| const pending = loadSidecar(glue); | ||
| if (pending && typeof pending.then === "function") { | ||
| if (options && options.glue) { | ||
| throw new Error("LLGo: preload " + glue + " before new Go({ glue })"); |
There was a problem hiding this comment.
[P2] 保留外部 script 预加载 glue 的构造路径
这次去掉页面 script 检查后,只有经过 Go.loadGlue 的模块会进入 factories 缓存。通过 <script src="second.exec.js"> 或自行插入 script 并等待 onload 预加载后,new Go({glue: "second.exec.js"}) 仍会重新注入脚本并抛出 preload 错误。已在 Chrome 中确认 second.exec.js 的外部 script 执行完成,等待 Go.ready 后显式构造仍报 LLGo: preload second.exec.js before new Go({ glue }),使用 Go.loadGlue 预加载的对应测试则通过。这使上一轮已经修复的外部预加载方式再次失效。请让外部预加载也能准确登记 URL 与工厂的对应关系,并分别覆盖 Go.loadGlue 和外部 script 两种入口。
There was a problem hiding this comment.
新实现 e30a1865a 已改为每个程序的合并加载文件。真实 Chrome 中加载两份 host 后,new Go({wasm: "first.wasm"})、new Go({wasm: "second.wasm"}) 和 new Go() 均可运行,动态注入两份 host 也通过。此前 sidecar 预加载对应的代码路径已由新设计替代。
Publish a single wasm_exec.js for standalone GOOS=js GOARCH=wasm builds by concatenating the Emscripten factory with the Go-compatible host. new Go() works immediately after the script loads. Keep wasm_exec.js for the first program, emit stem_wasm_exec.js when another wasm already owns that file, and route print/printErr through fs.writeSync.
f1fbbaa to
7c7a46b
Compare
Register published hosts by wasm name. new Go() uses the last factory;
new Go({ wasm: "app.wasm" }) selects one, and a later host script
does not redefine Go. Trim redundant tests and update comments.
| func goJSWasmModuleName(ctx *context, driverOutput string) string { | ||
| if ctx != nil && ctx.buildConf != nil { | ||
| if out := ctx.buildConf.OutFile; strings.HasSuffix(out, ".wasm") { | ||
| return filepath.Base(out) | ||
| } | ||
| } | ||
| return strings.TrimSuffix(filepath.Base(driverOutput), filepath.Ext(driverOutput)) + ".wasm" |
There was a problem hiding this comment.
[P2] 避免不同目录的同名 Wasm 覆盖注册项
程序身份被 filepath.Base 截成文件名,随后用同一个全局 __llgoFactories[name] 注册。一个项目若分别构建 a/app.wasm 和 b/app.wasm,再加载 a/wasm_exec.js 与 b/wasm_exec.js,第二份会静默覆盖第一份 app.wasm 的 factory。已用本提交的真实产物在 Chrome 复现,注册表只剩 app.wasm,new Go({wasm: "a/app.wasm"}) 和 b/app.wasm 都报 no factory registered,而用 app.wasm 选择后实例化 a/app.wasm 会报 WebAssembly.LinkError。这样两份产物单独可运行,放到同一项目后第一份就无法再选中。请以解析后的 Wasm URL 或另一种不会冲突的程序身份注册,并在无歧义时兼容裸文件名选择,增加不同目录同名文件的端到端用例。
There was a problem hiding this comment.
已修复。factory 同时按裸文件名和由 host 脚本解析出的 Wasm URL 注册。new Go({ wasm: "app.wasm" }) 在无歧义时仍可用;不同目录的同名文件用 URL 或相对路径(如 a/app.wasm)选择。
There was a problem hiding this comment.
复核 9abd62446,a/app.wasm、b/app.wasm 和完整 URL 均能选择正确工厂,这部分已修复。但同名问题仍有裸路径分支未解决。真实 Chrome 中,在 /a/index.html 加载 a、b 两份 host 后,new Go({wasm: "app.wasm"}) 直接取被 b 覆盖的裸名注册项,而 fetch("app.wasm") 明确请求 /a/app.wasm,最终仍报 WebAssembly.LinkError。原因是 targets/wasm_exec.js:105-106 在 URL 解析之前就返回 factories[name]。请优先按调用上下文解析 URL,再在唯一时使用裸名别名,避免同名加载顺序改变显式选择的结果。当前单测反而断言裸名在冲突后返回 b,需要补充页面位于 a/ 的实际入口用例。
There was a problem hiding this comment.
已修复。先按页面 URL 解析 wasm 名,再在唯一时使用裸名。/a/index.html 上 new Go({ wasm: "app.wasm" }) 会选中 /a/app.wasm,不再被后加载的同名 factory 覆盖。
There was a problem hiding this comment.
已在 17b1ae2 验证本条及后续页面 URL 优先级问题已修复:页面位于 a/ 时,new Go({wasm:"app.wasm"}) 能选中 a/app.wasm,不会被后加载的 b/app.wasm 覆盖;从上级页面传入 a/app.wasm、b/app.wasm 或完整 URL 也均可正确运行。页面目录没有对应目标而裸文件名存在歧义时,会明确报错。浏览器多实例并行及动态脚本加载回归通过。
| func goJSRegisterFactory(wasmName string) []byte { | ||
| name, _ := json.Marshal(wasmName) | ||
| return []byte("globalThis.__llgoFactories = globalThis.__llgoFactories || {};\nif (typeof globalThis.LLGoModule === \"function\") globalThis.__llgoFactories[" + string(name) + "] = globalThis.LLGoModule;\n") |
There was a problem hiding this comment.
[P2] 从合并文件的实际作用域注册 factory,支持 CommonJS 加载
真实 emcc 输出使用 var LLGoModule = ...。通过 Node 的 require 加载合并脚本时,该变量属于 CommonJS 模块作用域,globalThis.LLGoModule 并不存在,所以这里不会注册任何 factory,默认构造也找不到它。已直接执行 require("./wasm_exec.js"); new Go(),得到 missing the bundled LLGoModule factory,new Go({wasm: "first.wasm"}) 则报 no factory registered。require 的返回值实际是 factory,说明合并文件中已有正确工厂,只是注册时读错作用域。浏览器经典 script 与 vm.runInThisContext 用例可以通过,因此现有 VM/mock 测试未覆盖该问题。请显式注册词法作用域中的 factory,并同步维护 new Go() 所用的默认工厂引用,补一个真实合并产物通过 require 加载的 Node 用例。
There was a problem hiding this comment.
已修复。注册改为读取词法作用域中的 LLGoModule,并写回 globalThis.LLGoModule 作为 new Go() 的默认 factory。Node 的 require("./wasm_exec.js") 后可以直接 new Go()。
There was a problem hiding this comment.
已复核 9abd62446,直接 require 两份真实合并脚本后,new Go() 和按完整文件 URL 选择的实例均可启动并正常结束,CommonJS 工厂注册问题已修复。另发现带 ./ 的 Node 相对路径选择问题,已在最新选择器代码旁补充 inline。
| if driverOut := emscriptenDriverOutput(conf, out.Out); needsGoJSLoader(conf, nil, driverOut) { | ||
| if err := add(ArtifactRoleDeployment, "javascript", goJSLoaderSidecar(driverOut)); err != nil { | ||
| return nil, err | ||
| } | ||
| unique := goJSUniqueLoaderSidecar(driverOut) | ||
| if _, err := os.Stat(unique); err == nil { | ||
| if err := add(ArtifactRoleDeployment, "javascript", unique); err != nil { | ||
| return nil, err |
There was a problem hiding this comment.
[P2] 按当前 Wasm 的归属报告合并加载器
第二个程序已经有独立的 second_wasm_exec.js,仍无条件把 wasm_exec.js 作为必需产物加入,但后者属于第一个程序。实际依次构建 first.wasm、second.wasm 后,CollectArtifacts(second) 同时报告 first 的 wasm_exec.js(98223 字节)和 second_wasm_exec.js(91700 字节),会把另一个程序的完整 factory 算入 second 的部署体积。进一步仅复制 second.wasm 与 second_wasm_exec.js 到独立目录,浏览器可正常启动,CollectArtifacts 却因缺少该目录下的 wasm_exec.js 报错。请与 chooseGoJSLoaderDest 使用一致的 ownership 判定,只报告并验证当前程序实际拥有的加载器,并覆盖第二个程序独立部署的产物清单用例。
There was a problem hiding this comment.
已修复。CollectArtifacts 与 chooseGoJSLoaderDest 使用同一套 ownership:只报告当前程序实际拥有的 host。第二个程序只列出 second_wasm_exec.js;单独部署缺少 wasm_exec.js 时不再报错。
There was a problem hiding this comment.
已复核 9abd62446。CollectArtifacts(second) 只报告 second.wasm 与 second_wasm_exec.js。将这两个文件复制到独立目录、没有 wasm_exec.js 时,浏览器运行和 CollectArtifacts 均成功,此项已修复。
Register factories from the lexical LLGoModule so Node require works, and also key them by resolved wasm URL so same-named files in different directories stay distinct. CollectArtifacts reports only the host this program owns.
| const base = (typeof location !== "undefined" && location.href) || | ||
| (typeof document !== "undefined" && document.currentScript && document.currentScript.src) || | ||
| (typeof __filename === "string" ? "file://" + String(__filename).replace(/\\/g, "/") : "") || ""; | ||
| if (base) { | ||
| const url = new URL(name, base).href; |
There was a problem hiding this comment.
[P2] 统一 Node 相对 Wasm 路径的解析基准和规范化
Go 类只在第一份 host 中定义,因此此处的 __filename 永远属于第一份加载器。Node 中 require(project/a/wasm_exec.js) 和 require(project/b/wasm_exec.js) 后,相对路径会以 project/a/ 为基准解析,字符串 suffix 回退又没有规范化 ./,导致等价路径表现不同。已用本提交真实产物复现,在 project 目录运行时,new Go({wasm: "a/app.wasm"}) 与 b/app.wasm 都成功,但 ./a/app.wasm、./b/app.wasm 都报 no factory registered。a/app.wasm 能成功仅因为后面的 suffix 回退碰巧命中注册 URL。请明确并统一 Node 相对路径的基准,例如与 fs.readFileSync 读取 Wasm 一致的 process.cwd(),规范化后按 URL 查找,并覆盖带 ./、../ 的选择用例。
There was a problem hiding this comment.
已修复。Node 相对路径相对 process.cwd() 解析,不再使用第一份 host 闭包里的 __filename。./a/app.wasm 与 a/app.wasm 会规范成同一 URL。
There was a problem hiding this comment.
已在 17b1ae2 用真实 Wasm 产物验证修复:以项目根目录为 cwd 时,a/app.wasm、./a/app.wasm、b/app.wasm、./b/app.wasm 均能选中对应工厂;切换 cwd 到 b/ 后,../a/app.wasm 和 ./app.wasm 也均成功。另发的含 # 目录路径编码问题在 9abd624 已存在,与本条普通相对路径用例的修复分开记录。
Look up factories against location.href or process.cwd before any bare-name alias, and normalize ./ relative paths. Conflicting app.wasm entries no longer follow load order when the page path selects one.
| let dir = String(process.cwd()).replace(/\\/g, "/"); | ||
| if (!dir.startsWith("/")) dir = "/" + dir; | ||
| if (!dir.endsWith("/")) dir += "/"; | ||
| return "file://" + dir; |
There was a problem hiding this comment.
[P2] 使用正确的原生路径到 file URL 转换
目录名含合法字符 # 时,直接拼接 file:// 会把后面的路径解释为 URL fragment。实际复现:在 project#tag 下加载 a/wasm_exec.js、b/wasm_exec.js(各有自己的 app.wasm),两个工厂的 URL 注册键都变成父目录的同一个 .../app.wasm;new Go({wasm:"./a/app.wasm"})、./b/app.wasm 均报 no factory registered,传入正确编码的绝对 file:///.../project%23tag/a/app.wasm 也无法选中。
这里的 cwd 转换和 internal/build/gojs_loader.go:222 的 __filename 注册都需要正确转换并编码文件路径(Node 可使用 node:url.pathToFileURL(),cwd 保留目录末尾分隔符),只修这里仍会留下损坏的注册键。建议增加含 # 目录下两个同名 Wasm 的回归用例。Node 官方文档也给出了这个字符的转换示例:https://nodejs.org/api/url.html#urlpathtofileurlpath-options
来源说明:已用上一版 9abd624 产物复现;注册端的问题在上一版已有,本提交新增的 cwd 转换沿用了同一拼接方式。这是本轮新发现的原有问题。
Summary
wasm_exec.jshost for standaloneGOOS=js GOARCH=wasmbuilds.wasm_exec.jssonew Go()works immediately after the script loads, matching official Go.wasm_exec.jsfor the first program. If another wasm already owns that file, emitstem_wasm_exec.js. Artifact reporting lists only the host this program owns.LLGoModule(so Noderequireworks) by bare wasm name and resolved URL.new Go()uses the last factory;new Go({ wasm: "app.wasm" })selects one when unambiguous; a path or URL selects among same-named files in different directories. A later host script only registers its factory and does not redefineGo.-target emscripten,llgo run/test, and explicit-o *.mjson the existing Emscripten module factory.fs,process, andpathfallbacks consistent with Go's loader.print/printErrthroughfs.writeSync, so callers can hookglobalThis.fs.Validation
go test ./internal/build -run 'TestGoJSLoader|TestPublishGoJS|TestCombineGoJS|TestCollectArtifactsGoJS|TestExecuteMainLinkPublishesGoJS|TestGoJSWasmMarker'require("./wasm_exec.js"),new Go(),new Go({ wasm }),WebAssembly.instantiate,go.run(instance)syscall/jsand browser filesystemENOSYS