At Dylibso, we are intimately aware of the challenges of working with a diverse set of WebAssembly (Wasm) modules and runtimes. As maintainers of Extism, the WebAssembly Observe SDK, and Modsurfer, we encounter Wasm modules that have been compiled from nearly every language that can target WebAssembly. Furthering these challenges, we strive to build our tools and products such that they support the broadest set of Wasm runtimes. In this diverse landscape, one thing we often see developers stumble over is correctly discerning between WebAssembly System Interface (WASI)’s Command and Reactor modules.
In order to gain a better grasp on this topic, let’s start by explaining why these two types of modules even exist. A Wasm module (like most software) is either an application or a library. To help standardize the interfaces of Wasm modules, the WASI spec defines the Command interface for application modules and the Reactor interface for library modules. In this article we’re going to detail out the differences between the WASI Command and Reactor interfaces and explain why you might choose one over the other.
Note: The WebAssembly Component Model is in development and introduces an additional method of compiling and running your WebAssembly modules which we will not discuss in this article
Command modules in WebAssembly are conceptually similar to executable programs. In many cases, Command modules read their input from
stdin and write their output to
stdout. Programs that are compiled to Wasm using WASI’s Command interface typicaly have a single entry point in their native language. After compiling to Wasm, a function representing that single entry point is exported from the Wasm module called
_start function is the initial point of execution when the module is loaded by the runtime. Command modules are currently more prevalent than Reactor modules, so let’s take a deeper look at the
_start function doesn’t take any parameters. There are multiple ways to pass data into a Command module. Given this is a WASI module, you may either pass data over
stdin, or initialize the module with command line args (
argv) before it is called. The implementation of the
_start function varies between Wasm compilers. The Emscripten compiler for example uses initialization functions such as constructors (i.e.
__wasm_call_ctors ), runs the entry point function of an application (typically your
main function) and then performs some cleanup (in this case
__wasm_call_dtors). Resources (such as linear memory) available during execution may not be available after cleanup. The specifics depend on the compiler used, so take caution when utilizing exported functions of a Command module after the
_start function has been called. In most cases, we recommend you reinitialize a Command module after the
_start function has been executed.
The following is a non-exhaustive list of languages & compilers that support compilation to WASI Command modules:
- Go (& TinyGo)
- C/C++ (via Emscripten)
Reactor modules in WebAssembly are conceptually similar to libraries. Unlike Command modules, there is no “entry point” (
_start) function. Instead, a Reactor module exports functions for a host application to invoke at any point throughout the lifecycle of the module’s instance. A Reactor module may optionally export an
_initialize function. If this function is exported, it is expected that
_initialize is called before any of the other exported functions are called. Unlike Command modules (which may have undefined behavior if you don’t reinitialize the module after calling its
_start function), Reactor modules are designed to allow their exported functions to be called multiple times without reinitialization. Reactor module authors must take additional care to manage memory correctly to avoid running out of memory or undefined behavior when functions are used multiple times.
The following is a non-exhaustive list of languages & compilers that support compilation to WASI Reactor modules:
Haskell, C and C++ (with Clang) and Zig support Reactor modules using the
-mexec-model=reactorflag. The developers of
ghc-wasm-metaprovide a great write up on the support for Reactor modules in Haskell.
Rust, on its nightly release, supports Reactor modules with
cargo +nightly rustc --release --target=wasm32-wasi -- -Z wasi-exec-model=reactor.
We talked about the differences between Command and Reactor modules. While we’re at it, let’s talk a bit about some of the differences in module instantiation across Wasm runtimes. This has some implications for Command & Reactor modules as well.
Wasmtime + V8
In Wasmtime and V8, instantiation involves taking a WebAssembly module, compiling it, and making the exports available for invocation. This implies that the developer must explicitly call
_start (for Command modules) or
_initialize (for Reactor modules) after the module has been instantiated. This provides some level of flexibility, but also allows the developer to make mistakes by using the module in a non-standard way.
In Wazero, instantiation follows a similar process to Wasmtime and V8, where the module is compiled and exports are made available. However, by default, Wazero also calls the
_start function during instantiation. This behavior can be disabled using
config.WithStartFunctions. There does not appear to be a similar configuration for Reactor type modules and the
_initialize function. This omission gives a good indication as to the current popularity of Command modules vs. Reactor modules.
In this article, we talked about the differences between WASI Command and Reactor modules, and their behavior in a small set of Wasm runtimes to give you an idea of some of the considerations to take into account when executing these types of Wasm modules. If you are compiling your program to Wasm with WASI, there are pros and cons to it being a Command module or a Reactor module. A Command module is often easiest to integrate with, as WASI libraries handle converting CLI arguments into native Wasm types and input and output can be handled via standard file based IO. Reactor modules typically leave it up to the developer to pass arguments to and from the host application over the linear memory boundary, which may result in serialization issues as well as memory leaks if that memory is not handled properly. A Reactor module allows lighter-weight usage of a Wasm module at the cost of having to do more memory management on your own. It’s worth noting that Extism allows developers to create Reactor type WebAssembly modules while eliminating the hardships of memory management. We hope this article has helped you understand the differences between Command and Reactor modules.
Whether you're curious about WebAssembly or already putting it into production, we've got plenty more to share.
We are here to help, so click & let us know:Get in touch