Skip to content

wasip3: Implement nonblocking UDP I/O #775

Open
alexcrichton wants to merge 1 commit intoWebAssembly:mainfrom
alexcrichton:udp-nonblocking
Open

wasip3: Implement nonblocking UDP I/O #775
alexcrichton wants to merge 1 commit intoWebAssembly:mainfrom
alexcrichton:udp-nonblocking

Conversation

@alexcrichton
Copy link
Collaborator

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.

@alexcrichton
Copy link
Collaborator Author

I'll note that procedurally this is based on #774 for now. This requires a Wasmtime due to the reliance of cancellation with nonblocking I/O and cancellation does not work well on 41.0.0. Updating Wasmtime, however, requires updating the wasip3 snapshot, hence the dependency.

@badeend you're likely pretty interested in this as well. Out of curiosity would you be up for reviewing this? I understand you haven't done a lot of stuff in wasi-libc so there's no need to boot up on all the context if you aren't willing/able to, but I figured I'd ask nonetheless!

@badeend
Copy link
Member

badeend commented Mar 18, 2026

@alexcrichton Now that #774 is merged, would you mind rebasing this PR off main? That'll produce a cleaner diff to review.

Copy link
Member

@badeend badeend left a comment

Choose a reason for hiding this comment

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

I went ahead and assumed this commit is actually the meat of this PR.

So far I;ve reviewed only the polling part. I'll continue with the UDP bits tomorrow.

Comment on lines +431 to +433
// the subtask it's no longer candiate for cancellation so it's immediately
// dropped and zero'd out to avoid process in the `out` label below.
if (event.waitable == timeout_subtask) {
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
// the subtask it's no longer candiate for cancellation so it's immediately
// dropped and zero'd out to avoid process in the `out` label below.
if (event.waitable == timeout_subtask) {
// the subtask it's no longer candidate for cancellation so it's immediately
// dropped and zero'd out to avoid processing in the `out` label below.
if (event.waitable == timeout_subtask) {

}
}

static int poll_impl(struct pollfd *fds, size_t nfds, int timeout) {
Copy link
Member

Choose a reason for hiding this comment

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

I'm not able to immediately point a finger to any particular place, but something tells me there may be trouble if fds contains multiple entries for the same fd. Especially surrounding the register/ready callbacks per descriptor.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Agreed yeah, I suspect that'll have a lot of problems. I'll work on that.

Copy link
Member

Choose a reason for hiding this comment

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

It is an edge case, so as long as libc doesn't silently corrupt itself it's fine by me.

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.
Copy link
Member

@badeend badeend left a comment

Choose a reason for hiding this comment

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

Quite some intricate logic, but I think it all checks out.

return ret;
}

/// TODO
Copy link
Member

Choose a reason for hiding this comment

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

FYI; TODO comment

} while (0)

#define _DEBUG 0
#define _DEBUG 1
Copy link
Member

Choose a reason for hiding this comment

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

Was this just for testing or do you want this enabled indefinitely?

Copy link
Member

Choose a reason for hiding this comment

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

Its been a while since the last time I worked on this file. So I want to leave behind a general note, not related to the task at hand:

I see the p3 implementation builds on top of the p2 "streams" infrastructure. Strictly speaking, this should be unnecessary. All this bookkeeping exists because in 0.2 implicit binds were forbidden as that would bypass the concept of network handles. 0.3 doesn't have network handles and fully relies on virtualization. Everything regarding udp_socket_state_t, UDP_SOCKET_STATE_**, udp_socket_streams_t, etc. can be considered p2-only. The p3 async bookkeeping could in theory also be direct members on udp_socket_t.

Just a piece of background info.

return true;
}

// Ends the process of receiving a packet on `sokcet`.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
// Ends the process of receiving a packet on `sokcet`.
// Ends the process of receiving a packet on `socket`.

switch (socket->state.tag) {
case UDP_SOCKET_STATE_UNBOUND:
case UDP_SOCKET_STATE_BOUND_NOSTREAMS:
udp_socket_streams_t *streams = udp_streams(socket);;
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
udp_socket_streams_t *streams = udp_streams(socket);;
udp_socket_streams_t *streams = udp_streams(socket);

// Ensure there's a subtask started or a packet ready. If this is a nonblocking
// operation and a subtask was started then polling below isn't very useful,
// so go ahead and return `EWOULDBLOCK`.
bool started = wasip3_recv_start(socket, streams);
Copy link
Member

Choose a reason for hiding this comment

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

wasip3_recv_start also returns true when the subtask finished immediately without blocking. Ideally, this function shouldn't return EWOULDBLOCK for that case. To prevent a redundant roundtrip to poll.

// * If this is a nonblocking context, poll the result and see what happens,
// bailing out if the subtask isn't ready yet.
//
// Upon completion of the subtask it's cleared out and then the reuslt is
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
// Upon completion of the subtask it's cleared out and then the reuslt is
// Upon completion of the subtask it's cleared out and then the result is

Comment on lines +714 to +716
// This is technically deferring an error on a call to `send` to the next call
// to `send` for nonblocking I/O, but there's only so much that can be done
// about that...
Copy link
Member

Choose a reason for hiding this comment

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

In case it makes you feel any better; exposing errors from previous sends is normal for UDP.

There is no "connection", so the only way to find out about network issues is to just send out data, and then see if follow-up sends come back with e.g. ECONNREFUSED or EHOSTUNREACH. Those errors actually indicate that the prior sends failed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants