diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index 2220916..e0caa16 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -36,6 +36,6 @@ jobs: go-version: 'stable' - uses: bytecodealliance/actions/wasmtime/setup@v1 with: - version: "40.0.2" + version: "43.0.0" - run: rustup update stable --no-self-update && rustup default stable - run: cargo test --workspace diff --git a/Cargo.lock b/Cargo.lock index 963710b..ab2c168 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -228,9 +228,15 @@ name = "componentize-go" version = "0.2.0" dependencies = [ "anyhow", + "bzip2", "clap", + "dirs", "regex", - "wat", + "reqwest", + "serde", + "tar", + "toml", + "which", "wit-bindgen-go", "wit-component", "wit-parser", @@ -262,6 +268,27 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -847,6 +874,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f50d9b3dabb09ecd771ad0aa242ca6894994c130308ca3d7684634df8037391" +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + [[package]] name = "percent-encoding" version = "2.3.2" @@ -1001,6 +1034,17 @@ dependencies = [ "bitflags", ] +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.16", + "libredox", + "thiserror 2.0.17", +] + [[package]] name = "regex" version = "1.12.3" @@ -1274,6 +1318,15 @@ dependencies = [ "zmij", ] +[[package]] +name = "serde_spanned" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "876ac351060d4f882bb1032b6369eb0aef79ad9df1ea8bc404874d8cc3d0cd98" +dependencies = [ + "serde_core", +] + [[package]] name = "shlex" version = "1.3.0" @@ -1510,6 +1563,45 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "1.1.0+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8195ca05e4eb728f4ba94f3e3291661320af739c4e43779cbdfae82ab239fcc" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow", +] + +[[package]] +name = "toml_datetime" +version = "1.1.0+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97251a7c317e03ad83774a8752a7e81fb6067740609f75ea2b585b569a59198f" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_parser" +version = "1.1.0+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2334f11ee363607eb04df9b8fc8a13ca1715a72ba8662a26ac285c98aabb4011" +dependencies = [ + "winnow", +] + +[[package]] +name = "toml_writer" +version = "1.1.0+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d282ade6016312faf3e41e57ebbba0c073e4056dab1232ab1cb624199648f8ed" + [[package]] name = "tower" version = "0.5.2" @@ -1586,12 +1678,6 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" -[[package]] -name = "unicode-width" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" - [[package]] name = "unicode-xid" version = "0.2.6" @@ -1754,28 +1840,6 @@ dependencies = [ "semver", ] -[[package]] -name = "wast" -version = "245.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28cf1149285569120b8ce39db8b465e8a2b55c34cbb586bd977e43e2bc7300bf" -dependencies = [ - "bumpalo", - "leb128fmt", - "memchr", - "unicode-width", - "wasm-encoder", -] - -[[package]] -name = "wat" -version = "1.245.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd48d1679b6858988cb96b154dda0ec5bbb09275b71db46057be37332d5477be" -dependencies = [ - "wast", -] - [[package]] name = "web-sys" version = "0.3.83" @@ -1805,6 +1869,15 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "which" +version = "8.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81995fafaaaf6ae47a7d0cc83c67caf92aeb7e5331650ae6ff856f7c0c60c459" +dependencies = [ + "libc", +] + [[package]] name = "winapi-util" version = "0.1.11" @@ -2071,6 +2144,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" +[[package]] +name = "winnow" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a90e88e4667264a994d34e6d1ab2d26d398dcdca8b7f52bec8668957517fc7d8" + [[package]] name = "wit-bindgen" version = "0.46.0" @@ -2079,8 +2158,8 @@ checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" [[package]] name = "wit-bindgen-core" -version = "0.53.1" -source = "git+https://github.com/bytecodealliance/wit-bindgen?rev=3ee9fe20a5bce398360d5d291e81a4224a6d7c76#3ee9fe20a5bce398360d5d291e81a4224a6d7c76" +version = "0.54.0" +source = "git+https://github.com/dicej/wit-bindgen?rev=661ade1e#661ade1ee006000b0cc5fe066c4d08ff1bca3cef" dependencies = [ "anyhow", "heck", @@ -2089,8 +2168,8 @@ dependencies = [ [[package]] name = "wit-bindgen-go" -version = "0.53.1" -source = "git+https://github.com/bytecodealliance/wit-bindgen?rev=3ee9fe20a5bce398360d5d291e81a4224a6d7c76#3ee9fe20a5bce398360d5d291e81a4224a6d7c76" +version = "0.54.0" +source = "git+https://github.com/dicej/wit-bindgen?rev=661ade1e#661ade1ee006000b0cc5fe066c4d08ff1bca3cef" dependencies = [ "anyhow", "heck", diff --git a/Cargo.toml b/Cargo.toml index 5c03e24..e3f9e2e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,10 @@ members = ["./tests"] [workspace.dependencies] anyhow = "1.0.102" +bzip2 = "0.6.1" +once_cell = "1.21.3" +reqwest = { version = "0.13.1", features = ["blocking"] } +tar = "0.4" [workspace.package] version = "0.2.0" @@ -34,10 +38,18 @@ unnecessary_cast = 'warn' allow_attributes_without_reason = 'warn' [dependencies] -anyhow = { workspace = true} +anyhow = { workspace = true } +bzip2 = { workspace = true } +reqwest = { workspace = true } +tar = { workspace = true } clap = { version = "4.5.60", features = ["derive"] } regex = "1.12.3" -wat = { version = "1.245.1"} -wit-bindgen-go = { git = "https://github.com/bytecodealliance/wit-bindgen", rev = "3ee9fe20a5bce398360d5d291e81a4224a6d7c76" } +serde = { version = "1.0.228", features = ["derive"] } +toml = "1.1.0" +# TODO: Switch back to upstream once +# https://github.com/bytecodealliance/wit-bindgen/pull/1572 is merged: +wit-bindgen-go = { git = "https://github.com/dicej/wit-bindgen", rev = "661ade1e" } wit-component = "0.245.1" wit-parser = "0.245.1" +which = "8.0.2" +dirs = "6.0.0" diff --git a/examples/multiple-worlds/export_wasi_http_handler/handler.go b/examples/multiple-worlds/export_wasi_http_handler/handler.go new file mode 100644 index 0000000..9c44f8b --- /dev/null +++ b/examples/multiple-worlds/export_wasi_http_handler/handler.go @@ -0,0 +1,61 @@ +package export_wasi_http_handler + +import ( + . "wit_component/wasi_http_types" + + . "go.bytecodealliance.org/pkg/wit/types" +) + +// Handle the specified `Request`, returning a `Response` +func Handle(request *Request) Result[*Response, ErrorCode] { + method := request.GetMethod().Tag() + path := request.GetPathWithQuery().SomeOr("/") + + if method == MethodGet && path == "/hello" { + // Say hello! + + tx, rx := MakeStreamU8() + + go func() { + defer tx.Drop() + tx.WriteAll([]uint8("Hello, world!")) + }() + + response, send := ResponseNew( + FieldsFromList([]Tuple2[string, []byte]{ + {F0: "content-type", F1: []byte("text/plain")}, + }).Ok(), + Some(rx), + trailersFuture(), + ) + send.Drop() + + return Ok[*Response, ErrorCode](response) + + } else { + // Bad request + + response, send := ResponseNew( + MakeFields(), + None[*StreamReader[uint8]](), + trailersFuture(), + ) + send.Drop() + response.SetStatusCode(400).Ok() + + return Ok[*Response, ErrorCode](response) + + } +} + +func trailersFuture() *FutureReader[Result[Option[*Fields], ErrorCode]] { + tx, rx := MakeFutureResultOptionFieldsErrorCode() + go tx.Write(Ok[Option[*Fields], ErrorCode](None[*Fields]())) + return rx +} + +func unitFuture() *FutureReader[Result[Unit, ErrorCode]] { + tx, rx := MakeFutureResultUnitErrorCode() + go tx.Write(Ok[Unit, ErrorCode](Unit{})) + return rx +} diff --git a/examples/multiple-worlds/export_wit_world/foo.go b/examples/multiple-worlds/export_wit_world/foo.go new file mode 100644 index 0000000..4a97618 --- /dev/null +++ b/examples/multiple-worlds/export_wit_world/foo.go @@ -0,0 +1,3 @@ +package export_wit_world + +func Foo() {} diff --git a/examples/multiple-worlds/wit1/deps/cli.wit b/examples/multiple-worlds/wit1/deps/cli.wit new file mode 100644 index 0000000..8ba52c5 --- /dev/null +++ b/examples/multiple-worlds/wit1/deps/cli.wit @@ -0,0 +1,256 @@ +package wasi:cli@0.3.0-rc-2026-03-15; + +@since(version = 0.3.0-rc-2026-03-15) +interface environment { + /// Get the POSIX-style environment variables. + /// + /// Each environment variable is provided as a pair of string variable names + /// and string value. + /// + /// Morally, these are a value import, but until value imports are available + /// in the component model, this import function should return the same + /// values each time it is called. + @since(version = 0.3.0-rc-2026-03-15) + get-environment: func() -> list>; + + /// Get the POSIX-style arguments to the program. + @since(version = 0.3.0-rc-2026-03-15) + get-arguments: func() -> list; + + /// Return a path that programs should use as their initial current working + /// directory, interpreting `.` as shorthand for this. + @since(version = 0.3.0-rc-2026-03-15) + get-initial-cwd: func() -> option; +} + +@since(version = 0.3.0-rc-2026-03-15) +interface exit { + /// Exit the current instance and any linked instances. + @since(version = 0.3.0-rc-2026-03-15) + exit: func(status: result); + + /// Exit the current instance and any linked instances, reporting the + /// specified status code to the host. + /// + /// The meaning of the code depends on the context, with 0 usually meaning + /// "success", and other values indicating various types of failure. + /// + /// This function does not return; the effect is analogous to a trap, but + /// without the connotation that something bad has happened. + @unstable(feature = cli-exit-with-code) + exit-with-code: func(status-code: u8); +} + +@since(version = 0.3.0-rc-2026-03-15) +interface run { + /// Run the program. + @since(version = 0.3.0-rc-2026-03-15) + run: async func() -> result; +} + +@since(version = 0.3.0-rc-2026-03-15) +interface types { + @since(version = 0.3.0-rc-2026-03-15) + enum error-code { + /// Input/output error + io, + /// Invalid or incomplete multibyte or wide character + illegal-byte-sequence, + /// Broken pipe + pipe, + } +} + +@since(version = 0.3.0-rc-2026-03-15) +interface stdin { + use types.{error-code}; + + /// Return a stream for reading from stdin. + /// + /// This function returns a stream which provides data read from stdin, + /// and a future to signal read results. + /// + /// If the stream's readable end is dropped the future will resolve to success. + /// + /// If the stream's writable end is dropped the future will either resolve to + /// success if stdin was closed by the writer or to an error-code if reading + /// failed for some other reason. + /// + /// Multiple streams may be active at the same time. The behavior of concurrent + /// reads is implementation-specific. + @since(version = 0.3.0-rc-2026-03-15) + read-via-stream: func() -> tuple, future>>; +} + +@since(version = 0.3.0-rc-2026-03-15) +interface stdout { + use types.{error-code}; + + /// Write the given stream to stdout. + /// + /// If the stream's writable end is dropped this function will either return + /// success once the entire contents of the stream have been written or an + /// error-code representing a failure. + /// + /// Otherwise if there is an error the readable end of the stream will be + /// dropped and this function will return an error-code. + @since(version = 0.3.0-rc-2026-03-15) + write-via-stream: func(data: stream) -> future>; +} + +@since(version = 0.3.0-rc-2026-03-15) +interface stderr { + use types.{error-code}; + + /// Write the given stream to stderr. + /// + /// If the stream's writable end is dropped this function will either return + /// success once the entire contents of the stream have been written or an + /// error-code representing a failure. + /// + /// Otherwise if there is an error the readable end of the stream will be + /// dropped and this function will return an error-code. + @since(version = 0.3.0-rc-2026-03-15) + write-via-stream: func(data: stream) -> future>; +} + +/// Terminal input. +/// +/// In the future, this may include functions for disabling echoing, +/// disabling input buffering so that keyboard events are sent through +/// immediately, querying supported features, and so on. +@since(version = 0.3.0-rc-2026-03-15) +interface terminal-input { + /// The input side of a terminal. + @since(version = 0.3.0-rc-2026-03-15) + resource terminal-input; +} + +/// Terminal output. +/// +/// In the future, this may include functions for querying the terminal +/// size, being notified of terminal size changes, querying supported +/// features, and so on. +@since(version = 0.3.0-rc-2026-03-15) +interface terminal-output { + /// The output side of a terminal. + @since(version = 0.3.0-rc-2026-03-15) + resource terminal-output; +} + +/// An interface providing an optional `terminal-input` for stdin as a +/// link-time authority. +@since(version = 0.3.0-rc-2026-03-15) +interface terminal-stdin { + @since(version = 0.3.0-rc-2026-03-15) + use terminal-input.{terminal-input}; + + /// If stdin is connected to a terminal, return a `terminal-input` handle + /// allowing further interaction with it. + @since(version = 0.3.0-rc-2026-03-15) + get-terminal-stdin: func() -> option; +} + +/// An interface providing an optional `terminal-output` for stdout as a +/// link-time authority. +@since(version = 0.3.0-rc-2026-03-15) +interface terminal-stdout { + @since(version = 0.3.0-rc-2026-03-15) + use terminal-output.{terminal-output}; + + /// If stdout is connected to a terminal, return a `terminal-output` handle + /// allowing further interaction with it. + @since(version = 0.3.0-rc-2026-03-15) + get-terminal-stdout: func() -> option; +} + +/// An interface providing an optional `terminal-output` for stderr as a +/// link-time authority. +@since(version = 0.3.0-rc-2026-03-15) +interface terminal-stderr { + @since(version = 0.3.0-rc-2026-03-15) + use terminal-output.{terminal-output}; + + /// If stderr is connected to a terminal, return a `terminal-output` handle + /// allowing further interaction with it. + @since(version = 0.3.0-rc-2026-03-15) + get-terminal-stderr: func() -> option; +} + +@since(version = 0.3.0-rc-2026-03-15) +world imports { + @since(version = 0.3.0-rc-2026-03-15) + import environment; + @since(version = 0.3.0-rc-2026-03-15) + import exit; + @since(version = 0.3.0-rc-2026-03-15) + import types; + @since(version = 0.3.0-rc-2026-03-15) + import stdin; + @since(version = 0.3.0-rc-2026-03-15) + import stdout; + @since(version = 0.3.0-rc-2026-03-15) + import stderr; + @since(version = 0.3.0-rc-2026-03-15) + import terminal-input; + @since(version = 0.3.0-rc-2026-03-15) + import terminal-output; + @since(version = 0.3.0-rc-2026-03-15) + import terminal-stdin; + @since(version = 0.3.0-rc-2026-03-15) + import terminal-stdout; + @since(version = 0.3.0-rc-2026-03-15) + import terminal-stderr; + import wasi:clocks/types@0.3.0-rc-2026-03-15; + import wasi:clocks/monotonic-clock@0.3.0-rc-2026-03-15; + import wasi:clocks/system-clock@0.3.0-rc-2026-03-15; + @unstable(feature = clocks-timezone) + import wasi:clocks/timezone@0.3.0-rc-2026-03-15; + import wasi:filesystem/types@0.3.0-rc-2026-03-15; + import wasi:filesystem/preopens@0.3.0-rc-2026-03-15; + import wasi:sockets/types@0.3.0-rc-2026-03-15; + import wasi:sockets/ip-name-lookup@0.3.0-rc-2026-03-15; + import wasi:random/random@0.3.0-rc-2026-03-15; + import wasi:random/insecure@0.3.0-rc-2026-03-15; + import wasi:random/insecure-seed@0.3.0-rc-2026-03-15; +} +@since(version = 0.3.0-rc-2026-03-15) +world command { + @since(version = 0.3.0-rc-2026-03-15) + import environment; + @since(version = 0.3.0-rc-2026-03-15) + import exit; + @since(version = 0.3.0-rc-2026-03-15) + import types; + @since(version = 0.3.0-rc-2026-03-15) + import stdin; + @since(version = 0.3.0-rc-2026-03-15) + import stdout; + @since(version = 0.3.0-rc-2026-03-15) + import stderr; + @since(version = 0.3.0-rc-2026-03-15) + import terminal-input; + @since(version = 0.3.0-rc-2026-03-15) + import terminal-output; + @since(version = 0.3.0-rc-2026-03-15) + import terminal-stdin; + @since(version = 0.3.0-rc-2026-03-15) + import terminal-stdout; + @since(version = 0.3.0-rc-2026-03-15) + import terminal-stderr; + import wasi:clocks/types@0.3.0-rc-2026-03-15; + import wasi:clocks/monotonic-clock@0.3.0-rc-2026-03-15; + import wasi:clocks/system-clock@0.3.0-rc-2026-03-15; + @unstable(feature = clocks-timezone) + import wasi:clocks/timezone@0.3.0-rc-2026-03-15; + import wasi:filesystem/types@0.3.0-rc-2026-03-15; + import wasi:filesystem/preopens@0.3.0-rc-2026-03-15; + import wasi:sockets/types@0.3.0-rc-2026-03-15; + import wasi:sockets/ip-name-lookup@0.3.0-rc-2026-03-15; + import wasi:random/random@0.3.0-rc-2026-03-15; + import wasi:random/insecure@0.3.0-rc-2026-03-15; + import wasi:random/insecure-seed@0.3.0-rc-2026-03-15; + + @since(version = 0.3.0-rc-2026-03-15) + export run; +} diff --git a/examples/multiple-worlds/wit1/deps/clocks.wit b/examples/multiple-worlds/wit1/deps/clocks.wit new file mode 100644 index 0000000..19fc4bc --- /dev/null +++ b/examples/multiple-worlds/wit1/deps/clocks.wit @@ -0,0 +1,161 @@ +package wasi:clocks@0.3.0-rc-2026-03-15; + +/// This interface common types used throughout wasi:clocks. +@since(version = 0.3.0-rc-2026-03-15) +interface types { + /// A duration of time, in nanoseconds. + @since(version = 0.3.0-rc-2026-03-15) + type duration = u64; +} + +/// WASI Monotonic Clock is a clock API intended to let users measure elapsed +/// time. +/// +/// It is intended to be portable at least between Unix-family platforms and +/// Windows. +/// +/// A monotonic clock is a clock which has an unspecified initial value, and +/// successive reads of the clock will produce non-decreasing values. +@since(version = 0.3.0-rc-2026-03-15) +interface monotonic-clock { + use types.{duration}; + + /// A mark on a monotonic clock is a number of nanoseconds since an + /// unspecified initial value, and can only be compared to instances from + /// the same monotonic-clock. + @since(version = 0.3.0-rc-2026-03-15) + type mark = u64; + + /// Read the current value of the clock. + /// + /// The clock is monotonic, therefore calling this function repeatedly will + /// produce a sequence of non-decreasing values. + /// + /// For completeness, this function traps if it's not possible to represent + /// the value of the clock in a `mark`. Consequently, implementations + /// should ensure that the starting time is low enough to avoid the + /// possibility of overflow in practice. + @since(version = 0.3.0-rc-2026-03-15) + now: func() -> mark; + + /// Query the resolution of the clock. Returns the duration of time + /// corresponding to a clock tick. + @since(version = 0.3.0-rc-2026-03-15) + get-resolution: func() -> duration; + + /// Wait until the specified mark has occurred. + @since(version = 0.3.0-rc-2026-03-15) + wait-until: async func(when: mark); + + /// Wait for the specified duration to elapse. + @since(version = 0.3.0-rc-2026-03-15) + wait-for: async func(how-long: duration); +} + +/// WASI System Clock is a clock API intended to let users query the current +/// time. The clock is not necessarily monotonic as it may be reset. +/// +/// It is intended to be portable at least between Unix-family platforms and +/// Windows. +/// +/// External references may be reset, so this clock is not necessarily +/// monotonic, making it unsuitable for measuring elapsed time. +/// +/// It is intended for reporting the current date and time for humans. +@since(version = 0.3.0-rc-2026-03-15) +interface system-clock { + use types.{duration}; + + /// An "instant", or "exact time", is a point in time without regard to any + /// time zone: just the time since a particular external reference point, + /// often called an "epoch". + /// + /// Here, the epoch is 1970-01-01T00:00:00Z, also known as + /// [POSIX's Seconds Since the Epoch], also known as [Unix Time]. + /// + /// Note that even if the seconds field is negative, incrementing + /// nanoseconds always represents moving forwards in time. + /// For example, `{ -1 seconds, 999999999 nanoseconds }` represents the + /// instant one nanosecond before the epoch. + /// For more on various different ways to represent time, see + /// https://tc39.es/proposal-temporal/docs/timezone.html + /// + /// [POSIX's Seconds Since the Epoch]: https://pubs.opengroup.org/onlinepubs/9699919799/xrat/V4_xbd_chap04.html#tag_21_04_16 + /// [Unix Time]: https://en.wikipedia.org/wiki/Unix_time + @since(version = 0.3.0-rc-2026-03-15) + record instant { + seconds: s64, + nanoseconds: u32, + } + + /// Read the current value of the clock. + /// + /// This clock is not monotonic, therefore calling this function repeatedly + /// will not necessarily produce a sequence of non-decreasing values. + /// + /// The nanoseconds field of the output is always less than 1000000000. + @since(version = 0.3.0-rc-2026-03-15) + now: func() -> instant; + + /// Query the resolution of the clock. Returns the smallest duration of time + /// that the implementation permits distinguishing. + @since(version = 0.3.0-rc-2026-03-15) + get-resolution: func() -> duration; +} + +@unstable(feature = clocks-timezone) +interface timezone { + @unstable(feature = clocks-timezone) + use system-clock.{instant}; + + /// Return the IANA identifier of the currently configured timezone. This + /// should be an identifier from the IANA Time Zone Database. + /// + /// For displaying to a user, the identifier should be converted into a + /// localized name by means of an internationalization API. + /// + /// If the implementation does not expose an actual timezone, or is unable + /// to provide mappings from times to deltas between the configured timezone + /// and UTC, or determining the current timezone fails, or the timezone does + /// not have an IANA identifier, this returns nothing. + @unstable(feature = clocks-timezone) + iana-id: func() -> option; + + /// The number of nanoseconds difference between UTC time and the local + /// time of the currently configured timezone, at the exact time of + /// `instant`. + /// + /// The magnitude of the returned value will always be less than + /// 86,400,000,000,000 which is the number of nanoseconds in a day + /// (24*60*60*1e9). + /// + /// If the implementation does not expose an actual timezone, or is unable + /// to provide mappings from times to deltas between the configured timezone + /// and UTC, or determining the current timezone fails, this returns + /// nothing. + @unstable(feature = clocks-timezone) + utc-offset: func(when: instant) -> option; + + /// Returns a string that is suitable to assist humans in debugging whether + /// any timezone is available, and if so, which. This may be the same string + /// as `iana-id`, or a formatted representation of the UTC offset such as + /// `-04:00`, or something else. + /// + /// WARNING: The returned string should not be consumed mechanically! It may + /// change across platforms, hosts, or other implementation details. Parsing + /// this string is a major platform-compatibility hazard. + @unstable(feature = clocks-timezone) + to-debug-string: func() -> string; +} + +@since(version = 0.3.0-rc-2026-03-15) +world imports { + @since(version = 0.3.0-rc-2026-03-15) + import types; + @since(version = 0.3.0-rc-2026-03-15) + import monotonic-clock; + @since(version = 0.3.0-rc-2026-03-15) + import system-clock; + @unstable(feature = clocks-timezone) + import timezone; +} diff --git a/examples/multiple-worlds/wit1/deps/filesystem.wit b/examples/multiple-worlds/wit1/deps/filesystem.wit new file mode 100644 index 0000000..697681f --- /dev/null +++ b/examples/multiple-worlds/wit1/deps/filesystem.wit @@ -0,0 +1,575 @@ +package wasi:filesystem@0.3.0-rc-2026-03-15; + +/// WASI filesystem is a filesystem API primarily intended to let users run WASI +/// programs that access their files on their existing filesystems, without +/// significant overhead. +/// +/// Paths are passed as interface-type `string`s, meaning they must consist of +/// a sequence of Unicode Scalar Values (USVs). Some filesystems may contain +/// paths which are not accessible by this API. +/// +/// The directory separator in WASI is always the forward-slash (`/`). +/// +/// All paths in WASI are relative paths, and are interpreted relative to a +/// `descriptor` referring to a base directory. If a `path` argument to any WASI +/// function starts with `/`, or if any step of resolving a `path`, including +/// `..` and symbolic link steps, reaches a directory outside of the base +/// directory, or reaches a symlink to an absolute or rooted path in the +/// underlying filesystem, the function fails with `error-code::not-permitted`. +/// +/// For more information about WASI path resolution and sandboxing, see +/// [WASI filesystem path resolution]. +/// +/// Though this package presents a portable interface modelled on POSIX, it +/// prioritizes compatibility over portability: allowing users to access their +/// files on their machine is more important than exposing a single semantics +/// across all platforms. Notably, depending on the underlying operating system +/// and file system: +/// * Paths may be case-folded or not. +/// * Deleting (unlinking) a file may fail if there are other file descriptors +/// open. +/// * Durability and atomicity of changes to underlying files when there are +/// concurrent writers. +/// +/// Users that need well-defined, portable semantics should use a key-value +/// store or a database instead. +/// +/// [WASI filesystem path resolution]: https://github.com/WebAssembly/wasi-filesystem/blob/main/path-resolution.md +@since(version = 0.3.0-rc-2026-03-15) +interface types { + @since(version = 0.3.0-rc-2026-03-15) + use wasi:clocks/system-clock@0.3.0-rc-2026-03-15.{instant}; + + /// File size or length of a region within a file. + @since(version = 0.3.0-rc-2026-03-15) + type filesize = u64; + + /// The type of a filesystem object referenced by a descriptor. + /// + /// Note: This was called `filetype` in earlier versions of WASI. + @since(version = 0.3.0-rc-2026-03-15) + variant descriptor-type { + /// The descriptor refers to a block device inode. + block-device, + /// The descriptor refers to a character device inode. + character-device, + /// The descriptor refers to a directory inode. + directory, + /// The descriptor refers to a named pipe. + fifo, + /// The file refers to a symbolic link inode. + symbolic-link, + /// The descriptor refers to a regular file inode. + regular-file, + /// The descriptor refers to a socket. + socket, + /// The type of the descriptor or file is different from any of the + /// other types specified. + other(option), + } + + /// Descriptor flags. + /// + /// Note: This was called `fdflags` in earlier versions of WASI. + @since(version = 0.3.0-rc-2026-03-15) + flags descriptor-flags { + /// Read mode: Data can be read. + read, + /// Write mode: Data can be written to. + write, + /// Request that writes be performed according to synchronized I/O file + /// integrity completion. The data stored in the file and the file's + /// metadata are synchronized. This is similar to `O_SYNC` in POSIX. + /// + /// The precise semantics of this operation have not yet been defined for + /// WASI. At this time, it should be interpreted as a request, and not a + /// requirement. + file-integrity-sync, + /// Request that writes be performed according to synchronized I/O data + /// integrity completion. Only the data stored in the file is + /// synchronized. This is similar to `O_DSYNC` in POSIX. + /// + /// The precise semantics of this operation have not yet been defined for + /// WASI. At this time, it should be interpreted as a request, and not a + /// requirement. + data-integrity-sync, + /// Requests that reads be performed at the same level of integrity + /// requested for writes. This is similar to `O_RSYNC` in POSIX. + /// + /// The precise semantics of this operation have not yet been defined for + /// WASI. At this time, it should be interpreted as a request, and not a + /// requirement. + requested-write-sync, + /// Mutating directories mode: Directory contents may be mutated. + /// + /// When this flag is unset on a descriptor, operations using the + /// descriptor which would create, rename, delete, modify the data or + /// metadata of filesystem objects, or obtain another handle which + /// would permit any of those, shall fail with `error-code::read-only` if + /// they would otherwise succeed. + /// + /// This may only be set on directories. + mutate-directory, + } + + /// Flags determining the method of how paths are resolved. + @since(version = 0.3.0-rc-2026-03-15) + flags path-flags { + /// As long as the resolved path corresponds to a symbolic link, it is + /// expanded. + symlink-follow, + } + + /// Open flags used by `open-at`. + @since(version = 0.3.0-rc-2026-03-15) + flags open-flags { + /// Create file if it does not exist, similar to `O_CREAT` in POSIX. + create, + /// Fail if not a directory, similar to `O_DIRECTORY` in POSIX. + directory, + /// Fail if file already exists, similar to `O_EXCL` in POSIX. + exclusive, + /// Truncate file to size 0, similar to `O_TRUNC` in POSIX. + truncate, + } + + /// Number of hard links to an inode. + @since(version = 0.3.0-rc-2026-03-15) + type link-count = u64; + + /// File attributes. + /// + /// Note: This was called `filestat` in earlier versions of WASI. + @since(version = 0.3.0-rc-2026-03-15) + record descriptor-stat { + /// File type. + %type: descriptor-type, + /// Number of hard links to the file. + link-count: link-count, + /// For regular files, the file size in bytes. For symbolic links, the + /// length in bytes of the pathname contained in the symbolic link. + size: filesize, + /// Last data access timestamp. + /// + /// If the `option` is none, the platform doesn't maintain an access + /// timestamp for this file. + data-access-timestamp: option, + /// Last data modification timestamp. + /// + /// If the `option` is none, the platform doesn't maintain a + /// modification timestamp for this file. + data-modification-timestamp: option, + /// Last file status-change timestamp. + /// + /// If the `option` is none, the platform doesn't maintain a + /// status-change timestamp for this file. + status-change-timestamp: option, + } + + /// When setting a timestamp, this gives the value to set it to. + @since(version = 0.3.0-rc-2026-03-15) + variant new-timestamp { + /// Leave the timestamp set to its previous value. + no-change, + /// Set the timestamp to the current time of the system clock associated + /// with the filesystem. + now, + /// Set the timestamp to the given value. + timestamp(instant), + } + + /// A directory entry. + @since(version = 0.3.0-rc-2026-03-15) + record directory-entry { + /// The type of the file referred to by this directory entry. + %type: descriptor-type, + /// The name of the object. + name: string, + } + + /// Error codes returned by functions, similar to `errno` in POSIX. + /// Not all of these error codes are returned by the functions provided by this + /// API; some are used in higher-level library layers, and others are provided + /// merely for alignment with POSIX. + @since(version = 0.3.0-rc-2026-03-15) + variant error-code { + /// Permission denied, similar to `EACCES` in POSIX. + access, + /// Connection already in progress, similar to `EALREADY` in POSIX. + already, + /// Bad descriptor, similar to `EBADF` in POSIX. + bad-descriptor, + /// Device or resource busy, similar to `EBUSY` in POSIX. + busy, + /// Resource deadlock would occur, similar to `EDEADLK` in POSIX. + deadlock, + /// Storage quota exceeded, similar to `EDQUOT` in POSIX. + quota, + /// File exists, similar to `EEXIST` in POSIX. + exist, + /// File too large, similar to `EFBIG` in POSIX. + file-too-large, + /// Illegal byte sequence, similar to `EILSEQ` in POSIX. + illegal-byte-sequence, + /// Operation in progress, similar to `EINPROGRESS` in POSIX. + in-progress, + /// Interrupted function, similar to `EINTR` in POSIX. + interrupted, + /// Invalid argument, similar to `EINVAL` in POSIX. + invalid, + /// I/O error, similar to `EIO` in POSIX. + io, + /// Is a directory, similar to `EISDIR` in POSIX. + is-directory, + /// Too many levels of symbolic links, similar to `ELOOP` in POSIX. + loop, + /// Too many links, similar to `EMLINK` in POSIX. + too-many-links, + /// Message too large, similar to `EMSGSIZE` in POSIX. + message-size, + /// Filename too long, similar to `ENAMETOOLONG` in POSIX. + name-too-long, + /// No such device, similar to `ENODEV` in POSIX. + no-device, + /// No such file or directory, similar to `ENOENT` in POSIX. + no-entry, + /// No locks available, similar to `ENOLCK` in POSIX. + no-lock, + /// Not enough space, similar to `ENOMEM` in POSIX. + insufficient-memory, + /// No space left on device, similar to `ENOSPC` in POSIX. + insufficient-space, + /// Not a directory or a symbolic link to a directory, similar to `ENOTDIR` in POSIX. + not-directory, + /// Directory not empty, similar to `ENOTEMPTY` in POSIX. + not-empty, + /// State not recoverable, similar to `ENOTRECOVERABLE` in POSIX. + not-recoverable, + /// Not supported, similar to `ENOTSUP` and `ENOSYS` in POSIX. + unsupported, + /// Inappropriate I/O control operation, similar to `ENOTTY` in POSIX. + no-tty, + /// No such device or address, similar to `ENXIO` in POSIX. + no-such-device, + /// Value too large to be stored in data type, similar to `EOVERFLOW` in POSIX. + overflow, + /// Operation not permitted, similar to `EPERM` in POSIX. + not-permitted, + /// Broken pipe, similar to `EPIPE` in POSIX. + pipe, + /// Read-only file system, similar to `EROFS` in POSIX. + read-only, + /// Invalid seek, similar to `ESPIPE` in POSIX. + invalid-seek, + /// Text file busy, similar to `ETXTBSY` in POSIX. + text-file-busy, + /// Cross-device link, similar to `EXDEV` in POSIX. + cross-device, + /// A catch-all for errors not captured by the existing variants. + /// Implementations can use this to extend the error type without + /// breaking existing code. + other(option), + } + + /// File or memory access pattern advisory information. + @since(version = 0.3.0-rc-2026-03-15) + enum advice { + /// The application has no advice to give on its behavior with respect + /// to the specified data. + normal, + /// The application expects to access the specified data sequentially + /// from lower offsets to higher offsets. + sequential, + /// The application expects to access the specified data in a random + /// order. + random, + /// The application expects to access the specified data in the near + /// future. + will-need, + /// The application expects that it will not access the specified data + /// in the near future. + dont-need, + /// The application expects to access the specified data once and then + /// not reuse it thereafter. + no-reuse, + } + + /// A 128-bit hash value, split into parts because wasm doesn't have a + /// 128-bit integer type. + @since(version = 0.3.0-rc-2026-03-15) + record metadata-hash-value { + /// 64 bits of a 128-bit hash value. + lower: u64, + /// Another 64 bits of a 128-bit hash value. + upper: u64, + } + + /// A descriptor is a reference to a filesystem object, which may be a file, + /// directory, named pipe, special file, or other object on which filesystem + /// calls may be made. + @since(version = 0.3.0-rc-2026-03-15) + resource descriptor { + /// Return a stream for reading from a file. + /// + /// Multiple read, write, and append streams may be active on the same open + /// file and they do not interfere with each other. + /// + /// This function returns a `stream` which provides the data received from the + /// file, and a `future` providing additional error information in case an + /// error is encountered. + /// + /// If no error is encountered, `stream.read` on the `stream` will return + /// `read-status::closed` with no `error-context` and the future resolves to + /// the value `ok`. If an error is encountered, `stream.read` on the + /// `stream` returns `read-status::closed` with an `error-context` and the future + /// resolves to `err` with an `error-code`. + /// + /// Note: This is similar to `pread` in POSIX. + @since(version = 0.3.0-rc-2026-03-15) + read-via-stream: func(offset: filesize) -> tuple, future>>; + /// Return a stream for writing to a file, if available. + /// + /// May fail with an error-code describing why the file cannot be written. + /// + /// It is valid to write past the end of a file; the file is extended to the + /// extent of the write, with bytes between the previous end and the start of + /// the write set to zero. + /// + /// This function returns once either full contents of the stream are + /// written or an error is encountered. + /// + /// Note: This is similar to `pwrite` in POSIX. + @since(version = 0.3.0-rc-2026-03-15) + write-via-stream: func(data: stream, offset: filesize) -> future>; + /// Return a stream for appending to a file, if available. + /// + /// May fail with an error-code describing why the file cannot be appended. + /// + /// This function returns once either full contents of the stream are + /// written or an error is encountered. + /// + /// Note: This is similar to `write` with `O_APPEND` in POSIX. + @since(version = 0.3.0-rc-2026-03-15) + append-via-stream: func(data: stream) -> future>; + /// Provide file advisory information on a descriptor. + /// + /// This is similar to `posix_fadvise` in POSIX. + @since(version = 0.3.0-rc-2026-03-15) + advise: async func(offset: filesize, length: filesize, advice: advice) -> result<_, error-code>; + /// Synchronize the data of a file to disk. + /// + /// This function succeeds with no effect if the file descriptor is not + /// opened for writing. + /// + /// Note: This is similar to `fdatasync` in POSIX. + @since(version = 0.3.0-rc-2026-03-15) + sync-data: async func() -> result<_, error-code>; + /// Get flags associated with a descriptor. + /// + /// Note: This returns similar flags to `fcntl(fd, F_GETFL)` in POSIX. + /// + /// Note: This returns the value that was the `fs_flags` value returned + /// from `fdstat_get` in earlier versions of WASI. + @since(version = 0.3.0-rc-2026-03-15) + get-flags: async func() -> result; + /// Get the dynamic type of a descriptor. + /// + /// Note: This returns the same value as the `type` field of the `fd-stat` + /// returned by `stat`, `stat-at` and similar. + /// + /// Note: This returns similar flags to the `st_mode & S_IFMT` value provided + /// by `fstat` in POSIX. + /// + /// Note: This returns the value that was the `fs_filetype` value returned + /// from `fdstat_get` in earlier versions of WASI. + @since(version = 0.3.0-rc-2026-03-15) + get-type: async func() -> result; + /// Adjust the size of an open file. If this increases the file's size, the + /// extra bytes are filled with zeros. + /// + /// Note: This was called `fd_filestat_set_size` in earlier versions of WASI. + @since(version = 0.3.0-rc-2026-03-15) + set-size: async func(size: filesize) -> result<_, error-code>; + /// Adjust the timestamps of an open file or directory. + /// + /// Note: This is similar to `futimens` in POSIX. + /// + /// Note: This was called `fd_filestat_set_times` in earlier versions of WASI. + @since(version = 0.3.0-rc-2026-03-15) + set-times: async func(data-access-timestamp: new-timestamp, data-modification-timestamp: new-timestamp) -> result<_, error-code>; + /// Read directory entries from a directory. + /// + /// On filesystems where directories contain entries referring to themselves + /// and their parents, often named `.` and `..` respectively, these entries + /// are omitted. + /// + /// This always returns a new stream which starts at the beginning of the + /// directory. Multiple streams may be active on the same directory, and they + /// do not interfere with each other. + /// + /// This function returns a future, which will resolve to an error code if + /// reading full contents of the directory fails. + @since(version = 0.3.0-rc-2026-03-15) + read-directory: func() -> tuple, future>>; + /// Synchronize the data and metadata of a file to disk. + /// + /// This function succeeds with no effect if the file descriptor is not + /// opened for writing. + /// + /// Note: This is similar to `fsync` in POSIX. + @since(version = 0.3.0-rc-2026-03-15) + sync: async func() -> result<_, error-code>; + /// Create a directory. + /// + /// Note: This is similar to `mkdirat` in POSIX. + @since(version = 0.3.0-rc-2026-03-15) + create-directory-at: async func(path: string) -> result<_, error-code>; + /// Return the attributes of an open file or directory. + /// + /// Note: This is similar to `fstat` in POSIX, except that it does not return + /// device and inode information. For testing whether two descriptors refer to + /// the same underlying filesystem object, use `is-same-object`. To obtain + /// additional data that can be used do determine whether a file has been + /// modified, use `metadata-hash`. + /// + /// Note: This was called `fd_filestat_get` in earlier versions of WASI. + @since(version = 0.3.0-rc-2026-03-15) + stat: async func() -> result; + /// Return the attributes of a file or directory. + /// + /// Note: This is similar to `fstatat` in POSIX, except that it does not + /// return device and inode information. See the `stat` description for a + /// discussion of alternatives. + /// + /// Note: This was called `path_filestat_get` in earlier versions of WASI. + @since(version = 0.3.0-rc-2026-03-15) + stat-at: async func(path-flags: path-flags, path: string) -> result; + /// Adjust the timestamps of a file or directory. + /// + /// Note: This is similar to `utimensat` in POSIX. + /// + /// Note: This was called `path_filestat_set_times` in earlier versions of + /// WASI. + @since(version = 0.3.0-rc-2026-03-15) + set-times-at: async func(path-flags: path-flags, path: string, data-access-timestamp: new-timestamp, data-modification-timestamp: new-timestamp) -> result<_, error-code>; + /// Create a hard link. + /// + /// Fails with `error-code::no-entry` if the old path does not exist, + /// with `error-code::exist` if the new path already exists, and + /// `error-code::not-permitted` if the old path is not a file. + /// + /// Note: This is similar to `linkat` in POSIX. + @since(version = 0.3.0-rc-2026-03-15) + link-at: async func(old-path-flags: path-flags, old-path: string, new-descriptor: borrow, new-path: string) -> result<_, error-code>; + /// Open a file or directory. + /// + /// If `flags` contains `descriptor-flags::mutate-directory`, and the base + /// descriptor doesn't have `descriptor-flags::mutate-directory` set, + /// `open-at` fails with `error-code::read-only`. + /// + /// If `flags` contains `write` or `mutate-directory`, or `open-flags` + /// contains `truncate` or `create`, and the base descriptor doesn't have + /// `descriptor-flags::mutate-directory` set, `open-at` fails with + /// `error-code::read-only`. + /// + /// Note: This is similar to `openat` in POSIX. + @since(version = 0.3.0-rc-2026-03-15) + open-at: async func(path-flags: path-flags, path: string, open-flags: open-flags, %flags: descriptor-flags) -> result; + /// Read the contents of a symbolic link. + /// + /// If the contents contain an absolute or rooted path in the underlying + /// filesystem, this function fails with `error-code::not-permitted`. + /// + /// Note: This is similar to `readlinkat` in POSIX. + @since(version = 0.3.0-rc-2026-03-15) + readlink-at: async func(path: string) -> result; + /// Remove a directory. + /// + /// Return `error-code::not-empty` if the directory is not empty. + /// + /// Note: This is similar to `unlinkat(fd, path, AT_REMOVEDIR)` in POSIX. + @since(version = 0.3.0-rc-2026-03-15) + remove-directory-at: async func(path: string) -> result<_, error-code>; + /// Rename a filesystem object. + /// + /// Note: This is similar to `renameat` in POSIX. + @since(version = 0.3.0-rc-2026-03-15) + rename-at: async func(old-path: string, new-descriptor: borrow, new-path: string) -> result<_, error-code>; + /// Create a symbolic link (also known as a "symlink"). + /// + /// If `old-path` starts with `/`, the function fails with + /// `error-code::not-permitted`. + /// + /// Note: This is similar to `symlinkat` in POSIX. + @since(version = 0.3.0-rc-2026-03-15) + symlink-at: async func(old-path: string, new-path: string) -> result<_, error-code>; + /// Unlink a filesystem object that is not a directory. + /// + /// This is similar to `unlinkat(fd, path, 0)` in POSIX. + /// + /// Error returns are as specified by POSIX. + /// + /// If the filesystem object is a directory, `error-code::access` or + /// `error-code::is-directory` may be returned instead of the + /// POSIX-specified `error-code::not-permitted`. + @since(version = 0.3.0-rc-2026-03-15) + unlink-file-at: async func(path: string) -> result<_, error-code>; + /// Test whether two descriptors refer to the same filesystem object. + /// + /// In POSIX, this corresponds to testing whether the two descriptors have the + /// same device (`st_dev`) and inode (`st_ino` or `d_ino`) numbers. + /// wasi-filesystem does not expose device and inode numbers, so this function + /// may be used instead. + @since(version = 0.3.0-rc-2026-03-15) + is-same-object: async func(other: borrow) -> bool; + /// Return a hash of the metadata associated with a filesystem object referred + /// to by a descriptor. + /// + /// This returns a hash of the last-modification timestamp and file size, and + /// may also include the inode number, device number, birth timestamp, and + /// other metadata fields that may change when the file is modified or + /// replaced. It may also include a secret value chosen by the + /// implementation and not otherwise exposed. + /// + /// Implementations are encouraged to provide the following properties: + /// + /// - If the file is not modified or replaced, the computed hash value should + /// usually not change. + /// - If the object is modified or replaced, the computed hash value should + /// usually change. + /// - The inputs to the hash should not be easily computable from the + /// computed hash. + /// + /// However, none of these is required. + @since(version = 0.3.0-rc-2026-03-15) + metadata-hash: async func() -> result; + /// Return a hash of the metadata associated with a filesystem object referred + /// to by a directory descriptor and a relative path. + /// + /// This performs the same hash computation as `metadata-hash`. + @since(version = 0.3.0-rc-2026-03-15) + metadata-hash-at: async func(path-flags: path-flags, path: string) -> result; + } +} + +@since(version = 0.3.0-rc-2026-03-15) +interface preopens { + @since(version = 0.3.0-rc-2026-03-15) + use types.{descriptor}; + + /// Return the set of preopened directories, and their paths. + @since(version = 0.3.0-rc-2026-03-15) + get-directories: func() -> list>; +} + +@since(version = 0.3.0-rc-2026-03-15) +world imports { + @since(version = 0.3.0-rc-2026-03-15) + import wasi:clocks/types@0.3.0-rc-2026-03-15; + @since(version = 0.3.0-rc-2026-03-15) + import wasi:clocks/system-clock@0.3.0-rc-2026-03-15; + @since(version = 0.3.0-rc-2026-03-15) + import types; + @since(version = 0.3.0-rc-2026-03-15) + import preopens; +} diff --git a/examples/wasip3/wit/deps/http/types.wit b/examples/multiple-worlds/wit1/deps/http.wit similarity index 70% rename from examples/wasip3/wit/deps/http/types.wit rename to examples/multiple-worlds/wit1/deps/http.wit index 8269eea..c1c1e68 100644 --- a/examples/wasip3/wit/deps/http/types.wit +++ b/examples/multiple-worlds/wit1/deps/http.wit @@ -1,9 +1,13 @@ +package wasi:http@0.3.0-rc-2026-03-15; + /// This interface defines all of the types and methods for implementing HTTP /// Requests and Responses, as well as their headers, trailers, and bodies. +@since(version = 0.3.0-rc-2026-03-15) interface types { - use wasi:clocks/monotonic-clock@0.3.0-rc-2025-09-16.{duration}; + use wasi:clocks/types@0.3.0-rc-2026-03-15.{duration}; /// This type corresponds to HTTP standard Methods. + @since(version = 0.3.0-rc-2026-03-15) variant method { get, head, @@ -14,18 +18,41 @@ interface types { options, trace, patch, - other(string) + other(string), } /// This type corresponds to HTTP standard Related Schemes. + @since(version = 0.3.0-rc-2026-03-15) variant scheme { HTTP, HTTPS, - other(string) + other(string), + } + + /// Defines the case payload type for `DNS-error` above: + @since(version = 0.3.0-rc-2026-03-15) + record DNS-error-payload { + rcode: option, + info-code: option, + } + + /// Defines the case payload type for `TLS-alert-received` above: + @since(version = 0.3.0-rc-2026-03-15) + record TLS-alert-received-payload { + alert-id: option, + alert-message: option, + } + + /// Defines the case payload type for `HTTP-response-{header,trailer}-size` above: + @since(version = 0.3.0-rc-2026-03-15) + record field-size-payload { + field-name: option, + field-size: option, } /// These cases are inspired by the IANA HTTP Proxy Error Types: /// + @since(version = 0.3.0-rc-2026-03-15) variant error-code { DNS-timeout, DNS-error(DNS-error-payload), @@ -70,64 +97,66 @@ interface types { /// unstructured description of the error. Users should not depend on the /// string for diagnosing errors, as it's not required to be consistent /// between implementations. - internal-error(option) - } - - /// Defines the case payload type for `DNS-error` above: - record DNS-error-payload { - rcode: option, - info-code: option - } - - /// Defines the case payload type for `TLS-alert-received` above: - record TLS-alert-received-payload { - alert-id: option, - alert-message: option - } - - /// Defines the case payload type for `HTTP-response-{header,trailer}-size` above: - record field-size-payload { - field-name: option, - field-size: option + internal-error(option), } /// This type enumerates the different kinds of errors that may occur when /// setting or appending to a `fields` resource. + @since(version = 0.3.0-rc-2026-03-15) variant header-error { /// This error indicates that a `field-name` or `field-value` was /// syntactically invalid when used with an operation that sets headers in a /// `fields`. invalid-syntax, - /// This error indicates that a forbidden `field-name` was used when trying /// to set a header in a `fields`. forbidden, - /// This error indicates that the operation on the `fields` was not /// permitted because the fields are immutable. immutable, + /// This error indicates that the operation would exceed an + /// implementation-defined limit on field sizes. This may apply to + /// an individual `field-value`, a single `field-name` plus all its + /// values, or the total aggregate size of all fields. + size-exceeded, + /// This is a catch-all error for anything that doesn't fit cleanly into a + /// more specific case. Implementations can use this to extend the error + /// type without breaking existing code. It also includes an optional + /// string for an unstructured description of the error. Users should not + /// depend on the string for diagnosing errors, as it's not required to be + /// consistent between implementations. + other(option), } /// This type enumerates the different kinds of errors that may occur when /// setting fields of a `request-options` resource. + @since(version = 0.3.0-rc-2026-03-15) variant request-options-error { /// Indicates the specified field is not supported by this implementation. not-supported, - /// Indicates that the operation on the `request-options` was not permitted /// because it is immutable. immutable, + /// This is a catch-all error for anything that doesn't fit cleanly into a + /// more specific case. Implementations can use this to extend the error + /// type without breaking existing code. It also includes an optional + /// string for an unstructured description of the error. Users should not + /// depend on the string for diagnosing errors, as it's not required to be + /// consistent between implementations. + other(option), } /// Field names are always strings. /// /// Field names should always be treated as case insensitive by the `fields` /// resource for the purposes of equality checking. + @since(version = 0.3.0-rc-2026-03-15) type field-name = string; /// Field values should always be ASCII strings. However, in /// reality, HTTP implementations often have to interpret malformed values, /// so they are provided as a list of bytes. + @since(version = 0.3.0-rc-2026-03-15) type field-value = list; /// This following block defines the `fields` resource which corresponds to @@ -145,13 +174,16 @@ interface types { /// original casing used to construct or mutate the `fields` resource. The `fields` /// resource should use that original casing when serializing the fields for /// transport or when returning them from a method. + /// + /// Implementations may impose limits on individual field values and on total + /// aggregate field section size. Operations that would exceed these limits + /// fail with `header-error.size-exceeded` + @since(version = 0.3.0-rc-2026-03-15) resource fields { - /// Construct an empty HTTP Fields. /// /// The resulting `fields` is mutable. constructor(); - /// Construct an HTTP Fields. /// /// The resulting `fields` is mutable. @@ -166,33 +198,30 @@ interface types { /// well-formed, so they are represented as a raw list of bytes. /// /// An error result will be returned if any header or value was - /// syntactically invalid, or if a header was forbidden. - from-list: static func( - entries: list> - ) -> result; - + /// syntactically invalid, if a header was forbidden, or if the + /// entries would exceed an implementation size limit. + from-list: static func(entries: list>) -> result; /// Get all of the values corresponding to a name. If the name is not present /// in this `fields`, an empty list is returned. However, if the name is /// present but empty, this is represented by a list with one or more /// empty field-values present. get: func(name: field-name) -> list; - /// Returns `true` when the name is present in this `fields`. If the name is /// syntactically invalid, `false` is returned. has: func(name: field-name) -> bool; - /// Set all of the values for a name. Clears any existing values for that /// name, if they have been set. /// /// Fails with `header-error.immutable` if the `fields` are immutable. + /// + /// Fails with `header-error.size-exceeded` if the name or values would + /// exceed an implementation-defined size limit. set: func(name: field-name, value: list) -> result<_, header-error>; - /// Delete all values for a name. Does nothing if no values for the name /// exist. /// /// Fails with `header-error.immutable` if the `fields` are immutable. delete: func(name: field-name) -> result<_, header-error>; - /// Delete all values for a name. Does nothing if no values for the name /// exist. /// @@ -200,13 +229,14 @@ interface types { /// /// Fails with `header-error.immutable` if the `fields` are immutable. get-and-delete: func(name: field-name) -> result, header-error>; - /// Append a value for a name. Does not change or delete any existing /// values for that name. /// /// Fails with `header-error.immutable` if the `fields` are immutable. + /// + /// Fails with `header-error.size-exceeded` if the value would exceed + /// an implementation-defined size limit. append: func(name: field-name, value: field-value) -> result<_, header-error>; - /// Retrieve the full set of names and values in the Fields. Like the /// constructor, the list represents each name-value pair. /// @@ -216,8 +246,7 @@ interface types { /// /// The names and values are always returned in the original casing and in /// the order in which they will be serialized for transport. - copy-all: func() -> list>; - + copy-all: func() -> list>; /// Make a deep copy of the Fields. Equivalent in behavior to calling the /// `fields` constructor on the return value of `copy-all`. The resulting /// `fields` is mutable. @@ -225,14 +254,16 @@ interface types { } /// Headers is an alias for Fields. + @since(version = 0.3.0-rc-2026-03-15) type headers = fields; /// Trailers is an alias for Fields. + @since(version = 0.3.0-rc-2026-03-15) type trailers = fields; /// Represents an HTTP Request. + @since(version = 0.3.0-rc-2026-03-15) resource request { - /// Construct a new `request` with a default `method` of `GET`, and /// `none` values for `path-with-query`, `scheme`, and `authority`. /// @@ -254,19 +285,12 @@ interface types { /// to reject invalid constructions of `request`. /// /// The returned future resolves to result of transmission of this request. - new: static func( - headers: headers, - contents: option>, - trailers: future, error-code>>, - options: option - ) -> tuple>>; - + new: static func(headers: headers, contents: option>, trailers: future, error-code>>, options: option) -> tuple>>; /// Get the Method for the Request. get-method: func() -> method; /// Set the Method for the Request. Fails if the string present in a /// `method.other` argument is not a syntactically valid method. set-method: func(method: method) -> result; - /// Get the combination of the HTTP Path and Query for the Request. When /// `none`, this represents an empty Path and empty Query. get-path-with-query: func() -> option; @@ -274,7 +298,6 @@ interface types { /// `none`, this represents an empty Path and empty Query. Fails is the /// string given is not a syntactically valid path and query uri component. set-path-with-query: func(path-with-query: option) -> result; - /// Get the HTTP Related Scheme for the Request. When `none`, the /// implementation may choose an appropriate default scheme. get-scheme: func() -> option; @@ -282,7 +305,6 @@ interface types { /// implementation may choose an appropriate default scheme. Fails if the /// string given is not a syntactically valid uri scheme. set-scheme: func(scheme: option) -> result; - /// Get the authority of the Request's target URI. A value of `none` may be used /// with Related Schemes which do not require an authority. The HTTP and /// HTTPS schemes always require an authority. @@ -292,7 +314,6 @@ interface types { /// HTTPS schemes always require an authority. Fails if the string given is /// not a syntactically valid URI authority. set-authority: func(authority: option) -> result; - /// Get the `request-options` to be associated with this request /// /// The returned `request-options` resource is immutable: `set-*` operations @@ -302,13 +323,11 @@ interface types { /// the parent `request` is dropped, or its ownership is transferred to /// another component by e.g. `handler.handle`. get-options: func() -> option; - /// Get the headers associated with the Request. /// /// The returned `headers` resource is immutable: `set`, `append`, and /// `delete` operations will fail with `header-error.immutable`. get-headers: func() -> headers; - /// Get body of the Request. /// /// Stream returned by this method represents the contents of the body. @@ -330,46 +349,41 @@ interface types { /// /// These timeouts are separate from any the user may use to bound an /// asynchronous call. + @since(version = 0.3.0-rc-2026-03-15) resource request-options { /// Construct a default `request-options` value. constructor(); - /// The timeout for the initial connect to the HTTP Server. get-connect-timeout: func() -> option; - /// Set the timeout for the initial connect to the HTTP Server. An error /// return value indicates that this timeout is not supported or that this /// handle is immutable. set-connect-timeout: func(duration: option) -> result<_, request-options-error>; - /// The timeout for receiving the first byte of the Response body. get-first-byte-timeout: func() -> option; - /// Set the timeout for receiving the first byte of the Response body. An /// error return value indicates that this timeout is not supported or that /// this handle is immutable. set-first-byte-timeout: func(duration: option) -> result<_, request-options-error>; - /// The timeout for receiving subsequent chunks of bytes in the Response /// body stream. get-between-bytes-timeout: func() -> option; - /// Set the timeout for receiving subsequent chunks of bytes in the Response /// body stream. An error return value indicates that this timeout is not /// supported or that this handle is immutable. set-between-bytes-timeout: func(duration: option) -> result<_, request-options-error>; - /// Make a deep copy of the `request-options`. /// The resulting `request-options` is mutable. clone: func() -> request-options; } /// This type corresponds to the HTTP standard Status Code. + @since(version = 0.3.0-rc-2026-03-15) type status-code = u16; /// Represents an HTTP Response. + @since(version = 0.3.0-rc-2026-03-15) resource response { - /// Construct a new `response`, with a default `status-code` of `200`. /// If a different `status-code` is needed, it must be set via the /// `set-status-code` method. @@ -383,25 +397,17 @@ interface types { /// will be closed immediately. /// /// The returned future resolves to result of transmission of this response. - new: static func( - headers: headers, - contents: option>, - trailers: future, error-code>>, - ) -> tuple>>; - + new: static func(headers: headers, contents: option>, trailers: future, error-code>>) -> tuple>>; /// Get the HTTP Status Code for the Response. get-status-code: func() -> status-code; - /// Set the HTTP Status Code for the Response. Fails if the status-code /// given is not a valid http status code. set-status-code: func(status-code: status-code) -> result; - /// Get the headers associated with the Response. /// /// The returned `headers` resource is immutable: `set`, `append`, and /// `delete` operations will fail with `header-error.immutable`. get-headers: func() -> headers; - /// Get body of the Response. /// /// Stream returned by this method represents the contents of the body. @@ -417,3 +423,87 @@ interface types { consume-body: static func(this: response, res: future>) -> tuple, future, error-code>>>; } } + +/// This interface defines a handler of HTTP Requests. +/// +/// In a `wasi:http/service` this interface is exported to respond to an +/// incoming HTTP Request with a Response. +/// +/// In `wasi:http/middleware` this interface is both exported and imported as +/// the "downstream" and "upstream" directions of the middleware chain. +@since(version = 0.3.0-rc-2026-03-15) +interface handler { + use types.{request, response, error-code}; + + /// This function may be called with either an incoming request read from the + /// network or a request synthesized or forwarded by another component. + handle: async func(request: request) -> result; +} + +/// This interface defines an HTTP client for sending "outgoing" requests. +/// +/// Most components are expected to import this interface to provide the +/// capability to send HTTP requests to arbitrary destinations on a network. +/// +/// The type signature of `client.send` is the same as `handler.handle`. This +/// duplication is currently necessary because some Component Model tooling +/// (including WIT itself) is unable to represent a component importing two +/// instances of the same interface. A `client.send` import may be linked +/// directly to a `handler.handle` export to bypass the network. +@since(version = 0.3.0-rc-2026-03-15) +interface client { + use types.{request, response, error-code}; + + /// This function may be used to either send an outgoing request over the + /// network or to forward it to another component. + send: async func(request: request) -> result; +} + +/// The `wasi:http/service` world captures a broad category of HTTP services +/// including web applications, API servers, and proxies. It may be `include`d +/// in more specific worlds such as `wasi:http/middleware`. +@since(version = 0.3.0-rc-2026-03-15) +world service { + import wasi:cli/types@0.3.0-rc-2026-03-15; + import wasi:cli/stdout@0.3.0-rc-2026-03-15; + import wasi:cli/stderr@0.3.0-rc-2026-03-15; + import wasi:cli/stdin@0.3.0-rc-2026-03-15; + import wasi:clocks/types@0.3.0-rc-2026-03-15; + import types; + import client; + import wasi:clocks/monotonic-clock@0.3.0-rc-2026-03-15; + import wasi:clocks/system-clock@0.3.0-rc-2026-03-15; + @unstable(feature = clocks-timezone) + import wasi:clocks/timezone@0.3.0-rc-2026-03-15; + import wasi:random/random@0.3.0-rc-2026-03-15; + import wasi:random/insecure@0.3.0-rc-2026-03-15; + import wasi:random/insecure-seed@0.3.0-rc-2026-03-15; + + export handler; +} +/// The `wasi:http/middleware` world captures HTTP services that forward HTTP +/// Requests to another handler. +/// +/// Components may implement this world to allow them to participate in handler +/// "chains" where a `request` flows through handlers on its way to some terminal +/// `service` and corresponding `response` flows in the opposite direction. +@since(version = 0.3.0-rc-2026-03-15) +world middleware { + import wasi:clocks/types@0.3.0-rc-2026-03-15; + import types; + import handler; + import wasi:cli/types@0.3.0-rc-2026-03-15; + import wasi:cli/stdout@0.3.0-rc-2026-03-15; + import wasi:cli/stderr@0.3.0-rc-2026-03-15; + import wasi:cli/stdin@0.3.0-rc-2026-03-15; + import client; + import wasi:clocks/monotonic-clock@0.3.0-rc-2026-03-15; + import wasi:clocks/system-clock@0.3.0-rc-2026-03-15; + @unstable(feature = clocks-timezone) + import wasi:clocks/timezone@0.3.0-rc-2026-03-15; + import wasi:random/random@0.3.0-rc-2026-03-15; + import wasi:random/insecure@0.3.0-rc-2026-03-15; + import wasi:random/insecure-seed@0.3.0-rc-2026-03-15; + + export handler; +} diff --git a/examples/multiple-worlds/wit1/deps/random.wit b/examples/multiple-worlds/wit1/deps/random.wit new file mode 100644 index 0000000..026f44a --- /dev/null +++ b/examples/multiple-worlds/wit1/deps/random.wit @@ -0,0 +1,107 @@ +package wasi:random@0.3.0-rc-2026-03-15; + +/// The insecure-seed interface for seeding hash-map DoS resistance. +/// +/// It is intended to be portable at least between Unix-family platforms and +/// Windows. +@since(version = 0.3.0-rc-2026-03-15) +interface insecure-seed { + /// Return a 128-bit value that may contain a pseudo-random value. + /// + /// The returned value is not required to be computed from a CSPRNG, and may + /// even be entirely deterministic. Host implementations are encouraged to + /// provide pseudo-random values to any program exposed to + /// attacker-controlled content, to enable DoS protection built into many + /// languages' hash-map implementations. + /// + /// This function is intended to only be called once, by a source language + /// to initialize Denial Of Service (DoS) protection in its hash-map + /// implementation. + /// + /// # Expected future evolution + /// + /// This will likely be changed to a value import, to prevent it from being + /// called multiple times and potentially used for purposes other than DoS + /// protection. + @since(version = 0.3.0-rc-2026-03-15) + get-insecure-seed: func() -> tuple; +} + +/// The insecure interface for insecure pseudo-random numbers. +/// +/// It is intended to be portable at least between Unix-family platforms and +/// Windows. +@since(version = 0.3.0-rc-2026-03-15) +interface insecure { + /// Return up to `max-len` insecure pseudo-random bytes. + /// + /// This function is not cryptographically secure. Do not use it for + /// anything related to security. + /// + /// There are no requirements on the values of the returned bytes, however + /// implementations are encouraged to return evenly distributed values with + /// a long period. + /// + /// Implementations MAY return fewer bytes than requested (a short read). + /// Callers that require exactly `max-len` bytes MUST call this function in + /// a loop until the desired number of bytes has been accumulated. + /// Implementations MUST return at least 1 byte when `max-len` is greater + /// than zero. When `max-len` is zero, implementations MUST return an empty + /// list without trapping. + @since(version = 0.3.0-rc-2026-03-15) + get-insecure-random-bytes: func(max-len: u64) -> list; + + /// Return an insecure pseudo-random `u64` value. + /// + /// This function returns the same type of pseudo-random data as + /// `get-insecure-random-bytes`, represented as a `u64`. + @since(version = 0.3.0-rc-2026-03-15) + get-insecure-random-u64: func() -> u64; +} + +/// WASI Random is a random data API. +/// +/// It is intended to be portable at least between Unix-family platforms and +/// Windows. +@since(version = 0.3.0-rc-2026-03-15) +interface random { + /// Return up to `max-len` cryptographically-secure random or pseudo-random + /// bytes. + /// + /// This function must produce data at least as cryptographically secure and + /// fast as an adequately seeded cryptographically-secure pseudo-random + /// number generator (CSPRNG). It must not block, from the perspective of + /// the calling program, under any circumstances, including on the first + /// request and on requests for numbers of bytes. The returned data must + /// always be unpredictable. + /// + /// Implementations MAY return fewer bytes than requested (a short read). + /// Callers that require exactly `max-len` bytes MUST call this function in + /// a loop until the desired number of bytes has been accumulated. + /// Implementations MUST return at least 1 byte when `max-len` is greater + /// than zero. When `max-len` is zero, implementations MUST return an empty + /// list without trapping. + /// + /// This function must always return fresh data. Deterministic environments + /// must omit this function, rather than implementing it with deterministic + /// data. + @since(version = 0.3.0-rc-2026-03-15) + get-random-bytes: func(max-len: u64) -> list; + + /// Return a cryptographically-secure random or pseudo-random `u64` value. + /// + /// This function returns the same type of data as `get-random-bytes`, + /// represented as a `u64`. + @since(version = 0.3.0-rc-2026-03-15) + get-random-u64: func() -> u64; +} + +@since(version = 0.3.0-rc-2026-03-15) +world imports { + @since(version = 0.3.0-rc-2026-03-15) + import random; + @since(version = 0.3.0-rc-2026-03-15) + import insecure; + @since(version = 0.3.0-rc-2026-03-15) + import insecure-seed; +} diff --git a/examples/multiple-worlds/wit1/deps/sockets.wit b/examples/multiple-worlds/wit1/deps/sockets.wit new file mode 100644 index 0000000..cde2e4d --- /dev/null +++ b/examples/multiple-worlds/wit1/deps/sockets.wit @@ -0,0 +1,839 @@ +package wasi:sockets@0.3.0-rc-2026-03-15; + +@since(version = 0.3.0-rc-2026-03-15) +interface types { + @since(version = 0.3.0-rc-2026-03-15) + use wasi:clocks/types@0.3.0-rc-2026-03-15.{duration}; + + /// Error codes. + /// + /// In theory, every API can return any error code. + /// In practice, API's typically only return the errors documented per API + /// combined with a couple of errors that are always possible: + /// - `other` + /// - `access-denied` + /// - `not-supported` + /// - `out-of-memory` + /// + /// See each individual API for what the POSIX equivalents are. They sometimes differ per API. + @since(version = 0.3.0-rc-2026-03-15) + variant error-code { + /// Access denied. + /// + /// POSIX equivalent: EACCES, EPERM + access-denied, + /// The operation is not supported. + /// + /// POSIX equivalent: EOPNOTSUPP, ENOPROTOOPT, EPFNOSUPPORT, EPROTONOSUPPORT, ESOCKTNOSUPPORT + not-supported, + /// One of the arguments is invalid. + /// + /// POSIX equivalent: EINVAL, EDESTADDRREQ, EAFNOSUPPORT + invalid-argument, + /// Not enough memory to complete the operation. + /// + /// POSIX equivalent: ENOMEM, ENOBUFS + out-of-memory, + /// The operation timed out before it could finish completely. + /// + /// POSIX equivalent: ETIMEDOUT + timeout, + /// The operation is not valid in the socket's current state. + invalid-state, + /// The local address is not available. + /// + /// POSIX equivalent: EADDRNOTAVAIL + address-not-bindable, + /// A bind operation failed because the provided address is already in + /// use or because there are no ephemeral ports available. + /// + /// POSIX equivalent: EADDRINUSE + address-in-use, + /// The remote address is not reachable. + /// + /// POSIX equivalent: EHOSTUNREACH, EHOSTDOWN, ENETDOWN, ENETUNREACH, ENONET + remote-unreachable, + /// The connection was forcefully rejected. + /// + /// POSIX equivalent: ECONNREFUSED + connection-refused, + /// A write failed because the connection was broken. + /// + /// POSIX equivalent: EPIPE + connection-broken, + /// The connection was reset. + /// + /// POSIX equivalent: ECONNRESET + connection-reset, + /// The connection was aborted. + /// + /// POSIX equivalent: ECONNABORTED + connection-aborted, + /// The size of a datagram sent to a UDP socket exceeded the maximum + /// supported size. + /// + /// POSIX equivalent: EMSGSIZE + datagram-too-large, + /// A catch-all for errors not captured by the existing variants. + /// Implementations can use this to extend the error type without + /// breaking existing code. + other(option), + } + + @since(version = 0.3.0-rc-2026-03-15) + enum ip-address-family { + /// Similar to `AF_INET` in POSIX. + ipv4, + /// Similar to `AF_INET6` in POSIX. + ipv6, + } + + @since(version = 0.3.0-rc-2026-03-15) + type ipv4-address = tuple; + + @since(version = 0.3.0-rc-2026-03-15) + type ipv6-address = tuple; + + @since(version = 0.3.0-rc-2026-03-15) + variant ip-address { + ipv4(ipv4-address), + ipv6(ipv6-address), + } + + @since(version = 0.3.0-rc-2026-03-15) + record ipv4-socket-address { + /// sin_port + port: u16, + /// sin_addr + address: ipv4-address, + } + + @since(version = 0.3.0-rc-2026-03-15) + record ipv6-socket-address { + /// sin6_port + port: u16, + /// sin6_flowinfo + flow-info: u32, + /// sin6_addr + address: ipv6-address, + /// sin6_scope_id + scope-id: u32, + } + + @since(version = 0.3.0-rc-2026-03-15) + variant ip-socket-address { + ipv4(ipv4-socket-address), + ipv6(ipv6-socket-address), + } + + /// A TCP socket resource. + /// + /// The socket can be in one of the following states: + /// - `unbound` + /// - `bound` (See note below) + /// - `listening` + /// - `connecting` + /// - `connected` + /// - `closed` + /// See + /// for more information. + /// + /// Note: Except where explicitly mentioned, whenever this documentation uses + /// the term "bound" without backticks it actually means: in the `bound` state *or higher*. + /// (i.e. `bound`, `listening`, `connecting` or `connected`) + /// + /// WASI uses shared ownership semantics: the `tcp-socket` handle and all + /// derived `stream` and `future` values reference a single underlying OS + /// socket: + /// - Send/receive streams remain functional after the original `tcp-socket` + /// handle is dropped. + /// - The stream returned by `listen` behaves similarly. + /// - Client sockets returned by `tcp-socket::listen` are independent and do + /// not keep the listening socket alive. + /// + /// The OS socket is closed only after the last handle is dropped. This + /// model has observable effects; for example, it affects when the local + /// port binding is released. + /// + /// In addition to the general error codes documented on the + /// `types::error-code` type, TCP socket methods may always return + /// `error(invalid-state)` when in the `closed` state. + @since(version = 0.3.0-rc-2026-03-15) + resource tcp-socket { + /// Create a new TCP socket. + /// + /// Similar to `socket(AF_INET or AF_INET6, SOCK_STREAM, IPPROTO_TCP)` + /// in POSIX. On IPv6 sockets, IPV6_V6ONLY is enabled by default and + /// can't be configured otherwise. + /// + /// Unlike POSIX, WASI sockets have no notion of a socket-level + /// `O_NONBLOCK` flag. Instead they fully rely on the Component Model's + /// async support. + /// + /// # Typical errors + /// - `not-supported`: The `address-family` is not supported. (EAFNOSUPPORT) + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.3.0-rc-2026-03-15) + create: static func(address-family: ip-address-family) -> result; + /// Bind the socket to the provided IP address and port. + /// + /// 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. + /// + /// Bind can be attempted multiple times on the same socket, even with + /// different arguments on each iteration. But never concurrently and + /// only as long as the previous bind failed. Once a bind succeeds, the + /// binding can't be changed anymore. + /// + /// # Typical errors + /// - `invalid-argument`: The `local-address` has the wrong address family. (EAFNOSUPPORT, EFAULT on Windows) + /// - `invalid-argument`: `local-address` is not a unicast address. (EINVAL) + /// - `invalid-argument`: `local-address` is an IPv4-mapped IPv6 address. (EINVAL) + /// - `invalid-state`: The socket is already bound. (EINVAL) + /// - `address-in-use`: 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 can be bound to. (EADDRNOTAVAIL) + /// + /// # Implementors note + /// The bind operation shouldn't be affected by the TIME_WAIT state of a + /// recently closed socket on the same local address. In practice this + /// means that the SO_REUSEADDR socket option should be set implicitly + /// on all platforms, except on Windows where this is the default + /// behavior and SO_REUSEADDR performs something different. + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.3.0-rc-2026-03-15) + bind: func(local-address: ip-socket-address) -> result<_, error-code>; + /// Connect to a remote endpoint. + /// + /// On success, the socket is transitioned into the `connected` state + /// and the `remote-address` of the socket is updated. + /// The `local-address` may be updated as well, based on the best network + /// path to `remote-address`. If the socket was not already explicitly + /// bound, this function will implicitly bind the socket to a random + /// free port. + /// + /// After a failed connection attempt, the socket will be in the `closed` + /// state and the only valid action left is to `drop` the socket. A single + /// socket can not be used to connect more than once. + /// + /// # Typical errors + /// - `invalid-argument`: The `remote-address` has the wrong address family. (EAFNOSUPPORT) + /// - `invalid-argument`: `remote-address` is not a unicast address. (EINVAL, ENETUNREACH on Linux, EAFNOSUPPORT on MacOS) + /// - `invalid-argument`: `remote-address` is an IPv4-mapped IPv6 address. (EINVAL, EADDRNOTAVAIL on Illumos) + /// - `invalid-argument`: The IP address in `remote-address` is set to INADDR_ANY (`0.0.0.0` / `::`). (EADDRNOTAVAIL on Windows) + /// - `invalid-argument`: The port in `remote-address` is set to 0. (EADDRNOTAVAIL on Windows) + /// - `invalid-state`: The socket is already in the `connecting` state. (EALREADY) + /// - `invalid-state`: The socket is already in the `connected` state. (EISCONN) + /// - `invalid-state`: The socket is already in the `listening` state. (EOPNOTSUPP, EINVAL on Windows) + /// - `timeout`: Connection timed out. (ETIMEDOUT) + /// - `connection-refused`: The connection was forcefully rejected. (ECONNREFUSED) + /// - `connection-reset`: The connection was reset. (ECONNRESET) + /// - `connection-aborted`: The connection was aborted. (ECONNABORTED) + /// - `remote-unreachable`: The remote address is not reachable. (EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN, ENONET) + /// - `address-in-use`: Tried to perform an implicit bind, but there were no ephemeral ports available. (EADDRINUSE, EADDRNOTAVAIL on Linux, EAGAIN on BSD) + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.3.0-rc-2026-03-15) + connect: async func(remote-address: ip-socket-address) -> result<_, error-code>; + /// Start listening and return a stream of new inbound connections. + /// + /// Transitions the socket into the `listening` state. This can be called + /// at most once per socket. + /// + /// If the socket is not already explicitly bound, this function will + /// implicitly bind the socket to a random free port. + /// + /// Normally, the returned sockets are bound, in the `connected` state + /// and immediately ready for I/O. Though, depending on exact timing and + /// circumstances, a newly accepted connection may already be `closed` + /// by the time the server attempts to perform its first I/O on it. This + /// is true regardless of whether the WASI implementation uses + /// "synthesized" sockets or not (see Implementors Notes below). + /// + /// The following properties are inherited from the listener socket: + /// - `address-family` + /// - `keep-alive-enabled` + /// - `keep-alive-idle-time` + /// - `keep-alive-interval` + /// - `keep-alive-count` + /// - `hop-limit` + /// - `receive-buffer-size` + /// - `send-buffer-size` + /// + /// # Typical errors + /// - `invalid-state`: The socket is already in the `connected` state. (EISCONN, EINVAL on BSD) + /// - `invalid-state`: The socket is already in the `listening` state. + /// - `address-in-use`: Tried to perform an implicit bind, but there were no ephemeral ports available. (EADDRINUSE) + /// + /// # Implementors note + /// This method returns a single perpetual stream that should only close + /// on fatal errors (if any). Yet, the POSIX' `accept` function may also + /// return transient errors (e.g. ECONNABORTED). The exact details differ + /// per operation system. For example, the Linux manual mentions: + /// + /// > Linux accept() passes already-pending network errors on the new + /// > socket as an error code from accept(). This behavior differs from + /// > other BSD socket implementations. For reliable operation the + /// > application should detect the network errors defined for the + /// > protocol after accept() and treat them like EAGAIN by retrying. + /// > In the case of TCP/IP, these are ENETDOWN, EPROTO, ENOPROTOOPT, + /// > EHOSTDOWN, ENONET, EHOSTUNREACH, EOPNOTSUPP, and ENETUNREACH. + /// Source: https://man7.org/linux/man-pages/man2/accept.2.html + /// + /// WASI implementations have two options to handle this: + /// - Optionally log it and then skip over non-fatal errors returned by + /// `accept`. Guest code never gets to see these failures. Or: + /// - Synthesize a `tcp-socket` resource that exposes the error when + /// attempting to send or receive on it. Guest code then sees these + /// failures as regular I/O errors. + /// + /// In either case, the stream returned by this `listen` method remains + /// operational. + /// + /// WASI requires `listen` to perform an implicit bind if the socket + /// has not already been bound. Not all platforms (notably Windows) + /// exhibit this behavior out of the box. On platforms that require it, + /// the WASI implementation can emulate this behavior by performing + /// the bind itself if the guest hasn't already done so. + /// + /// # References + /// - + /// - + /// - + /// - + /// - + /// - + /// - + /// - + @since(version = 0.3.0-rc-2026-03-15) + listen: func() -> result, error-code>; + /// Transmit data to peer. + /// + /// The caller should close the stream when it has no more data to send + /// to the peer. Under normal circumstances this will cause a FIN packet + /// to be sent out. Closing the stream is equivalent to calling + /// `shutdown(SHUT_WR)` in POSIX. + /// + /// This function may be called at most once and returns once the full + /// contents of the stream are transmitted or an error is encountered. + /// + /// # Typical errors + /// - `invalid-state`: The socket is not in the `connected` state. (ENOTCONN) + /// - `invalid-state`: `send` has already been called on this socket. + /// - `connection-broken`: The connection is not writable anymore. (EPIPE, ECONNABORTED on Windows) + /// - `connection-reset`: The connection was reset. (ECONNRESET) + /// - `remote-unreachable`: The remote address is not reachable. (EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN, ENONET) + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.3.0-rc-2026-03-15) + send: func(data: stream) -> future>; + /// Read data from peer. + /// + /// Returns a `stream` of data sent by the peer. The implementation + /// drops the stream once no more data is available. At that point, the + /// returned `future` resolves to: + /// - `ok` after a graceful shutdown from the peer (i.e. a FIN packet), or + /// - `err` if the socket was closed abnormally. + /// + /// `receive` may be called only once per socket. Subsequent calls return + /// a closed stream and a future resolved to `err(invalid-state)`. + /// + /// If the caller is not expecting to receive any more data from the peer, + /// they should drop the stream. Any data still in the receive queue + /// will be discarded. This is equivalent to calling `shutdown(SHUT_RD)` + /// in POSIX. + /// + /// # Typical errors + /// - `invalid-state`: The socket is not in the `connected` state. (ENOTCONN) + /// - `invalid-state`: `receive` has already been called on this socket. + /// - `connection-reset`: The connection was reset. (ECONNRESET) + /// - `remote-unreachable`: The remote address is not reachable. (EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN, ENONET) + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.3.0-rc-2026-03-15) + receive: func() -> tuple, future>>; + /// Get the bound local address. + /// + /// POSIX mentions: + /// > If the socket has not been bound to a local name, the value + /// > stored in the object pointed to by `address` is unspecified. + /// + /// WASI is stricter and requires `get-local-address` to return + /// `invalid-state` when the socket hasn't been bound yet. + /// + /// # Typical errors + /// - `invalid-state`: The socket is not bound to any local address. + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.3.0-rc-2026-03-15) + get-local-address: func() -> result; + /// Get the remote address. + /// + /// # Typical errors + /// - `invalid-state`: The socket is not connected to a remote address. (ENOTCONN) + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.3.0-rc-2026-03-15) + get-remote-address: func() -> result; + /// Whether the socket is in the `listening` state. + /// + /// Equivalent to the SO_ACCEPTCONN socket option. + @since(version = 0.3.0-rc-2026-03-15) + get-is-listening: func() -> bool; + /// Whether this is a IPv4 or IPv6 socket. + /// + /// This is the value passed to the constructor. + /// + /// Equivalent to the SO_DOMAIN socket option. + @since(version = 0.3.0-rc-2026-03-15) + get-address-family: func() -> ip-address-family; + /// Hints the desired listen queue size. Implementations are free to + /// ignore this. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// Any other value will never cause an error, but it might be silently + /// clamped and/or rounded. + /// + /// # Typical errors + /// - `not-supported`: (set) The platform does not support changing the backlog size after the initial listen. + /// - `invalid-argument`: (set) The provided value was 0. + /// - `invalid-state`: (set) The socket is in the `connecting` or `connected` state. + @since(version = 0.3.0-rc-2026-03-15) + set-listen-backlog-size: func(value: u64) -> result<_, error-code>; + /// Enables or disables keepalive. + /// + /// The keepalive behavior can be adjusted using: + /// - `keep-alive-idle-time` + /// - `keep-alive-interval` + /// - `keep-alive-count` + /// These properties can be configured while `keep-alive-enabled` is + /// false, but only come into effect when `keep-alive-enabled` is true. + /// + /// Equivalent to the SO_KEEPALIVE socket option. + @since(version = 0.3.0-rc-2026-03-15) + get-keep-alive-enabled: func() -> result; + @since(version = 0.3.0-rc-2026-03-15) + set-keep-alive-enabled: func(value: bool) -> result<_, error-code>; + /// Amount of time the connection has to be idle before TCP starts + /// sending keepalive packets. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// All other values are accepted without error, but may be + /// clamped or rounded. As a result, the value read back from + /// this setting may differ from the value that was set. + /// + /// Equivalent to the TCP_KEEPIDLE socket option. (TCP_KEEPALIVE on MacOS) + /// + /// # Typical errors + /// - `invalid-argument`: (set) The provided value was 0. + @since(version = 0.3.0-rc-2026-03-15) + get-keep-alive-idle-time: func() -> result; + @since(version = 0.3.0-rc-2026-03-15) + set-keep-alive-idle-time: func(value: duration) -> result<_, error-code>; + /// The time between keepalive packets. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// All other values are accepted without error, but may be + /// clamped or rounded. As a result, the value read back from + /// this setting may differ from the value that was set. + /// + /// Equivalent to the TCP_KEEPINTVL socket option. + /// + /// # Typical errors + /// - `invalid-argument`: (set) The provided value was 0. + @since(version = 0.3.0-rc-2026-03-15) + get-keep-alive-interval: func() -> result; + @since(version = 0.3.0-rc-2026-03-15) + set-keep-alive-interval: func(value: duration) -> result<_, error-code>; + /// The maximum amount of keepalive packets TCP should send before + /// aborting the connection. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// All other values are accepted without error, but may be + /// clamped or rounded. As a result, the value read back from + /// this setting may differ from the value that was set. + /// + /// Equivalent to the TCP_KEEPCNT socket option. + /// + /// # Typical errors + /// - `invalid-argument`: (set) The provided value was 0. + @since(version = 0.3.0-rc-2026-03-15) + get-keep-alive-count: func() -> result; + @since(version = 0.3.0-rc-2026-03-15) + set-keep-alive-count: func(value: u32) -> result<_, error-code>; + /// Equivalent to the IP_TTL & IPV6_UNICAST_HOPS socket options. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// + /// # Typical errors + /// - `invalid-argument`: (set) The TTL value must be 1 or higher. + @since(version = 0.3.0-rc-2026-03-15) + get-hop-limit: func() -> result; + @since(version = 0.3.0-rc-2026-03-15) + set-hop-limit: func(value: u8) -> result<_, error-code>; + /// Kernel buffer space reserved for sending/receiving on this socket. + /// Implementations usually treat this as a cap the buffer can grow to, + /// rather than allocating the full amount immediately. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// All other values are accepted without error, but may be + /// clamped or rounded. As a result, the value read back from + /// this setting may differ from the value that was set. + /// + /// This is only a performance hint. The implementation may ignore it or + /// tweak it based on real traffic patterns. + /// Linux and macOS appear to behave differently depending on whether a + /// buffer size was explicitly set. When set, they tend to honor it; when + /// not set, they dynamically adjust the buffer size as the connection + /// progresses. This is especially noticeable when comparing the values + /// from before and after connection establishment. + /// + /// Equivalent to the SO_RCVBUF and SO_SNDBUF socket options. + /// + /// # Typical errors + /// - `invalid-argument`: (set) The provided value was 0. + @since(version = 0.3.0-rc-2026-03-15) + get-receive-buffer-size: func() -> result; + @since(version = 0.3.0-rc-2026-03-15) + set-receive-buffer-size: func(value: u64) -> result<_, error-code>; + @since(version = 0.3.0-rc-2026-03-15) + get-send-buffer-size: func() -> result; + @since(version = 0.3.0-rc-2026-03-15) + set-send-buffer-size: func(value: u64) -> result<_, error-code>; + } + + /// A UDP socket handle. + @since(version = 0.3.0-rc-2026-03-15) + resource udp-socket { + /// Create a new UDP socket. + /// + /// Similar to `socket(AF_INET or AF_INET6, SOCK_DGRAM, IPPROTO_UDP)` + /// in POSIX. On IPv6 sockets, IPV6_V6ONLY is enabled by default and + /// can't be configured otherwise. + /// + /// Unlike POSIX, WASI sockets have no notion of a socket-level + /// `O_NONBLOCK` flag. Instead they fully rely on the Component Model's + /// async support. + /// + /// # References: + /// - + /// - + /// - + /// - + @since(version = 0.3.0-rc-2026-03-15) + create: static func(address-family: ip-address-family) -> result; + /// Bind the socket to the provided IP address and port. + /// + /// 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 port is zero, the socket will be bound to a random + /// free port. + /// + /// # Typical errors + /// - `invalid-argument`: The `local-address` has the wrong address family. (EAFNOSUPPORT, EFAULT on Windows) + /// - `invalid-state`: The socket is already bound. (EINVAL) + /// - `address-in-use`: 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 can be bound to. (EADDRNOTAVAIL) + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.3.0-rc-2026-03-15) + bind: func(local-address: ip-socket-address) -> result<_, error-code>; + /// Associate this socket with a specific peer address. + /// + /// On success, the `remote-address` of the socket is updated. + /// The `local-address` may be updated as well, based on the best network + /// path to `remote-address`. If the socket was not already explicitly + /// bound, this function will implicitly bind the socket to a random + /// free port. + /// + /// When a UDP socket is "connected", the `send` and `receive` methods + /// are limited to communicating with that peer only: + /// - `send` can only be used to send to this destination. + /// - `receive` will only return datagrams sent from the provided `remote-address`. + /// + /// The name "connect" was kept to align with the existing POSIX + /// terminology. Other than that, this function only changes the local + /// socket configuration and does not generate any network traffic. + /// The peer is not aware of this "connection". + /// + /// This method may be called multiple times on the same socket to change + /// its association, but only the most recent one will be effective. + /// + /// # Typical errors + /// - `invalid-argument`: The `remote-address` has the wrong address family. (EAFNOSUPPORT) + /// - `invalid-argument`: The IP address in `remote-address` is set to INADDR_ANY (`0.0.0.0` / `::`). (EDESTADDRREQ, EADDRNOTAVAIL) + /// - `invalid-argument`: The port in `remote-address` is set to 0. (EDESTADDRREQ, EADDRNOTAVAIL) + /// - `address-in-use`: Tried to perform an implicit bind, but there were no ephemeral ports available. (EADDRINUSE, EADDRNOTAVAIL on Linux, EAGAIN on BSD) + /// + /// # Implementors note + /// If the socket is already connected, some platforms (e.g. Linux) + /// require a disconnect before connecting to a different peer address. + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.3.0-rc-2026-03-15) + connect: func(remote-address: ip-socket-address) -> result<_, error-code>; + /// Dissociate this socket from its peer address. + /// + /// After calling this method, `send` & `receive` are free to communicate + /// with any remote address again. + /// + /// The POSIX equivalent of this is calling `connect` with an `AF_UNSPEC` address. + /// + /// # Typical errors + /// - `invalid-state`: The socket is not connected. + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.3.0-rc-2026-03-15) + disconnect: func() -> result<_, error-code>; + /// Send a message on the socket to a particular peer. + /// + /// If the socket is connected, the peer address may be left empty. In + /// that case this is equivalent to `send` in POSIX. Otherwise it is + /// equivalent to `sendto`. + /// + /// Additionally, if the socket is connected, a `remote-address` argument + /// _may_ be provided but then it must be identical to the address + /// passed to `connect`. + /// + /// If the socket has not been explicitly bound, it will be + /// implicitly bound to a random free port. + /// + /// Implementations may trap if the `data` length exceeds 64 KiB. + /// + /// # Typical errors + /// - `invalid-argument`: The `remote-address` has the wrong address family. (EAFNOSUPPORT) + /// - `invalid-argument`: The IP address in `remote-address` is set to INADDR_ANY (`0.0.0.0` / `::`). (EDESTADDRREQ, EADDRNOTAVAIL) + /// - `invalid-argument`: The port in `remote-address` is set to 0. (EDESTADDRREQ, EADDRNOTAVAIL) + /// - `invalid-argument`: The socket is in "connected" mode and `remote-address` is `some` value that does not match the address passed to `connect`. (EISCONN) + /// - `invalid-argument`: The socket is not "connected" and no value for `remote-address` was provided. (EDESTADDRREQ) + /// - `remote-unreachable`: The remote address is not reachable. (ECONNRESET, ENETRESET on Windows, EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN, ENONET) + /// - `connection-refused`: The connection was refused. (ECONNREFUSED) + /// - `datagram-too-large`: The datagram is too large. (EMSGSIZE) + /// - `address-in-use`: Tried to perform an implicit bind, but there were no ephemeral ports available. (EADDRINUSE) + /// + /// # Implementors note + /// WASI requires `send` to perform an implicit bind if the socket + /// has not been bound. Not all platforms (notably Windows) exhibit + /// this behavior natively. On such platforms, the WASI implementation + /// should emulate it by performing the bind if the guest has not + /// already done so. + /// + /// # References + /// - + /// - + /// - + /// - + /// - + /// - + /// - + /// - + @since(version = 0.3.0-rc-2026-03-15) + send: async func(data: list, remote-address: option) -> result<_, error-code>; + /// Receive a message on the socket. + /// + /// On success, the return value contains a tuple of the received data + /// and the address of the sender. Theoretical maximum length of the + /// data is 64 KiB. Though in practice, it will typically be less than + /// 1500 bytes. + /// + /// If the socket is connected, the sender address is guaranteed to + /// match the remote address passed to `connect`. + /// + /// # Typical errors + /// - `invalid-state`: The socket has not been bound yet. + /// - `remote-unreachable`: The remote address is not reachable. (ECONNRESET, ENETRESET on Windows, EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN, ENONET) + /// - `connection-refused`: The connection was refused. (ECONNREFUSED) + /// + /// # References + /// - + /// - + /// - + /// - + /// - + /// - + /// - + @since(version = 0.3.0-rc-2026-03-15) + receive: async func() -> result, ip-socket-address>, error-code>; + /// Get the current bound address. + /// + /// POSIX mentions: + /// > If the socket has not been bound to a local name, the value + /// > stored in the object pointed to by `address` is unspecified. + /// + /// WASI is stricter and requires `get-local-address` to return + /// `invalid-state` when the socket hasn't been bound yet. + /// + /// # Typical errors + /// - `invalid-state`: The socket is not bound to any local address. + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.3.0-rc-2026-03-15) + get-local-address: func() -> result; + /// Get the address the socket is currently "connected" to. + /// + /// # Typical errors + /// - `invalid-state`: The socket is not "connected" to a specific remote address. (ENOTCONN) + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.3.0-rc-2026-03-15) + get-remote-address: func() -> result; + /// Whether this is a IPv4 or IPv6 socket. + /// + /// This is the value passed to the constructor. + /// + /// Equivalent to the SO_DOMAIN socket option. + @since(version = 0.3.0-rc-2026-03-15) + get-address-family: func() -> ip-address-family; + /// Equivalent to the IP_TTL & IPV6_UNICAST_HOPS socket options. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// + /// # Typical errors + /// - `invalid-argument`: (set) The TTL value must be 1 or higher. + @since(version = 0.3.0-rc-2026-03-15) + get-unicast-hop-limit: func() -> result; + @since(version = 0.3.0-rc-2026-03-15) + set-unicast-hop-limit: func(value: u8) -> result<_, error-code>; + /// Kernel buffer space reserved for sending/receiving on this socket. + /// Implementations usually treat this as a cap the buffer can grow to, + /// rather than allocating the full amount immediately. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// All other values are accepted without error, but may be + /// clamped or rounded. As a result, the value read back from + /// this setting may differ from the value that was set. + /// + /// Equivalent to the SO_RCVBUF and SO_SNDBUF socket options. + /// + /// # Typical errors + /// - `invalid-argument`: (set) The provided value was 0. + @since(version = 0.3.0-rc-2026-03-15) + get-receive-buffer-size: func() -> result; + @since(version = 0.3.0-rc-2026-03-15) + set-receive-buffer-size: func(value: u64) -> result<_, error-code>; + @since(version = 0.3.0-rc-2026-03-15) + get-send-buffer-size: func() -> result; + @since(version = 0.3.0-rc-2026-03-15) + set-send-buffer-size: func(value: u64) -> result<_, error-code>; + } +} + +@since(version = 0.3.0-rc-2026-03-15) +interface ip-name-lookup { + @since(version = 0.3.0-rc-2026-03-15) + use types.{ip-address}; + + /// Lookup error codes. + @since(version = 0.3.0-rc-2026-03-15) + variant error-code { + /// Access denied. + /// + /// POSIX equivalent: EACCES, EPERM + access-denied, + /// `name` is a syntactically invalid domain name or IP address. + /// + /// POSIX equivalent: EINVAL + invalid-argument, + /// Name does not exist or has no suitable associated IP addresses. + /// + /// POSIX equivalent: EAI_NONAME, EAI_NODATA, EAI_ADDRFAMILY + name-unresolvable, + /// A temporary failure in name resolution occurred. + /// + /// POSIX equivalent: EAI_AGAIN + temporary-resolver-failure, + /// A permanent failure in name resolution occurred. + /// + /// POSIX equivalent: EAI_FAIL + permanent-resolver-failure, + /// A catch-all for errors not captured by the existing variants. + /// Implementations can use this to extend the error type without + /// breaking existing code. + other(option), + } + + /// Resolve an internet host name to a list of IP addresses. + /// + /// Unicode domain names are automatically converted to ASCII using IDNA + /// encoding. If the input is an IP address string, the address is parsed + /// and returned as-is without making any external requests. + /// + /// See the wasi-socket proposal README.md for a comparison with getaddrinfo. + /// + /// The results are returned in connection order preference. + /// + /// This function never succeeds with 0 results. It either fails or succeeds + /// with at least one address. Additionally, this function never returns + /// IPv4-mapped IPv6 addresses. + /// + /// # References: + /// - + /// - + /// - + /// - + @since(version = 0.3.0-rc-2026-03-15) + resolve-addresses: async func(name: string) -> result, error-code>; +} + +@since(version = 0.3.0-rc-2026-03-15) +world imports { + @since(version = 0.3.0-rc-2026-03-15) + import wasi:clocks/types@0.3.0-rc-2026-03-15; + @since(version = 0.3.0-rc-2026-03-15) + import types; + @since(version = 0.3.0-rc-2026-03-15) + import ip-name-lookup; +} diff --git a/examples/multiple-worlds/wit1/world.wit b/examples/multiple-worlds/wit1/world.wit new file mode 100644 index 0000000..8061b2d --- /dev/null +++ b/examples/multiple-worlds/wit1/world.wit @@ -0,0 +1,5 @@ +package componentize-go:example; + +world world1 { + include wasi:http/service@0.3.0-rc-2026-03-15; +} diff --git a/examples/multiple-worlds/wit2/world.wit b/examples/multiple-worlds/wit2/world.wit new file mode 100644 index 0000000..002a44a --- /dev/null +++ b/examples/multiple-worlds/wit2/world.wit @@ -0,0 +1,5 @@ +package componentize-go:example; + +world world2 { + export foo: func(); +} diff --git a/examples/wasip3/export_wasi_http_handler/handler.go b/examples/wasip3/export_wasi_http_handler/handler.go index c878458..6a38655 100644 --- a/examples/wasip3/export_wasi_http_handler/handler.go +++ b/examples/wasip3/export_wasi_http_handler/handler.go @@ -5,7 +5,7 @@ import ( "encoding/hex" "fmt" "net/url" - "wit_component/wasi_http_handler" + client "wit_component/wasi_http_client" . "wit_component/wasi_http_types" . "go.bytecodealliance.org/pkg/wit/types" @@ -150,7 +150,7 @@ func getSha256(urlString string) string { request.SetAuthority(Some(parsed.Host)).Ok() request.SetPathWithQuery(Some(parsed.Path)).Ok() - result := wasi_http_handler.Handle(request) + result := client.Send(request) switch result.Tag() { case ResultOk: response := result.Ok() diff --git a/examples/wasip3/wit/deps/cli.wit b/examples/wasip3/wit/deps/cli.wit new file mode 100644 index 0000000..8ba52c5 --- /dev/null +++ b/examples/wasip3/wit/deps/cli.wit @@ -0,0 +1,256 @@ +package wasi:cli@0.3.0-rc-2026-03-15; + +@since(version = 0.3.0-rc-2026-03-15) +interface environment { + /// Get the POSIX-style environment variables. + /// + /// Each environment variable is provided as a pair of string variable names + /// and string value. + /// + /// Morally, these are a value import, but until value imports are available + /// in the component model, this import function should return the same + /// values each time it is called. + @since(version = 0.3.0-rc-2026-03-15) + get-environment: func() -> list>; + + /// Get the POSIX-style arguments to the program. + @since(version = 0.3.0-rc-2026-03-15) + get-arguments: func() -> list; + + /// Return a path that programs should use as their initial current working + /// directory, interpreting `.` as shorthand for this. + @since(version = 0.3.0-rc-2026-03-15) + get-initial-cwd: func() -> option; +} + +@since(version = 0.3.0-rc-2026-03-15) +interface exit { + /// Exit the current instance and any linked instances. + @since(version = 0.3.0-rc-2026-03-15) + exit: func(status: result); + + /// Exit the current instance and any linked instances, reporting the + /// specified status code to the host. + /// + /// The meaning of the code depends on the context, with 0 usually meaning + /// "success", and other values indicating various types of failure. + /// + /// This function does not return; the effect is analogous to a trap, but + /// without the connotation that something bad has happened. + @unstable(feature = cli-exit-with-code) + exit-with-code: func(status-code: u8); +} + +@since(version = 0.3.0-rc-2026-03-15) +interface run { + /// Run the program. + @since(version = 0.3.0-rc-2026-03-15) + run: async func() -> result; +} + +@since(version = 0.3.0-rc-2026-03-15) +interface types { + @since(version = 0.3.0-rc-2026-03-15) + enum error-code { + /// Input/output error + io, + /// Invalid or incomplete multibyte or wide character + illegal-byte-sequence, + /// Broken pipe + pipe, + } +} + +@since(version = 0.3.0-rc-2026-03-15) +interface stdin { + use types.{error-code}; + + /// Return a stream for reading from stdin. + /// + /// This function returns a stream which provides data read from stdin, + /// and a future to signal read results. + /// + /// If the stream's readable end is dropped the future will resolve to success. + /// + /// If the stream's writable end is dropped the future will either resolve to + /// success if stdin was closed by the writer or to an error-code if reading + /// failed for some other reason. + /// + /// Multiple streams may be active at the same time. The behavior of concurrent + /// reads is implementation-specific. + @since(version = 0.3.0-rc-2026-03-15) + read-via-stream: func() -> tuple, future>>; +} + +@since(version = 0.3.0-rc-2026-03-15) +interface stdout { + use types.{error-code}; + + /// Write the given stream to stdout. + /// + /// If the stream's writable end is dropped this function will either return + /// success once the entire contents of the stream have been written or an + /// error-code representing a failure. + /// + /// Otherwise if there is an error the readable end of the stream will be + /// dropped and this function will return an error-code. + @since(version = 0.3.0-rc-2026-03-15) + write-via-stream: func(data: stream) -> future>; +} + +@since(version = 0.3.0-rc-2026-03-15) +interface stderr { + use types.{error-code}; + + /// Write the given stream to stderr. + /// + /// If the stream's writable end is dropped this function will either return + /// success once the entire contents of the stream have been written or an + /// error-code representing a failure. + /// + /// Otherwise if there is an error the readable end of the stream will be + /// dropped and this function will return an error-code. + @since(version = 0.3.0-rc-2026-03-15) + write-via-stream: func(data: stream) -> future>; +} + +/// Terminal input. +/// +/// In the future, this may include functions for disabling echoing, +/// disabling input buffering so that keyboard events are sent through +/// immediately, querying supported features, and so on. +@since(version = 0.3.0-rc-2026-03-15) +interface terminal-input { + /// The input side of a terminal. + @since(version = 0.3.0-rc-2026-03-15) + resource terminal-input; +} + +/// Terminal output. +/// +/// In the future, this may include functions for querying the terminal +/// size, being notified of terminal size changes, querying supported +/// features, and so on. +@since(version = 0.3.0-rc-2026-03-15) +interface terminal-output { + /// The output side of a terminal. + @since(version = 0.3.0-rc-2026-03-15) + resource terminal-output; +} + +/// An interface providing an optional `terminal-input` for stdin as a +/// link-time authority. +@since(version = 0.3.0-rc-2026-03-15) +interface terminal-stdin { + @since(version = 0.3.0-rc-2026-03-15) + use terminal-input.{terminal-input}; + + /// If stdin is connected to a terminal, return a `terminal-input` handle + /// allowing further interaction with it. + @since(version = 0.3.0-rc-2026-03-15) + get-terminal-stdin: func() -> option; +} + +/// An interface providing an optional `terminal-output` for stdout as a +/// link-time authority. +@since(version = 0.3.0-rc-2026-03-15) +interface terminal-stdout { + @since(version = 0.3.0-rc-2026-03-15) + use terminal-output.{terminal-output}; + + /// If stdout is connected to a terminal, return a `terminal-output` handle + /// allowing further interaction with it. + @since(version = 0.3.0-rc-2026-03-15) + get-terminal-stdout: func() -> option; +} + +/// An interface providing an optional `terminal-output` for stderr as a +/// link-time authority. +@since(version = 0.3.0-rc-2026-03-15) +interface terminal-stderr { + @since(version = 0.3.0-rc-2026-03-15) + use terminal-output.{terminal-output}; + + /// If stderr is connected to a terminal, return a `terminal-output` handle + /// allowing further interaction with it. + @since(version = 0.3.0-rc-2026-03-15) + get-terminal-stderr: func() -> option; +} + +@since(version = 0.3.0-rc-2026-03-15) +world imports { + @since(version = 0.3.0-rc-2026-03-15) + import environment; + @since(version = 0.3.0-rc-2026-03-15) + import exit; + @since(version = 0.3.0-rc-2026-03-15) + import types; + @since(version = 0.3.0-rc-2026-03-15) + import stdin; + @since(version = 0.3.0-rc-2026-03-15) + import stdout; + @since(version = 0.3.0-rc-2026-03-15) + import stderr; + @since(version = 0.3.0-rc-2026-03-15) + import terminal-input; + @since(version = 0.3.0-rc-2026-03-15) + import terminal-output; + @since(version = 0.3.0-rc-2026-03-15) + import terminal-stdin; + @since(version = 0.3.0-rc-2026-03-15) + import terminal-stdout; + @since(version = 0.3.0-rc-2026-03-15) + import terminal-stderr; + import wasi:clocks/types@0.3.0-rc-2026-03-15; + import wasi:clocks/monotonic-clock@0.3.0-rc-2026-03-15; + import wasi:clocks/system-clock@0.3.0-rc-2026-03-15; + @unstable(feature = clocks-timezone) + import wasi:clocks/timezone@0.3.0-rc-2026-03-15; + import wasi:filesystem/types@0.3.0-rc-2026-03-15; + import wasi:filesystem/preopens@0.3.0-rc-2026-03-15; + import wasi:sockets/types@0.3.0-rc-2026-03-15; + import wasi:sockets/ip-name-lookup@0.3.0-rc-2026-03-15; + import wasi:random/random@0.3.0-rc-2026-03-15; + import wasi:random/insecure@0.3.0-rc-2026-03-15; + import wasi:random/insecure-seed@0.3.0-rc-2026-03-15; +} +@since(version = 0.3.0-rc-2026-03-15) +world command { + @since(version = 0.3.0-rc-2026-03-15) + import environment; + @since(version = 0.3.0-rc-2026-03-15) + import exit; + @since(version = 0.3.0-rc-2026-03-15) + import types; + @since(version = 0.3.0-rc-2026-03-15) + import stdin; + @since(version = 0.3.0-rc-2026-03-15) + import stdout; + @since(version = 0.3.0-rc-2026-03-15) + import stderr; + @since(version = 0.3.0-rc-2026-03-15) + import terminal-input; + @since(version = 0.3.0-rc-2026-03-15) + import terminal-output; + @since(version = 0.3.0-rc-2026-03-15) + import terminal-stdin; + @since(version = 0.3.0-rc-2026-03-15) + import terminal-stdout; + @since(version = 0.3.0-rc-2026-03-15) + import terminal-stderr; + import wasi:clocks/types@0.3.0-rc-2026-03-15; + import wasi:clocks/monotonic-clock@0.3.0-rc-2026-03-15; + import wasi:clocks/system-clock@0.3.0-rc-2026-03-15; + @unstable(feature = clocks-timezone) + import wasi:clocks/timezone@0.3.0-rc-2026-03-15; + import wasi:filesystem/types@0.3.0-rc-2026-03-15; + import wasi:filesystem/preopens@0.3.0-rc-2026-03-15; + import wasi:sockets/types@0.3.0-rc-2026-03-15; + import wasi:sockets/ip-name-lookup@0.3.0-rc-2026-03-15; + import wasi:random/random@0.3.0-rc-2026-03-15; + import wasi:random/insecure@0.3.0-rc-2026-03-15; + import wasi:random/insecure-seed@0.3.0-rc-2026-03-15; + + @since(version = 0.3.0-rc-2026-03-15) + export run; +} diff --git a/examples/wasip3/wit/deps/cli/command.wit b/examples/wasip3/wit/deps/cli/command.wit deleted file mode 100644 index f2f613e..0000000 --- a/examples/wasip3/wit/deps/cli/command.wit +++ /dev/null @@ -1,10 +0,0 @@ -package wasi:cli@0.3.0-rc-2025-09-16; - -@since(version = 0.3.0-rc-2025-09-16) -world command { - @since(version = 0.3.0-rc-2025-09-16) - include imports; - - @since(version = 0.3.0-rc-2025-09-16) - export run; -} diff --git a/examples/wasip3/wit/deps/cli/environment.wit b/examples/wasip3/wit/deps/cli/environment.wit deleted file mode 100644 index 3763f2f..0000000 --- a/examples/wasip3/wit/deps/cli/environment.wit +++ /dev/null @@ -1,22 +0,0 @@ -@since(version = 0.3.0-rc-2025-09-16) -interface environment { - /// Get the POSIX-style environment variables. - /// - /// Each environment variable is provided as a pair of string variable names - /// and string value. - /// - /// Morally, these are a value import, but until value imports are available - /// in the component model, this import function should return the same - /// values each time it is called. - @since(version = 0.3.0-rc-2025-09-16) - get-environment: func() -> list>; - - /// Get the POSIX-style arguments to the program. - @since(version = 0.3.0-rc-2025-09-16) - get-arguments: func() -> list; - - /// Return a path that programs should use as their initial current working - /// directory, interpreting `.` as shorthand for this. - @since(version = 0.3.0-rc-2025-09-16) - get-initial-cwd: func() -> option; -} diff --git a/examples/wasip3/wit/deps/cli/exit.wit b/examples/wasip3/wit/deps/cli/exit.wit deleted file mode 100644 index 1efba7d..0000000 --- a/examples/wasip3/wit/deps/cli/exit.wit +++ /dev/null @@ -1,17 +0,0 @@ -@since(version = 0.3.0-rc-2025-09-16) -interface exit { - /// Exit the current instance and any linked instances. - @since(version = 0.3.0-rc-2025-09-16) - exit: func(status: result); - - /// Exit the current instance and any linked instances, reporting the - /// specified status code to the host. - /// - /// The meaning of the code depends on the context, with 0 usually meaning - /// "success", and other values indicating various types of failure. - /// - /// This function does not return; the effect is analogous to a trap, but - /// without the connotation that something bad has happened. - @unstable(feature = cli-exit-with-code) - exit-with-code: func(status-code: u8); -} diff --git a/examples/wasip3/wit/deps/cli/imports.wit b/examples/wasip3/wit/deps/cli/imports.wit deleted file mode 100644 index 660a2dd..0000000 --- a/examples/wasip3/wit/deps/cli/imports.wit +++ /dev/null @@ -1,34 +0,0 @@ -package wasi:cli@0.3.0-rc-2025-09-16; - -@since(version = 0.3.0-rc-2025-09-16) -world imports { - @since(version = 0.3.0-rc-2025-09-16) - include wasi:clocks/imports@0.3.0-rc-2025-09-16; - @since(version = 0.3.0-rc-2025-09-16) - include wasi:filesystem/imports@0.3.0-rc-2025-09-16; - @since(version = 0.3.0-rc-2025-09-16) - include wasi:sockets/imports@0.3.0-rc-2025-09-16; - @since(version = 0.3.0-rc-2025-09-16) - include wasi:random/imports@0.3.0-rc-2025-09-16; - - @since(version = 0.3.0-rc-2025-09-16) - import environment; - @since(version = 0.3.0-rc-2025-09-16) - import exit; - @since(version = 0.3.0-rc-2025-09-16) - import stdin; - @since(version = 0.3.0-rc-2025-09-16) - import stdout; - @since(version = 0.3.0-rc-2025-09-16) - import stderr; - @since(version = 0.3.0-rc-2025-09-16) - import terminal-input; - @since(version = 0.3.0-rc-2025-09-16) - import terminal-output; - @since(version = 0.3.0-rc-2025-09-16) - import terminal-stdin; - @since(version = 0.3.0-rc-2025-09-16) - import terminal-stdout; - @since(version = 0.3.0-rc-2025-09-16) - import terminal-stderr; -} diff --git a/examples/wasip3/wit/deps/cli/run.wit b/examples/wasip3/wit/deps/cli/run.wit deleted file mode 100644 index 631441a..0000000 --- a/examples/wasip3/wit/deps/cli/run.wit +++ /dev/null @@ -1,6 +0,0 @@ -@since(version = 0.3.0-rc-2025-09-16) -interface run { - /// Run the program. - @since(version = 0.3.0-rc-2025-09-16) - run: async func() -> result; -} diff --git a/examples/wasip3/wit/deps/cli/stdio.wit b/examples/wasip3/wit/deps/cli/stdio.wit deleted file mode 100644 index 51e5ae4..0000000 --- a/examples/wasip3/wit/deps/cli/stdio.wit +++ /dev/null @@ -1,65 +0,0 @@ -@since(version = 0.3.0-rc-2025-09-16) -interface types { - @since(version = 0.3.0-rc-2025-09-16) - enum error-code { - /// Input/output error - io, - /// Invalid or incomplete multibyte or wide character - illegal-byte-sequence, - /// Broken pipe - pipe, - } -} - -@since(version = 0.3.0-rc-2025-09-16) -interface stdin { - use types.{error-code}; - - /// Return a stream for reading from stdin. - /// - /// This function returns a stream which provides data read from stdin, - /// and a future to signal read results. - /// - /// If the stream's readable end is dropped the future will resolve to success. - /// - /// If the stream's writable end is dropped the future will either resolve to - /// success if stdin was closed by the writer or to an error-code if reading - /// failed for some other reason. - /// - /// Multiple streams may be active at the same time. The behavior of concurrent - /// reads is implementation-specific. - @since(version = 0.3.0-rc-2025-09-16) - read-via-stream: func() -> tuple, future>>; -} - -@since(version = 0.3.0-rc-2025-09-16) -interface stdout { - use types.{error-code}; - - /// Write the given stream to stdout. - /// - /// If the stream's writable end is dropped this function will either return - /// success once the entire contents of the stream have been written or an - /// error-code representing a failure. - /// - /// Otherwise if there is an error the readable end of the stream will be - /// dropped and this function will return an error-code. - @since(version = 0.3.0-rc-2025-09-16) - write-via-stream: async func(data: stream) -> result<_, error-code>; -} - -@since(version = 0.3.0-rc-2025-09-16) -interface stderr { - use types.{error-code}; - - /// Write the given stream to stderr. - /// - /// If the stream's writable end is dropped this function will either return - /// success once the entire contents of the stream have been written or an - /// error-code representing a failure. - /// - /// Otherwise if there is an error the readable end of the stream will be - /// dropped and this function will return an error-code. - @since(version = 0.3.0-rc-2025-09-16) - write-via-stream: async func(data: stream) -> result<_, error-code>; -} diff --git a/examples/wasip3/wit/deps/cli/terminal.wit b/examples/wasip3/wit/deps/cli/terminal.wit deleted file mode 100644 index 74c1769..0000000 --- a/examples/wasip3/wit/deps/cli/terminal.wit +++ /dev/null @@ -1,62 +0,0 @@ -/// Terminal input. -/// -/// In the future, this may include functions for disabling echoing, -/// disabling input buffering so that keyboard events are sent through -/// immediately, querying supported features, and so on. -@since(version = 0.3.0-rc-2025-09-16) -interface terminal-input { - /// The input side of a terminal. - @since(version = 0.3.0-rc-2025-09-16) - resource terminal-input; -} - -/// Terminal output. -/// -/// In the future, this may include functions for querying the terminal -/// size, being notified of terminal size changes, querying supported -/// features, and so on. -@since(version = 0.3.0-rc-2025-09-16) -interface terminal-output { - /// The output side of a terminal. - @since(version = 0.3.0-rc-2025-09-16) - resource terminal-output; -} - -/// An interface providing an optional `terminal-input` for stdin as a -/// link-time authority. -@since(version = 0.3.0-rc-2025-09-16) -interface terminal-stdin { - @since(version = 0.3.0-rc-2025-09-16) - use terminal-input.{terminal-input}; - - /// If stdin is connected to a terminal, return a `terminal-input` handle - /// allowing further interaction with it. - @since(version = 0.3.0-rc-2025-09-16) - get-terminal-stdin: func() -> option; -} - -/// An interface providing an optional `terminal-output` for stdout as a -/// link-time authority. -@since(version = 0.3.0-rc-2025-09-16) -interface terminal-stdout { - @since(version = 0.3.0-rc-2025-09-16) - use terminal-output.{terminal-output}; - - /// If stdout is connected to a terminal, return a `terminal-output` handle - /// allowing further interaction with it. - @since(version = 0.3.0-rc-2025-09-16) - get-terminal-stdout: func() -> option; -} - -/// An interface providing an optional `terminal-output` for stderr as a -/// link-time authority. -@since(version = 0.3.0-rc-2025-09-16) -interface terminal-stderr { - @since(version = 0.3.0-rc-2025-09-16) - use terminal-output.{terminal-output}; - - /// If stderr is connected to a terminal, return a `terminal-output` handle - /// allowing further interaction with it. - @since(version = 0.3.0-rc-2025-09-16) - get-terminal-stderr: func() -> option; -} diff --git a/examples/wasip3/wit/deps/clocks.wit b/examples/wasip3/wit/deps/clocks.wit new file mode 100644 index 0000000..19fc4bc --- /dev/null +++ b/examples/wasip3/wit/deps/clocks.wit @@ -0,0 +1,161 @@ +package wasi:clocks@0.3.0-rc-2026-03-15; + +/// This interface common types used throughout wasi:clocks. +@since(version = 0.3.0-rc-2026-03-15) +interface types { + /// A duration of time, in nanoseconds. + @since(version = 0.3.0-rc-2026-03-15) + type duration = u64; +} + +/// WASI Monotonic Clock is a clock API intended to let users measure elapsed +/// time. +/// +/// It is intended to be portable at least between Unix-family platforms and +/// Windows. +/// +/// A monotonic clock is a clock which has an unspecified initial value, and +/// successive reads of the clock will produce non-decreasing values. +@since(version = 0.3.0-rc-2026-03-15) +interface monotonic-clock { + use types.{duration}; + + /// A mark on a monotonic clock is a number of nanoseconds since an + /// unspecified initial value, and can only be compared to instances from + /// the same monotonic-clock. + @since(version = 0.3.0-rc-2026-03-15) + type mark = u64; + + /// Read the current value of the clock. + /// + /// The clock is monotonic, therefore calling this function repeatedly will + /// produce a sequence of non-decreasing values. + /// + /// For completeness, this function traps if it's not possible to represent + /// the value of the clock in a `mark`. Consequently, implementations + /// should ensure that the starting time is low enough to avoid the + /// possibility of overflow in practice. + @since(version = 0.3.0-rc-2026-03-15) + now: func() -> mark; + + /// Query the resolution of the clock. Returns the duration of time + /// corresponding to a clock tick. + @since(version = 0.3.0-rc-2026-03-15) + get-resolution: func() -> duration; + + /// Wait until the specified mark has occurred. + @since(version = 0.3.0-rc-2026-03-15) + wait-until: async func(when: mark); + + /// Wait for the specified duration to elapse. + @since(version = 0.3.0-rc-2026-03-15) + wait-for: async func(how-long: duration); +} + +/// WASI System Clock is a clock API intended to let users query the current +/// time. The clock is not necessarily monotonic as it may be reset. +/// +/// It is intended to be portable at least between Unix-family platforms and +/// Windows. +/// +/// External references may be reset, so this clock is not necessarily +/// monotonic, making it unsuitable for measuring elapsed time. +/// +/// It is intended for reporting the current date and time for humans. +@since(version = 0.3.0-rc-2026-03-15) +interface system-clock { + use types.{duration}; + + /// An "instant", or "exact time", is a point in time without regard to any + /// time zone: just the time since a particular external reference point, + /// often called an "epoch". + /// + /// Here, the epoch is 1970-01-01T00:00:00Z, also known as + /// [POSIX's Seconds Since the Epoch], also known as [Unix Time]. + /// + /// Note that even if the seconds field is negative, incrementing + /// nanoseconds always represents moving forwards in time. + /// For example, `{ -1 seconds, 999999999 nanoseconds }` represents the + /// instant one nanosecond before the epoch. + /// For more on various different ways to represent time, see + /// https://tc39.es/proposal-temporal/docs/timezone.html + /// + /// [POSIX's Seconds Since the Epoch]: https://pubs.opengroup.org/onlinepubs/9699919799/xrat/V4_xbd_chap04.html#tag_21_04_16 + /// [Unix Time]: https://en.wikipedia.org/wiki/Unix_time + @since(version = 0.3.0-rc-2026-03-15) + record instant { + seconds: s64, + nanoseconds: u32, + } + + /// Read the current value of the clock. + /// + /// This clock is not monotonic, therefore calling this function repeatedly + /// will not necessarily produce a sequence of non-decreasing values. + /// + /// The nanoseconds field of the output is always less than 1000000000. + @since(version = 0.3.0-rc-2026-03-15) + now: func() -> instant; + + /// Query the resolution of the clock. Returns the smallest duration of time + /// that the implementation permits distinguishing. + @since(version = 0.3.0-rc-2026-03-15) + get-resolution: func() -> duration; +} + +@unstable(feature = clocks-timezone) +interface timezone { + @unstable(feature = clocks-timezone) + use system-clock.{instant}; + + /// Return the IANA identifier of the currently configured timezone. This + /// should be an identifier from the IANA Time Zone Database. + /// + /// For displaying to a user, the identifier should be converted into a + /// localized name by means of an internationalization API. + /// + /// If the implementation does not expose an actual timezone, or is unable + /// to provide mappings from times to deltas between the configured timezone + /// and UTC, or determining the current timezone fails, or the timezone does + /// not have an IANA identifier, this returns nothing. + @unstable(feature = clocks-timezone) + iana-id: func() -> option; + + /// The number of nanoseconds difference between UTC time and the local + /// time of the currently configured timezone, at the exact time of + /// `instant`. + /// + /// The magnitude of the returned value will always be less than + /// 86,400,000,000,000 which is the number of nanoseconds in a day + /// (24*60*60*1e9). + /// + /// If the implementation does not expose an actual timezone, or is unable + /// to provide mappings from times to deltas between the configured timezone + /// and UTC, or determining the current timezone fails, this returns + /// nothing. + @unstable(feature = clocks-timezone) + utc-offset: func(when: instant) -> option; + + /// Returns a string that is suitable to assist humans in debugging whether + /// any timezone is available, and if so, which. This may be the same string + /// as `iana-id`, or a formatted representation of the UTC offset such as + /// `-04:00`, or something else. + /// + /// WARNING: The returned string should not be consumed mechanically! It may + /// change across platforms, hosts, or other implementation details. Parsing + /// this string is a major platform-compatibility hazard. + @unstable(feature = clocks-timezone) + to-debug-string: func() -> string; +} + +@since(version = 0.3.0-rc-2026-03-15) +world imports { + @since(version = 0.3.0-rc-2026-03-15) + import types; + @since(version = 0.3.0-rc-2026-03-15) + import monotonic-clock; + @since(version = 0.3.0-rc-2026-03-15) + import system-clock; + @unstable(feature = clocks-timezone) + import timezone; +} diff --git a/examples/wasip3/wit/deps/clocks/monotonic-clock.wit b/examples/wasip3/wit/deps/clocks/monotonic-clock.wit deleted file mode 100644 index a91d495..0000000 --- a/examples/wasip3/wit/deps/clocks/monotonic-clock.wit +++ /dev/null @@ -1,48 +0,0 @@ -package wasi:clocks@0.3.0-rc-2025-09-16; -/// WASI Monotonic Clock is a clock API intended to let users measure elapsed -/// time. -/// -/// It is intended to be portable at least between Unix-family platforms and -/// Windows. -/// -/// A monotonic clock is a clock which has an unspecified initial value, and -/// successive reads of the clock will produce non-decreasing values. -@since(version = 0.3.0-rc-2025-09-16) -interface monotonic-clock { - use types.{duration}; - - /// An instant in time, in nanoseconds. An instant is relative to an - /// unspecified initial value, and can only be compared to instances from - /// the same monotonic-clock. - @since(version = 0.3.0-rc-2025-09-16) - type instant = u64; - - /// Read the current value of the clock. - /// - /// The clock is monotonic, therefore calling this function repeatedly will - /// produce a sequence of non-decreasing values. - /// - /// For completeness, this function traps if it's not possible to represent - /// the value of the clock in an `instant`. Consequently, implementations - /// should ensure that the starting time is low enough to avoid the - /// possibility of overflow in practice. - @since(version = 0.3.0-rc-2025-09-16) - now: func() -> instant; - - /// Query the resolution of the clock. Returns the duration of time - /// corresponding to a clock tick. - @since(version = 0.3.0-rc-2025-09-16) - get-resolution: func() -> duration; - - /// Wait until the specified instant has occurred. - @since(version = 0.3.0-rc-2025-09-16) - wait-until: async func( - when: instant, - ); - - /// Wait for the specified duration to elapse. - @since(version = 0.3.0-rc-2025-09-16) - wait-for: async func( - how-long: duration, - ); -} diff --git a/examples/wasip3/wit/deps/clocks/timezone.wit b/examples/wasip3/wit/deps/clocks/timezone.wit deleted file mode 100644 index ab8f5c0..0000000 --- a/examples/wasip3/wit/deps/clocks/timezone.wit +++ /dev/null @@ -1,55 +0,0 @@ -package wasi:clocks@0.3.0-rc-2025-09-16; - -@unstable(feature = clocks-timezone) -interface timezone { - @unstable(feature = clocks-timezone) - use wall-clock.{datetime}; - - /// Return information needed to display the given `datetime`. This includes - /// the UTC offset, the time zone name, and a flag indicating whether - /// daylight saving time is active. - /// - /// If the timezone cannot be determined for the given `datetime`, return a - /// `timezone-display` for `UTC` with a `utc-offset` of 0 and no daylight - /// saving time. - @unstable(feature = clocks-timezone) - display: func(when: datetime) -> timezone-display; - - /// The same as `display`, but only return the UTC offset. - @unstable(feature = clocks-timezone) - utc-offset: func(when: datetime) -> s32; - - /// Information useful for displaying the timezone of a specific `datetime`. - /// - /// This information may vary within a single `timezone` to reflect daylight - /// saving time adjustments. - @unstable(feature = clocks-timezone) - record timezone-display { - /// The number of seconds difference between UTC time and the local - /// time of the timezone. - /// - /// The returned value will always be less than 86400 which is the - /// number of seconds in a day (24*60*60). - /// - /// In implementations that do not expose an actual time zone, this - /// should return 0. - utc-offset: s32, - - /// The abbreviated name of the timezone to display to a user. The name - /// `UTC` indicates Coordinated Universal Time. Otherwise, this should - /// reference local standards for the name of the time zone. - /// - /// In implementations that do not expose an actual time zone, this - /// should be the string `UTC`. - /// - /// In time zones that do not have an applicable name, a formatted - /// representation of the UTC offset may be returned, such as `-04:00`. - name: string, - - /// Whether daylight saving time is active. - /// - /// In implementations that do not expose an actual time zone, this - /// should return false. - in-daylight-saving-time: bool, - } -} diff --git a/examples/wasip3/wit/deps/clocks/types.wit b/examples/wasip3/wit/deps/clocks/types.wit deleted file mode 100644 index aff7c2a..0000000 --- a/examples/wasip3/wit/deps/clocks/types.wit +++ /dev/null @@ -1,8 +0,0 @@ -package wasi:clocks@0.3.0-rc-2025-09-16; -/// This interface common types used throughout wasi:clocks. -@since(version = 0.3.0-rc-2025-09-16) -interface types { - /// A duration of time, in nanoseconds. - @since(version = 0.3.0-rc-2025-09-16) - type duration = u64; -} diff --git a/examples/wasip3/wit/deps/clocks/wall-clock.wit b/examples/wasip3/wit/deps/clocks/wall-clock.wit deleted file mode 100644 index ea94050..0000000 --- a/examples/wasip3/wit/deps/clocks/wall-clock.wit +++ /dev/null @@ -1,46 +0,0 @@ -package wasi:clocks@0.3.0-rc-2025-09-16; -/// WASI Wall Clock is a clock API intended to let users query the current -/// time. The name "wall" makes an analogy to a "clock on the wall", which -/// is not necessarily monotonic as it may be reset. -/// -/// It is intended to be portable at least between Unix-family platforms and -/// Windows. -/// -/// A wall clock is a clock which measures the date and time according to -/// some external reference. -/// -/// External references may be reset, so this clock is not necessarily -/// monotonic, making it unsuitable for measuring elapsed time. -/// -/// It is intended for reporting the current date and time for humans. -@since(version = 0.3.0-rc-2025-09-16) -interface wall-clock { - /// A time and date in seconds plus nanoseconds. - @since(version = 0.3.0-rc-2025-09-16) - record datetime { - seconds: u64, - nanoseconds: u32, - } - - /// Read the current value of the clock. - /// - /// This clock is not monotonic, therefore calling this function repeatedly - /// will not necessarily produce a sequence of non-decreasing values. - /// - /// The returned timestamps represent the number of seconds since - /// 1970-01-01T00:00:00Z, also known as [POSIX's Seconds Since the Epoch], - /// also known as [Unix Time]. - /// - /// The nanoseconds field of the output is always less than 1000000000. - /// - /// [POSIX's Seconds Since the Epoch]: https://pubs.opengroup.org/onlinepubs/9699919799/xrat/V4_xbd_chap04.html#tag_21_04_16 - /// [Unix Time]: https://en.wikipedia.org/wiki/Unix_time - @since(version = 0.3.0-rc-2025-09-16) - now: func() -> datetime; - - /// Query the resolution of the clock. - /// - /// The nanoseconds field of the output is always less than 1000000000. - @since(version = 0.3.0-rc-2025-09-16) - get-resolution: func() -> datetime; -} diff --git a/examples/wasip3/wit/deps/clocks/world.wit b/examples/wasip3/wit/deps/clocks/world.wit deleted file mode 100644 index a6b885f..0000000 --- a/examples/wasip3/wit/deps/clocks/world.wit +++ /dev/null @@ -1,11 +0,0 @@ -package wasi:clocks@0.3.0-rc-2025-09-16; - -@since(version = 0.3.0-rc-2025-09-16) -world imports { - @since(version = 0.3.0-rc-2025-09-16) - import monotonic-clock; - @since(version = 0.3.0-rc-2025-09-16) - import wall-clock; - @unstable(feature = clocks-timezone) - import timezone; -} diff --git a/examples/wasip3/wit/deps/filesystem.wit b/examples/wasip3/wit/deps/filesystem.wit new file mode 100644 index 0000000..697681f --- /dev/null +++ b/examples/wasip3/wit/deps/filesystem.wit @@ -0,0 +1,575 @@ +package wasi:filesystem@0.3.0-rc-2026-03-15; + +/// WASI filesystem is a filesystem API primarily intended to let users run WASI +/// programs that access their files on their existing filesystems, without +/// significant overhead. +/// +/// Paths are passed as interface-type `string`s, meaning they must consist of +/// a sequence of Unicode Scalar Values (USVs). Some filesystems may contain +/// paths which are not accessible by this API. +/// +/// The directory separator in WASI is always the forward-slash (`/`). +/// +/// All paths in WASI are relative paths, and are interpreted relative to a +/// `descriptor` referring to a base directory. If a `path` argument to any WASI +/// function starts with `/`, or if any step of resolving a `path`, including +/// `..` and symbolic link steps, reaches a directory outside of the base +/// directory, or reaches a symlink to an absolute or rooted path in the +/// underlying filesystem, the function fails with `error-code::not-permitted`. +/// +/// For more information about WASI path resolution and sandboxing, see +/// [WASI filesystem path resolution]. +/// +/// Though this package presents a portable interface modelled on POSIX, it +/// prioritizes compatibility over portability: allowing users to access their +/// files on their machine is more important than exposing a single semantics +/// across all platforms. Notably, depending on the underlying operating system +/// and file system: +/// * Paths may be case-folded or not. +/// * Deleting (unlinking) a file may fail if there are other file descriptors +/// open. +/// * Durability and atomicity of changes to underlying files when there are +/// concurrent writers. +/// +/// Users that need well-defined, portable semantics should use a key-value +/// store or a database instead. +/// +/// [WASI filesystem path resolution]: https://github.com/WebAssembly/wasi-filesystem/blob/main/path-resolution.md +@since(version = 0.3.0-rc-2026-03-15) +interface types { + @since(version = 0.3.0-rc-2026-03-15) + use wasi:clocks/system-clock@0.3.0-rc-2026-03-15.{instant}; + + /// File size or length of a region within a file. + @since(version = 0.3.0-rc-2026-03-15) + type filesize = u64; + + /// The type of a filesystem object referenced by a descriptor. + /// + /// Note: This was called `filetype` in earlier versions of WASI. + @since(version = 0.3.0-rc-2026-03-15) + variant descriptor-type { + /// The descriptor refers to a block device inode. + block-device, + /// The descriptor refers to a character device inode. + character-device, + /// The descriptor refers to a directory inode. + directory, + /// The descriptor refers to a named pipe. + fifo, + /// The file refers to a symbolic link inode. + symbolic-link, + /// The descriptor refers to a regular file inode. + regular-file, + /// The descriptor refers to a socket. + socket, + /// The type of the descriptor or file is different from any of the + /// other types specified. + other(option), + } + + /// Descriptor flags. + /// + /// Note: This was called `fdflags` in earlier versions of WASI. + @since(version = 0.3.0-rc-2026-03-15) + flags descriptor-flags { + /// Read mode: Data can be read. + read, + /// Write mode: Data can be written to. + write, + /// Request that writes be performed according to synchronized I/O file + /// integrity completion. The data stored in the file and the file's + /// metadata are synchronized. This is similar to `O_SYNC` in POSIX. + /// + /// The precise semantics of this operation have not yet been defined for + /// WASI. At this time, it should be interpreted as a request, and not a + /// requirement. + file-integrity-sync, + /// Request that writes be performed according to synchronized I/O data + /// integrity completion. Only the data stored in the file is + /// synchronized. This is similar to `O_DSYNC` in POSIX. + /// + /// The precise semantics of this operation have not yet been defined for + /// WASI. At this time, it should be interpreted as a request, and not a + /// requirement. + data-integrity-sync, + /// Requests that reads be performed at the same level of integrity + /// requested for writes. This is similar to `O_RSYNC` in POSIX. + /// + /// The precise semantics of this operation have not yet been defined for + /// WASI. At this time, it should be interpreted as a request, and not a + /// requirement. + requested-write-sync, + /// Mutating directories mode: Directory contents may be mutated. + /// + /// When this flag is unset on a descriptor, operations using the + /// descriptor which would create, rename, delete, modify the data or + /// metadata of filesystem objects, or obtain another handle which + /// would permit any of those, shall fail with `error-code::read-only` if + /// they would otherwise succeed. + /// + /// This may only be set on directories. + mutate-directory, + } + + /// Flags determining the method of how paths are resolved. + @since(version = 0.3.0-rc-2026-03-15) + flags path-flags { + /// As long as the resolved path corresponds to a symbolic link, it is + /// expanded. + symlink-follow, + } + + /// Open flags used by `open-at`. + @since(version = 0.3.0-rc-2026-03-15) + flags open-flags { + /// Create file if it does not exist, similar to `O_CREAT` in POSIX. + create, + /// Fail if not a directory, similar to `O_DIRECTORY` in POSIX. + directory, + /// Fail if file already exists, similar to `O_EXCL` in POSIX. + exclusive, + /// Truncate file to size 0, similar to `O_TRUNC` in POSIX. + truncate, + } + + /// Number of hard links to an inode. + @since(version = 0.3.0-rc-2026-03-15) + type link-count = u64; + + /// File attributes. + /// + /// Note: This was called `filestat` in earlier versions of WASI. + @since(version = 0.3.0-rc-2026-03-15) + record descriptor-stat { + /// File type. + %type: descriptor-type, + /// Number of hard links to the file. + link-count: link-count, + /// For regular files, the file size in bytes. For symbolic links, the + /// length in bytes of the pathname contained in the symbolic link. + size: filesize, + /// Last data access timestamp. + /// + /// If the `option` is none, the platform doesn't maintain an access + /// timestamp for this file. + data-access-timestamp: option, + /// Last data modification timestamp. + /// + /// If the `option` is none, the platform doesn't maintain a + /// modification timestamp for this file. + data-modification-timestamp: option, + /// Last file status-change timestamp. + /// + /// If the `option` is none, the platform doesn't maintain a + /// status-change timestamp for this file. + status-change-timestamp: option, + } + + /// When setting a timestamp, this gives the value to set it to. + @since(version = 0.3.0-rc-2026-03-15) + variant new-timestamp { + /// Leave the timestamp set to its previous value. + no-change, + /// Set the timestamp to the current time of the system clock associated + /// with the filesystem. + now, + /// Set the timestamp to the given value. + timestamp(instant), + } + + /// A directory entry. + @since(version = 0.3.0-rc-2026-03-15) + record directory-entry { + /// The type of the file referred to by this directory entry. + %type: descriptor-type, + /// The name of the object. + name: string, + } + + /// Error codes returned by functions, similar to `errno` in POSIX. + /// Not all of these error codes are returned by the functions provided by this + /// API; some are used in higher-level library layers, and others are provided + /// merely for alignment with POSIX. + @since(version = 0.3.0-rc-2026-03-15) + variant error-code { + /// Permission denied, similar to `EACCES` in POSIX. + access, + /// Connection already in progress, similar to `EALREADY` in POSIX. + already, + /// Bad descriptor, similar to `EBADF` in POSIX. + bad-descriptor, + /// Device or resource busy, similar to `EBUSY` in POSIX. + busy, + /// Resource deadlock would occur, similar to `EDEADLK` in POSIX. + deadlock, + /// Storage quota exceeded, similar to `EDQUOT` in POSIX. + quota, + /// File exists, similar to `EEXIST` in POSIX. + exist, + /// File too large, similar to `EFBIG` in POSIX. + file-too-large, + /// Illegal byte sequence, similar to `EILSEQ` in POSIX. + illegal-byte-sequence, + /// Operation in progress, similar to `EINPROGRESS` in POSIX. + in-progress, + /// Interrupted function, similar to `EINTR` in POSIX. + interrupted, + /// Invalid argument, similar to `EINVAL` in POSIX. + invalid, + /// I/O error, similar to `EIO` in POSIX. + io, + /// Is a directory, similar to `EISDIR` in POSIX. + is-directory, + /// Too many levels of symbolic links, similar to `ELOOP` in POSIX. + loop, + /// Too many links, similar to `EMLINK` in POSIX. + too-many-links, + /// Message too large, similar to `EMSGSIZE` in POSIX. + message-size, + /// Filename too long, similar to `ENAMETOOLONG` in POSIX. + name-too-long, + /// No such device, similar to `ENODEV` in POSIX. + no-device, + /// No such file or directory, similar to `ENOENT` in POSIX. + no-entry, + /// No locks available, similar to `ENOLCK` in POSIX. + no-lock, + /// Not enough space, similar to `ENOMEM` in POSIX. + insufficient-memory, + /// No space left on device, similar to `ENOSPC` in POSIX. + insufficient-space, + /// Not a directory or a symbolic link to a directory, similar to `ENOTDIR` in POSIX. + not-directory, + /// Directory not empty, similar to `ENOTEMPTY` in POSIX. + not-empty, + /// State not recoverable, similar to `ENOTRECOVERABLE` in POSIX. + not-recoverable, + /// Not supported, similar to `ENOTSUP` and `ENOSYS` in POSIX. + unsupported, + /// Inappropriate I/O control operation, similar to `ENOTTY` in POSIX. + no-tty, + /// No such device or address, similar to `ENXIO` in POSIX. + no-such-device, + /// Value too large to be stored in data type, similar to `EOVERFLOW` in POSIX. + overflow, + /// Operation not permitted, similar to `EPERM` in POSIX. + not-permitted, + /// Broken pipe, similar to `EPIPE` in POSIX. + pipe, + /// Read-only file system, similar to `EROFS` in POSIX. + read-only, + /// Invalid seek, similar to `ESPIPE` in POSIX. + invalid-seek, + /// Text file busy, similar to `ETXTBSY` in POSIX. + text-file-busy, + /// Cross-device link, similar to `EXDEV` in POSIX. + cross-device, + /// A catch-all for errors not captured by the existing variants. + /// Implementations can use this to extend the error type without + /// breaking existing code. + other(option), + } + + /// File or memory access pattern advisory information. + @since(version = 0.3.0-rc-2026-03-15) + enum advice { + /// The application has no advice to give on its behavior with respect + /// to the specified data. + normal, + /// The application expects to access the specified data sequentially + /// from lower offsets to higher offsets. + sequential, + /// The application expects to access the specified data in a random + /// order. + random, + /// The application expects to access the specified data in the near + /// future. + will-need, + /// The application expects that it will not access the specified data + /// in the near future. + dont-need, + /// The application expects to access the specified data once and then + /// not reuse it thereafter. + no-reuse, + } + + /// A 128-bit hash value, split into parts because wasm doesn't have a + /// 128-bit integer type. + @since(version = 0.3.0-rc-2026-03-15) + record metadata-hash-value { + /// 64 bits of a 128-bit hash value. + lower: u64, + /// Another 64 bits of a 128-bit hash value. + upper: u64, + } + + /// A descriptor is a reference to a filesystem object, which may be a file, + /// directory, named pipe, special file, or other object on which filesystem + /// calls may be made. + @since(version = 0.3.0-rc-2026-03-15) + resource descriptor { + /// Return a stream for reading from a file. + /// + /// Multiple read, write, and append streams may be active on the same open + /// file and they do not interfere with each other. + /// + /// This function returns a `stream` which provides the data received from the + /// file, and a `future` providing additional error information in case an + /// error is encountered. + /// + /// If no error is encountered, `stream.read` on the `stream` will return + /// `read-status::closed` with no `error-context` and the future resolves to + /// the value `ok`. If an error is encountered, `stream.read` on the + /// `stream` returns `read-status::closed` with an `error-context` and the future + /// resolves to `err` with an `error-code`. + /// + /// Note: This is similar to `pread` in POSIX. + @since(version = 0.3.0-rc-2026-03-15) + read-via-stream: func(offset: filesize) -> tuple, future>>; + /// Return a stream for writing to a file, if available. + /// + /// May fail with an error-code describing why the file cannot be written. + /// + /// It is valid to write past the end of a file; the file is extended to the + /// extent of the write, with bytes between the previous end and the start of + /// the write set to zero. + /// + /// This function returns once either full contents of the stream are + /// written or an error is encountered. + /// + /// Note: This is similar to `pwrite` in POSIX. + @since(version = 0.3.0-rc-2026-03-15) + write-via-stream: func(data: stream, offset: filesize) -> future>; + /// Return a stream for appending to a file, if available. + /// + /// May fail with an error-code describing why the file cannot be appended. + /// + /// This function returns once either full contents of the stream are + /// written or an error is encountered. + /// + /// Note: This is similar to `write` with `O_APPEND` in POSIX. + @since(version = 0.3.0-rc-2026-03-15) + append-via-stream: func(data: stream) -> future>; + /// Provide file advisory information on a descriptor. + /// + /// This is similar to `posix_fadvise` in POSIX. + @since(version = 0.3.0-rc-2026-03-15) + advise: async func(offset: filesize, length: filesize, advice: advice) -> result<_, error-code>; + /// Synchronize the data of a file to disk. + /// + /// This function succeeds with no effect if the file descriptor is not + /// opened for writing. + /// + /// Note: This is similar to `fdatasync` in POSIX. + @since(version = 0.3.0-rc-2026-03-15) + sync-data: async func() -> result<_, error-code>; + /// Get flags associated with a descriptor. + /// + /// Note: This returns similar flags to `fcntl(fd, F_GETFL)` in POSIX. + /// + /// Note: This returns the value that was the `fs_flags` value returned + /// from `fdstat_get` in earlier versions of WASI. + @since(version = 0.3.0-rc-2026-03-15) + get-flags: async func() -> result; + /// Get the dynamic type of a descriptor. + /// + /// Note: This returns the same value as the `type` field of the `fd-stat` + /// returned by `stat`, `stat-at` and similar. + /// + /// Note: This returns similar flags to the `st_mode & S_IFMT` value provided + /// by `fstat` in POSIX. + /// + /// Note: This returns the value that was the `fs_filetype` value returned + /// from `fdstat_get` in earlier versions of WASI. + @since(version = 0.3.0-rc-2026-03-15) + get-type: async func() -> result; + /// Adjust the size of an open file. If this increases the file's size, the + /// extra bytes are filled with zeros. + /// + /// Note: This was called `fd_filestat_set_size` in earlier versions of WASI. + @since(version = 0.3.0-rc-2026-03-15) + set-size: async func(size: filesize) -> result<_, error-code>; + /// Adjust the timestamps of an open file or directory. + /// + /// Note: This is similar to `futimens` in POSIX. + /// + /// Note: This was called `fd_filestat_set_times` in earlier versions of WASI. + @since(version = 0.3.0-rc-2026-03-15) + set-times: async func(data-access-timestamp: new-timestamp, data-modification-timestamp: new-timestamp) -> result<_, error-code>; + /// Read directory entries from a directory. + /// + /// On filesystems where directories contain entries referring to themselves + /// and their parents, often named `.` and `..` respectively, these entries + /// are omitted. + /// + /// This always returns a new stream which starts at the beginning of the + /// directory. Multiple streams may be active on the same directory, and they + /// do not interfere with each other. + /// + /// This function returns a future, which will resolve to an error code if + /// reading full contents of the directory fails. + @since(version = 0.3.0-rc-2026-03-15) + read-directory: func() -> tuple, future>>; + /// Synchronize the data and metadata of a file to disk. + /// + /// This function succeeds with no effect if the file descriptor is not + /// opened for writing. + /// + /// Note: This is similar to `fsync` in POSIX. + @since(version = 0.3.0-rc-2026-03-15) + sync: async func() -> result<_, error-code>; + /// Create a directory. + /// + /// Note: This is similar to `mkdirat` in POSIX. + @since(version = 0.3.0-rc-2026-03-15) + create-directory-at: async func(path: string) -> result<_, error-code>; + /// Return the attributes of an open file or directory. + /// + /// Note: This is similar to `fstat` in POSIX, except that it does not return + /// device and inode information. For testing whether two descriptors refer to + /// the same underlying filesystem object, use `is-same-object`. To obtain + /// additional data that can be used do determine whether a file has been + /// modified, use `metadata-hash`. + /// + /// Note: This was called `fd_filestat_get` in earlier versions of WASI. + @since(version = 0.3.0-rc-2026-03-15) + stat: async func() -> result; + /// Return the attributes of a file or directory. + /// + /// Note: This is similar to `fstatat` in POSIX, except that it does not + /// return device and inode information. See the `stat` description for a + /// discussion of alternatives. + /// + /// Note: This was called `path_filestat_get` in earlier versions of WASI. + @since(version = 0.3.0-rc-2026-03-15) + stat-at: async func(path-flags: path-flags, path: string) -> result; + /// Adjust the timestamps of a file or directory. + /// + /// Note: This is similar to `utimensat` in POSIX. + /// + /// Note: This was called `path_filestat_set_times` in earlier versions of + /// WASI. + @since(version = 0.3.0-rc-2026-03-15) + set-times-at: async func(path-flags: path-flags, path: string, data-access-timestamp: new-timestamp, data-modification-timestamp: new-timestamp) -> result<_, error-code>; + /// Create a hard link. + /// + /// Fails with `error-code::no-entry` if the old path does not exist, + /// with `error-code::exist` if the new path already exists, and + /// `error-code::not-permitted` if the old path is not a file. + /// + /// Note: This is similar to `linkat` in POSIX. + @since(version = 0.3.0-rc-2026-03-15) + link-at: async func(old-path-flags: path-flags, old-path: string, new-descriptor: borrow, new-path: string) -> result<_, error-code>; + /// Open a file or directory. + /// + /// If `flags` contains `descriptor-flags::mutate-directory`, and the base + /// descriptor doesn't have `descriptor-flags::mutate-directory` set, + /// `open-at` fails with `error-code::read-only`. + /// + /// If `flags` contains `write` or `mutate-directory`, or `open-flags` + /// contains `truncate` or `create`, and the base descriptor doesn't have + /// `descriptor-flags::mutate-directory` set, `open-at` fails with + /// `error-code::read-only`. + /// + /// Note: This is similar to `openat` in POSIX. + @since(version = 0.3.0-rc-2026-03-15) + open-at: async func(path-flags: path-flags, path: string, open-flags: open-flags, %flags: descriptor-flags) -> result; + /// Read the contents of a symbolic link. + /// + /// If the contents contain an absolute or rooted path in the underlying + /// filesystem, this function fails with `error-code::not-permitted`. + /// + /// Note: This is similar to `readlinkat` in POSIX. + @since(version = 0.3.0-rc-2026-03-15) + readlink-at: async func(path: string) -> result; + /// Remove a directory. + /// + /// Return `error-code::not-empty` if the directory is not empty. + /// + /// Note: This is similar to `unlinkat(fd, path, AT_REMOVEDIR)` in POSIX. + @since(version = 0.3.0-rc-2026-03-15) + remove-directory-at: async func(path: string) -> result<_, error-code>; + /// Rename a filesystem object. + /// + /// Note: This is similar to `renameat` in POSIX. + @since(version = 0.3.0-rc-2026-03-15) + rename-at: async func(old-path: string, new-descriptor: borrow, new-path: string) -> result<_, error-code>; + /// Create a symbolic link (also known as a "symlink"). + /// + /// If `old-path` starts with `/`, the function fails with + /// `error-code::not-permitted`. + /// + /// Note: This is similar to `symlinkat` in POSIX. + @since(version = 0.3.0-rc-2026-03-15) + symlink-at: async func(old-path: string, new-path: string) -> result<_, error-code>; + /// Unlink a filesystem object that is not a directory. + /// + /// This is similar to `unlinkat(fd, path, 0)` in POSIX. + /// + /// Error returns are as specified by POSIX. + /// + /// If the filesystem object is a directory, `error-code::access` or + /// `error-code::is-directory` may be returned instead of the + /// POSIX-specified `error-code::not-permitted`. + @since(version = 0.3.0-rc-2026-03-15) + unlink-file-at: async func(path: string) -> result<_, error-code>; + /// Test whether two descriptors refer to the same filesystem object. + /// + /// In POSIX, this corresponds to testing whether the two descriptors have the + /// same device (`st_dev`) and inode (`st_ino` or `d_ino`) numbers. + /// wasi-filesystem does not expose device and inode numbers, so this function + /// may be used instead. + @since(version = 0.3.0-rc-2026-03-15) + is-same-object: async func(other: borrow) -> bool; + /// Return a hash of the metadata associated with a filesystem object referred + /// to by a descriptor. + /// + /// This returns a hash of the last-modification timestamp and file size, and + /// may also include the inode number, device number, birth timestamp, and + /// other metadata fields that may change when the file is modified or + /// replaced. It may also include a secret value chosen by the + /// implementation and not otherwise exposed. + /// + /// Implementations are encouraged to provide the following properties: + /// + /// - If the file is not modified or replaced, the computed hash value should + /// usually not change. + /// - If the object is modified or replaced, the computed hash value should + /// usually change. + /// - The inputs to the hash should not be easily computable from the + /// computed hash. + /// + /// However, none of these is required. + @since(version = 0.3.0-rc-2026-03-15) + metadata-hash: async func() -> result; + /// Return a hash of the metadata associated with a filesystem object referred + /// to by a directory descriptor and a relative path. + /// + /// This performs the same hash computation as `metadata-hash`. + @since(version = 0.3.0-rc-2026-03-15) + metadata-hash-at: async func(path-flags: path-flags, path: string) -> result; + } +} + +@since(version = 0.3.0-rc-2026-03-15) +interface preopens { + @since(version = 0.3.0-rc-2026-03-15) + use types.{descriptor}; + + /// Return the set of preopened directories, and their paths. + @since(version = 0.3.0-rc-2026-03-15) + get-directories: func() -> list>; +} + +@since(version = 0.3.0-rc-2026-03-15) +world imports { + @since(version = 0.3.0-rc-2026-03-15) + import wasi:clocks/types@0.3.0-rc-2026-03-15; + @since(version = 0.3.0-rc-2026-03-15) + import wasi:clocks/system-clock@0.3.0-rc-2026-03-15; + @since(version = 0.3.0-rc-2026-03-15) + import types; + @since(version = 0.3.0-rc-2026-03-15) + import preopens; +} diff --git a/examples/wasip3/wit/deps/filesystem/preopens.wit b/examples/wasip3/wit/deps/filesystem/preopens.wit deleted file mode 100644 index 9036e90..0000000 --- a/examples/wasip3/wit/deps/filesystem/preopens.wit +++ /dev/null @@ -1,11 +0,0 @@ -package wasi:filesystem@0.3.0-rc-2025-09-16; - -@since(version = 0.3.0-rc-2025-09-16) -interface preopens { - @since(version = 0.3.0-rc-2025-09-16) - use types.{descriptor}; - - /// Return the set of preopened directories, and their paths. - @since(version = 0.3.0-rc-2025-09-16) - get-directories: func() -> list>; -} diff --git a/examples/wasip3/wit/deps/filesystem/types.wit b/examples/wasip3/wit/deps/filesystem/types.wit deleted file mode 100644 index 41d91be..0000000 --- a/examples/wasip3/wit/deps/filesystem/types.wit +++ /dev/null @@ -1,636 +0,0 @@ -package wasi:filesystem@0.3.0-rc-2025-09-16; -/// WASI filesystem is a filesystem API primarily intended to let users run WASI -/// programs that access their files on their existing filesystems, without -/// significant overhead. -/// -/// It is intended to be roughly portable between Unix-family platforms and -/// Windows, though it does not hide many of the major differences. -/// -/// Paths are passed as interface-type `string`s, meaning they must consist of -/// a sequence of Unicode Scalar Values (USVs). Some filesystems may contain -/// paths which are not accessible by this API. -/// -/// The directory separator in WASI is always the forward-slash (`/`). -/// -/// All paths in WASI are relative paths, and are interpreted relative to a -/// `descriptor` referring to a base directory. If a `path` argument to any WASI -/// function starts with `/`, or if any step of resolving a `path`, including -/// `..` and symbolic link steps, reaches a directory outside of the base -/// directory, or reaches a symlink to an absolute or rooted path in the -/// underlying filesystem, the function fails with `error-code::not-permitted`. -/// -/// For more information about WASI path resolution and sandboxing, see -/// [WASI filesystem path resolution]. -/// -/// [WASI filesystem path resolution]: https://github.com/WebAssembly/wasi-filesystem/blob/main/path-resolution.md -@since(version = 0.3.0-rc-2025-09-16) -interface types { - @since(version = 0.3.0-rc-2025-09-16) - use wasi:clocks/wall-clock@0.3.0-rc-2025-09-16.{datetime}; - - /// File size or length of a region within a file. - @since(version = 0.3.0-rc-2025-09-16) - type filesize = u64; - - /// The type of a filesystem object referenced by a descriptor. - /// - /// Note: This was called `filetype` in earlier versions of WASI. - @since(version = 0.3.0-rc-2025-09-16) - enum descriptor-type { - /// The type of the descriptor or file is unknown or is different from - /// any of the other types specified. - unknown, - /// The descriptor refers to a block device inode. - block-device, - /// The descriptor refers to a character device inode. - character-device, - /// The descriptor refers to a directory inode. - directory, - /// The descriptor refers to a named pipe. - fifo, - /// The file refers to a symbolic link inode. - symbolic-link, - /// The descriptor refers to a regular file inode. - regular-file, - /// The descriptor refers to a socket. - socket, - } - - /// Descriptor flags. - /// - /// Note: This was called `fdflags` in earlier versions of WASI. - @since(version = 0.3.0-rc-2025-09-16) - flags descriptor-flags { - /// Read mode: Data can be read. - read, - /// Write mode: Data can be written to. - write, - /// Request that writes be performed according to synchronized I/O file - /// integrity completion. The data stored in the file and the file's - /// metadata are synchronized. This is similar to `O_SYNC` in POSIX. - /// - /// The precise semantics of this operation have not yet been defined for - /// WASI. At this time, it should be interpreted as a request, and not a - /// requirement. - file-integrity-sync, - /// Request that writes be performed according to synchronized I/O data - /// integrity completion. Only the data stored in the file is - /// synchronized. This is similar to `O_DSYNC` in POSIX. - /// - /// The precise semantics of this operation have not yet been defined for - /// WASI. At this time, it should be interpreted as a request, and not a - /// requirement. - data-integrity-sync, - /// Requests that reads be performed at the same level of integrity - /// requested for writes. This is similar to `O_RSYNC` in POSIX. - /// - /// The precise semantics of this operation have not yet been defined for - /// WASI. At this time, it should be interpreted as a request, and not a - /// requirement. - requested-write-sync, - /// Mutating directories mode: Directory contents may be mutated. - /// - /// When this flag is unset on a descriptor, operations using the - /// descriptor which would create, rename, delete, modify the data or - /// metadata of filesystem objects, or obtain another handle which - /// would permit any of those, shall fail with `error-code::read-only` if - /// they would otherwise succeed. - /// - /// This may only be set on directories. - mutate-directory, - } - - /// File attributes. - /// - /// Note: This was called `filestat` in earlier versions of WASI. - @since(version = 0.3.0-rc-2025-09-16) - record descriptor-stat { - /// File type. - %type: descriptor-type, - /// Number of hard links to the file. - link-count: link-count, - /// For regular files, the file size in bytes. For symbolic links, the - /// length in bytes of the pathname contained in the symbolic link. - size: filesize, - /// Last data access timestamp. - /// - /// If the `option` is none, the platform doesn't maintain an access - /// timestamp for this file. - data-access-timestamp: option, - /// Last data modification timestamp. - /// - /// If the `option` is none, the platform doesn't maintain a - /// modification timestamp for this file. - data-modification-timestamp: option, - /// Last file status-change timestamp. - /// - /// If the `option` is none, the platform doesn't maintain a - /// status-change timestamp for this file. - status-change-timestamp: option, - } - - /// Flags determining the method of how paths are resolved. - @since(version = 0.3.0-rc-2025-09-16) - flags path-flags { - /// As long as the resolved path corresponds to a symbolic link, it is - /// expanded. - symlink-follow, - } - - /// Open flags used by `open-at`. - @since(version = 0.3.0-rc-2025-09-16) - flags open-flags { - /// Create file if it does not exist, similar to `O_CREAT` in POSIX. - create, - /// Fail if not a directory, similar to `O_DIRECTORY` in POSIX. - directory, - /// Fail if file already exists, similar to `O_EXCL` in POSIX. - exclusive, - /// Truncate file to size 0, similar to `O_TRUNC` in POSIX. - truncate, - } - - /// Number of hard links to an inode. - @since(version = 0.3.0-rc-2025-09-16) - type link-count = u64; - - /// When setting a timestamp, this gives the value to set it to. - @since(version = 0.3.0-rc-2025-09-16) - variant new-timestamp { - /// Leave the timestamp set to its previous value. - no-change, - /// Set the timestamp to the current time of the system clock associated - /// with the filesystem. - now, - /// Set the timestamp to the given value. - timestamp(datetime), - } - - /// A directory entry. - record directory-entry { - /// The type of the file referred to by this directory entry. - %type: descriptor-type, - - /// The name of the object. - name: string, - } - - /// Error codes returned by functions, similar to `errno` in POSIX. - /// Not all of these error codes are returned by the functions provided by this - /// API; some are used in higher-level library layers, and others are provided - /// merely for alignment with POSIX. - enum error-code { - /// Permission denied, similar to `EACCES` in POSIX. - access, - /// Connection already in progress, similar to `EALREADY` in POSIX. - already, - /// Bad descriptor, similar to `EBADF` in POSIX. - bad-descriptor, - /// Device or resource busy, similar to `EBUSY` in POSIX. - busy, - /// Resource deadlock would occur, similar to `EDEADLK` in POSIX. - deadlock, - /// Storage quota exceeded, similar to `EDQUOT` in POSIX. - quota, - /// File exists, similar to `EEXIST` in POSIX. - exist, - /// File too large, similar to `EFBIG` in POSIX. - file-too-large, - /// Illegal byte sequence, similar to `EILSEQ` in POSIX. - illegal-byte-sequence, - /// Operation in progress, similar to `EINPROGRESS` in POSIX. - in-progress, - /// Interrupted function, similar to `EINTR` in POSIX. - interrupted, - /// Invalid argument, similar to `EINVAL` in POSIX. - invalid, - /// I/O error, similar to `EIO` in POSIX. - io, - /// Is a directory, similar to `EISDIR` in POSIX. - is-directory, - /// Too many levels of symbolic links, similar to `ELOOP` in POSIX. - loop, - /// Too many links, similar to `EMLINK` in POSIX. - too-many-links, - /// Message too large, similar to `EMSGSIZE` in POSIX. - message-size, - /// Filename too long, similar to `ENAMETOOLONG` in POSIX. - name-too-long, - /// No such device, similar to `ENODEV` in POSIX. - no-device, - /// No such file or directory, similar to `ENOENT` in POSIX. - no-entry, - /// No locks available, similar to `ENOLCK` in POSIX. - no-lock, - /// Not enough space, similar to `ENOMEM` in POSIX. - insufficient-memory, - /// No space left on device, similar to `ENOSPC` in POSIX. - insufficient-space, - /// Not a directory or a symbolic link to a directory, similar to `ENOTDIR` in POSIX. - not-directory, - /// Directory not empty, similar to `ENOTEMPTY` in POSIX. - not-empty, - /// State not recoverable, similar to `ENOTRECOVERABLE` in POSIX. - not-recoverable, - /// Not supported, similar to `ENOTSUP` and `ENOSYS` in POSIX. - unsupported, - /// Inappropriate I/O control operation, similar to `ENOTTY` in POSIX. - no-tty, - /// No such device or address, similar to `ENXIO` in POSIX. - no-such-device, - /// Value too large to be stored in data type, similar to `EOVERFLOW` in POSIX. - overflow, - /// Operation not permitted, similar to `EPERM` in POSIX. - not-permitted, - /// Broken pipe, similar to `EPIPE` in POSIX. - pipe, - /// Read-only file system, similar to `EROFS` in POSIX. - read-only, - /// Invalid seek, similar to `ESPIPE` in POSIX. - invalid-seek, - /// Text file busy, similar to `ETXTBSY` in POSIX. - text-file-busy, - /// Cross-device link, similar to `EXDEV` in POSIX. - cross-device, - } - - /// File or memory access pattern advisory information. - @since(version = 0.3.0-rc-2025-09-16) - enum advice { - /// The application has no advice to give on its behavior with respect - /// to the specified data. - normal, - /// The application expects to access the specified data sequentially - /// from lower offsets to higher offsets. - sequential, - /// The application expects to access the specified data in a random - /// order. - random, - /// The application expects to access the specified data in the near - /// future. - will-need, - /// The application expects that it will not access the specified data - /// in the near future. - dont-need, - /// The application expects to access the specified data once and then - /// not reuse it thereafter. - no-reuse, - } - - /// A 128-bit hash value, split into parts because wasm doesn't have a - /// 128-bit integer type. - @since(version = 0.3.0-rc-2025-09-16) - record metadata-hash-value { - /// 64 bits of a 128-bit hash value. - lower: u64, - /// Another 64 bits of a 128-bit hash value. - upper: u64, - } - - /// A descriptor is a reference to a filesystem object, which may be a file, - /// directory, named pipe, special file, or other object on which filesystem - /// calls may be made. - @since(version = 0.3.0-rc-2025-09-16) - resource descriptor { - /// Return a stream for reading from a file. - /// - /// Multiple read, write, and append streams may be active on the same open - /// file and they do not interfere with each other. - /// - /// This function returns a `stream` which provides the data received from the - /// file, and a `future` providing additional error information in case an - /// error is encountered. - /// - /// If no error is encountered, `stream.read` on the `stream` will return - /// `read-status::closed` with no `error-context` and the future resolves to - /// the value `ok`. If an error is encountered, `stream.read` on the - /// `stream` returns `read-status::closed` with an `error-context` and the future - /// resolves to `err` with an `error-code`. - /// - /// Note: This is similar to `pread` in POSIX. - @since(version = 0.3.0-rc-2025-09-16) - read-via-stream: func( - /// The offset within the file at which to start reading. - offset: filesize, - ) -> tuple, future>>; - - /// Return a stream for writing to a file, if available. - /// - /// May fail with an error-code describing why the file cannot be written. - /// - /// It is valid to write past the end of a file; the file is extended to the - /// extent of the write, with bytes between the previous end and the start of - /// the write set to zero. - /// - /// This function returns once either full contents of the stream are - /// written or an error is encountered. - /// - /// Note: This is similar to `pwrite` in POSIX. - @since(version = 0.3.0-rc-2025-09-16) - write-via-stream: async func( - /// Data to write - data: stream, - /// The offset within the file at which to start writing. - offset: filesize, - ) -> result<_, error-code>; - - /// Return a stream for appending to a file, if available. - /// - /// May fail with an error-code describing why the file cannot be appended. - /// - /// This function returns once either full contents of the stream are - /// written or an error is encountered. - /// - /// Note: This is similar to `write` with `O_APPEND` in POSIX. - @since(version = 0.3.0-rc-2025-09-16) - append-via-stream: async func(data: stream) -> result<_, error-code>; - - /// Provide file advisory information on a descriptor. - /// - /// This is similar to `posix_fadvise` in POSIX. - @since(version = 0.3.0-rc-2025-09-16) - advise: async func( - /// The offset within the file to which the advisory applies. - offset: filesize, - /// The length of the region to which the advisory applies. - length: filesize, - /// The advice. - advice: advice - ) -> result<_, error-code>; - - /// Synchronize the data of a file to disk. - /// - /// This function succeeds with no effect if the file descriptor is not - /// opened for writing. - /// - /// Note: This is similar to `fdatasync` in POSIX. - @since(version = 0.3.0-rc-2025-09-16) - sync-data: async func() -> result<_, error-code>; - - /// Get flags associated with a descriptor. - /// - /// Note: This returns similar flags to `fcntl(fd, F_GETFL)` in POSIX. - /// - /// Note: This returns the value that was the `fs_flags` value returned - /// from `fdstat_get` in earlier versions of WASI. - @since(version = 0.3.0-rc-2025-09-16) - get-flags: async func() -> result; - - /// Get the dynamic type of a descriptor. - /// - /// Note: This returns the same value as the `type` field of the `fd-stat` - /// returned by `stat`, `stat-at` and similar. - /// - /// Note: This returns similar flags to the `st_mode & S_IFMT` value provided - /// by `fstat` in POSIX. - /// - /// Note: This returns the value that was the `fs_filetype` value returned - /// from `fdstat_get` in earlier versions of WASI. - @since(version = 0.3.0-rc-2025-09-16) - get-type: async func() -> result; - - /// Adjust the size of an open file. If this increases the file's size, the - /// extra bytes are filled with zeros. - /// - /// Note: This was called `fd_filestat_set_size` in earlier versions of WASI. - @since(version = 0.3.0-rc-2025-09-16) - set-size: async func(size: filesize) -> result<_, error-code>; - - /// Adjust the timestamps of an open file or directory. - /// - /// Note: This is similar to `futimens` in POSIX. - /// - /// Note: This was called `fd_filestat_set_times` in earlier versions of WASI. - @since(version = 0.3.0-rc-2025-09-16) - set-times: async func( - /// The desired values of the data access timestamp. - data-access-timestamp: new-timestamp, - /// The desired values of the data modification timestamp. - data-modification-timestamp: new-timestamp, - ) -> result<_, error-code>; - - /// Read directory entries from a directory. - /// - /// On filesystems where directories contain entries referring to themselves - /// and their parents, often named `.` and `..` respectively, these entries - /// are omitted. - /// - /// This always returns a new stream which starts at the beginning of the - /// directory. Multiple streams may be active on the same directory, and they - /// do not interfere with each other. - /// - /// This function returns a future, which will resolve to an error code if - /// reading full contents of the directory fails. - @since(version = 0.3.0-rc-2025-09-16) - read-directory: async func() -> tuple, future>>; - - /// Synchronize the data and metadata of a file to disk. - /// - /// This function succeeds with no effect if the file descriptor is not - /// opened for writing. - /// - /// Note: This is similar to `fsync` in POSIX. - @since(version = 0.3.0-rc-2025-09-16) - sync: async func() -> result<_, error-code>; - - /// Create a directory. - /// - /// Note: This is similar to `mkdirat` in POSIX. - @since(version = 0.3.0-rc-2025-09-16) - create-directory-at: async func( - /// The relative path at which to create the directory. - path: string, - ) -> result<_, error-code>; - - /// Return the attributes of an open file or directory. - /// - /// Note: This is similar to `fstat` in POSIX, except that it does not return - /// device and inode information. For testing whether two descriptors refer to - /// the same underlying filesystem object, use `is-same-object`. To obtain - /// additional data that can be used do determine whether a file has been - /// modified, use `metadata-hash`. - /// - /// Note: This was called `fd_filestat_get` in earlier versions of WASI. - @since(version = 0.3.0-rc-2025-09-16) - stat: async func() -> result; - - /// Return the attributes of a file or directory. - /// - /// Note: This is similar to `fstatat` in POSIX, except that it does not - /// return device and inode information. See the `stat` description for a - /// discussion of alternatives. - /// - /// Note: This was called `path_filestat_get` in earlier versions of WASI. - @since(version = 0.3.0-rc-2025-09-16) - stat-at: async func( - /// Flags determining the method of how the path is resolved. - path-flags: path-flags, - /// The relative path of the file or directory to inspect. - path: string, - ) -> result; - - /// Adjust the timestamps of a file or directory. - /// - /// Note: This is similar to `utimensat` in POSIX. - /// - /// Note: This was called `path_filestat_set_times` in earlier versions of - /// WASI. - @since(version = 0.3.0-rc-2025-09-16) - set-times-at: async func( - /// Flags determining the method of how the path is resolved. - path-flags: path-flags, - /// The relative path of the file or directory to operate on. - path: string, - /// The desired values of the data access timestamp. - data-access-timestamp: new-timestamp, - /// The desired values of the data modification timestamp. - data-modification-timestamp: new-timestamp, - ) -> result<_, error-code>; - - /// Create a hard link. - /// - /// Fails with `error-code::no-entry` if the old path does not exist, - /// with `error-code::exist` if the new path already exists, and - /// `error-code::not-permitted` if the old path is not a file. - /// - /// Note: This is similar to `linkat` in POSIX. - @since(version = 0.3.0-rc-2025-09-16) - link-at: async func( - /// Flags determining the method of how the path is resolved. - old-path-flags: path-flags, - /// The relative source path from which to link. - old-path: string, - /// The base directory for `new-path`. - new-descriptor: borrow, - /// The relative destination path at which to create the hard link. - new-path: string, - ) -> result<_, error-code>; - - /// Open a file or directory. - /// - /// If `flags` contains `descriptor-flags::mutate-directory`, and the base - /// descriptor doesn't have `descriptor-flags::mutate-directory` set, - /// `open-at` fails with `error-code::read-only`. - /// - /// If `flags` contains `write` or `mutate-directory`, or `open-flags` - /// contains `truncate` or `create`, and the base descriptor doesn't have - /// `descriptor-flags::mutate-directory` set, `open-at` fails with - /// `error-code::read-only`. - /// - /// Note: This is similar to `openat` in POSIX. - @since(version = 0.3.0-rc-2025-09-16) - open-at: async func( - /// Flags determining the method of how the path is resolved. - path-flags: path-flags, - /// The relative path of the object to open. - path: string, - /// The method by which to open the file. - open-flags: open-flags, - /// Flags to use for the resulting descriptor. - %flags: descriptor-flags, - ) -> result; - - /// Read the contents of a symbolic link. - /// - /// If the contents contain an absolute or rooted path in the underlying - /// filesystem, this function fails with `error-code::not-permitted`. - /// - /// Note: This is similar to `readlinkat` in POSIX. - @since(version = 0.3.0-rc-2025-09-16) - readlink-at: async func( - /// The relative path of the symbolic link from which to read. - path: string, - ) -> result; - - /// Remove a directory. - /// - /// Return `error-code::not-empty` if the directory is not empty. - /// - /// Note: This is similar to `unlinkat(fd, path, AT_REMOVEDIR)` in POSIX. - @since(version = 0.3.0-rc-2025-09-16) - remove-directory-at: async func( - /// The relative path to a directory to remove. - path: string, - ) -> result<_, error-code>; - - /// Rename a filesystem object. - /// - /// Note: This is similar to `renameat` in POSIX. - @since(version = 0.3.0-rc-2025-09-16) - rename-at: async func( - /// The relative source path of the file or directory to rename. - old-path: string, - /// The base directory for `new-path`. - new-descriptor: borrow, - /// The relative destination path to which to rename the file or directory. - new-path: string, - ) -> result<_, error-code>; - - /// Create a symbolic link (also known as a "symlink"). - /// - /// If `old-path` starts with `/`, the function fails with - /// `error-code::not-permitted`. - /// - /// Note: This is similar to `symlinkat` in POSIX. - @since(version = 0.3.0-rc-2025-09-16) - symlink-at: async func( - /// The contents of the symbolic link. - old-path: string, - /// The relative destination path at which to create the symbolic link. - new-path: string, - ) -> result<_, error-code>; - - /// Unlink a filesystem object that is not a directory. - /// - /// Return `error-code::is-directory` if the path refers to a directory. - /// Note: This is similar to `unlinkat(fd, path, 0)` in POSIX. - @since(version = 0.3.0-rc-2025-09-16) - unlink-file-at: async func( - /// The relative path to a file to unlink. - path: string, - ) -> result<_, error-code>; - - /// Test whether two descriptors refer to the same filesystem object. - /// - /// In POSIX, this corresponds to testing whether the two descriptors have the - /// same device (`st_dev`) and inode (`st_ino` or `d_ino`) numbers. - /// wasi-filesystem does not expose device and inode numbers, so this function - /// may be used instead. - @since(version = 0.3.0-rc-2025-09-16) - is-same-object: async func(other: borrow) -> bool; - - /// Return a hash of the metadata associated with a filesystem object referred - /// to by a descriptor. - /// - /// This returns a hash of the last-modification timestamp and file size, and - /// may also include the inode number, device number, birth timestamp, and - /// other metadata fields that may change when the file is modified or - /// replaced. It may also include a secret value chosen by the - /// implementation and not otherwise exposed. - /// - /// Implementations are encouraged to provide the following properties: - /// - /// - If the file is not modified or replaced, the computed hash value should - /// usually not change. - /// - If the object is modified or replaced, the computed hash value should - /// usually change. - /// - The inputs to the hash should not be easily computable from the - /// computed hash. - /// - /// However, none of these is required. - @since(version = 0.3.0-rc-2025-09-16) - metadata-hash: async func() -> result; - - /// Return a hash of the metadata associated with a filesystem object referred - /// to by a directory descriptor and a relative path. - /// - /// This performs the same hash computation as `metadata-hash`. - @since(version = 0.3.0-rc-2025-09-16) - metadata-hash-at: async func( - /// Flags determining the method of how the path is resolved. - path-flags: path-flags, - /// The relative path of the file or directory to inspect. - path: string, - ) -> result; - } -} diff --git a/examples/wasip3/wit/deps/filesystem/world.wit b/examples/wasip3/wit/deps/filesystem/world.wit deleted file mode 100644 index 87fc727..0000000 --- a/examples/wasip3/wit/deps/filesystem/world.wit +++ /dev/null @@ -1,9 +0,0 @@ -package wasi:filesystem@0.3.0-rc-2025-09-16; - -@since(version = 0.3.0-rc-2025-09-16) -world imports { - @since(version = 0.3.0-rc-2025-09-16) - import types; - @since(version = 0.3.0-rc-2025-09-16) - import preopens; -} diff --git a/examples/wasip3/wit/deps/http.wit b/examples/wasip3/wit/deps/http.wit new file mode 100644 index 0000000..c1c1e68 --- /dev/null +++ b/examples/wasip3/wit/deps/http.wit @@ -0,0 +1,509 @@ +package wasi:http@0.3.0-rc-2026-03-15; + +/// This interface defines all of the types and methods for implementing HTTP +/// Requests and Responses, as well as their headers, trailers, and bodies. +@since(version = 0.3.0-rc-2026-03-15) +interface types { + use wasi:clocks/types@0.3.0-rc-2026-03-15.{duration}; + + /// This type corresponds to HTTP standard Methods. + @since(version = 0.3.0-rc-2026-03-15) + variant method { + get, + head, + post, + put, + delete, + connect, + options, + trace, + patch, + other(string), + } + + /// This type corresponds to HTTP standard Related Schemes. + @since(version = 0.3.0-rc-2026-03-15) + variant scheme { + HTTP, + HTTPS, + other(string), + } + + /// Defines the case payload type for `DNS-error` above: + @since(version = 0.3.0-rc-2026-03-15) + record DNS-error-payload { + rcode: option, + info-code: option, + } + + /// Defines the case payload type for `TLS-alert-received` above: + @since(version = 0.3.0-rc-2026-03-15) + record TLS-alert-received-payload { + alert-id: option, + alert-message: option, + } + + /// Defines the case payload type for `HTTP-response-{header,trailer}-size` above: + @since(version = 0.3.0-rc-2026-03-15) + record field-size-payload { + field-name: option, + field-size: option, + } + + /// These cases are inspired by the IANA HTTP Proxy Error Types: + /// + @since(version = 0.3.0-rc-2026-03-15) + variant error-code { + DNS-timeout, + DNS-error(DNS-error-payload), + destination-not-found, + destination-unavailable, + destination-IP-prohibited, + destination-IP-unroutable, + connection-refused, + connection-terminated, + connection-timeout, + connection-read-timeout, + connection-write-timeout, + connection-limit-reached, + TLS-protocol-error, + TLS-certificate-error, + TLS-alert-received(TLS-alert-received-payload), + HTTP-request-denied, + HTTP-request-length-required, + HTTP-request-body-size(option), + HTTP-request-method-invalid, + HTTP-request-URI-invalid, + HTTP-request-URI-too-long, + HTTP-request-header-section-size(option), + HTTP-request-header-size(option), + HTTP-request-trailer-section-size(option), + HTTP-request-trailer-size(field-size-payload), + HTTP-response-incomplete, + HTTP-response-header-section-size(option), + HTTP-response-header-size(field-size-payload), + HTTP-response-body-size(option), + HTTP-response-trailer-section-size(option), + HTTP-response-trailer-size(field-size-payload), + HTTP-response-transfer-coding(option), + HTTP-response-content-coding(option), + HTTP-response-timeout, + HTTP-upgrade-failed, + HTTP-protocol-error, + loop-detected, + configuration-error, + /// This is a catch-all error for anything that doesn't fit cleanly into a + /// more specific case. It also includes an optional string for an + /// unstructured description of the error. Users should not depend on the + /// string for diagnosing errors, as it's not required to be consistent + /// between implementations. + internal-error(option), + } + + /// This type enumerates the different kinds of errors that may occur when + /// setting or appending to a `fields` resource. + @since(version = 0.3.0-rc-2026-03-15) + variant header-error { + /// This error indicates that a `field-name` or `field-value` was + /// syntactically invalid when used with an operation that sets headers in a + /// `fields`. + invalid-syntax, + /// This error indicates that a forbidden `field-name` was used when trying + /// to set a header in a `fields`. + forbidden, + /// This error indicates that the operation on the `fields` was not + /// permitted because the fields are immutable. + immutable, + /// This error indicates that the operation would exceed an + /// implementation-defined limit on field sizes. This may apply to + /// an individual `field-value`, a single `field-name` plus all its + /// values, or the total aggregate size of all fields. + size-exceeded, + /// This is a catch-all error for anything that doesn't fit cleanly into a + /// more specific case. Implementations can use this to extend the error + /// type without breaking existing code. It also includes an optional + /// string for an unstructured description of the error. Users should not + /// depend on the string for diagnosing errors, as it's not required to be + /// consistent between implementations. + other(option), + } + + /// This type enumerates the different kinds of errors that may occur when + /// setting fields of a `request-options` resource. + @since(version = 0.3.0-rc-2026-03-15) + variant request-options-error { + /// Indicates the specified field is not supported by this implementation. + not-supported, + /// Indicates that the operation on the `request-options` was not permitted + /// because it is immutable. + immutable, + /// This is a catch-all error for anything that doesn't fit cleanly into a + /// more specific case. Implementations can use this to extend the error + /// type without breaking existing code. It also includes an optional + /// string for an unstructured description of the error. Users should not + /// depend on the string for diagnosing errors, as it's not required to be + /// consistent between implementations. + other(option), + } + + /// Field names are always strings. + /// + /// Field names should always be treated as case insensitive by the `fields` + /// resource for the purposes of equality checking. + @since(version = 0.3.0-rc-2026-03-15) + type field-name = string; + + /// Field values should always be ASCII strings. However, in + /// reality, HTTP implementations often have to interpret malformed values, + /// so they are provided as a list of bytes. + @since(version = 0.3.0-rc-2026-03-15) + type field-value = list; + + /// This following block defines the `fields` resource which corresponds to + /// HTTP standard Fields. Fields are a common representation used for both + /// Headers and Trailers. + /// + /// A `fields` may be mutable or immutable. A `fields` created using the + /// constructor, `from-list`, or `clone` will be mutable, but a `fields` + /// resource given by other means (including, but not limited to, + /// `request.headers`) might be be immutable. In an immutable fields, the + /// `set`, `append`, and `delete` operations will fail with + /// `header-error.immutable`. + /// + /// A `fields` resource should store `field-name`s and `field-value`s in their + /// original casing used to construct or mutate the `fields` resource. The `fields` + /// resource should use that original casing when serializing the fields for + /// transport or when returning them from a method. + /// + /// Implementations may impose limits on individual field values and on total + /// aggregate field section size. Operations that would exceed these limits + /// fail with `header-error.size-exceeded` + @since(version = 0.3.0-rc-2026-03-15) + resource fields { + /// Construct an empty HTTP Fields. + /// + /// The resulting `fields` is mutable. + constructor(); + /// Construct an HTTP Fields. + /// + /// The resulting `fields` is mutable. + /// + /// The list represents each name-value pair in the Fields. Names + /// which have multiple values are represented by multiple entries in this + /// list with the same name. + /// + /// The tuple is a pair of the field name, represented as a string, and + /// Value, represented as a list of bytes. In a valid Fields, all names + /// and values are valid UTF-8 strings. However, values are not always + /// well-formed, so they are represented as a raw list of bytes. + /// + /// An error result will be returned if any header or value was + /// syntactically invalid, if a header was forbidden, or if the + /// entries would exceed an implementation size limit. + from-list: static func(entries: list>) -> result; + /// Get all of the values corresponding to a name. If the name is not present + /// in this `fields`, an empty list is returned. However, if the name is + /// present but empty, this is represented by a list with one or more + /// empty field-values present. + get: func(name: field-name) -> list; + /// Returns `true` when the name is present in this `fields`. If the name is + /// syntactically invalid, `false` is returned. + has: func(name: field-name) -> bool; + /// Set all of the values for a name. Clears any existing values for that + /// name, if they have been set. + /// + /// Fails with `header-error.immutable` if the `fields` are immutable. + /// + /// Fails with `header-error.size-exceeded` if the name or values would + /// exceed an implementation-defined size limit. + set: func(name: field-name, value: list) -> result<_, header-error>; + /// Delete all values for a name. Does nothing if no values for the name + /// exist. + /// + /// Fails with `header-error.immutable` if the `fields` are immutable. + delete: func(name: field-name) -> result<_, header-error>; + /// Delete all values for a name. Does nothing if no values for the name + /// exist. + /// + /// Returns all values previously corresponding to the name, if any. + /// + /// Fails with `header-error.immutable` if the `fields` are immutable. + get-and-delete: func(name: field-name) -> result, header-error>; + /// Append a value for a name. Does not change or delete any existing + /// values for that name. + /// + /// Fails with `header-error.immutable` if the `fields` are immutable. + /// + /// Fails with `header-error.size-exceeded` if the value would exceed + /// an implementation-defined size limit. + append: func(name: field-name, value: field-value) -> result<_, header-error>; + /// Retrieve the full set of names and values in the Fields. Like the + /// constructor, the list represents each name-value pair. + /// + /// The outer list represents each name-value pair in the Fields. Names + /// which have multiple values are represented by multiple entries in this + /// list with the same name. + /// + /// The names and values are always returned in the original casing and in + /// the order in which they will be serialized for transport. + copy-all: func() -> list>; + /// Make a deep copy of the Fields. Equivalent in behavior to calling the + /// `fields` constructor on the return value of `copy-all`. The resulting + /// `fields` is mutable. + clone: func() -> fields; + } + + /// Headers is an alias for Fields. + @since(version = 0.3.0-rc-2026-03-15) + type headers = fields; + + /// Trailers is an alias for Fields. + @since(version = 0.3.0-rc-2026-03-15) + type trailers = fields; + + /// Represents an HTTP Request. + @since(version = 0.3.0-rc-2026-03-15) + resource request { + /// Construct a new `request` with a default `method` of `GET`, and + /// `none` values for `path-with-query`, `scheme`, and `authority`. + /// + /// `headers` is the HTTP Headers for the Request. + /// + /// `contents` is the optional body content stream with `none` + /// representing a zero-length content stream. + /// Once it is closed, `trailers` future must resolve to a result. + /// If `trailers` resolves to an error, underlying connection + /// will be closed immediately. + /// + /// `options` is optional `request-options` resource to be used + /// if the request is sent over a network connection. + /// + /// It is possible to construct, or manipulate with the accessor functions + /// below, a `request` with an invalid combination of `scheme` + /// and `authority`, or `headers` which are not permitted to be sent. + /// It is the obligation of the `handler.handle` implementation + /// to reject invalid constructions of `request`. + /// + /// The returned future resolves to result of transmission of this request. + new: static func(headers: headers, contents: option>, trailers: future, error-code>>, options: option) -> tuple>>; + /// Get the Method for the Request. + get-method: func() -> method; + /// Set the Method for the Request. Fails if the string present in a + /// `method.other` argument is not a syntactically valid method. + set-method: func(method: method) -> result; + /// Get the combination of the HTTP Path and Query for the Request. When + /// `none`, this represents an empty Path and empty Query. + get-path-with-query: func() -> option; + /// Set the combination of the HTTP Path and Query for the Request. When + /// `none`, this represents an empty Path and empty Query. Fails is the + /// string given is not a syntactically valid path and query uri component. + set-path-with-query: func(path-with-query: option) -> result; + /// Get the HTTP Related Scheme for the Request. When `none`, the + /// implementation may choose an appropriate default scheme. + get-scheme: func() -> option; + /// Set the HTTP Related Scheme for the Request. When `none`, the + /// implementation may choose an appropriate default scheme. Fails if the + /// string given is not a syntactically valid uri scheme. + set-scheme: func(scheme: option) -> result; + /// Get the authority of the Request's target URI. A value of `none` may be used + /// with Related Schemes which do not require an authority. The HTTP and + /// HTTPS schemes always require an authority. + get-authority: func() -> option; + /// Set the authority of the Request's target URI. A value of `none` may be used + /// with Related Schemes which do not require an authority. The HTTP and + /// HTTPS schemes always require an authority. Fails if the string given is + /// not a syntactically valid URI authority. + set-authority: func(authority: option) -> result; + /// Get the `request-options` to be associated with this request + /// + /// The returned `request-options` resource is immutable: `set-*` operations + /// will fail if invoked. + /// + /// This `request-options` resource is a child: it must be dropped before + /// the parent `request` is dropped, or its ownership is transferred to + /// another component by e.g. `handler.handle`. + get-options: func() -> option; + /// Get the headers associated with the Request. + /// + /// The returned `headers` resource is immutable: `set`, `append`, and + /// `delete` operations will fail with `header-error.immutable`. + get-headers: func() -> headers; + /// Get body of the Request. + /// + /// Stream returned by this method represents the contents of the body. + /// Once the stream is reported as closed, callers should await the returned + /// future to determine whether the body was received successfully. + /// The future will only resolve after the stream is reported as closed. + /// + /// This function takes a `res` future as a parameter, which can be used to + /// communicate an error in handling of the request. + /// + /// Note that function will move the `request`, but references to headers or + /// request options acquired from it previously will remain valid. + consume-body: static func(this: request, res: future>) -> tuple, future, error-code>>>; + } + + /// Parameters for making an HTTP Request. Each of these parameters is + /// currently an optional timeout applicable to the transport layer of the + /// HTTP protocol. + /// + /// These timeouts are separate from any the user may use to bound an + /// asynchronous call. + @since(version = 0.3.0-rc-2026-03-15) + resource request-options { + /// Construct a default `request-options` value. + constructor(); + /// The timeout for the initial connect to the HTTP Server. + get-connect-timeout: func() -> option; + /// Set the timeout for the initial connect to the HTTP Server. An error + /// return value indicates that this timeout is not supported or that this + /// handle is immutable. + set-connect-timeout: func(duration: option) -> result<_, request-options-error>; + /// The timeout for receiving the first byte of the Response body. + get-first-byte-timeout: func() -> option; + /// Set the timeout for receiving the first byte of the Response body. An + /// error return value indicates that this timeout is not supported or that + /// this handle is immutable. + set-first-byte-timeout: func(duration: option) -> result<_, request-options-error>; + /// The timeout for receiving subsequent chunks of bytes in the Response + /// body stream. + get-between-bytes-timeout: func() -> option; + /// Set the timeout for receiving subsequent chunks of bytes in the Response + /// body stream. An error return value indicates that this timeout is not + /// supported or that this handle is immutable. + set-between-bytes-timeout: func(duration: option) -> result<_, request-options-error>; + /// Make a deep copy of the `request-options`. + /// The resulting `request-options` is mutable. + clone: func() -> request-options; + } + + /// This type corresponds to the HTTP standard Status Code. + @since(version = 0.3.0-rc-2026-03-15) + type status-code = u16; + + /// Represents an HTTP Response. + @since(version = 0.3.0-rc-2026-03-15) + resource response { + /// Construct a new `response`, with a default `status-code` of `200`. + /// If a different `status-code` is needed, it must be set via the + /// `set-status-code` method. + /// + /// `headers` is the HTTP Headers for the Response. + /// + /// `contents` is the optional body content stream with `none` + /// representing a zero-length content stream. + /// Once it is closed, `trailers` future must resolve to a result. + /// If `trailers` resolves to an error, underlying connection + /// will be closed immediately. + /// + /// The returned future resolves to result of transmission of this response. + new: static func(headers: headers, contents: option>, trailers: future, error-code>>) -> tuple>>; + /// Get the HTTP Status Code for the Response. + get-status-code: func() -> status-code; + /// Set the HTTP Status Code for the Response. Fails if the status-code + /// given is not a valid http status code. + set-status-code: func(status-code: status-code) -> result; + /// Get the headers associated with the Response. + /// + /// The returned `headers` resource is immutable: `set`, `append`, and + /// `delete` operations will fail with `header-error.immutable`. + get-headers: func() -> headers; + /// Get body of the Response. + /// + /// Stream returned by this method represents the contents of the body. + /// Once the stream is reported as closed, callers should await the returned + /// future to determine whether the body was received successfully. + /// The future will only resolve after the stream is reported as closed. + /// + /// This function takes a `res` future as a parameter, which can be used to + /// communicate an error in handling of the response. + /// + /// Note that function will move the `response`, but references to headers + /// acquired from it previously will remain valid. + consume-body: static func(this: response, res: future>) -> tuple, future, error-code>>>; + } +} + +/// This interface defines a handler of HTTP Requests. +/// +/// In a `wasi:http/service` this interface is exported to respond to an +/// incoming HTTP Request with a Response. +/// +/// In `wasi:http/middleware` this interface is both exported and imported as +/// the "downstream" and "upstream" directions of the middleware chain. +@since(version = 0.3.0-rc-2026-03-15) +interface handler { + use types.{request, response, error-code}; + + /// This function may be called with either an incoming request read from the + /// network or a request synthesized or forwarded by another component. + handle: async func(request: request) -> result; +} + +/// This interface defines an HTTP client for sending "outgoing" requests. +/// +/// Most components are expected to import this interface to provide the +/// capability to send HTTP requests to arbitrary destinations on a network. +/// +/// The type signature of `client.send` is the same as `handler.handle`. This +/// duplication is currently necessary because some Component Model tooling +/// (including WIT itself) is unable to represent a component importing two +/// instances of the same interface. A `client.send` import may be linked +/// directly to a `handler.handle` export to bypass the network. +@since(version = 0.3.0-rc-2026-03-15) +interface client { + use types.{request, response, error-code}; + + /// This function may be used to either send an outgoing request over the + /// network or to forward it to another component. + send: async func(request: request) -> result; +} + +/// The `wasi:http/service` world captures a broad category of HTTP services +/// including web applications, API servers, and proxies. It may be `include`d +/// in more specific worlds such as `wasi:http/middleware`. +@since(version = 0.3.0-rc-2026-03-15) +world service { + import wasi:cli/types@0.3.0-rc-2026-03-15; + import wasi:cli/stdout@0.3.0-rc-2026-03-15; + import wasi:cli/stderr@0.3.0-rc-2026-03-15; + import wasi:cli/stdin@0.3.0-rc-2026-03-15; + import wasi:clocks/types@0.3.0-rc-2026-03-15; + import types; + import client; + import wasi:clocks/monotonic-clock@0.3.0-rc-2026-03-15; + import wasi:clocks/system-clock@0.3.0-rc-2026-03-15; + @unstable(feature = clocks-timezone) + import wasi:clocks/timezone@0.3.0-rc-2026-03-15; + import wasi:random/random@0.3.0-rc-2026-03-15; + import wasi:random/insecure@0.3.0-rc-2026-03-15; + import wasi:random/insecure-seed@0.3.0-rc-2026-03-15; + + export handler; +} +/// The `wasi:http/middleware` world captures HTTP services that forward HTTP +/// Requests to another handler. +/// +/// Components may implement this world to allow them to participate in handler +/// "chains" where a `request` flows through handlers on its way to some terminal +/// `service` and corresponding `response` flows in the opposite direction. +@since(version = 0.3.0-rc-2026-03-15) +world middleware { + import wasi:clocks/types@0.3.0-rc-2026-03-15; + import types; + import handler; + import wasi:cli/types@0.3.0-rc-2026-03-15; + import wasi:cli/stdout@0.3.0-rc-2026-03-15; + import wasi:cli/stderr@0.3.0-rc-2026-03-15; + import wasi:cli/stdin@0.3.0-rc-2026-03-15; + import client; + import wasi:clocks/monotonic-clock@0.3.0-rc-2026-03-15; + import wasi:clocks/system-clock@0.3.0-rc-2026-03-15; + @unstable(feature = clocks-timezone) + import wasi:clocks/timezone@0.3.0-rc-2026-03-15; + import wasi:random/random@0.3.0-rc-2026-03-15; + import wasi:random/insecure@0.3.0-rc-2026-03-15; + import wasi:random/insecure-seed@0.3.0-rc-2026-03-15; + + export handler; +} diff --git a/examples/wasip3/wit/deps/http/handler.wit b/examples/wasip3/wit/deps/http/handler.wit deleted file mode 100644 index e4446cb..0000000 --- a/examples/wasip3/wit/deps/http/handler.wit +++ /dev/null @@ -1,17 +0,0 @@ -/// This interface defines a handler of HTTP Requests. It may be imported by -/// components which wish to send HTTP Requests and also exported by components -/// which can respond to HTTP Requests. In addition, it may be used to pass -/// a request from one component to another without any use of a network. -interface handler { - use types.{request, response, error-code}; - - /// When exported, this function may be called with either an incoming - /// request read from the network or a request synthesized or forwarded by - /// another component. - /// - /// When imported, this function may be used to either send an outgoing - /// request over the network or pass it to another component. - handle: async func( - request: request, - ) -> result; -} diff --git a/examples/wasip3/wit/deps/http/proxy.wit b/examples/wasip3/wit/deps/http/proxy.wit deleted file mode 100644 index 223083e..0000000 --- a/examples/wasip3/wit/deps/http/proxy.wit +++ /dev/null @@ -1,44 +0,0 @@ -package wasi:http@0.3.0-rc-2025-09-16; - -/// The `wasi:http/imports` world imports all the APIs for HTTP proxies. -/// It is intended to be `include`d in other worlds. -world imports { - /// HTTP proxies have access to time and randomness. - include wasi:clocks/imports@0.3.0-rc-2025-09-16; - import wasi:random/random@0.3.0-rc-2025-09-16; - - /// Proxies have standard output and error streams which are expected to - /// terminate in a developer-facing console provided by the host. - import wasi:cli/stdout@0.3.0-rc-2025-09-16; - import wasi:cli/stderr@0.3.0-rc-2025-09-16; - - /// TODO: this is a temporary workaround until component tooling is able to - /// gracefully handle the absence of stdin. Hosts must return an eof stream - /// for this import, which is what wasi-libc + tooling will do automatically - /// when this import is properly removed. - import wasi:cli/stdin@0.3.0-rc-2025-09-16; - - /// This is the default handler to use when user code simply wants to make an - /// HTTP request (e.g., via `fetch()`). - /// - /// This may also be used to pass synthesized or forwarded requests to another - /// component. - import handler; -} - -/// The `wasi:http/proxy` world captures a widely-implementable intersection of -/// hosts that includes HTTP forward and reverse proxies. Components targeting -/// this world may concurrently stream in and out any number of incoming and -/// outgoing HTTP requests. -world proxy { - include imports; - - /// The host delivers incoming HTTP requests to a component by calling the - /// `handle` function of this exported interface. A host may arbitrarily reuse - /// or not reuse component instance when delivering incoming HTTP requests and - /// thus a component must be able to handle 0..N calls to `handle`. - /// - /// This may also be used to receive synthesized or forwarded requests from - /// another component. - export handler; -} diff --git a/examples/wasip3/wit/deps/random.wit b/examples/wasip3/wit/deps/random.wit new file mode 100644 index 0000000..026f44a --- /dev/null +++ b/examples/wasip3/wit/deps/random.wit @@ -0,0 +1,107 @@ +package wasi:random@0.3.0-rc-2026-03-15; + +/// The insecure-seed interface for seeding hash-map DoS resistance. +/// +/// It is intended to be portable at least between Unix-family platforms and +/// Windows. +@since(version = 0.3.0-rc-2026-03-15) +interface insecure-seed { + /// Return a 128-bit value that may contain a pseudo-random value. + /// + /// The returned value is not required to be computed from a CSPRNG, and may + /// even be entirely deterministic. Host implementations are encouraged to + /// provide pseudo-random values to any program exposed to + /// attacker-controlled content, to enable DoS protection built into many + /// languages' hash-map implementations. + /// + /// This function is intended to only be called once, by a source language + /// to initialize Denial Of Service (DoS) protection in its hash-map + /// implementation. + /// + /// # Expected future evolution + /// + /// This will likely be changed to a value import, to prevent it from being + /// called multiple times and potentially used for purposes other than DoS + /// protection. + @since(version = 0.3.0-rc-2026-03-15) + get-insecure-seed: func() -> tuple; +} + +/// The insecure interface for insecure pseudo-random numbers. +/// +/// It is intended to be portable at least between Unix-family platforms and +/// Windows. +@since(version = 0.3.0-rc-2026-03-15) +interface insecure { + /// Return up to `max-len` insecure pseudo-random bytes. + /// + /// This function is not cryptographically secure. Do not use it for + /// anything related to security. + /// + /// There are no requirements on the values of the returned bytes, however + /// implementations are encouraged to return evenly distributed values with + /// a long period. + /// + /// Implementations MAY return fewer bytes than requested (a short read). + /// Callers that require exactly `max-len` bytes MUST call this function in + /// a loop until the desired number of bytes has been accumulated. + /// Implementations MUST return at least 1 byte when `max-len` is greater + /// than zero. When `max-len` is zero, implementations MUST return an empty + /// list without trapping. + @since(version = 0.3.0-rc-2026-03-15) + get-insecure-random-bytes: func(max-len: u64) -> list; + + /// Return an insecure pseudo-random `u64` value. + /// + /// This function returns the same type of pseudo-random data as + /// `get-insecure-random-bytes`, represented as a `u64`. + @since(version = 0.3.0-rc-2026-03-15) + get-insecure-random-u64: func() -> u64; +} + +/// WASI Random is a random data API. +/// +/// It is intended to be portable at least between Unix-family platforms and +/// Windows. +@since(version = 0.3.0-rc-2026-03-15) +interface random { + /// Return up to `max-len` cryptographically-secure random or pseudo-random + /// bytes. + /// + /// This function must produce data at least as cryptographically secure and + /// fast as an adequately seeded cryptographically-secure pseudo-random + /// number generator (CSPRNG). It must not block, from the perspective of + /// the calling program, under any circumstances, including on the first + /// request and on requests for numbers of bytes. The returned data must + /// always be unpredictable. + /// + /// Implementations MAY return fewer bytes than requested (a short read). + /// Callers that require exactly `max-len` bytes MUST call this function in + /// a loop until the desired number of bytes has been accumulated. + /// Implementations MUST return at least 1 byte when `max-len` is greater + /// than zero. When `max-len` is zero, implementations MUST return an empty + /// list without trapping. + /// + /// This function must always return fresh data. Deterministic environments + /// must omit this function, rather than implementing it with deterministic + /// data. + @since(version = 0.3.0-rc-2026-03-15) + get-random-bytes: func(max-len: u64) -> list; + + /// Return a cryptographically-secure random or pseudo-random `u64` value. + /// + /// This function returns the same type of data as `get-random-bytes`, + /// represented as a `u64`. + @since(version = 0.3.0-rc-2026-03-15) + get-random-u64: func() -> u64; +} + +@since(version = 0.3.0-rc-2026-03-15) +world imports { + @since(version = 0.3.0-rc-2026-03-15) + import random; + @since(version = 0.3.0-rc-2026-03-15) + import insecure; + @since(version = 0.3.0-rc-2026-03-15) + import insecure-seed; +} diff --git a/examples/wasip3/wit/deps/random/insecure-seed.wit b/examples/wasip3/wit/deps/random/insecure-seed.wit deleted file mode 100644 index 302151b..0000000 --- a/examples/wasip3/wit/deps/random/insecure-seed.wit +++ /dev/null @@ -1,27 +0,0 @@ -package wasi:random@0.3.0-rc-2025-09-16; -/// The insecure-seed interface for seeding hash-map DoS resistance. -/// -/// It is intended to be portable at least between Unix-family platforms and -/// Windows. -@since(version = 0.3.0-rc-2025-09-16) -interface insecure-seed { - /// Return a 128-bit value that may contain a pseudo-random value. - /// - /// The returned value is not required to be computed from a CSPRNG, and may - /// even be entirely deterministic. Host implementations are encouraged to - /// provide pseudo-random values to any program exposed to - /// attacker-controlled content, to enable DoS protection built into many - /// languages' hash-map implementations. - /// - /// This function is intended to only be called once, by a source language - /// to initialize Denial Of Service (DoS) protection in its hash-map - /// implementation. - /// - /// # Expected future evolution - /// - /// This will likely be changed to a value import, to prevent it from being - /// called multiple times and potentially used for purposes other than DoS - /// protection. - @since(version = 0.3.0-rc-2025-09-16) - get-insecure-seed: func() -> tuple; -} diff --git a/examples/wasip3/wit/deps/random/insecure.wit b/examples/wasip3/wit/deps/random/insecure.wit deleted file mode 100644 index 39146e3..0000000 --- a/examples/wasip3/wit/deps/random/insecure.wit +++ /dev/null @@ -1,25 +0,0 @@ -package wasi:random@0.3.0-rc-2025-09-16; -/// The insecure interface for insecure pseudo-random numbers. -/// -/// It is intended to be portable at least between Unix-family platforms and -/// Windows. -@since(version = 0.3.0-rc-2025-09-16) -interface insecure { - /// Return `len` insecure pseudo-random bytes. - /// - /// This function is not cryptographically secure. Do not use it for - /// anything related to security. - /// - /// There are no requirements on the values of the returned bytes, however - /// implementations are encouraged to return evenly distributed values with - /// a long period. - @since(version = 0.3.0-rc-2025-09-16) - get-insecure-random-bytes: func(len: u64) -> list; - - /// Return an insecure pseudo-random `u64` value. - /// - /// This function returns the same type of pseudo-random data as - /// `get-insecure-random-bytes`, represented as a `u64`. - @since(version = 0.3.0-rc-2025-09-16) - get-insecure-random-u64: func() -> u64; -} diff --git a/examples/wasip3/wit/deps/random/random.wit b/examples/wasip3/wit/deps/random/random.wit deleted file mode 100644 index fa1f111..0000000 --- a/examples/wasip3/wit/deps/random/random.wit +++ /dev/null @@ -1,29 +0,0 @@ -package wasi:random@0.3.0-rc-2025-09-16; -/// WASI Random is a random data API. -/// -/// It is intended to be portable at least between Unix-family platforms and -/// Windows. -@since(version = 0.3.0-rc-2025-09-16) -interface random { - /// Return `len` cryptographically-secure random or pseudo-random bytes. - /// - /// This function must produce data at least as cryptographically secure and - /// fast as an adequately seeded cryptographically-secure pseudo-random - /// number generator (CSPRNG). It must not block, from the perspective of - /// the calling program, under any circumstances, including on the first - /// request and on requests for numbers of bytes. The returned data must - /// always be unpredictable. - /// - /// This function must always return fresh data. Deterministic environments - /// must omit this function, rather than implementing it with deterministic - /// data. - @since(version = 0.3.0-rc-2025-09-16) - get-random-bytes: func(len: u64) -> list; - - /// Return a cryptographically-secure random or pseudo-random `u64` value. - /// - /// This function returns the same type of data as `get-random-bytes`, - /// represented as a `u64`. - @since(version = 0.3.0-rc-2025-09-16) - get-random-u64: func() -> u64; -} diff --git a/examples/wasip3/wit/deps/random/world.wit b/examples/wasip3/wit/deps/random/world.wit deleted file mode 100644 index 08c5ed8..0000000 --- a/examples/wasip3/wit/deps/random/world.wit +++ /dev/null @@ -1,13 +0,0 @@ -package wasi:random@0.3.0-rc-2025-09-16; - -@since(version = 0.3.0-rc-2025-09-16) -world imports { - @since(version = 0.3.0-rc-2025-09-16) - import random; - - @since(version = 0.3.0-rc-2025-09-16) - import insecure; - - @since(version = 0.3.0-rc-2025-09-16) - import insecure-seed; -} diff --git a/examples/wasip3/wit/deps/sockets.wit b/examples/wasip3/wit/deps/sockets.wit new file mode 100644 index 0000000..cde2e4d --- /dev/null +++ b/examples/wasip3/wit/deps/sockets.wit @@ -0,0 +1,839 @@ +package wasi:sockets@0.3.0-rc-2026-03-15; + +@since(version = 0.3.0-rc-2026-03-15) +interface types { + @since(version = 0.3.0-rc-2026-03-15) + use wasi:clocks/types@0.3.0-rc-2026-03-15.{duration}; + + /// Error codes. + /// + /// In theory, every API can return any error code. + /// In practice, API's typically only return the errors documented per API + /// combined with a couple of errors that are always possible: + /// - `other` + /// - `access-denied` + /// - `not-supported` + /// - `out-of-memory` + /// + /// See each individual API for what the POSIX equivalents are. They sometimes differ per API. + @since(version = 0.3.0-rc-2026-03-15) + variant error-code { + /// Access denied. + /// + /// POSIX equivalent: EACCES, EPERM + access-denied, + /// The operation is not supported. + /// + /// POSIX equivalent: EOPNOTSUPP, ENOPROTOOPT, EPFNOSUPPORT, EPROTONOSUPPORT, ESOCKTNOSUPPORT + not-supported, + /// One of the arguments is invalid. + /// + /// POSIX equivalent: EINVAL, EDESTADDRREQ, EAFNOSUPPORT + invalid-argument, + /// Not enough memory to complete the operation. + /// + /// POSIX equivalent: ENOMEM, ENOBUFS + out-of-memory, + /// The operation timed out before it could finish completely. + /// + /// POSIX equivalent: ETIMEDOUT + timeout, + /// The operation is not valid in the socket's current state. + invalid-state, + /// The local address is not available. + /// + /// POSIX equivalent: EADDRNOTAVAIL + address-not-bindable, + /// A bind operation failed because the provided address is already in + /// use or because there are no ephemeral ports available. + /// + /// POSIX equivalent: EADDRINUSE + address-in-use, + /// The remote address is not reachable. + /// + /// POSIX equivalent: EHOSTUNREACH, EHOSTDOWN, ENETDOWN, ENETUNREACH, ENONET + remote-unreachable, + /// The connection was forcefully rejected. + /// + /// POSIX equivalent: ECONNREFUSED + connection-refused, + /// A write failed because the connection was broken. + /// + /// POSIX equivalent: EPIPE + connection-broken, + /// The connection was reset. + /// + /// POSIX equivalent: ECONNRESET + connection-reset, + /// The connection was aborted. + /// + /// POSIX equivalent: ECONNABORTED + connection-aborted, + /// The size of a datagram sent to a UDP socket exceeded the maximum + /// supported size. + /// + /// POSIX equivalent: EMSGSIZE + datagram-too-large, + /// A catch-all for errors not captured by the existing variants. + /// Implementations can use this to extend the error type without + /// breaking existing code. + other(option), + } + + @since(version = 0.3.0-rc-2026-03-15) + enum ip-address-family { + /// Similar to `AF_INET` in POSIX. + ipv4, + /// Similar to `AF_INET6` in POSIX. + ipv6, + } + + @since(version = 0.3.0-rc-2026-03-15) + type ipv4-address = tuple; + + @since(version = 0.3.0-rc-2026-03-15) + type ipv6-address = tuple; + + @since(version = 0.3.0-rc-2026-03-15) + variant ip-address { + ipv4(ipv4-address), + ipv6(ipv6-address), + } + + @since(version = 0.3.0-rc-2026-03-15) + record ipv4-socket-address { + /// sin_port + port: u16, + /// sin_addr + address: ipv4-address, + } + + @since(version = 0.3.0-rc-2026-03-15) + record ipv6-socket-address { + /// sin6_port + port: u16, + /// sin6_flowinfo + flow-info: u32, + /// sin6_addr + address: ipv6-address, + /// sin6_scope_id + scope-id: u32, + } + + @since(version = 0.3.0-rc-2026-03-15) + variant ip-socket-address { + ipv4(ipv4-socket-address), + ipv6(ipv6-socket-address), + } + + /// A TCP socket resource. + /// + /// The socket can be in one of the following states: + /// - `unbound` + /// - `bound` (See note below) + /// - `listening` + /// - `connecting` + /// - `connected` + /// - `closed` + /// See + /// for more information. + /// + /// Note: Except where explicitly mentioned, whenever this documentation uses + /// the term "bound" without backticks it actually means: in the `bound` state *or higher*. + /// (i.e. `bound`, `listening`, `connecting` or `connected`) + /// + /// WASI uses shared ownership semantics: the `tcp-socket` handle and all + /// derived `stream` and `future` values reference a single underlying OS + /// socket: + /// - Send/receive streams remain functional after the original `tcp-socket` + /// handle is dropped. + /// - The stream returned by `listen` behaves similarly. + /// - Client sockets returned by `tcp-socket::listen` are independent and do + /// not keep the listening socket alive. + /// + /// The OS socket is closed only after the last handle is dropped. This + /// model has observable effects; for example, it affects when the local + /// port binding is released. + /// + /// In addition to the general error codes documented on the + /// `types::error-code` type, TCP socket methods may always return + /// `error(invalid-state)` when in the `closed` state. + @since(version = 0.3.0-rc-2026-03-15) + resource tcp-socket { + /// Create a new TCP socket. + /// + /// Similar to `socket(AF_INET or AF_INET6, SOCK_STREAM, IPPROTO_TCP)` + /// in POSIX. On IPv6 sockets, IPV6_V6ONLY is enabled by default and + /// can't be configured otherwise. + /// + /// Unlike POSIX, WASI sockets have no notion of a socket-level + /// `O_NONBLOCK` flag. Instead they fully rely on the Component Model's + /// async support. + /// + /// # Typical errors + /// - `not-supported`: The `address-family` is not supported. (EAFNOSUPPORT) + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.3.0-rc-2026-03-15) + create: static func(address-family: ip-address-family) -> result; + /// Bind the socket to the provided IP address and port. + /// + /// 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. + /// + /// Bind can be attempted multiple times on the same socket, even with + /// different arguments on each iteration. But never concurrently and + /// only as long as the previous bind failed. Once a bind succeeds, the + /// binding can't be changed anymore. + /// + /// # Typical errors + /// - `invalid-argument`: The `local-address` has the wrong address family. (EAFNOSUPPORT, EFAULT on Windows) + /// - `invalid-argument`: `local-address` is not a unicast address. (EINVAL) + /// - `invalid-argument`: `local-address` is an IPv4-mapped IPv6 address. (EINVAL) + /// - `invalid-state`: The socket is already bound. (EINVAL) + /// - `address-in-use`: 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 can be bound to. (EADDRNOTAVAIL) + /// + /// # Implementors note + /// The bind operation shouldn't be affected by the TIME_WAIT state of a + /// recently closed socket on the same local address. In practice this + /// means that the SO_REUSEADDR socket option should be set implicitly + /// on all platforms, except on Windows where this is the default + /// behavior and SO_REUSEADDR performs something different. + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.3.0-rc-2026-03-15) + bind: func(local-address: ip-socket-address) -> result<_, error-code>; + /// Connect to a remote endpoint. + /// + /// On success, the socket is transitioned into the `connected` state + /// and the `remote-address` of the socket is updated. + /// The `local-address` may be updated as well, based on the best network + /// path to `remote-address`. If the socket was not already explicitly + /// bound, this function will implicitly bind the socket to a random + /// free port. + /// + /// After a failed connection attempt, the socket will be in the `closed` + /// state and the only valid action left is to `drop` the socket. A single + /// socket can not be used to connect more than once. + /// + /// # Typical errors + /// - `invalid-argument`: The `remote-address` has the wrong address family. (EAFNOSUPPORT) + /// - `invalid-argument`: `remote-address` is not a unicast address. (EINVAL, ENETUNREACH on Linux, EAFNOSUPPORT on MacOS) + /// - `invalid-argument`: `remote-address` is an IPv4-mapped IPv6 address. (EINVAL, EADDRNOTAVAIL on Illumos) + /// - `invalid-argument`: The IP address in `remote-address` is set to INADDR_ANY (`0.0.0.0` / `::`). (EADDRNOTAVAIL on Windows) + /// - `invalid-argument`: The port in `remote-address` is set to 0. (EADDRNOTAVAIL on Windows) + /// - `invalid-state`: The socket is already in the `connecting` state. (EALREADY) + /// - `invalid-state`: The socket is already in the `connected` state. (EISCONN) + /// - `invalid-state`: The socket is already in the `listening` state. (EOPNOTSUPP, EINVAL on Windows) + /// - `timeout`: Connection timed out. (ETIMEDOUT) + /// - `connection-refused`: The connection was forcefully rejected. (ECONNREFUSED) + /// - `connection-reset`: The connection was reset. (ECONNRESET) + /// - `connection-aborted`: The connection was aborted. (ECONNABORTED) + /// - `remote-unreachable`: The remote address is not reachable. (EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN, ENONET) + /// - `address-in-use`: Tried to perform an implicit bind, but there were no ephemeral ports available. (EADDRINUSE, EADDRNOTAVAIL on Linux, EAGAIN on BSD) + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.3.0-rc-2026-03-15) + connect: async func(remote-address: ip-socket-address) -> result<_, error-code>; + /// Start listening and return a stream of new inbound connections. + /// + /// Transitions the socket into the `listening` state. This can be called + /// at most once per socket. + /// + /// If the socket is not already explicitly bound, this function will + /// implicitly bind the socket to a random free port. + /// + /// Normally, the returned sockets are bound, in the `connected` state + /// and immediately ready for I/O. Though, depending on exact timing and + /// circumstances, a newly accepted connection may already be `closed` + /// by the time the server attempts to perform its first I/O on it. This + /// is true regardless of whether the WASI implementation uses + /// "synthesized" sockets or not (see Implementors Notes below). + /// + /// The following properties are inherited from the listener socket: + /// - `address-family` + /// - `keep-alive-enabled` + /// - `keep-alive-idle-time` + /// - `keep-alive-interval` + /// - `keep-alive-count` + /// - `hop-limit` + /// - `receive-buffer-size` + /// - `send-buffer-size` + /// + /// # Typical errors + /// - `invalid-state`: The socket is already in the `connected` state. (EISCONN, EINVAL on BSD) + /// - `invalid-state`: The socket is already in the `listening` state. + /// - `address-in-use`: Tried to perform an implicit bind, but there were no ephemeral ports available. (EADDRINUSE) + /// + /// # Implementors note + /// This method returns a single perpetual stream that should only close + /// on fatal errors (if any). Yet, the POSIX' `accept` function may also + /// return transient errors (e.g. ECONNABORTED). The exact details differ + /// per operation system. For example, the Linux manual mentions: + /// + /// > Linux accept() passes already-pending network errors on the new + /// > socket as an error code from accept(). This behavior differs from + /// > other BSD socket implementations. For reliable operation the + /// > application should detect the network errors defined for the + /// > protocol after accept() and treat them like EAGAIN by retrying. + /// > In the case of TCP/IP, these are ENETDOWN, EPROTO, ENOPROTOOPT, + /// > EHOSTDOWN, ENONET, EHOSTUNREACH, EOPNOTSUPP, and ENETUNREACH. + /// Source: https://man7.org/linux/man-pages/man2/accept.2.html + /// + /// WASI implementations have two options to handle this: + /// - Optionally log it and then skip over non-fatal errors returned by + /// `accept`. Guest code never gets to see these failures. Or: + /// - Synthesize a `tcp-socket` resource that exposes the error when + /// attempting to send or receive on it. Guest code then sees these + /// failures as regular I/O errors. + /// + /// In either case, the stream returned by this `listen` method remains + /// operational. + /// + /// WASI requires `listen` to perform an implicit bind if the socket + /// has not already been bound. Not all platforms (notably Windows) + /// exhibit this behavior out of the box. On platforms that require it, + /// the WASI implementation can emulate this behavior by performing + /// the bind itself if the guest hasn't already done so. + /// + /// # References + /// - + /// - + /// - + /// - + /// - + /// - + /// - + /// - + @since(version = 0.3.0-rc-2026-03-15) + listen: func() -> result, error-code>; + /// Transmit data to peer. + /// + /// The caller should close the stream when it has no more data to send + /// to the peer. Under normal circumstances this will cause a FIN packet + /// to be sent out. Closing the stream is equivalent to calling + /// `shutdown(SHUT_WR)` in POSIX. + /// + /// This function may be called at most once and returns once the full + /// contents of the stream are transmitted or an error is encountered. + /// + /// # Typical errors + /// - `invalid-state`: The socket is not in the `connected` state. (ENOTCONN) + /// - `invalid-state`: `send` has already been called on this socket. + /// - `connection-broken`: The connection is not writable anymore. (EPIPE, ECONNABORTED on Windows) + /// - `connection-reset`: The connection was reset. (ECONNRESET) + /// - `remote-unreachable`: The remote address is not reachable. (EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN, ENONET) + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.3.0-rc-2026-03-15) + send: func(data: stream) -> future>; + /// Read data from peer. + /// + /// Returns a `stream` of data sent by the peer. The implementation + /// drops the stream once no more data is available. At that point, the + /// returned `future` resolves to: + /// - `ok` after a graceful shutdown from the peer (i.e. a FIN packet), or + /// - `err` if the socket was closed abnormally. + /// + /// `receive` may be called only once per socket. Subsequent calls return + /// a closed stream and a future resolved to `err(invalid-state)`. + /// + /// If the caller is not expecting to receive any more data from the peer, + /// they should drop the stream. Any data still in the receive queue + /// will be discarded. This is equivalent to calling `shutdown(SHUT_RD)` + /// in POSIX. + /// + /// # Typical errors + /// - `invalid-state`: The socket is not in the `connected` state. (ENOTCONN) + /// - `invalid-state`: `receive` has already been called on this socket. + /// - `connection-reset`: The connection was reset. (ECONNRESET) + /// - `remote-unreachable`: The remote address is not reachable. (EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN, ENONET) + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.3.0-rc-2026-03-15) + receive: func() -> tuple, future>>; + /// Get the bound local address. + /// + /// POSIX mentions: + /// > If the socket has not been bound to a local name, the value + /// > stored in the object pointed to by `address` is unspecified. + /// + /// WASI is stricter and requires `get-local-address` to return + /// `invalid-state` when the socket hasn't been bound yet. + /// + /// # Typical errors + /// - `invalid-state`: The socket is not bound to any local address. + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.3.0-rc-2026-03-15) + get-local-address: func() -> result; + /// Get the remote address. + /// + /// # Typical errors + /// - `invalid-state`: The socket is not connected to a remote address. (ENOTCONN) + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.3.0-rc-2026-03-15) + get-remote-address: func() -> result; + /// Whether the socket is in the `listening` state. + /// + /// Equivalent to the SO_ACCEPTCONN socket option. + @since(version = 0.3.0-rc-2026-03-15) + get-is-listening: func() -> bool; + /// Whether this is a IPv4 or IPv6 socket. + /// + /// This is the value passed to the constructor. + /// + /// Equivalent to the SO_DOMAIN socket option. + @since(version = 0.3.0-rc-2026-03-15) + get-address-family: func() -> ip-address-family; + /// Hints the desired listen queue size. Implementations are free to + /// ignore this. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// Any other value will never cause an error, but it might be silently + /// clamped and/or rounded. + /// + /// # Typical errors + /// - `not-supported`: (set) The platform does not support changing the backlog size after the initial listen. + /// - `invalid-argument`: (set) The provided value was 0. + /// - `invalid-state`: (set) The socket is in the `connecting` or `connected` state. + @since(version = 0.3.0-rc-2026-03-15) + set-listen-backlog-size: func(value: u64) -> result<_, error-code>; + /// Enables or disables keepalive. + /// + /// The keepalive behavior can be adjusted using: + /// - `keep-alive-idle-time` + /// - `keep-alive-interval` + /// - `keep-alive-count` + /// These properties can be configured while `keep-alive-enabled` is + /// false, but only come into effect when `keep-alive-enabled` is true. + /// + /// Equivalent to the SO_KEEPALIVE socket option. + @since(version = 0.3.0-rc-2026-03-15) + get-keep-alive-enabled: func() -> result; + @since(version = 0.3.0-rc-2026-03-15) + set-keep-alive-enabled: func(value: bool) -> result<_, error-code>; + /// Amount of time the connection has to be idle before TCP starts + /// sending keepalive packets. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// All other values are accepted without error, but may be + /// clamped or rounded. As a result, the value read back from + /// this setting may differ from the value that was set. + /// + /// Equivalent to the TCP_KEEPIDLE socket option. (TCP_KEEPALIVE on MacOS) + /// + /// # Typical errors + /// - `invalid-argument`: (set) The provided value was 0. + @since(version = 0.3.0-rc-2026-03-15) + get-keep-alive-idle-time: func() -> result; + @since(version = 0.3.0-rc-2026-03-15) + set-keep-alive-idle-time: func(value: duration) -> result<_, error-code>; + /// The time between keepalive packets. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// All other values are accepted without error, but may be + /// clamped or rounded. As a result, the value read back from + /// this setting may differ from the value that was set. + /// + /// Equivalent to the TCP_KEEPINTVL socket option. + /// + /// # Typical errors + /// - `invalid-argument`: (set) The provided value was 0. + @since(version = 0.3.0-rc-2026-03-15) + get-keep-alive-interval: func() -> result; + @since(version = 0.3.0-rc-2026-03-15) + set-keep-alive-interval: func(value: duration) -> result<_, error-code>; + /// The maximum amount of keepalive packets TCP should send before + /// aborting the connection. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// All other values are accepted without error, but may be + /// clamped or rounded. As a result, the value read back from + /// this setting may differ from the value that was set. + /// + /// Equivalent to the TCP_KEEPCNT socket option. + /// + /// # Typical errors + /// - `invalid-argument`: (set) The provided value was 0. + @since(version = 0.3.0-rc-2026-03-15) + get-keep-alive-count: func() -> result; + @since(version = 0.3.0-rc-2026-03-15) + set-keep-alive-count: func(value: u32) -> result<_, error-code>; + /// Equivalent to the IP_TTL & IPV6_UNICAST_HOPS socket options. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// + /// # Typical errors + /// - `invalid-argument`: (set) The TTL value must be 1 or higher. + @since(version = 0.3.0-rc-2026-03-15) + get-hop-limit: func() -> result; + @since(version = 0.3.0-rc-2026-03-15) + set-hop-limit: func(value: u8) -> result<_, error-code>; + /// Kernel buffer space reserved for sending/receiving on this socket. + /// Implementations usually treat this as a cap the buffer can grow to, + /// rather than allocating the full amount immediately. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// All other values are accepted without error, but may be + /// clamped or rounded. As a result, the value read back from + /// this setting may differ from the value that was set. + /// + /// This is only a performance hint. The implementation may ignore it or + /// tweak it based on real traffic patterns. + /// Linux and macOS appear to behave differently depending on whether a + /// buffer size was explicitly set. When set, they tend to honor it; when + /// not set, they dynamically adjust the buffer size as the connection + /// progresses. This is especially noticeable when comparing the values + /// from before and after connection establishment. + /// + /// Equivalent to the SO_RCVBUF and SO_SNDBUF socket options. + /// + /// # Typical errors + /// - `invalid-argument`: (set) The provided value was 0. + @since(version = 0.3.0-rc-2026-03-15) + get-receive-buffer-size: func() -> result; + @since(version = 0.3.0-rc-2026-03-15) + set-receive-buffer-size: func(value: u64) -> result<_, error-code>; + @since(version = 0.3.0-rc-2026-03-15) + get-send-buffer-size: func() -> result; + @since(version = 0.3.0-rc-2026-03-15) + set-send-buffer-size: func(value: u64) -> result<_, error-code>; + } + + /// A UDP socket handle. + @since(version = 0.3.0-rc-2026-03-15) + resource udp-socket { + /// Create a new UDP socket. + /// + /// Similar to `socket(AF_INET or AF_INET6, SOCK_DGRAM, IPPROTO_UDP)` + /// in POSIX. On IPv6 sockets, IPV6_V6ONLY is enabled by default and + /// can't be configured otherwise. + /// + /// Unlike POSIX, WASI sockets have no notion of a socket-level + /// `O_NONBLOCK` flag. Instead they fully rely on the Component Model's + /// async support. + /// + /// # References: + /// - + /// - + /// - + /// - + @since(version = 0.3.0-rc-2026-03-15) + create: static func(address-family: ip-address-family) -> result; + /// Bind the socket to the provided IP address and port. + /// + /// 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 port is zero, the socket will be bound to a random + /// free port. + /// + /// # Typical errors + /// - `invalid-argument`: The `local-address` has the wrong address family. (EAFNOSUPPORT, EFAULT on Windows) + /// - `invalid-state`: The socket is already bound. (EINVAL) + /// - `address-in-use`: 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 can be bound to. (EADDRNOTAVAIL) + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.3.0-rc-2026-03-15) + bind: func(local-address: ip-socket-address) -> result<_, error-code>; + /// Associate this socket with a specific peer address. + /// + /// On success, the `remote-address` of the socket is updated. + /// The `local-address` may be updated as well, based on the best network + /// path to `remote-address`. If the socket was not already explicitly + /// bound, this function will implicitly bind the socket to a random + /// free port. + /// + /// When a UDP socket is "connected", the `send` and `receive` methods + /// are limited to communicating with that peer only: + /// - `send` can only be used to send to this destination. + /// - `receive` will only return datagrams sent from the provided `remote-address`. + /// + /// The name "connect" was kept to align with the existing POSIX + /// terminology. Other than that, this function only changes the local + /// socket configuration and does not generate any network traffic. + /// The peer is not aware of this "connection". + /// + /// This method may be called multiple times on the same socket to change + /// its association, but only the most recent one will be effective. + /// + /// # Typical errors + /// - `invalid-argument`: The `remote-address` has the wrong address family. (EAFNOSUPPORT) + /// - `invalid-argument`: The IP address in `remote-address` is set to INADDR_ANY (`0.0.0.0` / `::`). (EDESTADDRREQ, EADDRNOTAVAIL) + /// - `invalid-argument`: The port in `remote-address` is set to 0. (EDESTADDRREQ, EADDRNOTAVAIL) + /// - `address-in-use`: Tried to perform an implicit bind, but there were no ephemeral ports available. (EADDRINUSE, EADDRNOTAVAIL on Linux, EAGAIN on BSD) + /// + /// # Implementors note + /// If the socket is already connected, some platforms (e.g. Linux) + /// require a disconnect before connecting to a different peer address. + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.3.0-rc-2026-03-15) + connect: func(remote-address: ip-socket-address) -> result<_, error-code>; + /// Dissociate this socket from its peer address. + /// + /// After calling this method, `send` & `receive` are free to communicate + /// with any remote address again. + /// + /// The POSIX equivalent of this is calling `connect` with an `AF_UNSPEC` address. + /// + /// # Typical errors + /// - `invalid-state`: The socket is not connected. + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.3.0-rc-2026-03-15) + disconnect: func() -> result<_, error-code>; + /// Send a message on the socket to a particular peer. + /// + /// If the socket is connected, the peer address may be left empty. In + /// that case this is equivalent to `send` in POSIX. Otherwise it is + /// equivalent to `sendto`. + /// + /// Additionally, if the socket is connected, a `remote-address` argument + /// _may_ be provided but then it must be identical to the address + /// passed to `connect`. + /// + /// If the socket has not been explicitly bound, it will be + /// implicitly bound to a random free port. + /// + /// Implementations may trap if the `data` length exceeds 64 KiB. + /// + /// # Typical errors + /// - `invalid-argument`: The `remote-address` has the wrong address family. (EAFNOSUPPORT) + /// - `invalid-argument`: The IP address in `remote-address` is set to INADDR_ANY (`0.0.0.0` / `::`). (EDESTADDRREQ, EADDRNOTAVAIL) + /// - `invalid-argument`: The port in `remote-address` is set to 0. (EDESTADDRREQ, EADDRNOTAVAIL) + /// - `invalid-argument`: The socket is in "connected" mode and `remote-address` is `some` value that does not match the address passed to `connect`. (EISCONN) + /// - `invalid-argument`: The socket is not "connected" and no value for `remote-address` was provided. (EDESTADDRREQ) + /// - `remote-unreachable`: The remote address is not reachable. (ECONNRESET, ENETRESET on Windows, EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN, ENONET) + /// - `connection-refused`: The connection was refused. (ECONNREFUSED) + /// - `datagram-too-large`: The datagram is too large. (EMSGSIZE) + /// - `address-in-use`: Tried to perform an implicit bind, but there were no ephemeral ports available. (EADDRINUSE) + /// + /// # Implementors note + /// WASI requires `send` to perform an implicit bind if the socket + /// has not been bound. Not all platforms (notably Windows) exhibit + /// this behavior natively. On such platforms, the WASI implementation + /// should emulate it by performing the bind if the guest has not + /// already done so. + /// + /// # References + /// - + /// - + /// - + /// - + /// - + /// - + /// - + /// - + @since(version = 0.3.0-rc-2026-03-15) + send: async func(data: list, remote-address: option) -> result<_, error-code>; + /// Receive a message on the socket. + /// + /// On success, the return value contains a tuple of the received data + /// and the address of the sender. Theoretical maximum length of the + /// data is 64 KiB. Though in practice, it will typically be less than + /// 1500 bytes. + /// + /// If the socket is connected, the sender address is guaranteed to + /// match the remote address passed to `connect`. + /// + /// # Typical errors + /// - `invalid-state`: The socket has not been bound yet. + /// - `remote-unreachable`: The remote address is not reachable. (ECONNRESET, ENETRESET on Windows, EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN, ENONET) + /// - `connection-refused`: The connection was refused. (ECONNREFUSED) + /// + /// # References + /// - + /// - + /// - + /// - + /// - + /// - + /// - + @since(version = 0.3.0-rc-2026-03-15) + receive: async func() -> result, ip-socket-address>, error-code>; + /// Get the current bound address. + /// + /// POSIX mentions: + /// > If the socket has not been bound to a local name, the value + /// > stored in the object pointed to by `address` is unspecified. + /// + /// WASI is stricter and requires `get-local-address` to return + /// `invalid-state` when the socket hasn't been bound yet. + /// + /// # Typical errors + /// - `invalid-state`: The socket is not bound to any local address. + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.3.0-rc-2026-03-15) + get-local-address: func() -> result; + /// Get the address the socket is currently "connected" to. + /// + /// # Typical errors + /// - `invalid-state`: The socket is not "connected" to a specific remote address. (ENOTCONN) + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.3.0-rc-2026-03-15) + get-remote-address: func() -> result; + /// Whether this is a IPv4 or IPv6 socket. + /// + /// This is the value passed to the constructor. + /// + /// Equivalent to the SO_DOMAIN socket option. + @since(version = 0.3.0-rc-2026-03-15) + get-address-family: func() -> ip-address-family; + /// Equivalent to the IP_TTL & IPV6_UNICAST_HOPS socket options. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// + /// # Typical errors + /// - `invalid-argument`: (set) The TTL value must be 1 or higher. + @since(version = 0.3.0-rc-2026-03-15) + get-unicast-hop-limit: func() -> result; + @since(version = 0.3.0-rc-2026-03-15) + set-unicast-hop-limit: func(value: u8) -> result<_, error-code>; + /// Kernel buffer space reserved for sending/receiving on this socket. + /// Implementations usually treat this as a cap the buffer can grow to, + /// rather than allocating the full amount immediately. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// All other values are accepted without error, but may be + /// clamped or rounded. As a result, the value read back from + /// this setting may differ from the value that was set. + /// + /// Equivalent to the SO_RCVBUF and SO_SNDBUF socket options. + /// + /// # Typical errors + /// - `invalid-argument`: (set) The provided value was 0. + @since(version = 0.3.0-rc-2026-03-15) + get-receive-buffer-size: func() -> result; + @since(version = 0.3.0-rc-2026-03-15) + set-receive-buffer-size: func(value: u64) -> result<_, error-code>; + @since(version = 0.3.0-rc-2026-03-15) + get-send-buffer-size: func() -> result; + @since(version = 0.3.0-rc-2026-03-15) + set-send-buffer-size: func(value: u64) -> result<_, error-code>; + } +} + +@since(version = 0.3.0-rc-2026-03-15) +interface ip-name-lookup { + @since(version = 0.3.0-rc-2026-03-15) + use types.{ip-address}; + + /// Lookup error codes. + @since(version = 0.3.0-rc-2026-03-15) + variant error-code { + /// Access denied. + /// + /// POSIX equivalent: EACCES, EPERM + access-denied, + /// `name` is a syntactically invalid domain name or IP address. + /// + /// POSIX equivalent: EINVAL + invalid-argument, + /// Name does not exist or has no suitable associated IP addresses. + /// + /// POSIX equivalent: EAI_NONAME, EAI_NODATA, EAI_ADDRFAMILY + name-unresolvable, + /// A temporary failure in name resolution occurred. + /// + /// POSIX equivalent: EAI_AGAIN + temporary-resolver-failure, + /// A permanent failure in name resolution occurred. + /// + /// POSIX equivalent: EAI_FAIL + permanent-resolver-failure, + /// A catch-all for errors not captured by the existing variants. + /// Implementations can use this to extend the error type without + /// breaking existing code. + other(option), + } + + /// Resolve an internet host name to a list of IP addresses. + /// + /// Unicode domain names are automatically converted to ASCII using IDNA + /// encoding. If the input is an IP address string, the address is parsed + /// and returned as-is without making any external requests. + /// + /// See the wasi-socket proposal README.md for a comparison with getaddrinfo. + /// + /// The results are returned in connection order preference. + /// + /// This function never succeeds with 0 results. It either fails or succeeds + /// with at least one address. Additionally, this function never returns + /// IPv4-mapped IPv6 addresses. + /// + /// # References: + /// - + /// - + /// - + /// - + @since(version = 0.3.0-rc-2026-03-15) + resolve-addresses: async func(name: string) -> result, error-code>; +} + +@since(version = 0.3.0-rc-2026-03-15) +world imports { + @since(version = 0.3.0-rc-2026-03-15) + import wasi:clocks/types@0.3.0-rc-2026-03-15; + @since(version = 0.3.0-rc-2026-03-15) + import types; + @since(version = 0.3.0-rc-2026-03-15) + import ip-name-lookup; +} diff --git a/examples/wasip3/wit/deps/sockets/ip-name-lookup.wit b/examples/wasip3/wit/deps/sockets/ip-name-lookup.wit deleted file mode 100644 index 6a652ff..0000000 --- a/examples/wasip3/wit/deps/sockets/ip-name-lookup.wit +++ /dev/null @@ -1,62 +0,0 @@ -@since(version = 0.3.0-rc-2025-09-16) -interface ip-name-lookup { - @since(version = 0.3.0-rc-2025-09-16) - use types.{ip-address}; - - /// Lookup error codes. - @since(version = 0.3.0-rc-2025-09-16) - enum error-code { - /// Unknown error - unknown, - - /// Access denied. - /// - /// POSIX equivalent: EACCES, EPERM - access-denied, - - /// `name` is a syntactically invalid domain name or IP address. - /// - /// POSIX equivalent: EINVAL - invalid-argument, - - /// Name does not exist or has no suitable associated IP addresses. - /// - /// POSIX equivalent: EAI_NONAME, EAI_NODATA, EAI_ADDRFAMILY - name-unresolvable, - - /// A temporary failure in name resolution occurred. - /// - /// POSIX equivalent: EAI_AGAIN - temporary-resolver-failure, - - /// A permanent failure in name resolution occurred. - /// - /// POSIX equivalent: EAI_FAIL - permanent-resolver-failure, - } - - /// Resolve an internet host name to a list of IP addresses. - /// - /// Unicode domain names are automatically converted to ASCII using IDNA encoding. - /// If the input is an IP address string, the address is parsed and returned - /// as-is without making any external requests. - /// - /// See the wasi-socket proposal README.md for a comparison with getaddrinfo. - /// - /// The results are returned in connection order preference. - /// - /// This function never succeeds with 0 results. It either fails or succeeds - /// with at least one address. Additionally, this function never returns - /// IPv4-mapped IPv6 addresses. - /// - /// The returned future will resolve to an error code in case of failure. - /// It will resolve to success once the returned stream is exhausted. - /// - /// # References: - /// - - /// - - /// - - /// - - @since(version = 0.3.0-rc-2025-09-16) - resolve-addresses: async func(name: string) -> result, error-code>; -} diff --git a/examples/wasip3/wit/deps/sockets/types.wit b/examples/wasip3/wit/deps/sockets/types.wit deleted file mode 100644 index 2ed1912..0000000 --- a/examples/wasip3/wit/deps/sockets/types.wit +++ /dev/null @@ -1,725 +0,0 @@ -@since(version = 0.3.0-rc-2025-09-16) -interface types { - @since(version = 0.3.0-rc-2025-09-16) - use wasi:clocks/monotonic-clock@0.3.0-rc-2025-09-16.{duration}; - - /// Error codes. - /// - /// In theory, every API can return any error code. - /// In practice, API's typically only return the errors documented per API - /// combined with a couple of errors that are always possible: - /// - `unknown` - /// - `access-denied` - /// - `not-supported` - /// - `out-of-memory` - /// - /// See each individual API for what the POSIX equivalents are. They sometimes differ per API. - @since(version = 0.3.0-rc-2025-09-16) - enum error-code { - /// Unknown error - unknown, - - /// Access denied. - /// - /// POSIX equivalent: EACCES, EPERM - access-denied, - - /// The operation is not supported. - /// - /// POSIX equivalent: EOPNOTSUPP - not-supported, - - /// One of the arguments is invalid. - /// - /// POSIX equivalent: EINVAL - invalid-argument, - - /// Not enough memory to complete the operation. - /// - /// POSIX equivalent: ENOMEM, ENOBUFS, EAI_MEMORY - out-of-memory, - - /// The operation timed out before it could finish completely. - timeout, - - /// The operation is not valid in the socket's current state. - invalid-state, - - /// A bind operation failed because the provided address is not an address that the `network` can bind to. - address-not-bindable, - - /// A bind operation failed because the provided address is already in use or because there are no ephemeral ports available. - address-in-use, - - /// The remote address is not reachable - remote-unreachable, - - - /// The TCP connection was forcefully rejected - connection-refused, - - /// The TCP connection was reset. - connection-reset, - - /// A TCP connection was aborted. - connection-aborted, - - - /// The size of a datagram sent to a UDP socket exceeded the maximum - /// supported size. - datagram-too-large, - } - - @since(version = 0.3.0-rc-2025-09-16) - enum ip-address-family { - /// Similar to `AF_INET` in POSIX. - ipv4, - - /// Similar to `AF_INET6` in POSIX. - ipv6, - } - - @since(version = 0.3.0-rc-2025-09-16) - type ipv4-address = tuple; - @since(version = 0.3.0-rc-2025-09-16) - type ipv6-address = tuple; - - @since(version = 0.3.0-rc-2025-09-16) - variant ip-address { - ipv4(ipv4-address), - ipv6(ipv6-address), - } - - @since(version = 0.3.0-rc-2025-09-16) - record ipv4-socket-address { - /// sin_port - port: u16, - /// sin_addr - address: ipv4-address, - } - - @since(version = 0.3.0-rc-2025-09-16) - record ipv6-socket-address { - /// sin6_port - port: u16, - /// sin6_flowinfo - flow-info: u32, - /// sin6_addr - address: ipv6-address, - /// sin6_scope_id - scope-id: u32, - } - - @since(version = 0.3.0-rc-2025-09-16) - variant ip-socket-address { - ipv4(ipv4-socket-address), - ipv6(ipv6-socket-address), - } - - /// A TCP socket resource. - /// - /// The socket can be in one of the following states: - /// - `unbound` - /// - `bound` (See note below) - /// - `listening` - /// - `connecting` - /// - `connected` - /// - `closed` - /// See - /// for more information. - /// - /// Note: Except where explicitly mentioned, whenever this documentation uses - /// the term "bound" without backticks it actually means: in the `bound` state *or higher*. - /// (i.e. `bound`, `listening`, `connecting` or `connected`) - /// - /// In addition to the general error codes documented on the - /// `types::error-code` type, TCP socket methods may always return - /// `error(invalid-state)` when in the `closed` state. - @since(version = 0.3.0-rc-2025-09-16) - resource tcp-socket { - - /// Create a new TCP socket. - /// - /// Similar to `socket(AF_INET or AF_INET6, SOCK_STREAM, IPPROTO_TCP)` in POSIX. - /// On IPv6 sockets, IPV6_V6ONLY is enabled by default and can't be configured otherwise. - /// - /// Unlike POSIX, WASI sockets have no notion of a socket-level - /// `O_NONBLOCK` flag. Instead they fully rely on the Component Model's - /// async support. - /// - /// # References - /// - - /// - - /// - - /// - - @since(version = 0.3.0-rc-2025-09-16) - create: static func(address-family: ip-address-family) -> result; - - /// Bind the socket to the provided IP address and port. - /// - /// 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. - /// - /// Bind can be attempted multiple times on the same socket, even with - /// different arguments on each iteration. But never concurrently and - /// only as long as the previous bind failed. Once a bind succeeds, the - /// binding can't be changed anymore. - /// - /// # Typical errors - /// - `invalid-argument`: The `local-address` has the wrong address family. (EAFNOSUPPORT, EFAULT on Windows) - /// - `invalid-argument`: `local-address` is not a unicast address. (EINVAL) - /// - `invalid-argument`: `local-address` is an IPv4-mapped IPv6 address. (EINVAL) - /// - `invalid-state`: The socket is already bound. (EINVAL) - /// - `address-in-use`: 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 can be bound to. (EADDRNOTAVAIL) - /// - /// # Implementors note - /// When binding to a non-zero port, this bind operation shouldn't be affected by the TIME_WAIT - /// state of a recently closed socket on the same local address. In practice this means that the SO_REUSEADDR - /// socket option should be set implicitly on all platforms, except on Windows where this is the default behavior - /// and SO_REUSEADDR performs something different entirely. - /// - /// # References - /// - - /// - - /// - - /// - - @since(version = 0.3.0-rc-2025-09-16) - bind: func(local-address: ip-socket-address) -> result<_, error-code>; - - /// Connect to a remote endpoint. - /// - /// On success, the socket is transitioned into the `connected` state and this function returns a connection resource. - /// - /// After a failed connection attempt, the socket will be in the `closed` - /// state and the only valid action left is to `drop` the socket. A single - /// socket can not be used to connect more than once. - /// - /// # Typical errors - /// - `invalid-argument`: The `remote-address` has the wrong address family. (EAFNOSUPPORT) - /// - `invalid-argument`: `remote-address` is not a unicast address. (EINVAL, ENETUNREACH on Linux, EAFNOSUPPORT on MacOS) - /// - `invalid-argument`: `remote-address` is an IPv4-mapped IPv6 address. (EINVAL, EADDRNOTAVAIL on Illumos) - /// - `invalid-argument`: The IP address in `remote-address` is set to INADDR_ANY (`0.0.0.0` / `::`). (EADDRNOTAVAIL on Windows) - /// - `invalid-argument`: The port in `remote-address` is set to 0. (EADDRNOTAVAIL on Windows) - /// - `invalid-state`: The socket is already in the `connecting` state. (EALREADY) - /// - `invalid-state`: The socket is already in the `connected` state. (EISCONN) - /// - `invalid-state`: The socket is already in the `listening` state. (EOPNOTSUPP, EINVAL on Windows) - /// - `timeout`: Connection timed out. (ETIMEDOUT) - /// - `connection-refused`: The connection was forcefully rejected. (ECONNREFUSED) - /// - `connection-reset`: The connection was reset. (ECONNRESET) - /// - `connection-aborted`: The connection was aborted. (ECONNABORTED) - /// - `remote-unreachable`: The remote address is not reachable. (EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN, ENONET) - /// - `address-in-use`: Tried to perform an implicit bind, but there were no ephemeral ports available. (EADDRINUSE, EADDRNOTAVAIL on Linux, EAGAIN on BSD) - /// - /// # References - /// - - /// - - /// - - /// - - @since(version = 0.3.0-rc-2025-09-16) - connect: async func(remote-address: ip-socket-address) -> result<_, error-code>; - - /// Start listening and return a stream of new inbound connections. - /// - /// Transitions the socket into the `listening` state. This can be called - /// at most once per socket. - /// - /// If the socket is not already explicitly bound, this function will - /// implicitly bind the socket to a random free port. - /// - /// Normally, the returned sockets are bound, in the `connected` state - /// and immediately ready for I/O. Though, depending on exact timing and - /// circumstances, a newly accepted connection may already be `closed` - /// by the time the server attempts to perform its first I/O on it. This - /// is true regardless of whether the WASI implementation uses - /// "synthesized" sockets or not (see Implementors Notes below). - /// - /// The following properties are inherited from the listener socket: - /// - `address-family` - /// - `keep-alive-enabled` - /// - `keep-alive-idle-time` - /// - `keep-alive-interval` - /// - `keep-alive-count` - /// - `hop-limit` - /// - `receive-buffer-size` - /// - `send-buffer-size` - /// - /// # Typical errors - /// - `invalid-state`: The socket is already in the `connected` state. (EISCONN, EINVAL on BSD) - /// - `invalid-state`: The socket is already in the `listening` state. - /// - `address-in-use`: Tried to perform an implicit bind, but there were no ephemeral ports available. (EADDRINUSE) - /// - /// # Implementors note - /// This method returns a single perpetual stream that should only close - /// on fatal errors (if any). Yet, the POSIX' `accept` function may also - /// return transient errors (e.g. ECONNABORTED). The exact details differ - /// per operation system. For example, the Linux manual mentions: - /// - /// > Linux accept() passes already-pending network errors on the new - /// > socket as an error code from accept(). This behavior differs from - /// > other BSD socket implementations. For reliable operation the - /// > application should detect the network errors defined for the - /// > protocol after accept() and treat them like EAGAIN by retrying. - /// > In the case of TCP/IP, these are ENETDOWN, EPROTO, ENOPROTOOPT, - /// > EHOSTDOWN, ENONET, EHOSTUNREACH, EOPNOTSUPP, and ENETUNREACH. - /// Source: https://man7.org/linux/man-pages/man2/accept.2.html - /// - /// WASI implementations have two options to handle this: - /// - Optionally log it and then skip over non-fatal errors returned by - /// `accept`. Guest code never gets to see these failures. Or: - /// - Synthesize a `tcp-socket` resource that exposes the error when - /// attempting to send or receive on it. Guest code then sees these - /// failures as regular I/O errors. - /// - /// In either case, the stream returned by this `listen` method remains - /// operational. - /// - /// # References - /// - - /// - - /// - - /// - - /// - - /// - - /// - - /// - - @since(version = 0.3.0-rc-2025-09-16) - listen: func() -> result, error-code>; - - /// Transmit data to peer. - /// - /// The caller should close the stream when it has no more data to send - /// to the peer. Under normal circumstances this will cause a FIN packet - /// to be sent out. Closing the stream is equivalent to calling - /// `shutdown(SHUT_WR)` in POSIX. - /// - /// This function may be called at most once and returns once the full - /// contents of the stream are transmitted or an error is encountered. - /// - /// # Typical errors - /// - `invalid-state`: The socket is not in the `connected` state. (ENOTCONN) - /// - `connection-reset`: The connection was reset. (ECONNRESET) - /// - `remote-unreachable`: The remote address is not reachable. (EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN, ENONET) - /// - /// # References - /// - - /// - - /// - - /// - - @since(version = 0.3.0-rc-2025-09-16) - send: async func(data: stream) -> result<_, error-code>; - - /// Read data from peer. - /// - /// This function returns a `stream` which provides the data received from the - /// socket, and a `future` providing additional error information in case the - /// socket is closed abnormally. - /// - /// If the socket is closed normally, `stream.read` on the `stream` will return - /// `read-status::closed` with no `error-context` and the future resolves to - /// the value `ok`. If the socket is closed abnormally, `stream.read` on the - /// `stream` returns `read-status::closed` with an `error-context` and the future - /// resolves to `err` with an `error-code`. - /// - /// `receive` is meant to be called only once per socket. If it is called more - /// than once, the subsequent calls return a new `stream` that fails as if it - /// were closed abnormally. - /// - /// If the caller is not expecting to receive any data from the peer, - /// they may drop the stream. Any data still in the receive queue - /// will be discarded. This is equivalent to calling `shutdown(SHUT_RD)` - /// in POSIX. - /// - /// # Typical errors - /// - `invalid-state`: The socket is not in the `connected` state. (ENOTCONN) - /// - `connection-reset`: The connection was reset. (ECONNRESET) - /// - `remote-unreachable`: The remote address is not reachable. (EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN, ENONET) - /// - /// # References - /// - - /// - - /// - - /// - - @since(version = 0.3.0-rc-2025-09-16) - receive: func() -> tuple, future>>; - - /// Get the bound local address. - /// - /// POSIX mentions: - /// > If the socket has not been bound to a local name, the value - /// > stored in the object pointed to by `address` is unspecified. - /// - /// WASI is stricter and requires `get-local-address` to return `invalid-state` when the socket hasn't been bound yet. - /// - /// # Typical errors - /// - `invalid-state`: The socket is not bound to any local address. - /// - /// # References - /// - - /// - - /// - - /// - - @since(version = 0.3.0-rc-2025-09-16) - get-local-address: func() -> result; - - /// Get the remote address. - /// - /// # Typical errors - /// - `invalid-state`: The socket is not connected to a remote address. (ENOTCONN) - /// - /// # References - /// - - /// - - /// - - /// - - @since(version = 0.3.0-rc-2025-09-16) - get-remote-address: func() -> result; - - /// Whether the socket is in the `listening` state. - /// - /// Equivalent to the SO_ACCEPTCONN socket option. - @since(version = 0.3.0-rc-2025-09-16) - get-is-listening: func() -> bool; - - /// Whether this is a IPv4 or IPv6 socket. - /// - /// This is the value passed to the constructor. - /// - /// Equivalent to the SO_DOMAIN socket option. - @since(version = 0.3.0-rc-2025-09-16) - get-address-family: func() -> ip-address-family; - - /// Hints the desired listen queue size. Implementations are free to ignore this. - /// - /// If the provided value is 0, an `invalid-argument` error is returned. - /// Any other value will never cause an error, but it might be silently clamped and/or rounded. - /// - /// # Typical errors - /// - `not-supported`: (set) The platform does not support changing the backlog size after the initial listen. - /// - `invalid-argument`: (set) The provided value was 0. - /// - `invalid-state`: (set) The socket is in the `connecting` or `connected` state. - @since(version = 0.3.0-rc-2025-09-16) - set-listen-backlog-size: func(value: u64) -> result<_, error-code>; - - /// Enables or disables keepalive. - /// - /// The keepalive behavior can be adjusted using: - /// - `keep-alive-idle-time` - /// - `keep-alive-interval` - /// - `keep-alive-count` - /// These properties can be configured while `keep-alive-enabled` is false, but only come into effect when `keep-alive-enabled` is true. - /// - /// Equivalent to the SO_KEEPALIVE socket option. - @since(version = 0.3.0-rc-2025-09-16) - get-keep-alive-enabled: func() -> result; - @since(version = 0.3.0-rc-2025-09-16) - set-keep-alive-enabled: func(value: bool) -> result<_, error-code>; - - /// Amount of time the connection has to be idle before TCP starts sending keepalive packets. - /// - /// If the provided value is 0, an `invalid-argument` error is returned. - /// Any other value will never cause an error, but it might be silently clamped and/or rounded. - /// I.e. after setting a value, reading the same setting back may return a different value. - /// - /// Equivalent to the TCP_KEEPIDLE socket option. (TCP_KEEPALIVE on MacOS) - /// - /// # Typical errors - /// - `invalid-argument`: (set) The provided value was 0. - @since(version = 0.3.0-rc-2025-09-16) - get-keep-alive-idle-time: func() -> result; - @since(version = 0.3.0-rc-2025-09-16) - set-keep-alive-idle-time: func(value: duration) -> result<_, error-code>; - - /// The time between keepalive packets. - /// - /// If the provided value is 0, an `invalid-argument` error is returned. - /// Any other value will never cause an error, but it might be silently clamped and/or rounded. - /// I.e. after setting a value, reading the same setting back may return a different value. - /// - /// Equivalent to the TCP_KEEPINTVL socket option. - /// - /// # Typical errors - /// - `invalid-argument`: (set) The provided value was 0. - @since(version = 0.3.0-rc-2025-09-16) - get-keep-alive-interval: func() -> result; - @since(version = 0.3.0-rc-2025-09-16) - set-keep-alive-interval: func(value: duration) -> result<_, error-code>; - - /// The maximum amount of keepalive packets TCP should send before aborting the connection. - /// - /// If the provided value is 0, an `invalid-argument` error is returned. - /// Any other value will never cause an error, but it might be silently clamped and/or rounded. - /// I.e. after setting a value, reading the same setting back may return a different value. - /// - /// Equivalent to the TCP_KEEPCNT socket option. - /// - /// # Typical errors - /// - `invalid-argument`: (set) The provided value was 0. - @since(version = 0.3.0-rc-2025-09-16) - get-keep-alive-count: func() -> result; - @since(version = 0.3.0-rc-2025-09-16) - set-keep-alive-count: func(value: u32) -> result<_, error-code>; - - /// Equivalent to the IP_TTL & IPV6_UNICAST_HOPS socket options. - /// - /// If the provided value is 0, an `invalid-argument` error is returned. - /// - /// # Typical errors - /// - `invalid-argument`: (set) The TTL value must be 1 or higher. - @since(version = 0.3.0-rc-2025-09-16) - get-hop-limit: func() -> result; - @since(version = 0.3.0-rc-2025-09-16) - set-hop-limit: func(value: u8) -> result<_, error-code>; - - /// The kernel buffer space reserved for sends/receives on this socket. - /// - /// If the provided value is 0, an `invalid-argument` error is returned. - /// Any other value will never cause an error, but it might be silently clamped and/or rounded. - /// I.e. after setting a value, reading the same setting back may return a different value. - /// - /// Equivalent to the SO_RCVBUF and SO_SNDBUF socket options. - /// - /// # Typical errors - /// - `invalid-argument`: (set) The provided value was 0. - @since(version = 0.3.0-rc-2025-09-16) - get-receive-buffer-size: func() -> result; - @since(version = 0.3.0-rc-2025-09-16) - set-receive-buffer-size: func(value: u64) -> result<_, error-code>; - @since(version = 0.3.0-rc-2025-09-16) - get-send-buffer-size: func() -> result; - @since(version = 0.3.0-rc-2025-09-16) - set-send-buffer-size: func(value: u64) -> result<_, error-code>; - } - - /// A UDP socket handle. - @since(version = 0.3.0-rc-2025-09-16) - resource udp-socket { - - /// Create a new UDP socket. - /// - /// Similar to `socket(AF_INET or AF_INET6, SOCK_DGRAM, IPPROTO_UDP)` in POSIX. - /// On IPv6 sockets, IPV6_V6ONLY is enabled by default and can't be configured otherwise. - /// - /// Unlike POSIX, WASI sockets have no notion of a socket-level - /// `O_NONBLOCK` flag. Instead they fully rely on the Component Model's - /// async support. - /// - /// # References: - /// - - /// - - /// - - /// - - @since(version = 0.3.0-rc-2025-09-16) - create: static func(address-family: ip-address-family) -> result; - - /// Bind the socket to the provided IP address and port. - /// - /// 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 port is zero, the socket will be bound to a random free port. - /// - /// # Typical errors - /// - `invalid-argument`: The `local-address` has the wrong address family. (EAFNOSUPPORT, EFAULT on Windows) - /// - `invalid-state`: The socket is already bound. (EINVAL) - /// - `address-in-use`: 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 can be bound to. (EADDRNOTAVAIL) - /// - /// # References - /// - - /// - - /// - - /// - - @since(version = 0.3.0-rc-2025-09-16) - bind: func(local-address: ip-socket-address) -> result<_, error-code>; - - /// Associate this socket with a specific peer address. - /// - /// On success, the `remote-address` of the socket is updated. - /// The `local-address` may be updated as well, based on the best network - /// path to `remote-address`. If the socket was not already explicitly - /// bound, this function will implicitly bind the socket to a random - /// free port. - /// - /// When a UDP socket is "connected", the `send` and `receive` methods - /// are limited to communicating with that peer only: - /// - `send` can only be used to send to this destination. - /// - `receive` will only return datagrams sent from the provided `remote-address`. - /// - /// The name "connect" was kept to align with the existing POSIX - /// terminology. Other than that, this function only changes the local - /// socket configuration and does not generate any network traffic. - /// The peer is not aware of this "connection". - /// - /// This method may be called multiple times on the same socket to change - /// its association, but only the most recent one will be effective. - /// - /// # Typical errors - /// - `invalid-argument`: The `remote-address` has the wrong address family. (EAFNOSUPPORT) - /// - `invalid-argument`: The IP address in `remote-address` is set to INADDR_ANY (`0.0.0.0` / `::`). (EDESTADDRREQ, EADDRNOTAVAIL) - /// - `invalid-argument`: The port in `remote-address` is set to 0. (EDESTADDRREQ, EADDRNOTAVAIL) - /// - `address-in-use`: Tried to perform an implicit bind, but there were no ephemeral ports available. (EADDRINUSE, EADDRNOTAVAIL on Linux, EAGAIN on BSD) - /// - /// # Implementors note - /// If the socket is already connected, some platforms (e.g. Linux) - /// require a disconnect before connecting to a different peer address. - /// - /// # References - /// - - /// - - /// - - /// - - @since(version = 0.3.0-rc-2025-09-16) - connect: func(remote-address: ip-socket-address) -> result<_, error-code>; - - /// Dissociate this socket from its peer address. - /// - /// After calling this method, `send` & `receive` are free to communicate - /// with any address again. - /// - /// The POSIX equivalent of this is calling `connect` with an `AF_UNSPEC` address. - /// - /// # Typical errors - /// - `invalid-state`: The socket is not connected. - /// - /// # References - /// - - /// - - /// - - /// - - @since(version = 0.3.0-rc-2025-09-16) - disconnect: func() -> result<_, error-code>; - - /// Send a message on the socket to a particular peer. - /// - /// If the socket is connected, the peer address may be left empty. In - /// that case this is equivalent to `send` in POSIX. Otherwise it is - /// equivalent to `sendto`. - /// - /// Additionally, if the socket is connected, a `remote-address` argument - /// _may_ be provided but then it must be identical to the address - /// passed to `connect`. - /// - /// Implementations may trap if the `data` length exceeds 64 KiB. - /// - /// # Typical errors - /// - `invalid-argument`: The `remote-address` has the wrong address family. (EAFNOSUPPORT) - /// - `invalid-argument`: The IP address in `remote-address` is set to INADDR_ANY (`0.0.0.0` / `::`). (EDESTADDRREQ, EADDRNOTAVAIL) - /// - `invalid-argument`: The port in `remote-address` is set to 0. (EDESTADDRREQ, EADDRNOTAVAIL) - /// - `invalid-argument`: The socket is in "connected" mode and `remote-address` is `some` value that does not match the address passed to `connect`. (EISCONN) - /// - `invalid-argument`: The socket is not "connected" and no value for `remote-address` was provided. (EDESTADDRREQ) - /// - `remote-unreachable`: The remote address is not reachable. (ECONNRESET, ENETRESET on Windows, EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN, ENONET) - /// - `connection-refused`: The connection was refused. (ECONNREFUSED) - /// - `datagram-too-large`: The datagram is too large. (EMSGSIZE) - /// - /// # References - /// - - /// - - /// - - /// - - /// - - /// - - /// - - /// - - @since(version = 0.3.0-rc-2025-09-16) - send: async func(data: list, remote-address: option) -> result<_, error-code>; - - /// Receive a message on the socket. - /// - /// On success, the return value contains a tuple of the received data - /// and the address of the sender. Theoretical maximum length of the - /// data is 64 KiB. Though in practice, it will typically be less than - /// 1500 bytes. - /// - /// If the socket is connected, the sender address is guaranteed to - /// match the remote address passed to `connect`. - /// - /// # Typical errors - /// - `invalid-state`: The socket has not been bound yet. - /// - `remote-unreachable`: The remote address is not reachable. (ECONNRESET, ENETRESET on Windows, EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN, ENONET) - /// - `connection-refused`: The connection was refused. (ECONNREFUSED) - /// - /// # References - /// - - /// - - /// - - /// - - /// - - /// - - /// - - @since(version = 0.3.0-rc-2025-09-16) - receive: async func() -> result, ip-socket-address>, error-code>; - - /// Get the current bound address. - /// - /// POSIX mentions: - /// > If the socket has not been bound to a local name, the value - /// > stored in the object pointed to by `address` is unspecified. - /// - /// WASI is stricter and requires `get-local-address` to return `invalid-state` when the socket hasn't been bound yet. - /// - /// # Typical errors - /// - `invalid-state`: The socket is not bound to any local address. - /// - /// # References - /// - - /// - - /// - - /// - - @since(version = 0.3.0-rc-2025-09-16) - get-local-address: func() -> result; - - /// Get the address the socket is currently "connected" to. - /// - /// # Typical errors - /// - `invalid-state`: The socket is not "connected" to a specific remote address. (ENOTCONN) - /// - /// # References - /// - - /// - - /// - - /// - - @since(version = 0.3.0-rc-2025-09-16) - get-remote-address: func() -> result; - - /// Whether this is a IPv4 or IPv6 socket. - /// - /// This is the value passed to the constructor. - /// - /// Equivalent to the SO_DOMAIN socket option. - @since(version = 0.3.0-rc-2025-09-16) - get-address-family: func() -> ip-address-family; - - /// Equivalent to the IP_TTL & IPV6_UNICAST_HOPS socket options. - /// - /// If the provided value is 0, an `invalid-argument` error is returned. - /// - /// # Typical errors - /// - `invalid-argument`: (set) The TTL value must be 1 or higher. - @since(version = 0.3.0-rc-2025-09-16) - get-unicast-hop-limit: func() -> result; - @since(version = 0.3.0-rc-2025-09-16) - set-unicast-hop-limit: func(value: u8) -> result<_, error-code>; - - /// The kernel buffer space reserved for sends/receives on this socket. - /// - /// If the provided value is 0, an `invalid-argument` error is returned. - /// Any other value will never cause an error, but it might be silently clamped and/or rounded. - /// I.e. after setting a value, reading the same setting back may return a different value. - /// - /// Equivalent to the SO_RCVBUF and SO_SNDBUF socket options. - /// - /// # Typical errors - /// - `invalid-argument`: (set) The provided value was 0. - @since(version = 0.3.0-rc-2025-09-16) - get-receive-buffer-size: func() -> result; - @since(version = 0.3.0-rc-2025-09-16) - set-receive-buffer-size: func(value: u64) -> result<_, error-code>; - @since(version = 0.3.0-rc-2025-09-16) - get-send-buffer-size: func() -> result; - @since(version = 0.3.0-rc-2025-09-16) - set-send-buffer-size: func(value: u64) -> result<_, error-code>; - } -} diff --git a/examples/wasip3/wit/deps/sockets/world.wit b/examples/wasip3/wit/deps/sockets/world.wit deleted file mode 100644 index 44cc427..0000000 --- a/examples/wasip3/wit/deps/sockets/world.wit +++ /dev/null @@ -1,9 +0,0 @@ -package wasi:sockets@0.3.0-rc-2025-09-16; - -@since(version = 0.3.0-rc-2025-09-16) -world imports { - @since(version = 0.3.0-rc-2025-09-16) - import types; - @since(version = 0.3.0-rc-2025-09-16) - import ip-name-lookup; -} diff --git a/examples/wasip3/wit/world.wit b/examples/wasip3/wit/world.wit index bc1a736..8e7c324 100644 --- a/examples/wasip3/wit/world.wit +++ b/examples/wasip3/wit/world.wit @@ -1,6 +1,6 @@ // We actually don't use this; it's just to let bindgen! find the corresponding world in wit/deps. package wasmtime:wasi-http; -world wasip3-example { - include wasi:http/proxy@0.3.0-rc-2025-09-16; +world bindings { + include wasi:http/service@0.3.0-rc-2026-03-15; } diff --git a/src/cmd_bindings.rs b/src/cmd_bindings.rs index 8fa6b54..5a5560b 100644 --- a/src/cmd_bindings.rs +++ b/src/cmd_bindings.rs @@ -1,19 +1,19 @@ -use crate::utils::{make_path_absolute, parse_wit}; +use crate::utils::make_path_absolute; use anyhow::Result; use std::path::{Path, PathBuf}; +use wit_parser::{Resolve, WorldId}; #[allow(clippy::too_many_arguments)] pub fn generate_bindings( - wit_path: &[impl AsRef], - world: Option<&str>, - features: &[String], - all_features: bool, + resolve: &mut Resolve, + world: WorldId, generate_stubs: bool, should_format: bool, output: Option<&Path>, pkg_name: Option, + export_pkg_name: Option, + include_versions: bool, ) -> Result<()> { - let (mut resolve, world) = parse_wit(wit_path, world, features, all_features)?; let mut files = Default::default(); let format = if should_format { @@ -36,13 +36,15 @@ pub fn generate_bindings( generate_stubs, format, pkg_name, + export_pkg_name, + include_versions, ..Default::default() } .build() - .generate(&mut resolve, world, &mut files)?; + .generate(resolve, world, &mut files)?; let output_path = match output { - Some(p) => make_path_absolute(&p.to_path_buf())?, + Some(p) => make_path_absolute(p)?, None => PathBuf::from("."), }; diff --git a/src/cmd_build.rs b/src/cmd_build.rs index 156cdcf..f9ffdb3 100644 --- a/src/cmd_build.rs +++ b/src/cmd_build.rs @@ -1,23 +1,16 @@ use crate::utils::{check_go_version, make_path_absolute}; use anyhow::{Result, anyhow}; -use std::{path::PathBuf, process::Command}; +use std::{ + path::{Path, PathBuf}, + process::Command, +}; /// Compiles a Go application to a wasm module with `go build`. /// /// If the module is not going to be adapted to the component model, /// set the `only_wasip1` arg to true. -pub fn build_module( - go_module: Option<&PathBuf>, - out: Option<&PathBuf>, - go_path: Option<&PathBuf>, - only_wasip1: bool, -) -> Result { - let go = match &go_path { - Some(p) => make_path_absolute(p)?, - None => PathBuf::from("go"), - }; - - check_go_version(&go)?; +pub fn build_module(out: Option<&PathBuf>, go: &Path, only_wasip1: bool) -> Result { + check_go_version(go)?; let out_path_buf = match &out { Some(p) => make_path_absolute(p)?, @@ -33,22 +26,11 @@ pub fn build_module( .to_str() .ok_or_else(|| anyhow!("Output path is not valid unicode"))?; - let module_path = match &go_module { - Some(p) => { - if !p.is_dir() { - return Err(anyhow!("Module path '{}' is not a directory", p.display())); - } - p.to_str() - .ok_or_else(|| anyhow!("Module path is not valid unicode"))? - } - None => ".", - }; - // The -buildmode flag mutes the module's output, so it is ommitted let module_args = [ "build", "-C", - module_path, + ".", "-ldflags=-checklinkname=0", "-o", out_path, @@ -57,7 +39,7 @@ pub fn build_module( let component_args = [ "build", "-C", - module_path, + ".", "-buildmode=c-shared", "-ldflags=-checklinkname=0", "-o", @@ -65,13 +47,13 @@ pub fn build_module( ]; let output = if only_wasip1 { - Command::new(&go) + Command::new(go) .args(module_args) .env("GOOS", "wasip1") .env("GOARCH", "wasm") .output()? } else { - Command::new(&go) + Command::new(go) .args(component_args) .env("GOOS", "wasip1") .env("GOARCH", "wasm") diff --git a/src/cmd_test.rs b/src/cmd_test.rs index 7b47f50..1cd1954 100644 --- a/src/cmd_test.rs +++ b/src/cmd_test.rs @@ -12,15 +12,10 @@ use std::{ pub fn build_test_module( path: &Path, output_dir: Option<&PathBuf>, - go_path: Option<&PathBuf>, + go: &Path, only_wasip1: bool, ) -> Result { - let go = match &go_path { - Some(p) => make_path_absolute(p)?, - None => PathBuf::from("go"), - }; - - check_go_version(&go)?; + check_go_version(go)?; let test_wasm_path = { // The directory in which the test component will be placed @@ -68,7 +63,7 @@ pub fn build_test_module( ]; let output = if only_wasip1 { - Command::new(&go) + Command::new(go) .args(module_args) .env("GOOS", "wasip1") .env("GOARCH", "wasm") @@ -80,7 +75,7 @@ pub fn build_test_module( // TODO: for when we figure out how wasip2 tests are to be run #[allow(unreachable_code)] - Command::new(&go) + Command::new(go) .args(component_args) .env("GOOS", "wasip1") .env("GOARCH", "wasm") diff --git a/src/command.rs b/src/command.rs index f50dadf..336e2e5 100644 --- a/src/command.rs +++ b/src/command.rs @@ -2,7 +2,7 @@ use crate::{ cmd_bindings::generate_bindings, cmd_build::build_module, cmd_test::build_test_module, - utils::{embed_wit, module_to_component}, + utils::{dummy_wit, embed_wit, module_to_component, parse_wit, pick_go}, }; use anyhow::{Result, anyhow}; use clap::{Parser, Subcommand}; @@ -28,12 +28,30 @@ pub struct WitOpts { /// /// These paths can be either directories containing `*.wit` files, `*.wit` /// files themselves, or `*.wasm` files which are wasm-encoded WIT packages. + /// + /// Note that, unless `--ignore-toml-files` is specified, `componentize-go` + /// will also use `go list` to scan the current Go module and its + /// dependencies to find any `componentize-go.toml` files. The WIT + /// documents referenced by any such files will be added to this list + /// automatically. #[arg(long, short = 'd')] pub wit_path: Vec, - /// Name of world to target (or default world if `None`). + /// Name of world to target (or default world if not specified). + /// + /// This may be specified more than once, in which case the worlds will be + /// merged. + /// + /// Note that, unless `--ignore-toml-files` _or_ at least one `--world` + /// option is specified, `componentize-go` will use `go list` to scan the + /// current Go module and its dependencies to find any + /// `componentize-go.toml` files, and the WIT worlds referenced by any such + /// files will be used. #[arg(long, short = 'w')] - pub world: Option, + pub world: Vec, + + #[arg(long)] + pub ignore_toml_files: bool, /// Whether or not to activate all WIT features when processing WIT files. /// @@ -73,11 +91,14 @@ pub struct Build { #[arg(long, short = 'o')] pub output: Option, - /// The directory containing the "go.mod" file (or current directory if `None`). - #[arg(long = "mod")] - pub mod_path: Option, - /// The path to the Go binary (or look for binary in PATH if `None`). + /// + /// If the target WIT world uses async features, and the specified Go binary + /// (or the one in PATH if `None`) does not include [this + /// patch](https://github.com/golang/go/pull/76775), a patched version will + /// be downloaded, stored in the current user's [cache + /// directory](https://docs.rs/dirs/latest/dirs/fn.cache_dir.html), and used + /// for building. #[arg(long)] pub go: Option, @@ -111,6 +132,13 @@ pub struct Test { pub output: Option, /// The path to the Go binary (or look for binary in PATH if `None`). + /// + /// If the target WIT world uses async features, and the specified Go binary + /// (or the one in PATH if `None`) does not include [this + /// patch](https://github.com/golang/go/pull/76775), a patched version will + /// be downloaded, stored in the current user's [cache + /// directory](https://docs.rs/dirs/latest/dirs/fn.cache_dir.html), and used + /// for testing. #[arg(long)] pub go: Option, @@ -139,6 +167,24 @@ pub struct Bindings { /// otherwise (if None), the bindings will be organized for use as a standalone executable. #[arg(long)] pub pkg_name: Option, + + /// When `--pkg-name` is specified, optionally specify a different package + /// for exports. + /// + /// This allows you to put the exports and imports in separate packages when + /// building a library. If only `--pkg-name` is specified, this will + /// default to that value. + #[arg(long, requires = "pkg_name")] + pub export_pkg_name: Option, + + /// When generating Go package names, include the WIT package version even + /// if only one version of that package is referenced by the specified + /// world. + /// + /// By default, the version will only be included in the name if the world + /// references more than one version of the WIT package. + #[arg(long)] + pub include_versions: bool, } pub fn run + Clone, I: IntoIterator>(args: I) -> Result<()> { @@ -151,52 +197,63 @@ pub fn run + Clone, I: IntoIterator>(args: I) -> Res } fn build(wit_opts: WitOpts, build: Build) -> Result<()> { + let (resolve, world) = if build.wasip1 { + dummy_wit() + } else { + parse_wit( + &wit_opts.wit_path, + &wit_opts.world, + wit_opts.ignore_toml_files, + &wit_opts.features, + wit_opts.all_features, + )? + }; + + let go = &pick_go(&resolve, world, build.go.as_deref())?; + // Build a wasm module using `go build`. - let module = build_module( - build.mod_path.as_ref(), - build.output.as_ref(), - build.go.as_ref(), - build.wasip1, - )?; + let module = build_module(build.output.as_ref(), go, build.wasip1)?; if !build.wasip1 { // Embed the WIT documents in the wasip1 component. - embed_wit( - &module, - &wit_opts.wit_path, - wit_opts.world.as_deref(), - &wit_opts.features, - wit_opts.all_features, - )?; + embed_wit(&module, &resolve, world)?; // Update the wasm module to use the current component model ABI. - module_to_component(&module, &build.adapt)?; + module_to_component(&module, build.adapt.as_deref())?; } Ok(()) } fn test(wit_opts: WitOpts, test: Test) -> Result<()> { + let (resolve, world) = if test.wasip1 { + dummy_wit() + } else { + parse_wit( + &wit_opts.wit_path, + &wit_opts.world, + wit_opts.ignore_toml_files, + &wit_opts.features, + wit_opts.all_features, + )? + }; + + let go = &pick_go(&resolve, world, test.go.as_deref())?; + if test.pkg.is_empty() { return Err(anyhow!("Path to a package containing Go tests is required")); } for pkg in test.pkg.iter() { // Build a wasm module using `go test -c`. - let module = build_test_module(pkg, test.output.as_ref(), test.go.as_ref(), test.wasip1)?; + let module = build_test_module(pkg, test.output.as_ref(), go, test.wasip1)?; if !test.wasip1 { // Embed the WIT documents in the wasm module. - embed_wit( - &module, - &wit_opts.wit_path, - wit_opts.world.as_deref(), - &wit_opts.features, - wit_opts.all_features, - )?; + embed_wit(&module, &resolve, world)?; // Update the wasm module to use the current component model ABI. - module_to_component(&module, &test.adapt)?; + module_to_component(&module, test.adapt.as_deref())?; } } @@ -204,14 +261,22 @@ fn test(wit_opts: WitOpts, test: Test) -> Result<()> { } fn bindings(wit_opts: WitOpts, bindings: Bindings) -> Result<()> { - generate_bindings( - wit_opts.wit_path.as_ref(), - wit_opts.world.as_deref(), + let (mut resolve, world) = parse_wit( + &wit_opts.wit_path, + &wit_opts.world, + wit_opts.ignore_toml_files, &wit_opts.features, wit_opts.all_features, + )?; + + generate_bindings( + &mut resolve, + world, bindings.generate_stubs, bindings.format, bindings.output.as_deref(), bindings.pkg_name, + bindings.export_pkg_name, + bindings.include_versions, ) } diff --git a/src/utils.rs b/src/utils.rs index 7e8a03d..1de5696 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,9 +1,33 @@ -use anyhow::{Context, Result, anyhow}; +use anyhow::{Context, Result, anyhow, bail}; +use bzip2::read::BzDecoder; +use serde::Deserialize; use std::{ + collections::BTreeSet, + fs::{self, File}, + io::Cursor, path::{Path, PathBuf}, process::Command, }; -use wit_parser::{PackageId, Resolve, WorldId}; +use tar::Archive; +use wit_parser::{ + CloneMaps, Function, Interface, Package, PackageName, Resolve, Stability, Type, TypeDef, + TypeDefKind, World, WorldId, WorldItem, +}; + +pub fn dummy_wit() -> (Resolve, WorldId) { + let mut resolve = Resolve::default(); + let world = resolve.worlds.alloc(World { + name: "dummy-world".into(), + imports: Default::default(), + exports: Default::default(), + package: Default::default(), + docs: Default::default(), + stability: Default::default(), + includes: Default::default(), + span: Default::default(), + }); + (resolve, world) +} // In the rare case the snapshot needs to be updated, the latest version // can be found here: https://github.com/bytecodealliance/wasmtime/releases @@ -11,15 +35,18 @@ const WASIP1_SNAPSHOT_ADAPT: &[u8] = include_bytes!("wasi_snapshot_preview1.reac pub fn parse_wit( paths: &[impl AsRef], - world: Option<&str>, + worlds: &[String], + ignore_toml_files: bool, features: &[String], all_features: bool, ) -> Result<(Resolve, WorldId)> { + let (paths, worlds) = &maybe_add_dependencies(paths, worlds, ignore_toml_files)?; + // If no WIT directory was provided as a parameter and none were referenced // by Go packages, use ./wit by default. if paths.is_empty() { let paths = &[Path::new("wit")]; - return parse_wit(paths, world, features, all_features); + return parse_wit(paths, worlds, ignore_toml_files, features, all_features); } debug_assert!(!paths.is_empty(), "The paths should not be empty"); @@ -37,18 +64,131 @@ pub fn parse_wit( } } - let mut main_packages: Vec = vec![]; - for path in paths.iter().map(AsRef::as_ref) { - let (pkg, _files) = resolve.push_path(path)?; - main_packages.push(pkg); - } + let packages = paths + .iter() + .map(|path| { + // Consolidates if the same package is referenced in multiple worlds + let mut tmp = Resolve { + all_features, + features: resolve.features.clone(), + ..Default::default() + }; + let (pkg, _files) = tmp.push_path(path)?; + let consolidated = resolve.merge(tmp)?; + Ok(consolidated.packages[pkg.index()]) + }) + .collect::>>()?; + + let worlds = worlds + .iter() + .map(|world| { + packages + .iter() + .find_map(|&pkg| resolve.select_world(&[pkg], Some(world)).ok()) + .ok_or_else(|| { + anyhow!("no world named `{world}` found in any of the loaded WIT packages") + }) + }) + .collect::>>()?; + + let world = match &worlds[..] { + [] => packages + .iter() + .find_map(|&pkg| resolve.select_world(&[pkg], None).ok()) + .ok_or_else(|| anyhow!("no default world found in any of the loaded WIT packages"))?, + &[world] => world, + worlds => { + let union_package = resolve.packages.alloc(Package { + name: PackageName { + namespace: "componentize-go".into(), + name: "union".into(), + version: None, + }, + docs: Default::default(), + interfaces: Default::default(), + worlds: Default::default(), + }); + + let union_world = resolve.worlds.alloc(World { + name: "union".into(), + imports: Default::default(), + exports: Default::default(), + package: Some(union_package), + docs: Default::default(), + stability: Stability::Unknown, + includes: Default::default(), + span: Default::default(), + }); + + resolve.packages[union_package] + .worlds + .insert("union".into(), union_world); + + for &world in worlds { + resolve.merge_worlds(world, union_world, &mut CloneMaps::default())?; + } + + union_world + } + }; - let world = resolve.select_world(&main_packages, world)?; Ok((resolve, world)) } +/// Unless `ignore_toml_files` is `true`, use `go list` to search the current +/// module and its dependencies for any `componentize-go.toml` files. The WIT +/// path and/or world specified in each such file will be added to the +/// respective list and returned. +fn maybe_add_dependencies( + paths: &[impl AsRef], + worlds: &[String], + ignore_toml_files: bool, +) -> Result<(Vec, Vec)> { + let mut paths = paths + .iter() + .map(|v| PathBuf::from(v.as_ref())) + .collect::>(); + let mut worlds = worlds.iter().cloned().collect::>(); + // Only add worlds from `componentize-go.toml` files if none were specified + // explicitly via the CLI: + let add_worlds = worlds.is_empty(); + + if !ignore_toml_files && Path::new("go.mod").exists() { + let output = Command::new("go") + .args(["list", "-mod=readonly", "-m", "-f", "{{.Dir}}", "all"]) + .output()?; + if !output.status.success() { + bail!( + "`go list` failed: {}", + String::from_utf8_lossy(&output.stderr) + ); + } + + #[derive(Deserialize)] + struct ComponentizeGoConfig { + #[serde(default)] + worlds: Vec, + #[serde(default)] + wit_paths: Vec, + } + + for module in String::from_utf8(output.stdout)?.lines() { + let module = PathBuf::from(module); + if let Ok(manifest) = fs::read_to_string(module.join("componentize-go.toml")) { + let config = toml::from_str::(&manifest)?; + if add_worlds { + worlds.extend(config.worlds); + } + paths.extend(config.wit_paths.into_iter().map(|v| module.join(v))); + } + } + } + + Ok((paths.into_iter().collect(), worlds.into_iter().collect())) +} + // Converts a relative path to an absolute path. -pub fn make_path_absolute(p: &PathBuf) -> Result { +pub fn make_path_absolute(p: &Path) -> Result { if p.is_relative() { Ok(std::env::current_dir()?.join(p)) } else { @@ -56,34 +196,26 @@ pub fn make_path_absolute(p: &PathBuf) -> Result { } } -pub fn embed_wit( - wasm_file: &PathBuf, - wit_path: &[PathBuf], - world: Option<&str>, - features: &[String], - all_features: bool, -) -> Result<()> { - let mut wasm = wat::Parser::new().parse_file(wasm_file)?; - let (resolve, world_id) = parse_wit(wit_path, world, features, all_features)?; +pub fn embed_wit(wasm_file: &Path, resolve: &Resolve, world: WorldId) -> Result<()> { + let mut wasm = fs::read(wasm_file)?; wit_component::embed_component_metadata( &mut wasm, - &resolve, - world_id, + resolve, + world, wit_component::StringEncoding::UTF8, )?; - std::fs::write(wasm_file, wasm) - .context(format!("failed to write '{}'", wasm_file.display()))?; + fs::write(wasm_file, wasm).context(format!("failed to write '{}'", wasm_file.display()))?; Ok(()) } /// Update the wasm module to use the current component model ABI. -pub fn module_to_component(wasm_file: &PathBuf, adapt_file: &Option) -> Result<()> { - let wasm: Vec = wat::Parser::new().parse_file(wasm_file)?; +pub fn module_to_component(wasm_file: &Path, adapt_file: Option<&Path>) -> Result<()> { + let wasm: Vec = fs::read(wasm_file)?; let mut encoder = wit_component::ComponentEncoder::default().validate(true); encoder = encoder.module(&wasm)?; let adapt_bytes = if let Some(adapt) = adapt_file { - std::fs::read(adapt) + fs::read(adapt) .with_context(|| format!("failed to read adapt file '{}'", adapt.display()))? } else { WASIP1_SNAPSHOT_ADAPT.to_vec() @@ -94,14 +226,13 @@ pub fn module_to_component(wasm_file: &PathBuf, adapt_file: &Option) -> .encode() .context("failed to encode component from module")?; - std::fs::write(wasm_file, bytes) - .context(format!("failed to write `{}`", wasm_file.display()))?; + fs::write(wasm_file, bytes).context(format!("failed to write `{}`", wasm_file.display()))?; Ok(()) } /// Ensure that the Go version is compatible with the embedded Wasm tooling. -pub fn check_go_version(go_path: &PathBuf) -> Result<()> { +pub fn check_go_version(go_path: &Path) -> Result<()> { let output = Command::new(go_path).arg("version").output()?; if !output.status.success() { @@ -140,3 +271,184 @@ pub fn check_go_version(go_path: &PathBuf) -> Result<()> { )) } } + +fn check_go_async_support(go: &Path) -> Option<()> { + fs::read_to_string( + go.parent()? + .parent()? + .join("src") + .join("runtime") + .join("lock_wasip1.go"), + ) + .ok()? + .contains("wasiOnIdle") + .then_some(()) +} + +fn world_needs_async(resolve: &Resolve, world: WorldId) -> bool { + fn typedef_needs_async(resolve: &Resolve, ty: &TypeDef) -> bool { + match &ty.kind { + TypeDefKind::Record(v) => v.fields.iter().any(|v| type_needs_async(resolve, v.ty)), + TypeDefKind::Tuple(v) => v.types.iter().any(|&v| type_needs_async(resolve, v)), + TypeDefKind::Variant(v) => v + .cases + .iter() + .any(|v| v.ty.map(|v| type_needs_async(resolve, v)).unwrap_or(false)), + &TypeDefKind::Type(v) + | &TypeDefKind::Option(v) + | &TypeDefKind::List(v) + | &TypeDefKind::FixedLengthList(v, _) => type_needs_async(resolve, v), + TypeDefKind::Result(v) => { + v.ok.map(|v| type_needs_async(resolve, v)).unwrap_or(false) + || v.err.map(|v| type_needs_async(resolve, v)).unwrap_or(false) + } + &TypeDefKind::Map(k, v) => type_needs_async(resolve, k) || type_needs_async(resolve, v), + TypeDefKind::Future(_) | TypeDefKind::Stream(_) => true, + TypeDefKind::Resource + | TypeDefKind::Handle(_) + | TypeDefKind::Flags(_) + | TypeDefKind::Enum(_) => false, + TypeDefKind::Unknown => unreachable!(), + } + } + + fn type_needs_async(resolve: &Resolve, ty: Type) -> bool { + match ty { + Type::Bool + | Type::U8 + | Type::U16 + | Type::U32 + | Type::U64 + | Type::S8 + | Type::S16 + | Type::S32 + | Type::S64 + | Type::F32 + | Type::F64 + | Type::Char + | Type::String + | Type::ErrorContext => false, + Type::Id(id) => typedef_needs_async(resolve, &resolve.types[id]), + } + } + + let function_needs_async = |fun: &Function| { + fun.kind.is_async() + || fun.params.iter().any(|v| type_needs_async(resolve, v.ty)) + || fun + .result + .map(|ty| type_needs_async(resolve, ty)) + .unwrap_or(false) + }; + + let interface_needs_async = |interface: &Interface| { + interface + .types + .values() + .any(|&id| type_needs_async(resolve, Type::Id(id))) + || interface.functions.values().any(function_needs_async) + }; + + let world = &resolve.worlds[world]; + world + .imports + .values() + .chain(world.exports.values()) + .any(|item| { + match item { + &WorldItem::Interface { id, .. } => { + if interface_needs_async(&resolve.interfaces[id]) { + return true; + } + } + WorldItem::Function(fun) => { + if function_needs_async(fun) { + return true; + } + } + &WorldItem::Type { id, .. } => { + if type_needs_async(resolve, Type::Id(id)) { + return true; + } + } + } + false + }) +} + +pub fn pick_go(resolve: &Resolve, world: WorldId, go_path: Option<&Path>) -> Result { + let go = match go_path { + Some(p) => Some(make_path_absolute(p)?), + None => which::which("go").ok(), + }; + + if let Some(go) = go { + if check_go_version(&go).is_err() { + eprintln!( + "Note: {} is not a compatible version of Go; will use downloaded version.", + go.display() + ); + } else if world_needs_async(resolve, world) && check_go_async_support(&go).is_none() { + eprintln!( + "Note: {} does not support async operation; will use downloaded version.\n\ + See https://github.com/golang/go/pull/76775 for details.", + go.display() + ); + } else { + return Ok(go); + } + } else { + eprintln!("Note: `go` command not found; will use downloaded version."); + } + + let Some(cache_dir) = dirs::cache_dir() else { + bail!("unable to determine cache directory for current user"); + }; + + // Determine OS and architecture + let os = match std::env::consts::OS { + "macos" => "darwin", + "linux" => "linux", + "windows" => "windows", + bad_os => panic!("OS not supported: {bad_os}"), + }; + + // Map to Go's naming conventions + let arch = match std::env::consts::ARCH { + "aarch64" => "arm64", + "x86_64" => "amd64", + bad_arch => panic!("ARCH not supported: {bad_arch}"), + }; + + let cache_dir = &cache_dir.join("componentize-go"); + let name = &format!("go-{os}-{arch}-bootstrap"); + let dir = cache_dir.join(name); + let bin = dir.join("bin").join("go"); + + fs::create_dir_all(cache_dir)?; + + // Grab a lock to avoid concurrent downloads + let lock_file = File::create(cache_dir.join("lock"))?; + lock_file.lock()?; + + if !bin.exists() { + let url = format!( + "https://github.com/dicej/go/releases/download/go1.25.5-wasi-on-idle/{name}.tbz" + ); + + eprintln!("Downloading patched Go from {url}."); + + let content = reqwest::blocking::get(&url)?.error_for_status()?.bytes()?; + + eprintln!("Extracting patched Go to {}.", cache_dir.display()); + + Archive::new(BzDecoder::new(Cursor::new(content))).unpack(cache_dir)?; + } + + check_go_version(&bin)?; + check_go_async_support(&bin).ok_or_else(|| anyhow!("downloaded Go does not support async"))?; + + eprintln!("Using {}.", bin.display()); + + Ok(bin) +} diff --git a/tests/Cargo.toml b/tests/Cargo.toml index e25713e..6340b4b 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -9,8 +9,8 @@ workspace = true [dependencies] componentize-go = { path = ".." } anyhow = { workspace = true } -bzip2 = "0.6.1" +bzip2 = { workspace = true } once_cell = "1.21.3" -reqwest = { version = "0.13.1", features = ["blocking"] } -tar = "0.4" +reqwest = { workspace = true } +tar = { workspace = true } tokio = { version = "1.49.0", features = ["macros", "rt-multi-thread"] } diff --git a/tests/src/lib.rs b/tests/src/lib.rs index b7ed602..8805911 100644 --- a/tests/src/lib.rs +++ b/tests/src/lib.rs @@ -4,8 +4,9 @@ mod tests { use core::panic; use once_cell::sync::Lazy; use std::{ + env, net::TcpListener, - path::PathBuf, + path::{Path, PathBuf}, process::{Child, Command, Stdio}, time::Duration, }; @@ -15,7 +16,6 @@ mod tests { let root_manifest = test_manifest.parent().unwrap(); let build_output = Command::new("cargo") .arg("build") - .arg("--release") .args([ "--manifest-path", root_manifest.join("Cargo.toml").to_str().unwrap(), @@ -27,80 +27,18 @@ mod tests { panic!("{}", String::from_utf8_lossy(&build_output.stderr)); } - root_manifest.join("target/release/componentize-go") + root_manifest.join("target/debug/componentize-go") }); - // TODO: Once the patch is merged in Big Go, this needs to be removed. - async fn patched_go_path() -> PathBuf { - let test_manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - let root_manifest = test_manifest.parent().unwrap(); - - // Determine OS and architecture - let os = match std::env::consts::OS { - "macos" => "darwin", - "linux" => "linux", - "windows" => "windows", - bad_os => panic!("OS not supported: {bad_os}"), - }; - - // Map to Go's naming conventions - let arch = match std::env::consts::ARCH { - "aarch64" => "arm64", - "x86_64" => "amd64", - bad_arch => panic!("ARCH not supported: {bad_arch}"), - }; - - let go_dir = format!("go-{os}-{arch}-bootstrap"); - let go_path = root_manifest.join(&go_dir); - let go_bin = go_path.join("bin").join("go"); - - // Skip if already installed - if go_bin.exists() { - return go_bin; - } - - // Download the patched Go toolchain - let archive_name = format!("{go_dir}.tbz"); - let archive_path = root_manifest.join(&archive_name); - let download_url = format!( - "https://github.com/dicej/go/releases/download/go1.25.5-wasi-on-idle/{archive_name}" - ); - - println!("Downloading patched Go from {download_url}"); - let response = reqwest::get(&download_url) - .await - .expect("Failed to download patched Go"); - - std::fs::write( - &archive_path, - response.bytes().await.expect("Failed to read download"), - ) - .expect("Failed to write archive"); - - // Extract the archive - println!("Extracting {} to {}", archive_name, root_manifest.display()); - let tar_file = std::fs::File::open(&archive_path).expect("Failed to open archive"); - let tar_decoder = bzip2::read::BzDecoder::new(tar_file); - let mut archive = tar::Archive::new(tar_decoder); - archive - .unpack(root_manifest) - .expect("Failed to extract archive"); - - // Clean up archive - std::fs::remove_file(&archive_path).ok(); - - go_bin - } - struct App { /// The path to the example application - path: String, - /// The WIT world to target - world: Option, + path: PathBuf, + /// The WIT worlds to target + worlds: Vec, /// The output path of the wasm file wasm_path: String, - /// The path to the directory containing the WIT files - wit_path: Option, + /// The paths to the directories containing the WIT files + wit_paths: Vec, /// The child process ID of a running wasm app process: Option, /// Any tests that need to be compiled and run as such @@ -116,60 +54,47 @@ mod tests { impl App { /// Create a new app runner. fn new( - path: &str, - world: Option<&str>, + path: &Path, + wit_paths: &[&Path], + worlds: &[&str], tests: Option>, is_component: bool, ) -> Self { - let path = componentize_go::utils::make_path_absolute(&PathBuf::from(path)) + let path = &componentize_go::utils::make_path_absolute(&PathBuf::from(path)) .expect("failed to make app path absolute"); - let world = if is_component { - Some(world.expect("WIT world must be specified").to_string()) - } else { - None - }; - - let wit_path = if is_component { - Some( - path.join("wit") - .to_str() - .expect("wit_path is not valid unicode") - .to_string(), - ) - } else { - None - }; + if is_component { + assert!(!wit_paths.is_empty()); + assert!(!worlds.is_empty()); + } App { - path: path - .clone() - .to_str() - .expect("app path is not valid unicode") - .to_string(), - world, + path: path.into(), + worlds: worlds.iter().map(|v| v.to_string()).collect(), wasm_path: path .join("main.wasm") .to_str() .expect("wasm_path is not valid unicode") .to_string(), - wit_path, + wit_paths: wit_paths.iter().map(|v| v.into()).collect(), process: None, tests, } } - fn build_test_modules(&self, go: Option<&PathBuf>) -> Result<()> { + fn build_test_modules(&self) -> Result<()> { let test_pkgs = self.tests.as_ref().expect("missing test_pkg_paths"); - self.generate_bindings(go)?; + self.generate_bindings()?; let mut test_cmd = Command::new(COMPONENTIZE_GO_PATH.as_path()); - test_cmd - .args(["-w", self.world.as_ref().expect("missing WIT world")]) - .args(["-d", self.wit_path.as_ref().expect("missing WIT path")]) - .arg("test") - .arg("--wasip1"); + for world in &self.worlds { + test_cmd.args(["-w", world]); + } + for path in &self.wit_paths { + test_cmd.arg("-d").arg(path); + } + test_cmd.arg("test").arg("--wasip1"); // Add all the paths to the packages that have unit tests to compile for test in test_pkgs.iter() { @@ -181,13 +106,13 @@ mod tests { let test_output = test_cmd.output().expect(&format!( "failed to execute componentize-go for \"{}\"", - self.path + self.path.display() )); if !test_output.status.success() { return Err(anyhow!( "failed to build application \"{}\": {}", - self.path, + self.path.display(), String::from_utf8_lossy(&test_output.stderr) )); } @@ -262,7 +187,7 @@ mod tests { Ok(()) } - fn build_module(&self, go: Option<&PathBuf>) -> Result<()> { + fn build_module(&self) -> Result<()> { // Build component let mut build_cmd = Command::new(COMPONENTIZE_GO_PATH.as_path()); build_cmd @@ -270,22 +195,18 @@ mod tests { .arg("--wasip1") .args(["-o", &self.wasm_path]); - if let Some(go_path) = go.as_ref() { - build_cmd.args(["--go", go_path.to_str().unwrap()]); - } - // Run `go build` in the same directory as the go.mod file. build_cmd.current_dir(&self.path); let build_output = build_cmd.output().expect(&format!( "failed to execute componentize-go for \"{}\"", - self.path + self.path.display() )); if !build_output.status.success() { return Err(anyhow!( "failed to build application \"{}\": {}", - self.path, + self.path.display(), String::from_utf8_lossy(&build_output.stderr) )); } @@ -293,33 +214,31 @@ mod tests { Ok(()) } - fn build_component(&self, go: Option<&PathBuf>) -> Result<()> { - self.generate_bindings(go)?; + fn build_component(&self) -> Result<()> { + self.generate_bindings()?; // Build component let mut build_cmd = Command::new(COMPONENTIZE_GO_PATH.as_path()); - build_cmd - .args(["-w", self.world.as_ref().expect("missing WIT world")]) - .args(["-d", self.wit_path.as_ref().expect("missing WIT path")]) - .arg("build") - .args(["-o", &self.wasm_path]); - - if let Some(go_path) = go.as_ref() { - build_cmd.args(["--go", go_path.to_str().unwrap()]); + for world in &self.worlds { + build_cmd.args(["-w", world]); + } + for path in &self.wit_paths { + build_cmd.arg("-d").arg(path); } + build_cmd.arg("build").args(["-o", &self.wasm_path]); // Run `go build` in the same directory as the go.mod file. build_cmd.current_dir(&self.path); let build_output = build_cmd.output().expect(&format!( "failed to execute componentize-go for \"{}\"", - self.path + self.path.display() )); if !build_output.status.success() { return Err(anyhow!( "failed to build application \"{}\": {}", - self.path, + self.path.display(), String::from_utf8_lossy(&build_output.stderr) )); } @@ -327,17 +246,24 @@ mod tests { Ok(()) } - fn generate_bindings(&self, go: Option<&PathBuf>) -> Result<()> { - let bindings_output = Command::new(COMPONENTIZE_GO_PATH.as_path()) - .args(["-w", self.world.as_ref().expect("missing WIT world")]) - .args(["-d", self.wit_path.as_ref().expect("missing WIT path")]) + fn generate_bindings(&self) -> Result<()> { + let mut cmd = Command::new(COMPONENTIZE_GO_PATH.as_path()); + for world in &self.worlds { + cmd.args(["-w", world]); + } + for path in &self.wit_paths { + cmd.arg("-d").arg(path); + } + + let bindings_output = cmd .arg("bindings") - .args(["-o", &self.path]) + .arg("-o") + .arg(&self.path) .current_dir(&self.path) .output() .expect(&format!( "failed to generate bindings for application \"{}\"", - &self.path + self.path.display() )); if !bindings_output.status.success() { return Err(anyhow!( @@ -347,17 +273,12 @@ mod tests { } // Tidy Go mod - let tidy_output = Command::new(if let Some(path) = go.as_ref() { - String::from(path.to_str().unwrap()) - } else { - // Default to PATH - "go".to_string() - }) - .arg("mod") - .arg("tidy") - .current_dir(&self.path) - .output() - .expect("failed to tidy Go mod"); + let tidy_output = Command::new("go") + .arg("mod") + .arg("tidy") + .current_dir(&self.path) + .output() + .expect("failed to tidy Go mod"); if !tidy_output.status.success() { return Err(anyhow!("{}", String::from_utf8_lossy(&tidy_output.stderr))); } @@ -414,8 +335,8 @@ mod tests { #[test] fn example_wasip1() { - let app = App::new("../examples/wasip1", None, None, false); - app.build_module(None).expect("failed to build app module"); + let app = App::new(Path::new("../examples/wasip1"), &[], &[], None, false); + app.build_module().expect("failed to build app module"); app.run_module().expect("failed to run app module"); } @@ -432,20 +353,23 @@ mod tests { }, ]; + let cwd = env::current_dir().unwrap(); + let app_dir = cwd.parent().unwrap().join("examples").join("wasip2"); let mut app = App::new( - "../examples/wasip2", - Some("wasip2-example"), + &app_dir, + &[&app_dir.join("wit")], + &["wasip2-example"], Some(unit_tests), true, ); - app.build_component(None).expect("failed to build app"); + app.build_component().expect("failed to build app"); app.run_component("/", "Hello, world!") .await .expect("app failed to run"); - app.build_test_modules(None) + app.build_test_modules() .expect("failed to build app unit tests"); app.run_test_modules() @@ -454,11 +378,40 @@ mod tests { #[tokio::test] async fn example_wasip3() { - let mut app = App::new("../examples/wasip3", Some("wasip3-example"), None, true); - app.build_component(Some(&patched_go_path().await)) - .expect("failed to build app"); + let cwd = env::current_dir().unwrap(); + let app_dir = cwd.parent().unwrap().join("examples").join("wasip3"); + let mut app = App::new( + &app_dir, + &[&app_dir.join("wit")], + &["wasi:http/service"], + None, + true, + ); + app.build_component().expect("failed to build app"); + app.run_component("/hello", "Hello, world!") + .await + .expect("app failed to run"); + } + + #[tokio::test] + async fn example_multiple_worlds() -> Result<()> { + let cwd = env::current_dir()?; + let app_dir = cwd + .parent() + .unwrap() + .join("examples") + .join("multiple-worlds"); + let mut app = App::new( + &app_dir, + &[&app_dir.join("wit1"), &app_dir.join("wit2")], + &["world1", "world2"], + None, + true, + ); + app.build_component().expect("failed to build app"); app.run_component("/hello", "Hello, world!") .await .expect("app failed to run"); + Ok(()) } }