Skip to content

Commit 2dd631a

Browse files
committed
Enable real dynamic linking with -shared by default
This change essentially disables `FAKE_DYLIBS` by default, which in turn means `-shared` will now produce dynamic libraries by default. If you want the old behaviour you now need `-sFAKE_DYLIBS`.
1 parent 8672d43 commit 2dd631a

10 files changed

Lines changed: 105 additions & 102 deletions

File tree

ChangeLog.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,12 @@ See docs/process.md for more on how version tagging works.
4949
e.g. Intel x64 Macs and Windows-on-ARM, downloading Java SE Development Kit
5050
21.0.11 from https://www.oracle.com/europe/java/technologies/downloads/#java21
5151
is required in order to use Emscripten's Closure Compiler integration.
52+
Closure compiler now provides a native macOS arm64 binary for Apple Silicon.
53+
- The `FAKE_DYLIBS` setting is now disabled by default. This means that
54+
`-shared` will produce real dynamic libraries by default (`-sSIDE_MODULE` is
55+
implied). Also, if you include real dynamic libraries in your link comment
56+
emscripten will now automatically produce a dynamically linked program
57+
(`-sMAIN_MODULE=2` is implied). (#25930)
5258

5359
5.0.7 - 04/30/26
5460
----------------

site/source/docs/compiling/Dynamic-Linking.rst

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,8 @@ before your application starts to run.
111111
- Build one part of your code as the main module, linking it using
112112
``-sMAIN_MODULE`` (See :ref:`MAIN_MODULE`).
113113
- Build other parts of your code as side modules, linking it using
114-
``-sSIDE_MODULE`` (See :ref:`SIDE_MODULE`).
114+
``-shared``. You can also used the emscripten-specific :ref:`SIDE_MODULE`
115+
setting which does the same thing by default.
115116

116117
For the main module the output suffix should be ``.js`` (the WebAssembly
117118
file will be generated alongside it just like normal). For the side
@@ -149,19 +150,17 @@ Building Dynamic Libraries using ``-shared``
149150
============================================
150151

151152
In traditional toolchains the ``-shared`` flag is used to generated dynamic
152-
libraries. However, because dynamic linking in Emscripten comes with caveats
153-
and has some overhead, Emscripten does not currently produce real dynamic
154-
libraries when this flag is used. Instead, Emscripten will produce a fake
155-
dynamic library (along with a warning) that is actually a single static object
156-
file. When your main program is linked against this fake dynamic library it
157-
gets linked into your main program like any other object file.
153+
libraries. Historically, due to early limitations, Emscripten would produce a
154+
fake dynamic library (along with a warning) when the ``-shared`` flag was used.
155+
When your main program is linked against these fake dynamic library they would
156+
be linked into your main program like regular static other object files.
158157

159-
The reason for this behaviour is to allow projects (and build systems) that
160-
assume a working ``-shared`` flag to build successfully (albeit using static
161-
linking).
158+
These days Emscripten will produce real dynamic libraries by default and
159+
``-shared`` is essentially the same as ``-sSIDE_MODULE``.
162160

163161
This behaviour can be controlled using the :ref:`FAKE_DYLIBS` settings. If you
164-
disable `FAKE_DYLIBS` then ``-shared`` will act like ``-sSIDE_MODULE``.
162+
prefer the older behaviour with fake dynamic libraryies you can enable this
163+
setting.
165164

166165
Code Size
167166
=========

site/source/docs/tools_reference/settings_reference.rst

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3356,13 +3356,13 @@ Default value: false
33563356
FAKE_DYLIBS
33573357
===========
33583358

3359-
This setting changes the behaviour of the ``-shared`` flag. The default
3360-
setting of ``true`` means the ``-shared`` flag actually produces a normal
3361-
object file (i.e. ``ld -r``). Setting this to false will cause ``-shared``
3362-
to behave like :ref:`SIDE_MODULE` and produce a dynamically linked
3363-
library.
3359+
This setting changes the behaviour of the ``-shared`` flag. When set to true
3360+
you get the old emscripten behaviour where the ``-shared`` flag actually
3361+
produces a normal object file (i.e. ``ld -r``). When set to true (the
3362+
default) the ``-shared`` flag is equivelent to :ref:`SIDE_MODULE` and will
3363+
produce a Wasn dynamic library.
33643364

3365-
Default value: true
3365+
Default value: false
33663366

33673367
.. _executable:
33683368

src/settings.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2201,12 +2201,12 @@ var GROWABLE_ARRAYBUFFERS = false;
22012201
// indirectly using `importScripts`
22022202
var CROSS_ORIGIN = false;
22032203

2204-
// This setting changes the behaviour of the ``-shared`` flag. The default
2205-
// setting of ``true`` means the ``-shared`` flag actually produces a normal
2206-
// object file (i.e. ``ld -r``). Setting this to false will cause ``-shared``
2207-
// to behave like :ref:`SIDE_MODULE` and produce a dynamically linked
2208-
// library.
2209-
var FAKE_DYLIBS = true;
2204+
// This setting changes the behaviour of the ``-shared`` flag. When set to true
2205+
// you get the old emscripten behaviour where the ``-shared`` flag actually
2206+
// produces a normal object file (i.e. ``ld -r``). When set to true (the
2207+
// default) the ``-shared`` flag is equivelent to :ref:`SIDE_MODULE` and will
2208+
// produce a Wasn dynamic library.
2209+
var FAKE_DYLIBS = false;
22102210

22112211
// Add a #! line to generated JS file and make it executable. This is useful
22122212
// for building command line tools that run under node.

test/common.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1517,7 +1517,7 @@ def get_poppler_library(self, env_init=None):
15171517

15181518
return poppler + freetype
15191519

1520-
def get_zlib_library(self, cmake, cflags=None):
1520+
def get_zlib_library(self, cmake, cflags=None, target='libz.a'):
15211521
assert cmake or not WINDOWS, 'on windows, get_zlib_library only supports cmake'
15221522

15231523
old_args = self.cflags.copy()
@@ -1531,12 +1531,12 @@ def get_zlib_library(self, cmake, cflags=None):
15311531
# https://github.com/emscripten-core/emscripten/issues/16908 is fixed
15321532
self.cflags.append('-Wno-pointer-sign')
15331533
if cmake:
1534-
rtn = self.get_library(os.path.join('third_party', 'zlib'), os.path.join('libz.a'),
1534+
rtn = self.get_library(os.path.join('third_party', 'zlib'), target,
15351535
configure=['cmake', '.'],
15361536
make=['cmake', '--build', '.', '--'],
15371537
make_args=[])
15381538
else:
1539-
rtn = self.get_library(os.path.join('third_party', 'zlib'), os.path.join('libz.a'), make_args=['libz.a'])
1539+
rtn = self.get_library(os.path.join('third_party', 'zlib'), target, make_args=['libz.a', target])
15401540
self.cflags = old_args
15411541
return rtn
15421542

test/test_core.py

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4051,8 +4051,8 @@ def dylink_testf(self, main, side=None, expected=None, force_c=False, main_cflag
40514051

40524052
shutil.move(so_file, so_file + '.orig')
40534053

4054-
# Verify that building with -sSIDE_MODULE is essentially the same as building with `-shared -fPIC -sFAKE_DYLIBS=0`.
4055-
flags = ['-shared', '-fPIC', '-sFAKE_DYLIBS=0']
4054+
# Verify that building with -sSIDE_MODULE is essentially the same as building with `-shared -fPIC`
4055+
flags = ['-shared', '-fPIC']
40564056
if isinstance(side, list):
40574057
# side is just a library
40584058
self.run_process([EMCC] + side + self.get_cflags() + flags + ['-o', so_file])
@@ -5079,7 +5079,8 @@ def do_run(src, expected_output, cflags=None):
50795079

50805080
@with_dylink_reversed
50815081
def test_dylink_dot_a(self):
5082-
# .a linking must force all .o files inside it, when in a shared module
5082+
# Tests that it's possible to link a .a archive into a dynamic library using
5083+
# `-Wl,--whole-archive`
50835084
create_file('third.c', 'int sidef() { return 36; }')
50845085
create_file('fourth.c', 'int sideg() { return 17; }')
50855086

@@ -5097,7 +5098,7 @@ def test_dylink_dot_a(self):
50975098
}
50985099
''',
50995100
# contents of libfourth.a must be included, even if they aren't referred to!
5100-
side=['libfourth.a', 'third.o'],
5101+
side=['-Wl,--whole-archive', 'libfourth.a', '-Wl,--no-whole-archive', 'third.o'],
51015102
expected=['sidef: 36, sideg: 17.\n'], force_c=True)
51025103

51035104
@with_dylink_reversed
@@ -5141,16 +5142,25 @@ def test_dylink_spaghetti(self):
51415142
'''])
51425143

