diff --git a/ChangeLog.md b/ChangeLog.md index 92dd03ef909bd..cb0db2c2048c3 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -24,6 +24,13 @@ See docs/process.md for more on how version tagging works. `{{{ cDefs.FOO }}}`) are now exposed as `const` JavaScript library symbols (e.g., `ENOENT`, `O_CREAT`) and referenced directly in JS library code, making unminified output more readable. (#28144) +- 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. As a result, calling `addFunction` + in a worker on a main-module function that is not exported (for example one + obtained with `getWasmTableEntry`) now allocates a new table slot instead of + returning its existing address. (#28105) 7.0.0 - 11/06/26 ---------------- diff --git a/src/lib/libdylink.js b/src/lib/libdylink.js index 0972538cecb8a..38871cf542592 100644 --- a/src/lib/libdylink.js +++ b/src/lib/libdylink.js @@ -201,6 +201,35 @@ var LibraryDylink = { ; }, +#if PTHREADS + // Called once the main module is instantiated, before updateGOT. The first + // getFunctionAddress walks the whole table, creating a JS function object for + // every entry, which every worker would keep for its lifetime. The dynamic + // linker only looks up the main module's exported functions, so the main + // thread records their table slots and workers start functionsInTableMap + // with just those. Side modules add their own slots when they are loaded. + // As a consequence, addFunction in a worker on a main-module function that + // is in the table but not exported gives it a new slot (ASSERTIONS=2 reports + // this). + $initFunctionsInTableMap__internal: true, + $initFunctionsInTableMap: (exports) => { + if (!ENVIRONMENT_IS_PTHREAD) { + mainModuleExportSlots = []; + for (var value of Object.values(exports)) { + var addr = typeof value == 'function' && getFunctionAddress(value); + if (addr) { + mainModuleExportSlots.push(addr); + } + } + } else if (mainModuleExportSlots) { + functionsInTableMap = new WeakMap(); + for (var slot of mainModuleExportSlots) { + functionsInTableMap.set(getWasmTableEntry(slot), 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 5cfd2180ff438..05cedb74d3c32 100644 --- a/src/lib/libpthread.js +++ b/src/lib/libpthread.js @@ -434,6 +434,7 @@ var LibraryPThread = { // Share all modules that have been loaded so far. New workers // won't start running threads until these are all loaded. sharedModules, + mainModuleExportSlots, #endif #if ASSERTIONS workerID: worker.workerID, diff --git a/src/preamble.js b/src/preamble.js index 7f5d17868b610..3a996c8dc179c 100644 --- a/src/preamble.js +++ b/src/preamble.js @@ -798,6 +798,9 @@ function getWasmImports() { assignWasmExports(wasmExports); #if MAIN_MODULE +#if PTHREADS + initFunctionsInTableMap(origExports); +#endif updateGOT(origExports); #endif diff --git a/src/runtime_pthread.js b/src/runtime_pthread.js index 4f0841c60582f..61dfb80bd4384 100644 --- a/src/runtime_pthread.js +++ b/src/runtime_pthread.js @@ -12,6 +12,11 @@ // 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 = {}; + +// Table slots of the main module's exported functions. Recorded by the main +// thread and shared with all new workers via the initial `load` message (see +// initFunctionsInTableMap). +var mainModuleExportSlots; #endif var startWorker; @@ -67,6 +72,7 @@ if (ENVIRONMENT_IS_PTHREAD) { #if MAIN_MODULE dynamicLibraries = msgData.dynamicLibraries; sharedModules = msgData.sharedModules; + 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 c33dcf453753e..8643fc413306f 100644 --- a/tools/link.py +++ b/tools/link.py @@ -1283,6 +1283,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 += ['$initFunctionsInTableMap'] # `__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