Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions system/lib/libc/emscripten_syscall_stubs.c
Original file line number Diff line number Diff line change
Expand Up @@ -262,8 +262,6 @@ weak int __syscall_setsockopt(int sockfd, int level, int optname, intptr_t optva

UNIMPLEMENTED(acct, (intptr_t filename))
UNIMPLEMENTED(mincore, (intptr_t addr, size_t length, intptr_t vec))
UNIMPLEMENTED(pselect6, (int nfds, intptr_t readfds, intptr_t writefds, intptr_t exceptfds, intptr_t timeout, intptr_t sigmaks))
UNIMPLEMENTED(ppoll, (intptr_t fds, int nfds, intptr_t timeout, intptr_t sigmask, int size))
UNIMPLEMENTED(recvmmsg, (int sockfd, intptr_t msgvec, size_t vlen, int flags, ...))
UNIMPLEMENTED(sendmmsg, (int sockfd, intptr_t msgvec, size_t vlen, int flags, ...))
UNIMPLEMENTED(shutdown, (int sockfd, int how, int dummy, int dummy2, int dummy3, int dummy4))
Expand Down
2 changes: 0 additions & 2 deletions system/lib/libc/musl/arch/emscripten/bits/syscall.h
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,6 @@
#define SYS_readlinkat __syscall_readlinkat
#define SYS_fchmodat2 __syscall_fchmodat2
#define SYS_faccessat __syscall_faccessat
#define SYS_pselect6 __syscall_pselect6
#define SYS_ppoll __syscall_ppoll
#define SYS_utimensat __syscall_utimensat
#define SYS_fallocate __syscall_fallocate
#define SYS_dup3 __syscall_dup3
Expand Down
2 changes: 0 additions & 2 deletions system/lib/libc/musl/arch/emscripten/syscall_arch.h
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,6 @@ int __syscall_symlinkat(intptr_t target, int newdirfd, intptr_t linkpath);
int __syscall_readlinkat(int dirfd, intptr_t path, intptr_t buf, size_t bufsize);
int __syscall_fchmodat2(int dirfd, intptr_t path, int mode, int flags);
int __syscall_faccessat(int dirfd, intptr_t path, int amode, int flags);
int __syscall_pselect6(int nfds, intptr_t readfds, intptr_t writefds, intptr_t exceptfds, intptr_t timeout, intptr_t sigmask);
int __syscall_ppoll(intptr_t fds, int nfds, intptr_t timeout, intptr_t sigmask, int size);
int __syscall_utimensat(int dirfd, intptr_t path, intptr_t times, int flags);
int __syscall_fallocate(int fd, int mode, off_t offset, off_t len);
int __syscall_dup3(int fd, int suggestfd, int flags);
Expand Down
11 changes: 11 additions & 0 deletions system/lib/libc/musl/src/select/ppoll.c
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,16 @@

int ppoll(struct pollfd *fds, nfds_t n, const struct timespec *to, const sigset_t *mask)
{
#ifdef __EMSCRIPTEN__
// Emscripten does not support true async signals so we just implement ppoll
// in terms of poll here in userspace.
int timeout = (to == NULL) ? -1 : (to->tv_sec * 1000 + to->tv_nsec / 1000000);
sigset_t origmask;
pthread_sigmask(SIG_SETMASK, mask, &origmask);
int rtn = poll(fds, n, timeout);
pthread_sigmask(SIG_SETMASK, &origmask, NULL);
return rtn;
#else
time_t s = to ? to->tv_sec : 0;
long ns = to ? to->tv_nsec : 0;
#ifdef SYS_ppoll_time64
Expand All @@ -23,4 +33,5 @@ int ppoll(struct pollfd *fds, nfds_t n, const struct timespec *to, const sigset_
#endif
return syscall_cp(SYS_ppoll, fds, n,
to ? ((long[]){s, ns}) : 0, mask, _NSIG/8);
#endif
}
15 changes: 15 additions & 0 deletions system/lib/libc/musl/src/select/pselect.c
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,20 @@

int pselect(int n, fd_set *restrict rfds, fd_set *restrict wfds, fd_set *restrict efds, const struct timespec *restrict ts, const sigset_t *restrict mask)
{
#ifdef __EMSCRIPTEN__
// Emscripten does not support true async signals so we just implement pselect
// in terms of select here in userspace.
struct timeval tv_timeout;
if (ts) {
tv_timeout.tv_sec = ts->tv_sec;
tv_timeout.tv_usec = ts->tv_nsec / 1000;
}
sigset_t origmask;
pthread_sigmask(SIG_SETMASK, mask, &origmask);
int rtn = select(n, rfds, wfds, efds, ts ? &tv_timeout : NULL);
pthread_sigmask(SIG_SETMASK, &origmask, NULL);
return rtn;
#else
syscall_arg_t data[2] = { (uintptr_t)mask, _NSIG/8 };
time_t s = ts ? ts->tv_sec : 0;
long ns = ts ? ts->tv_nsec : 0;
Expand All @@ -23,4 +37,5 @@ int pselect(int n, fd_set *restrict rfds, fd_set *restrict wfds, fd_set *restric
#endif
return syscall_cp(SYS_pselect6, n, rfds, wfds, efds,
ts ? ((long[]){s, ns}) : 0, data);
#endif
}
6 changes: 2 additions & 4 deletions test/codesize/test_codesize_hello_dylink_all.json
Original file line number Diff line number Diff line change
Expand Up @@ -1889,9 +1889,7 @@
"__syscall_munlockall",
"__syscall_munmap",
"__syscall_pause",
"__syscall_ppoll",
"__syscall_prlimit64",
"__syscall_pselect6",
"__syscall_recvmmsg",
"__syscall_sendmmsg",
"__syscall_setdomainname",
Expand Down Expand Up @@ -3751,13 +3749,13 @@
"$__syscall_msync",
"$__syscall_munmap",
"$__syscall_pause",
"$__syscall_ppoll",
"$__syscall_prlimit64",
"$__syscall_pselect6",
"$__syscall_recvmmsg",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you perhaps know why this symbol (and $__syscall_shutdown below) is included now? I noticed a similar thing in PR #19559, but haven't investigated this further.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm.. I actually have no idea. Asking gemini if it can figure that out now..

Copy link
Collaborator Author

@sbc100 sbc100 Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wow, AI figured it out pretty quick:

"""
The investigation reveals that the appearance of __syscall_recvmmsg and __syscall_shutdown in the funcs list of test_codesize_hello_dylink_all.json is a side effect of Identical Code Folding (ICF) in the linker, combined with the removal of ppoll and pselect6 from the UNIMPLEMENTED syscall stubs.
"""

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Key Findings:

  1. ICF Merging: In system/lib/libc/emscripten_syscall_stubs.c, many syscalls are implemented using the UNIMPLEMENTED macro. When NDEBUG is
    defined (as it is in this test), these functions all have an identical body (return -ENOSYS;). The linker (Wasm-LD) merges functions with
    identical bodies and the same signatures into a single WASM function to save space.
  2. Canonical Names: When multiple functions are merged, the linker chooses one name to be the internal name of the function in the WASM.
    This is usually the alphabetically first name among the merged symbols.
  3. The Change:
    • Group 1 (5 arguments): Previously contained __syscall_ppoll, __syscall_recvmmsg, and __syscall_sendmmsg. Since ppoll was
      alphabetically first, it was chosen as the canonical name ($__syscall_ppoll).
    • Group 2 (6 arguments): Previously contained __syscall_pselect6, __syscall_shutdown, and __syscall_socketpair. Since pselect6 was
      alphabetically first, it was chosen as the canonical name ($__syscall_pselect6).
  4. Effect of the Commit: The commit removed ppoll and pselect6 from the UNIMPLEMENTED list in emscripten_syscall_stubs.c (as they now have
    real implementations).
    • In the 5-arg group, __syscall_recvmmsg is now the alphabetically first member, so it became the new canonical name.
    • In the 6-arg group, __syscall_shutdown is now the alphabetically first member, so it became the new canonical name. Key Findings:
  5. ICF Merging: In system/lib/libc/emscripten_syscall_stubs.c, many syscalls are implemented using the UNIMPLEMENTED macro. When NDEBUG is
    defined (as it is in this test), these functions all have an identical body (return -ENOSYS;). The linker (Wasm-LD) merges functions with
    identical bodies and the same signatures into a single WASM function to save space.
  6. Canonical Names: When multiple functions are merged, the linker chooses one name to be the internal name of the function in the WASM.
    This is usually the alphabetically first name among the merged symbols.
  7. The Change:
    • Group 1 (5 arguments): Previously contained __syscall_ppoll, __syscall_recvmmsg, and __syscall_sendmmsg. Since ppoll was
      alphabetically first, it was chosen as the canonical name ($__syscall_ppoll).
    • Group 2 (6 arguments): Previously contained __syscall_pselect6, __syscall_shutdown, and __syscall_socketpair. Since pselect6 was
      alphabetically first, it was chosen as the canonical name ($__syscall_pselect6).
  8. Effect of the Commit: The commit removed ppoll and pselect6 from the UNIMPLEMENTED list in emscripten_syscall_stubs.c (as they now have
    real implementations).
    • In the 5-arg group, __syscall_recvmmsg is now the alphabetically first member, so it became the new canonical name.
    • In the 6-arg group, __syscall_shutdown is now the alphabetically first member, so it became the new canonical name.
  9. Validation: The total number of functions in the funcs list remained exactly the same (1689 entries). Only the names changed because the
    previous "representatives" of these merged groups were removed from the codebase.

The symbols were already present as exports before the change; they only "appeared" as new internal function names because they were promoted
to being the representatives for their respective merged groups.

  1. Validation: The total number of functions in the funcs list remained exactly the same (1689 entries). Only the names changed because the
    previous "representatives" of these merged groups were removed from the codebase.

The symbols were already present as exports before the change; they only "appeared" as new internal function names because they were promoted
to being the representatives for their respective merged groups.

"$__syscall_setdomainname",
"$__syscall_setpgid",
"$__syscall_setpriority",
"$__syscall_setsockopt",
"$__syscall_shutdown",
"$__syscall_sync",
"$__syscall_umask",
"$__syscall_uname",
Expand Down
221 changes: 221 additions & 0 deletions test/core/test_ppoll_blocking.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
/*
* Copyright 2025 The Emscripten Authors. All rights reserved.
* Emscripten is available under two separate licenses, the MIT license and the
* University of Illinois/NCSA Open Source License. Both these licenses can be
* found in the LICENSE file.
*/

// Duplicate of test_poll_blocking.c using ppoll() instead of poll()

#include <poll.h>
#include <time.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <string.h>
#include <sys/time.h>

#define TIMEOUT_MS 300
#define TIMEOUT_NS (TIMEOUT_MS * 1000000)

// It is possible for the node timers (such as setTimeout or Atomics.wait) to wake up
// slightly earlier than requested. Because we measure times accurately using
// clock_gettime, we give tests a 5 milliseconds error margin to avoid flaky timeouts.
#define TIMEOUT_MARGIN_MS 5

void sleep_ms(int ms) {
usleep(ms * 1000);
}

int64_t timespec_delta_ms(struct timespec* begin, struct timespec* end) {
int64_t delta_sec = end->tv_sec - begin->tv_sec;
int64_t delta_nsec = end->tv_nsec - begin->tv_nsec;

assert(delta_sec >= 0);
assert(delta_nsec > -1000000000 && delta_nsec < 1000000000);

int64_t delta_ms = (delta_sec * 1000) + (delta_nsec / 1000000);
assert(delta_ms >= 0);
return delta_ms;
}

// Check if timeout works without fds
void test_timeout_without_fds() {
printf("test_timeout_without_fds\n");
struct timespec begin, end;

clock_gettime(CLOCK_MONOTONIC, &begin);
struct timespec timeout;
timeout.tv_sec = 0;
timeout.tv_nsec = TIMEOUT_NS;
assert(ppoll(NULL, 0, &timeout, NULL) == 0);
clock_gettime(CLOCK_MONOTONIC, &end);

int64_t duration = timespec_delta_ms(&begin, &end);
printf(" -> duration: %lld ms\n", duration);
assert(duration >= TIMEOUT_MS - TIMEOUT_MARGIN_MS);
}

// Check if timeout works with fds without events
void test_timeout_with_fds_without_events() {
printf("test_timeout_with_fds_without_events\n");
struct timespec begin, end;
int pipe_a[2];

assert(pipe(pipe_a) == 0);

clock_gettime(CLOCK_MONOTONIC, &begin);
struct pollfd fds = {pipe_a[0], 0, 0};
struct timespec timeout;
timeout.tv_sec = 0;
timeout.tv_nsec = TIMEOUT_NS;
assert(ppoll(&fds, 1, &timeout, NULL) == 0);
clock_gettime(CLOCK_MONOTONIC, &end);

int64_t duration = timespec_delta_ms(&begin, &end);
printf(" -> duration: %lld ms\n", duration);
assert(duration >= TIMEOUT_MS - TIMEOUT_MARGIN_MS);

close(pipe_a[0]); close(pipe_a[1]);
}

int pipe_shared[2];

void *write_after_sleep(void * arg) {
const char *t = "test\n";

sleep_ms(TIMEOUT_MS);
write(pipe_shared[1], t, strlen(t));

return NULL;
}

// Check if ppoll can unblock on an event
void test_unblock_ppoll() {
printf("test_unblock_ppoll\n");
struct timespec begin, end;
pthread_t tid;
int pipe_a[2];

assert(pipe(pipe_a) == 0);
assert(pipe(pipe_shared) == 0);

struct pollfd fds[2] = {
{pipe_a[0], POLLIN, 0},
{pipe_shared[0], POLLIN, 0},
};
clock_gettime(CLOCK_MONOTONIC, &begin);
assert(pthread_create(&tid, NULL, write_after_sleep, NULL) == 0);
assert(ppoll(fds, 2, NULL, NULL) == 1);
clock_gettime(CLOCK_MONOTONIC, &end);
assert(fds[1].revents & POLLIN);

int64_t duration = timespec_delta_ms(&begin, &end);
printf(" -> duration: %lld ms\n", duration);
assert(duration >= TIMEOUT_MS - TIMEOUT_MARGIN_MS);

pthread_join(tid, NULL);

close(pipe_a[0]); close(pipe_a[1]);
close(pipe_shared[0]); close(pipe_shared[1]);
}

int threads_running = 0;
pthread_mutex_t running_lock = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t running_cv = PTHREAD_COND_INITIALIZER;

void *do_ppoll_in_thread(void * arg) {
struct timespec begin, end;

clock_gettime(CLOCK_MONOTONIC, &begin);
struct pollfd fds = {pipe_shared[0], POLLIN, 0};
pthread_mutex_lock(&running_lock);
threads_running++;
pthread_cond_signal(&running_cv);
pthread_mutex_unlock(&running_lock);
struct timespec timeout;
timeout.tv_sec = 4;
timeout.tv_nsec = 0;
assert(ppoll(&fds, 1, &timeout, NULL) == 1);
clock_gettime(CLOCK_MONOTONIC, &end);
assert(fds.revents & POLLIN);

int64_t duration = timespec_delta_ms(&begin, &end);
printf(" -> duration: %lld ms\n", duration);
assert((duration >= TIMEOUT_MS - TIMEOUT_MARGIN_MS) && (duration < 4000));

return NULL;
}

// Check if ppoll works in threads
void test_ppoll_in_threads() {
printf("test_ppoll_in_threads\n");
pthread_t tid1, tid2;
const char *t = "test\n";

assert(pipe(pipe_shared) == 0);

assert(pthread_create(&tid1, NULL, do_ppoll_in_thread, NULL) == 0);
assert(pthread_create(&tid2, NULL, do_ppoll_in_thread, NULL) == 0);
pthread_mutex_lock(&running_lock);
while (threads_running != 2) {
pthread_cond_wait(&running_cv, &running_lock);
}
pthread_mutex_unlock(&running_lock);

sleep_ms(2 * TIMEOUT_MS);
write(pipe_shared[1], t, strlen(t));

pthread_join(tid1, NULL);
pthread_join(tid2, NULL);

close(pipe_shared[0]); close(pipe_shared[1]);
}

// Check if ppoll works with ready fds
void test_ready_fds() {
printf("test_ready_fds\n");
struct timespec zero_timeout;
zero_timeout.tv_sec = 0;
zero_timeout.tv_nsec = 0;
fd_set readfds;
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));

struct pollfd fds[2] = {
{pipe_c[0], POLLIN, 0},
{pipe_d[0], POLLIN, 0},
};

assert(ppoll(fds, 2, &zero_timeout, NULL) == 2);
assert(fds[0].revents & POLLIN);
assert(fds[1].revents & POLLIN);

fds[0].revents = 0;
fds[1].revents = 0;

assert(ppoll(fds, 2, &zero_timeout, NULL) == 2);
assert(fds[0].revents & POLLIN);
assert(fds[1].revents & POLLIN);

close(pipe_c[0]); close(pipe_c[1]);
close(pipe_d[0]); close(pipe_d[1]);
}

int main() {
test_ppoll_in_threads();
test_timeout_without_fds();
test_timeout_with_fds_without_events();
test_unblock_ppoll();
test_ready_fds();
printf("done\n");
return 0;
}
Loading
Loading