diff --git a/cmake/Modules/Platform/Emscripten.cmake b/cmake/Modules/Platform/Emscripten.cmake index ca4c770ad75eb..0bc3653d419aa 100644 --- a/cmake/Modules/Platform/Emscripten.cmake +++ b/cmake/Modules/Platform/Emscripten.cmake @@ -18,7 +18,7 @@ set(CMAKE_SYSTEM_NAME Emscripten) set(CMAKE_SYSTEM_VERSION 1) set(CMAKE_CROSSCOMPILING TRUE) -set_property(GLOBAL PROPERTY TARGET_SUPPORTS_SHARED_LIBS FALSE) +set_property(GLOBAL PROPERTY TARGET_SUPPORTS_SHARED_LIBS TRUE) # Advertise Emscripten as a 32-bit platform (as opposed to # CMAKE_SYSTEM_PROCESSOR=x86_64 for 64-bit platform), since some projects (e.g. diff --git a/src/settings.js b/src/settings.js index b8fae5acab2a4..e3732bf4e7fc9 100644 --- a/src/settings.js +++ b/src/settings.js @@ -2230,3 +2230,9 @@ 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 settings changes the behaviour of the ``-shared`` flag. When set to true +// you get the old emscripten behaivour where the ``-shared`` flag actually +// produces a normal object file (i.e. ``ld -r``). The default behaviour is that +// `-shared` is the as :ref:`SIDE_MODULE`. +var FAKE_DYLIBS = false; diff --git a/test/test_other.py b/test/test_other.py index 7be8a15ae9eca..9c27ebb5328e4 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -1235,10 +1235,10 @@ def test_odd_suffixes(self): shutil.copy(test_file('hello_world.c'), 'test.' + suffix) self.do_runf('test.' + suffix, 'hello, world!') - for suffix in ('lo'): + for suffix in ('lo',): self.clear() print(suffix) - self.run_process([EMCC, test_file('hello_world.c'), '-shared', '-o', 'binary.' + suffix]) + self.run_process([EMCC, test_file('hello_world.c'), '-sFAKE_DYLIBS', '-shared', '-o', 'binary.' + suffix]) self.run_process([EMCC, 'binary.' + suffix]) self.assertContained('hello, world!', self.run_js('a.out.js')) @@ -1419,7 +1419,7 @@ def test_multiply_defined_libsymbols(self): ''') self.cflags.remove('-Werror') - self.emcc('libA.c', ['-shared', '-o', 'libA.so']) + self.emcc('libA.c', ['-shared', '-sFAKE_DYLIBS', '-o', 'libA.so']) self.emcc('a2.c', ['-r', '-L.', '-lA', '-o', 'a2.o']) self.emcc('b2.c', ['-r', '-L.', '-lA', '-o', 'b2.o']) @@ -1552,7 +1552,12 @@ def test_link_group_bitcode(self): # We deliberately ignore duplicate input files in order to allow # "libA.so" on the command line twice. This is not really .so support # and the .so files are really object files. - def test_redundant_link(self): + @parameterized({ + '': ([],), + 'fake_dylibs': (['-sFAKE_DYLIBS'],), + }) + def test_redundant_link(self, args): + self.cflags += args create_file('libA.c', 'int mult() { return 1; }') create_file('main.c', r''' #include @@ -1564,7 +1569,7 @@ def test_redundant_link(self): ''') self.cflags.remove('-Werror') - self.emcc('libA.c', ['-shared', '-o', 'libA.so']) + self.emcc('libA.c', ['-fPIC', '-shared', '-o', 'libA.so']) self.emcc('main.c', ['libA.so', 'libA.so', '-o', 'a.out.js']) self.assertContained('result: 1', self.run_js('a.out.js')) @@ -2159,9 +2164,9 @@ def test_multidynamic_link(self, link_flags, lib_suffix): ''') # Build libfile normally into an .so - self.run_process([EMCC, 'libdir/libfile.c', '-shared', '-o', 'libdir/libfile.so' + lib_suffix]) + self.run_process([EMCC, 'libdir/libfile.c', '-sFAKE_DYLIBS', '-shared', '-fPIC', '-o', 'libdir/libfile.so' + lib_suffix]) # Build libother and dynamically link it to libfile - self.run_process([EMCC, '-Llibdir', 'libdir/libother.c'] + link_flags + ['-shared', '-o', 'libdir/libother.so']) + self.run_process([EMCC, '-Llibdir', 'libdir/libother.c'] + link_flags + ['-sFAKE_DYLIBS', '-shared', '-fPIC', '-o', 'libdir/libother.so']) # Build the main file, linking in both the libs self.run_process([EMCC, '-Llibdir', os.path.join('main.c')] + link_flags + ['-lother', '-c']) print('...') @@ -4906,20 +4911,6 @@ def test_valid_abspath_2(self): self.run_process(cmd) self.assertContained('hello, world!', self.run_js('a.out.js')) - def test_warn_dylibs(self): - shared_suffixes = ['.so', '.dylib', '.dll'] - - for suffix in ('.o', '.bc', '.so', '.dylib', '.js', '.html'): - print(suffix) - cmd = [EMCC, test_file('hello_world.c'), '-o', 'out' + suffix] - if suffix in ['.o', '.bc']: - cmd.append('-c') - if suffix in ['.dylib', '.so']: - cmd.append('-shared') - err = self.run_process(cmd, stderr=PIPE).stderr - warning = 'linking a library with `-shared` will emit a static object file' - self.assertContainedIf(warning, err, suffix in shared_suffixes) - @crossplatform @parameterized({ 'O2': [['-O2']], @@ -8369,20 +8360,6 @@ def test_side_module_folder_deps(self): self.run_process([EMCC, test_file('hello_world.c'), '-sSIDE_MODULE', '-o', 'subdir/libside2.so', '-L', 'subdir', '-lside1']) self.run_process([EMCC, test_file('hello_world.c'), '-sMAIN_MODULE', '-o', 'main.js', '-L', 'subdir', '-lside2']) - @crossplatform - def test_side_module_ignore(self): - self.run_process([EMCC, test_file('hello_world.c'), '-sSIDE_MODULE', '-o', 'libside.so']) - - # Attempting to link statically against a side module (libside.so) should fail. - err = self.expect_fail([EMCC, '-L.', '-lside']) - self.assertContained(r'error: attempted static link of dynamic object \.[/\\]libside.so', err, regex=True) - - # But a static library in the same location (libside.a) should take precedence. - self.run_process([EMCC, test_file('hello_world.c'), '-c']) - self.run_process([EMAR, 'cr', 'libside.a', 'hello_world.o']) - self.run_process([EMCC, '-L.', '-lside']) - self.assertContained('hello, world!', self.run_js('a.out.js')) - @is_slow_test @parameterized({ '': ([],), @@ -11903,23 +11880,24 @@ def test_err(self): 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']) + def test_fake_dylibs(self): + self.run_process([EMCC, '-shared', '-sFAKE_DYLIBS', '-fPIC', test_file('hello_world.c'), '-o', 'libother.so']) + self.assertIsObjectFile('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 - self.assertContained('linking a library with `-shared` will emit a static object', err) + # Test that `-sFAKE_DYLIBS` flag causes object file generation and will generate a warning about + # dylink dependencies being ignored. + err = self.run_process([EMCC, '-shared', '-sFAKE_DYLIBS', '-fPIC', test_file('hello_world.c'), '-o', 'out.foo', 'libother.so'], stderr=PIPE).stderr 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 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'], + err = self.run_process([EMCC, '-shared', '-sFAKE_DYLIBS', '-fPIC', test_file('hello_world.c'), '-o', 'out.js'], stderr=PIPE).stderr self.assertContained('warning: -shared/-r used with executable output suffix', err) self.run_js('out.js') def test_shared_soname(self): - self.run_process([EMCC, '-shared', '-Wl,-soname', '-Wl,libfoo.so.13', test_file('hello_world.c'), '-lc', '-o', 'libfoo.so']) + self.run_process([EMCC, '-shared', '-sFAKE_DYLIBS', '-Wl,-soname', '-Wl,libfoo.so.13', test_file('hello_world.c'), '-lc', '-o', 'libfoo.so']) self.run_process([EMCC, '-sSTRICT', 'libfoo.so']) self.assertContained('hello, world!', self.run_js('a.out.js')) diff --git a/tools/building.py b/tools/building.py index c1de2a9062ce9..c566b501fa0cb 100644 --- a/tools/building.py +++ b/tools/building.py @@ -170,6 +170,9 @@ def lld_flags_for_executable(external_symbols): stub = create_stub_object(external_symbols) cmd.append(stub) + if not settings.FAKE_DYLIBS: + cmd.append('-Bdynamic') + if not settings.ERROR_ON_UNDEFINED_SYMBOLS: cmd.append('--import-undefined') @@ -208,7 +211,6 @@ def lld_flags_for_executable(external_symbols): c_exports = [e for e in c_exports if e not in external_symbols] c_exports += settings.REQUIRED_EXPORTS if settings.MAIN_MODULE: - cmd.append('-Bdynamic') c_exports += side_module_external_deps(external_symbols) for export in c_exports: if settings.ERROR_ON_UNDEFINED_SYMBOLS: diff --git a/tools/cmdline.py b/tools/cmdline.py index 13673a066fe64..1748805bda6bc 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 79aa4b8418c89..5db3dc5ef7e91 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 vis `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,15 @@ def phase_linker_setup(options, linker_args): # noqa: C901, PLR0912, PLR0915 setup_environment_settings() apply_library_settings(linker_args) + + if options.shared and not settings.FAKE_DYLIBS: + default_setting('SIDE_MODULE', 1) + default_setting('RELOCATABLE', 1) + + options.dylibs = get_dylibs(options, linker_args) + 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 +961,12 @@ 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)): + # 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.') options.oformat = OFormat.OBJECT if not options.oformat: @@ -2752,10 +2773,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] @@ -2793,22 +2814,22 @@ def process_libraries(options, flags): continue static_lib = f'lib{lib}.a' - if not settings.RELOCATABLE and not find_library(static_lib, options.lib_dirs): + if not find_library(static_lib, options.lib_dirs): # Normally we can rely on the native linker to expand `-l` args. # However, emscripten also supports `.so` files that are actually just # regular object file. This means we need to support `.so` files even # when statically linking. The native linker (wasm-ld) will otherwise # ignore .so files in this mode. - found_dylib = False + found_fake_dylib = False for ext in DYLIB_EXTENSIONS: name = 'lib' + lib + ext path = find_library(name, options.lib_dirs) - if path: - found_dylib = True + if path and not building.is_wasm_dylib(path): + found_fake_dylib = True new_flags.append(path) break - if found_dylib: + if found_fake_dylib: continue new_flags.append(flag) @@ -2876,7 +2897,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: @@ -2888,11 +2909,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) @@ -3108,11 +3131,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) + if not settings.SIDE_MODULE and not settings.FAKE_DYLIBS: + process_dynamic_libs(options.dylibs, options.lib_dirs) return linker_args