How Wasm Handles Dynamic Linking of Multiple Modules
WebAssembly (Wasm) handles dynamic linking of multiple modules at runtime by leveraging exports, imports, shared memory, and function tables, all orchestrated by a host environment like a web browser or a system runtime. Unlike traditional operating systems where the OS loader resolves symbols at runtime, Wasm relies on the host to pass exports from one instantiated module as imports to another. This article explains the core mechanisms behind Wasm dynamic linking, including the role of the host, WebAssembly tables, shared linear memory, and the emerging WebAssembly Component Model.
The Role of the Host Environment
WebAssembly modules are isolated sandboxes that cannot directly access each other’s address spaces or functions without explicit permission. Because Wasm lacks a native runtime loader, the host environment (such as JavaScript in the browser, or runtimes like Wasmtime and Wasmer on the server) acts as the dynamic linker.
During runtime, the process typically follows these steps: 1. The host compiles and instantiates a “library” Wasm module. 2. The host extracts the exported functions, memories, or tables from this library module. 3. The host compiles a “main” Wasm module that declares these same functions, memories, or tables as imports. 4. The host instantiates the main module, passing the library’s exports into the main module’s import object.
This mechanism allows the main module to call functions defined in the library module seamlessly.
Shared Memory and Globals
For dynamic linking to be useful, modules often need to share state
and data pointers. Wasm accomplishes this by sharing a single
WebAssembly.Memory object across multiple modules.
- Imported Memory: Instead of each module allocating its own private linear memory, the host creates a single memory object and passes it as an import to all linked modules. This allows pointers (which are simply integer offsets in Wasm) to remain valid when passed from one module to another.
- Global Shadow Stack and Heap: Toolchains (like Emscripten or LLVM) ensure that the linked modules agree on how this shared memory is partitioned, preventing them from overwriting each other’s stack or heap allocations.
WebAssembly Tables and Function Pointers
In languages like C or C++, dynamic linking relies heavily on
function pointers (for callbacks, virtual method tables, or dynamic
loading via dlopen). WebAssembly enforces strict security,
meaning you cannot call a raw memory address as a function.
To solve this, Wasm uses Tables. A Table is a secure
array of reference types (usually function references) that resides
outside of the linear memory. * To dynamically link functions, modules
share a single WebAssembly.Table. * When a module needs to
call a dynamically linked function via a pointer, it uses the
call_indirect instruction, referencing an index in the
shared Table. * The host or the loading module populates this table at
runtime with the appropriate function references.
Toolchain Implementation: The dlopen Model
To make dynamic linking feel familiar to developers, toolchains like
Emscripten and WASI-SDK implement standard C library functions like
dlopen and dlsym using Wasm’s primitives.
When a program calls dlopen at runtime: 1. The
system-level Wasm code calls out to the host environment. 2. The host
fetches, compiles, and instantiates the requested .wasm
shared library module. 3. The host integrates the new module’s functions
into the shared Table and maps its memory requirements. 4.
dlsym then returns the index of the requested function
within the WebAssembly Table, allowing the application to call it
indirectly.
The WebAssembly Component Model
The traditional approach to dynamic linking requires tight coordination and often forces modules to share the same language toolchain and memory layout. The WebAssembly Component Model is a newer specification designed to standardize and simplify this process.
The Component Model introduces: * Interfaces (WIT): WebAssembly Interface Type files define the boundaries and types of a component without exposing raw memory layouts. * Shared-Nothing Linking: Instead of sharing a single linear memory (which poses security risks), components copy data across boundaries using canonical ABI definitions. * Declarative Composition: Tools can compose multiple independent Wasm components (even those written in different programming languages) into a single executable component before or during runtime, automating the generation of the necessary import and export glue code.