Skip to content
4 changes: 4 additions & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ See docs/process.md for more on how version tagging works.

4.0.22 (in development)
-----------------------
- Source maps now support 'names' field with function name information.
emsymbolizer will show function names when used with a source map. The size
of source maps may increase 2-3x and the link time can increase slightly due
to more processing on source map creation. (#25870)
- The minimum version of python required to run emscripten was updated from 3.8
to 3.10. (#25891)

Expand Down
26 changes: 26 additions & 0 deletions test/core/test_dwarf.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#include <emscripten.h>

EM_JS(int, out_to_js, (int x), {})

class MyClass {
public:
void foo();
void bar();
};

void __attribute__((noinline)) MyClass::foo() {
out_to_js(0); // line 12
out_to_js(1);
out_to_js(2);
}

void __attribute__((always_inline)) MyClass::bar() {
out_to_js(3);
__builtin_trap(); // line 19
}

int main() {
MyClass mc;
mc.foo();
mc.bar();
}
88 changes: 63 additions & 25 deletions test/test_other.py
Original file line number Diff line number Diff line change
Expand Up @@ -9631,12 +9631,49 @@ def check_dwarf_loc_info(address, funcs, locs):
for loc in locs:
self.assertIn(loc, out)

def check_source_map_loc_info(address, loc):
def check_source_map_loc_info(address, func, loc):
out = self.run_process(
[emsymbolizer, '-s', 'sourcemap', 'test_dwarf.wasm', address],
stdout=PIPE).stdout
self.assertIn(func, out)
self.assertIn(loc, out)

def do_tests(src):
# 1. Test DWARF + source map together
# For DWARF, we check for the full inlined info for both function names and
# source locations. Source maps does not provide inlined info. So we only
# check for the info of the outermost function.
self.run_process([EMCC, test_file(src), '-g', '-gsource-map', '-O1', '-o',
'test_dwarf.js'])
check_dwarf_loc_info(out_to_js_call_addr, out_to_js_call_func,
out_to_js_call_loc)
check_source_map_loc_info(out_to_js_call_addr, out_to_js_call_func[0],
out_to_js_call_loc[0])
check_dwarf_loc_info(unreachable_addr, unreachable_func, unreachable_loc)
# Source map shows the original (inlined) source location with the original
# function name
check_source_map_loc_info(unreachable_addr, unreachable_func[0],
unreachable_loc[0])

# 2. Test source map only
# The addresses, function names, and source locations are the same across
# the builds because they are relative offsets from the code section, so we
# don't need to recompute them
self.run_process([EMCC, test_file(src), '-gsource-map', '-O1', '-o',
'test_dwarf.js'])
check_source_map_loc_info(out_to_js_call_addr, out_to_js_call_func[0],
out_to_js_call_loc[0])
check_source_map_loc_info(unreachable_addr, unreachable_func[0],
unreachable_loc[0])

# 3. Test DWARF only
self.run_process([EMCC, test_file(src), '-g', '-O1', '-o',
'test_dwarf.js'])
check_dwarf_loc_info(out_to_js_call_addr, out_to_js_call_func,
out_to_js_call_loc)
check_dwarf_loc_info(unreachable_addr, unreachable_func, unreachable_loc)

# -- C program test --
# We test two locations within test_dwarf.c:
# out_to_js(0); // line 6
# __builtin_trap(); // line 13
Expand All @@ -9659,31 +9696,32 @@ def check_source_map_loc_info(address, loc):
# The first one corresponds to the innermost inlined location.
unreachable_loc = ['test_dwarf.c:13:3', 'test_dwarf.c:18:3']

# 1. Test DWARF + source map together
# For DWARF, we check for the full inlined info for both function names and
# source locations. Source maps provide neither function names nor inlined
# info. So we only check for the source location of the outermost function.
check_dwarf_loc_info(out_to_js_call_addr, out_to_js_call_func,
out_to_js_call_loc)
check_source_map_loc_info(out_to_js_call_addr, out_to_js_call_loc[0])
check_dwarf_loc_info(unreachable_addr, unreachable_func, unreachable_loc)
check_source_map_loc_info(unreachable_addr, unreachable_loc[0])

# 2. Test source map only
# The addresses, function names, and source locations are the same across
# the builds because they are relative offsets from the code section, so we
# don't need to recompute them
self.run_process([EMCC, test_file('core/test_dwarf.c'),
'-gsource-map', '-O1', '-o', 'test_dwarf.js'])
check_source_map_loc_info(out_to_js_call_addr, out_to_js_call_loc[0])
check_source_map_loc_info(unreachable_addr, unreachable_loc[0])
do_tests('core/test_dwarf.c')

# 3. Test DWARF only
self.run_process([EMCC, test_file('core/test_dwarf.c'),
'-g', '-O1', '-o', 'test_dwarf.js'])
check_dwarf_loc_info(out_to_js_call_addr, out_to_js_call_func,
out_to_js_call_loc)
check_dwarf_loc_info(unreachable_addr, unreachable_func, unreachable_loc)
# -- C++ program test --
# We test two locations within test_dwarf.cpp:
# out_to_js(0); // line 12
# __builtin_trap(); // line 19
self.run_process([EMCC, test_file('core/test_dwarf.cpp'),
'-g', '-gsource-map', '-O1', '-o', 'test_dwarf.js'])
# Address of out_to_js(0) within MyClass::foo(), uninlined
out_to_js_call_addr = self.get_instr_addr('call\t0', 'test_dwarf.wasm')
# Address of __builtin_trap() within MyClass::bar(), inlined into main()
unreachable_addr = self.get_instr_addr('unreachable', 'test_dwarf.wasm')

# Function name of out_to_js(0) within MyClass::foo(), uninlined
out_to_js_call_func = ['MyClass::foo()']
# Function names of __builtin_trap() within MyClass::bar(), inlined into
# main(). The first one corresponds to the innermost inlined function.
unreachable_func = ['MyClass::bar()', 'main']

# Source location of out_to_js(0) within MyClass::foo(), uninlined
out_to_js_call_loc = ['test_dwarf.cpp:12:3']
# Source locations of __builtin_trap() within MyClass::bar(), inlined into
# main(). The first one corresponds to the innermost inlined location.
unreachable_loc = ['test_dwarf.cpp:19:3', 'test_dwarf.cpp:25:6']

do_tests('core/test_dwarf.cpp')

def test_emsymbolizer_functions(self):
'Test emsymbolizer use cases that only provide function-granularity info'
Expand Down
8 changes: 7 additions & 1 deletion tools/emsymbolizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ class Location:
def __init__(self):
self.version = None
self.sources = []
self.funcs = []
self.mappings = {}
self.offsets = []

Expand All @@ -128,6 +129,7 @@ def parse(self, filename):

self.version = source_map_json['version']
self.sources = source_map_json['sources']
self.funcs = source_map_json['names']

chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='
vlq_map = {c: i for i, c in enumerate(chars)}
Expand Down Expand Up @@ -155,6 +157,7 @@ def decodeVLQ(string):
src = 0
line = 1
col = 1
func = 0
for segment in source_map_json['mappings'].split(','):
data = decodeVLQ(segment)
info = []
Expand All @@ -169,7 +172,9 @@ def decodeVLQ(string):
if len(data) >= 4:
col += data[3]
info.append(col)
# TODO: see if we need the name, which is the next field (data[4])
if len(data) == 5:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be >= 5 like above on line 173?

Copy link
Member Author

@aheejin aheejin Dec 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It can be 5 at maximum: https://tc39.es/ecma426/#sec-mappings

func += data[4]
info.append(func)

self.mappings[offset] = WasmSourceMap.Location(*info)
self.offsets.append(offset)
Expand Down Expand Up @@ -207,6 +212,7 @@ def lookup(self, offset, lower_bound=None):
self.sources[info.source] if info.source is not None else None,
info.line,
info.column,
self.funcs[info.func] if info.func is not None else None,
)


Expand Down
Loading