Reading the name Custom Section

This page answers one question: where do function names in WebAssembly stack traces and profiles come from, what exactly is in the name section, and should you ship it?

Prerequisites

  • [ ] A module built with names (most dev builds) and one without (a stripped release build) to compare.
  • [ ] WABT’s wasm-objdump and wasm-strip, or wasm-tools.

Names are optional metadata

WebAssembly code refers to functions, locals and globals by index. The code section contains no names at all; a function is “function 412”, and its third local is “local 2”. Names exist only in a custom section — a section with id 0 and a string name — called name. Engines are required to ignore malformed custom sections and are not required to use them, but in practice every browser and runtime reads the name section to label stack frames, profiler entries and debugger views.

That makes the name section the difference between a crash report that says at app::parser::parse_header and one that says at wasm-function[412]. It is also pure metadata: removing it changes nothing about how the module runs, only how readable it is when something goes wrong.

The same stack trace with and without the name section With the name section, browser stack traces and profiles show source-level function names. Without it, they show function indices that must be mapped back by hand using an unstripped build. name section present at parse_header (wasm-function[412]) profiler shows readable frames debugger lists locals by name readable, slightly larger name section stripped at wasm-function[412] profiler shows indices needs the unstripped build to decode smaller, opaque

Step 1 — see whether a module has names

wasm-objdump -h app.wasm | grep -i 'custom.*name'
wasm-objdump -x -j name app.wasm | head
 Custom start=0x0004f2a1 end=0x0005a8d0 (size=0x0000b62f) "name"
Custom:
 - name: "name"
 - module 
 - func[0] 
 - func[1] 
 - func[2] 

Here the section is 46 KB — names are long in Rust and C++ because they include module paths and, in C++, demangled templates.

Step 2 — understand the subsections

The name section’s content is a sequence of subsections, each with an id byte and a LEB128 size, so readers can skip what they do not need:

00  module name        a single name for the whole module
01  function names     a map: function index → name
02  local names        for each function, a map: local index → name
03  label names        (extended name section) block labels
04  type names         names for type indices
05  table names
06  memory names
07  global names
08  element segment names
09  data segment names

Each map is a LEB128 count followed by (index, name) pairs, where names are LEB128-length-prefixed UTF-8 strings — the same string encoding as imports and exports, described in reading the import and export sections. Toolchains emit subsections 1 and 2 most of the time; the others come from the extended name section proposal and are used by some toolchains for debugging.

Layout of a name custom section A custom section with id 0 and the string "name", containing a module-name subsection, a function-names subsection mapping indices to names, and a local-names subsection mapping each function's locals to names. Each subsection carries its own size so readers can skip it. section id 0, size, "name" custom section header subsection 0: module name "app" subsection 1: function names count, then (index, name) pairs subsection 2: local names per function: (local index, name) pairs

Step 3 — decode a function-name entry

A fragment of the function-names subsection:

01                       subsection id 1: function names
93 0b                    subsection size: 1427 (LEB128)
a2 01                    count: 162 entries
00                       function index 0
1b 77 62 67 3a 3a ...    name length 27, "wbg::__wbg_log_1d3ae13c"
01                       function index 1
1b 61 70 70 3a 3a ...    name length 27, "app::parser::parse_header"

Indices refer to the full function index space, imports included — so imported functions can have names too, which is how stack traces label calls into JavaScript glue.

Step 4 — keep names in development, decide for release

Dev builds keep names by default. Release builds differ by toolchain: wasm-pack and wasm-bindgen keep the name section unless wasm-opt --strip-debug or --strip removes it; Emscripten drops names at -O2 and above unless you pass --profiling-funcs; wasm-ld --strip-all removes them; wasm-strip removes every custom section.

# keep only names, drop DWARF and other debug data
wasm-opt -O3 --strip-dwarf app.wasm -o app.named.wasm
# drop names too
wasm-strip app.wasm -o app.stripped.wasm

The choice is a trade between size and diagnosability. A common compromise is to strip names from the shipped module and keep the unstripped build as an artifact for each release, then translate wasm-function[412] back to parse_header when a crash report arrives — the process described in symbolicating Wasm stack traces in production.

