Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

proposal: cmd/go: option to bundle wasm output with wasm_exec.js #72055

Open
jcbhmr opened this issue Mar 1, 2025 · 9 comments
Open

proposal: cmd/go: option to bundle wasm output with wasm_exec.js #72055

jcbhmr opened this issue Mar 1, 2025 · 9 comments
Labels
arch-wasm WebAssembly issues GoCommand cmd/go OS-JS Proposal ToolProposal Issues describing a requested change to a Go tool or command-line program.
Milestone

Comments

@jcbhmr
Copy link

jcbhmr commented Mar 1, 2025

Proposal Details

Similar to how there's -ldflags "-H windowsgui" for Windows I think there should be something like -ldflags "-H jsmodule" (output ES module which uses top-level await) and/or -ldflags "-H jsscript" (output classic global script which just does .run() and doesn't await it). Or it could be some completely different option. I don't know where this feature request makes the most sense.

Why do this? So that users have the option to get the correct wasm_exec.js bundled with the output instead of needing to remember to tell any downstream user of myapp.wasm "hey! this was built against Go v1.24.0 so remember to use Go v1.24.0's wasm_exec.js and NOT Go v1.23.0's wasm_exec.js" or similar that you might already have in your codebase.

This might make Go WASM more "reproducible" (is that the right word?) because now the JS can optionally be part of the included output and thus doesn't have to be specified out-of-band as "I used this wasm_exec.js (paste code)" when filing bug reports or whatever.

Here's an idea for a template. I have no idea if it works -- this is just illustrative.

#!/usr/bin/env node
// ^^ this syntax is also supported (ignored) by the ecma spec so this file
// can be <script type=module src="./a.out.js"> too!

/* copy-paste existing wasm_exec.js here. */

const base64 = /* EMBED THE WASM AS BASE64 STRING HERE */;
let bytes;
if (Uint8Array.fromBase64) {
  bytes = Uint8Array.fromBase64(base64)
} else {
  bytes = Uint8Array.from(atob(base64), c => c.charCodeAt(0))
}

const go = new Go();
go.importMeta = import.meta;

// true on Node.js, Deno, Bun. false in browsers and anywhere else.
const isNodeLike = await import("node:process").then(() => true, () => false)
if (isNodeLike) {
  const { default: process } = await import("node:process")
  const fs = await import("node:fs")
  const path = await import("node:path")
  const os = await import("node:os")

  go.argv = process.argv.slice(2);
  go.env = { TMPDIR: os.tmpdir(), ...process.env };
  go.exit = process.exit;

  process.on("exit", (code) => { // Node.js exits if no event handler is pending
    if (code === 0 && !go.exited) {
      // deadlock, make Go print error and stack traces
      go._pendingEvent = { id: 0 };
      go._resume();
    }
  });

  globalThis.process = process;
  globalThis.fs = fs;
  globalThis.path = path;
}

const { instance } = await WebAssembly.instantiate(bytes, go.importObject)

await go.run(instance)

Remember, currently js/wasm only outputs main command executables and not libraries so you can assume that it should always be run as a top-level item and not imported as a library. ex: node a.out or somePrepWork(); import "./a.out" but not import { f } from "./a.out"; restOfCode()

THIS SHOULD NOT CHANGE THE DEFAULT BEHAVIOUR I understand that changing the default output of GOOS=js GOARCH=wasm go build is a bad idea. Instead, I'm proposing an optional mode that outputs JS wrapping the wasm

The -H flag as part of https://pkg.go.dev/cmd/link seems like a good fit but idk I'm not a compiler dev lol

-H type
	Set executable format type.
	The default format is inferred from GOOS and GOARCH.
	On Windows, -H windowsgui writes a "GUI binary" instead of a "console binary."

so I thought "hey maybe -H jsmodule fits the -H <goos><type> of "windowsgui" where goos=js and type=module".

Alternatives

  • Create a golang.org/x/tools/cmd/go-toolexec-output-jsmodule that can be used with -toolexec to wrap and post-process some part of the build commands and output JS instead of WASM to the out file
  • Do nothing. Rely on out-of-band existing workflow to specify which wasm_exec.js to use
  • Change the default output of GOOS=js GOARCH=wasm to JS instead of WASM
  • Output a sibling a.out.js file that fetch()-es the ./a.out.wasm file
  • publish wasm_exec.js to npm proposal: syscall/js: publish wasm_exec.js and wasm_exec_node.js bindings to npm #58250
  • codify and standardize a stability policy on what the Go WASM target expects to be exposed from JS
