{"id":27993139,"url":"https://github.com/paralin/tinygo-wasip2-prototype","last_synced_at":"2026-02-23T20:12:26.171Z","repository":{"id":282135335,"uuid":"947454444","full_name":"paralin/tinygo-wasip2-prototype","owner":"paralin","description":"Basic prototype of running Go in the browser with Tinygo and wasip2.","archived":false,"fork":false,"pushed_at":"2025-03-23T21:45:30.000Z","size":199,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-05-05T04:08:07.830Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/paralin.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2025-03-12T17:58:39.000Z","updated_at":"2025-03-23T20:48:52.000Z","dependencies_parsed_at":"2025-03-13T01:30:21.634Z","dependency_job_id":"59650fe0-3c34-4644-a72b-38099242757f","html_url":"https://github.com/paralin/tinygo-wasip2-prototype","commit_stats":null,"previous_names":["paralin/tinygo-wasip2-prototype"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/paralin%2Ftinygo-wasip2-prototype","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/paralin%2Ftinygo-wasip2-prototype/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/paralin%2Ftinygo-wasip2-prototype/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/paralin%2Ftinygo-wasip2-prototype/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/paralin","download_url":"https://codeload.github.com/paralin/tinygo-wasip2-prototype/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":253132876,"owners_count":21859106,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-05-08T18:46:42.732Z","updated_at":"2026-02-23T20:12:21.124Z","avatar_url":"https://github.com/paralin.png","language":"JavaScript","readme":"# TinyGo WebAssembly with WASI Preview2\n\nThis repository contains a demonstration of running Go code in a browser using WebAssembly with WASI Preview2 support via TinyGo.\n\n## Running the Demo\n\nTo build and run the demo, simply execute:\n\n```bash\n./serve.bash\n```\n\nThis will:\n1. Compile the Go code to WebAssembly\n2. Transpile the WASM module to JavaScript\n3. Bundle the web application\n4. Start a local server (typically on port 8000)\n\nOpen your browser to the displayed URL to see the demo in action.\n\n## How It Works\n\nThis demo showcases how to bridge Go and WebAssembly with WASI Preview2, allowing Go code to run in browser environments with access to system interfaces like stdout through the WebAssembly System Interface standard.\n\nThe TypeScript code sets up the necessary WASI Preview2 interfaces (stdin, stdout, stderr, clocks, etc.) that the Go code expects, enabling seamless execution in the browser.\n\n### Importing Wasi Functions\n\nTinygo uses this construct internally to import functions from the \"imports\" structure:\n\n```go\n//go:wasmimport wasi:io/poll@0.2.0 [method]pollable.ready\n//go:noescape\nfunc wasmimport_PollableReady(self0 uint32) (result0 uint32)\n```\n\n## Asynchronous I/O\n\nWe use Atomics.wait to block the javascript thread while waiting for a duration to implement Pollable.block() with scheduleDuration for time.After and time.Sleep.\n\n- **WebWorker**: The WASM module runs in a dedicated WebWorker thread, leaving the main thread responsive\n- **WASI Preview2**: We've implemented the missing parts of the WASI Preview2 interfaces for clocks and polling\n- **Synchronous Blocking**: Uses SharedArrayBuffer and Atomics.wait to block the thread synchronously w/ a timeout\n- **Cross-origin Isolation**: The server is configured with required COOP/COEP headers for SharedArrayBuffer support\n\nThis implementation allows standard Go code using `time.Sleep` to work without modification by implementing the underlying WASI interfaces that TinyGo uses.\n\n### Implementation Flow\n\n1. Main thread creates a WebWorker and initializes it\n2. WebWorker loads and runs the WASM module\n3. Go code in WASM calls `time.Sleep(duration)`\n4. TinyGo runtime calls WASI's `monotonic-clock.subscribeDuration(duration)`\n5. Our shim returns a Pollable object\n6. TinyGo runtime calls `pollable.block()` on that object\n7. The Pollable's block method uses Atomics.wait with a timeout set to the desired duration\n8. The worker thread blocks synchronously until Atomics.wait times out after the specified duration\n9. Execution continues after the timeout\n\n### Nonblocking I/O\n\nThis section describes the challenges we aim to address in this implementation.\n\nWhen we call \"time.Sleep\" in Go this results in calling monotonic-clock\nsubscribeDuration which is currently stubbed in the wasip2 shim implementation\nfrom bytecodealliance. Even worse, this depends on block() in Pollable.\n\nPollable is something from the host environment that the wasm code can wait for.\nIt has a single function block() which is supposed to block until the event\noccurs, then return. The problem is that the JavaScript environment in the web\nbrowser doesn't have blocking within a function call, it must return right away.\nWe can't block returning from block().\n\nOne option to fix this is to transform blocking I/O calls into async calls. jco\nsupports this experimentally with the `--async-mode jspi` (JavaScript Promise\nIntegration) flag. We then can set the poll function as async with\n`--async-imports wasi:io/poll#[method]pollable.block` - then we get something\nlike `const trampoline0 = new WebAssembly.Suspending(async function(arg0) {`\nwith an `await pollable.block()` inside.\n\n[The announcement for jspi] and [the jspi spec] describes how it is supposed to\nwork. The problem is: **WebAssembly.Suspending** is not implemented in Chrome or\nFirefox as of 03/12/2025. It is currently in a [jspi origin trial] in Chrome,\nending in July 2025, so this is an extremely bleeding edge feature that is not\nyet widely supported. See [jspi proposal] for more details.\n\n[The announcement for jspi]: https://v8.dev/blog/jspi\n[the jspi spec]: https://github.com/WebAssembly/js-promise-integration/blob/main/proposals/js-promise-integration/Overview.md\n[jspi origin trial]: https://developer.chrome.com/origintrials/#/view_trial/1603844417297317889\n[jspi proposal]: https://github.com/WebAssembly/js-promise-integration\n\nThe latest update on jspi can be found in the [extent to extend experiment]\nemail on the Blink mailing list in which, as of January 29, 2025:\n\n\u003e SPI has been inherently hard to specify, and validate security requirements\n\u003e for given that it is somewhat sandwiched between JS \u0026 Wasm. Concretely, since\n\u003e the last OT extension, the late breaking changes have been merged into the\n\u003e specification and we've gotten more signals about the exploitable security\n\u003e surface of JSPI (OT features are treated as shipped features for V8), and we'd\n\u003e like to focus on hardening security ahead of an intent to ship.\n\u003e\n\u003e ~ Deepti Gandluri\n\n[extent to extend experiment]: https://groups.google.com/a/chromium.org/g/blink-dev/c/ke9rpIdSTwI/m/pq9PnNtCAAAJ\n\nSince jspi is not yet supported and there's apparently no way to return\nasynchronously from a call from wasm =\u003e javascript I guess the only way to make\nthis work is to force all the calls out of Go to be synchronous in nature and\nhandle the async with a callback calling into the Go runtime from outside of\nwasm when the promise resolves.\n\n### Blocking I/O\n\nThere is a way to block JavaScript in the web browser; we can use [Atomics.wait]\nwith a SharedArrayBuffer to block the JS thread. This is supported in all major\nbrowsers but requires [secure context] and [cross-origin isolated] headers. This\nwould require two coopoerating Worker where one Worker sets up the JavaScript\nasync Promise callbacks and the other (wasm) uses Atomics.wait to block the wasm\nfunction call until the Promise resolves.\n\n[Atomics.wait]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Atomics/wait\n[SharedArrayBuffer]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer\n[secure context]: https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts\n[cross-origin isolated]: https://developer.mozilla.org/en-US/docs/Web/API/Window/crossOriginIsolated\n\n### Asynchronous I/O With Asyncify\n\nTinyGo already uses [Asyncify] from Binaryen to implement Goroutines.\n\n[Asyncify]: https://github.com/WebAssembly/binaryen/blob/main/src/passes/Asyncify.cpp\n\nTinyGo uses the asyncify scheduler implementation to enable goroutines to be\nproperly suspended and resumed in WebAssembly environments, particularly for\nhandling async functions imported with `//go:wasmimport`.\n\n- TinyGo uses Binaryen's wasm-opt tool with the `--asyncify` flag to transform WebAssembly code.\n- Goroutines are represented as Task w/ a Stack within the tinygo source code (the Go runtime).\n- When a goroutine needs to wait, it's unwound using the asyncify mechanism\n\nThe key tinygo source files are:\n\n- `task_asyncify.go`: implementation of Task calling the Asyncify functions\n- `task_asyncify_wasm.S`: WebAssembly-specific assembly code for stack manipulation\n- `scheduler_cooperative.go`: cooperative scheduler that manages Goroutine as Task\n\nWhen the tinygo program starts, we call `run` which is defined in scheduler_cooporative.go.\n\n```go\n// run is called by the program entry point to execute the go program.\n// With a scheduler, init and the main function are invoked in a goroutine before starting the scheduler.\nfunc run() {\n\tinitHeap()\n\tinitRand()\n\tgo func() {\n\t\tinitAll()\n\t\tcallMain()\n\t\tmainExited = true\n\t}()\n\tscheduler(false)\n}\n```\n\nThis creates a new Goroutine to call `init()` and `main()` which is initially sleeping.\n\nThe `scheduler` has the following general loop, while the program has not exited:\n\n- Add Task that are done sleeping to the end of the runqueue.\n- Run the callback for any timers that have ticked.\n- Pop the next Task from the runqueue.\n  - If there is no queued Task: sleep until the next sleepQueue or timerQueue entry is ready.\n  - Otherwise return with an error saying we are deadlocked.\n- Call Resume on the Task.\n\nWhen a goroutine needs to yield control (e.g., mutex lock, channel operation, sleep) it calls `Task.Pause()`:\n1. Check for stack overflow by verifying the stack canary value.\n2. Call `currentTask.state.unwind()` which invokes `tinygo_unwind` in assembly\n3. `tinygo_unwind` performs these steps:\n   - Checks if already in rewinding mode:\n     - If yes, calls `stop_rewind` and clears the flag\n   - If not in rewinding mode:\n     - Saves the current stack pointer to `state.csp`\n     - Calls `asyncify.start_unwind(state)`\n     - This triggers the asyncify transformation to unwind the entire stack\n     - Control returns to the scheduler without executing the rest of the goroutine\n\nWhen the scheduler decides to run a goroutine (represented as a Task), it calls `Task.Resume()`:\n\n1. If the task has never run before (`!t.state.launched`):\n   - Calls `t.state.launch()` which invokes `tinygo_launch` in assembly\n   - `tinygo_launch` performs these steps:\n     - Switches to the goroutine's stack by setting `__stack_pointer`\n     - Retrieves the entry function and arguments from task state\n     - Calls the entry function with arguments\n     - Calls `stop_unwind` to ensure normal execution mode\n     - Restores the original stack pointer\n   - Sets `t.state.launched = true` to indicate this task has started\n\n2. If the task has run before and was suspended:\n   - Calls `t.state.rewind()` which invokes `tinygo_rewind` in assembly\n   - `tinygo_rewind` performs these steps:\n     - Switches to the goroutine's stack\n     - Retrieves the entry function and arguments\n     - Sets `tinygo_rewinding = true` to indicate we're in rewind mode\n     - Calls `asyncify.start_rewind` with the stack state\n     - Calls the entry function, which (due to asyncify's transformation) will jump directly to where the task was previously suspended\n     - Calls `stop_unwind` when the task suspends again or completes\n     - Restores the original stack pointer\n\nWhen we call `start_unwind` in `task.Pause` the function call in `tinygo_rewind`\nreturns, and we then call `stop_unwind` to finish the unwind operation, and\nreturn back to the scheduler, returning from `task.Resume`.\n\n## Next Steps\n\nHere is a summary of what we have determined so far:\n\n- Running with `GOOS=js` returns to JavaScript after each scheduler run.\n- Running with `GOOS=wasip2` runs a loop within Go and does not return to JavaScript.\n- wasip2 waits for events by creating one or more `Pollable` then calling `poll([pollables...])`\n- this depends on the imported `poll` function to **block** until one or more `Pollable` return\n\nWe can use `Atomics.wait` to block the JavaScript thread, and pass a timeout for sleeping.\n\nThe key issue is since we never return to the JavaScript event loop, we cannot\nregister and consume event handlers, making implementing the stdlib difficult.\n\nThe only apparent solution is to use two threads within two WebWorker:\n\n- **event thread**: manage JavaScript callbacks and notify `WebAssembly` via `Atomics.notify`\n- **WebAssembly thread**: run the main WebAssembly process and block with `Atomics.wait`\n\n`Atomics.wait` works on a `SharedArrayBuffer` that is shared between the two threads.\n\nWe cannot use MessagePort or other communication primitives to talk between the\nworkers, so instead we need to pass all data via the `SharedArrayBuffer`.\n\nNot all browsers support growing the buffer, so we must define a `MTU` (maximum\ntransmission unit) up front and pack the data into that size in the SharedArrayBuffer.\n\nThis loosely implements a packet queue with shared memory. On top of this packet\nqueue we can use a typical ordered stream implementation like `yamux` to\nmitigate the limitations of the MTU and multiplex unlimited sized data.\n\nFor optimization, we can queue packets, and transmit multiple at a time via the\navailable SharedBufferArray space (batching).\n\nFor transmitting data back from the WebAssembly thread to the event thread, we\ncan use a typical MessagePort in a one-way configuration writing Uint8Array into\nthe MessagePort as a packet queue back to the event thread.\n\nSee the [blocking-io prototype] which demonstrates this approach with several\nother optimizations to achieve ~90MB/s transfer between the two workers.\n\n[blocking-io prototype]: https://github.com/paralin/blocking-worker-io-prototype\n\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fparalin%2Ftinygo-wasip2-prototype","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fparalin%2Ftinygo-wasip2-prototype","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fparalin%2Ftinygo-wasip2-prototype/lists"}