Exporting Symbols with wasm-ld Flags
This page answers one task: make a C, C++ or Rust function callable from JavaScript (or from a host runtime) by exporting it
from the final .wasm file — and make sure nothing else leaks out as an export.
Prerequisites
- [ ] clang with the
wasm32target (from LLVM, the WASI SDK or emsdk) or Rust targetingwasm32-unknown-unknown. - [ ]
wasm-ld, which ships with LLVM and is what clang and rustc invoke for Wasm targets. - [ ] WABT’s
wasm-objdumpto inspect the export section.
What decides whether a function is exported
When the compiler finishes, each object file contains functions with symbol names and visibility. The linker, wasm-ld,
combines those objects into one module and decides which symbols become entries in the module’s export section — the list
the host sees after instantiation. By default it exports almost nothing: the entry point for a command (_start), the memory
(as memory), and whatever the compiler or toolchain has marked for export. Everything else is internal, and if nothing calls
it, the linker’s garbage collection removes it entirely.
That default is deliberate. A module’s exports are its public API, and every exported function is kept alive regardless of whether anything calls it, defeating dead-code elimination. So exporting is opt-in, symbol by symbol, through either attributes in the source or flags on the link.
Step 1 — export from the source with an attribute
The most maintainable way to export is to mark the function where it is defined. In C and C++ with clang:
// math.c
__attribute__((export_name("add")))
int add(int a, int b) { return a + b; }
__attribute__((export_name("dot3")))
float dot3(const float *a, const float *b) {
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
}
static int helper(int x) { return x * 2; } // never exported
export_name both marks the function for export and sets the name it is exported under, which can differ from the C symbol.
In Rust, #[no_mangle] pub extern "C" fn exports a function under its own name from a cdylib; wasm-bindgen’s
#[wasm_bindgen] does the same and adds generated glue.
clang --target=wasm32 -O2 -nostdlib -Wl,--no-entry math.c -o math.wasm
--no-entry tells the linker this is a library-style module with no _start, which would otherwise be required and would
fail with an undefined-symbol error for _start.
Step 2 — export from the link command
When you cannot or do not want to change the source — a third-party library, or a build where exports are configured per product — name the symbols on the link:
clang --target=wasm32 -O2 -nostdlib -c math.c -o math.o
wasm-ld --no-entry --export=add --export=dot3 math.o -o math.wasm
Through the clang driver, pass linker flags with -Wl,:
clang --target=wasm32 -O2 -nostdlib math.c -o math.wasm \
-Wl,--no-entry -Wl,--export=add -Wl,--export=dot3
--export fails the link if the symbol does not exist, which is useful: a typo is an error, not a silently missing export. When
a symbol is optional — present in some configurations and not others — use --export-if-defined=name, which exports it if it
exists and ignores it otherwise.
Step 3 — avoid exporting everything
Two flags export broadly, and both are usually the wrong choice for a shipped module. --export-all exports every function with
a symbol, including internal helpers and libc routines; the module gets larger, slower to compile, and its interface becomes
whatever happened to be linked. --export-dynamic exports all symbols with default visibility, which in C is every non-static
function. Both are useful for debugging — to call internals from a test — and harmful in release builds.
The fix for C code is to compile with hidden visibility by default, so only explicitly marked functions are candidates:
clang --target=wasm32 -O2 -fvisibility=hidden -nostdlib math.c -o math.wasm -Wl,--no-entry
With -fvisibility=hidden, a function needs export_name (or __attribute__((visibility("default"))) together with
--export-dynamic) to be exported. That makes the export list an explicit, reviewable decision.
Step 4 — export globals and the memory
Data can be exported too. A global variable marked for export appears as an exported global holding its address in linear memory, which JavaScript can use to locate the data:
__attribute__((export_name("lookup_table")))
const unsigned char lookup_table[256] = { /* ... */ };
const { instance } = await WebAssembly.instantiate(bytes, {});
const addr = instance.exports.lookup_table.value; // a WebAssembly.Global
const table = new Uint8Array(instance.exports.memory.buffer, addr, 256);
The memory itself is exported as memory by default. If the module should use memory created by the host instead, see
importing memory from the host with --import-memory.
Treat the export list as an API
Once a module ships, its exports are a contract with every host that loads it. JavaScript code, wrappers and other modules refer to exports by name and by signature, and changing either breaks them — usually at instantiation or first call, rather than at build time. It pays to manage the export list the way you would manage a library’s public interface.
Keep it small. Every export is a promise to keep a function with that name and signature, and it is a root that keeps code alive for dead-code elimination. A module that exports five well-chosen functions is easier to version, smaller, and simpler to wrap than one that exports fifty convenience entry points.
Keep it explicit. Export names written in the source with export_name, or listed in one linker response file checked into
the repository, can be reviewed in a pull request. Exports that arise from default visibility appear and disappear as code
changes, and nobody notices until a host breaks.
Keep it checked. A test that compares the module’s export list with an expected list fails the build when an export is added or removed accidentally:
wasm-objdump -x -j Export math.wasm | sed -n 's/.*-> "\(.*\)"/\1/p' | sort > exports.actual
diff exports.expected exports.actual || { echo "export list changed"; exit 1; }
That one check catches the most common release mistake in module builds — an export lost because a flag changed — before
any user sees a LinkError or an undefined function.
Step 5 — verify the export section
Always check the result. The export section is short and tells you exactly what the host will see:
wasm-objdump -x -j Export math.wasm
Export[3]:
- memory[0] -> "memory"
- func[0] -> "add"
- func[1] -> "dot3"
helper is absent — not exported and, since nothing calls it, removed. If unexpected entries appear, look for
--export-dynamic or --export-all in the build flags, or for libraries compiled without hidden visibility.
Expected output
From JavaScript, the exports behave like ordinary functions:
const { instance } = await WebAssembly.instantiateStreaming(fetch("math.wasm"));
console.log(instance.exports.add(2, 40)); // 42
console.log(Object.keys(instance.exports)); // [ 'memory', 'add', 'dot3' ]
Gotchas
wasm-ld: error: entry symbol not defined (pass --no-entry to suppress): _start. The module is a library. Add--no-entry.- The function is missing from the exports. It was not marked and not named on the link, so the linker removed it. Add
export_nameor--export. wasm-ld: error: symbol exported via --export not found: dot3. The symbol name differs — C++ name mangling is the usual cause. Declare the functionextern "C", or export by the mangled name.- Dozens of unexpected exports.
--export-allor default visibility. Switch to-fvisibility=hiddenand explicit exports.
Performance note
On a 140 KB C library, linking with --export-all produced a 212 KB module with 1,180 exports; explicit exports of the 14
functions the application used produced 96 KB. The smaller module also compiled in the browser in 11 ms instead of 23 ms, because
the engine had less code to compile.
Frequently Asked Questions
Does Emscripten use these flags?
Emscripten drives wasm-ld itself and exposes exports through -sEXPORTED_FUNCTIONS, which it translates into linker exports
plus JavaScript glue. The underlying mechanism is the same.
Can an export have a different name from the C function?
Yes — export_name("public_name") sets it independently of the C symbol, which is useful for stable public names over
internal renames.
Can I export a function only in debug builds?
Yes: put --export=debug_dump in the debug link flags only, or guard the attribute with a preprocessor check. Keeping debug entry
points out of release exports also keeps their code out of the release module.
Are exported functions always kept, even if unused? Yes. Exports are roots for dead-code elimination; anything reachable from an export stays. That is why minimal exports produce smaller modules.
How do I export from Rust without wasm-bindgen?
#[no_mangle] pub extern "C" fn name(...) in a cdylib crate is exported by default. Use -C link-arg=--export=... for extra
symbols, or #[export_name = "..."] to rename.
Related
- Fixing undefined symbol errors from wasm-ld — the inverse problem: imports.
- Building a Wasm module without libc — the flags used above, explained.
- Reading the import and export sections — how exports are encoded.
- Calling C functions from JavaScript with ccall and cwrap — the Emscripten-side equivalent.
← Back to Linking Wasm Objects with wasm-ld