51435144
@needs_make('mingw32-make')
5144-
@with_dylink_reversed
5145-
def test_dylink_zlib(self):
5146-
zlib_archive = self.get_zlib_library(cmake=WINDOWS, cflags=['-fPIC'])
5145+
@needs_dylink
5146+
@parameterized({
5147+
# Cmake support for dyanamic libraries is currently broken.
5148+
# We set `TARGET_SUPPORTS_SHARED_LIBS` to `FALSE` in in cmake/Modules/Platform/Emscripten.cmake
5149+
# which means cmake won't build shared libraries yet.
5150+
# https://gitlab.kitware.com/cmake/cmake/-/work_items/27240
5151+
# 'cmake': (True,),
5152+
'configure': (False,),
5153+
})
5154+
def test_dylink_zlib(self, cmake):
5155+
zlib_archive = self.get_zlib_library(cmake=cmake, target='libz.so.1.2.5', cflags=['-fPIC'])[0]
5156+
zlib_basename = os.path.basename(zlib_archive)
5157+
shutil.copyfile(zlib_archive, zlib_basename)
51475158
# example.c uses K&R style function declarations
51485159
self.cflags.append('-Wno-deprecated-non-prototype')
51495160
self.cflags.append('-I' + test_file('third_party/zlib'))
5150-
self.dylink_test(main=read_file(test_file('third_party/zlib/example.c')),
5151-
side=zlib_archive,
5152-
expected=read_file(test_file('core/test_zlib.out')),
5153-
force_c=True)
5161+
self.do_runf(test_file('third_party/zlib/example.c'),
5162+
read_file(test_file('core/test_zlib.out')),
5163+
cflags=['-L.', zlib_archive])
51545164