@jcbhmr jcbhmr added the Proposal label Mar 1, 2025
@gopherbot gopherbot added this to the Proposal milestone Mar 1, 2025
@seankhliao seankhliao changed the title proposal: cmd/go: add a way to output JS code proposal: cmd/go: option to bundle wasm output with wasm_exec.js Mar 1, 2025
@seankhliao seankhliao added GoCommand cmd/go arch-wasm WebAssembly issues OS-JS labels Mar 1, 2025
@seankhliao
Copy link
Member

is this appropriate / efficient for the js ecosystem?
if you're distributing recompiled binaries, you can just distribute it together with the js file? I don't think we've seen many reports of using the wrong wrapper

@gabyhelp gabyhelp added the ToolProposal Issues describing a requested change to a Go tool or command-line program. label Mar 1, 2025
@jcbhmr
Copy link
Author

jcbhmr commented Mar 1, 2025

I believe it is, yes. You're right that wasm_exec.js doesn't change very often so it's not very likely that Go v1.N wasm_exec.js will break when trying to run a Go v1.N+1 binary. But if, say, https://go-review.googlesource.com/c/go/+/555417 were to be merged that would immediately cause existing Go v1.24.0 wasm_exec.js to not work with any newer Go v1.24+1 Go-built .wasm files.

The current manual management of the sibling wasm_exec.js increases the complexity and general this feels hacky and unofficial feeling which can be detrimental to the 0-60 getting started first-timers.

To provide an example from another ecosystem: wasm-bindgen from Rust does a similar thing to GOOS=js GOARCH=wasm -- it runs & binds to JavaScript through a JS-provided suite of functions -- and wasm-bindgen does automatically generate accompanying .js code in your output directory (albeit separate .js and .wasm file with an extra .d.ts). Point being though that wasm-bindgen does generate the JS glue code instead of saying "ok I did most of it; now go find the right glue code yourself before you can run it".

That "ok I did most of it; now go find the right glue code yourself" is what I would like the option to skip via a go build -ldflags "-H jsmodule" or similar trick.

Obviously, yes, the current workflow works but I think that this would be strictly an optional improvement that would emphasize and reinforce the idea that the generated .wasm and the accompanying wasm_exec.js are intertwined in lockstep with each other and are very much not guaranteed to work with other non-lockstep versions of eachother

@dmitshur
Copy link
Contributor

dmitshur commented Mar 1, 2025

Noting that there's some similarity here with a previous request to add cmd/go support for multi-architecture macOS binaries (issue #40698). In both cases, it's something that can also be done by an external tool, such as https://github.com/randall77/makefat for the former request, where there are fewer constraints (cmd/go has a 6-month release cycle, high bar for breaking compatibility, etc.).

@jcbhmr
Copy link
Author

jcbhmr commented Mar 1, 2025

This is similar yes. Similar: Both of these are "post-process the compiled output pls" requests. They can both be accomplished outside of the go build pipeline. Difference: the wasm_exec.js is tightly coupled to the Go compiler & runtime and that post-processing step should/could be in lockstep with the Go compiler. macOS fat binaries seem generic and not coupled to the Go compiler in such a lockstep way.

@jcbhmr
Copy link
Author

jcbhmr commented Mar 1, 2025

I think this comes down to a) how integrated the wasm_exec.js is with the Go toolchain/output itself and whether that warrants baking it in to the output and b) whether or not it's good DX to bake it in.

I am willing to try and contribute code to make this a thing if this is deemed an acceptable proposal. (whatever form it takes; -ldflags, env var, a sibling file, whatever)

@ianlancetaylor ianlancetaylor moved this to Incoming in Proposals Mar 3, 2025
@ianlancetaylor
Copy link
Member

CC @golang/js

@Zxilly
Copy link
Member

Zxilly commented Mar 3, 2025

Base64-encoding WASM incurs a ~33% size penalty. Also, I'm not quite sure if this will affect the user's ability to use DevTools to diagnose the generated wasm file?

It might be more likely to add a sibling file when using the -H flag.

@Zxilly
Copy link
Member

Zxilly commented Mar 3, 2025

But if, say, https://go-review.googlesource.com/c/go/+/555417 were to be merged that would immediately cause existing Go v1.24.0 wasm_exec.js to not work with any newer Go v1.24+1 Go-built .wasm files.

This doesn't affect the 1.24.x versions, they've been separated into separate branches for development, and code on the mainline is always pointing to the next major version.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
arch-wasm WebAssembly issues GoCommand cmd/go OS-JS Proposal ToolProposal Issues describing a requested change to a Go tool or command-line program.
Projects
Status: Incoming
Development

No branches or pull requests

7 participants