-
Notifications
You must be signed in to change notification settings - Fork 738
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
add wasm32-wasip2
support
#1836
base: master
Are you sure you want to change the base?
Conversation
This implementation currently uses a mix of POSIX-style APIs (provided by `wasi-libc` via the `libc` crate) and WASIp2-native APIs (provided by the `wasi` crate). Alternatively, we could implement `Selector` using only POSIX APIs, e.g. `poll(2)`. However, that would add an extra layer of abstraction to support and debug, as well as make it impossible to support polling `wasi:io/poll/pollable` objects which cannot be represented as POSIX file descriptors (e.g. timer events, DNS queries, HTTP requests, etc.). Another approach would be to use _only_ the WASIp2 APIs and bypass `wasi-libc` entirely. However, that would break interoperability with both Rust `std` and e.g. C libraries which expect to work with file descriptors. Since `wasi-libc` does not yet provide a public API for converting between file descriptors and WASIp2 resource handles, we currently use a non-public API (see the `netc` module below) to do so. Once WebAssembly/wasi-libc#542 is addressed, we'll be able to switch to a public API. I've tested this end-to-end using https://github.com/dicej/wasi-sockets-tests, which includes smoke tests for `mio`, `tokio`, `tokio-postgres`, etc. Signed-off-by: Joel Dice <[email protected]>
This adds support for the new `wasm32-wasip2` target platform, which includes more extensive support for sockets than `wasm32-wasip1` (formerly known as `wasm32-wasi`). The bulk of the changes are in tokio-rs/mio#1836. This patch just tweaks a few `cfg` directives to indicate `wasm32-wasip2`'s additional capabilities. In the future, we could consider adding support for `ToSocketAddrs`. WASIp2 natively supports asynchronous DNS lookups and is single threaded, whereas Tokio currently assumes DNS lookups are blocking and require multithreading to emulate async lookups. A WASIp2-specific implementation could do the lookup directly without multithreading. I've tested this end-to-end using https://github.com/dicej/wasi-sockets-tests, which includes smoke tests for `mio`, `tokio`, `tokio-postgres`, etc. I'd also be happy to add tests to this repo if appropriate; it would require adding a dev-dependency on e.g. `wasmtime` to actually run the test cases. Signed-off-by: Joel Dice <[email protected]>
I just realized this PR doesn't include support for accepting incoming connections -- only initiating outgoing ones. I forgot about the former since I don't personally have an urgent need to support it, but it could be added as a follow-up PR. To be clear: WASIp2 is fully capable of handling that case. |
let mut push_event = || { | ||
events.push(Event { | ||
token: subscription.token, | ||
interests: *interests, | ||
}) | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just out of curiosity, if there any reason to do that in a closure vs. doing it inline at L276, L289 and L297?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No particular reason other than avoiding the repetition.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Alright! Was not sure if it was DRY or there was a mechanism I am not aware of, thanks for clarification 🙏
Also, I have a last clarifying question: reading But your comment:
Actually speak of connections, so I am not sure my previous assumption was correct. Anyway, great job 💪 |
WASIp2 represents things like TCP and UDP sockets as resources (e.g. tcp-socket) which have methods for binding, listening, connecting, etc. Once a socket is connected, you can get its Consequently,
So you can think of a Adding support for binding, listening and accepting WASIp2 sockets in Does that help? Happy to go into more detail if desired. Also, the wasi-sockets docs are quite thorough if you haven't perused them yet. |
BTW, this PR doesn't include UDP support either, again because that hasn't been a priority for me. Shouldn't be hard to add as a follow-up PR. |
Thanks a lot for the time you took answering me 🙏 My question was more related to how In this case, I suspect you still have the |
Oh right, great question. Yeah, I think we'd need to add a new, WASIp2-only API for registering Another approach would be to add an API to @badeend and @sunfishcode might have thoughts about this. |
Alright thanks for the answer! Reading WebAssembly/wasi-libc#542 it seems to me that the question of allocating fd to pollable was among the initial options, I wonder though how would you implement stuff like To get back to the main subject, |
Honestly, I just copied that from the existing WASIp1 implementation. I wrote this code almost a year ago and only came back to it yesterday, so it's not super fresh in my mind, but that part at least came straight from the existing code. I'm guessing the original code either needed it to make the compiler happy (e.g. make |
[target.'cfg(all(target_os = "wasi", not(target_env = "p2")))'.dependencies] | ||
wasi = "0.11.0" | ||
|
||
[target.'cfg(all(target_os = "wasi", target_env = "p2"))'.dependencies] | ||
wasi = "0.13.3" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this really the correct way to go about this? Only 0.11 supports p1?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking at https://docs.rs/wasi/latest/wasi/
I would say yes, the last mention I see of p1
is on 0.11
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@@ -175,7 +175,7 @@ where | |||
} | |||
} | |||
|
|||
#[cfg(target_os = "wasi")] | |||
#[cfg(all(target_os = "wasi", not(target_env = "p2")))] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we do this?
#[cfg(all(target_os = "wasi", not(target_env = "p2")))] | |
#[cfg(all(target_os = "wasi", target_env = "p1"))] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The reason I did it that way is because not(target_env = "p2")
is backwards compatible with rustc
versions prior to the introduction of the wasm32-wasip2
target (i.e. all stable rustc
versions as of this writing). See here for further discussion. It does mean we'll need to make changes when p3
becomes available, though. Happy to do whatever you think is best here.
@@ -87,10 +87,10 @@ impl TcpStream { | |||
/// entries in the routing cache. | |||
/// | |||
/// [write interest]: Interest::WRITABLE | |||
#[cfg(not(target_os = "wasi"))] | |||
#[cfg(any(not(target_os = "wasi"), target_env = "p2"))] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you use not(all(...))
here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you mean not(all(target_os = "wasi", target_env = "p1"))
then the same rationale I gave above for not using target_env = "p1"
applies. Again, though, happy to change it if we're not concerned about backwards rustc
compatibility.
After a good night of sleep, I realised the name of the PR should probably mention that it only adds support for established WDYT? |
Makes sense; I'll add some TODO comments to the code and to the commit message. |
// TODO tokio-rs#1: Add a public, WASIp2-only API for registering // `wasi::io::poll::Pollable`s directly (i.e. those which do not correspond to // any `wasi-libc` file descriptor, such as `wasi:http` requests). // // TODO tokio-rs#2: Add support for binding, listening, and accepting. This would // involve adding cases for `TCP_SOCKET_STATE_UNBOUND`, // `TCP_SOCKET_STATE_BOUND`, and `TCP_SOCKET_STATE_LISTENING` to the `match` // statements in `Selector::select`. // // TODO tokio-rs#3: Add support for UDP sockets. This would involve adding cases for // the `UDP_SOCKET_STATE_*` tags to the `match` statements in // `Selector::select`. Signed-off-by: Joel Dice <[email protected]>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't really have much time to review this.
Can we split this up in multiple prs as this seems to do multiple things.
- A minimal pr that adds support for v2, no adding of v2 functionality.
- The selector rewrite (not sure why this is needed)
- Any v2 additional we make, such as support for more API
let mut subscriptions = self.subscriptions.lock().unwrap(); | ||
|
||
let mut states = Vec::new(); | ||
for (fd, subscription) in subscriptions.deref() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think looping of all active subscriptions is a great idea. Nor is the allocation for states
.
Why switch from a Vec
to a HashMap
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm happy to optimize by reducing the number of allocations if desired. Would you prefer that I update this PR or leave that for a follow-up PR?
Why switch from a Vec to a HashMap?
I was aiming for O(1) lookups by file descriptor in the IoSourceState
implementation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm happy to optimize by reducing the number of allocations if desired. Would you prefer that I update this PR or leave that for a follow-up PR?
Why switch from a Vec to a HashMap?
I was aiming for O(1) lookups by file descriptor in the
IoSourceState
implementation.
It might be slower when taking into account hashing tho 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It might be slower when taking into account hashing tho 🤔
Possibly; I can switch back to using a Vec
if preferred.
Items 1 and 2 are intertwined. None of the WASIp1 APIs are available in WASIp2, so a new selector implementation is needed to target the new APIs. There is an adapter which implements (most of) the WASIp1 APIs in terms of their WASIp2 counterparts, but wasi-libc doesn't use the adapter for sockets -- it uses the WASIp2 APIs directly in order to access the much broader socket support that WASIp2 provides. As I mentioned above, we do have a few different options for a WASIp2 selector implementation (use POSIX Totally agreed that we can leave item 3 in your list for a later PR, though. |
Just to clarify, is this currently "waiting-on-review", to use the rust-lang parlance? Or, conversely, is further review currently blocked on any action from the author? |
@jeffparsons this is waiting on an action on #1836 (review). We currently don't have the capacity to review a pr of this size, so it needs to be split up. |
Thanks, @Thomasdezeeuw. I wonder if that was perhaps not clear to @dicej because his later comment suggests there's no natural decomposition of what's in this PR, which gave me the impression that he was waiting for feedback on his explanation. Just floating this idea: would it be possible/acceptable to both author and reviewer to split this into at least:
Would that make review more manageable? Are there other axes this could be reasonably split on? I'm just an observer, so sorry if this isn't helpful. Just trying to see if there's anything that could get this out of limbo. 💖 |
Along the lines of what @jeffparsons suggested, I can definitely split this up if we're not concerned about delivering useful functionality to |
@Thomasdezeeuw Does the proposal in #1836 (comment) sound okay to you? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I had a little time to look into this and TL;DR it's a no-go from me.
With the changes to wasi crate it seems to have lost the raw system (runtime) call layer. poll_oneoff
seems to have been changed to poll
, except that wasi now allocates for the result, which we do not want.
Specific to this pr, the netc
module with all the type definitions will not be accepted. Similar to other OS I do not want to maintain the definitions, especially not one for a specification that is still in flux. This should live in a libc-like crate, which I would expect to be wasi but I'm not sure after having (quickly) looked at v0.13.
Also specific to this pr is the actual implementation. Looking at wasi-p2 to me it seems we can change poll_oneoff
to poll
and it should mostly work (though I would expect certain edge cases). I don't understand why we need this many changes for this.
I think these are the three main blockers at the moment. But I didn't do a full review.
Thanks for the feedback, @Thomasdezeeuw !
That's an artifact of how WASIp2's poll function is defined, along with the underlying ABI, which determines how the host allocates and returns a It's possible to optimize that by pre-allocating an conservatively-sized buffer before calling
That's fair. Seems like
The main difference between the WASIp1 and WASIp2 APIs is that the latter doesn't have any types corresponding to p1's subscription and eventtype types, etc. In p1, those types allowed the caller to tell In the case of p2, the That said, I definitely hear what you're saying about not wanting to review or maintain a lot of new code. With that in mind, we could come at this from a completely different direction, per the code comment I put in
Although I am a bit concerned about the extra level of abstraction, simply reusing @Thomasdezeeuw, what do you think of the following as a path forward?
|
This implementation currently uses a mix of POSIX-style APIs (provided by
wasi-libc
via thelibc
crate) and WASIp2-native APIs (provided by thewasi
crate).Alternatively, we could implement
Selector
using only POSIX APIs, e.g.poll(2)
. However, that would add an extra layer of abstraction to support and debug, as well as make it impossible to support pollingwasi:io/poll/pollable
objects which cannot be represented as POSIX file descriptors (e.g. timer events, DNS queries, HTTP requests, etc.).Another approach would be to use only the WASIp2 APIs and bypass
wasi-libc
entirely. However, that would break interoperability with both Ruststd
and e.g. C libraries which expect to work with file descriptors.Since
wasi-libc
does not yet provide a public API for converting between file descriptors and WASIp2 resource handles, we currently use a non-public API (see thenetc
module below) to do so. OnceWebAssembly/wasi-libc#542 is addressed, we'll be able to switch to a public API.
I've tested this end-to-end using https://github.com/dicej/wasi-sockets-tests, which includes smoke tests for
mio
,tokio
,tokio-postgres
, etc.