The Go Blog
Extensible Wasm Applications with Go
Go 1.24 enhances its WebAssembly (Wasm) capabilities with the
addition of the go:wasmexport
directive and the ability to build a reactor
for WebAssembly System Interface (WASI).
These features enable Go developers to export Go functions to Wasm,
facilitating better integration with Wasm hosts and expanding the possibilities
for Go-based Wasm applications.
WebAssembly and the WebAssembly System Interface
WebAssembly (Wasm) is a binary instruction format that was initially created for web browsers, providing the execution of high-performance, low-level code at speeds approaching native performance. Since then, Wasm’s utility has expanded, and it is now used in various environments beyond the browser. Notably, cloud providers offer services that directly execute Wasm executables, taking advantage of the WebAssembly System Interface (WASI) system call API. WASI allows these executables to interact with system resources.
Go first added support for compiling to Wasm in the 1.11 release, through the
js/wasm
port.
Go 1.21 added a new port targeting the WASI preview 1 syscall API through the
new GOOS=wasip1
port.
Exporting Go Functions to Wasm with go:wasmexport
Go 1.24 introduces a new compiler directive, go:wasmexport
, which allows
developers to export Go functions to be called from outside of the
Wasm module, typically from a host application that runs the Wasm runtime.
This directive instructs the compiler to make the annotated function available
as a Wasm export
in the resulting Wasm binary.
To use the go:wasmexport
directive, simply add it to a function definition:
//go:wasmexport add
func add(a, b int32) int32 { return a + b }
With this, the Wasm module will have an exported function named add
that
can be called from the host.
This is analogous to the cgo export
directive,
which makes the function available to be called from C,
though go:wasmexport
uses a different, simpler mechanism.
Building a WASI Reactor
A WASI reactor is a WebAssembly module that operates continuously, and can be called upon multiple times to react on events or requests. Unlike a “command” module, which terminates after its main function finishes, a reactor instance remains live after initialization, and its exports remain accessible.
With Go 1.24, one can build a WASI reactor with the -buildmode=c-shared
build
flag.
$ GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared -o reactor.wasm
The build flag signals to the linker not to generate the _start
function
(the entry point for a command module), and instead generate an
_initialize
function, which performs runtime and package initialization,
along with any exported functions and their dependencies.
The _initialize
function must be called before any other exported functions.
The main
function will not be automatically invoked.
To use a WASI reactor, the host application first initializes it by calling
_initialize
, then simply invoke the exported functions.
Here is an example using Wazero, a Go-based Wasm runtime
implementation:
// Create a Wasm runtime, set up WASI.
r := wazero.NewRuntime(ctx)
defer r.Close(ctx)
wasi_snapshot_preview1.MustInstantiate(ctx, r)
// Configure the module to initialize the reactor.
config := wazero.NewModuleConfig().WithStartFunctions("_initialize")
// Instantiate the module.
wasmModule, _ := r.InstantiateWithConfig(ctx, wasmFile, config)
// Call the exported function.
fn := wasmModule.ExportedFunction("add")
var a, b int32 = 1, 2
res, _ := fn.Call(ctx, api.EncodeI32(a), api.EncodeI32(b))
c := api.DecodeI32(res[0])
fmt.Printf("add(%d, %d) = %d\n", a, b, c)
// The instance is still alive. We can call the function again.
res, _ = fn.Call(ctx, api.EncodeI32(b), api.EncodeI32(c))
fmt.Printf("add(%d, %d) = %d\n", b, c, api.DecodeI32(res[0]))
The go:wasmexport
directive and the reactor build mode allow applications to
be extended by calling into Go-based Wasm code.
This is particularly valuable for applications that have adopted Wasm as a
plugin or extension mechanism with well-defined interfaces.
By exporting Go functions, applications can leverage the Go Wasm modules to
provide functionality without needing to recompile the entire application.
Furthermore, building as a reactor ensures that the exported functions can be
called multiple times without requiring reinitialization, making it suitable
for long-running applications or services.
Supporting rich types between the host and the client
Go 1.24 also relaxes the constraints on types that can be used as input and
result parameters with go:wasmimport
functions.
For example, one can pass a bool, a string, a pointer to an int32
, or a
pointer to a struct which embeds structs.HostLayout
and contains supported
field types
(see the documentation for detail).
This allows Go Wasm applications to be written in a more natural and ergonomic
way, and removes some unnecessary type conversions.
Limitations
While Go 1.24 has made significant enhancements to its Wasm capabilities, there are still some notable limitations.
Wasm is a single-threaded architecture with no parallelism.
A go:wasmexport
function can spawn new goroutines.
But if a function creates a background goroutine, it will not continue
executing when the go:wasmexport
function returns, until calling back into
the Go-based Wasm module.
While some type restrictions have been relaxed in Go 1.24, there are still
limitations on the types that can be used with go:wasmimport
and
go:wasmexport
functions.
Due to the unfortunate mismatch between the 64-bit architecture of the client
and the 32-bit architecture of the host, it is not possible to pass pointers in
memory.
For example, a go:wasmimport
function cannot take a pointer to a struct that
contains a pointer-typed field.
Conclusion
The addition of the ability to build a WASI reactor and export Go functions to Wasm in Go 1.24 represent a significant step forward for Go’s WebAssembly capabilities. These features empower developers to create more versatile and powerful Go-based Wasm applications, opening up new possibilities for Go in the Wasm ecosystem.
Next article: Testing concurrent code with testing/synctest
Previous article: Go 1.24 is released!
Blog Index