Dev

Integration of Go WASM with V8 Isolate: Errors in Existing Methods and Solutions

The standard GOOS=js method for adding Go to the browser-based code execution platform "dailyprog" failed in a V8 Isolate environment. The issue was resolved by transitioning to the WASI target through technical validation.

6 min read Reviewed & edited by the SINGULISM Editorial Team

Integration of Go WASM with V8 Isolate: Errors in Existing Methods and Solutions
Photo by Florian Olivo on Unsplash

Adding a new language to an online platform that allows code execution in the browser might seem like a straightforward task, easily accomplished by following existing procedures. However, unique execution environments can sometimes render conventional methods ineffective.

A comprehensive investigative article published by lvmbdv on blog.lvmbdv.dev via Lobsters reveals that the standard Go-to-WASM conversion methods widely used by developers do not work at all in certain environments. The issue lies in the incompatibility between the Go runtime’s initialization method and the isolated execution environment of the code.

The Challenge of Adding a Fourth Language

Dailyprog is an online platform that allows users to write and execute code directly in their browsers. Initially, it supported only JavaScript, but functionality for Python and C execution was later added. Each language required a distinct technical approach: Python utilized Pyodide (a WebAssembly-compiled CPython interpreter), while C relied on a custom harness to manage the WebAssembly binary of PicoC and its printf outputs.

Go was planned as the fourth language. Numerous tutorials detail the process of compiling Go to WebAssembly, giving the impression that the path is well-established. However, this standard approach did not work in the specific execution environment of dailyprog.

The Mechanics and Problems of the Standard

GOOS=js Method

The standard approach for compiling Go to WebAssembly for browsers involves setting the environment variables GOOS=js and GOARCH=wasm. This build target generates a WebAssembly binary with JavaScript system call bridges. Additionally, a support file, wasm_exec.js, must be provided to implement the features expected by the Go runtime on the JavaScript side.

Many browser-based Go playgrounds (such as LiveCodes—note that the official Go playground avoids this issue by executing code server-side) adopt this method. With a well-documented history of successful use, this approach benefits from a wealth of knowledge on platforms such as Stack Overflow.

However, dailyprog executes user code not on the browser’s main thread but within an isolated-vm. This environment, a restricted V8 Isolate, lacks a DOM and an event loop, and offers a limited API surface. The standard boot sequence asynchronously invokes WebAssembly.instantiate(), which returns a Promise. Unfortunately, within the isolated-vm, this Promise remains unresolved, as there is no event loop to drive its resolution. As a result, the program hangs until it times out.

The author spent two days attempting to resolve this issue. An error message reading “Maximum call stack size exceeded” led to suspicions of a stack overflow; increasing V8’s stack size through the --stack-size option proved ineffective, however, as it did not reach the Isolate. The stack overflow turned out to be merely a symptom of the startup hang. Attempts to inject a polyfill for setTimeout into the Isolate also failed. This asynchronous initialization in a synchronous environment proved to be a fundamental deadlock.

The Dual Barrier of the Bridging Mechanism

In addition to the asynchronous initialization problem, another issue with GOOS=js emerged. The syscall/js bridge (a mechanism that allows Go to call JavaScript and vice versa) relies on setting properties on globalThis. In a browser environment, globalThis refers to window. However, in an isolated-vm, the globalThis captured during the bridge’s initialization differs from the globalThis visible to the executing code. This mismatch prevents callbacks from being properly received, even if they are triggered. Since the bridge is compiled into the WebAssembly binary, it cannot be externally modified.

GOOS=js is designed with a fully-fledged browser environment in mind. When that assumption is broken, this target fails in undocumented ways. The lack of attempts to execute Go WebAssembly within V8 Isolates had delayed the discovery of these issues.

WASI as the Solution

WASI (WebAssembly System Interface) is a standard system interface for WebAssembly modules. It defines a set of imports for functions like file I/O, environment variables, clocks, and random number generation, which the host provides. This interface is far simpler than JavaScript bridges; it involves no Promises, no event loops, and no reliance on globalThis. Only linear memory buffers and a few function imports are used.

As of version 1.21, Go supports the WASI build target with GOOS=wasip1 GOARCH=wasm. Compiling with this target ensures that Go’s fmt.Println outputs are handled as standard WASI calls, eliminating the need for JavaScript bridges. Since isolated-vm supports WASI implementations, this approach enables the execution of Go code.

This method broadens the options for WebAssembly execution platforms in the Go ecosystem. Even in isolated environments where the standard GOOS=js fails, the WASI target offers a viable path forward.

Environment-Dependent Pitfalls of Standard

Procedures

This case provides a clear lesson: even well-established standard procedures may fail when their assumed execution environment differs. The GOOS=js target for Go’s WebAssembly compilation is heavily designed for browser environments. When these implicit assumptions are removed, WASI emerges as a viable alternative.

The dailyprog case demonstrates how differences in execution environments can influence technical choices for language support in browser-based code execution platforms. It also highlights the growing importance of the WASI standard, not only for Go but also for the broader WebAssembly ecosystem. Since Go 1.21 introduced WASI support, practical use in constrained environments is still in its infancy. Depending on the development of WASI Preview 2 and Preview 3, greater integration of WebAssembly execution platforms across environments may become possible, reducing environmental disparities.

Editorial Opinion

In the short term, developers working on browser-based code execution platforms or edge computing environments may need to reconsider their execution platform selection based on these insights. For constrained environments like V8 Isolates where Go WebAssembly execution is required, GOOS=wasip1 could become the de facto choice over GOOS=js. As noted in the article “Anthropic Declares the Revival of Fable 5,” the compatibility between execution environments and toolchains is a critical factor influencing development efficiency.

In the long term, the adoption of the WASI standard is expected to impact not only Go but other languages within the WebAssembly ecosystem. With Go 1.21’s recent introduction of wasip1 support, the accumulation of practical experience in similar constrained environments is still underway. Depending on the progress of WASI Preview 2 and Preview 3, the integration of WebAssembly execution platforms across browser and non-browser environments may advance, potentially reducing environmental differences even further.

References

Frequently Asked Questions

What is `isolated-vm`?
It is an isolated V8 engine environment designed to securely execute untrusted user code within browsers. It lacks a DOM and an event loop, offering a restricted API surface. By running code in a separate context from the main thread, it balances security and performance, though it imposes limitations on asynchronous processing and the use of browser APIs.
What is the difference between `GOOS=js` and `GOOS=wasip1`?
`GOOS=js` assumes a browser environment and uses a JavaScript system call bridge to interface with the DOM and event loop, relying on asynchronous initialization and `globalThis`. In contrast, `GOOS=wasip1` is based on the WASI standard, operating with only linear memory buffers and a few function imports. It requires no Promises, event loop, or reliance on `globalThis`, making it suitable for constrained environments like `isolated-vm`.
From which version of Go is wasip1 available?
WASI build target `GOOS=wasip1 GOARCH=wasm` is officially supported starting from Go 1.21. Older versions do not support this, so upgrading to Go 1.21 or later is necessary to compile using the WASI target.
Source: Lobsters

Comments

← Back to Home