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 wasm32 target (from LLVM, the WASI SDK or emsdk) or Rust targeting wasm32-unknown-unknown.
  • [ ] wasm-ld, which ships with LLVM and is what clang and rustc invoke for Wasm targets.
  • [ ] WABT’s wasm-objdump to 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.

From a C function to an entry in the export section A function compiled into an object file becomes an export only if the source marks it with an export attribute or the link command names it with --export. Otherwise the linker treats it as internal and garbage-collects it when nothing calls it. int add(int,int) in math.c math.o symbol hidden by default wasm-ld decides attribute or --export? export section "add" → func 3 Without either signal the function is internal, and --gc-sections removes it if nothing reachable calls it.

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.

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.

Ways to choose exports, and what each costs Explicit per-symbol exports keep the interface minimal and let dead-code elimination work. Exporting dynamically or exporting everything grows the module and turns internals into public API. explicit (export_name / --export) only named functions exported typos fail the link everything else can be removed release builds --export-dynamic every default-visibility symbol controlled by -fvisibility easy to leak helpers plugins with many entry points --export-all every function, libc included largest module, slowest compile internals become API debugging only

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_name or --export.
  • wasm-ld: error: symbol exported via --export not found: dot3. The symbol name differs — C++ name mangling is the usual cause. Declare the function extern "C", or export by the mangled name.
  • Dozens of unexpected exports. --export-all or default visibility. Switch to -fvisibility=hidden and 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.

Module size by export strategy for the same library A C library linked three ways. Exporting everything keeps every function alive; explicit exports let the linker remove what the application does not use. module size (KB) after wasm-opt -O2 --export-all 212 KB --export-dynamic, default visibility 168 KB explicit export_name × 14 96 KB

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.

← Back to Linking Wasm Objects with wasm-ld