From 3d9563a0a76a6ed79e83d7d3676cb41bc893b1c4 Mon Sep 17 00:00:00 2001 From: Hood Chatham Date: Sun, 8 Feb 2026 11:49:05 -0800 Subject: [PATCH 1/7] Add `IMPORTED_TABLE` setting and `RELOCATABLE` now implies `IMPORTED_TABLE`. Resolves issue 26217 --- src/lib/libcore.js | 3 +-- src/settings.js | 8 ++++++++ test/test_core.py | 14 ++++++++++++++ tools/building.py | 11 +++++++---- tools/emscripten.py | 17 +++++++++++------ tools/link.py | 6 ++++++ tools/webassembly.py | 16 ++++++++++++---- 7 files changed, 59 insertions(+), 16 deletions(-) diff --git a/src/lib/libcore.js b/src/lib/libcore.js index 1fb5e90f2597b..95e7c31b72955 100644 --- a/src/lib/libcore.js +++ b/src/lib/libcore.js @@ -2254,8 +2254,7 @@ addToLibrary({ }, $wasmTable__docs: '/** @type {WebAssembly.Table} */', -#if RELOCATABLE - // In RELOCATABLE mode we create the table in JS. +#if IMPORTED_TABLE $wasmTable: `=new WebAssembly.Table({ 'initial': {{{ toIndexType(INITIAL_TABLE) }}}, #if !ALLOW_TABLE_GROWTH diff --git a/src/settings.js b/src/settings.js index 9a755c2b50bb4..e5ce11377df58 100644 --- a/src/settings.js +++ b/src/settings.js @@ -2142,6 +2142,14 @@ var PURE_WASI = false; // [link] var IMPORTED_MEMORY = false; +// Set to 1 to define the WebAssembly.Table object outside of the wasm module. +// By default the wasm module defines the table and exports it to JavaScript. +// Use of the `RELOCATABLE` setting will enable this setting since it depends +// on defining the table in JavaScript. +// +// [link] +var IMPORTED_TABLE = false; + // Generate code to load split wasm modules. // This option will automatically generate two wasm files as output, one // with the ``.orig`` suffix and one without. The default file (without diff --git a/test/test_core.py b/test/test_core.py index 03aed366e18c4..f6a80acb90a6c 100644 --- a/test/test_core.py +++ b/test/test_core.py @@ -2258,6 +2258,20 @@ def test_module_wasm_memory(self): self.set_setting('INCOMING_MODULE_JS_API', ['wasmMemory']) self.do_runf('core/test_module_wasm_memory.c', 'success', cflags=['--pre-js', test_file('core/test_module_wasm_memory.js')]) + def test_module_wasm_table(self): + create_file('pre.js', ''' + Module['preRun'] = () => { + assert(typeof wasmTable === 'object', 'wasmTable should be defined'); + console.log('table check passed'); + }; + ''') + # Without IMPORTED_TABLE, wasmTable is not yet defined when pre.js code is run + self.do_runf('hello_world.c', 'wasmTable should be defined', + cflags=['--pre-js', 'pre.js'], assert_returncode=NON_ZERO) + # With IMPORTED_TABLE, wasmTable is available + self.set_setting('IMPORTED_TABLE') + self.do_runf('hello_world.c', 'table check passed', cflags=['--pre-js', 'pre.js']) + def test_ssr(self): # struct self-ref src = ''' #include diff --git a/tools/building.py b/tools/building.py index 182fb3ea311e5..d90a77cae36bb 100644 --- a/tools/building.py +++ b/tools/building.py @@ -176,6 +176,13 @@ def lld_flags_for_executable(external_symbols): if settings.IMPORTED_MEMORY: cmd.append('--import-memory') + if settings.IMPORTED_TABLE: + cmd.append('--import-table') + else: + cmd.append('--export-table') + if settings.ALLOW_TABLE_GROWTH: + cmd.append('--growable-table') + if settings.SHARED_MEMORY: cmd.append('--shared-memory') @@ -235,10 +242,6 @@ def lld_flags_for_executable(external_symbols): cmd.append('-pie') if not settings.LINKABLE: cmd.append('--no-export-dynamic') - else: - cmd.append('--export-table') - if settings.ALLOW_TABLE_GROWTH: - cmd.append('--growable-table') if not settings.SIDE_MODULE: cmd += ['-z', 'stack-size=%s' % settings.STACK_SIZE] diff --git a/tools/emscripten.py b/tools/emscripten.py index 2f3a484a8b7c0..8fefae3a365e6 100644 --- a/tools/emscripten.py +++ b/tools/emscripten.py @@ -429,11 +429,14 @@ def emscript(in_wasm, out_wasm, outfile_js, js_syms, finalize=True, base_metadat set_memory(static_bump) logger.debug('stack_low: %d, stack_high: %d, heap_base: %d', settings.STACK_LOW, settings.STACK_HIGH, settings.HEAP_BASE) - # When building relocatable output (e.g. MAIN_MODULE) the reported table - # size does not include the reserved slot at zero for the null pointer. - # So we need to offset the elements by 1. - if settings.INITIAL_TABLE == -1: - settings.INITIAL_TABLE = dylink_sec.table_size + 1 + if settings.IMPORTED_TABLE and settings.INITIAL_TABLE == -1: + # For builds with IMPORTED_TABLE, get the table size from the wasm module's + # table import. + with webassembly.Module(in_wasm) as module: + table_import = module.get_function_table_import() + if not table_import: + exit_with_error('IMPORTED_TABLE requires a table import in the wasm module') + settings.INITIAL_TABLE = table_import.limits.initial if metadata.invoke_funcs: settings.DEFAULT_LIBRARY_FUNCS_TO_INCLUDE += ['$getWasmTableEntry'] @@ -823,8 +826,10 @@ def add_standard_wasm_imports(send_items_map): if settings.IMPORTED_MEMORY: send_items_map['memory'] = 'wasmMemory' - if settings.RELOCATABLE: + if settings.IMPORTED_TABLE: send_items_map['__indirect_function_table'] = 'wasmTable' + + if settings.RELOCATABLE: if settings.MEMORY64: send_items_map['__table_base32'] = '___table_base32' diff --git a/tools/link.py b/tools/link.py index 03be0d432ed06..f6a1857d47ce5 100644 --- a/tools/link.py +++ b/tools/link.py @@ -1167,6 +1167,9 @@ def limit_incoming_module_api(): settings.DEFAULT_LIBRARY_FUNCS_TO_INCLUDE.append('$wasmMemory') + if settings.IMPORTED_TABLE: + settings.DEFAULT_LIBRARY_FUNCS_TO_INCLUDE.append('$wasmTable') + if 'noExitRuntime' in settings.INCOMING_MODULE_JS_API: settings.DEFAULT_LIBRARY_FUNCS_TO_INCLUDE.append('$noExitRuntime') @@ -1599,6 +1602,9 @@ def limit_incoming_module_api(): if settings.PTHREADS or settings.WASM_WORKERS or settings.RELOCATABLE: settings.IMPORTED_MEMORY = 1 + if settings.RELOCATABLE: + settings.IMPORTED_TABLE = 1 + set_initial_memory() # When not declaring wasm module exports in outer scope one by one, disable minifying diff --git a/tools/webassembly.py b/tools/webassembly.py index 12aa31d884997..f30c3c4df6114 100644 --- a/tools/webassembly.py +++ b/tools/webassembly.py @@ -178,7 +178,7 @@ class InvalidWasmError(BaseException): Section = namedtuple('Section', ['type', 'size', 'offset', 'name']) Limits = namedtuple('Limits', ['flags', 'initial', 'maximum']) -Import = namedtuple('Import', ['kind', 'module', 'field', 'type']) +Import = namedtuple('Import', ['kind', 'module', 'field', 'type', 'limits']) Export = namedtuple('Export', ['name', 'kind', 'index']) Global = namedtuple('Global', ['type', 'mutable', 'init']) Dylink = namedtuple('Dylink', ['mem_size', 'mem_align', 'table_size', 'table_align', 'needed', 'export_info', 'import_info', 'runtime_paths']) @@ -412,6 +412,7 @@ def get_imports(self): field = self.read_string() kind = ExternType(self.read_byte()) type_ = None + limits = None match kind: case ExternType.FUNC: type_ = self.read_uleb() @@ -419,19 +420,26 @@ def get_imports(self): type_ = self.read_sleb() self.read_byte() # mutable case ExternType.MEMORY: - self.read_limits() # limits + limits = self.read_limits() # limits case ExternType.TABLE: type_ = self.read_sleb() - self.read_limits() # limits + limits = self.read_limits() # limits case ExternType.TAG: self.read_byte() # attribute type_ = self.read_uleb() case _: raise AssertionError() - imports.append(Import(kind, mod, field, type_)) + imports.append(Import(kind, mod, field, type_, limits)) return imports + @memoize + def get_function_table_import(self): + for import_ in self.get_imports(): + if import_.module == 'env' and import_.field == '__indirect_function_table': + return import_ + return None + @memoize def get_globals(self): global_section = self.get_section(SecType.GLOBAL) From aa72e94c84aa428e824e70cc3075ee1ee7607c64 Mon Sep 17 00:00:00 2001 From: Hood Chatham Date: Sun, 8 Feb 2026 15:31:35 -0800 Subject: [PATCH 2/7] Fix test name --- test/test_core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_core.py b/test/test_core.py index f6a80acb90a6c..4c1c81bfc0e19 100644 --- a/test/test_core.py +++ b/test/test_core.py @@ -2258,7 +2258,7 @@ def test_module_wasm_memory(self): self.set_setting('INCOMING_MODULE_JS_API', ['wasmMemory']) self.do_runf('core/test_module_wasm_memory.c', 'success', cflags=['--pre-js', test_file('core/test_module_wasm_memory.js')]) - def test_module_wasm_table(self): + def test_imported_table(self): create_file('pre.js', ''' Module['preRun'] = () => { assert(typeof wasmTable === 'object', 'wasmTable should be defined'); From 1257fc70305b375f5ebb95fc8ccf1be8b05a9527 Mon Sep 17 00:00:00 2001 From: Hood Chatham Date: Sun, 8 Feb 2026 20:27:14 -0800 Subject: [PATCH 3/7] Tidy up --- tools/emscripten.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tools/emscripten.py b/tools/emscripten.py index 8fefae3a365e6..3ee91ad22c301 100644 --- a/tools/emscripten.py +++ b/tools/emscripten.py @@ -829,9 +829,8 @@ def add_standard_wasm_imports(send_items_map): if settings.IMPORTED_TABLE: send_items_map['__indirect_function_table'] = 'wasmTable' - if settings.RELOCATABLE: - if settings.MEMORY64: - send_items_map['__table_base32'] = '___table_base32' + if settings.RELOCATABLE and settings.MEMORY64: + send_items_map['__table_base32'] = '___table_base32' if settings.AUTODEBUG: extra_sent_items += [ From 46798c1de9c3a48a23ab1ef981fdde525624da40 Mon Sep 17 00:00:00 2001 From: Hood Chatham Date: Sun, 8 Feb 2026 20:35:22 -0800 Subject: [PATCH 4/7] Have to add one to table size for relocatable --- tools/emscripten.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tools/emscripten.py b/tools/emscripten.py index 3ee91ad22c301..cf9cac1d4be91 100644 --- a/tools/emscripten.py +++ b/tools/emscripten.py @@ -437,6 +437,11 @@ def emscript(in_wasm, out_wasm, outfile_js, js_syms, finalize=True, base_metadat if not table_import: exit_with_error('IMPORTED_TABLE requires a table import in the wasm module') settings.INITIAL_TABLE = table_import.limits.initial + if settings.RELOCATABLE: + # When building relocatable output (e.g. MAIN_MODULE) the reported table + # size does not include the reserved slot at zero for the null pointer. + # So we need to offset the elements by 1. + settings.INITIAL_TABLE += 1 if metadata.invoke_funcs: settings.DEFAULT_LIBRARY_FUNCS_TO_INCLUDE += ['$getWasmTableEntry'] From 092a91fe665f7f2e75c66412cc565f406583d18c Mon Sep 17 00:00:00 2001 From: Hood Chatham Date: Mon, 9 Feb 2026 08:15:13 -0800 Subject: [PATCH 5/7] Skip wasm2js tests --- test/test_core.py | 1 + 1 file changed, 1 insertion(+) diff --git a/test/test_core.py b/test/test_core.py index 4c1c81bfc0e19..650ac9b273422 100644 --- a/test/test_core.py +++ b/test/test_core.py @@ -2258,6 +2258,7 @@ def test_module_wasm_memory(self): self.set_setting('INCOMING_MODULE_JS_API', ['wasmMemory']) self.do_runf('core/test_module_wasm_memory.c', 'success', cflags=['--pre-js', test_file('core/test_module_wasm_memory.js')]) + @no_wasm2js('no WebAssembly.Table()') def test_imported_table(self): create_file('pre.js', ''' Module['preRun'] = () => { From 2c3871870b7b160a1affc0c223addc8a9456db04 Mon Sep 17 00:00:00 2001 From: Hood Chatham Date: Mon, 9 Feb 2026 09:37:56 -0800 Subject: [PATCH 6/7] Update settings doc --- .../docs/tools_reference/settings_reference.rst | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/site/source/docs/tools_reference/settings_reference.rst b/site/source/docs/tools_reference/settings_reference.rst index 8941a9d44b207..1246b1dc90258 100644 --- a/site/source/docs/tools_reference/settings_reference.rst +++ b/site/source/docs/tools_reference/settings_reference.rst @@ -3244,6 +3244,18 @@ depend on being able to define the memory in JavaScript: Default value: false +.. _imported_table: + +IMPORTED_TABLE +============== + +Set to 1 to define the WebAssembly.Table object outside of the wasm module. +By default the wasm module defines the table and exports it to JavaScript. +Use of the `RELOCATABLE` setting will enable this setting since it depends +on defining the table in JavaScript. + +Default value: false + .. _split_module: SPLIT_MODULE From 131350b4ccc86306bb1550c8700092dceb04164e Mon Sep 17 00:00:00 2001 From: Hood Chatham Date: Fri, 13 Feb 2026 02:25:22 -0800 Subject: [PATCH 7/7] Add IMPORTED_MEMORY test --- test/test_core.py | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/test/test_core.py b/test/test_core.py index 650ac9b273422..a2b5a56f1b002 100644 --- a/test/test_core.py +++ b/test/test_core.py @@ -2261,17 +2261,32 @@ def test_module_wasm_memory(self): @no_wasm2js('no WebAssembly.Table()') def test_imported_table(self): create_file('pre.js', ''' - Module['preRun'] = () => { + Module['preInit'] = () => { assert(typeof wasmTable === 'object', 'wasmTable should be defined'); console.log('table check passed'); }; ''') - # Without IMPORTED_TABLE, wasmTable is not yet defined when pre.js code is run + # Without IMPORTED_TABLE, wasmTable is not yet defined when preInit is run self.do_runf('hello_world.c', 'wasmTable should be defined', - cflags=['--pre-js', 'pre.js'], assert_returncode=NON_ZERO) + cflags=['--pre-js=pre.js'], assert_returncode=NON_ZERO) # With IMPORTED_TABLE, wasmTable is available self.set_setting('IMPORTED_TABLE') - self.do_runf('hello_world.c', 'table check passed', cflags=['--pre-js', 'pre.js']) + self.do_runf('hello_world.c', 'table check passed', cflags=['--pre-js=pre.js']) + + @no_wasm2js('no WebAssembly.Memory()') + def test_imported_memory(self): + create_file('pre.js', ''' + Module['preInit'] = () => { + assert(typeof wasmMemory === 'object', 'wasmMemory should be defined'); + console.log('memory check passed'); + }; + ''') + # Without IMPORTED_MEMORY, wasmMemory is not yet defined when preInit is run + self.do_runf('hello_world.c', 'wasmMemory should be defined', + cflags=['--pre-js=pre.js'], assert_returncode=NON_ZERO) + # With IMPORTED_MEMORY, wasmMemory is available + self.set_setting('IMPORTED_MEMORY') + self.do_runf('hello_world.c', 'memory check passed', cflags=['--pre-js=pre.js']) def test_ssr(self): # struct self-ref src = '''