diff --git a/Cargo.lock b/Cargo.lock index 0524df9..3f33880 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1809,7 +1809,7 @@ dependencies = [ [[package]] name = "wasi-preview2-prototype" -version = "0.0.2" +version = "0.0.3" dependencies = [ "anyhow", "bytes", diff --git a/wasi/Cargo.toml b/wasi/Cargo.toml index 26ba63d..825102a 100644 --- a/wasi/Cargo.toml +++ b/wasi/Cargo.toml @@ -1,7 +1,7 @@ [package] # name = "wasi" name = "wasi-preview2-prototype" -version = "0.0.2" +version = "0.0.3" # version = "0.12.0+wasi-snapshot-preview2" description = "Experimental WASI Preview2 API bindings for Rust" edition.workspace = true diff --git a/wasi/src/http_client.rs b/wasi/src/http_client.rs index 8d58ec4..136c13a 100644 --- a/wasi/src/http_client.rs +++ b/wasi/src/http_client.rs @@ -7,6 +7,16 @@ use crate::snapshots::preview_2::wasi::http::{outgoing_handler, types as http_ty use crate::snapshots::preview_2::wasi::io::streams; use crate::snapshots::preview_2::wasi::poll::poll; +struct DropPollable { + pollable: poll::Pollable, +} + +impl Drop for DropPollable { + fn drop(&mut self) { + poll::drop_pollable(self.pollable); + } +} + pub struct DefaultClient { options: Option, } @@ -68,16 +78,36 @@ impl TryFrom> for Request { .map_err(|_| anyhow!("outgoing request write failed"))?; if body.is_empty() { - streams::write(request_body, &[]).map_err(|_| anyhow!("writing request body"))?; - } else { - let output_stream_pollable = streams::subscribe_to_output_stream(request_body); - let mut body_cursor = 0; - while body_cursor < body.len() { - let (written, _) = streams::write(request_body, &body[body_cursor..]) - .map_err(|_| anyhow!("writing request body"))?; - body_cursor += written as usize; + let sub = DropPollable { + pollable: streams::subscribe_to_output_stream(request_body), + }; + let mut buf = body.as_ref(); + while !buf.is_empty() { + poll::poll_oneoff(&[sub.pollable]); + + let permit = match streams::check_write(request_body) { + Ok(n) => usize::try_from(n)?, + Err(_) => anyhow::bail!("output stream error"), + }; + + let len = buf.len().min(permit); + let (chunk, rest) = buf.split_at(len); + buf = rest; + + if streams::write(request_body, chunk).is_err() { + anyhow::bail!("output stream error") + } + } + + if streams::flush(request_body).is_err() { + anyhow::bail!("output stream error") + } + + poll::poll_oneoff(&[sub.pollable]); + + if streams::check_write(request_body).is_err() { + anyhow::bail!("output stream error") } - poll::drop_pollable(output_stream_pollable); } Ok(Request::new(request, request_body)) @@ -146,21 +176,25 @@ impl TryFrom for http::Response { let body_stream = http_types::incoming_response_consume(incoming_response) .map_err(|_| anyhow!("consuming incoming response"))?; - let input_stream_pollable = streams::subscribe_to_input_stream(body_stream); let mut body = BytesMut::new(); - let mut eof = streams::StreamStatus::Open; - while eof != streams::StreamStatus::Ended { - let (body_chunk, stream_status) = streams::read(body_stream, u64::MAX) - .map_err(|e| anyhow!("reading response body: {e:?}"))?; - eof = if body_chunk.is_empty() { - streams::StreamStatus::Ended - } else { - stream_status + { + let sub = DropPollable { + pollable: streams::subscribe_to_input_stream(body_stream), }; - body.put(body_chunk.as_slice()); + poll::poll_oneoff(&[sub.pollable]); + let mut eof = streams::StreamStatus::Open; + while eof != streams::StreamStatus::Ended { + let (body_chunk, stream_status) = streams::read(body_stream, u64::MAX) + .map_err(|e| anyhow!("reading response body: {e:?}"))?; + eof = if body_chunk.is_empty() { + streams::StreamStatus::Ended + } else { + stream_status + }; + body.put(body_chunk.as_slice()); + } } - poll::drop_pollable(input_stream_pollable); let mut res = http::Response::builder() .status(status) diff --git a/wasi/wit/deps.toml b/wasi/wit/deps.toml index 9aef68e..3846922 100644 --- a/wasi/wit/deps.toml +++ b/wasi/wit/deps.toml @@ -1 +1 @@ -preview = "https://gitpkg.now.sh/bytecodealliance/wasmtime/crates/wasi?e250334b8ebfba9359802ab7f61bdd7c6085d87a" +preview = "https://gitpkg.now.sh/bytecodealliance/wasmtime/crates/wasi?aec4b25b8f62f409175a3cc6c4a4ed18b446d3ae" diff --git a/wasi/wit/deps/io/streams.wit b/wasi/wit/deps/io/streams.wit index 98df181..e2631f6 100644 --- a/wasi/wit/deps/io/streams.wit +++ b/wasi/wit/deps/io/streams.wit @@ -134,58 +134,115 @@ interface streams { /// This [represents a resource](https://github.com/WebAssembly/WASI/blob/main/docs/WitInWasi.md#Resources). type output-stream = u32 - /// Perform a non-blocking write of bytes to a stream. + /// An error for output-stream operations. /// - /// This function returns a `u64` and a `stream-status`. The `u64` indicates - /// the number of bytes from `buf` that were written, which may be less than - /// the length of `buf`. The `stream-status` indicates if further writes to - /// the stream are expected to be read. + /// Contrary to input-streams, a closed output-stream is reported using + /// an error. + enum write-error { + /// The last operation (a write or flush) failed before completion. + last-operation-failed, + /// The stream is closed: no more input will be accepted by the + /// stream. A closed output-stream will return this error on all + /// future operations. + closed + } + /// Check readiness for writing. This function never blocks. + /// + /// Returns the number of bytes permitted for the next call to `write`, + /// or an error. Calling `write` with more bytes than this function has + /// permitted will trap. + /// + /// When this function returns 0 bytes, the `subscribe-to-output-stream` + /// pollable will become ready when this function will report at least + /// 1 byte, or an error. + check-write: func( + this: output-stream + ) -> result + + /// Perform a write. This function never blocks. /// - /// When the returned `stream-status` is `open`, the `u64` return value may - /// be less than the length of `buf`. This indicates that no more bytes may - /// be written to the stream promptly. In that case the - /// `subscribe-to-output-stream` pollable will indicate when additional bytes - /// may be promptly written. + /// Precondition: check-write gave permit of Ok(n) and contents has a + /// length of less than or equal to n. Otherwise, this function will trap. /// - /// Writing an empty list must return a non-error result with `0` for the - /// `u64` return value, and the current `stream-status`. + /// returns Err(closed) without writing if the stream has closed since + /// the last call to check-write provided a permit. write: func( this: output-stream, - /// Data to write - buf: list - ) -> result> + contents: list + ) -> result<_, write-error> - /// Blocking write of bytes to a stream. + /// Perform a write of up to 4096 bytes, and then flush the stream. Block + /// until all of these operations are complete, or an error occurs. /// - /// This is similar to `write`, except that it blocks until at least one - /// byte can be written. - blocking-write: func( - this: output-stream, - /// Data to write - buf: list - ) -> result> + /// This is a convenience wrapper around the use of `check-write`, + /// `subscribe-to-output-stream`, `write`, and `flush`, and is implemented + /// with the following pseudo-code: + /// + /// ```text + /// let pollable = subscribe-to-output-stream(this); + /// while !contents.is_empty() { + /// // Wait for the stream to become writable + /// poll-oneoff(pollable); + /// let Ok(n) = check-write(this); // eliding error handling + /// let len = min(n, contents.len()); + /// let (chunk, rest) = contents.split_at(len); + /// write(this, chunk); // eliding error handling + /// contents = rest; + /// } + /// flush(this); + /// // Wait for completion of `flush` + /// poll-oneoff(pollable); + /// // Check for any errors that arose during `flush` + /// let _ = check-write(this); // eliding error handling + /// ``` + blocking-write-and-flush: func( + this: output-stream, + contents: list + ) -> result<_, write-error> - /// Write multiple zero-bytes to a stream. + /// Request to flush buffered output. This function never blocks. /// - /// This function returns a `u64` indicating the number of zero-bytes - /// that were written; it may be less than `len`. Equivelant to a call to - /// `write` with a list of zeroes of the given length. - write-zeroes: func( + /// This tells the output-stream that the caller intends any buffered + /// output to be flushed. the output which is expected to be flushed + /// is all that has been passed to `write` prior to this call. + /// + /// Upon calling this function, the `output-stream` will not accept any + /// writes (`check-write` will return `ok(0)`) until the flush has + /// completed. The `subscribe-to-output-stream` pollable will become ready + /// when the flush has completed and the stream can accept more writes. + flush: func( this: output-stream, - /// The number of zero-bytes to write - len: u64 - ) -> result> + ) -> result<_, write-error> + + /// Request to flush buffered output, and block until flush completes + /// and stream is ready for writing again. + blocking-flush: func( + this: output-stream, + ) -> result<_, write-error> + + /// Create a `pollable` which will resolve once the output-stream + /// is ready for more writing, or an error has occured. When this + /// pollable is ready, `check-write` will return `ok(n)` with n>0, or an + /// error. + /// + /// If the stream is closed, this pollable is always ready immediately. + /// + /// The created `pollable` is a child resource of the `output-stream`. + /// Implementations may trap if the `output-stream` is dropped before + /// all derived `pollable`s created with this function are dropped. + subscribe-to-output-stream: func(this: output-stream) -> pollable - /// Write multiple zero bytes to a stream, with blocking. + /// Write zeroes to a stream. /// - /// This is similar to `write-zeroes`, except that it blocks until at least - /// one byte can be written. Equivelant to a call to `blocking-write` with - /// a list of zeroes of the given length. - blocking-write-zeroes: func( + /// this should be used precisely like `write` with the exact same + /// preconditions (must use check-write first), but instead of + /// passing a list of bytes, you simply pass the number of zero-bytes + /// that should be written. + write-zeroes: func( this: output-stream, - /// The number of zero bytes to write + /// The number of zero-bytes to write len: u64 - ) -> result> + ) -> result<_, write-error> /// Read from one stream and write to another. /// @@ -232,16 +289,6 @@ interface streams { src: input-stream ) -> result> - /// Create a `pollable` which will resolve once either the specified stream - /// is ready to accept bytes or the `stream-state` has become closed. - /// - /// Once the stream-state is closed, this pollable is always ready - /// immediately. - /// - /// The created `pollable` is a child resource of the `output-stream`. - /// Implementations may trap if the `output-stream` is dropped before - /// all derived `pollable`s created with this function are dropped. - subscribe-to-output-stream: func(this: output-stream) -> pollable /// Dispose of the specified `output-stream`, after which it may no longer /// be used. diff --git a/wasi/wit/deps/preview/test.wit b/wasi/wit/deps/preview/test.wit index 447304c..4543cb1 100644 --- a/wasi/wit/deps/preview/test.wit +++ b/wasi/wit/deps/preview/test.wit @@ -26,3 +26,16 @@ world test-command { import wasi:cli/stdout import wasi:cli/stderr } + +world test-command-with-sockets { + import wasi:poll/poll + import wasi:io/streams + import wasi:cli/environment + import wasi:cli/stdin + import wasi:cli/stdout + import wasi:cli/stderr + import wasi:sockets/tcp + import wasi:sockets/tcp-create-socket + import wasi:sockets/network + import wasi:sockets/instance-network +} diff --git a/wasi/wit/deps/sockets/ip-name-lookup.wit b/wasi/wit/deps/sockets/ip-name-lookup.wit index 6c64b46..f15d19d 100644 --- a/wasi/wit/deps/sockets/ip-name-lookup.wit +++ b/wasi/wit/deps/sockets/ip-name-lookup.wit @@ -5,9 +5,9 @@ interface ip-name-lookup { /// Resolve an internet host name to a list of IP addresses. - /// + /// /// See the wasi-socket proposal README.md for a comparison with getaddrinfo. - /// + /// /// # Parameters /// - `name`: The name to look up. IP addresses are not allowed. Unicode domain names are automatically converted /// to ASCII using IDNA encoding. @@ -17,18 +17,18 @@ interface ip-name-lookup { /// systems without an active IPv6 interface. Notes: /// - Even when no public IPv6 interfaces are present or active, names like "localhost" can still resolve to an IPv6 address. /// - Whatever is "available" or "unavailable" is volatile and can change everytime a network cable is unplugged. - /// + /// /// This function never blocks. It either immediately fails or immediately returns successfully with a `resolve-address-stream` /// that can be used to (asynchronously) fetch the results. - /// + /// /// At the moment, the stream never completes successfully with 0 items. Ie. the first call /// to `resolve-next-address` never returns `ok(none)`. This may change in the future. - /// + /// /// # Typical errors /// - `invalid-name`: `name` is a syntactically invalid domain name. /// - `invalid-name`: `name` is an IP address. /// - `address-family-not-supported`: The specified `address-family` is not supported. (EAI_FAMILY) - /// + /// /// # References: /// - /// - @@ -41,14 +41,14 @@ interface ip-name-lookup { type resolve-address-stream = u32 /// Returns the next address from the resolver. - /// + /// /// This function should be called multiple times. On each call, it will /// return the next address in connection order preference. If all /// addresses have been exhausted, this function returns `none`. /// After which, you should release the stream with `drop-resolve-address-stream`. - /// + /// /// This function never returns IPv4-mapped IPv6 addresses. - /// + /// /// # Typical errors /// - `name-unresolvable`: Name does not exist or has no suitable associated IP addresses. (EAI_NONAME, EAI_NODATA, EAI_ADDRFAMILY) /// - `temporary-resolver-failure`: A temporary failure in name resolution occurred. (EAI_AGAIN) @@ -57,12 +57,12 @@ interface ip-name-lookup { resolve-next-address: func(this: resolve-address-stream) -> result, error-code> /// Dispose of the specified `resolve-address-stream`, after which it may no longer be used. - /// + /// /// Note: this function is scheduled to be removed when Resources are natively supported in Wit. drop-resolve-address-stream: func(this: resolve-address-stream) /// Create a `pollable` which will resolve once the stream is ready for I/O. - /// + /// /// Note: this function is here for WASI Preview2 only. /// It's planned to be removed when `future` is natively supported in Preview3. subscribe: func(this: resolve-address-stream) -> pollable diff --git a/wasi/wit/deps/sockets/network.wit b/wasi/wit/deps/sockets/network.wit index c370214..a198ea8 100644 --- a/wasi/wit/deps/sockets/network.wit +++ b/wasi/wit/deps/sockets/network.wit @@ -4,12 +4,12 @@ interface network { /// An opaque resource that represents access to (a subset of) the network. /// This enables context-based security for networking. /// There is no need for this to map 1:1 to a physical network interface. - /// + /// /// FYI, In the future this will be replaced by handle types. type network = u32 /// Dispose of the specified `network`, after which it may no longer be used. - /// + /// /// Note: this function is scheduled to be removed when Resources are natively supported in Wit. drop-network: func(this: network) @@ -153,7 +153,7 @@ interface network { enum ip-address-family { /// Similar to `AF_INET` in POSIX. - ipv4, + ipv4, /// Similar to `AF_INET6` in POSIX. ipv6, diff --git a/wasi/wit/deps/sockets/tcp-create-socket.wit b/wasi/wit/deps/sockets/tcp-create-socket.wit index f467d28..f43bc89 100644 --- a/wasi/wit/deps/sockets/tcp-create-socket.wit +++ b/wasi/wit/deps/sockets/tcp-create-socket.wit @@ -4,20 +4,20 @@ interface tcp-create-socket { use tcp.{tcp-socket} /// Create a new TCP socket. - /// + /// /// Similar to `socket(AF_INET or AF_INET6, SOCK_STREAM, IPPROTO_TCP)` in POSIX. - /// + /// /// This function does not require a network capability handle. This is considered to be safe because /// at time of creation, the socket is not bound to any `network` yet. Up to the moment `bind`/`listen`/`connect` /// is called, the socket is effectively an in-memory configuration object, unable to communicate with the outside world. - /// + /// /// All sockets are non-blocking. Use the wasi-poll interface to block on asynchronous operations. - /// + /// /// # Typical errors /// - `not-supported`: The host does not support TCP sockets. (EOPNOTSUPP) /// - `address-family-not-supported`: The specified `address-family` is not supported. (EAFNOSUPPORT) /// - `new-socket-limit`: The new socket resource could not be created because of a system limit. (EMFILE, ENFILE) - /// + /// /// # References /// - /// - diff --git a/wasi/wit/deps/sockets/tcp.wit b/wasi/wit/deps/sockets/tcp.wit index 7ed46a6..3922769 100644 --- a/wasi/wit/deps/sockets/tcp.wit +++ b/wasi/wit/deps/sockets/tcp.wit @@ -6,7 +6,7 @@ interface tcp { /// A TCP socket handle. type tcp-socket = u32 - + enum shutdown-type { /// Similar to `SHUT_RD` in POSIX. @@ -25,24 +25,24 @@ interface tcp { /// If the IP address is zero (`0.0.0.0` in IPv4, `::` in IPv6), it is left to the implementation to decide which /// network interface(s) to bind to. /// If the TCP/UDP port is zero, the socket will be bound to a random free port. - /// + /// /// When a socket is not explicitly bound, the first invocation to a listen or connect operation will /// implicitly bind the socket. - /// + /// /// Unlike in POSIX, this function is async. This enables interactive WASI hosts to inject permission prompts. - /// + /// /// # Typical `start` errors /// - `address-family-mismatch`: The `local-address` has the wrong address family. (EINVAL) /// - `already-bound`: The socket is already bound. (EINVAL) /// - `concurrency-conflict`: Another `bind`, `connect` or `listen` operation is already in progress. (EALREADY) - /// + /// /// # Typical `finish` errors /// - `ephemeral-ports-exhausted`: No ephemeral ports available. (EADDRINUSE, ENOBUFS on Windows) /// - `address-in-use`: Address is already in use. (EADDRINUSE) /// - `address-not-bindable`: `local-address` is not an address that the `network` can bind to. (EADDRNOTAVAIL) /// - `not-in-progress`: A `bind` operation is not in progress. /// - `would-block`: Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN) - /// + /// /// # References /// - /// - @@ -52,11 +52,11 @@ interface tcp { finish-bind: func(this: tcp-socket) -> result<_, error-code> /// Connect to a remote endpoint. - /// + /// /// On success: /// - the socket is transitioned into the Connection state /// - a pair of streams is returned that can be used to read & write to the connection - /// + /// /// # Typical `start` errors /// - `address-family-mismatch`: The `remote-address` has the wrong address family. (EAFNOSUPPORT) /// - `invalid-remote-address`: The IP address in `remote-address` is set to INADDR_ANY (`0.0.0.0` / `::`). (EADDRNOTAVAIL on Windows) @@ -65,7 +65,7 @@ interface tcp { /// - `already-connected`: The socket is already in the Connection state. (EISCONN) /// - `already-listening`: The socket is already in the Listener state. (EOPNOTSUPP, EINVAL on Windows) /// - `concurrency-conflict`: Another `bind`, `connect` or `listen` operation is already in progress. (EALREADY) - /// + /// /// # Typical `finish` errors /// - `timeout`: Connection timed out. (ETIMEDOUT) /// - `connection-refused`: The connection was forcefully rejected. (ECONNREFUSED) @@ -74,23 +74,28 @@ interface tcp { /// - `ephemeral-ports-exhausted`: Tried to perform an implicit bind, but there were no ephemeral ports available. (EADDRINUSE, EADDRNOTAVAIL on Linux, EAGAIN on BSD) /// - `not-in-progress`: A `connect` operation is not in progress. /// - `would-block`: Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN) - /// + /// /// # References /// - /// - /// - /// - start-connect: func(this: tcp-socket, network: network, remote-address: ip-socket-address) -> result<_, error-code> + /// Note: the returned `input-stream` and `output-stream` are child + /// resources of the `tcp-socket`. Implementations may trap if the + /// `tcp-socket` is dropped before both of these streams are dropped. finish-connect: func(this: tcp-socket) -> result, error-code> /// Start listening for new connections. - /// + /// /// Transitions the socket into the Listener state. - /// - /// Unlike in POSIX, this function is async. This enables interactive WASI hosts to inject permission prompts. - /// + /// + /// Unlike POSIX: + /// - this function is async. This enables interactive WASI hosts to inject permission prompts. + /// - the socket must already be explicitly bound. + /// /// # Typical `start` errors - /// - `already-attached`: The socket is already attached to a different network. The `network` passed to `listen` must be identical to the one passed to `bind`. + /// - `not-bound`: The socket is not bound to any local address. (EDESTADDRREQ) /// - `already-connected`: The socket is already in the Connection state. (EISCONN, EINVAL on BSD) /// - `already-listening`: The socket is already in the Listener state. /// - `concurrency-conflict`: Another `bind`, `connect` or `listen` operation is already in progress. (EINVAL on BSD) @@ -105,22 +110,26 @@ interface tcp { /// - /// - /// - - start-listen: func(this: tcp-socket, network: network) -> result<_, error-code> + start-listen: func(this: tcp-socket) -> result<_, error-code> finish-listen: func(this: tcp-socket) -> result<_, error-code> /// Accept a new client socket. - /// + /// /// The returned socket is bound and in the Connection state. - /// + /// /// On success, this function returns the newly accepted client socket along with /// a pair of streams that can be used to read & write to the connection. /// + /// Note: the returned `input-stream` and `output-stream` are child + /// resources of the returned `tcp-socket`. Implementations may trap if the + /// `tcp-socket` is dropped before its child streams are dropped. + /// /// # Typical errors /// - `not-listening`: Socket is not in the Listener state. (EINVAL) /// - `would-block`: No pending connections at the moment. (EWOULDBLOCK, EAGAIN) - /// + /// /// Host implementations must skip over transient errors returned by the native accept syscall. - /// + /// /// # References /// - /// - @@ -129,10 +138,10 @@ interface tcp { accept: func(this: tcp-socket) -> result, error-code> /// Get the bound local address. - /// + /// /// # Typical errors /// - `not-bound`: The socket is not bound to any local address. - /// + /// /// # References /// - /// - @@ -141,10 +150,10 @@ interface tcp { local-address: func(this: tcp-socket) -> result /// Get the bound remote address. - /// + /// /// # Typical errors /// - `not-connected`: The socket is not connected to a remote address. (ENOTCONN) - /// + /// /// # References /// - /// - @@ -153,14 +162,14 @@ interface tcp { remote-address: func(this: tcp-socket) -> result /// Whether this is a IPv4 or IPv6 socket. - /// + /// /// Equivalent to the SO_DOMAIN socket option. address-family: func(this: tcp-socket) -> ip-address-family - + /// Whether IPv4 compatibility (dual-stack) mode is disabled or not. - /// + /// /// Equivalent to the IPV6_V6ONLY socket option. - /// + /// /// # Typical errors /// - `ipv6-only-operation`: (get/set) `this` socket is an IPv4 socket. /// - `already-bound`: (set) The socket is already bound. @@ -170,28 +179,28 @@ interface tcp { set-ipv6-only: func(this: tcp-socket, value: bool) -> result<_, error-code> /// Hints the desired listen queue size. Implementations are free to ignore this. - /// + /// /// # Typical errors /// - `already-connected`: (set) The socket is already in the Connection state. /// - `concurrency-conflict`: (set) A `bind`, `connect` or `listen` operation is already in progress. (EALREADY) set-listen-backlog-size: func(this: tcp-socket, value: u64) -> result<_, error-code> /// Equivalent to the SO_KEEPALIVE socket option. - /// + /// /// # Typical errors /// - `concurrency-conflict`: (set) A `bind`, `connect` or `listen` operation is already in progress. (EALREADY) keep-alive: func(this: tcp-socket) -> result set-keep-alive: func(this: tcp-socket, value: bool) -> result<_, error-code> /// Equivalent to the TCP_NODELAY socket option. - /// + /// /// # Typical errors /// - `concurrency-conflict`: (set) A `bind`, `connect` or `listen` operation is already in progress. (EALREADY) no-delay: func(this: tcp-socket) -> result set-no-delay: func(this: tcp-socket, value: bool) -> result<_, error-code> - + /// Equivalent to the IP_TTL & IPV6_UNICAST_HOPS socket options. - /// + /// /// # Typical errors /// - `already-connected`: (set) The socket is already in the Connection state. /// - `already-listening`: (set) The socket is already in the Listener state. @@ -200,16 +209,16 @@ interface tcp { set-unicast-hop-limit: func(this: tcp-socket, value: u8) -> result<_, error-code> /// The kernel buffer space reserved for sends/receives on this socket. - /// + /// /// Note #1: an implementation may choose to cap or round the buffer size when setting the value. /// In other words, after setting a value, reading the same setting back may return a different value. - /// + /// /// Note #2: there is not necessarily a direct relationship between the kernel buffer size and the bytes of /// actual data to be sent/received by the application, because the kernel might also use the buffer space /// for internal metadata structures. - /// + /// /// Equivalent to the SO_RCVBUF and SO_SNDBUF socket options. - /// + /// /// # Typical errors /// - `already-connected`: (set) The socket is already in the Connection state. /// - `already-listening`: (set) The socket is already in the Listener state. @@ -220,25 +229,29 @@ interface tcp { set-send-buffer-size: func(this: tcp-socket, value: u64) -> result<_, error-code> /// Create a `pollable` which will resolve once the socket is ready for I/O. + /// + /// The created `pollable` is a child resource of the `tcp-socket`. + /// Implementations may trap if the `tcp-socket` is dropped before all + /// derived `pollable`s created with this function are dropped. /// /// Note: this function is here for WASI Preview2 only. /// It's planned to be removed when `future` is natively supported in Preview3. subscribe: func(this: tcp-socket) -> pollable /// Initiate a graceful shutdown. - /// + /// /// - receive: the socket is not expecting to receive any more data from the peer. All subsequent read /// operations on the `input-stream` associated with this socket will return an End Of Stream indication. /// Any data still in the receive queue at time of calling `shutdown` will be discarded. /// - send: the socket is not expecting to send any more data to the peer. All subsequent write /// operations on the `output-stream` associated with this socket will return an error. /// - both: same effect as receive & send combined. - /// + /// /// The shutdown function does not close (drop) the socket. - /// + /// /// # Typical errors /// - `not-connected`: The socket is not in the Connection state. (ENOTCONN) - /// + /// /// # References /// - /// - @@ -247,9 +260,9 @@ interface tcp { shutdown: func(this: tcp-socket, shutdown-type: shutdown-type) -> result<_, error-code> /// Dispose of the specified `tcp-socket`, after which it may no longer be used. - /// + /// /// Similar to the POSIX `close` function. - /// + /// /// Note: this function is scheduled to be removed when Resources are natively supported in Wit. drop-tcp-socket: func(this: tcp-socket) } diff --git a/wasi/wit/deps/sockets/udp-create-socket.wit b/wasi/wit/deps/sockets/udp-create-socket.wit index 1cfbd7f..cd4c08f 100644 --- a/wasi/wit/deps/sockets/udp-create-socket.wit +++ b/wasi/wit/deps/sockets/udp-create-socket.wit @@ -4,20 +4,20 @@ interface udp-create-socket { use udp.{udp-socket} /// Create a new UDP socket. - /// + /// /// Similar to `socket(AF_INET or AF_INET6, SOCK_DGRAM, IPPROTO_UDP)` in POSIX. - /// + /// /// This function does not require a network capability handle. This is considered to be safe because /// at time of creation, the socket is not bound to any `network` yet. Up to the moment `bind`/`connect` is called, /// the socket is effectively an in-memory configuration object, unable to communicate with the outside world. - /// + /// /// All sockets are non-blocking. Use the wasi-poll interface to block on asynchronous operations. - /// + /// /// # Typical errors /// - `not-supported`: The host does not support UDP sockets. (EOPNOTSUPP) /// - `address-family-not-supported`: The specified `address-family` is not supported. (EAFNOSUPPORT) /// - `new-socket-limit`: The new socket resource could not be created because of a system limit. (EMFILE, ENFILE) - /// + /// /// # References: /// - /// - diff --git a/wasi/wit/deps/sockets/udp.wit b/wasi/wit/deps/sockets/udp.wit index 9dd4573..700b9e2 100644 --- a/wasi/wit/deps/sockets/udp.wit +++ b/wasi/wit/deps/sockets/udp.wit @@ -27,23 +27,23 @@ interface udp { /// If the IP address is zero (`0.0.0.0` in IPv4, `::` in IPv6), it is left to the implementation to decide which /// network interface(s) to bind to. /// If the TCP/UDP port is zero, the socket will be bound to a random free port. - /// + /// /// When a socket is not explicitly bound, the first invocation to connect will implicitly bind the socket. - /// + /// /// Unlike in POSIX, this function is async. This enables interactive WASI hosts to inject permission prompts. - /// + /// /// # Typical `start` errors /// - `address-family-mismatch`: The `local-address` has the wrong address family. (EINVAL) /// - `already-bound`: The socket is already bound. (EINVAL) /// - `concurrency-conflict`: Another `bind` or `connect` operation is already in progress. (EALREADY) - /// + /// /// # Typical `finish` errors /// - `ephemeral-ports-exhausted`: No ephemeral ports available. (EADDRINUSE, ENOBUFS on Windows) /// - `address-in-use`: Address is already in use. (EADDRINUSE) /// - `address-not-bindable`: `local-address` is not an address that the `network` can bind to. (EADDRNOTAVAIL) /// - `not-in-progress`: A `bind` operation is not in progress. /// - `would-block`: Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN) - /// + /// /// # References /// - /// - @@ -53,29 +53,29 @@ interface udp { finish-bind: func(this: udp-socket) -> result<_, error-code> /// Set the destination address. - /// + /// /// The local-address is updated based on the best network path to `remote-address`. - /// + /// /// When a destination address is set: /// - all receive operations will only return datagrams sent from the provided `remote-address`. /// - the `send` function can only be used to send to this destination. - /// + /// /// Note that this function does not generate any network traffic and the peer is not aware of this "connection". - /// + /// /// Unlike in POSIX, this function is async. This enables interactive WASI hosts to inject permission prompts. - /// + /// /// # Typical `start` errors /// - `address-family-mismatch`: The `remote-address` has the wrong address family. (EAFNOSUPPORT) /// - `invalid-remote-address`: The IP address in `remote-address` is set to INADDR_ANY (`0.0.0.0` / `::`). (EDESTADDRREQ, EADDRNOTAVAIL) /// - `invalid-remote-address`: The port in `remote-address` is set to 0. (EDESTADDRREQ, EADDRNOTAVAIL) /// - `already-attached`: The socket is already bound to a different network. The `network` passed to `connect` must be identical to the one passed to `bind`. /// - `concurrency-conflict`: Another `bind` or `connect` operation is already in progress. (EALREADY) - /// + /// /// # Typical `finish` errors /// - `ephemeral-ports-exhausted`: Tried to perform an implicit bind, but there were no ephemeral ports available. (EADDRINUSE, EADDRNOTAVAIL on Linux, EAGAIN on BSD) /// - `not-in-progress`: A `connect` operation is not in progress. /// - `would-block`: Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN) - /// + /// /// # References /// - /// - @@ -84,32 +84,42 @@ interface udp { start-connect: func(this: udp-socket, network: network, remote-address: ip-socket-address) -> result<_, error-code> finish-connect: func(this: udp-socket) -> result<_, error-code> - /// Receive a message. - /// - /// Returns: - /// - The sender address of the datagram - /// - The number of bytes read. - /// + /// Receive messages on the socket. + /// + /// This function attempts to receive up to `max-results` datagrams on the socket without blocking. + /// The returned list may contain fewer elements than requested, but never more. + /// If `max-results` is 0, this function returns successfully with an empty list. + /// /// # Typical errors /// - `not-bound`: The socket is not bound to any local address. (EINVAL) /// - `remote-unreachable`: The remote address is not reachable. (ECONNREFUSED, ECONNRESET, ENETRESET on Windows, EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN) /// - `would-block`: There is no pending data available to be read at the moment. (EWOULDBLOCK, EAGAIN) - /// + /// /// # References /// - /// - /// - + /// - /// - /// - /// - /// - - receive: func(this: udp-socket) -> result - - /// Send a message to a specific destination address. - /// + receive: func(this: udp-socket, max-results: u64) -> result, error-code> + + /// Send messages on the socket. + /// + /// This function attempts to send all provided `datagrams` on the socket without blocking and + /// returns how many messages were actually sent (or queued for sending). + /// + /// This function semantically behaves the same as iterating the `datagrams` list and sequentially + /// sending each individual datagram until either the end of the list has been reached or the first error occurred. + /// If at least one datagram has been sent successfully, this function never returns an error. + /// + /// If the input list is empty, the function returns `ok(0)`. + /// /// The remote address option is required. To send a message to the "connected" peer, /// call `remote-address` to get their address. - /// + /// /// # Typical errors /// - `address-family-mismatch`: The `remote-address` has the wrong address family. (EAFNOSUPPORT) /// - `invalid-remote-address`: The IP address in `remote-address` is set to INADDR_ANY (`0.0.0.0` / `::`). (EDESTADDRREQ, EADDRNOTAVAIL) @@ -119,22 +129,23 @@ interface udp { /// - `remote-unreachable`: The remote address is not reachable. (ECONNREFUSED, ECONNRESET, ENETRESET on Windows, EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN) /// - `datagram-too-large`: The datagram is too large. (EMSGSIZE) /// - `would-block`: The send buffer is currently full. (EWOULDBLOCK, EAGAIN) - /// + /// /// # References /// - /// - /// - + /// - /// - /// - /// - /// - - send: func(this: udp-socket, datagram: datagram) -> result<_, error-code> + send: func(this: udp-socket, datagrams: list) -> result /// Get the current bound address. - /// + /// /// # Typical errors /// - `not-bound`: The socket is not bound to any local address. - /// + /// /// # References /// - /// - @@ -143,10 +154,10 @@ interface udp { local-address: func(this: udp-socket) -> result /// Get the address set with `connect`. - /// + /// /// # Typical errors /// - `not-connected`: The socket is not connected to a remote address. (ENOTCONN) - /// + /// /// # References /// - /// - @@ -155,14 +166,14 @@ interface udp { remote-address: func(this: udp-socket) -> result /// Whether this is a IPv4 or IPv6 socket. - /// + /// /// Equivalent to the SO_DOMAIN socket option. address-family: func(this: udp-socket) -> ip-address-family /// Whether IPv4 compatibility (dual-stack) mode is disabled or not. - /// + /// /// Equivalent to the IPV6_V6ONLY socket option. - /// + /// /// # Typical errors /// - `ipv6-only-operation`: (get/set) `this` socket is an IPv4 socket. /// - `already-bound`: (set) The socket is already bound. @@ -172,25 +183,23 @@ interface udp { set-ipv6-only: func(this: udp-socket, value: bool) -> result<_, error-code> /// Equivalent to the IP_TTL & IPV6_UNICAST_HOPS socket options. - /// + /// /// # Typical errors /// - `concurrency-conflict`: (set) Another `bind` or `connect` operation is already in progress. (EALREADY) unicast-hop-limit: func(this: udp-socket) -> result set-unicast-hop-limit: func(this: udp-socket, value: u8) -> result<_, error-code> /// The kernel buffer space reserved for sends/receives on this socket. - /// + /// /// Note #1: an implementation may choose to cap or round the buffer size when setting the value. /// In other words, after setting a value, reading the same setting back may return a different value. - /// + /// /// Note #2: there is not necessarily a direct relationship between the kernel buffer size and the bytes of /// actual data to be sent/received by the application, because the kernel might also use the buffer space /// for internal metadata structures. - /// - /// Fails when this socket is in the Listening state. - /// + /// /// Equivalent to the SO_RCVBUF and SO_SNDBUF socket options. - /// + /// /// # Typical errors /// - `concurrency-conflict`: (set) Another `bind` or `connect` operation is already in progress. (EALREADY) receive-buffer-size: func(this: udp-socket) -> result @@ -199,13 +208,13 @@ interface udp { set-send-buffer-size: func(this: udp-socket, value: u64) -> result<_, error-code> /// Create a `pollable` which will resolve once the socket is ready for I/O. - /// + /// /// Note: this function is here for WASI Preview2 only. /// It's planned to be removed when `future` is natively supported in Preview3. subscribe: func(this: udp-socket) -> pollable /// Dispose of the specified `udp-socket`, after which it may no longer be used. - /// + /// /// Note: this function is scheduled to be removed when Resources are natively supported in Wit. drop-udp-socket: func(this: udp-socket) }