diff --git a/ChangeLog.md b/ChangeLog.md index 584f498653de8..9ba354a4fc94b 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -39,6 +39,10 @@ See docs/process.md for more on how version tagging works. detected automatically by the compiler, so `__deps` is no longer required for direct JS-to-JS references (`__deps` is still supported for dependencies on compiled C/C++ symbols or indirect dependencies). (#27995) +- In pthreads builds with `MAIN_MODULE`, workers no longer walk the main + module's whole function table, which created a JS function object for every + table entry in every worker. The main thread now passes them the table slots + of the main module's exported functions. (#28105) 6.0.13 - 10/19/26 ----------------- diff --git a/src/lib/libaddfunction.js b/src/lib/libaddfunction.js index 4f772bc6bfeff..d7838350d4331 100644 --- a/src/lib/libaddfunction.js +++ b/src/lib/libaddfunction.js @@ -124,6 +124,18 @@ addToLibrary({ // First, create the map if this is the first use. if (!functionsInTableMap) { functionsInTableMap = new WeakMap(); +#if MAIN_MODULE && PTHREADS + // Walking the main module's part of the table would create a JS wrapper + // for every function in it, in every worker. The dynamic linker only + // looks up the main module's exported functions, so workers add just the + // slots of those, as recorded by the main thread. + if (ENVIRONMENT_IS_PTHREAD && mainModuleExportSlots) { + for (var slot of mainModuleExportSlots) { + functionsInTableMap.set(getWasmTableEntry(slot), slot); + } + updateTableMap(mainModuleTableSize, {{{ from64Expr('wasmTable.length') }}} - mainModuleTableSize); + } else +#endif updateTableMap(0, {{{ from64Expr('wasmTable.length') }}}); } return functionsInTableMap.get(func) || 0; diff --git a/src/lib/libdylink.js b/src/lib/libdylink.js index ca593f7d4337f..c8b07324ec7b6 100644 --- a/src/lib/libdylink.js +++ b/src/lib/libdylink.js @@ -200,6 +200,24 @@ var LibraryDylink = { ; }, +#if PTHREADS + // Called on the main thread once the main module is instantiated. Records + // which table slots hold the main module's exported functions, which new + // workers use in getFunctionAddress. + $recordMainModuleExportSlots__internal: true, + $recordMainModuleExportSlots: (exports) => { + mainModuleTableSize = {{{ from64Expr('wasmTable.length') }}}; + mainModuleExportSlots = []; + for (var symName in exports) { + var value = exports[symName]; + var slot = typeof value === 'function' && getFunctionAddress(value); + if (slot) { + mainModuleExportSlots.push(slot); + } + } + }, +#endif + $updateGOT__internal: true, $updateGOT__docs: '/** @param {boolean=} replace */', $updateGOT: (exports, replace) => { diff --git a/src/lib/libpthread.js b/src/lib/libpthread.js index eabb60ac34f87..f615d137ce391 100644 --- a/src/lib/libpthread.js +++ b/src/lib/libpthread.js @@ -435,6 +435,8 @@ var LibraryPThread = { // Share all modules that have been loaded so far. New workers // won't start running threads until these are all loaded. sharedModules, + mainModuleTableSize, + mainModuleExportSlots, #endif #if ASSERTIONS workerID: worker.workerID, diff --git a/src/preamble.js b/src/preamble.js index c6656a977b60f..4a4cfc99909a8 100644 --- a/src/preamble.js +++ b/src/preamble.js @@ -798,6 +798,11 @@ function getWasmImports() { assignWasmExports(wasmExports); #if MAIN_MODULE +#if PTHREADS + if (!ENVIRONMENT_IS_PTHREAD) { + recordMainModuleExportSlots(origExports); + } +#endif updateGOT(origExports); #endif diff --git a/src/runtime_pthread.js b/src/runtime_pthread.js index 27996fc16499e..bd9ae42344f83 100644 --- a/src/runtime_pthread.js +++ b/src/runtime_pthread.js @@ -18,6 +18,11 @@ var workerID = 0; // Map of modules to be shared with new threads. This gets populated by the // main thread and shared with all new workers via the initial `load` message. var sharedModules = {}; + +// The main module's initial table size, and the table slots of its exported +// functions. Recorded by the main thread and shared with all new workers via +// the initial `load` message (see getFunctionAddress). +var mainModuleTableSize, mainModuleExportSlots; #endif var startWorker; @@ -73,6 +78,8 @@ if (ENVIRONMENT_IS_PTHREAD) { #if MAIN_MODULE dynamicLibraries = msgData.dynamicLibraries; sharedModules = msgData.sharedModules; + mainModuleTableSize = msgData.mainModuleTableSize; + mainModuleExportSlots = msgData.mainModuleExportSlots; #if RUNTIME_DEBUG dbg(`worker: received ${Object.keys(msgData.sharedModules).length} shared modules: ${Object.keys(msgData.sharedModules)}`); #endif diff --git a/test/core/pthread/test_pthread_dylink.c b/test/core/pthread/test_pthread_dylink.c index 6ab09b24cb07d..49a25adb3ca37 100644 --- a/test/core/pthread/test_pthread_dylink.c +++ b/test/core/pthread/test_pthread_dylink.c @@ -1,3 +1,4 @@ +#include #include #include #include "test_pthread_dylink.h" @@ -13,6 +14,8 @@ void print_addresses() { printf("main_module: &foo_side %p -> %d\n", &foo_side, foo_side); printf("main_module: &bar_main %p\n", &bar_main); printf("main_module: &bar_side %p\n", &bar_side); + // Each function has one address in every module and every thread. + assert(bar_main_addr_side() == &bar_main); } void* thread_main(void* arg) { diff --git a/test/core/pthread/test_pthread_dylink.h b/test/core/pthread/test_pthread_dylink.h index 2367131225ddc..7f17ab3ec0e79 100644 --- a/test/core/pthread/test_pthread_dylink.h +++ b/test/core/pthread/test_pthread_dylink.h @@ -5,3 +5,6 @@ int bar_main(); // Function defined in main int bar_side(); // Function defined in side void print_addresses_side(); + +typedef int (*bar_func)(); +bar_func bar_main_addr_side(); // &bar_main, as seen by side diff --git a/test/core/pthread/test_pthread_dylink_side.c b/test/core/pthread/test_pthread_dylink_side.c index cb33cb6614c76..80ab9d8461ba6 100644 --- a/test/core/pthread/test_pthread_dylink_side.c +++ b/test/core/pthread/test_pthread_dylink_side.c @@ -13,3 +13,7 @@ void print_addresses_side() { printf("side_module: &bar_main %p\n", &bar_main); printf("side_module: &bar_side %p\n", &bar_side); } + +bar_func bar_main_addr_side() { + return &bar_main; +} diff --git a/tools/link.py b/tools/link.py index 9b817534cd5fd..f94874fb43f0a 100644 --- a/tools/link.py +++ b/tools/link.py @@ -1278,6 +1278,8 @@ def limit_incoming_module_api(): settings.INCLUDE_FULL_LIBRARY = 1 # Called from preamble.js once the main module is instantiated. settings.DEFAULT_LIBRARY_FUNCS_TO_INCLUDE += ['$loadDylibs'] + if settings.PTHREADS: + settings.DEFAULT_LIBRARY_FUNCS_TO_INCLUDE += ['$recordMainModuleExportSlots'] # `__stack_pointer` is exported for PIC side modules. `__heap_base` is used # by `$getMemory` in `libdylink.js` (cannot be in `$getMemory__deps` because # `wasm-ld` processes stub library dependencies before synthesizing optional