51555165
# @with_dylink_reversed
51565166
# def test_dylink_bullet(self):

test/test_other.py

Lines changed: 38 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -932,7 +932,8 @@ def test_cmake_explicit_generator(self):
932932
# use -Wno-dev to suppress an irrelevant warning about the test files only.
933933
cmd = [EMCMAKE, 'cmake', '-GNinja', '-Wno-dev', test_file('cmake/cpp_lib')]
934934
self.run_process(cmd)
935-
self.assertExists(self.get_dir() + '/build.ninja')
935+
self.assertExists('build.ninja')
936+
self.run_process(['ninja', '-v'])
936937

937938
# Tests that it's possible to pass C++17 or GNU++17 build modes to CMake by building code that
938939
# needs C++17 (embind)
@@ -1175,7 +1176,7 @@ def test_odd_suffixes(self):
11751176
for suffix in ('lo',):
11761177
self.clear()
11771178
print(suffix)
1178-
self.run_process([EMCC, test_file('hello_world.c'), '-shared', '-o', 'binary.' + suffix])
1179+
self.run_process([EMCC, test_file('hello_world.c'), '-shared', '-fPIC', '-o', 'binary.' + suffix])
11791180
self.run_process([EMCC, 'binary.' + suffix])
11801181
self.assertContained('Hello, world!', self.run_js('a.out.js'))
11811182

@@ -1360,11 +1361,11 @@ def test_multiply_defined_libsymbols(self):
13601361
# we model shared libraries using regular object files. Without special handling
13611362
# fake `libA.so` could get linked multiple times.
13621363
self.cflags.remove('-Werror')
1363-
self.emcc('libA.c', ['-shared', '-o', 'libA.so'])
1364+
self.emcc('libA.c', ['-sFAKE_DYLIBS', '-shared', '-fPIC', '-o', 'libA.so'])
13641365

