From 1beb18dc9f939b37e8216aceeba1f72032f23146 Mon Sep 17 00:00:00 2001 From: Guy Bedford Date: Thu, 23 Jul 2026 23:00:15 -0700 Subject: [PATCH] Fix embind exports with MODULARIZE=instance and AUTO_INIT With AUTO_INIT the embind AOT export snippet was appended after the top-level `await init()`, so its addOnPostCtor callback registered after onPostCtors had already fired, leaving the ES module exports undefined. Emit the snippet via a placeholder ahead of the init logic in the postamble instead, and assert against late addOnPostCtor registration. Fixes #27411 --- src/lib/libcore.js | 7 ++++++- src/postamble.js | 5 +++++ test/test_other.py | 19 +++++++++++++++++++ tools/link.py | 2 +- 4 files changed, 31 insertions(+), 2 deletions(-) diff --git a/src/lib/libcore.js b/src/lib/libcore.js index ad1c1d48023d1..34c209b99fa6a 100644 --- a/src/lib/libcore.js +++ b/src/lib/libcore.js @@ -2402,7 +2402,12 @@ addToLibrary({ ATPOSTCTORS.unshift('callRuntimeCallbacks(onPostCtors);'); }, $addOnPostCtor__deps: ['$onPostCtors'], - $addOnPostCtor: (cb) => onPostCtors.push(cb), + $addOnPostCtor: (cb) => { +#if ASSERTIONS + assert(!runtimeInitialized, 'addOnPostCtor called too late: ctors have already run'); +#endif + onPostCtors.push(cb); + }, // See ATMAINS in parseTools.mjs for more information. $onMains: [], $onMains__internal: true, diff --git a/src/postamble.js b/src/postamble.js index d130ded126fbf..a474aa57280f3 100644 --- a/src/postamble.js +++ b/src/postamble.js @@ -244,6 +244,11 @@ var wasmRawExports; #endif #if MODULARIZE == 'instance' +#if EMBIND_AOT +// The embind exports are declared here so that their post-ctor registration +// precedes any self-initialization below. See phase_embind_aot in link.py. +<<< EMBIND_AOT_EXPORTS >>> +#endif // In MODULARIZE=instance mode we delay most of the initialization work until // the `init` function is called. #if ASSERTIONS diff --git a/test/test_other.py b/test/test_other.py index 09df84c0ed5c5..4b9b6691d9de4 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -6273,6 +6273,25 @@ def test_modularize_instance_auto_init(self): self.assertContained('main1\nmain2\nfoo\nbar\nbaz\n', self.run_js('runner.mjs')) + def test_modularize_instance_auto_init_embind(self): + # Embind exports must be assigned when the module self-initializes on + # import. See https://github.com/emscripten-core/emscripten/issues/27411 + self.run_process([EMXX, test_file('modularize_instance_embind.cpp'), + '-sMODULARIZE=instance', '-sAUTO_INIT', + '-Wno-experimental', + '-lembind', '-sEMBIND_AOT', + '-o', 'modularize_instance_embind.mjs'] + self.get_cflags()) + + create_file('runner.mjs', ''' + import { foo, Bar } from "./modularize_instance_embind.mjs"; + foo(); + const bar = new Bar(); + bar.print(); + bar.delete(); + ''') + + self.assertContained('main\nfoo\nbar\n', self.run_js('runner.mjs')) + @also_with_pthreads @requires_node_25 def test_esm_integration_auto_init(self): diff --git a/tools/link.py b/tools/link.py index c287e1311c2e6..e5389eab1b2e7 100644 --- a/tools/link.py +++ b/tools/link.py @@ -2126,7 +2126,7 @@ def phase_embind_aot(options, wasm_target, js_syms): addOnPostCtor(assignEmbindExports); {decls} // end embind exports''' - src += exports + src = do_replace(src, '<<< EMBIND_AOT_EXPORTS >>>', exports) write_file(final_js, src) if settings.WASM_ESM_INTEGRATION: # With ESM integration the embind exports also need to be exported by the main file.