From bc1913efe2b78356d4abba789205f5058d010fe2 Mon Sep 17 00:00:00 2001 From: Kohei Tokunaga Date: Wed, 8 Oct 2025 01:34:29 +0900 Subject: [PATCH 01/10] select: separate parsers to functions Signed-off-by: Kohei Tokunaga --- src/lib/libsyscall.js | 128 +++++++++++++++++++++++++----------------- 1 file changed, 76 insertions(+), 52 deletions(-) diff --git a/src/lib/libsyscall.js b/src/lib/libsyscall.js index 5f9a0be9afb17..da7e8e3882282 100644 --- a/src/lib/libsyscall.js +++ b/src/lib/libsyscall.js @@ -102,6 +102,71 @@ var SyscallsLibrary = { #endif return ret; }, + + getTimeoutInMillis(timeout) { + // select(2) is declared to accept "struct timeval { time_t tv_sec; suseconds_t tv_usec; }". + // However, musl passes the two values to the syscall as an array of long values. + // Note that sizeof(time_t) != sizeof(long) in wasm32. The former is 8, while the latter is 4. + // This means using "C_STRUCTS.timeval.tv_usec" leads to a wrong offset. + // So, instead, we use POINTER_SIZE. + var tv_sec = ({{{ makeGetValue('timeout', 0, 'i32') }}}), + tv_usec = ({{{ makeGetValue('timeout', POINTER_SIZE, 'i32') }}}); + return (tv_sec + tv_usec / 1000000) * 1000; + }, + + parseSelectFDSet(readfds, writefds, exceptfds) { + var total = 0; + + var srcReadLow = (readfds ? {{{ makeGetValue('readfds', 0, 'i32') }}} : 0), + srcReadHigh = (readfds ? {{{ makeGetValue('readfds', 4, 'i32') }}} : 0); + var srcWriteLow = (writefds ? {{{ makeGetValue('writefds', 0, 'i32') }}} : 0), + srcWriteHigh = (writefds ? {{{ makeGetValue('writefds', 4, 'i32') }}} : 0); + var srcExceptLow = (exceptfds ? {{{ makeGetValue('exceptfds', 0, 'i32') }}} : 0), + srcExceptHigh = (exceptfds ? {{{ makeGetValue('exceptfds', 4, 'i32') }}} : 0); + + var dstReadLow = 0, + dstReadHigh = 0; + var dstWriteLow = 0, + dstWriteHigh = 0; + var dstExceptLow = 0, + dstExceptHigh = 0; + + var check = (fd, low, high, val) => fd < 32 ? (low & val) : (high & val); + + return { + getTotal: () => total, + setFlags: (fd, flags) => { + var mask = 1 << (fd % 32); + + if ((flags & {{{ cDefs.POLLIN }}}) && check(fd, srcReadLow, srcReadHigh, mask)) { + fd < 32 ? (dstReadLow = dstReadLow | mask) : (dstReadHigh = dstReadHigh | mask); + total++; + } + if ((flags & {{{ cDefs.POLLOUT }}}) && check(fd, srcWriteLow, srcWriteHigh, mask)) { + fd < 32 ? (dstWriteLow = dstWriteLow | mask) : (dstWriteHigh = dstWriteHigh | mask); + total++; + } + if ((flags & {{{ cDefs.POLLPRI }}}) && check(fd, srcExceptLow, srcExceptHigh, mask)) { + fd < 32 ? (dstExceptLow = dstExceptLow | mask) : (dstExceptHigh = dstExceptHigh | mask); + total++; + } + }, + commit: () => { + if (readfds) { + {{{ makeSetValue('readfds', '0', 'dstReadLow', 'i32') }}}; + {{{ makeSetValue('readfds', '4', 'dstReadHigh', 'i32') }}}; + } + if (writefds) { + {{{ makeSetValue('writefds', '0', 'dstWriteLow', 'i32') }}}; + {{{ makeSetValue('writefds', '4', 'dstWriteHigh', 'i32') }}}; + } + if (exceptfds) { + {{{ makeSetValue('exceptfds', '0', 'dstExceptLow', 'i32') }}}; + {{{ makeSetValue('exceptfds', '4', 'dstExceptHigh', 'i32') }}}; + } + } + }; + }, }, $syscallGetVarargI__internal: true, @@ -552,21 +617,7 @@ var SyscallsLibrary = { assert(nfds <= 64, 'nfds must be less than or equal to 64'); // fd sets have 64 bits // TODO: this could be 1024 based on current musl headers #endif - var total = 0; - - var srcReadLow = (readfds ? {{{ makeGetValue('readfds', 0, 'i32') }}} : 0), - srcReadHigh = (readfds ? {{{ makeGetValue('readfds', 4, 'i32') }}} : 0); - var srcWriteLow = (writefds ? {{{ makeGetValue('writefds', 0, 'i32') }}} : 0), - srcWriteHigh = (writefds ? {{{ makeGetValue('writefds', 4, 'i32') }}} : 0); - var srcExceptLow = (exceptfds ? {{{ makeGetValue('exceptfds', 0, 'i32') }}} : 0), - srcExceptHigh = (exceptfds ? {{{ makeGetValue('exceptfds', 4, 'i32') }}} : 0); - - var dstReadLow = 0, - dstReadHigh = 0; - var dstWriteLow = 0, - dstWriteHigh = 0; - var dstExceptLow = 0, - dstExceptHigh = 0; + var fdSet = SYSCALLS.parseSelectFDSet(readfds, writefds, exceptfds); var allLow = (readfds ? {{{ makeGetValue('readfds', 0, 'i32') }}} : 0) | (writefds ? {{{ makeGetValue('writefds', 0, 'i32') }}} : 0) | @@ -577,6 +628,11 @@ var SyscallsLibrary = { var check = (fd, low, high, val) => fd < 32 ? (low & val) : (high & val); + var timeoutInMillis = -1; + if (timeout) { + timeoutInMillis = SYSCALLS.getTimeoutInMillis(timeout); + } + for (var fd = 0; fd < nfds; fd++) { var mask = 1 << (fd % 32); if (!(check(fd, allLow, allHigh, mask))) { @@ -588,48 +644,16 @@ var SyscallsLibrary = { var flags = SYSCALLS.DEFAULT_POLLMASK; if (stream.stream_ops.poll) { - var timeoutInMillis = -1; - if (timeout) { - // select(2) is declared to accept "struct timeval { time_t tv_sec; suseconds_t tv_usec; }". - // However, musl passes the two values to the syscall as an array of long values. - // Note that sizeof(time_t) != sizeof(long) in wasm32. The former is 8, while the latter is 4. - // This means using "C_STRUCTS.timeval.tv_usec" leads to a wrong offset. - // So, instead, we use POINTER_SIZE. - var tv_sec = (readfds ? {{{ makeGetValue('timeout', 0, 'i32') }}} : 0), - tv_usec = (readfds ? {{{ makeGetValue('timeout', POINTER_SIZE, 'i32') }}} : 0); - timeoutInMillis = (tv_sec + tv_usec / 1000000) * 1000; - } - flags = stream.stream_ops.poll(stream, timeoutInMillis); + flags = stream.stream_ops.poll(stream, ((timeoutInMillis < 0) || readfds) ? timeoutInMillis : 0); } - if ((flags & {{{ cDefs.POLLIN }}}) && check(fd, srcReadLow, srcReadHigh, mask)) { - fd < 32 ? (dstReadLow = dstReadLow | mask) : (dstReadHigh = dstReadHigh | mask); - total++; - } - if ((flags & {{{ cDefs.POLLOUT }}}) && check(fd, srcWriteLow, srcWriteHigh, mask)) { - fd < 32 ? (dstWriteLow = dstWriteLow | mask) : (dstWriteHigh = dstWriteHigh | mask); - total++; - } - if ((flags & {{{ cDefs.POLLPRI }}}) && check(fd, srcExceptLow, srcExceptHigh, mask)) { - fd < 32 ? (dstExceptLow = dstExceptLow | mask) : (dstExceptHigh = dstExceptHigh | mask); - total++; - } + fdSet.setFlags(fd, flags); } - if (readfds) { - {{{ makeSetValue('readfds', '0', 'dstReadLow', 'i32') }}}; - {{{ makeSetValue('readfds', '4', 'dstReadHigh', 'i32') }}}; - } - if (writefds) { - {{{ makeSetValue('writefds', '0', 'dstWriteLow', 'i32') }}}; - {{{ makeSetValue('writefds', '4', 'dstWriteHigh', 'i32') }}}; - } - if (exceptfds) { - {{{ makeSetValue('exceptfds', '0', 'dstExceptLow', 'i32') }}}; - {{{ makeSetValue('exceptfds', '4', 'dstExceptHigh', 'i32') }}}; - } - return total; + fdSet.commit(fd, flags); + + return fdSet.getTotal(); }, _msync_js__i53abi: true, _msync_js: (addr, len, prot, flags, fd, offset) => { From 586aaa73d68fcad63e3bc637e362d184c91eb7b3 Mon Sep 17 00:00:00 2001 From: Kohei Tokunaga Date: Wed, 8 Oct 2025 02:05:16 +0900 Subject: [PATCH 02/10] select: Enable timeout on select when PROXY_TO_PTHREAD This commit enables the select syscall to handle timeout with multiple event sources. PROXY_TO_PTHREAD is needed to prevent blocking the main worker. When a thread worker invokes the select syscall with non-zero timeout and no fd is ready, it blocks using Atmoics.wait until it receives a readiness notification. On the main worker, the underlying stream implementation can trigger readiness using Atomics.notify through a callback. A notification also issued automatically once the specified timeout expires. Communication between the thread worker and the main worker occurs via a shared memory region. To prevent a select invocation being unblocked by the callbacks of a previous invocation, all active callbacks are tracked in a list. See the comments in activeSelectCallbacks for details. Usage of the notification callback is optional. If the stream implementation doesn't support it, the "poll" method can still synchronously return the event status. Signed-off-by: Kohei Tokunaga --- src/lib/libpthread.js | 54 ++++++++++++++++++++++++++++++++++ src/lib/libsyscall.js | 68 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 122 insertions(+) diff --git a/src/lib/libpthread.js b/src/lib/libpthread.js index b4885c15f0a15..a388eed5a3536 100644 --- a/src/lib/libpthread.js +++ b/src/lib/libpthread.js @@ -88,6 +88,11 @@ var LibraryPThread = { 'exit', #if PTHREADS_DEBUG || ASSERTIONS '$ptrToString', +#endif +#if PROXY_TO_PTHREAD + '$addThreadToActiveSelectCallbacks', + '$removeThreadFromActiveSelectCallbacks', + '$activeSelectCallbacks', #endif ], $PThread: { @@ -577,6 +582,9 @@ var LibraryPThread = { #endif #if ASSERTIONS assert(worker); +#endif +#if PROXY_TO_PTHREAD + removeThreadFromActiveSelectCallbacks(pthread_ptr); #endif PThread.returnWorkerToPool(worker); }, @@ -621,6 +629,49 @@ var LibraryPThread = { $registerTLSInit: (tlsInitFunc) => PThread.tlsInitFunctions.push(tlsInitFunc), #endif +#if PROXY_TO_PTHREAD + // On the main worker, activeSelectCallbacks records the set of callbacks + // that are allowed to update the shared region. Any callback not in this + // set (i.e. when !isActiveSelectCallback) must not update the region. + // + // Each select syscall invocation must call deactivateSelectCallbacks to + // reset this set, ensuring that callbacks from previous invocations don't + // affect the current one. + // + // If a callback executes after the thread worker has already returned (due + // to a timeout, a readiness notification or other exceptional conditions) + // but before the next deactivation, it may still update the shared region. + // However the thread worker will not read that value and just ignore it. + // + // activeSelectCallbacks records multiple callback lists one per thread + // worker so that each worker can manage its own set of active callbacks + // independently. + $activeSelectCallbacks: {}, + $addThreadToActiveSelectCallbacks__deps: ['malloc'], + $addThreadToActiveSelectCallbacks: (pthread_ptr) => { + activeSelectCallbacks[pthread_ptr] = { + buf: _malloc(8), + callbacks: [], + }; + }, + $removeThreadFromActiveSelectCallbacks: (pthread_ptr) => { + delete activeSelectCallbacks[pthread_ptr]; + }, + $getActiveSelectCallbacks: (pthread_ptr) => { + return activeSelectCallbacks[pthread_ptr]; + }, + $deactivateSelectCallbacks: (pthread_ptr) => { + activeSelectCallbacks[pthread_ptr].callbacks = []; + }, + $activateSelectCallback: (pthread_ptr, cb) => { + activeSelectCallbacks[pthread_ptr].callbacks.push(cb); + }, + $isActiveSelectCallback: (pthread_ptr, cb) => { + return (activeSelectCallbacks[pthread_ptr] != null) && + (activeSelectCallbacks[pthread_ptr].callbacks.indexOf(cb) != -1); + }, +#endif + $spawnThread: (threadParams) => { #if ASSERTIONS assert(!ENVIRONMENT_IS_PTHREAD, 'Internal Error! spawnThread() can only ever be called from main application thread!'); @@ -648,6 +699,9 @@ var LibraryPThread = { arg: threadParams.arg, pthread_ptr: threadParams.pthread_ptr, }; +#if PROXY_TO_PTHREAD + addThreadToActiveSelectCallbacks(threadParams.pthread_ptr); +#endif #if OFFSCREENCANVAS_SUPPORT // Note that we do not need to quote these names because they are only used // in this file, and not from the external worker.js. diff --git a/src/lib/libsyscall.js b/src/lib/libsyscall.js index da7e8e3882282..2b380252dd437 100644 --- a/src/lib/libsyscall.js +++ b/src/lib/libsyscall.js @@ -607,7 +607,35 @@ var SyscallsLibrary = { FS.chdir(stream.path); return 0; }, + __syscall__newselect__deps: ['$newselectInner','malloc','free'], + __syscall__newselect__proxy: 'none', __syscall__newselect: (nfds, readfds, writefds, exceptfds, timeout) => { +#if PROXY_TO_PTHREAD + var waitPtr = _malloc(8); + var result = newselectInner(nfds, readfds, writefds, exceptfds, timeout, waitPtr); + if ((result != 0) || ((timeout) && (SYSCALLS.getTimeoutInMillis(timeout) == 0))) { + _free(waitPtr); + return result; + } + var fdRegion = {{{ makeGetValue('waitPtr', 0, '*') }}}; + Atomics.wait(HEAP32 , fdRegion >> 2, -1); + var fd = Atomics.load(HEAP32 , fdRegion >> 2); + var flags = Atomics.load(HEAP32 , fdRegion >> 2 + 1); + _free(waitPtr); + if (fd < 0) return 0; + var fdSet = SYSCALLS.parseSelectFDSet(readfds, writefds, exceptfds); + fdSet.setFlags(fd, flags); + fdSet.commit(); + return fdSet.getTotal(); +#else + return newselectInner(nfds, readfds, writefds, exceptfds, timeout, -1); +#endif + }, +#if PROXY_TO_PTHREAD + $newselectInner__deps: ['$PThread', '$deactivateSelectCallbacks', '$getActiveSelectCallbacks', '$activateSelectCallback', '$isActiveSelectCallback'], +#endif + $newselectInner__proxy: 'sync', + $newselectInner: (nfds, readfds, writefds, exceptfds, timeout, waitPtr) => { // readfds are supported, // writefds checks socket open status // exceptfds are supported, although on web, such exceptional conditions never arise in web sockets @@ -633,6 +661,34 @@ var SyscallsLibrary = { timeoutInMillis = SYSCALLS.getTimeoutInMillis(timeout); } +#if PROXY_TO_PTHREAD + const pthread_ptr = PThread.currentProxiedOperationCallerThread; + deactivateSelectCallbacks(pthread_ptr); // deactivate all old callbacks + var makeNotifyCallback = (fd) => null; + if (timeoutInMillis != 0) { + var info = getActiveSelectCallbacks(pthread_ptr); + {{{ makeSetValue('waitPtr', 0, 'info.buf', '*') }}}; + Atomics.store(HEAP32, info.buf >> 2, -1); // Initialize the shared region + makeNotifyCallback = (fd) => { + var cb = (flags) => { + if (!isActiveSelectCallback(pthread_ptr, cb)) { + return; // This callback is no longer active. + } + deactivateSelectCallbacks(pthread_ptr); // Only the first event is notified. + Atomics.store(HEAP32, info.buf >> 2 + 1, flags); + Atomics.store(HEAP32, info.buf >> 2, fd); + Atomics.notify(HEAP32, info.buf >> 2); + } + activateSelectCallback(pthread_ptr, cb); + return cb; + } + if (timeoutInMillis > 0) { + var cb = makeNotifyCallback(-2); + setTimeout(() => cb(0), timeoutInMillis); + } + } +#endif + for (var fd = 0; fd < nfds; fd++) { var mask = 1 << (fd % 32); if (!(check(fd, allLow, allHigh, mask))) { @@ -644,14 +700,26 @@ var SyscallsLibrary = { var flags = SYSCALLS.DEFAULT_POLLMASK; if (stream.stream_ops.poll) { +#if PROXY_TO_PTHREAD + flags = stream.stream_ops.poll(stream, timeoutInMillis, makeNotifyCallback(fd)); +#else flags = stream.stream_ops.poll(stream, ((timeoutInMillis < 0) || readfds) ? timeoutInMillis : 0); +#endif } fdSet.setFlags(fd, flags); } +#if PROXY_TO_PTHREAD + if ((fdSet.getTotal() > 0) || (timeoutInMillis == 0) ) { + fdSet.commit(fd, flags); + // No wait will happen in the caller. Deactivate all callbacks. + deactivateSelectCallbacks(pthread_ptr); + } +#else fdSet.commit(fd, flags); +#endif return fdSet.getTotal(); }, From b241a3e9bf3bf33b4218aa4a9bd3fd99ef27fc1b Mon Sep 17 00:00:00 2001 From: Kohei Tokunaga Date: Wed, 8 Oct 2025 02:05:44 +0900 Subject: [PATCH 03/10] select: Allow registering cleanup method for the callback This commit adds the registerCleanupFunc method for the callback passed to the stream implementation. It allows the stream implementation to optionally register a cleanup function that will be invoked when select is no longer interested in the stream events. This enables the stream implementation to perform additional cleanups such as discarding the reference to the event callback. Signed-off-by: Kohei Tokunaga --- src/lib/libsyscall.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/lib/libsyscall.js b/src/lib/libsyscall.js index 2b380252dd437..2df831a2d5835 100644 --- a/src/lib/libsyscall.js +++ b/src/lib/libsyscall.js @@ -665,6 +665,7 @@ var SyscallsLibrary = { const pthread_ptr = PThread.currentProxiedOperationCallerThread; deactivateSelectCallbacks(pthread_ptr); // deactivate all old callbacks var makeNotifyCallback = (fd) => null; + var cleanupFuncs = []; if (timeoutInMillis != 0) { var info = getActiveSelectCallbacks(pthread_ptr); {{{ makeSetValue('waitPtr', 0, 'info.buf', '*') }}}; @@ -675,10 +676,14 @@ var SyscallsLibrary = { return; // This callback is no longer active. } deactivateSelectCallbacks(pthread_ptr); // Only the first event is notified. + cleanupFuncs.forEach(cb => cb()); Atomics.store(HEAP32, info.buf >> 2 + 1, flags); Atomics.store(HEAP32, info.buf >> 2, fd); Atomics.notify(HEAP32, info.buf >> 2); } + cb.registerCleanupFunc = (f) => { + if (f != null) cleanupFuncs.push(f); + } activateSelectCallback(pthread_ptr, cb); return cb; } @@ -716,6 +721,7 @@ var SyscallsLibrary = { fdSet.commit(fd, flags); // No wait will happen in the caller. Deactivate all callbacks. deactivateSelectCallbacks(pthread_ptr); + cleanupFuncs.forEach(f => f()); } #else fdSet.commit(fd, flags); From 5318eb8e781a3d54176c2901dc3fcffd61a3f427 Mon Sep 17 00:00:00 2001 From: Kohei Tokunaga Date: Wed, 8 Oct 2025 01:36:00 +0900 Subject: [PATCH 04/10] select: update code comment about timeout Signed-off-by: Kohei Tokunaga --- src/lib/libsyscall.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/lib/libsyscall.js b/src/lib/libsyscall.js index 2df831a2d5835..1a491130df20c 100644 --- a/src/lib/libsyscall.js +++ b/src/lib/libsyscall.js @@ -640,7 +640,8 @@ var SyscallsLibrary = { // writefds checks socket open status // exceptfds are supported, although on web, such exceptional conditions never arise in web sockets // and so the exceptfds list will always return empty. - // timeout is supported, although on SOCKFS and PIPEFS these are ignored and always treated as 0 - fully async + // timeout is supported, although on SOCKFS these are ignored and always treated as 0 - fully async + // and PIPEFS supports timeout only when PROXY_TO_PTHREAD is enabled. #if ASSERTIONS assert(nfds <= 64, 'nfds must be less than or equal to 64'); // fd sets have 64 bits // TODO: this could be 1024 based on current musl headers #endif From 7ac724a988b7579405f6daea95922e77cc3d9e3d Mon Sep 17 00:00:00 2001 From: Kohei Tokunaga Date: Wed, 8 Oct 2025 01:36:40 +0900 Subject: [PATCH 05/10] pipefs: Enable pipes to notify readiness The poll method of PIPEFS receives a notification callback from the caller. PIPEFS notifies the caller when the fd becomes readable using that callback. Signed-off-by: Kohei Tokunaga --- src/lib/libpipefs.js | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/lib/libpipefs.js b/src/lib/libpipefs.js index d9a3fed62180e..25ad2cea03cfd 100644 --- a/src/lib/libpipefs.js +++ b/src/lib/libpipefs.js @@ -21,6 +21,19 @@ addToLibrary({ // able to read from the read end after write end is closed. refcnt : 2, timestamp: new Date(), + readableHandlers: [], + registerReadableHanlders: (notifyCallback) => { + if (notifyCallback == null) return; + notifyCallback.registerCleanupFunc(() => { + const i = pipe.readableHandlers.indexOf(notifyCallback); + if (i !== -1) pipe.readableHandlers.splice(i, 1); + }); + pipe.readableHandlers.push(notifyCallback); + }, + notifyReadableHanders: () => { + pipe.readableHandlers.forEach(cb => cb({{{ cDefs.POLLRDNORM }}} | {{{ cDefs.POLLIN }}})); + pipe.readableHandlers = []; + } }; pipe.buckets.push({ @@ -80,7 +93,7 @@ addToLibrary({ blocks: 0, }; }, - poll(stream) { + poll(stream, timeout, notifyCallback) { var pipe = stream.node.pipe; if ((stream.flags & {{{ cDefs.O_ACCMODE }}}) === {{{ cDefs.O_WRONLY }}}) { @@ -92,6 +105,7 @@ addToLibrary({ } } + pipe.registerReadableHanlders(notifyCallback); return 0; }, dup(stream) { @@ -204,6 +218,7 @@ addToLibrary({ if (freeBytesInCurrBuffer >= dataLen) { currBucket.buffer.set(data, currBucket.offset); currBucket.offset += dataLen; + pipe.notifyReadableHanders(); return dataLen; } else if (freeBytesInCurrBuffer > 0) { currBucket.buffer.set(data.subarray(0, freeBytesInCurrBuffer), currBucket.offset); @@ -235,6 +250,7 @@ addToLibrary({ newBucket.buffer.set(data); } + pipe.notifyReadableHanders(); return dataLen; }, close(stream) { From f51dbb8b76a805a12178b04b7c1a3f0d33c8f189 Mon Sep 17 00:00:00 2001 From: Kohei Tokunaga Date: Wed, 8 Oct 2025 01:37:23 +0900 Subject: [PATCH 06/10] test: Add tests for timeout on select with PROXY_TO_PTHREAD Signed-off-by: Kohei Tokunaga --- test/core/test_pthread_select_timeout.c | 129 ++++++++++++++++++++++++ test/test_core.py | 3 + 2 files changed, 132 insertions(+) create mode 100644 test/core/test_pthread_select_timeout.c diff --git a/test/core/test_pthread_select_timeout.c b/test/core/test_pthread_select_timeout.c new file mode 100644 index 0000000000000..2f8cd4fa81a1d --- /dev/null +++ b/test/core/test_pthread_select_timeout.c @@ -0,0 +1,129 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +// Check if timeout works without fds +void test_timeout_without_fds() +{ + struct timeval tv, begin, end; + + tv.tv_sec = 1; + tv.tv_usec = 0; + gettimeofday(&begin, NULL); + assert(select(0, NULL, NULL, NULL, &tv) == 0); + gettimeofday(&end, NULL); + assert((end.tv_sec - begin.tv_sec) * 1000000 + end.tv_usec - begin.tv_usec >= 1000000); +} + +// Check if timeout works with fds without events +void test_timeout_with_fds_without_events() +{ + struct timeval tv, begin, end; + fd_set readfds; + int pipe_a[2]; + + assert(pipe(pipe_a) == 0); + + tv.tv_sec = 1; + tv.tv_usec = 0; + FD_ZERO(&readfds); + FD_SET(pipe_a[0], &readfds); + gettimeofday(&begin, NULL); + assert(select(pipe_a[0] + 1, &readfds, NULL, NULL, &tv) == 0); + gettimeofday(&end, NULL); + assert((end.tv_sec - begin.tv_sec) * 1000000 + end.tv_usec - begin.tv_usec >= 1000000); + + close(pipe_a[0]); close(pipe_a[1]); +} + +int pipe_shared[2]; + +void *wakeup_after_2s(void * arg) +{ + const char *t = "test\n"; + + sleep(2); + write(pipe_shared[1], t, strlen(t)); + + return NULL; +} + +// Check if select can unblock on an event +void test_unblock_select() +{ + struct timeval begin, end; + fd_set readfds; + int maxfd; + pthread_t tid; + int pipe_a[2]; + + assert(pipe(pipe_a) == 0); + assert(pipe(pipe_shared) == 0); + + FD_ZERO(&readfds); + FD_SET(pipe_a[0], &readfds); + FD_SET(pipe_shared[0], &readfds); + maxfd = (pipe_a[0] > pipe_shared[0] ? pipe_a[0] : pipe_shared[0]); + assert(pthread_create(&tid, NULL, wakeup_after_2s, NULL) == 0); + gettimeofday(&begin, NULL); + assert(select(maxfd + 1, &readfds, NULL, NULL, NULL) == 1); + gettimeofday(&end, NULL); + assert(FD_ISSET(pipe_shared[0], &readfds)); + assert((end.tv_sec - begin.tv_sec) * 1000000 + end.tv_usec - begin.tv_usec >= 1000000); + + pthread_join(tid, NULL); + + close(pipe_a[0]); close(pipe_a[1]); + close(pipe_shared[0]); close(pipe_shared[1]); +} + +// Check if select works with ready fds +void test_ready_fds() +{ + struct timeval tv; + fd_set readfds; + int maxfd; + const char *t = "test\n"; + int pipe_c[2]; + int pipe_d[2]; + + assert(pipe(pipe_c) == 0); + assert(pipe(pipe_d) == 0); + + write(pipe_c[1], t, strlen(t)); + write(pipe_d[1], t, strlen(t)); + maxfd = (pipe_c[0] > pipe_d[0] ? pipe_c[0] : pipe_d[0]); + + tv.tv_sec = 0; + tv.tv_usec = 0; + FD_ZERO(&readfds); + FD_SET(pipe_c[0], &readfds); + FD_SET(pipe_d[0], &readfds); + assert(select(maxfd + 1, &readfds, NULL, NULL, &tv) == 2); + assert(FD_ISSET(pipe_c[0], &readfds)); + assert(FD_ISSET(pipe_d[0], &readfds)); + + FD_ZERO(&readfds); + FD_SET(pipe_c[0], &readfds); + FD_SET(pipe_d[0], &readfds); + assert(select(maxfd + 1, &readfds, NULL, NULL, NULL) == 2); + assert(FD_ISSET(pipe_c[0], &readfds)); + assert(FD_ISSET(pipe_d[0], &readfds)); + + close(pipe_c[0]); close(pipe_c[1]); + close(pipe_d[0]); close(pipe_d[1]); +} + +int main() +{ + test_timeout_without_fds(); + test_timeout_with_fds_without_events(); + test_unblock_select(); + test_ready_fds(); + return 0; +} diff --git a/test/test_core.py b/test/test_core.py index 0c9611728978a..e28e592758fa2 100644 --- a/test/test_core.py +++ b/test/test_core.py @@ -9680,6 +9680,9 @@ def test_externref_emjs(self, dynlink): def test_syscall_intercept(self): self.do_core_test('test_syscall_intercept.c') + def test_pthread_select_timeout(self): + self.do_runf('core/test_pthread_select_timeout.c', cflags=['-pthread', '-sPROXY_TO_PTHREAD=1', '-sEXIT_RUNTIME=1', '-Wno-pthreads-mem-growth']) + @also_without_bigint def test_jslib_i64_params(self): # Tests the defineI64Param and receiveI64ParamAsI53 helpers that are From 23a3811ad35cf8c8d2b51a909df8305acc2bd203 Mon Sep 17 00:00:00 2001 From: Kohei Tokunaga Date: Wed, 8 Oct 2025 01:37:42 +0900 Subject: [PATCH 07/10] test: Add test for select without PROXY_TO_PTHREAD Signed-off-by: Kohei Tokunaga --- test/core/test_select.c | 25 +++++++++++++++++++++++++ test/test_core.py | 3 +++ 2 files changed, 28 insertions(+) create mode 100644 test/core/test_select.c diff --git a/test/core/test_select.c b/test/core/test_select.c new file mode 100644 index 0000000000000..97412d7049120 --- /dev/null +++ b/test/core/test_select.c @@ -0,0 +1,25 @@ +#include +#include +#include +#include +#include +#include + +int pipe_a[2]; + +int main() +{ + fd_set readfds; + const char *t = "test\n"; + + assert(pipe(pipe_a) == 0); + FD_ZERO(&readfds); + FD_SET(pipe_a[0], &readfds); + write(pipe_a[1], t, strlen(t)); + assert(select(pipe_a[0] + 1, &readfds, NULL, NULL, NULL) == 1); + assert(FD_ISSET(pipe_a[0], &readfds)); + + close(pipe_a[0]); close(pipe_a[1]); + + return 0; +} diff --git a/test/test_core.py b/test/test_core.py index e28e592758fa2..48dc82b9face6 100644 --- a/test/test_core.py +++ b/test/test_core.py @@ -9683,6 +9683,9 @@ def test_syscall_intercept(self): def test_pthread_select_timeout(self): self.do_runf('core/test_pthread_select_timeout.c', cflags=['-pthread', '-sPROXY_TO_PTHREAD=1', '-sEXIT_RUNTIME=1', '-Wno-pthreads-mem-growth']) + def test_select(self): + self.do_runf('core/test_select.c') + @also_without_bigint def test_jslib_i64_params(self): # Tests the defineI64Param and receiveI64ParamAsI53 helpers that are From 2e87162fa5974a935dd58171c6f60b08ae25ff72 Mon Sep 17 00:00:00 2001 From: Kohei Tokunaga Date: Wed, 8 Oct 2025 16:13:33 +0900 Subject: [PATCH 08/10] test: specify timeout as 0 to select when expecting nonblocking Signed-off-by: Kohei Tokunaga --- test/sockets/test_sockets_echo_client.c | 5 ++++- test/sockets/test_sockets_echo_server.c | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/test/sockets/test_sockets_echo_client.c b/test/sockets/test_sockets_echo_client.c index 64df6b969b8cc..1c1afd52d1787 100644 --- a/test/sockets/test_sockets_echo_client.c +++ b/test/sockets/test_sockets_echo_client.c @@ -67,13 +67,16 @@ void main_loop() { fd_set fdr; fd_set fdw; int res; + struct timeval tv; // make sure that server.fd is ready to read / write FD_ZERO(&fdr); FD_ZERO(&fdw); FD_SET(server.fd, &fdr); FD_SET(server.fd, &fdw); - res = select(64, &fdr, &fdw, NULL, NULL); + tv.tv_sec = 0; + tv.tv_usec = 0; + res = select(64, &fdr, &fdw, NULL, &tv); if (res == -1) { perror("select failed"); finish(EXIT_FAILURE); diff --git a/test/sockets/test_sockets_echo_server.c b/test/sockets/test_sockets_echo_server.c index a77c599e3c310..43377c88327f6 100644 --- a/test/sockets/test_sockets_echo_server.c +++ b/test/sockets/test_sockets_echo_server.c @@ -77,6 +77,7 @@ void main_loop() { int res; fd_set fdr; fd_set fdw; + struct timeval tv; // see if there are any connections to accept or read / write from FD_ZERO(&fdr); @@ -87,7 +88,9 @@ void main_loop() { if (client.fd) FD_SET(client.fd, &fdr); if (client.fd) FD_SET(client.fd, &fdw); #endif - res = select(64, &fdr, &fdw, NULL, NULL); + tv.tv_sec = 0; + tv.tv_usec = 0; + res = select(64, &fdr, &fdw, NULL, &tv); if (res == -1) { perror("select failed"); exit(EXIT_SUCCESS); From 018f3c3bae2fa2a832558a2a0e1821e387285341 Mon Sep 17 00:00:00 2001 From: Kohei Tokunaga Date: Wed, 8 Oct 2025 16:51:40 +0900 Subject: [PATCH 09/10] select: remove the word "eval." from the comment This is needed to avoid the following test failure. > AssertionError: Expected to NOT find 'eval. Signed-off-by: Kohei Tokunaga --- src/lib/libsyscall.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/libsyscall.js b/src/lib/libsyscall.js index 1a491130df20c..26409c2f55518 100644 --- a/src/lib/libsyscall.js +++ b/src/lib/libsyscall.js @@ -107,7 +107,7 @@ var SyscallsLibrary = { // select(2) is declared to accept "struct timeval { time_t tv_sec; suseconds_t tv_usec; }". // However, musl passes the two values to the syscall as an array of long values. // Note that sizeof(time_t) != sizeof(long) in wasm32. The former is 8, while the latter is 4. - // This means using "C_STRUCTS.timeval.tv_usec" leads to a wrong offset. + // This means using "C_STRUCTS.timeval\.tv_usec" leads to a wrong offset. // So, instead, we use POINTER_SIZE. var tv_sec = ({{{ makeGetValue('timeout', 0, 'i32') }}}), tv_usec = ({{{ makeGetValue('timeout', POINTER_SIZE, 'i32') }}}); From 05bd5fddf9066485138d286ad10e798313f59da5 Mon Sep 17 00:00:00 2001 From: Kohei Tokunaga Date: Thu, 9 Oct 2025 01:33:13 +0900 Subject: [PATCH 10/10] pthread: Enable the noleakcheck mark also for PROXY_TO_PTHREAD In addition to the existing OFFSCREENCANVAS_SUPPORT configuration, PROXY_TO_PTHREAD now also performs memory allocation internally for use by activeSelectCallbacks. Signed-off-by: Kohei Tokunaga --- src/lib/libpthread.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/libpthread.js b/src/lib/libpthread.js index a388eed5a3536..6a55ddf8aa1df 100644 --- a/src/lib/libpthread.js +++ b/src/lib/libpthread.js @@ -746,7 +746,7 @@ var LibraryPThread = { $pthreadCreateProxied__deps: ['__pthread_create_js'], $pthreadCreateProxied: (pthread_ptr, attr, startRoutine, arg) => ___pthread_create_js(pthread_ptr, attr, startRoutine, arg), -#if OFFSCREENCANVAS_SUPPORT +#if OFFSCREENCANVAS_SUPPORT || PROXY_TO_PTHREAD // ASan wraps the emscripten_builtin_pthread_create call in // __lsan::ScopedInterceptorDisabler. Unfortunately, that only disables it on // the thread that made the call. __pthread_create_js gets proxied to the