1365-
err = self.emcc('a2.c', ['-shared', '-L.', '-lA', '-o', 'liba2.so'], stderr=PIPE).stderr
1366+
err = self.emcc('a2.c', ['-sFAKE_DYLIBS', '-shared', '-fPIC', '-L.', '-lA', '-o', 'liba2.so'], stderr=PIPE).stderr
13661367
self.assertContained('emcc: warning: ignoring dynamic library libA.so when generating an object file', err)
1367-
err = self.emcc('b2.c', ['-shared', '-L.', '-lA', '-o', 'libb2.so'], stderr=PIPE).stderr
1368+
err = self.emcc('b2.c', ['-sFAKE_DYLIBS', '-shared', '-fPIC', '-L.', '-lA', '-o', 'libb2.so'], stderr=PIPE).stderr
13681369
self.assertContained('emcc: warning: ignoring dynamic library libA.so when generating an object file', err)
13691370

13701371
self.do_runf('main.c', 'result: 1', cflags=['-L.', '-lA', 'liba2.so', 'libb2.so'])
@@ -1495,7 +1496,12 @@ def test_link_group_bitcode(self):
14951496
# We deliberately ignore duplicate input files in order to allow
14961497
# "libA.so" on the command line twice. This is not really .so support
14971498
# and the .so files are really object files.
1498-
def test_redundant_link(self):
1499+
@parameterized({
1500+
'': ([],),
1501+
'fake_dylibs': (['-sFAKE_DYLIBS'],),
1502+
})
1503+
def test_redundant_link(self, args):
1504+
self.cflags += args
14991505
create_file('libA.c', 'int mult() { return 1; }')
15001506
create_file('main.c', r'''
15011507
#include <stdio.h>
@@ -1507,7 +1513,7 @@ def test_redundant_link(self):
15071513
''')
15081514

15091515
self.cflags.remove('-Werror')
1510-
self.emcc('libA.c', ['-shared', '-o', 'libA.so'])
1516+
self.emcc('libA.c', ['-fPIC', '-shared', '-o', 'libA.so'])
15111517
self.emcc('main.c', ['libA.so', 'libA.so', '-o', 'a.out.js'])
15121518
self.assertContained('result: 1', self.run_js('a.out.js'))
15131519

@@ -2155,9 +2161,9 @@ def test_multidynamic_link(self, link_flags, lib_suffix):
21552161
''')
21562162

21572163
# Build libfile normally into an .so
2158-
self.run_process([EMCC, 'libdir/libfile.c', '-shared', '-o', 'libdir/libfile.so' + lib_suffix])
2164+
self.run_process([EMCC, 'libdir/libfile.c', '-sFAKE_DYLIBS', '-shared', '-fPIC', '-o', 'libdir/libfile.so' + lib_suffix])
21592165
# Build libother and dynamically link it to libfile
2160-
self.run_process([EMCC, '-Llibdir', 'libdir/libother.c'] + link_flags + ['-shared', '-o', 'libdir/libother.so'])
2166+
self.run_process([EMCC, '-Llibdir', 'libdir/libother.c'] + link_flags + ['-sFAKE_DYLIBS', '-shared', '-fPIC', '-o', 'libdir/libother.so'])
21612167
# Build the main file, linking in both the libs
21622168
self.run_process([EMCC, '-Llibdir', os.path.join('main.c')] + link_flags + ['-lother', '-c'])
21632169
print('...')
@@ -2462,17 +2468,17 @@ def test_dylink_library_search(self):
24622468
}
24632469
''')
24642470

2465-
# By deafult we use static linking and prefer libside.a
2466-
self.do_runf('main.c', 'static linking used\n', cflags=['-L.', '-lside'])
2471+
# By default we use dynamic linking
2472+
self.do_runf('main.c', 'dynamic linking used\n', cflags=['-L.', '-lside'])
24672473

