From d05c12932ddd9982521862f38ee5ea4eb02327e2 Mon Sep 17 00:00:00 2001 From: Sam Clegg Date: Fri, 31 Jul 2026 14:48:38 -0700 Subject: [PATCH] Disable DEFAULT_TO_CXX by default Disable `DEFAULT_TO_CXX` setting by default. This means that `em++` is now required when linking C++ programs, matching the behavior of clang and gcc (clang++ and g++ respectively required) The old behavior (allowing C++ even when run via emcc) is still available by adding `-sDEFAULT_TO_CXX` explicitly. Fixes: #11121 --- ChangeLog.md | 3 +++ .../tools_reference/settings_reference.rst | 8 +++---- src/settings.js | 8 +++---- test/common.py | 5 ---- test/test_core.py | 1 - test/test_other.py | 15 +++++++----- test/test_sanity.py | 4 ++-- tools/building.py | 23 ++++++++++++++++++- tools/link.py | 1 - tools/shared.py | 16 +++++++++---- 10 files changed, 53 insertions(+), 31 deletions(-) diff --git a/ChangeLog.md b/ChangeLog.md index ed5ffb8785050..9baef98bad25e 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -20,6 +20,9 @@ See docs/process.md for more on how version tagging works. 6.0.6 (in development) ---------------------- +- `DEFAULT_TO_CXX` is now disabled by default. This means that `em++` is now + required when linking C++ programs, matching the behavior of clang and gcc. + The old behavior is still available using `-sDEFAULT_TO_CXX`. (#11121) - libcxx and libcxxabi were updated to LLVM 22.1.8. (#27428) 6.0.5 - 07/29/26 diff --git a/site/source/docs/tools_reference/settings_reference.rst b/site/source/docs/tools_reference/settings_reference.rst index f8567c530658a..73c3755c186ff 100644 --- a/site/source/docs/tools_reference/settings_reference.rst +++ b/site/source/docs/tools_reference/settings_reference.rst @@ -1770,7 +1770,6 @@ Changes enabled by this: - IGNORE_MISSING_MAIN is disabled. - AUTO_JS_LIBRARIES is disabled. - AUTO_NATIVE_LIBRARIES is disabled. - - DEFAULT_TO_CXX is disabled. - ALLOW_UNIMPLEMENTED_SYSCALLS is disabled. - INCOMING_MODULE_JS_API is set to empty by default. @@ -3180,11 +3179,10 @@ Default value: [] DEFAULT_TO_CXX ============== -Default to c++ mode even when run as ``emcc`` rather than ``emc++``. -When this is disabled ``em++`` is required when linking C++ programs. -Disabling this will match the behaviour of gcc/g++ and clang/clang++. +Default to c++ mode even when run as ``emcc`` rather than ``em++``. +By default, ``em++`` is required when linking C++ programs. -Default value: true +Default value: false .. _printf_long_double: diff --git a/src/settings.js b/src/settings.js index af2fecf89efed..7a07460b164fc 100644 --- a/src/settings.js +++ b/src/settings.js @@ -1220,7 +1220,6 @@ var LINKABLE = false; // - IGNORE_MISSING_MAIN is disabled. // - AUTO_JS_LIBRARIES is disabled. // - AUTO_NATIVE_LIBRARIES is disabled. -// - DEFAULT_TO_CXX is disabled. // - ALLOW_UNIMPLEMENTED_SYSCALLS is disabled. // - INCOMING_MODULE_JS_API is set to empty by default. // [compile+link] @@ -2116,11 +2115,10 @@ var ASAN_SHADOW_SIZE = -1; // [link] var SOURCE_MAP_PREFIXES = []; -// Default to c++ mode even when run as ``emcc`` rather than ``emc++``. -// When this is disabled ``em++`` is required when linking C++ programs. -// Disabling this will match the behaviour of gcc/g++ and clang/clang++. +// Default to c++ mode even when run as ``emcc`` rather than ``em++``. +// By default, ``em++`` is required when linking C++ programs. // [link] -var DEFAULT_TO_CXX = true; +var DEFAULT_TO_CXX = false; // While LLVM's wasm32 has long double = float128, we don't support printing // that at full precision by default. Instead we print as 64-bit doubles, which diff --git a/test/common.py b/test/common.py index dd6eff67a62fe..585cd1e1fc581 100644 --- a/test/common.py +++ b/test/common.py @@ -741,11 +741,6 @@ def setUp(self): self.skip_exec = None self.flaky = False self.cflags = ['-Wclosure', '-Werror', '-Wno-limited-postlink-optimizations'] - # TODO(https://github.com/emscripten-core/emscripten/issues/11121) - # For historical reasons emcc compiles and links as C++ by default. - # However we want to run our tests in a more strict manner. We can - # remove this if the issue above is ever fixed. - self.set_setting('NO_DEFAULT_TO_CXX') self.ldflags = [] # Increase the stack trace limit to maximise usefulness of test failure reports. # Also, include backtrace for all uncaught exceptions (not just Error). diff --git a/test/test_core.py b/test/test_core.py index b83fd1e89373f..f98b0abd42ef1 100644 --- a/test/test_core.py +++ b/test/test_core.py @@ -10076,7 +10076,6 @@ def setUp(self): esm_integration = make_run('esm_integration', init=lambda self: self.setup_esm_integration()) instance = make_run('instance', cflags=['-Wno-experimental'], settings={'MODULARIZE': 'instance'}) -# Add DEFAULT_TO_CXX=0 strict = make_run('strict', cflags=[], settings={'STRICT': 1}) strict_js = make_run('strict_js', cflags=[], settings={'STRICT_JS': 1}) diff --git a/test/test_other.py b/test/test_other.py index 683ce096c5d86..4c30e88c20cfd 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -2951,6 +2951,9 @@ def test_undefined_data_symbols(self): self.run_process([EMCC, 'main.c', '-Wl,--unresolved-symbols=ignore-all']) self.run_process([EMCC, 'main.c', '-Wl,--allow-undefined']) + def test_emcc_cxx_link_hint(self): + self.assert_fail([EMCC, test_file('hello_libcxx.cpp')], "warning: link failed with undefined C++ symbols. Try linking with 'em++' or passing '-sDEFAULT_TO_CXX'") + def test_GetProcAddress_LEGACY_GL_EMULATION(self): # without legacy gl emulation, getting a proc from there should fail self.do_other_test('test_GetProcAddress_LEGACY_GL_EMULATION.c', args=['0'], cflags=['-sLEGACY_GL_EMULATION=0', '-sGL_ENABLE_GET_PROC_ADDRESS']) @@ -3757,12 +3760,12 @@ def test_embind_tsgen_test_embind(self): def test_embind_tsgen_val(self): # Check that any dependencies from val still works with TS generation enabled. - self.run_process([EMCC, test_file('other/embind_tsgen_val.cpp'), + self.run_process([EMXX, test_file('other/embind_tsgen_val.cpp'), '-lembind', '--emit-tsd', 'embind_tsgen_val.d.ts']) self.assertExists('embind_tsgen_val.d.ts') def test_embind_tsgen_constant_only(self): - self.run_process([EMCC, test_file('other/embind_tsgen_constant_only.cpp'), + self.run_process([EMXX, test_file('other/embind_tsgen_constant_only.cpp'), '-lembind', '--emit-tsd', 'out.d.ts']) self.assertFilesMatch(test_file('other/embind_tsgen_constant_only.d.ts'), 'out.d.ts') @@ -9373,7 +9376,7 @@ def test(check, extra): 'gl_emu': (['-sLEGACY_GL_EMULATION', '-sMAXIMUM_MEMORY=4GB', '-sALLOW_MEMORY_GROWTH'],), 'no_exception_throwing': (['-sDISABLE_EXCEPTION_THROWING'],), 'minimal_runtime': (['-sMINIMAL_RUNTIME'],), - 'embind': (['-lembind'],), + 'embind': (['-lembind', '-sDEFAULT_TO_CXX'],), }) def test_full_js_library(self, args): self.run_process([EMCC, test_file('hello_world.c'), '-sSTRICT_JS', '-sINCLUDE_FULL_LIBRARY'] + args) @@ -11670,7 +11673,7 @@ def test_exceptions_with_closure_and_without_catching(self): throw 5; } ''') - self.run_process([EMCC, 'src.cpp', '-fexceptions', '--closure=1']) + self.run_process([EMXX, 'src.cpp', '-fexceptions', '--closure=1']) def test_assertions_on_incoming_module_api_changes(self): create_file('pre.js', 'Module.read = () => {};') @@ -14798,7 +14801,7 @@ def test_no_minify(self): def test_no_minify_and_later_closure(self): # test that running closure after --minify=0 works - self.run_process([EMCC, test_file('hello_libcxx.cpp'), '-O2', '--minify=0']) + self.run_process([EMXX, test_file('hello_libcxx.cpp'), '-O2', '--minify=0']) temp = building.closure_compiler('a.out.js', advanced=True, extra_closure_args=['--formatting', 'PRETTY_PRINT']) @@ -15753,7 +15756,7 @@ def test_empath_split(self): /emsdk/emscripten/system/lib/libcxx ''') - self.run_process([EMCC, 'main.cpp', 'foo.cpp', '-gsource-map', '-g2', '-o', 'test.js']) + self.run_process([EMXX, 'main.cpp', 'foo.cpp', '-gsource-map', '-g2', '-o', 'test.js']) empath_split_cmd = [empath_split, 'test.wasm', 'path_list.txt', '-g', '-o', 'test_primary.wasm', '--out-prefix=test_', '-v'] out = self.run_process(empath_split_cmd, stdout=PIPE).stdout diff --git a/test/test_sanity.py b/test/test_sanity.py index f091e2083dfe8..938da5bd13aca 100644 --- a/test/test_sanity.py +++ b/test/test_sanity.py @@ -29,7 +29,7 @@ from tools import building, cache, ports, response_file, shared, utils from tools.config import EM_CONFIG -from tools.shared import EMCC, config +from tools.shared import EMCC, EMXX, config from tools.utils import delete_dir, delete_file SANITY_FILE = cache.get_path('sanity.txt') @@ -419,7 +419,7 @@ def test_emcc_caching(self): for i in range(3): print(i) self.clear() - output = self.do([EMCC, '-O' + str(i), test_file('hello_libcxx.cpp'), '-sDISABLE_EXCEPTION_CATCHING=0']) + output = self.do([EMXX, '-O' + str(i), test_file('hello_libcxx.cpp'), '-sDISABLE_EXCEPTION_CATCHING=0']) if i == 0: libname = cache.get_lib_name('libc++-debug.a') else: diff --git a/tools/building.py b/tools/building.py index 10b1888d5738e..fb9a99eceb364 100644 --- a/tools/building.py +++ b/tools/building.py @@ -17,6 +17,7 @@ from . import ( cache, cmdline, + colored_logger, config, diagnostics, js_optimizer, @@ -344,7 +345,27 @@ def link_lld(args, target, external_symbols=None, linker_inputs=None): cmd += lld_flags_for_executable(external_symbols) cmd += lld_flags(args, linker_inputs) cmd = get_command_with_possible_response_file(cmd) - check_call(cmd) + if settings.LINK_AS_CXX: + check_call(cmd) + else: + # When not running C++ mode we currently capture the stderr of the linker + # so that we can recommend using `em++` when there are libc++ symbols missing. + # TODO: Remove this extra complexity one day. + if colored_logger.ansi_color_available(): + # We force color diagnostics from wasm-ld when we know that they are available + # in the current TTY. Without this, the use of stderr=PIPE would cause + # wasm-ld to always disable color output. + cmd.append('--color-diagnostics=always') + try: + proc = shared.run_process(cmd, stderr=subprocess.PIPE) + if proc.stderr: + sys.stderr.write(proc.stderr) + except subprocess.CalledProcessError as e: + sys.stderr.write(e.stderr) + cxx_symbols = ('std::', 'operator new', 'operator delete', 'vtable for', 'typeinfo for', '__cxa_') + if any(sym in e.stderr for sym in cxx_symbols): + diagnostics.warn("link failed with undefined C++ symbols. Try linking with 'em++' or passing '-sDEFAULT_TO_CXX'") + exit_with_error("'%s' failed (%s)", shlex.join(cmd), shared.returncode_to_str(e.returncode)) def get_command_with_possible_response_file(cmd): diff --git a/tools/link.py b/tools/link.py index c15b4f1227f09..8d787765624f0 100644 --- a/tools/link.py +++ b/tools/link.py @@ -1220,7 +1220,6 @@ def limit_incoming_module_api(): if settings.STRICT: if not settings.EXPORT_ES6: default_setting('STRICT_JS', 1) - default_setting('DEFAULT_TO_CXX', 0) default_setting('IGNORE_MISSING_MAIN', 0) default_setting('AUTO_NATIVE_LIBRARIES', 0) if settings.MAIN_MODULE != 1: diff --git a/tools/shared.py b/tools/shared.py index ffb53996376a0..6b23f1881ed24 100644 --- a/tools/shared.py +++ b/tools/shared.py @@ -183,19 +183,25 @@ def get_finished_process(): return [x[1] for x in std_outs] -def check_call(cmd, *args, **kw): - """Like `run_process` above but treat failures as fatal and exit_with_error.""" +def run_process(cmd, *args, **kw): + """Wrapper around utils.run_process used to running compiler sub-processes.""" print_compiler_stage(cmd) if SKIP_SUBPROCS: - return 0 + return subprocess.CompletedProcess(cmd, 0, stdout='', stderr='') try: return utils.run_process(cmd, *args, **kw) - except subprocess.CalledProcessError as e: - exit_with_error("'%s' failed (%s)", shlex.join(cmd), returncode_to_str(e.returncode)) except OSError as e: exit_with_error("'%s' failed: %s", shlex.join(cmd), e) +def check_call(cmd, *args, **kw): + """Like `run_process` above but treat failures as fatal and exit_with_error.""" + try: + return run_process(cmd, *args, **kw) + except subprocess.CalledProcessError as e: + exit_with_error("'%s' failed (%s)", shlex.join(cmd), returncode_to_str(e.returncode)) + + def exec_process(cmd): print_compiler_stage(cmd) utils.exec(cmd)