Step 5 — shorten names if you keep them

If you ship names, you can make them cheaper. Rust’s -C symbol-mangling-version=v0 changes little here because the name section stores demangled names, but limiting generic instantiations reduces the number of distinct functions. For C++, names of template-heavy code can be hundreds of bytes each; wasm-opt --strip-dwarf keeps names while dropping debug info, and some teams post-process the section to trim namespace prefixes. Measure before bothering: in many modules, names compress well under Brotli, and the on-the-wire cost is a fraction of the raw size.

Custom sections in general

The name section is the best-known example of a general mechanism. Any section with id 0 is a custom section: a name string followed by arbitrary bytes. Engines must accept custom sections they do not understand and ignore them; they can only use ones they recognise, and must not let a malformed custom section affect execution. That rule is what lets toolchains attach metadata freely without breaking anything.

The ecosystem uses this extensively. DWARF debug information lives in custom sections named .debug_info, .debug_line and so on. The producers section records which languages and tools built the module. target_features records which proposals the code uses, which linkers read when combining objects. wasm-bindgen stores its interface description in a custom section that its CLI consumes and removes. The sourceMappingURL section points debuggers at a source map, and external_debug_info points at a separate DWARF file. The linking and reloc.* sections in object files carry what the linker needs and disappear from the final module.

All of them follow the same rules as name: optional, removable and irrelevant to execution. When a module is larger than expected, listing its custom sections with wasm-objdump -h is a quick check — debug sections left in a release build are a common and easily fixed cause, and the technique for adding your own is in adding custom sections to a Wasm binary.

How tools use the section

Several tools you already use read the name section, and knowing that helps explain their behaviour. Browser DevTools label stack frames and the Sources panel’s function list from it. Profilers — Chrome’s Performance panel, the Firefox Profiler, perf with wasmtime’s perfmap — use it to name samples. wasm-objdump and wasm2wat use it to print $parse_header instead of $func412. twiggy uses it to attribute size to named functions. When any of these shows indices instead of names, the explanation is always the same: the module you are looking at has no name section, or a different build’s section that does not match.

Expected output

The same trap in a module with and without names:

RuntimeError: unreachable
    at app::parser::parse_header (wasm://wasm/8c1e2f1a:wasm-function[1]:0x2a3f)
RuntimeError: unreachable
    at wasm://wasm/8c1e2f1a:wasm-function[1]:0x2a3f

Gotchas

  • Names from a different build. Mapping indices with an unstripped artifact from another build gives wrong names. Archive the exact build.
  • wasm-opt silently stripping names. Some optimization flags remove the section. Pass --debuginfo (-g) to wasm-opt to keep it.
  • Huge C++ names. Template-heavy code can make the section larger than you expect. Measure it.
  • Names on imported functions mistaken for your code. Import entries carry names too, typically the glue’s. Check the import section before assuming a named frame is in your module.
  • Confusing names with DWARF. The name section gives function names only; source lines and variable types come from DWARF sections.

Performance note

For a Rust application, the name section was 46 KB raw and 11 KB after Brotli on a 610 KB module — about 2% of the compressed transfer. For a C++ application it was 410 KB raw and 96 KB compressed, about 7%. Engines parse it lazily or not at all until a name is needed, so it does not affect compile time.

Name section share of the compressed module Size of the name custom section after Brotli compression as a share of the whole compressed module, for a Rust application and a template-heavy C++ application. percent of compressed module Rust application 2 % C++ application 7 %

Frequently Asked Questions

Does the name section affect performance? No. It is not executed and engines read it only when they need a name.

Can I add names to a stripped module? Only by copying the section from an unstripped build of the same code. Names cannot be recovered from the code itself.

Can I rename functions in the section after building? Yes — tools such as wasm-tools can rewrite custom sections, and some teams shorten names this way. Make sure symbolication tools use the same renamed table.

Do names leak information? They reveal function and module names from your source, which some teams consider sensitive. Stripping is the remedy if that matters.

Is this the same as DWARF? No. DWARF is a separate set of custom sections with full debugging information; see debugging Wasm with DWARF and source maps.

← Back to Wasm Binary Format Deep Dive