Skip to content

Commit d65bee2

Browse files
authored
wasip3: Implement nonblocking UDP I/O (#775)
This commit implements nonblocking I/O for UDP sockets, specifically with `send` and `recv`. This additionally fills out enough of `poll` to get various tests doing nonblocking I/O to work. The main intention of this commit is to do the first foray into implementing nonblocking I/O in wasi-libc for wasip3. Broadly-speaking nonblocking I/O is in two categories: one being UDP and the other being `stream<u8>`-based reads/writes. This implements only the UDP half, and defers `stream<u8>` (and thus TCP for example) to a future commit. The implementation is relatively gnarly given the wasip3 APIs we currently have, but this was expected. I've done my best to be as careful as I can in terms of managing state machines but I'm pretty likely to inevitably get something wrong, so this should be expected to be something that's iterated on.
1 parent 0db888c commit d65bee2

File tree

11 files changed

+673
-152
lines changed

11 files changed

+673
-152
lines changed

expected/wasm32-wasip3/defined-symbols.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,9 @@ __wasilibc_nocwd_symlinkat
329329
__wasilibc_nocwd_utimensat
330330
__wasilibc_open_nomode
331331
__wasilibc_parse_port
332+
__wasilibc_poll_add
333+
__wasilibc_poll_ready
334+
__wasilibc_poll_waitable
332335
__wasilibc_populate_preopens
333336
__wasilibc_pthread_self
334337
__wasilibc_random

libc-bottom-half/cloudlibc/src/libc/poll/poll.c

Lines changed: 225 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -16,26 +16,6 @@ static int poll_impl(struct pollfd *fds, size_t nfds, int timeout) {
1616
__wasi_subscription_t subscriptions[maxevents];
1717
size_t nsubscriptions = 0;
1818

19-
// POLLPRI (exceptional/out-of-band data) is not supported in WASI.
20-
// If all requested events across all fds are POLLPRI, return ENOSYS.
21-
// Otherwise, remove POLLPRI so it never fires (same as no OOB data).
22-
{
23-
bool has_pri_only = false;
24-
bool has_non_pri = false;
25-
for (size_t i = 0; i < nfds; ++i) {
26-
if (fds[i].fd < 0 || fds[i].events == 0)
27-
continue;
28-
if (fds[i].events & ~POLLPRI)
29-
has_non_pri = true;
30-
else
31-
has_pri_only = true;
32-
}
33-
if (has_pri_only && !has_non_pri) {
34-
errno = ENOSYS;
35-
return -1;
36-
}
37-
}
38-
3919
for (size_t i = 0; i < nfds; ++i) {
4020
struct pollfd *pollfd = &fds[i];
4121
if (pollfd->fd < 0)
@@ -105,12 +85,6 @@ static int poll_impl(struct pollfd *fds, size_t nfds, int timeout) {
10585
return -1;
10686
}
10787

108-
// Clear revents fields.
109-
for (size_t i = 0; i < nfds; ++i) {
110-
struct pollfd *pollfd = &fds[i];
111-
pollfd->revents = 0;
112-
}
113-
11488
// Set revents fields.
11589
for (size_t i = 0; i < nevents; ++i) {
11690
const __wasi_event_t *event = &events[i];
@@ -196,29 +170,6 @@ void __wasilibc_poll_ready(poll_state_t *state, short events) {
196170
}
197171

198172
static int poll_impl(struct pollfd *fds, size_t nfds, int timeout) {
199-
for (size_t i = 0; i < nfds; ++i) {
200-
fds[i].revents = 0;
201-
}
202-
203-
// POLLPRI (exceptional/out-of-band data) is not supported in WASI.
204-
// If all requested events across all fds are exclusively POLLPRI,
205-
// return ENOSYS. Otherwise, strip POLLPRI and proceed.
206-
{
207-
bool has_pri_only = false;
208-
bool has_non_pri = false;
209-
for (size_t i = 0; i < nfds; ++i) {
210-
if (fds[i].fd < 0 || fds[i].events == 0)
211-
continue;
212-
if (fds[i].events & ~POLLPRI)
213-
has_non_pri = true;
214-
else
215-
has_pri_only = true;
216-
}
217-
if (has_pri_only && !has_non_pri) {
218-
errno = ENOSYS;
219-
return -1;
220-
}
221-
}
222173

223174
size_t max_pollables = (2 * nfds) + 1;
224175
state_t states[max_pollables];
@@ -330,18 +281,237 @@ static int poll_impl(struct pollfd *fds, size_t nfds, int timeout) {
330281

331282
#elif defined(__wasip3__)
332283

284+
typedef struct {
285+
struct pollfd *pollfd;
286+
uint32_t waitable;
287+
// Callback/data pointer to invoke when `waitable` receives an event, and that
288+
// runs any completion logic and figures out what to do next.
289+
poll_ready_t ready;
290+
void *ready_data;
291+
} state_t;
292+
293+
struct poll_state_t {
294+
state_t *states;
295+
size_t len;
296+
size_t cap;
297+
298+
wasip3_waitable_set_t set;
299+
300+
int event_count;
301+
struct pollfd *pollfd;
302+
};
303+
304+
int __wasilibc_poll_add(poll_state_t *state, uint32_t waitable,
305+
poll_ready_t ready, void *ready_data) {
306+
if (state->len >= state->cap) {
307+
errno = ENOMEM;
308+
return -1;
309+
}
310+
state->states[state->len].pollfd = state->pollfd;
311+
state->states[state->len].waitable = waitable;
312+
state->states[state->len].ready = ready;
313+
state->states[state->len].ready_data = ready_data;
314+
wasip3_waitable_join(waitable, state->set);
315+
state->len += 1;
316+
return 0;
317+
}
318+
319+
void __wasilibc_poll_ready(poll_state_t *state, short events) {
320+
if (events != 0) {
321+
if (state->pollfd->revents == 0) {
322+
++state->event_count;
323+
}
324+
state->pollfd->revents |= events;
325+
}
326+
}
327+
333328
static int poll_impl(struct pollfd *fds, size_t nfds, int timeout) {
334-
(void) fds;
335-
(void) nfds;
336-
(void) timeout;
337-
errno = ENOTSUP;
338-
return -1;
329+
// TODO(wasip3) this calculation of 2n+1 is a coarse approximation but not
330+
// necessarily accurate. This worked well for wasip2 but this should be
331+
// revisited for wasip3. Maybe make this a dynamically allocated array?
332+
size_t max_pollables = (2 * nfds) + 1;
333+
state_t states[max_pollables];
334+
335+
poll_state_t state;
336+
state.set = wasip3_waitable_set_new();
337+
state.states = states;
338+
state.len = 0;
339+
state.cap = max_pollables;
340+
state.event_count = 0;
341+
state.pollfd = fds;
342+
343+
int ret = -1;
344+
wasip3_subtask_t timeout_subtask = 0;
345+
346+
for (size_t i = 0; i < nfds; ++i) {
347+
struct pollfd *pollfd = fds + i;
348+
if (pollfd->fd < 0)
349+
continue;
350+
state.pollfd = pollfd;
351+
descriptor_table_entry_t *entry = descriptor_table_get_ref(pollfd->fd);
352+
if (!entry) {
353+
errno = EBADF;
354+
goto out;
355+
}
356+
357+
// If this descriptor has a custom registration function then
358+
// use that exclusively.
359+
if (entry->vtable->poll_register) {
360+
if (entry->vtable->poll_register(entry->data, &state, pollfd->events) < 0)
361+
goto out;
362+
continue;
363+
}
364+
365+
// Strip POLLPRI, it is never reported in WASI (no OOB data).
366+
short events = pollfd->events & ~POLLPRI;
367+
368+
// Without a custom registration handle read/write readiness
369+
// below, but everything else is unsupported.
370+
if (events & ~(POLLRDNORM | POLLWRNORM)) {
371+
errno = EOPNOTSUPP;
372+
goto out;
373+
}
374+
375+
376+
if (events & POLLRDNORM) {
377+
// TODO(wasip3): use `get_read_stream` and use that I/O to do something.
378+
// Requires a lot more integration with nonblocking I/O to get that
379+
// working.
380+
errno = EOPNOTSUPP;
381+
goto out;
382+
}
383+
384+
if (events & POLLWRNORM) {
385+
// TODO(wasip3): use `get_write_stream` to implement this (see
386+
// `POLLRDNORM` above).
387+
errno = EOPNOTSUPP;
388+
goto out;
389+
}
390+
}
391+
392+
// If something ended up being ready during registration then zero out the
393+
// timeout and continue proceeding as if this is a non-blocking call.
394+
if (state.event_count > 0)
395+
timeout = 0;
396+
397+
// If a timeout is specified then spawn a subtask using
398+
// `monotonic-clock#wait-for`. This subtask is then added to the
399+
// waitable-set. If the timeout completes immediately then pretend like
400+
// there's no timeout and keep going.
401+
//
402+
// Note that in `out`, the exit of this function, the subtask is cleaned up
403+
// if it's still in-progress.
404+
if (timeout > 0) {
405+
wasip3_subtask_status_t status = monotonic_clock_wait_for(timeout * 1000000);
406+
if (WASIP3_SUBTASK_STATE(status) == WASIP3_SUBTASK_RETURNED) {
407+
timeout = 0;
408+
} else {
409+
timeout_subtask = WASIP3_SUBTASK_HANDLE(status);
410+
wasip3_waitable_join(timeout_subtask, state.set);
411+
}
412+
}
413+
414+
// Perform the actual blocking operation, if applicable. If the timeout is
415+
// still nonzero at this point then it means that it's time to actually
416+
// block. That handles the case where the timeout is negative as no subtask
417+
// was spawned and this will wait indefinitely. It also handles the case of a
418+
// timeout where a subtask was spawned and it's in the set being waited on.
419+
//
420+
// If the timeout is 0, however, then just perform a poll-style operation to
421+
// see what happened.
422+
wasip3_event_t event;
423+
if (timeout != 0) {
424+
wasip3_waitable_set_wait(state.set, &event);
425+
} else {
426+
wasip3_waitable_set_poll(state.set, &event);
427+
}
428+
while (event.event != WASIP3_EVENT_NONE) {
429+
// If this event is for the timeout subtask then that's handled directly here
430+
// as it's not related to any fds. Upon receiving the terminal status of
431+
// the subtask it's no longer candidate for cancellation so it's immediately
432+
// dropped and zero'd out to avoid processing in the `out` label below.
433+
if (event.waitable == timeout_subtask) {
434+
assert(event.event == WASIP3_EVENT_SUBTASK);
435+
assert(event.code == WASIP3_SUBTASK_RETURNED);
436+
wasip3_subtask_drop(timeout_subtask);
437+
timeout_subtask = 0;
438+
} else {
439+
// If this event is not for the timeout subtask then it's something that
440+
// was added to the waitable-set. That's processed here directly. Search
441+
// through the list of `state_t` waitables, find the right one, and invoke
442+
// its `ready` callback. The `ready` callback will dictate what to do
443+
// with `event` and will call `__wasilibc_poll_ready` as appropriate.
444+
//
445+
// TODO: having to search through the list isn't great, but short of
446+
// having some sort of hash table or map going from waitable->state_t
447+
// that's the best that can be done right. At least `poll` as a POSIX
448+
// interface is expected to take linear time, right?
449+
for (size_t i = 0; i < state.len; i++) {
450+
state_t *p = &state.states[i];
451+
if (p->waitable != event.waitable)
452+
continue;
453+
state.pollfd = p->pollfd;
454+
// Clear the `waitable` since this event has fired and it doesn't need
455+
// to be join'd to 0 below. Then invoke the custom callback originally
456+
// added for this which will handle any necessary completion logic and
457+
// updating `state.pollfd` with various events.
458+
p->waitable = 0;
459+
p->ready(p->ready_data, &state, &event);
460+
}
461+
}
462+
463+
// After this event has finished being processed try to get another event.
464+
// This will drain the waitable-set of all ready events in one invocation of
465+
// this function and avoid unncessary setup/teardown in `poll`, in theory.
466+
wasip3_waitable_set_poll(state.set, &event);
467+
}
468+
469+
ret = state.event_count;
470+
471+
out:
472+
if (timeout_subtask != 0) {
473+
wasip3_subtask_cancel(timeout_subtask);
474+
wasip3_subtask_drop(timeout_subtask);
475+
}
476+
for (size_t i = 0; i < state.len; i++) {
477+
uint32_t waitable = state.states[i].waitable;
478+
if (waitable)
479+
wasip3_waitable_join(waitable, 0);
480+
}
481+
wasip3_waitable_set_drop(state.set);
482+
483+
return ret;
339484
}
340485

341486
#else
342487
# error "Unknown WASI version"
343488
#endif
344489

490+
// POLLPRI (exceptional/out-of-band data) is not supported in WASI.
491+
// If all requested events across all fds are exclusively POLLPRI,
492+
// return ENOSYS. Otherwise, strip POLLPRI and proceed.
493+
static int validate_something_not_pollpri(struct pollfd *fds, size_t nfds) {
494+
bool has_pri_only = false;
495+
bool has_non_pri = false;
496+
for (size_t i = 0; i < nfds; ++i) {
497+
if (fds[i].fd < 0 || fds[i].events == 0)
498+
continue;
499+
if (fds[i].events & ~POLLPRI)
500+
has_non_pri = true;
501+
else
502+
has_pri_only = true;
503+
}
504+
if (has_pri_only && !has_non_pri) {
505+
errno = ENOSYS;
506+
return -1;
507+
}
508+
return 0;
509+
}
510+
345511
int poll(struct pollfd* fds, nfds_t nfds, int timeout) {
346-
return poll_impl(fds, nfds, timeout);
512+
if (validate_something_not_pollpri(fds, nfds) < 0)
513+
return -1;
514+
for (size_t i = 0; i < nfds; ++i)
515+
fds[i].revents = 0;
516+
return poll_impl(fds, nfds, timeout);
347517
}

libc-bottom-half/headers/private/wasi/poll.h

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
#ifndef __wasip1__
77

88
#include <poll.h>
9+
#include <wasi/api.h>
910

1011
/// Opaque state that the `poll` function manages for itself.
1112
typedef struct poll_state_t poll_state_t;
@@ -18,8 +19,6 @@ void __wasilibc_poll_ready(poll_state_t *state, short events);
1819

1920
#ifdef __wasip2__
2021

21-
#include <wasi/wasip2.h>
22-
2322
/// Adds the `pollable` to the `state` provided.
2423
///
2524
/// This will add a `pollable` to get passed to `wasi:io/poll.poll`. If that
@@ -56,6 +55,20 @@ __wasilibc_poll_add_output_stream(poll_state_t *state,
5655

5756
#endif // __wasip2__
5857

58+
#ifdef __wasip3__
59+
60+
typedef void (*poll_ready_t)(void *data, poll_state_t *state,
61+
wasip3_event_t *event);
62+
63+
/// Adds the `waitable` to the poll `state`.
64+
///
65+
/// When `waitable` receives an event it'll invoke `ready` with `ready_data`,
66+
/// the poll state, and the event that happened.
67+
int __wasilibc_poll_add(poll_state_t *state, uint32_t waitable,
68+
poll_ready_t ready, void *ready_data);
69+
70+
#endif // __wasip3__
71+
5972
#endif // not(__wasip1__)
6073

6174
#endif // WASI_POLL_H

libc-bottom-half/headers/private/wasi/wasip3_block.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,12 @@ static inline size_t __wasilibc_stream_block_on(wasip3_waitable_status_t status,
8383
return ret;
8484
}
8585

86+
/// Polls the `waitable` provide, filling in `event`, if any.
87+
///
88+
/// This will call `waitable-set.poll` with `waitable` and the `event` is
89+
/// whatever that returns.
90+
void __wasilibc_poll_waitable(uint32_t waitable, wasip3_event_t *event);
91+
8692
#endif // __wasip3__
8793

8894
#endif // WASI_WASIP3_BLOCK_H

0 commit comments

Comments
 (0)