From b060b177c706861f8c97e5ce4c0d60406a126291 Mon Sep 17 00:00:00 2001 From: Brendan Dahl Date: Thu, 5 Mar 2026 23:50:02 +0000 Subject: [PATCH 1/3] Unify module splitting implementation for JSPI Previously, module splitting with JSPI required a special built-in function `__load_secondary_module` and wasm-split setting to coordinate the loading of the secondary module. This change updates the `splitModuleProxyHandler` to support JSPI by wrapping the generated placeholder functions in `WebAssembly.Suspending`. The placeholder will then automatically load the secondary module asynchronously. A standalone `save_profile_data.js` script is extracted from existing tests for reuse, and a new test `test_split_module_embind_jspi` is added to ensure module splitting functions correctly alongside embind and JSPI. --- src/lib/libasync.js | 11 ------- src/preamble.js | 17 +++++----- test/other/save_profile_data.js | 21 ++++++++++++ test/other/test_split_module.post.js | 21 ++---------- test/other/test_split_module_embind_jspi.cpp | 15 +++++++++ test/other/test_split_module_embind_jspi.out | 2 ++ .../test_split_module_embind_jspi.post.js | 13 ++++++++ .../test_split_module_embind_jspi.pre.js | 5 +++ test/test_other.py | 32 +++++++++++++++++-- tools/emscripten.py | 5 --- tools/link.py | 2 -- tools/maint/gen_sig_info.py | 2 +- 12 files changed, 98 insertions(+), 48 deletions(-) create mode 100644 test/other/save_profile_data.js create mode 100644 test/other/test_split_module_embind_jspi.cpp create mode 100644 test/other/test_split_module_embind_jspi.out create mode 100644 test/other/test_split_module_embind_jspi.post.js create mode 100644 test/other/test_split_module_embind_jspi.pre.js diff --git a/src/lib/libasync.js b/src/lib/libasync.js index 0045b8004c5e2..17ca2c0b011d4 100644 --- a/src/lib/libasync.js +++ b/src/lib/libasync.js @@ -514,17 +514,6 @@ addToLibrary({ }); }, - _load_secondary_module__sig: 'v', - _load_secondary_module__async: 'auto', - _load_secondary_module: async function() { - // Mark the module as loading for the wasm module (so it doesn't try to load it again). - wasmExports['load_secondary_module_status'].value = 1; - var imports = {'primary': wasmRawExports}; - // Replace '.wasm' suffix with '.deferred.wasm'. - var deferred = wasmBinaryFile.slice(0, -5) + '.deferred.wasm'; - await instantiateAsync(null, deferred, imports); - }, - $Fibers__deps: ['$Asyncify', 'emscripten_stack_set_limits', '$stackRestore'], $Fibers: { nextFiber: 0, diff --git a/src/preamble.js b/src/preamble.js index ddc3711dc7510..499ee4e7081f7 100644 --- a/src/preamble.js +++ b/src/preamble.js @@ -506,7 +506,7 @@ async function getWasmBinary(binaryFile) { #endif #if SPLIT_MODULE -{{{ makeModuleReceiveWithVar('loadSplitModule', undefined, 'instantiateSync') }}} +{{{ makeModuleReceiveWithVar('loadSplitModule', undefined, JSPI ? '(secondaryFile, imports) => instantiateAsync(null, secondaryFile, imports)' : 'instantiateSync') }}} var splitModuleProxyHandler = { get(target, moduleName, receiver) { if (moduleName.startsWith('placeholder')) { @@ -519,22 +519,23 @@ var splitModuleProxyHandler = { } return new Proxy({}, { get(target, base, receiver) { - return (...args) => { -#if ASYNCIFY == 2 - throw new Error('Placeholder function "' + base + '" should not be called when using JSPI.'); -#else + let ret = {{{ asyncIf(ASYNCIFY == 2) }}} (...args) => { #if RUNTIME_DEBUG dbg(`placeholder function called: ${base}`); #endif var imports = {'primary': wasmRawExports}; // Replace '.wasm' suffix with '.deferred.wasm'. - loadSplitModule(secondaryFile, imports, base); + {{{ awaitIf(ASYNCIFY == 2) }}}loadSplitModule(secondaryFile, imports, base); #if RUNTIME_DEBUG dbg('instantiated deferred module, continuing'); #endif - return wasmTable.get({{{ toIndexType('base') }}})(...args); + return wasmTable.get({{{toIndexType('base')}}})(...args); + }; +#if JSPI + return new WebAssembly.Suspending(ret); +#else + return ret; #endif - } } }); } diff --git a/test/other/save_profile_data.js b/test/other/save_profile_data.js new file mode 100644 index 0000000000000..31aaa42cfaa4b --- /dev/null +++ b/test/other/save_profile_data.js @@ -0,0 +1,21 @@ +function save_profile_data() { + var __write_profile = wasmExports['__write_profile']; + if (__write_profile) { + var len = __write_profile({{{ to64('0') }}}, 0); + var offset = _malloc(len); + var actualLen = __write_profile({{{ to64('offset') }}}, len); + var profile_data = HEAPU8.subarray(offset, offset + len); + if (typeof writeFile !== 'undefined') { + console.log('using writeFile') + writeFile('profile.data', profile_data); + } else if (typeof fs !== 'undefined') { + console.log('using fs.writeFileSync') + fs.writeFileSync('profile.data', profile_data); + } else { + console.log(JSON.stringify(Array.from(profile_data))); + } + console.log('profile size is', actualLen, 'bytes (allocated', len, 'bytes)'); + console.log('wrote profile data') + _free(offset); + } +} diff --git a/test/other/test_split_module.post.js b/test/other/test_split_module.post.js index ad3ad618c67e2..1adb6b79bfe6c 100644 --- a/test/other/test_split_module.post.js +++ b/test/other/test_split_module.post.js @@ -1,25 +1,8 @@ #preprocess +#include "save_profile_data.js" function saveProfileData() { - var __write_profile = wasmExports['__write_profile']; - if (__write_profile) { - var len = __write_profile({{{ to64('0') }}}, 0); - var offset = _malloc(len); - var actualLen = __write_profile({{{ to64('offset') }}}, len); - var profile_data = HEAPU8.subarray(offset, offset + len); - if (typeof writeFile !== 'undefined') { - console.log('using writeFile') - writeFile('profile.data', profile_data); - } else if (typeof fs !== 'undefined') { - console.log('using fs.writeFileSync') - fs.writeFileSync('profile.data', profile_data); - } else { - console.log(JSON.stringify(Array.from(profile_data))); - } - console.log('profile size is', actualLen, 'bytes (allocated', len, 'bytes)'); - console.log('wrote profile data') - _free(offset); - } + save_profile_data(); // Say hello *after* recording the profile so that all functions are deferred. var result = _say_hello(); diff --git a/test/other/test_split_module_embind_jspi.cpp b/test/other/test_split_module_embind_jspi.cpp new file mode 100644 index 0000000000000..3d91b68434e41 --- /dev/null +++ b/test/other/test_split_module_embind_jspi.cpp @@ -0,0 +1,15 @@ +#include +#include + +int primary_function() { + return 42; +} + +int deferred_function() { + return 82; +} + +EMSCRIPTEN_BINDINGS(module_splitting) { + emscripten::function("primary_function", &primary_function); + emscripten::function("deferred_function", &deferred_function, emscripten::async()); +} diff --git a/test/other/test_split_module_embind_jspi.out b/test/other/test_split_module_embind_jspi.out new file mode 100644 index 0000000000000..3fa2b863432ee --- /dev/null +++ b/test/other/test_split_module_embind_jspi.out @@ -0,0 +1,2 @@ +deferred_function: [object Promise] +deferred_function await: 82 \ No newline at end of file diff --git a/test/other/test_split_module_embind_jspi.post.js b/test/other/test_split_module_embind_jspi.post.js new file mode 100644 index 0000000000000..2bf954eb182f2 --- /dev/null +++ b/test/other/test_split_module_embind_jspi.post.js @@ -0,0 +1,13 @@ +#preprocess +#include "save_profile_data.js" + +async function saveProfileData() { + console.log('primary_function: ' + Module.primary_function()); + save_profile_data(); + // deferred_function *after* recording the profile so that all functions are deferred. + var result = Module.deferred_function(); + console.log('deferred_function: ' + result); + console.log('deferred_function await: ' + await result); +} + +addOnPostRun(saveProfileData); diff --git a/test/other/test_split_module_embind_jspi.pre.js b/test/other/test_split_module_embind_jspi.pre.js new file mode 100644 index 0000000000000..b7e2207d89b7a --- /dev/null +++ b/test/other/test_split_module_embind_jspi.pre.js @@ -0,0 +1,5 @@ +Module["loadSplitModule"] = async function(deferred, imports, prop) { + console.log('Custom handler for loading split module.'); + + return instantiateAsync(null, deferred, imports); +} diff --git a/test/test_other.py b/test/test_other.py index 037eca335cf0f..7851fbe4b2d7a 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -12424,8 +12424,6 @@ def test_split_module(self, customLoader, jspi, opt): wasm_split_run = [wasm_split, '-g', '--enable-mutable-globals', '--enable-bulk-memory', '--enable-nontrapping-float-to-int', '--export-prefix=%', 'test_split_module.wasm.orig', '-o1', 'primary.wasm', '-o2', 'secondary.wasm', '--profile=profile.data'] - if jspi: - wasm_split_run += ['--jspi', '--enable-reference-types'] if self.get_setting('MEMORY64'): wasm_split_run += ['--enable-memory64'] self.run_process(wasm_split_run) @@ -12483,6 +12481,36 @@ def test_split_main_module(self): self.assertIn('Hello from main!', result) self.assertIn('Hello from lib!', result) + @also_with_wasm64 + @requires_jspi + def test_split_module_embind_jspi(self): + self.set_setting('SPLIT_MODULE') + self.cflags += ['-Wno-experimental'] + self.cflags += ['--post-js', test_file('other/test_split_module_embind_jspi.post.js')] + self.cflags += ['--pre-js', test_file('other/test_split_module_embind_jspi.pre.js')] + self.cflags += ['-sEXPORTED_FUNCTIONS=_malloc,_free'] + self.cflags += ['-lembind'] + self.do_other_test('test_split_module_embind_jspi.cpp') + self.assertExists('test_split_module_embind_jspi.wasm') + self.assertExists('test_split_module_embind_jspi.wasm.orig') + self.assertExists('profile.data') + + wasm_split = os.path.join(building.get_binaryen_bin(), 'wasm-split') + wasm_split_run = [wasm_split, '-g', + '--enable-mutable-globals', '--enable-bulk-memory', '--enable-nontrapping-float-to-int', + '--export-prefix=%', 'test_split_module_embind_jspi.wasm.orig', '-o1', 'primary.wasm', '-o2', 'secondary.wasm', '--profile=profile.data'] + self.run_process(wasm_split_run) + + os.remove('test_split_module_embind_jspi.wasm') + os.rename('primary.wasm', 'test_split_module_embind_jspi.wasm') + os.rename('secondary.wasm', 'test_split_module_embind_jspi.deferred.wasm') + result = self.run_js('test_split_module_embind_jspi.js') + self.assertNotIn('profile', result) + self.assertIn('primary_function: 42\n' + + 'Custom handler for loading split module.\n' + + 'deferred_function: [object Promise]\n' + + 'deferred_function await: 82', result) + @crossplatform @flaky('https://github.com/emscripten-core/emscripten/issues/25206') def test_gen_struct_info(self): diff --git a/tools/emscripten.py b/tools/emscripten.py index 500bb25355564..3c074918622ce 100644 --- a/tools/emscripten.py +++ b/tools/emscripten.py @@ -842,11 +842,6 @@ def add_standard_wasm_imports(send_items_map): 'memory_grow_post', ] - if settings.SPLIT_MODULE and settings.ASYNCIFY == 2: - # Calls to this function are generated by binaryen so it must be manually - # imported. - extra_sent_items.append('__load_secondary_module') - for s in extra_sent_items: send_items_map[s] = s diff --git a/tools/link.py b/tools/link.py index 451a6541a1de0..4ddb29ffb819d 100644 --- a/tools/link.py +++ b/tools/link.py @@ -1629,8 +1629,6 @@ def limit_incoming_module_api(): if settings.SPLIT_MODULE: settings.INCOMING_MODULE_JS_API += ['loadSplitModule'] - if settings.ASYNCIFY == 2: - settings.DEFAULT_LIBRARY_FUNCS_TO_INCLUDE += ['_load_secondary_module'] # wasm side modules have suffix .wasm if settings.SIDE_MODULE and utils.suffix(target) in ('.js', '.mjs'): diff --git a/tools/maint/gen_sig_info.py b/tools/maint/gen_sig_info.py index 75c745a34c3fd..99c39c807b038 100755 --- a/tools/maint/gen_sig_info.py +++ b/tools/maint/gen_sig_info.py @@ -178,7 +178,7 @@ def ignore_symbol(s, cxx): if s.startswith('gl') and any(s.endswith(x) for x in ('NV', 'EXT', 'WEBGL', 'ARB', 'ANGLE')): return True if s in {'__stack_base', '__memory_base', '__table_base', '__global_base', '__heap_base', - '__stack_pointer', '__stack_high', '__stack_low', '_load_secondary_module', + '__stack_pointer', '__stack_high', '__stack_low', # legacy aliases, not callable from native code. 'stackSave', 'stackRestore', 'stackAlloc', 'getTempRet0', 'setTempRet0', }: From b474e9029a7454e5606678fcb024862ced441393 Mon Sep 17 00:00:00 2001 From: Brendan Dahl Date: Thu, 19 Mar 2026 15:40:42 -0700 Subject: [PATCH 2/3] Apply suggestions from code review Co-authored-by: Heejin Ahn --- src/preamble.js | 2 +- test/test_other.py | 6 +----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/preamble.js b/src/preamble.js index 499ee4e7081f7..b2da44cdddc74 100644 --- a/src/preamble.js +++ b/src/preamble.js @@ -529,7 +529,7 @@ var splitModuleProxyHandler = { #if RUNTIME_DEBUG dbg('instantiated deferred module, continuing'); #endif - return wasmTable.get({{{toIndexType('base')}}})(...args); + return wasmTable.get({{{ toIndexType('base') }}})(...args); }; #if JSPI return new WebAssembly.Suspending(ret); diff --git a/test/test_other.py b/test/test_other.py index 7851fbe4b2d7a..b54349bfc4597 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -12498,12 +12498,8 @@ def test_split_module_embind_jspi(self): wasm_split = os.path.join(building.get_binaryen_bin(), 'wasm-split') wasm_split_run = [wasm_split, '-g', '--enable-mutable-globals', '--enable-bulk-memory', '--enable-nontrapping-float-to-int', - '--export-prefix=%', 'test_split_module_embind_jspi.wasm.orig', '-o1', 'primary.wasm', '-o2', 'secondary.wasm', '--profile=profile.data'] + '--export-prefix=%', 'test_split_module_embind_jspi.wasm.orig', '-o1', 'test_split_module_embind_jspi.wasm', '-o2', 'test_split_module_embind_jspi.deferred.wasm', '--profile=profile.data'] self.run_process(wasm_split_run) - - os.remove('test_split_module_embind_jspi.wasm') - os.rename('primary.wasm', 'test_split_module_embind_jspi.wasm') - os.rename('secondary.wasm', 'test_split_module_embind_jspi.deferred.wasm') result = self.run_js('test_split_module_embind_jspi.js') self.assertNotIn('profile', result) self.assertIn('primary_function: 42\n' + From 8c81f9cfec95ec9fe9fa2712791cfcd12938bcfd Mon Sep 17 00:00:00 2001 From: Brendan Dahl Date: Thu, 19 Mar 2026 23:19:54 +0000 Subject: [PATCH 3/3] review comments --- test/other/save_profile_data.js | 3 --- test/other/test_split_module_embind_jspi.out | 2 -- test/test_other.py | 16 +++++++++++----- 3 files changed, 11 insertions(+), 10 deletions(-) delete mode 100644 test/other/test_split_module_embind_jspi.out diff --git a/test/other/save_profile_data.js b/test/other/save_profile_data.js index 31aaa42cfaa4b..913e8f156012e 100644 --- a/test/other/save_profile_data.js +++ b/test/other/save_profile_data.js @@ -6,15 +6,12 @@ function save_profile_data() { var actualLen = __write_profile({{{ to64('offset') }}}, len); var profile_data = HEAPU8.subarray(offset, offset + len); if (typeof writeFile !== 'undefined') { - console.log('using writeFile') writeFile('profile.data', profile_data); } else if (typeof fs !== 'undefined') { - console.log('using fs.writeFileSync') fs.writeFileSync('profile.data', profile_data); } else { console.log(JSON.stringify(Array.from(profile_data))); } - console.log('profile size is', actualLen, 'bytes (allocated', len, 'bytes)'); console.log('wrote profile data') _free(offset); } diff --git a/test/other/test_split_module_embind_jspi.out b/test/other/test_split_module_embind_jspi.out deleted file mode 100644 index 3fa2b863432ee..0000000000000 --- a/test/other/test_split_module_embind_jspi.out +++ /dev/null @@ -1,2 +0,0 @@ -deferred_function: [object Promise] -deferred_function await: 82 \ No newline at end of file diff --git a/test/test_other.py b/test/test_other.py index f9ade109d846d..064c5ee81ac7a 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -12428,7 +12428,13 @@ def test_split_module_embind_jspi(self): self.cflags += ['--pre-js', test_file('other/test_split_module_embind_jspi.pre.js')] self.cflags += ['-sEXPORTED_FUNCTIONS=_malloc,_free'] self.cflags += ['-lembind'] - self.do_other_test('test_split_module_embind_jspi.cpp') + expected_pre_split_output = 'primary_function: 42\n' + expected_post_split_output = ('deferred_function: [object Promise]\n' + 'deferred_function await: 82\n') + expected_output = (expected_pre_split_output + + 'wrote profile data\n' + + expected_post_split_output) + result = self.do_runf('other/test_split_module_embind_jspi.cpp', expected_output=expected_output) self.assertExists('test_split_module_embind_jspi.wasm') self.assertExists('test_split_module_embind_jspi.wasm.orig') self.assertExists('profile.data') @@ -12440,10 +12446,10 @@ def test_split_module_embind_jspi(self): self.run_process(wasm_split_run) result = self.run_js('test_split_module_embind_jspi.js') self.assertNotIn('profile', result) - self.assertIn('primary_function: 42\n' + - 'Custom handler for loading split module.\n' + - 'deferred_function: [object Promise]\n' + - 'deferred_function await: 82', result) + self.assertIn((expected_pre_split_output + + 'Custom handler for loading split module.\n' + + expected_post_split_output), + result) @crossplatform @flaky('https://github.com/emscripten-core/emscripten/issues/25206')