From 33b92ab98dc1d67dea986399728c73673e0f2302 Mon Sep 17 00:00:00 2001 From: David Peicho Date: Wed, 22 Feb 2023 17:13:44 +0100 Subject: [PATCH 1/6] Add EXPORT_KEEPALIVE to export symbols even in MINIMAL_RUNTIME --- emcc.py | 2 ++ emscripten.py | 7 ++++--- src/settings.js | 8 ++++++++ 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/emcc.py b/emcc.py index 222547aace2fc..a781b7b3c9c3e 100755 --- a/emcc.py +++ b/emcc.py @@ -1959,6 +1959,8 @@ def phase_linker_setup(options, state, newargs): default_setting('SUPPORT_ERRNO', 0) # Require explicit -lfoo.js flags to link with JS libraries. default_setting('AUTO_JS_LIBRARIES', 0) + # When using MINIMAL_RUNTIME, symbols should only be exported if requested. + default_setting('EXPORT_KEEPALIVE', 0) if settings.STRICT_JS and (settings.MODULARIZE or settings.EXPORT_ES6): exit_with_error("STRICT_JS doesn't work with MODULARIZE or EXPORT_ES6") diff --git a/emscripten.py b/emscripten.py index fcf545e0242d7..1f487ffa7e5e1 100644 --- a/emscripten.py +++ b/emscripten.py @@ -740,7 +740,7 @@ def install_wrapper(sym): wrapper = '/** @type {function(...*):?} */\nvar %s = ' % mangled # TODO(sbc): Can we avoid exporting the dynCall_ functions on the module. - if mangled in settings.EXPORTED_FUNCTIONS or name.startswith('dynCall_'): + if name.startswith('dynCall_') or (settings.EXPORT_KEEPALIVE and mangled in settings.EXPORTED_FUNCTIONS): exported = 'Module["%s"] = ' % mangled else: exported = '' @@ -793,8 +793,9 @@ def create_receiving(exports): mangled = asmjs_mangle(s) dynCallAssignment = ('dynCalls["' + s.replace('dynCall_', '') + '"] = ') if generate_dyncall_assignment and mangled.startswith('dynCall_') else '' export_assignment = '' - if settings.MODULARIZE and settings.EXPORT_ALL: - export_assignment = f'Module["{mangled}"] = ' + if settings.MODULARIZE: + if settings.EXPORT_ALL or (settings.EXPORT_KEEPALIVE and mangled in settings.EXPORTED_FUNCTIONS): + export_assignment = f'Module["{mangled}"] = ' receiving += [f'{export_assignment}{dynCallAssignment}{mangled} = asm["{s}"]'] else: receiving += make_export_wrappers(exports, delay_assignment) diff --git a/src/settings.js b/src/settings.js index 46800fffb3da5..ff8a6728badbe 100644 --- a/src/settings.js +++ b/src/settings.js @@ -994,6 +994,14 @@ var EXPORTED_FUNCTIONS = []; // [link] var EXPORT_ALL = false; +// If true, we export the symbols that are present in JS onto the Module +// object. +// It only does Module['X'] = X; +// +// This only applies to MINIMAL_RUNTIME, where symbols aren't exported by +// default. +var EXPORT_KEEPALIVE = true; + // Remembers the values of these settings, and makes them accessible // through getCompilerSetting and emscripten_get_compiler_setting. // To see what is retained, look for compilerSettings in the generated code. From bb242608f229fbcd13a7ce0e7101c748f06bbddf Mon Sep 17 00:00:00 2001 From: David Peicho Date: Wed, 22 Feb 2023 19:31:08 +0100 Subject: [PATCH 2/6] Add test for EXPORT_KEEPALIVE --- emscripten.py | 9 +++++---- test/test_other.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/emscripten.py b/emscripten.py index 1f487ffa7e5e1..f345eb0f89745 100644 --- a/emscripten.py +++ b/emscripten.py @@ -740,7 +740,8 @@ def install_wrapper(sym): wrapper = '/** @type {function(...*):?} */\nvar %s = ' % mangled # TODO(sbc): Can we avoid exporting the dynCall_ functions on the module. - if name.startswith('dynCall_') or (settings.EXPORT_KEEPALIVE and mangled in settings.EXPORTED_FUNCTIONS): + should_export = settings.EXPORT_KEEPALIVE and mangled in settings.EXPORTED_FUNCTIONS + if name.startswith('dynCall_') or should_export: exported = 'Module["%s"] = ' % mangled else: exported = '' @@ -792,10 +793,10 @@ def create_receiving(exports): for s in exports_that_are_not_initializers: mangled = asmjs_mangle(s) dynCallAssignment = ('dynCalls["' + s.replace('dynCall_', '') + '"] = ') if generate_dyncall_assignment and mangled.startswith('dynCall_') else '' + should_export = settings.EXPORT_ALL or (settings.EXPORT_KEEPALIVE and mangled in settings.EXPORTED_FUNCTIONS) export_assignment = '' - if settings.MODULARIZE: - if settings.EXPORT_ALL or (settings.EXPORT_KEEPALIVE and mangled in settings.EXPORTED_FUNCTIONS): - export_assignment = f'Module["{mangled}"] = ' + if settings.MODULARIZE and should_export: + export_assignment = f'Module["{mangled}"] = ' receiving += [f'{export_assignment}{dynCallAssignment}{mangled} = asm["{s}"]'] else: receiving += make_export_wrappers(exports, delay_assignment) diff --git a/test/test_other.py b/test/test_other.py index ddc88812bd68e..aec1dfa50be02 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -1307,6 +1307,37 @@ def test_export_all(self): self.emcc('lib.c', ['-Oz', '-sEXPORT_ALL', '-sLINKABLE', '--pre-js', 'main.js'], output_filename='a.out.js') self.assertContained('libf1\nlibf2\n', self.run_js('a.out.js')) + def test_modularize_export_keepalive(self): + create_file('main.c', r''' + #include + EMSCRIPTEN_KEEPALIVE int libf1() { return 42; } + ''') + + # By default, all kept alive functions should be exported. + self.emcc('main.c', ['-sMODULARIZE=1'], output_filename='test.js') + + # print(read_file('test.js')) + + assert ("Module[\"_libf1\"] = " in read_file('test.js')) + + # Ensures that EXPORT_KEEPALIVE=0 remove the exports + self.emcc('main.c', ['-sMODULARIZE=1', '-sEXPORT_KEEPALIVE=0'], output_filename='test.js') + assert (not ("Module[\"_libf1\"] = " in read_file('test.js'))) + + def test_minimal_modularize_export_keepalive(self): + create_file('main.c', r''' + #include + EMSCRIPTEN_KEEPALIVE int libf1() { return 42; } + ''') + + # By default, no symbols should be exported when using MINIMAL_RUNTIME. + self.emcc('main.c', ['-sMODULARIZE=1', '-sMINIMAL_RUNTIME=2'], output_filename='test.js') + assert (not ("Module[\"_libf1\"] = " in read_file('test.js'))) + + # Ensures that EXPORT_KEEPALIVE=1 exports the symbols. + self.emcc('main.c', ['-sMODULARIZE=1', '-sMINIMAL_RUNTIME=2', '-sEXPORT_KEEPALIVE'], output_filename='test.js') + assert ("Module[\"_libf1\"] = " in read_file('test.js')) + def test_minimal_runtime_export_all_modularize(self): """This test ensures that MODULARIZE and EXPORT_ALL work simultaneously. From 34aa235133efa94c366ab18cf7a0f5e0433c8617 Mon Sep 17 00:00:00 2001 From: David Peicho Date: Wed, 22 Feb 2023 21:17:20 +0100 Subject: [PATCH 3/6] Test output for EXPORT_KEEPALIVE instead of doing string check --- src/settings.js | 5 ++++- test/test_other.py | 37 ++++++++++++++++++++++++++----------- 2 files changed, 30 insertions(+), 12 deletions(-) diff --git a/src/settings.js b/src/settings.js index ff8a6728badbe..c5a6a2fe35201 100644 --- a/src/settings.js +++ b/src/settings.js @@ -1832,7 +1832,10 @@ var SUPPORT_ERRNO = true; // MINIMAL_RUNTIME=2 to further enable even more code size optimizations. These // opts are quite hacky, and work around limitations in Closure and other parts // of the build system, so they may not work in all generated programs (But can -// be useful for really small programs) +// be useful for really small programs). +// +// By default, no symbols will be exported on the `Module` object. In order +// to export kept alive symbols, please use `-sEXPORT_KEEPALIVE=1`. // [link] var MINIMAL_RUNTIME = 0; diff --git a/test/test_other.py b/test/test_other.py index aec1dfa50be02..f509f60917e4b 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -1307,36 +1307,51 @@ def test_export_all(self): self.emcc('lib.c', ['-Oz', '-sEXPORT_ALL', '-sLINKABLE', '--pre-js', 'main.js'], output_filename='a.out.js') self.assertContained('libf1\nlibf2\n', self.run_js('a.out.js')) - def test_modularize_export_keepalive(self): + def test_export_keepalive(self): create_file('main.c', r''' #include EMSCRIPTEN_KEEPALIVE int libf1() { return 42; } ''') - # By default, all kept alive functions should be exported. - self.emcc('main.c', ['-sMODULARIZE=1'], output_filename='test.js') - - # print(read_file('test.js')) + create_file('main.js', ''' + var Module = { + onRuntimeInitialized: function() { + console.log(Module._libf1 ? Module._libf1() : 'unexported'); + } + }; + ''') - assert ("Module[\"_libf1\"] = " in read_file('test.js')) + # By default, all kept alive functions should be exported. + self.emcc('main.c', ['--pre-js', 'main.js'], output_filename='test.js') + self.assertContained('42\n', self.run_js('test.js')) # Ensures that EXPORT_KEEPALIVE=0 remove the exports - self.emcc('main.c', ['-sMODULARIZE=1', '-sEXPORT_KEEPALIVE=0'], output_filename='test.js') - assert (not ("Module[\"_libf1\"] = " in read_file('test.js'))) + self.emcc('main.c', ['-sEXPORT_KEEPALIVE=0', '--pre-js', 'main.js'], output_filename='test.js') + self.assertContained('unexported', self.run_js('test.js')) + @requires_node def test_minimal_modularize_export_keepalive(self): create_file('main.c', r''' #include EMSCRIPTEN_KEEPALIVE int libf1() { return 42; } ''') + # With MINIMAL_RUNTIME, the module isn't exported. + def write_js_main(): + runtime = read_file('test.js') + write_file('main.js', f'{runtime}\nModule().then((mod) => console.log(mod._libf1()));') + # By default, no symbols should be exported when using MINIMAL_RUNTIME. self.emcc('main.c', ['-sMODULARIZE=1', '-sMINIMAL_RUNTIME=2'], output_filename='test.js') - assert (not ("Module[\"_libf1\"] = " in read_file('test.js'))) + write_js_main() + output = self.expect_fail(config.NODE_JS + ['main.js']) + self.assertContained('TypeError: mod._libf1 is not a function', output) # Ensures that EXPORT_KEEPALIVE=1 exports the symbols. - self.emcc('main.c', ['-sMODULARIZE=1', '-sMINIMAL_RUNTIME=2', '-sEXPORT_KEEPALIVE'], output_filename='test.js') - assert ("Module[\"_libf1\"] = " in read_file('test.js')) + self.emcc('main.c', ['-sMODULARIZE=1', '-sMINIMAL_RUNTIME=2', '-sEXPORT_KEEPALIVE=1'], output_filename='test.js') + write_js_main() + output = self.run_process(config.NODE_JS + ['main.js'], stdout=PIPE, stderr=PIPE) + self.assertContained('42\n', output.stdout) def test_minimal_runtime_export_all_modularize(self): """This test ensures that MODULARIZE and EXPORT_ALL work simultaneously. From 7ff0f765ce90e9dfa340d5fe89996535c64d9075 Mon Sep 17 00:00:00 2001 From: David Peicho Date: Wed, 22 Feb 2023 21:25:49 +0100 Subject: [PATCH 4/6] Add EXPORT_KEEPALIVE to ChangeLog.md --- ChangeLog.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index cc4db1d57cba0..e9d895c25ec6f 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -24,6 +24,10 @@ See docs/process.md for more on how version tagging works. - Update glfw header to 3.3.8 (#18826) - The `LLD_REPORT_UNDEFINED` setting has been removed. It's now essentially always enabled. (#18342) +- Added `-sEXPORT_KEEPALIVE` to export symbols. When using + `MINIMAL_RUNTIME`, the option will be **disabled** by default. + This option simply exports the symbols on the module object, i.e., + `Module['X'] = X;` 3.1.32 - 02/17/23 ----------------- From a6f341db8fd5a92f453b3f673ba149c13e9b58be Mon Sep 17 00:00:00 2001 From: David Peicho Date: Wed, 22 Feb 2023 22:02:46 +0100 Subject: [PATCH 5/6] Clean EXPORT_KEEPALIVE tests --- src/settings.js | 3 --- test/test_other.py | 25 +++++++++---------------- 2 files changed, 9 insertions(+), 19 deletions(-) diff --git a/src/settings.js b/src/settings.js index c5a6a2fe35201..0f70dd7c5746b 100644 --- a/src/settings.js +++ b/src/settings.js @@ -997,9 +997,6 @@ var EXPORT_ALL = false; // If true, we export the symbols that are present in JS onto the Module // object. // It only does Module['X'] = X; -// -// This only applies to MINIMAL_RUNTIME, where symbols aren't exported by -// default. var EXPORT_KEEPALIVE = true; // Remembers the values of these settings, and makes them accessible diff --git a/test/test_other.py b/test/test_other.py index f509f60917e4b..8ad518b130971 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -1313,23 +1313,18 @@ def test_export_keepalive(self): EMSCRIPTEN_KEEPALIVE int libf1() { return 42; } ''') - create_file('main.js', ''' - var Module = { - onRuntimeInitialized: function() { - console.log(Module._libf1 ? Module._libf1() : 'unexported'); - } + create_file('pre.js', ''' + Module.onRuntimeInitialized = () => { + console.log(Module._libf1 ? Module._libf1() : 'unexported'); }; ''') # By default, all kept alive functions should be exported. - self.emcc('main.c', ['--pre-js', 'main.js'], output_filename='test.js') - self.assertContained('42\n', self.run_js('test.js')) + self.do_runf('main.c', '42\n', emcc_args=['--pre-js', 'pre.js']) # Ensures that EXPORT_KEEPALIVE=0 remove the exports - self.emcc('main.c', ['-sEXPORT_KEEPALIVE=0', '--pre-js', 'main.js'], output_filename='test.js') - self.assertContained('unexported', self.run_js('test.js')) + self.do_runf('main.c', 'unexported\n', emcc_args=['-sEXPORT_KEEPALIVE=0', '--pre-js', 'pre.js']) - @requires_node def test_minimal_modularize_export_keepalive(self): create_file('main.c', r''' #include @@ -1342,16 +1337,14 @@ def write_js_main(): write_file('main.js', f'{runtime}\nModule().then((mod) => console.log(mod._libf1()));') # By default, no symbols should be exported when using MINIMAL_RUNTIME. - self.emcc('main.c', ['-sMODULARIZE=1', '-sMINIMAL_RUNTIME=2'], output_filename='test.js') + self.emcc('main.c', ['-sMODULARIZE=1', '-sMINIMAL_RUNTIME=2', '-sASSERTIONS=0'], output_filename='test.js') write_js_main() - output = self.expect_fail(config.NODE_JS + ['main.js']) - self.assertContained('TypeError: mod._libf1 is not a function', output) + self.assertContained('TypeError: mod._libf1 is not a function', self.run_js('main.js', assert_returncode=NON_ZERO)) # Ensures that EXPORT_KEEPALIVE=1 exports the symbols. - self.emcc('main.c', ['-sMODULARIZE=1', '-sMINIMAL_RUNTIME=2', '-sEXPORT_KEEPALIVE=1'], output_filename='test.js') + self.emcc('main.c', ['-sMODULARIZE=1', '-sMINIMAL_RUNTIME=2', '-sEXPORT_KEEPALIVE=1', '-sASSERTIONS=0'], output_filename='test.js') write_js_main() - output = self.run_process(config.NODE_JS + ['main.js'], stdout=PIPE, stderr=PIPE) - self.assertContained('42\n', output.stdout) + self.assertContained('42\n', self.run_js('main.js')) def test_minimal_runtime_export_all_modularize(self): """This test ensures that MODULARIZE and EXPORT_ALL work simultaneously. From cefc55788a996313d1705f01aa859e91154f9a13 Mon Sep 17 00:00:00 2001 From: David Peicho Date: Thu, 23 Feb 2023 18:55:54 +0100 Subject: [PATCH 6/6] Clean up EXPORT_KEEPALIVE test --- test/test_other.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/test/test_other.py b/test/test_other.py index 8ad518b130971..0314c1f538c22 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -1326,23 +1326,31 @@ def test_export_keepalive(self): self.do_runf('main.c', 'unexported\n', emcc_args=['-sEXPORT_KEEPALIVE=0', '--pre-js', 'pre.js']) def test_minimal_modularize_export_keepalive(self): + self.set_setting('MODULARIZE') + self.set_setting('MINIMAL_RUNTIME') + create_file('main.c', r''' #include EMSCRIPTEN_KEEPALIVE int libf1() { return 42; } ''') - # With MINIMAL_RUNTIME, the module isn't exported. def write_js_main(): + """ + With MINIMAL_RUNTIME, the module instantiation function isn't exported neither as a UMD nor as an ES6 module. + Thus, it's impossible to use `require` or `import`. + + This function simply appends the instantiation code to the generated code. + """ runtime = read_file('test.js') write_file('main.js', f'{runtime}\nModule().then((mod) => console.log(mod._libf1()));') # By default, no symbols should be exported when using MINIMAL_RUNTIME. - self.emcc('main.c', ['-sMODULARIZE=1', '-sMINIMAL_RUNTIME=2', '-sASSERTIONS=0'], output_filename='test.js') + self.emcc('main.c', [], output_filename='test.js') write_js_main() self.assertContained('TypeError: mod._libf1 is not a function', self.run_js('main.js', assert_returncode=NON_ZERO)) # Ensures that EXPORT_KEEPALIVE=1 exports the symbols. - self.emcc('main.c', ['-sMODULARIZE=1', '-sMINIMAL_RUNTIME=2', '-sEXPORT_KEEPALIVE=1', '-sASSERTIONS=0'], output_filename='test.js') + self.emcc('main.c', ['-sEXPORT_KEEPALIVE=1'], output_filename='test.js') write_js_main() self.assertContained('42\n', self.run_js('main.js'))