From bda7f13910dbae1ff8487a6572eb756c1c225a5a Mon Sep 17 00:00:00 2001 From: Sam Clegg Date: Wed, 12 Nov 2025 16:57:21 -0800 Subject: [PATCH] Enable `-shared` via a new `FAKE_DYLIBS` setting. NFC This this change the default behaviour does not change since `FAKE_DYLIBS` defaults to false. However for users who want to try out a more traditional shared library workflow `-sFAKE_DYLIBS=0` can be used enable shared library output with `-shared`. At some point in the future we can consider making this the default. Split out from #25817 --- ChangeLog.md | 6 ++ .../tools_reference/settings_reference.rst | 13 ++++ src/settings.js | 7 +++ test/test_core.py | 24 ++++++-- test/test_other.py | 9 ++- tools/cmdline.py | 1 + tools/link.py | 60 ++++++++++++++----- 7 files changed, 100 insertions(+), 20 deletions(-) diff --git a/ChangeLog.md b/ChangeLog.md index baa7b8adca665..0b001a962adab 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -30,6 +30,12 @@ See docs/process.md for more on how version tagging works. variables to the generated program, when running under Node. This setting is enabled by default when `-sNODERAWFS` is used but can also be controlled separately. (#18820) +- A new `-sFAKE_DYLIBS` setting was added. When enabled you get the current + emscripten behavior of the `-shared` flag, which is to produce regular object + files instead of actual shared shared libraries (side modules). Because this + setting is enabled by default this doesn't change the default behavior of the + compiler. If you want to experiment with real shared libraries you can + explicitly disable this setting. (#25826) 4.0.20 - 11/18/25 ----------------- diff --git a/site/source/docs/tools_reference/settings_reference.rst b/site/source/docs/tools_reference/settings_reference.rst index 21943519e801b..77871dc3d80af 100644 --- a/site/source/docs/tools_reference/settings_reference.rst +++ b/site/source/docs/tools_reference/settings_reference.rst @@ -3424,6 +3424,19 @@ indirectly using `importScripts` Default value: false +.. _fake_dylibs: + +FAKE_DYLIBS +=========== + +This setting changes the behaviour of the ``-shared`` flag. The default +setting of ``true`` means the ``-shared`` flag actually produces a normal +object file (i.e. ``ld -r``). Setting this to false will cause ``-shared`` +to behave like :ref:`SIDE_MODULE` and produce and dynamically linked +library. + +Default value: true + .. _deprecated-settings: =================== diff --git a/src/settings.js b/src/settings.js index 9048096cc47d2..1ba3b7856b8da 100644 --- a/src/settings.js +++ b/src/settings.js @@ -2240,3 +2240,10 @@ var WASM_JS_TYPES = false; // CROSS_ORIGIN uses an inline worker to instead load the worker script // indirectly using `importScripts` var CROSS_ORIGIN = false; + +// This setting changes the behaviour of the ``-shared`` flag. The default +// setting of ``true`` means the ``-shared`` flag actually produces a normal +// object file (i.e. ``ld -r``). Setting this to false will cause ``-shared`` +// to behave like :ref:`SIDE_MODULE` and produce and dynamically linked +// library. +var FAKE_DYLIBS = true; diff --git a/test/test_core.py b/test/test_core.py index e2abcda198dd9..8cf944473816b 100644 --- a/test/test_core.py +++ b/test/test_core.py @@ -4033,19 +4033,35 @@ def dylink_testf(self, main, side=None, expected=None, force_c=False, main_cflag old_settings = dict(self.settings_mods) self.clear_setting('MODULARIZE') self.clear_setting('MAIN_MODULE') - self.set_setting('SIDE_MODULE') + self.clear_setting('SIDE_MODULE') so_file = os.path.join(so_dir, so_name) + + # Using -shared + -sFAKE_DYLIBS should be the same as `-sSIDE_MODULE` + flags = ['-sSIDE_MODULE'] if isinstance(side, list): # side is just a library - self.run_process([EMCC] + side + self.get_cflags() + ['-o', so_file]) + self.run_process([EMCC] + side + self.get_cflags() + flags + ['-o', so_file]) else: - out_file = self.build(side, output_suffix='.so') + out_file = self.build(side, output_suffix='.so', cflags=flags) shutil.move(out_file, so_file) + shutil.move(so_file, so_file + '.orig') + + # Verify that building with -sSIDE_MODULE is essentailly the same as building with `-shared -fPIC -sFAKE_DYLIBS=0`. + flags = ['-shared', '-fPIC', '-sFAKE_DYLIBS=0'] + if isinstance(side, list): + # side is just a library + self.run_process([EMCC] + side + self.get_cflags() + flags + ['-o', so_file]) + else: + out_file = self.build(side, output_suffix='.so', cflags=flags) + shutil.move(out_file, so_file) + + self.assertEqual(read_binary(so_file), read_binary(so_file + '.orig')) + os.remove(so_file + '.orig') + # main settings self.settings_mods = old_settings self.set_setting('MAIN_MODULE', main_module) - self.clear_setting('SIDE_MODULE') self.cflags += main_cflags self.cflags.append(so_file) diff --git a/test/test_other.py b/test/test_other.py index db5299e953427..3aae23c36cfa1 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -11919,7 +11919,8 @@ def test_euidaccess(self): self.do_other_test('test_euidaccess.c') def test_shared_flag(self): - self.run_process([EMCC, '-shared', test_file('hello_world.c'), '-o', 'libother.so']) + create_file('side.c', 'int foo;') + self.run_process([EMCC, '-shared', 'side.c', '-o', 'libother.so']) # Test that `-shared` flag causes object file generation but gives a warning err = self.run_process([EMCC, '-shared', test_file('hello_world.c'), '-o', 'out.foo', 'libother.so'], stderr=PIPE).stderr @@ -11927,6 +11928,12 @@ def test_shared_flag(self): self.assertContained('emcc: warning: ignoring dynamic library libother.so when generating an object file, this will need to be included explicitly in the final link', err) self.assertIsObjectFile('out.foo') + # Test that adding `-sFAKE_DYIBS=0` build a real side module + err = self.run_process([EMCC, '-shared', '-fPIC', '-sFAKE_DYLIBS=0', test_file('hello_world.c'), '-o', 'out.foo', 'libother.so'], stderr=PIPE).stderr + self.assertNotContained('linking a library with `-shared` will emit a static object', err) + self.assertNotContained('emcc: warning: ignoring dynamic library libother.so when generating an object file, this will need to be included explicitly in the final link', err) + self.assertIsWasmDylib('out.foo') + # Test that using an executable output name overrides the `-shared` flag, but produces a warning. err = self.run_process([EMCC, '-shared', test_file('hello_world.c'), '-o', 'out.js'], stderr=PIPE).stderr diff --git a/tools/cmdline.py b/tools/cmdline.py index da5bd699be276..48f64147aa494 100644 --- a/tools/cmdline.py +++ b/tools/cmdline.py @@ -65,6 +65,7 @@ class EmccOptions: dash_M = False dash_S = False dash_c = False + dylibs: List[str] = [] embed_files: List[str] = [] emit_symbol_map = False emit_tsd = '' diff --git a/tools/link.py b/tools/link.py index e779c761b6d15..18f7d7cad912e 100644 --- a/tools/link.py +++ b/tools/link.py @@ -829,6 +829,22 @@ def setup_sanitizers(options): settings.LOAD_SOURCE_MAP = 1 +def get_dylibs(options, linker_args): + """Find all the Wasm dynanamic libraries specified on the command line, + either via `-lfoo` or via `libfoo.so` directly.""" + + dylibs = [] + for arg in linker_args: + if arg.startswith('-l'): + for ext in DYLIB_EXTENSIONS: + path = find_library('lib' + arg[2:] + ext, options.lib_dirs) + if path and building.is_wasm_dylib(path): + dylibs.append(path) + elif building.is_wasm_dylib(arg): + dylibs.append(arg) + return dylibs + + @ToolchainProfiler.profile_block('linker_setup') def phase_linker_setup(options, linker_args): # noqa: C901, PLR0912, PLR0915 """Future modifications should consider refactoring to reduce complexity. @@ -843,6 +859,21 @@ def phase_linker_setup(options, linker_args): # noqa: C901, PLR0912, PLR0915 setup_environment_settings() apply_library_settings(linker_args) + + if settings.SIDE_MODULE or settings.MAIN_MODULE: + default_setting('FAKE_DYLIBS', 0) + + if options.shared and not settings.FAKE_DYLIBS: + default_setting('SIDE_MODULE', 1) + default_setting('RELOCATABLE', 1) + + if not settings.FAKE_DYLIBS: + options.dylibs = get_dylibs(options, linker_args) + # If there are any dynamically linked libraries on the command line then + # need to enable `MAIN_MODULE` in order to produce JS code that can load them. + if not settings.MAIN_MODULE and not settings.SIDE_MODULE and options.dylibs: + default_setting('MAIN_MODULE', 2) + linker_args += calc_extra_ldflags(options) # We used to do this check during on startup during `check_sanity`, but @@ -936,16 +967,14 @@ def phase_linker_setup(options, linker_args): # noqa: C901, PLR0912, PLR0915 # If no output format was specified we try to deduce the format based on # the output filename extension - if not options.oformat and (options.relocatable or (options.shared and not settings.SIDE_MODULE)): - # Until we have a better story for actually producing runtime shared libraries - # we support a compatibility mode where shared libraries are actually just - # object files linked with `wasm-ld --relocatable` or `llvm-link` in the case - # of LTO. + if not options.oformat and (options.relocatable or (options.shared and settings.FAKE_DYLIBS and not settings.SIDE_MODULE)): + # With FAKE_DYLIBS we generate an normal object file rather than an shared object. + # This is linked with `wasm-ld --relocatable` or (`llvm-link` in the case of LTO). if final_suffix in EXECUTABLE_EXTENSIONS: diagnostics.warning('emcc', '-shared/-r used with executable output suffix. This behaviour is deprecated. Please remove -shared/-r to build an executable or avoid the executable suffix (%s) when building object files.' % final_suffix) else: - if options.shared: - diagnostics.warning('emcc', 'linking a library with `-shared` will emit a static object file. This is a form of emulation to support existing build systems. If you want to build a runtime shared library use the SIDE_MODULE setting.') + if options.shared and 'FAKE_DYLIBS' not in user_settings: + diagnostics.warning('emcc', 'linking a library with `-shared` will emit a static object file (FAKE_DYLIBS defaults to true). If you want to build a runtime shared library use the SIDE_MODULE or FAKE_DYLIBS=0.') options.oformat = OFormat.OBJECT if not options.oformat: @@ -2730,10 +2759,10 @@ def map_to_js_libs(library_name): def process_libraries(options, flags): + """Process `-l` and `--js-library` flags.""" new_flags = [] system_libs_map = system_libs.Library.get_usable_variations() - # Process `-l` and `--js-library` flags for flag in flags: if flag.startswith('--js-library='): js_lib = flag.split('=', 1)[1] @@ -2854,7 +2883,7 @@ def replacement(self): def filter_out_fake_dynamic_libs(options, inputs): - # Filters out "fake" dynamic libraries that are really just intermediate object files. + """Filter out "fake" dynamic libraries that are really just intermediate object files.""" def is_fake_dylib(input_file): if get_file_suffix(input_file) in DYLIB_EXTENSIONS and os.path.exists(input_file) and not building.is_wasm_dylib(input_file): if not options.ignore_dynamic_linking: @@ -2866,11 +2895,13 @@ def is_fake_dylib(input_file): return [f for f in inputs if not is_fake_dylib(f)] -def filter_out_duplicate_dynamic_libs(inputs): +def filter_out_duplicate_fake_dynamic_libs(inputs): + """Filter out duplicate "fake" shared libraries (intermediate object files). + + See test_core.py:test_redundant_link + """ seen = set() - # Filter out duplicate "fake" shared libraries (intermediate object files). - # See test_core.py:test_redundant_link def check(input_file): if get_file_suffix(input_file) in DYLIB_EXTENSIONS and not building.is_wasm_dylib(input_file): abspath = os.path.abspath(input_file) @@ -3086,11 +3117,10 @@ def phase_calculate_linker_inputs(options, linker_args): if options.oformat == OFormat.OBJECT or options.ignore_dynamic_linking: linker_args = filter_out_fake_dynamic_libs(options, linker_args) else: - linker_args = filter_out_duplicate_dynamic_libs(linker_args) + linker_args = filter_out_duplicate_fake_dynamic_libs(linker_args) if settings.MAIN_MODULE: - dylibs = [a for a in linker_args if building.is_wasm_dylib(a)] - process_dynamic_libs(dylibs, options.lib_dirs) + process_dynamic_libs(options.dylibs, options.lib_dirs) return linker_args