diff --git a/ChangeLog.md b/ChangeLog.md index 809278a81101f..2d95be00c92a4 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -21,6 +21,9 @@ See docs/process.md for more on how version tagging works. 6.0.7 (in development) ---------------------- +- JavaScript library symbols can now use the `__force` and `__export` + decorators to control inclusion and export visibility. (#27436) + 6.0.6 - 08/05/26 ---------------- - `DEFAULT_TO_CXX` is now disabled by default. This means that `em++` is now diff --git a/site/source/docs/porting/connecting_cpp_and_javascript/Interacting-with-code.rst b/site/source/docs/porting/connecting_cpp_and_javascript/Interacting-with-code.rst index 2d3df59fb45c6..30074c2688bf0 100644 --- a/site/source/docs/porting/connecting_cpp_and_javascript/Interacting-with-code.rst +++ b/site/source/docs/porting/connecting_cpp_and_javascript/Interacting-with-code.rst @@ -598,6 +598,9 @@ See the `library_*.js`_ files for other examples. by ``_``. In other words ``my_func: function() {},`` becomes ``function _my_func() {}``, as all C methods in emscripten have a ``_`` prefix. Keys starting with ``$`` have the ``$`` stripped and no underscore added. + - A library symbol can use ``__force: true`` to be included even when it is + not referenced, and ``__export: true`` to be exported when included. Use + both decorators to unconditionally include and export a symbol. .. _interacting-with-code-call-function-pointers-from-c: diff --git a/src/jsifier.mjs b/src/jsifier.mjs index 77a534648bb78..5e58566edb9ba 100644 --- a/src/jsifier.mjs +++ b/src/jsifier.mjs @@ -30,6 +30,7 @@ import { debugLog, error, errorOccured, + extraLibraryFuncs, isDecorator, isJsOnlySymbol, compileTimeContext, @@ -41,12 +42,10 @@ import { localFile, timer, } from './utility.mjs'; -import {LibraryManager, librarySymbols, nativeAliases} from './modules.mjs'; +import {extraExports, LibraryManager, librarySymbols, nativeAliases} from './modules.mjs'; const addedLibraryItems = {}; -const extraLibraryFuncs = []; - // Experimental feature to check for invalid __deps entries. // See `EMCC_CHECK_DEPS` in in the environment to try it out. const CHECK_DEPS = process.env.EMCC_CHECK_DEPS; @@ -700,6 +699,10 @@ function(${args}) { librarySymbols.push(mangled); + if (!isStub && LibraryManager.library[symbol + '__export']) { + extraExports.add(mangled); + } + const original = LibraryManager.library[symbol]; let snippet = original; const isUserSymbol = LibraryManager.library[symbol + '__user']; @@ -838,7 +841,7 @@ function(${args}) { contentText = `var ${mangled} = ${snippet};`; } - if (contentText && MODULARIZE == 'instance' && (EXPORT_ALL || EXPORTED_FUNCTIONS.has(mangled)) && !isStub) { + if (contentText && MODULARIZE == 'instance' && (EXPORT_ALL || EXPORTED_FUNCTIONS.has(mangled) || extraExports.has(mangled)) && !isStub) { // In MODULARIZE=instance mode mark JS library symbols are exported at // the point of declaration. contentText = 'export ' + contentText; @@ -973,6 +976,7 @@ var proxiedFunctionTable = [ '//FORWARDED_DATA:' + JSON.stringify({ librarySymbols, + extraExports: Array.from(extraExports), nativeAliases, warnings: warningOccured(), asyncFuncs, diff --git a/src/lib/libasync.js b/src/lib/libasync.js index 9f9a2d315b1f5..b0d59fa7503df 100644 --- a/src/lib/libasync.js +++ b/src/lib/libasync.js @@ -20,6 +20,7 @@ addToLibrary({ }, #if ASYNCIFY + $Asyncify__force: true, $Asyncify__deps: ['$runAndAbortIfError', '$callUserCallback', #if ASSERTIONS '$createNamedFunction', @@ -627,7 +628,3 @@ addToLibrary({ }, #endif // ASYNCIFY }); - -if (ASYNCIFY) { - extraLibraryFuncs.push('$Asyncify'); -} diff --git a/src/lib/libautodebug.js b/src/lib/libautodebug.js index 461201c02d184..435e8a24be7e6 100644 --- a/src/lib/libautodebug.js +++ b/src/lib/libautodebug.js @@ -9,7 +9,7 @@ #error "Should only be included in AUTODEBUG mode" #endif -addToLibrary({ +const LibraryAutodebug = { $log_execution: (loc) => dbg('log_execution ' + loc), $get_i32: (loc, index, value) => { dbg('get_i32 ' + [loc, index, value]); @@ -131,36 +131,12 @@ addToLibrary({ dbg('memory_grow_post ' + [loc, result]); return result; }, -}); +}; -extraLibraryFuncs.push( - '$log_execution', - '$get_i32', - '$get_i64', - '$get_f32', - '$get_f64', - '$get_funcref', - '$get_externref', - '$get_anyref', - '$get_exnref', - '$set_i32', - '$set_i64', - '$set_f32', - '$set_f64', - '$set_funcref', - '$set_externref', - '$set_anyref', - '$set_exnref', - '$load_ptr', - '$load_val_i32', - '$load_val_i64', - '$load_val_f32', - '$load_val_f64', - '$store_ptr', - '$store_val_i32', - '$store_val_i64', - '$store_val_f32', - '$store_val_f64', - '$memory_grow_pre', - '$memory_grow_post', -); +for (const symbol of Object.keys(LibraryAutodebug)) { + if (!isDecorator(symbol)) { + LibraryAutodebug[symbol + '__force'] = true; + } +} + +addToLibrary(LibraryAutodebug); diff --git a/src/lib/libcore.js b/src/lib/libcore.js index a621664b009fd..3fd173a480749 100644 --- a/src/lib/libcore.js +++ b/src/lib/libcore.js @@ -2517,7 +2517,7 @@ function autoAddDeps(lib, name) { #if LEGACY_RUNTIME // Library functions that were previously included as runtime functions are // automatically included when `LEGACY_RUNTIME` is set. -extraLibraryFuncs.push( +for (const symbol of [ '$addFunction', '$removeFunction', '$AsciiToString', @@ -2544,7 +2544,9 @@ extraLibraryFuncs.push( '$stringToUTF8Array', '$stringToUTF8', '$lengthBytesUTF8', -); +]) { + addToLibrary({[symbol + '__force']: true}, {allowMissing: true}); +} #endif function wrapSyscallFunction(x, library, isWasi) { diff --git a/src/lib/libembind_gen.js b/src/lib/libembind_gen.js index c20082e94daf1..132e8deeff214 100644 --- a/src/lib/libembind_gen.js +++ b/src/lib/libembind_gen.js @@ -914,6 +914,7 @@ var LibraryEmbind = { #endif ], $emitOutput__postset: () => { addAtPostCtor('emitOutput()'); }, + $emitOutput__force: true, $emitOutput: () => { for (const typeId in awaitingDependencies) { throwBindingError(`Missing binding for type: '${getTypeName(typeId)}' typeId: ${typeId}`); @@ -936,6 +937,4 @@ var LibraryEmbind = { $PureVirtualError: () => { throw new Error('stub function should not be called'); }, }; -extraLibraryFuncs.push('$emitOutput'); - addToLibrary(LibraryEmbind); diff --git a/src/lib/libfs.js b/src/lib/libfs.js index 70d3fda1334e7..4907180698353 100644 --- a/src/lib/libfs.js +++ b/src/lib/libfs.js @@ -5,6 +5,10 @@ */ var LibraryFS = { +#if FORCE_FILESYSTEM + // Include FS even when it is not referenced by compiled code. + $FS__force: true, +#endif $FS__deps: ['$randomFill', '$PATH', '$PATH_FS', '$TTY', '$MEMFS', '$FS_modeStringToFlags', '$FS_fileDataToTypedArray', diff --git a/src/lib/libfs_shared.js b/src/lib/libfs_shared.js index 838b52309701a..b7206da202c29 100644 --- a/src/lib/libfs_shared.js +++ b/src/lib/libfs_shared.js @@ -200,10 +200,3 @@ addToLibrary({ $FS_readFile__deps: ['$FS'], $FS_readFile: 'FS.readFile', }); - -// Normally only the FS things that the compiler sees are needed are included. -// FORCE_FILESYSTEM makes us always include the FS object, which lets the user -// call APIs on it from JS freely. -if (FORCE_FILESYSTEM) { - extraLibraryFuncs.push('$FS'); -} diff --git a/src/lib/libglemu.js b/src/lib/libglemu.js index 0dbcad1d93e45..872f594ae4491 100644 --- a/src/lib/libglemu.js +++ b/src/lib/libglemu.js @@ -34,6 +34,7 @@ var LibraryGLEmulation = { 'glVertexAttribPointer', 'glActiveTexture', '$stringToNewUTF8', '$ptrToString', '$getEmscriptenSupportedExtensions', ], + $GLEmulation__force: true, $GLEmulation__postset: ` // Forward declare GL functions that are overridden by GLEmulation. /**@suppress {duplicate, undefinedVars}*/var _emscripten_glDrawArrays; @@ -3942,8 +3943,6 @@ var LibraryGLEmulation = { gluOrtho2D: (left, right, bottom, top) => _glOrtho(left, right, bottom, top, -1, 1), }; -extraLibraryFuncs.push('$GLEmulation'); - recordGLProcAddressGet(LibraryGLEmulation); addToLibrary(LibraryGLEmulation); diff --git a/src/lib/libwasmfs.js b/src/lib/libwasmfs.js index ebe5d87869ed8..e5c36ba8a9b21 100644 --- a/src/lib/libwasmfs.js +++ b/src/lib/libwasmfs.js @@ -5,6 +5,10 @@ */ addToLibrary({ +#if FORCE_FILESYSTEM + // Include FS even when it is not referenced by compiled code. + $FS__force: true, +#endif $MEMFS__deps: ['wasmfs_create_memory_backend'], $MEMFS: { createBackend(opts) { diff --git a/src/modules.mjs b/src/modules.mjs index bc6aa5295f5fa..a7341ef20f3d1 100644 --- a/src/modules.mjs +++ b/src/modules.mjs @@ -30,6 +30,8 @@ import {preprocess, processMacros} from './parseTools.mjs'; // List of symbols that were added from the library. export const librarySymbols = []; +// Library symbols exported via the `__export` decorator. +export const extraExports = new Set(); // Map of library symbols which are aliases for native symbols // e.g. `wasmTable` -> `__indirect_function_table` export const nativeAliases = {}; @@ -575,6 +577,7 @@ function exportRuntimeSymbols() { if ( !EXPORTED_RUNTIME_METHODS.has(name) && !EXPORTED_FUNCTIONS.has(name) && + !extraExports.has(name) && !unusedLibSymbols.has(name) ) { unexported.push(name); @@ -601,7 +604,7 @@ function exportLibrarySymbols() { assert(MODULARIZE != 'instance'); const results = ['// Begin JS library exports']; for (const ident of librarySymbols) { - if ((EXPORT_ALL || EXPORTED_FUNCTIONS.has(ident)) && !nativeAliases[ident]) { + if ((EXPORT_ALL || EXPORTED_FUNCTIONS.has(ident) || extraExports.has(ident)) && !nativeAliases[ident]) { results.push(exportSymbol(ident)); } } @@ -621,6 +624,7 @@ addToCompileTimeContext({ loadStructInfo, LibraryManager, librarySymbols, + extraExports, addToLibrary, cDefs, cDefine, diff --git a/src/utility.mjs b/src/utility.mjs index 7d185e0c89fa5..f827f2e7eef76 100644 --- a/src/utility.mjs +++ b/src/utility.mjs @@ -96,6 +96,8 @@ function range(size) { return Array.from(Array(size).keys()); } +export const extraLibraryFuncs = []; + export function mergeInto(obj, other, options = null) { if (options) { // check for unintended symbol redefinition @@ -148,11 +150,11 @@ export function mergeInto(obj, other, options = null) { } const index = key.lastIndexOf('__'); + const decorated = key.slice(0, index); const decoratorName = key.slice(index); const type = typeof other[key]; if (decoratorName == '__async') { - const decorated = key.slice(0, index); if (isJsOnlySymbol(decorated)) { error(`__async decorator applied to JS symbol: ${decorated}`); } @@ -188,12 +190,18 @@ export function mergeInto(obj, other, options = null) { __user: 'boolean', __async: ['string', 'boolean'], __i53abi: 'boolean', + __export: 'boolean', + __force: 'boolean', }; const expected = decoratorTypes[decoratorName]; if (type !== expected && !expected.includes(type)) { error(`Decorator (${key}) has wrong type. Expected '${expected}' not '${type}'`); } } + + if (decoratorName === '__force' && other[key]) { + extraLibraryFuncs.push(decorated); + } } } @@ -219,6 +227,8 @@ export const decoratorSuffixes = [ '__user', '__async', '__i53abi', + '__export', + '__force', ]; export function isDecorator(ident) { diff --git a/test/test_jslib.py b/test/test_jslib.py index e8d26e1618087..4f0236404e71b 100644 --- a/test/test_jslib.py +++ b/test/test_jslib.py @@ -6,7 +6,7 @@ from subprocess import PIPE from common import RunnerCore, copy_asset, create_file, read_file, test_file -from decorators import also_with_wasm64, also_without_bigint, parameterized +from decorators import also_with_wasm64, also_without_bigint, parameterized, requires_node_25 from tools.shared import EMCC from tools.utils import delete_file @@ -164,6 +164,88 @@ def test_jslib_exported(self): self.do_runf('src.c', 'c calling: 12\njs calling: 10.', cflags=['--js-library', 'lib.js', '-sEXPORTED_FUNCTIONS=_main,_jslibfunc']) + @parameterized({ + '': ([],), + 'optimized': (['-O3'],), + 'esm_integration': (['-sWASM_ESM_INTEGRATION'],), + 'esm_integration_optimized': (['-sWASM_ESM_INTEGRATION', '-O3'],), + }) + @requires_node_25 + def test_jslib_export_decorators_instance(self, args): + self.node_args += ['--no-warnings'] + create_file('lib.js', '''\ +addToLibrary({ + $dependencyExport__export: true, + $dependencyExport: () => 41, + + $forceOnly__deps: ['$dependencyExport'], + $forceOnly__force: true, + $forceOnly__postset: 'globalThis.forceOnlyIncluded = true', + $forceOnly: () => {}, + + $forceExport__export: true, + $forceExport__force: true, + $forceExport: () => 42, + + $unusedExport__export: true, + $unusedExport: () => 43, +}); +''') + create_file('main.c', 'int main() { return 0; }') + self.run_process([EMCC, 'main.c', '-sMODULARIZE=instance', '-Wno-experimental', + '--js-library', 'lib.js', '-o', 'mod.mjs'] + args + self.get_cflags()) + create_file('runner.mjs', ''' + import { strict as assert } from 'assert'; + import init, * as exports from './mod.mjs'; + await init(); + assert.equal(exports.dependencyExport(), 41); + assert.equal(exports.forceExport(), 42); + assert.equal(exports.forceOnly, undefined); + assert.equal(exports.unusedExport, undefined); + assert(globalThis.forceOnlyIncluded); + console.log('ok'); + ''') + self.assertContained('ok', self.run_js('runner.mjs')) + + def test_jslib_export_decorators(self): + create_file('lib.js', '''\ +addToLibrary({ + $dependencyExport__export: true, + $dependencyExport: () => 41, + + $forceOnly__deps: ['$dependencyExport'], + $forceOnly__force: true, + $forceOnly__postset: "Module['forceOnlyIncluded'] = true", + $forceOnly: () => {}, + + $forceExport__export: true, + $forceExport__force: true, + $forceExport: () => 42, + + $unusedExport__export: true, + $unusedExport: () => 43, +}); +''') + create_file('post.js', ''' + Module.onRuntimeInitialized = () => { + if (Module.dependencyExport() != 41) throw new Error('dependency export failed'); + if (Module.forceExport() != 42) throw new Error('forced export failed'); + const isExported = (name) => { + try { + return Module[name] !== undefined; + } catch { + return false; + } + }; + if (isExported('forceOnly')) throw new Error('force-only symbol was exported'); + if (isExported('unusedExport')) throw new Error('unused symbol was exported'); + if (!Module.forceOnlyIncluded) throw new Error('force-only symbol was not included'); + out('ok'); + }; + ''') + create_file('main.c', 'int main() { return 0; }') + self.do_runf('main.c', 'ok\n', cflags=['--js-library', 'lib.js', '--post-js', 'post.js']) + def test_jslib_using_asm_lib(self): create_file('lib.js', r''' addToLibrary({ @@ -283,6 +365,15 @@ def test_jslib_invalid_decorator(self): self.assert_fail([EMCC, test_file('hello_world.c'), '--js-library', 'lib.js'], "lib.js: Decorator (jslibfunc__internal) has wrong type. Expected 'boolean' not 'string'") + create_file('lib.js', r''' +addToLibrary({ + jslibfunc__export: 'yes', + jslibfunc: (x) => {}, +}); +''') + self.assert_fail([EMCC, test_file('hello_world.c'), '--js-library', 'lib.js'], + "lib.js: Decorator (jslibfunc__export) has wrong type. Expected 'boolean' not 'string'") + @also_with_wasm64 @also_without_bigint def test_jslib_i53abi(self): diff --git a/tools/building.py b/tools/building.py index fb9a99eceb364..52d523b52b98e 100644 --- a/tools/building.py +++ b/tools/building.py @@ -59,6 +59,8 @@ _is_ar_cache: dict[str, bool] = {} # the exports the user requested user_requested_exports: set[str] = set() +# JS library symbols exported via the `__export` decorator. +extra_js_exports: set[str] = set() # A list of feature flags to pass to each binaryen invocation (like `wasm-opt`, # etc.). This is received by the first call to binaryen (e.g. `wasm-emscripten-finalize`) # which reads it using `--detect-features`. @@ -830,7 +832,7 @@ def metadce(js_file, wasm_file, debug_info, last): return js_file graph = json.loads(txt) # ensure that functions expected to be exported to the outside are roots - required_symbols = user_requested_exports.union(set(settings.SIDE_MODULE_IMPORTS)) + required_symbols = user_requested_exports.union(extra_js_exports, settings.SIDE_MODULE_IMPORTS) for item in graph: if 'export' in item: export = asmjs_mangle(item['export']) diff --git a/tools/emscripten.py b/tools/emscripten.py index 38dfd6cb5c43d..f9c84a0a93b02 100644 --- a/tools/emscripten.py +++ b/tools/emscripten.py @@ -446,6 +446,8 @@ def emscript(in_wasm, out_wasm, outfile_js, js_syms, finalize=True, base_metadat report_missing_exports(forwarded_json['librarySymbols']) + building.extra_js_exports.update(forwarded_json['extraExports']) + asm_const_pairs = ['%s: %s' % (key, value) for key, value in asm_consts] if asm_const_pairs or settings.MAIN_MODULE: pre += 'var ASM_CONSTS = {\n ' + ', \n '.join(asm_const_pairs) + '\n};\n' @@ -964,7 +966,7 @@ def install_debug_wrapper(sym): def should_export(sym): - return settings.EXPORT_ALL or (settings.EXPORT_KEEPALIVE and sym in settings.EXPORTED_FUNCTIONS) + return settings.EXPORT_ALL or sym in building.extra_js_exports or (settings.EXPORT_KEEPALIVE and sym in settings.EXPORTED_FUNCTIONS) def create_receiving(function_exports, other_exports, library_symbols, aliases): @@ -986,6 +988,9 @@ def create_receiving(function_exports, other_exports, library_symbols, aliases): receiving.append('import {') receiving.append(' ' + ',\n '.join(exports)) receiving.append(f"}} from './{settings.WASM_BINARY_FILE}';") + alias_exports = building.extra_js_exports.intersection(aliases) + if alias_exports: + receiving.append(f"export {{ {', '.join(sorted(alias_exports))} }};") if generate_dyncall_assignment: receiving.append('\nfunction assignDynCalls() {') diff --git a/tools/link.py b/tools/link.py index 8d787765624f0..45b76a0026573 100644 --- a/tools/link.py +++ b/tools/link.py @@ -2195,6 +2195,7 @@ def node_detection_code(): def create_esm_wrapper(wrapper_file, support_target, wasm_target): js_exports = building.user_requested_exports.union(settings.EXPORTED_RUNTIME_METHODS) + js_exports |= building.extra_js_exports js_exports = ', '.join(sorted(js_exports)) wrapper = []