2468-
# When using -sMAIN_MODULE we choose the dyanmic library
2469-
self.do_runf('main.c', 'dynamic linking used\n', cflags=['-sMAIN_MODULE=2', '-L.', '-lside'])
2474+
# When using -sMAIN_MODULE=0 we explictly opt out of dynammic linking
2475+
self.do_runf('main.c', 'static linking used\n', cflags=['-sMAIN_MODULE=0', '-L.', '-lside'])
24702476

2471-
# Same for `-sFAKE_DYLIBS=0
2472-
self.do_runf('main.c', 'dynamic linking used\n', cflags=['-sFAKE_DYLIBS=0', '-L.', '-lside'])
2477+
# Same for `-sFAKE_DYLIBS
2478+
self.do_runf('main.c', 'static linking used\n', cflags=['-sFAKE_DYLIBS', '-L.', '-lside'])
24732479

24742480
# With can also force static linking using `-Bstatic` linker falgs
2475-
self.do_runf('main.c', 'static linking used\n', cflags=['-sMAIN_MODULE=2', '-L.', '-Bstatic', '-lside'])
2481+
self.do_runf('main.c', 'static linking used\n', cflags=['-L.', '-Bstatic', '-lside'])
24762482

24772483
def test_js_link(self):
24782484
create_file('before.js', '''
@@ -4955,20 +4961,6 @@ def test_valid_abspath_2(self):
49554961
self.run_process(cmd)
49564962
self.assertContained('Hello, world!', self.run_js('a.out.js'))
49574963

4958-
def test_warn_dylibs(self):
4959-
shared_suffixes = ['.so', '.dylib', '.dll']
4960-
4961-
for suffix in ('.o', '.bc', '.so', '.dylib', '.js', '.html'):
4962-
print(suffix)
4963-
cmd = [EMCC, test_file('hello_world.c'), '-o', 'out' + suffix]
4964-
if suffix in {'.o', '.bc'}:
4965-
cmd.append('-c')
4966-
if suffix in {'.dylib', '.so'}:
4967-
cmd.append('-shared')
4968-
err = self.run_process(cmd, stderr=PIPE).stderr
4969-
warning = 'linking a library with `-shared` will emit a static object file'
4970-
self.assertContainedIf(warning, err, suffix in shared_suffixes)
4971-
49724964
@crossplatform
49734965
@parameterized({
49744966
'O2': [['-O2']],
@@ -8464,12 +8456,12 @@ def test_side_module_ignore(self):
84648456
self.run_process([EMCC, test_file('hello_world.c'), '-sSIDE_MODULE', '-o', 'libside.so'])
84658457

84668458
# Attempting to link statically against a side module (libside.so) should fail.
8467-
self.assert_fail([EMCC, '-L.', '-lside'], 'wasm-ld: error: unable to find library -lside')
8459+
self.assert_fail([EMCC, '-L.', '-Bstatic', '-lside'], 'wasm-ld: error: unable to find library -lside')
84688460

84698461
# But a static library in the same location (libside.a) should take precedence.
84708462
self.run_process([EMCC, test_file('hello_world.c'), '-c'])
84718463
self.run_process([EMAR, 'cr', 'libside.a', 'hello_world.o'])
8472-
self.run_process([EMCC, '-L.', '-lside'])
8464+
self.run_process([EMCC, '-L.', '-Bstatic', '-lside'])
84738465
self.assertContained('Hello, world!', self.run_js('a.out.js'))
84748466

84758467
@is_slow_test
@@ -12131,42 +12123,40 @@ def test_err(self):
1213112123
def test_euidaccess(self):
1213212124
self.do_other_test('test_euidaccess.c')
1213312125

12134-
def test_shared_flag(self):
12135-
create_file('side.c', 'int foo;')
12136-
self.run_process([EMCC, '-shared', 'side.c', '-o', 'libother.so'])
12126+
def test_fake_dylibs(self):
12127+
create_file('other.c', 'int foo = 10;')
12128+
self.run_process([EMCC, '-shared', '-sFAKE_DYLIBS', '-fPIC', 'other.c', '-o', 'libother.so'])
12129+
self.assertIsObjectFile('libother.so')
1213712130

12138-
# Test that `-shared` flag causes object file generation but gives a warning
12139-
err = self.run_process([EMCC, '-shared', test_file('hello_world.c'), '-o', 'out.foo', 'libother.so'], stderr=PIPE).stderr
12140-
self.assertContained('linking a library with `-shared` will emit a static object', err)
12131+
# Test that `-sFAKE_DYLIBS` flag causes object file generation and will generate a warning about
12132+
# dylink dependencies being ignored.
12133+
err = self.run_process([EMCC, '-shared', '-sFAKE_DYLIBS', '-fPIC', test_file('hello_world.c'), '-o', 'out.foo', 'libother.so'], stderr=PIPE).stderr
1214112134
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)
1214212135
self.assertIsObjectFile('out.foo')
1214312136

1214412137
# Test that adding `-sFAKE_DYLIBS=0` build a real side module
1214512138
err = self.run_process([EMCC, '-shared', '-fPIC', '-sFAKE_DYLIBS=0', test_file('hello_world.c'), '-o', 'out.foo', 'libother.so'], stderr=PIPE).stderr
12146-
self.assertNotContained('linking a library with `-shared` will emit a static object', err)
1214712139
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)
1214812140
self.assertIsWasmDylib('out.foo')
1214912141

1215012142
# Test that using an executable output name overrides the `-shared` flag, but produces a warning.
12151-
err = self.run_process([EMCC, '-shared', test_file('hello_world.c'), '-o', 'out.js'],
12143+
err = self.run_process([EMCC, '-shared', '-sFAKE_DYLIBS', '-fPIC', test_file('hello_world.c'), '-o', 'out.js'],
1215212144
stderr=PIPE).stderr
1215312145
self.assertContained('warning: -shared/-r used with executable output suffix', err)
1215412146
self.run_js('out.js')
1215512147

1215612148
def test_shared_soname(self):
12157-
self.run_process([EMCC, '-shared', '-Wl,-soname', '-Wl,libfoo.so.13', test_file('hello_world.c'), '-lc', '-o', 'libfoo.so'])
12149+
self.run_process([EMCC, '-shared', '-sFAKE_DYLIBS', '-Wl,-soname', '-Wl,libfoo.so.13', test_file('hello_world.c'), '-lc', '-o', 'libfoo.so'])
1215812150
self.run_process([EMCC, '-sSTRICT', 'libfoo.so'])
1215912151
self.assertContained('Hello, world!', self.run_js('a.out.js'))
1216012152

12161-
def test_shared_and_side_module_flag(self):
12162-
# Test that `-shared` and `-sSIDE_MODULE` flag causes wasm dylib generation without a warning.
12163-
err = self.run_process([EMCC, '-shared', '-sSIDE_MODULE', test_file('hello_world.c'), '-o', 'out.foo'], stderr=PIPE).stderr
12164-
self.assertNotContained('linking a library with `-shared` will emit a static object', err)
12153+
def test_shared_flag(self):
12154+
# Test that `-shared` flag causes wasm dylib generation
12155+
self.run_process([EMCC, '-shared', '-fPIC', test_file('hello_world.c'), '-o', 'out.foo'])
1216512156
self.assertIsWasmDylib('out.foo')
1216612157

12167-
# Test that `-shared` and `-sSIDE_MODULE` flag causes wasm dylib generation without a warning even if given executable output name.
12168-
err = self.run_process([EMCC, '-shared', '-sSIDE_MODULE', test_file('hello_world.c'), '-o', 'out.wasm'],
12169-
stderr=PIPE).stderr
12158+
# Test that `-shared` causes wasm dylib generation warning even if given executable output name.
12159+
err = self.run_process([EMCC, '-shared', '-fPIC', test_file('hello_world.c'), '-o', 'out.wasm'], stderr=PIPE).stderr
1217012160
self.assertNotContained('warning: -shared/-r used with executable output suffix', err)
1217112161
self.assertIsWasmDylib('out.wasm')
1217212162

0 commit comments

Comments
 (0)