From 3183c8b96c72a982fa1955f2500b6eeb11f4fd70 Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Wed, 8 Apr 2026 17:41:13 -0700 Subject: [PATCH 1/6] wasip3: Finish nonblocking I/O implementation for TCP This commit fills out the final bits of nonblocking I/O for TCP sockets. This necessitates an implementation of nonblocking I/O for `stream` on both the reading and the writing side of things. Additionally things like TCP accepts now work, too. All tests are now passing on the WASIp3 target and the `FAILP3` directive is now removed since it's no longer necessary. The internals of the implementation here are pretty gnarly. This is due to the need to bridge the readiness-based-model of `poll` with the completion-based model of the component model. This implementation contains optimizations for 0-length reads/writes to use those to signal readiness if the host supports that, but this falls back to internally-buffered reads/writes if that is not supported. This should resolve all remaining TODOs for WASIp3 that I know of at least in wasi-libc, so after this it'd be switching to bug-finding mode and handling bug reports as projects are compiled with wasip3. --- .../cloudlibc/src/libc/poll/poll.c | 42 +- .../cloudlibc/src/libc/sys/select/pselect.c | 2 - .../headers/private/wasi/descriptor_table.h | 49 +- .../headers/private/wasi/file_utils.h | 13 + libc-bottom-half/headers/private/wasi/tcp.h | 14 +- .../headers/private/wasi/wasip3_block.h | 8 + libc-bottom-half/sources/file_utils.c | 514 +++++++++++++++++- libc-bottom-half/sources/tcp.c | 231 ++++++-- libc-bottom-half/sources/wasip3_block_on.c | 9 +- test/CMakeLists.txt | 28 +- 10 files changed, 801 insertions(+), 109 deletions(-) diff --git a/libc-bottom-half/cloudlibc/src/libc/poll/poll.c b/libc-bottom-half/cloudlibc/src/libc/poll/poll.c index 706ad8a36..1999398ea 100644 --- a/libc-bottom-half/cloudlibc/src/libc/poll/poll.c +++ b/libc-bottom-half/cloudlibc/src/libc/poll/poll.c @@ -372,20 +372,30 @@ static int poll_impl(struct pollfd *fds, size_t nfds, int timeout) { goto out; } - if (events & POLLRDNORM) { - // TODO(wasip3): use `get_read_stream` and use that I/O to do something. - // Requires a lot more integration with nonblocking I/O to get that - // working. - errno = EOPNOTSUPP; - goto out; + if (entry->vtable->get_read_stream) { + wasi_read_t read; + if (entry->vtable->get_read_stream(entry->data, &read) < 0) + goto out; + if (__wasilibc_read_poll(read.state, &state) < 0) + goto out; + } else { + errno = EOPNOTSUPP; + goto out; + } } if (events & POLLWRNORM) { - // TODO(wasip3): use `get_write_stream` to implement this (see - // `POLLRDNORM` above). - errno = EOPNOTSUPP; - goto out; + if (entry->vtable->get_write_stream) { + wasi_write_t write; + if (entry->vtable->get_write_stream(entry->data, &write) < 0) + goto out; + if (__wasilibc_write_poll(write.state, &state) < 0) + goto out; + } else { + errno = EOPNOTSUPP; + goto out; + } } } @@ -451,10 +461,14 @@ static int poll_impl(struct pollfd *fds, size_t nfds, int timeout) { if (p->waitable != event.waitable) continue; state.pollfd = p->pollfd; - // Clear the `waitable` since this event has fired and it doesn't need - // to be join'd to 0 below. Then invoke the custom callback originally - // added for this which will handle any necessary completion logic and - // updating `state.pollfd` with various events. + // Remove this waitable from the `waitable-set` as the `ready` + // operation might end up deleting the handle. Set the list here to 0 + // so it's not removed down below. + // + // Then invoke the custom callback originally added for this + // which will handle any necessary completion logic and updating + // `state.pollfd` with various events. + wasip3_waitable_join(p->waitable, 0); p->waitable = 0; p->ready(p->ready_data, &state, &event); } diff --git a/libc-bottom-half/cloudlibc/src/libc/sys/select/pselect.c b/libc-bottom-half/cloudlibc/src/libc/sys/select/pselect.c index 92777587a..82b810d5a 100644 --- a/libc-bottom-half/cloudlibc/src/libc/sys/select/pselect.c +++ b/libc-bottom-half/cloudlibc/src/libc/sys/select/pselect.c @@ -3,9 +3,7 @@ // SPDX-License-Identifier: BSD-2-Clause #include - #include - #include #include #include diff --git a/libc-bottom-half/headers/private/wasi/descriptor_table.h b/libc-bottom-half/headers/private/wasi/descriptor_table.h index 571ee4cfb..9667752ab 100644 --- a/libc-bottom-half/headers/private/wasi/descriptor_table.h +++ b/libc-bottom-half/headers/private/wasi/descriptor_table.h @@ -6,26 +6,49 @@ #ifndef __wasip1__ #include #include +#include +#include #include #include #ifdef __wasip3__ +/// The stream is complete and no further operations are allowed on it. +#define WASIP3_IO_DONE (1 << 0) +/// An I/O operation, be it a read or write, is in flight. +#define WASIP3_IO_INPROGRESS (1 << 1) +/// The in-flight I/O operation is a zero-length operation. This means that if +/// it's finished we expect that the next operation finishes immediately. +#define WASIP3_IO_ZERO_INPROGRESS (1 << 2) +/// A zero-sized in-flight I/O operation just finished so the next I/O op +/// should finish immediately. If it doesn't it'll turn on the next flag. +#define WASIP3_IO_SHOULD_BE_READY (1 << 3) +/// This stream isn't compatible with zero-length reads/writes signaling +/// readiness, so libc must buffer data internally for reads/writes. +#define WASIP3_IO_MUST_BUFFER (1 << 4) + /// Helper structure to package up state related to a wasip3 `stream`. /// /// This is used by various helpers to coordinate reading/writing/etc on a /// stream. This simultaneously represents both readers and writers. typedef struct wasip3_io_state_t { uint32_t stream; - bool done; + /// Bitset of `WASIP3_IO_*` flags. + uint32_t flags; + /// Malloc'd buffer used for reads/writes. NULL if not present. + uint8_t *buf; + /// Start of `buf` that has data in-flight or ready. + size_t buf_start; + /// End of `buf` that has data in-flight or ready. + size_t buf_end; } wasip3_io_state_t; /// Initializes `state` with the `stream` provided. static inline void wasip3_io_state_init(wasip3_io_state_t *state, uint32_t stream) { assert(stream != 0); + memset(state, 0, sizeof(*state)); state->stream = stream; - state->done = false; } /// Tests whether `state` has been initialized with a stream yet. @@ -37,22 +60,26 @@ static inline bool wasip3_io_state_present(wasip3_io_state_t *state) { /// /// Internally the stream must be a reader-half of a `stream`. static inline void wasip3_read_state_close(wasip3_io_state_t *state) { - if (state->stream != 0) { + if (state->flags & WASIP3_IO_INPROGRESS) + filesystem_stream_u8_cancel_read(state->stream); + if (state->buf) + free(state->buf); + if (state->stream != 0) filesystem_stream_u8_drop_readable(state->stream); - state->stream = 0; - } - state->done = false; + memset(state, 0, sizeof(*state)); } /// Closes out the streams/etc internal to `state`. /// /// Internally the stream must be a writer-half of a `stream`. static inline void wasip3_write_state_close(wasip3_io_state_t *state) { - if (state->stream != 0) { + if (state->flags & WASIP3_IO_INPROGRESS) + filesystem_stream_u8_cancel_write(state->stream); + if (state->buf) + free(state->buf); + if (state->stream != 0) filesystem_stream_u8_drop_writable(state->stream); - state->stream = 0; - } - state->done = false; + memset(state, 0, sizeof(*state)); } #endif @@ -195,6 +222,7 @@ typedef struct descriptor_vtable_t { /// and `get_write_stream`, if present, to handle `POLL{RD,WR}NORM` events. int (*poll_register)(void *, poll_state_t *state, short events); +#ifdef __wasip2__ /// Invoked when `poll` has already run and detected that this object was /// ready. The `events` provided are the same as those provided to /// `__wasilibc_poll_add` originally. This function should call @@ -204,6 +232,7 @@ typedef struct descriptor_vtable_t { /// If this function is not provided then `events` will automatically /// be placed into the `revents` field of `pollfd`. int (*poll_finish)(void *, poll_state_t *state, short events); +#endif // __wasip2__ } descriptor_vtable_t; /// A "fat pointer" which is placed inside of the descriptor table. diff --git a/libc-bottom-half/headers/private/wasi/file_utils.h b/libc-bottom-half/headers/private/wasi/file_utils.h index 171367f0e..5b12baf85 100644 --- a/libc-bottom-half/headers/private/wasi/file_utils.h +++ b/libc-bottom-half/headers/private/wasi/file_utils.h @@ -9,6 +9,7 @@ #include #include #include +#include #include #ifdef __wasip2__ @@ -119,6 +120,18 @@ dir_entry_type_to_d_type(filesystem_descriptor_type_t *ty) { } } +#ifndef __wasip2__ +/// Starts the process of `poll` for `iostate` assuming it's backed by a +/// readable stream. +/// +/// This will handle all the internal logic for zero-length reads/writes/etc +/// and add to `state` as necessary. +int __wasilibc_read_poll(wasip3_io_state_t *iostate, poll_state_t *state); + +/// Write dual of `__wasilibc_read_poll`. +int __wasilibc_write_poll(wasip3_io_state_t *iostate, poll_state_t *state); +#endif // !__wasip2__ + #endif // __wasip2__ || __wasip3__ #endif // __wasi_file_utils_h diff --git a/libc-bottom-half/headers/private/wasi/tcp.h b/libc-bottom-half/headers/private/wasi/tcp.h index 284511ff3..6460b29a6 100644 --- a/libc-bottom-half/headers/private/wasi/tcp.h +++ b/libc-bottom-half/headers/private/wasi/tcp.h @@ -35,13 +35,25 @@ typedef struct { wasip3_subtask_t subtask; #endif } tcp_socket_state_connecting_t; + +/// The `stream` in `tcp_socket_state_listening_t` is finished and should not +/// be read again. +#define TCP_LISTENING_DONE (1 << 0) +/// There is an active read on `stream` which has yet to complete. +#define TCP_LISTENING_ACCEPTING (1 << 1) +/// The `accept_result` field is valid and ready to be processed. +#define TCP_LISTENING_ACCEPT_READY (1 << 2) + typedef struct { #ifdef __wasip2__ int dummy; #else // The `stream` that this is reading to receive accepted sockets. sockets_stream_own_tcp_socket_t stream; - bool done; + /// In-flight result of the read of `stream`. + sockets_own_tcp_socket_t accept_result; + /// Bitset of `TCP_LISTENING_*`. + uint32_t flags; #endif } tcp_socket_state_listening_t; diff --git a/libc-bottom-half/headers/private/wasi/wasip3_block.h b/libc-bottom-half/headers/private/wasi/wasip3_block.h index ca70feaa4..9581420a9 100644 --- a/libc-bottom-half/headers/private/wasi/wasip3_block.h +++ b/libc-bottom-half/headers/private/wasi/wasip3_block.h @@ -9,6 +9,14 @@ typedef wasip3_waitable_status_t (*wasip3_cancel_t)(uint32_t); +/// Wait at most `timeout` duration for `waitable` to become ready. +/// +/// If `timeout` is 0 this will wait indefinitely. Returns `true` if `waitable` +/// has become ready. Returns `false` if `timeout` elapsed. Upon returning +/// `true` the `event` field is filled in. +bool __wasilibc_waitable_block_on(uint32_t waitable, wasip3_event_t *event, + monotonic_clock_duration_t timeout); + // Waits for a subtask to return void __wasilibc_subtask_block_on_and_drop(wasip3_subtask_t subtask); diff --git a/libc-bottom-half/sources/file_utils.c b/libc-bottom-half/sources/file_utils.c index 323538ab0..281724065 100644 --- a/libc-bottom-half/sources/file_utils.c +++ b/libc-bottom-half/sources/file_utils.c @@ -82,6 +82,196 @@ int wasi_string_from_c(const char *s, wasi_string_t *out) { return 0; } +#ifndef __wasip2__ +/// Update `state` with the result of `code` that happened. +static size_t wasip3_io_update_code(wasip3_io_state_t *state, + wasip3_waitable_status_t code) { + assert(code != WASIP3_WAITABLE_STATUS_BLOCKED); + switch (WASIP3_WAITABLE_STATE(code)) { + case WASIP3_WAITABLE_COMPLETED: + case WASIP3_WAITABLE_CANCELLED: + break; + case WASIP3_WAITABLE_DROPPED: + state->flags |= WASIP3_IO_DONE; + break; + default: + assert(0 && "unexpected waitable state"); + abort(); + } + + assert(state->flags & WASIP3_IO_INPROGRESS); + state->flags &= ~WASIP3_IO_INPROGRESS; + + if (state->flags & WASIP3_IO_ZERO_INPROGRESS) { + state->flags &= ~WASIP3_IO_ZERO_INPROGRESS; + + assert(!(state->flags & WASIP3_IO_SHOULD_BE_READY)); + state->flags |= WASIP3_IO_SHOULD_BE_READY; + } + + return WASIP3_WAITABLE_COUNT(code); +} + +/// Update `write` with the result of `event` that happened. +static size_t wasip3_io_update_event(wasip3_io_state_t *state, + wasip3_event_t *event) { + assert(event->event == WASIP3_EVENT_STREAM_WRITE || + event->event == WASIP3_EVENT_STREAM_READ); + assert(event->waitable == state->stream); + return wasip3_io_update_code(state, event->code); +} + +/// Attempts to resolve any pending write that may be in-progress on `write`. +/// +/// This may notably end up issuing more writes to finish a buffered write that +/// was previously flagged as completed. +static int wasip3_write_resolve_pending(wasi_write_t *write) { + wasip3_event_t event; + wasip3_io_state_t *state = write->state; + + // If there's nothing in-progress, then there's nothing to do, so bail out. + if (!(state->flags & WASIP3_IO_INPROGRESS)) + return 0; + assert(state->buf); + + // This loop ensures that the entirety of `write->buf` is written out. Libc + // already reported that the write succeeded so if a short write happens then + // it's got to get started back up again. + while (1) { + // For blocking I/O this awaits the previous result. For non-blocking I/O a + // `poll` operation is done. + if (write->blocking) { + if (!__wasilibc_waitable_block_on(state->stream, &event, + write->timeout)) { + errno = ETIMEDOUT; + return -1; + } + } else { + __wasilibc_poll_waitable(state->stream, &event); + if (event.event == WASIP3_EVENT_NONE) { + errno = EWOULDBLOCK; + return -1; + } + } + + // Update the I/O internal state given the result of the write. + state->buf_start += wasip3_io_update_event(state, &event); + + // While there's remaining writes to perform, kick those off here. If a + // write is blocked then nonblocking mode returns as such and blocking + // mode breaks out to turn this outer `while (1)` loop again to block + // on the result. If the write finishes immediately then state is updated + // again and then further continues. + while (!(state->flags & WASIP3_IO_DONE) && + state->buf_start != state->buf_end) { + wasip3_waitable_status_t status = filesystem_stream_u8_write( + state->stream, state->buf + state->buf_start, + state->buf_end - state->buf_start); + state->flags |= WASIP3_IO_INPROGRESS; + if (status == WASIP3_WAITABLE_STATUS_BLOCKED) { + if (write->blocking) { + break; + } else { + errno = EWOULDBLOCK; + return -1; + } + } + state->buf_start += wasip3_io_update_code(state, status); + } + + // We either broke out of the `while` loop normally, or we hit the `break` + // in the loop which wants to continue to the top of this outer loop. Test + // which it is here and act accordingly. + if ((state->flags & WASIP3_IO_DONE) || state->buf_start == state->buf_end) { + free(state->buf); + state->buf = NULL; + state->buf_start = 0; + state->buf_end = 0; + return 0; + } + } + + abort(); +} + +/// Helper of `read`/`write` starting below. +static bool wasip3_start_zero_length(wasip3_io_state_t *state, + wasip3_waitable_status_t status) { + assert(!(state->flags & WASIP3_IO_INPROGRESS)); + assert(!(state->flags & WASIP3_IO_ZERO_INPROGRESS)); + assert(!(state->flags & WASIP3_IO_SHOULD_BE_READY)); + assert(!(state->flags & WASIP3_IO_DONE)); + state->flags |= WASIP3_IO_INPROGRESS; + state->flags |= WASIP3_IO_ZERO_INPROGRESS; + if (status == WASIP3_WAITABLE_STATUS_BLOCKED) + return true; + wasip3_io_update_code(state, status); + return false; +} + +/// Starts a zero-length write on the stream pointed to by `write`. +/// +/// This requires that there's no active I/O on the stream at this time and +/// that it's not a closed stream. +static bool wasip3_write_start_zero_length(wasip3_io_state_t *state) { + return wasip3_start_zero_length( + state, filesystem_stream_u8_write(state->stream, NULL, 0)); +} + +/// Starts a zero-length read on the stream pointed to by `read`. +/// +/// This requires that there's no active I/O on the stream at this time and +/// that it's not a closed stream. +static bool wasip3_read_start_zero_length(wasip3_io_state_t *state) { + return wasip3_start_zero_length( + state, filesystem_stream_u8_read(state->stream, NULL, 0)); +} + +/// Starts a zero-length read on the stream pointed to by `read`. +/// +/// This requires that there's no active I/O on the stream at this time and +/// that it's not a closed stream. +static int wasip3_read_start_nonzero(wasip3_io_state_t *state, size_t length) { + assert(state->flags & WASIP3_IO_MUST_BUFFER); + assert(!(state->flags & WASIP3_IO_SHOULD_BE_READY)); + assert(!(state->flags & WASIP3_IO_INPROGRESS)); + assert(!(state->flags & WASIP3_IO_ZERO_INPROGRESS)); + assert(!(state->flags & WASIP3_IO_DONE)); + assert(state->buf == NULL); + state->buf = malloc(length); + if (!state->buf) { + errno = ENOMEM; + return -1; + } + state->buf_start = 0; + state->buf_end = 0; + wasip3_waitable_status_t status = + filesystem_stream_u8_read(state->stream, state->buf, length); + state->flags |= WASIP3_IO_INPROGRESS; + if (status == WASIP3_WAITABLE_STATUS_BLOCKED) { + errno = EWOULDBLOCK; + return -1; + } + + return 0; +} + +static size_t wasip3_read_complete_internally(wasip3_io_state_t *state, + void *buffer, size_t length) { + size_t buf_len = state->buf_end - state->buf_start; + size_t amount = buf_len < length ? buf_len : length; + memcpy(buffer, state->buf + state->buf_start, amount); + state->buf_start += amount; + if (state->buf_start == state->buf_end) { + free(state->buf); + state->buf = NULL; + state->buf_start = 0; + state->buf_end = 0; + } + return amount; +} +#endif + ssize_t __wasilibc_write(wasi_write_t *write, const void *buffer, size_t length) { #if defined(__wasip2__) @@ -159,22 +349,132 @@ ssize_t __wasilibc_write(wasi_write_t *write, const void *buffer, } } #elif defined(__wasip3__) - assert(write->blocking); // TODO(wasip3) + wasip3_io_state_t *state = write->state; - // Perform writes until either a non-zero-length write completes or the stream - // is closed. - while (!write->state->done) { + // First resolve any pending I/O, should it exist. + if (wasip3_write_resolve_pending(write) < 0) + return -1; + assert((state->flags & WASIP3_IO_INPROGRESS) == 0); + assert((state->flags & WASIP3_IO_ZERO_INPROGRESS) == 0); + + // If this stream is complete, then delegate to EOF. + if (state->flags & WASIP3_IO_DONE) + return write->eof(write->eof_data); + + // For simplicity, handle blocking writes directly here. This involves + // blocking with an optional timeout and handling the result. + if (write->blocking) { + state->flags &= ~WASIP3_IO_SHOULD_BE_READY; + bool done = false; ssize_t amount = __wasilibc_stream_block_on_timeout( - filesystem_stream_u8_write(write->state->stream, buffer, length), - write->state->stream, &write->state->done, write->timeout, + filesystem_stream_u8_write(state->stream, buffer, length), + state->stream, &done, write->timeout, filesystem_stream_u8_cancel_write); - if (amount > 0 || length == 0) { - if (write->offset) - *write->offset += amount; - return amount; + if (done) + state->flags |= WASIP3_IO_DONE; + if (amount < 0) + return -1; + if (write->offset) + *write->offset += amount; + if (amount == 0 && done) + return write->eof(write->eof_data); + return amount; + } + + // ... and everything below is about implementing nonblocking writes ... + + assert(!write->blocking); + + while (1) { + bool should_be_ready = state->flags & WASIP3_IO_SHOULD_BE_READY; + state->flags &= ~WASIP3_IO_SHOULD_BE_READY; + + // If this stream isn't forced to buffer then do an opportunistic write + // with the user-provided buffer. If this doesn't work, but it should work, + // then it means we must buffer from now on. No matter what though if the + // write is in-progress then it's cancelled here as the user's input buffer + // won't persist beyond this function call. Throughout writing/cancellation + // if anything actually succeeded then that's immediately returned here. + if (!(state->flags & WASIP3_IO_MUST_BUFFER)) { + wasip3_waitable_status_t status = + filesystem_stream_u8_write(state->stream, buffer, length); + state->flags |= WASIP3_IO_INPROGRESS; + if (status == WASIP3_WAITABLE_STATUS_BLOCKED) { + if (should_be_ready) + state->flags |= WASIP3_IO_MUST_BUFFER; + status = filesystem_stream_u8_cancel_write(state->stream); + } + size_t amount = wasip3_io_update_code(state, status); + if (amount > 0) { + if (write->offset) + *write->offset += amount; + return amount; + } + if (state->flags & WASIP3_IO_DONE) + return write->eof(write->eof_data); } + + // If, at this point, we're still not forced to buffer then it means that a + // write was attempted above, cancelled, and nothing was transferred. In + // that case perform a zero-length write as a readiness test for this + // stream. + if (!(state->flags & WASIP3_IO_MUST_BUFFER)) { + if (wasip3_write_start_zero_length(state)) { + errno = EWOULDBLOCK; + return -1; + } + if (state->flags & WASIP3_IO_DONE) + return write->eof(write->eof_data); + + // The zero-length write succeeded, so turn the loop again to retry the + // write of the user-supplied buffer. Note that the I/O "should be ready" + // flag will be set here which will poison the loop and avoid coming here + // again if zero-length reads/writes don't actually work. + assert(state->flags & WASIP3_IO_SHOULD_BE_READY); + continue; + } + + break; + } + + // At this point we're in nonblocking mode, we're required to buffer data, + // and most other flags should all be turned off. Here the input `bufffer` is + // copied into the internal `state` and the I/O operation is issued. + assert(state->flags & WASIP3_IO_MUST_BUFFER); + assert(!(state->flags & WASIP3_IO_SHOULD_BE_READY)); + assert(!(state->flags & WASIP3_IO_INPROGRESS)); + assert(!(state->flags & WASIP3_IO_ZERO_INPROGRESS)); + assert(!(state->flags & WASIP3_IO_DONE)); + assert(state->buf == NULL); + state->buf = malloc(length); + if (!state->buf) { + errno = ENOMEM; + return -1; + } + memcpy(state->buf, buffer, length); + state->buf_start = 0; + state->buf_end = length; + wasip3_waitable_status_t status = + filesystem_stream_u8_write(state->stream, state->buf, length); + state->flags |= WASIP3_IO_INPROGRESS; + + // If the I/O is blocked, then that's ok. The data is all owned by `state` + // meaning that we've effectively just faked a write of `length` bytes. Here + // it's reported as having written everything. + if (status == WASIP3_WAITABLE_STATUS_BLOCKED) { + if (write->offset) + *write->offset += length; + return length; } - return write->eof(write->eof_data); + + // If that write actually succeeded, however, then deal with it here. That + // means we have to deallocate the internal buffer and then handle the result + // of the write. + free(state->buf); + state->buf = NULL; + state->buf_start = 0; + state->buf_end = 0; + return wasip3_io_update_code(state, status); #else #error "Unknown WASI version" #endif @@ -245,23 +545,195 @@ ssize_t __wasilibc_read(wasi_read_t *read, void *buffer, size_t length) { } } #elif defined(__wasip3__) - assert(read->blocking); // TODO(wasip3) + wasip3_io_state_t *state = read->state; + wasip3_event_t event; - // Attempt reads until a nonzero-length read is encountered or the stream is - // closed. - while (!read->state->done) { + // If there's active I/O in progress for this stream then this must wait for + // it to complete. + if (state->flags & WASIP3_IO_INPROGRESS) { + if (read->blocking) { + if (!__wasilibc_waitable_block_on(state->stream, &event, read->timeout)) { + errno = ETIMEDOUT; + return -1; + } + } else { + __wasilibc_poll_waitable(state->stream, &event); + if (event.event == WASIP3_EVENT_NONE) { + errno = EWOULDBLOCK; + return -1; + } + } + // Upon completion that's the size of the read buffer which we're now + // reading into. + state->buf_end = wasip3_io_update_event(state, &event); + } + + assert(!(state->flags & WASIP3_IO_INPROGRESS)); + + // If this stream has internally buffered data that's ready to read then copy + // that out here. + if (state->buf) + return wasip3_read_complete_internally(state, buffer, length); + + // If this stream has finished, then delegate to EOF. + if (state->flags & WASIP3_IO_DONE) + return read->eof(read->eof_data); + + // For simplicity handle the blocking read case here. + if (read->blocking) { + bool done = false; size_t amount = __wasilibc_stream_block_on_timeout( filesystem_stream_u8_read(read->state->stream, buffer, length), - read->state->stream, &read->state->done, read->timeout, + read->state->stream, &done, read->timeout, filesystem_stream_u8_cancel_read); - if (amount > 0 || length == 0) { - if (read->offset) - *read->offset += amount; - return amount; + if (done) + state->flags |= WASIP3_IO_DONE; + if (amount < 0) + return -1; + if (read->offset) + *read->offset += amount; + if (amount == 0 && done) + return read->eof(read->eof_data); + return amount; + } + + // ... and everything below is about implementing nonblocking reads ... + + // NB: this loop structure is intentionally quite similar to `write`'s + // structure above. See there for more comments. + while (1) { + bool should_be_ready = state->flags & WASIP3_IO_SHOULD_BE_READY; + state->flags &= ~WASIP3_IO_SHOULD_BE_READY; + + if (!(state->flags & WASIP3_IO_MUST_BUFFER)) { + wasip3_waitable_status_t status = + filesystem_stream_u8_read(state->stream, buffer, length); + state->flags |= WASIP3_IO_INPROGRESS; + if (status == WASIP3_WAITABLE_STATUS_BLOCKED) { + if (should_be_ready) + state->flags |= WASIP3_IO_MUST_BUFFER; + status = filesystem_stream_u8_cancel_read(state->stream); + } + size_t amount = wasip3_io_update_code(state, status); + if (amount > 0) { + if (read->offset) + *read->offset += amount; + return amount; + } + if (state->flags & WASIP3_IO_DONE) + return read->eof(read->eof_data); + } + + if (!(state->flags & WASIP3_IO_MUST_BUFFER)) { + if (wasip3_read_start_zero_length(state)) { + errno = EWOULDBLOCK; + return -1; + } + if (state->flags & WASIP3_IO_DONE) + return read->eof(read->eof_data); + + assert(state->flags & WASIP3_IO_SHOULD_BE_READY); + continue; } + + break; } - return read->eof(read->eof_data); + + // Like with writes above this is the point where we're forced to do internal + // buffering of reads. Issue the read here referencing internal data and + // return that we're blocked. + if (wasip3_read_start_nonzero(state, length) < 0) + return -1; + return wasip3_read_complete_internally(state, buffer, length); #else #error "Unknown WASI version" #endif } + +#ifndef __wasip2__ +static void wasip3_poll_read_ready(void *data, poll_state_t *state, + wasip3_event_t *event) { + wasip3_io_state_t *iostate = (wasip3_io_state_t *)data; + iostate->buf_end = wasip3_io_update_event(iostate, event); + __wasilibc_poll_ready(state, POLLRDNORM); +} + +static void wasip3_poll_write_ready(void *data, poll_state_t *state, + wasip3_event_t *event) { + wasip3_io_state_t *iostate = (wasip3_io_state_t *)data; + iostate->buf_start += wasip3_io_update_event(iostate, event); + __wasilibc_poll_ready(state, POLLWRNORM); +} + +static int wasip3_stream_poll(wasip3_io_state_t *iostate, poll_state_t *state, + short events, poll_ready_t ready) { + // If the I/O stream is finished it'll never block so it's always ready. + if (iostate->flags & WASIP3_IO_DONE) { + __wasilibc_poll_ready(state, events); + return 0; + } + + // If there's I/O in progress, then that's what we're interested in so add it + // to the set. + if (iostate->flags & WASIP3_IO_INPROGRESS) + return __wasilibc_poll_add(state, iostate->stream, ready, iostate); + + // For readable streams if there's buffered data then this is immediately + // ready since that data can be read without blocking. + if (events == POLLRDNORM && iostate->buf) { + __wasilibc_poll_ready(state, events); + return 0; + } + + if (iostate->flags & WASIP3_IO_MUST_BUFFER) { + // For writable streams, if buffering is required and zero-length writes + // don't work then this is immediately ready since those writes never block + // (they're buffered internally). + if (events == POLLWRNORM) { + __wasilibc_poll_ready(state, events); + return 0; + } + + // For readable streams if buffering is required then a read is kicked off + // here. An arbitrary read size is chosen for now. If that read isn't ready + // then the waitable is added, and otherwise it's flagged as immediately + // ready. + if (events == POLLRDNORM) { + if (wasip3_read_start_nonzero(iostate, 16 * 1024) < 0) { + if (errno == EWOULDBLOCK) + return __wasilibc_poll_add(state, iostate->stream, ready, iostate); + return -1; + } + __wasilibc_poll_ready(state, events); + return 0; + } + } + + // If I/O should be ready then that means a zero-length op has already + // completed, so this should be good to go. + if (iostate->flags & WASIP3_IO_SHOULD_BE_READY) { + __wasilibc_poll_ready(state, events); + return 0; + } + + // Kick off a zero-length write here. If that actually gets kicked off then + // we're waiting on it so add it to the set. Otherwise it completed so we're + // immediately ready. + bool started_work = events == POLLWRNORM + ? wasip3_write_start_zero_length(iostate) + : wasip3_read_start_zero_length(iostate); + if (started_work) + return __wasilibc_poll_add(state, iostate->stream, ready, iostate); + __wasilibc_poll_ready(state, events); + return 0; +} + +int __wasilibc_read_poll(wasip3_io_state_t *iostate, poll_state_t *state) { + return wasip3_stream_poll(iostate, state, POLLRDNORM, wasip3_poll_read_ready); +} + +int __wasilibc_write_poll(wasip3_io_state_t *iostate, poll_state_t *state) { + return wasip3_stream_poll(iostate, state, POLLWRNORM, + wasip3_poll_write_ready); +} +#endif // !__wasip2__ diff --git a/libc-bottom-half/sources/tcp.c b/libc-bottom-half/sources/tcp.c index f872e5ff4..259aaee47 100644 --- a/libc-bottom-half/sources/tcp.c +++ b/libc-bottom-half/sources/tcp.c @@ -95,6 +95,11 @@ int __wasilibc_add_tcp_socket(sockets_own_tcp_socket_t socket, return tcp_add(socket, family, blocking, NULL); } +#ifdef __wasip3__ +static void wasip3_tcp_accept_finish(tcp_socket_state_listening_t *state, + wasip3_waitable_status_t status); +#endif + static void tcp_free(void *data) { tcp_socket_t *tcp = (tcp_socket_t *)data; @@ -111,6 +116,11 @@ static void tcp_free(void *data) { case TCP_SOCKET_STATE_LISTENING: { tcp_socket_state_listening_t *state = &tcp->state.listening; + if (state->flags & TCP_LISTENING_ACCEPTING) + wasip3_tcp_accept_finish( + state, sockets_stream_own_tcp_socket_cancel_read(state->stream)); + if (state->flags & TCP_LISTENING_ACCEPT_READY) + sockets_tcp_socket_drop_own(state->accept_result); if (state->stream != 0) sockets_stream_own_tcp_socket_drop_readable(state->stream); break; @@ -290,6 +300,58 @@ static void tcp_setup_connected_state_wasip3(tcp_socket_t *socket) { } #endif +#ifndef __wasip2__ +static void wasip3_tcp_accept_finish(tcp_socket_state_listening_t *state, + wasip3_waitable_status_t status) { + assert(state->flags & TCP_LISTENING_ACCEPTING); + assert(!(state->flags & TCP_LISTENING_DONE)); + assert(!(state->flags & TCP_LISTENING_ACCEPT_READY)); + state->flags &= ~TCP_LISTENING_ACCEPTING; + + switch (WASIP3_WAITABLE_STATE(status)) { + case WASIP3_WAITABLE_DROPPED: + state->flags |= TCP_LISTENING_DONE; + break; + case WASIP3_WAITABLE_COMPLETED: + case WASIP3_WAITABLE_CANCELLED: + break; + default: + abort(); + } + if (WASIP3_WAITABLE_COUNT(status) > 0) + state->flags |= TCP_LISTENING_ACCEPT_READY; +} + +static void wasip3_tcp_accept_finish_event(tcp_socket_state_listening_t *state, + wasip3_event_t *event) { + assert(event->event == WASIP3_EVENT_STREAM_READ); + assert(event->waitable == state->stream); + wasip3_tcp_accept_finish(state, event->code); +} + +/// Kicks off any necessary work to accept a TCP socket. +/// +/// Returns `true` if this actually performed a stream read, and `false` +/// otherwise. Note that if `false` is returned there may still be an active +/// read or a pending value. +static bool wasip3_tcp_accept_start(tcp_socket_state_listening_t *state) { + // Kick off an accept while we're (a) not done, (b) there's not already an + // active accept, and (c) a previous accept hasn't finished. + while (!(state->flags & TCP_LISTENING_DONE) && + !(state->flags & TCP_LISTENING_ACCEPTING) && + !(state->flags & TCP_LISTENING_ACCEPT_READY)) { + wasip3_waitable_status_t status = sockets_stream_own_tcp_socket_read( + state->stream, &state->accept_result, 1); + if (status == WASIP3_WAITABLE_STATUS_BLOCKED) { + state->flags |= TCP_LISTENING_ACCEPTING; + return true; + } + wasip3_tcp_accept_finish(state, status); + } + return false; +} +#endif // !__wasip2__ + static int tcp_accept4(void *data, struct sockaddr *addr, socklen_t *addrlen, int flags) { tcp_socket_t *socket = (tcp_socket_t *)data; @@ -335,27 +397,53 @@ static int tcp_accept4(void *data, struct sockaddr *addr, socklen_t *addrlen, client_socket->state.connected.output = output; #else tcp_socket_state_listening_t *state = &socket->state.listening; - assert(socket->blocking); - sockets_own_tcp_socket_t result; - while (1) { + + // Turn this loop until a socket is fully accepted and ready to get + // processed. + while (!(state->flags & TCP_LISTENING_ACCEPT_READY)) { + bool started_work = wasip3_tcp_accept_start(state); + + // If the accept immediately completed we're good to go. + if (state->flags & TCP_LISTENING_ACCEPT_READY) + break; + // It's not clear what the correct error code to return here is, if any, // since in theory sockets being accepted are infinite until the socket is // closed. Handle this with at least some error for now. - if (state->done) { + if (state->flags & TCP_LISTENING_DONE) { errno = ENOTSUP; return -1; } - // Wait on a socket to be accepted, and then break out of the loop. Turn - // again on 0-length reads. - size_t amt = __wasilibc_stream_block_on( - sockets_stream_own_tcp_socket_read(state->stream, &result, 1), - state->stream, &state->done); - if (amt > 0) - break; + assert(state->flags & TCP_LISTENING_ACCEPTING); + + // Either block waiting for this to complete in blocking mode or poll to + // see what happened in non-blocking mode. As a minor optimization if + // an accept was kicked off above and it didn't finish then don't re-poll + // here and just bail out immediately. + wasip3_event_t event; + if (socket->blocking) { + __wasilibc_waitable_block_on(state->stream, &event, 0); + } else { + if (!started_work) + __wasilibc_poll_waitable(state->stream, &event); + if (started_work || event.event == WASIP3_EVENT_NONE) { + errno = EWOULDBLOCK; + return -1; + } + } + + // Update our own internal state with the result of the accept, and then + // turn the loop again. + wasip3_tcp_accept_finish_event(state, &event); } - int client_fd = tcp_add(result, socket->family, (flags & SOCK_NONBLOCK) == 0, - &client_socket); + + assert(!(state->flags & TCP_LISTENING_ACCEPTING)); + + int client_fd = tcp_add(state->accept_result, socket->family, + (flags & SOCK_NONBLOCK) == 0, &client_socket); + state->accept_result.__handle = 0; + state->flags &= ~TCP_LISTENING_ACCEPT_READY; if (client_fd < 0) return -1; tcp_setup_connected_state_wasip3(client_socket); @@ -423,6 +511,24 @@ static int tcp_bind(void *data, const struct sockaddr *addr, return tcp_do_bind(socket, &local_address); } +#ifndef __wasip2__ +static void tcp_connect_finish(tcp_socket_t *socket) { + assert(socket->state.tag == TCP_SOCKET_STATE_CONNECTING); + tcp_socket_state_connecting_t *conn = &socket->state.connecting; + + // The connect subtask has completed at this point, so check to see what the + // result was. + assert(conn->subtask == 0); + if (conn->result.is_err) { + sockets_error_code_t error = conn->result.val.err; + socket->state.tag = TCP_SOCKET_STATE_CONNECT_FAILED; + socket->state.connect_failed.error_code = error; + } else { + tcp_setup_connected_state_wasip3(socket); + } +} +#endif // !__wasip2__ + static int tcp_connect(void *data, const struct sockaddr *addr, socklen_t addrlen) { tcp_socket_t *socket = (tcp_socket_t *)data; @@ -493,6 +599,7 @@ static int tcp_connect(void *data, const struct sockaddr *addr, socket->state.connected.input = input; socket->state.connected.output = output; #else + socket->state.tag = TCP_SOCKET_STATE_CONNECTING; // Setup the arguments to the `connect` function as well as initializing the // `subtask` field of the `connecting` state to zero as we're not sure we'll // get a subtask just yet. @@ -521,18 +628,12 @@ static int tcp_connect(void *data, const struct sockaddr *addr, // The connect subtask has completed at this point, so check to see what the // result was. - assert(socket->state.connecting.subtask == 0); - if (socket->state.connecting.result.is_err) { - sockets_error_code_t error = socket->state.connecting.result.val.err; - __wasilibc_socket_error_to_errno(&error); - socket->state.tag = TCP_SOCKET_STATE_CONNECT_FAILED; - socket->state.connect_failed.error_code = error; + tcp_connect_finish(socket); + + if (socket->state.tag == TCP_SOCKET_STATE_CONNECT_FAILED) { + __wasilibc_socket_error_to_errno(&socket->state.connect_failed.error_code); return -1; } - - // Our socket is now connected, so establish the send/receive streams that - // will be used for data transmission. - tcp_setup_connected_state_wasip3(socket); #endif return 0; @@ -685,8 +786,8 @@ static int tcp_listen(void *data, int backlog) { return __wasilibc_socket_error_to_errno(&error); socket->state.tag = TCP_SOCKET_STATE_LISTENING; + memset(&socket->state.listening, 0, sizeof(socket->state.listening)); socket->state.listening.stream = stream; - socket->state.listening.done = false; #endif return 0; @@ -807,36 +908,94 @@ static int tcp_shutdown(void *data, int posix_how) { return 0; } -#ifdef __wasip2__ +#ifndef __wasip2__ +static void tcp_connect_ready(void *data, poll_state_t *state, + wasip3_event_t *event) { + tcp_socket_t *socket = (tcp_socket_t *)data; + assert(socket->state.tag == TCP_SOCKET_STATE_CONNECTING); + tcp_socket_state_connecting_t *conn = &socket->state.connecting; + + assert(event->event == WASIP3_EVENT_SUBTASK); + assert(event->waitable == conn->subtask); + assert(event->code == WASIP3_SUBTASK_RETURNED); + wasip3_subtask_drop(conn->subtask); + conn->subtask = 0; + + tcp_connect_finish(socket); + short events = POLLWRNORM; + if (socket->state.tag == TCP_SOCKET_STATE_CONNECT_FAILED) + events |= POLLRDNORM; + __wasilibc_poll_ready(state, events); +} + +static void tcp_accept_ready(void *data, poll_state_t *state, + wasip3_event_t *event) { + tcp_socket_t *socket = (tcp_socket_t *)data; + assert(socket->state.tag == TCP_SOCKET_STATE_LISTENING); + tcp_socket_state_listening_t *listen = &socket->state.listening; + wasip3_tcp_accept_finish_event(listen, event); + __wasilibc_poll_ready(state, POLLRDNORM); +} +#endif // !__wasip2__ + static int tcp_poll_register(void *data, poll_state_t *state, short events) { tcp_socket_t *socket = (tcp_socket_t *)data; switch (socket->state.tag) { case TCP_SOCKET_STATE_CONNECTING: { - if ((events & (POLLRDNORM | POLLWRNORM)) != 0) + if ((events & (POLLRDNORM | POLLWRNORM)) != 0) { +#ifdef __wasip2__ return __wasilibc_poll_add(state, events, tcp_pollable(socket)); +#else + tcp_socket_state_connecting_t *conn = &socket->state.connecting; + return __wasilibc_poll_add(state, conn->subtask, tcp_connect_ready, data); +#endif + } break; } case TCP_SOCKET_STATE_LISTENING: { // Listening sockets can only be ready to read, not write. - if ((events & POLLRDNORM) != 0) + if ((events & POLLRDNORM) != 0) { +#ifdef __wasip2__ return __wasilibc_poll_add(state, events, tcp_pollable(socket)); +#else + tcp_socket_state_listening_t *listen = &socket->state.listening; + // Kick off an accept if it's not already going, then see what happened. + wasip3_tcp_accept_start(listen); + if (listen->flags & (TCP_LISTENING_ACCEPT_READY | TCP_LISTENING_DONE)) { + __wasilibc_poll_ready(state, POLLRDNORM); + return 0; + } + assert(listen->flags & TCP_LISTENING_ACCEPTING); + return __wasilibc_poll_add(state, listen->stream, tcp_accept_ready, data); +#endif + } break; } case TCP_SOCKET_STATE_CONNECTED: { + tcp_socket_state_connected_t *conn = &socket->state.connected; if ((events & POLLRDNORM) != 0) { +#ifdef __wasip2__ if (__wasilibc_poll_add_input_stream( - state, streams_borrow_input_stream(socket->state.connected.input), - &socket->state.connected.input_pollable) < 0) + state, streams_borrow_input_stream(conn->input), + &conn->input_pollable) < 0) return -1; +#else + if (__wasilibc_read_poll(&conn->receive, state) < 0) + return -1; +#endif } if ((events & POLLWRNORM) != 0) { +#ifdef __wasip2__ if (__wasilibc_poll_add_output_stream( - state, - streams_borrow_output_stream(socket->state.connected.output), - &socket->state.connected.output_pollable) < 0) + state, streams_borrow_output_stream(conn->utput), + &conn->output_pollable) < 0) + return -1; +#else + if (__wasilibc_write_poll(&conn->send, state) < 0) return -1; +#endif } break; } @@ -853,6 +1012,7 @@ static int tcp_poll_register(void *data, poll_state_t *state, short events) { return 0; } +#ifdef __wasip2__ static int tcp_poll_finish(void *data, poll_state_t *state, short events) { tcp_socket_t *socket = (tcp_socket_t *)data; @@ -889,7 +1049,7 @@ static int tcp_poll_finish(void *data, poll_state_t *state, short events) { } return 0; } -#endif // __wasip2__ +#endif static int tcp_fcntl_getfl(void *data) { tcp_socket_t *socket = (tcp_socket_t *)data; @@ -1308,11 +1468,10 @@ static descriptor_vtable_t tcp_vtable = { .shutdown = tcp_shutdown, .getsockopt = tcp_getsockopt, .setsockopt = tcp_setsockopt, - -#ifdef __wasip2__ .poll_register = tcp_poll_register, +#ifdef __wasip2__ .poll_finish = tcp_poll_finish, -#endif // __wasip2__ +#endif .fcntl_getfl = tcp_fcntl_getfl, .fcntl_setfl = tcp_fcntl_setfl, diff --git a/libc-bottom-half/sources/wasip3_block_on.c b/libc-bottom-half/sources/wasip3_block_on.c index 03df0a516..345854b36 100644 --- a/libc-bottom-half/sources/wasip3_block_on.c +++ b/libc-bottom-half/sources/wasip3_block_on.c @@ -6,13 +6,8 @@ #include #include -/// Waits for `waitable` and fills in `event.` -/// -/// A `timeout` can optionally be specified as well. If the `timeout` is 0 then -/// this function will block indefinitely for the `waitable`. -static bool __wasilibc_waitable_block_on(uint32_t waitable, - wasip3_event_t *event, - monotonic_clock_duration_t timeout) { +bool __wasilibc_waitable_block_on(uint32_t waitable, wasip3_event_t *event, + monotonic_clock_duration_t timeout) { // If a timeout is requested then create a subtask using the monotonic-clock // interface which'll get added to the waitable-set below. If the timeout has // immediately elapsed then go ahead and poll the waitable set to see if the diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 8f16cddd5..c6bc3d0f9 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -107,7 +107,7 @@ function(add_test_executable executable_name src) target_link_libraries(${executable_name} PRIVATE c) else() clang_format_target(${executable_name}) - target_link_libraries(${executable_name} INTERFACE c-static) + target_link_libraries(${executable_name} PRIVATE c-static) endif() foreach(flag IN LISTS arg_CFLAGS) target_compile_options(${executable_name} PRIVATE ${flag}) @@ -129,10 +129,9 @@ endfunction() # * `ENV a=b b=c` - set env vars for when executing this test # * `NETWORK` - this test uses the network and sockets. # * `PASS_REGULAR_EXPRESSION` - a regex that must match the test output to pass -# * `FAILP3` - this test fails on wasip3 targets # * `SETJMP` - this test requires setjmp/longjmp function(register_test test_name executable_name) - set(options FS NETWORK FAILP3 SETJMP) + set(options FS NETWORK SETJMP) set(oneValueArgs CLIENT PASS_REGULAR_EXPRESSION) set(multiValueArgs ARGV ENV) cmake_parse_arguments(PARSE_ARGV 1 arg "${options}" "${oneValueArgs}" "${multiValueArgs}") @@ -192,9 +191,6 @@ function(register_test test_name executable_name) endif() set_tests_properties(${test_name} PROPERTIES TIMEOUT 10) - if (arg_FAILP3 AND (WASI STREQUAL "p3")) - set_tests_properties(${test_name} PROPERTIES WILL_FAIL TRUE) - endif() add_dependencies(${test_name} engine) endfunction() @@ -341,8 +337,8 @@ add_wasilibc_test(memchr.c LDFLAGS -Wl,--stack-first -Wl,--initial-memory=327680 add_wasilibc_test(memcmp.c LDFLAGS -Wl,--stack-first -Wl,--initial-memory=327680) add_wasilibc_test(opendir.c FS ARGV /) add_wasilibc_test(open_relative_path.c FS ARGV /) -add_wasilibc_test(poll.c FS FAILP3) -add_wasilibc_test(pselect.c FS FAILP3) +add_wasilibc_test(poll.c FS) +add_wasilibc_test(pselect.c FS) add_wasilibc_test(preadvwritev.c FS) add_wasilibc_test(preadwrite.c FS) add_wasilibc_test(readlink.c FS) @@ -408,16 +404,16 @@ endif() # ========= sockets-related tests =============================== if (NOT (WASI STREQUAL "p1")) - add_wasilibc_test(poll-connect.c NETWORK FAILP3) - add_wasilibc_test(poll-nonblocking-socket.c NETWORK FAILP3) + add_wasilibc_test(poll-connect.c NETWORK) + add_wasilibc_test(poll-nonblocking-socket.c NETWORK) add_wasilibc_test(setsockopt.c NETWORK) add_wasilibc_test(sockets-nonblocking-udp.c NETWORK) - add_wasilibc_test(sockets-nonblocking-multiple.c NETWORK FAILP3) + add_wasilibc_test(sockets-nonblocking-multiple.c NETWORK) add_wasilibc_test(sockets-nonblocking-udp-multiple.c NETWORK) add_wasilibc_test(sockets-fcntl.c NETWORK) add_wasilibc_test(sockets-udp-connected.c NETWORK) add_wasilibc_test(getaddrinfo.c NETWORK) - add_wasilibc_test(sockets-nonblocking.c NETWORK FAILP3) + add_wasilibc_test(sockets-nonblocking.c NETWORK) add_wasilibc_test(sockets-nonblocking-udp-no-connection.c NETWORK) # Define executables for server/client tests, and they're paired together in @@ -446,7 +442,7 @@ if (NOT (WASI STREQUAL "p1")) add_sockets_test_executable(sockets-multiple-server.c) function(sockets_test test_name client server) - set(options FAILP3) + set(options) set(oneValueArgs NCLIENTS) set(multiValueArgs) cmake_parse_arguments(PARSE_ARGV 1 arg "${options}" "${oneValueArgs}" "${multiValueArgs}") @@ -461,17 +457,13 @@ if (NOT (WASI STREQUAL "p1")) -DNCLIENTS=${arg_NCLIENTS} -P ${CMAKE_CURRENT_SOURCE_DIR}/socket-test.cmake ) - - if (arg_FAILP3 AND (WASI STREQUAL "p3")) - set_tests_properties(${test_name} PROPERTIES WILL_FAIL TRUE) - endif() endfunction() sockets_test(sockets sockets-client.wasm sockets-server.wasm) sockets_test(sockets-udp-blocking sockets-client-udp-blocking.wasm sockets-server-udp-blocking.wasm) sockets_test(sockets-multiple sockets-multiple-client.wasm sockets-multiple-server.wasm - NCLIENTS 10 FAILP3) + NCLIENTS 10) # Various forms of client hangups sockets_test(sockets-client-hangup-after-connect From 5a4ff9ce9edeb616c1a7741ef016e49170576437 Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Wed, 8 Apr 2026 18:13:40 -0700 Subject: [PATCH 2/6] Fix wasip2 compile --- libc-bottom-half/sources/tcp.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libc-bottom-half/sources/tcp.c b/libc-bottom-half/sources/tcp.c index 259aaee47..ab4e94ace 100644 --- a/libc-bottom-half/sources/tcp.c +++ b/libc-bottom-half/sources/tcp.c @@ -989,7 +989,7 @@ static int tcp_poll_register(void *data, poll_state_t *state, short events) { if ((events & POLLWRNORM) != 0) { #ifdef __wasip2__ if (__wasilibc_poll_add_output_stream( - state, streams_borrow_output_stream(conn->utput), + state, streams_borrow_output_stream(conn->output), &conn->output_pollable) < 0) return -1; #else From 85dc729dae95c24f0425100e6a0a60d73484b1b7 Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Wed, 8 Apr 2026 18:14:43 -0700 Subject: [PATCH 3/6] Update symbol listing for wasip3 --- expected/wasm32-wasip3/defined-symbols.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/expected/wasm32-wasip3/defined-symbols.txt b/expected/wasm32-wasip3/defined-symbols.txt index 5841422af..71f64fde5 100644 --- a/expected/wasm32-wasip3/defined-symbols.txt +++ b/expected/wasm32-wasip3/defined-symbols.txt @@ -336,6 +336,7 @@ __wasilibc_populate_preopens __wasilibc_pthread_self __wasilibc_random __wasilibc_read +__wasilibc_read_poll __wasilibc_rename_newat __wasilibc_rename_oldat __wasilibc_reset_preopens @@ -350,9 +351,11 @@ __wasilibc_tell __wasilibc_unlinkat __wasilibc_unspecified_addr __wasilibc_utimens +__wasilibc_waitable_block_on __wasilibc_wasi_family_to_libc __wasilibc_wasi_to_sockaddr __wasilibc_write +__wasilibc_write_poll __wasm_call_dtors __wcscoll_l __wcsftime_l From 5216a45a33973b13a6e6b828813c50475450cd75 Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Wed, 8 Apr 2026 18:19:39 -0700 Subject: [PATCH 4/6] Fix release build warning --- libc-bottom-half/sources/tcp.c | 1 + 1 file changed, 1 insertion(+) diff --git a/libc-bottom-half/sources/tcp.c b/libc-bottom-half/sources/tcp.c index ab4e94ace..16e8bd1ed 100644 --- a/libc-bottom-half/sources/tcp.c +++ b/libc-bottom-half/sources/tcp.c @@ -915,6 +915,7 @@ static void tcp_connect_ready(void *data, poll_state_t *state, assert(socket->state.tag == TCP_SOCKET_STATE_CONNECTING); tcp_socket_state_connecting_t *conn = &socket->state.connecting; + (void)event; assert(event->event == WASIP3_EVENT_SUBTASK); assert(event->waitable == conn->subtask); assert(event->code == WASIP3_SUBTASK_RETURNED); From e1d38d9577c3a0ba3e96bcbe517dc3b7717bc5ec Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Thu, 9 Apr 2026 15:05:21 -0700 Subject: [PATCH 5/6] Adjust comments --- libc-bottom-half/sources/file_utils.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/libc-bottom-half/sources/file_utils.c b/libc-bottom-half/sources/file_utils.c index 281724065..84a94387f 100644 --- a/libc-bottom-half/sources/file_utils.c +++ b/libc-bottom-half/sources/file_utils.c @@ -390,7 +390,9 @@ ssize_t __wasilibc_write(wasi_write_t *write, const void *buffer, state->flags &= ~WASIP3_IO_SHOULD_BE_READY; // If this stream isn't forced to buffer then do an opportunistic write - // with the user-provided buffer. If this doesn't work, but it should work, + // with the user-provided buffer. If this doesn't work (it was blocked + // instead of completing), but it should work (because a previous + // zero-length operation succeeded which can sometimes mean it's erady), // then it means we must buffer from now on. No matter what though if the // write is in-progress then it's cancelled here as the user's input buffer // won't persist beyond this function call. Throughout writing/cancellation From 592a5fb49304ccaffcef962351918a6f598f5403 Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Thu, 9 Apr 2026 18:09:01 -0500 Subject: [PATCH 6/6] Update libc-bottom-half/sources/file_utils.c Co-authored-by: Joel Dice --- libc-bottom-half/sources/file_utils.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libc-bottom-half/sources/file_utils.c b/libc-bottom-half/sources/file_utils.c index 84a94387f..ef2bd74b7 100644 --- a/libc-bottom-half/sources/file_utils.c +++ b/libc-bottom-half/sources/file_utils.c @@ -392,7 +392,7 @@ ssize_t __wasilibc_write(wasi_write_t *write, const void *buffer, // If this stream isn't forced to buffer then do an opportunistic write // with the user-provided buffer. If this doesn't work (it was blocked // instead of completing), but it should work (because a previous - // zero-length operation succeeded which can sometimes mean it's erady), + // zero-length operation succeeded which can sometimes mean it's ready), // then it means we must buffer from now on. No matter what though if the // write is in-progress then it's cancelled here as the user's input buffer // won't persist beyond this function call. Throughout writing/cancellation