diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 7fe7e71..d0e50e6 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -134,7 +134,7 @@ jobs: - uses: taiki-e/install-action@v2 with: - tool: wasmtime-cli@41.0.3 + tool: wasmtime-cli@43.0.0 - uses: actions/setup-python@v5 with: diff --git a/.gitignore b/.gitignore index 430b6e3..73b9a8d 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ examples/http/.spin examples/http/http.wasm examples/http/proxy examples/http/poll_loop.py +examples/http-p3/http.wasm examples/tcp-p3/tcp.wasm examples/tcp/tcp.wasm examples/tcp/command diff --git a/Cargo.lock b/Cargo.lock index 6f1413e..13c9229 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,7 +8,16 @@ version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" dependencies = [ - "gimli", + "gimli 0.32.3", +] + +[[package]] +name = "addr2line" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59317f77929f0e679d39364702289274de2f0f0b22cbf50b2b8cff2169a0b27a" +dependencies = [ + "gimli 0.33.1", ] [[package]] @@ -195,11 +204,11 @@ version = "0.3.76" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" dependencies = [ - "addr2line", + "addr2line 0.25.1", "cfg-if", "libc", "miniz_oxide", - "object", + "object 0.37.3", "rustc-demangle", "windows-link", ] @@ -292,9 +301,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.19.1" +version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" dependencies = [ "allocator-api2", ] @@ -477,7 +486,7 @@ dependencies = [ [[package]] name = "componentize-py" -version = "0.21.0" +version = "0.22.0" dependencies = [ "anyhow", "assert_cmd", @@ -576,46 +585,48 @@ dependencies = [ [[package]] name = "cranelift-assembler-x64" -version = "0.128.3" +version = "0.130.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0377b13bf002a0774fcccac4f1102a10f04893d24060cf4b7350c87e4cbb647c" +checksum = "4f248321c6a7d4de5dcf2939368e96a397ad3f53b6a076e38d0104d1da326d37" dependencies = [ "cranelift-assembler-x64-meta", ] [[package]] name = "cranelift-assembler-x64-meta" -version = "0.128.3" +version = "0.130.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfa027979140d023b25bf7509fb7ede3a54c3d3871fb5ead4673c4b633f671a2" +checksum = "ab6d78ff1f7d9bf8b7e1afbedbf78ba49e38e9da479d4c8a2db094e22f64e2bc" dependencies = [ "cranelift-srcgen", ] [[package]] name = "cranelift-bforest" -version = "0.128.3" +version = "0.130.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "618e4da87d9179a70b3c2f664451ca8898987aa6eb9f487d16988588b5d8cc40" +checksum = "6b6005ba640213a5b95382aeaf6b82bf028309581c8d7349778d66f27dc1180b" dependencies = [ "cranelift-entity", + "wasmtime-internal-core", ] [[package]] name = "cranelift-bitset" -version = "0.128.3" +version = "0.130.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db53764b5dad233b37b8f5dc54d3caa9900c54579195e00f17ea21f03f71aaa7" +checksum = "81fb5b134a12b559ff0c0f5af0fcd755ad380723b5016c4e0d36f74d39485340" dependencies = [ "serde", "serde_derive", + "wasmtime-internal-core", ] [[package]] name = "cranelift-codegen" -version = "0.128.3" +version = "0.130.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ae927f1d8c0abddaa863acd201471d56e7fc6c3925104f4861ed4dc3e28b421" +checksum = "85837de8be7f17a4034a6b08816f05a3144345d2091937b39d415990daca28f4" dependencies = [ "bumpalo", "cranelift-assembler-x64", @@ -626,8 +637,9 @@ dependencies = [ "cranelift-control", "cranelift-entity", "cranelift-isle", - "gimli", - "hashbrown 0.15.5", + "gimli 0.33.1", + "hashbrown 0.16.1", + "libm", "log", "pulley-interpreter", "regalloc2", @@ -635,14 +647,14 @@ dependencies = [ "serde", "smallvec", "target-lexicon", - "wasmtime-internal-math", + "wasmtime-internal-core", ] [[package]] name = "cranelift-codegen-meta" -version = "0.128.3" +version = "0.130.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3fcf1e3e6757834bd2584f4cbff023fcc198e9279dcb5d684b4bb27a9b19f54" +checksum = "e433faa87d38e5b8ff469e44a26fea4f93e58abd7a7c10bad9810056139700c9" dependencies = [ "cranelift-assembler-x64-meta", "cranelift-codegen-shared", @@ -653,35 +665,36 @@ dependencies = [ [[package]] name = "cranelift-codegen-shared" -version = "0.128.3" +version = "0.130.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "205dcb9e6ccf9d368b7466be675ff6ee54a63e36da6fe20e72d45169cf6fd254" +checksum = "5397ba61976e13944ca71230775db13ee1cb62849701ed35b753f4761ed0a9b7" [[package]] name = "cranelift-control" -version = "0.128.3" +version = "0.130.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "108eca9fcfe86026054f931eceaf57b722c1b97464bf8265323a9b5877238817" +checksum = "cc81c88765580720eb30f4fc2c1bfdb75fcbf3094f87b3cd69cecca79d77a245" dependencies = [ "arbitrary", ] [[package]] name = "cranelift-entity" -version = "0.128.3" +version = "0.130.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0d96496910065d3165f84ff8e1e393916f4c086f88ac8e1b407678bc78735aa" +checksum = "463feed5d46cf8763f3ba3045284cf706dd161496e20ec9c14afbb4ba09b9e66" dependencies = [ "cranelift-bitset", "serde", "serde_derive", + "wasmtime-internal-core", ] [[package]] name = "cranelift-frontend" -version = "0.128.3" +version = "0.130.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e303983ad7e23c850f24d9c41fc3cb346e1b930f066d3966545e4c98dac5c9fb" +checksum = "a4c5eca7696c1c04ab4c7ed8d18eadbb47d6cc9f14ec86fe0881bf1d7e97e261" dependencies = [ "cranelift-codegen", "log", @@ -691,15 +704,15 @@ dependencies = [ [[package]] name = "cranelift-isle" -version = "0.128.3" +version = "0.130.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24b0cf8d867d891245836cac7abafb0a5b0ea040a019d720702b3b8bcba40bfa" +checksum = "f1153844610cc9c6da8cf10ce205e45da1a585b7688ed558aa808bbe2e4e6d77" [[package]] name = "cranelift-native" -version = "0.128.3" +version = "0.130.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e24b641e315443e27807b69c440fe766737d7e718c68beb665a2d69259c77bf3" +checksum = "a97b583fe9a60f06b0464cee6be5a17f623fd91b217aaac99b51b339d19911af" dependencies = [ "cranelift-codegen", "libc", @@ -708,9 +721,9 @@ dependencies = [ [[package]] name = "cranelift-srcgen" -version = "0.128.3" +version = "0.130.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4e378a54e7168a689486d67ee1f818b7e5356e54ae51a1d7a53f4f13f7f8b7a" +checksum = "8594dc6bb4860fa8292f1814c76459dbfb933e1978d8222de6380efce45c7cee" [[package]] name = "crc32fast" @@ -869,12 +882,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "fallible-iterator" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" - [[package]] name = "fastrand" version = "2.3.0" @@ -1123,8 +1130,15 @@ name = "gimli" version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" + +[[package]] +name = "gimli" +version = "0.33.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19e16c5073773ccf057c282be832a59ee53ef5ff98db3aeff7f8314f52ffc196" dependencies = [ - "fallible-iterator", + "fnv", + "hashbrown 0.16.1", "indexmap", "stable_deref_trait", ] @@ -1709,9 +1723,18 @@ name = "object" version = "0.37.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + +[[package]] +name = "object" +version = "0.38.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271638cd5fa9cca89c4c304675ca658efc4e64a66c716b7cfe1afb4b9611dbbc" dependencies = [ "crc32fast", - "hashbrown 0.15.5", + "hashbrown 0.16.1", "indexmap", "memchr", ] @@ -1878,21 +1901,21 @@ dependencies = [ [[package]] name = "pulley-interpreter" -version = "41.0.3" +version = "43.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01051a5b172e07f9197b85060e6583b942aec679dac08416647bf7e7dc916b65" +checksum = "7975f0975fa2c047bf47d617bdf716689e42ee82b159bd000ead7330d7697a1b" dependencies = [ "cranelift-bitset", "log", "pulley-macros", - "wasmtime-internal-math", + "wasmtime-internal-core", ] [[package]] name = "pulley-macros" -version = "41.0.3" +version = "43.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cf194f5b1a415ef3a44ee35056f4009092cc4038a9f7e3c7c1e392f48ee7dbb" +checksum = "a210c0386ef0ddedb337ec99b91e560ae9c341415ef75958cb39ddb537bb0c84" dependencies = [ "proc-macro2", "quote", @@ -2156,9 +2179,9 @@ dependencies = [ [[package]] name = "regalloc2" -version = "0.13.5" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08effbc1fa53aaebff69521a5c05640523fab037b34a4a2c109506bc938246fa" +checksum = "952ddbfc6f9f64d006c3efd8c9851a6ba2f2b944ba94730db255d55006e0ffda" dependencies = [ "allocator-api2", "bumpalo", @@ -3110,9 +3133,9 @@ dependencies = [ [[package]] name = "wasm-compose" -version = "0.243.0" +version = "0.245.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af801b6f36459023eaec63fdbaedad2fd5a4ab7dc74ecc110a8b5d375c5775e4" +checksum = "5fd23d12cc95c451c1306db5bc63075fbebb612bb70c53b4237b1ce5bc178343" dependencies = [ "anyhow", "heck", @@ -3124,21 +3147,11 @@ dependencies = [ "serde_derive", "serde_yaml", "smallvec", - "wasm-encoder 0.243.0", - "wasmparser 0.243.0", + "wasm-encoder 0.245.1", + "wasmparser 0.245.1", "wat", ] -[[package]] -name = "wasm-encoder" -version = "0.243.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c55db9c896d70bd9fa535ce83cd4e1f2ec3726b0edd2142079f594fc3be1cb35" -dependencies = [ - "leb128fmt", - "wasmparser 0.243.0", -] - [[package]] name = "wasm-encoder" version = "0.244.0" @@ -3190,19 +3203,6 @@ dependencies = [ "wasmparser 0.245.1", ] -[[package]] -name = "wasmparser" -version = "0.243.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6d8db401b0528ec316dfbe579e6ab4152d61739cfe076706d2009127970159d" -dependencies = [ - "bitflags", - "hashbrown 0.15.5", - "indexmap", - "semver", - "serde", -] - [[package]] name = "wasmparser" version = "0.244.0" @@ -3231,23 +3231,22 @@ dependencies = [ [[package]] name = "wasmprinter" -version = "0.243.0" +version = "0.245.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb2b6035559e146114c29a909a3232928ee488d6507a1504d8934e8607b36d7b" +checksum = "5f41517a3716fbb8ccf46daa9c1325f760fcbff5168e75c7392288e410b91ac8" dependencies = [ "anyhow", "termcolor", - "wasmparser 0.243.0", + "wasmparser 0.245.1", ] [[package]] name = "wasmtime" -version = "41.0.3" +version = "43.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a19f56cece843fa95dd929f5568ff8739c7e3873b530ceea9eda2aa02a0b4142" +checksum = "54fa9f298901a64ed3eae16b130f0b30c80dbb74a9e7f129a791f4e74649b917" dependencies = [ - "addr2line", - "anyhow", + "addr2line 0.26.1", "async-trait", "bitflags", "bumpalo", @@ -3257,15 +3256,13 @@ dependencies = [ "encoding_rs", "futures", "fxprof-processed-profile", - "gimli", - "hashbrown 0.15.5", - "indexmap", + "gimli 0.33.1", "ittapi", "libc", "log", "mach2", "memfd", - "object", + "object 0.38.1", "once_cell", "postcard", "pulley-interpreter", @@ -3279,18 +3276,17 @@ dependencies = [ "target-lexicon", "tempfile", "wasm-compose", - "wasm-encoder 0.243.0", - "wasmparser 0.243.0", + "wasm-encoder 0.245.1", + "wasmparser 0.245.1", "wasmtime-environ", "wasmtime-internal-cache", "wasmtime-internal-component-macro", "wasmtime-internal-component-util", + "wasmtime-internal-core", "wasmtime-internal-cranelift", "wasmtime-internal-fiber", "wasmtime-internal-jit-debug", "wasmtime-internal-jit-icache-coherence", - "wasmtime-internal-math", - "wasmtime-internal-slab", "wasmtime-internal-unwinder", "wasmtime-internal-versioned-export-macros", "wasmtime-internal-winch", @@ -3300,36 +3296,40 @@ dependencies = [ [[package]] name = "wasmtime-environ" -version = "41.0.3" +version = "43.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bf9dff572c950258548cbbaf39033f68f8dcd0b43b22e80def9fe12d532d3e5" +checksum = "75a3aaaa3a522f443af67a7ed8d4efa20b0c3784e1031980537fbfcb497f70a7" dependencies = [ "anyhow", "cpp_demangle", + "cranelift-bforest", "cranelift-bitset", "cranelift-entity", - "gimli", + "gimli 0.33.1", + "hashbrown 0.16.1", "indexmap", "log", - "object", + "object 0.38.1", "postcard", "rustc-demangle", "semver", "serde", "serde_derive", + "sha2", "smallvec", "target-lexicon", - "wasm-encoder 0.243.0", - "wasmparser 0.243.0", + "wasm-encoder 0.245.1", + "wasmparser 0.245.1", "wasmprinter", "wasmtime-internal-component-util", + "wasmtime-internal-core", ] [[package]] name = "wasmtime-internal-cache" -version = "41.0.3" +version = "43.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f52a985f5b5dae53147fc596f3a313c334e2c24fd1ba708634e1382f6ecd727" +checksum = "0454f53d6c91d9a3b30be6d5dbd27e8ff595fddaafe69665df908fc385bbd836" dependencies = [ "base64", "directories-next", @@ -3347,9 +3347,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-component-macro" -version = "41.0.3" +version = "43.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7920dc7dcb608352f5fe93c52582e65075b7643efc5dac3fc717c1645a8d29a0" +checksum = "3e0d00d29ed90a63d2445072860a8a42d7151390157236a69bc3ae056786e9c9" dependencies = [ "anyhow", "proc-macro2", @@ -3357,20 +3357,32 @@ dependencies = [ "syn", "wasmtime-internal-component-util", "wasmtime-internal-wit-bindgen", - "wit-parser 0.243.0", + "wit-parser 0.245.1", ] [[package]] name = "wasmtime-internal-component-util" -version = "41.0.3" +version = "43.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "066f5aed35aa60580a2ac0df145c0f0d4b04319862fee1d6036693e1cca43a12" +checksum = "7acfd639ca7ab9e1cc37f053edd95bed6a7bed16370a8b2643dc7d9ef3047935" + +[[package]] +name = "wasmtime-internal-core" +version = "43.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e671917bb6856ae360cb59d7aaf26f1cfd042c7b924319dd06fd380739fc0b2e" +dependencies = [ + "anyhow", + "hashbrown 0.16.1", + "libm", + "serde", +] [[package]] name = "wasmtime-internal-cranelift" -version = "41.0.3" +version = "43.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afb8002dc415b7773d7949ee360c05ee8f91627ec25a7a0b01ee03831bdfdda1" +checksum = "f2dfd752e1dcf79eeeadc6f2681e2fb4a9f0b5534d18c5b9b93faccd0de2c80c" dependencies = [ "cfg-if", "cranelift-codegen", @@ -3378,26 +3390,26 @@ dependencies = [ "cranelift-entity", "cranelift-frontend", "cranelift-native", - "gimli", + "gimli 0.33.1", "itertools", "log", - "object", + "object 0.38.1", "pulley-interpreter", "smallvec", "target-lexicon", "thiserror 2.0.18", - "wasmparser 0.243.0", + "wasmparser 0.245.1", "wasmtime-environ", - "wasmtime-internal-math", + "wasmtime-internal-core", "wasmtime-internal-unwinder", "wasmtime-internal-versioned-export-macros", ] [[package]] name = "wasmtime-internal-fiber" -version = "41.0.3" +version = "43.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f9c562c5a272bc9f615d8f0c085a4360bafa28eef9aa5947e63d204b1129b22" +checksum = "d1e9171af643316c11d6ebe52f31f6e2a5d6d1d270de9167a7b7b6f0e3f72982" dependencies = [ "cc", "cfg-if", @@ -3410,61 +3422,46 @@ dependencies = [ [[package]] name = "wasmtime-internal-jit-debug" -version = "41.0.3" +version = "43.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db673148f26e1211db3913c12c75594be9e3858a71fa297561e9162b1a49cfb0" +checksum = "1fe23134536b9883ffc2afcffae23f7ffbcb1791e2d9fac6d6464a37ea4c8fdd" dependencies = [ "cc", - "object", + "object 0.38.1", "rustix 1.1.3", "wasmtime-internal-versioned-export-macros", ] [[package]] name = "wasmtime-internal-jit-icache-coherence" -version = "41.0.3" +version = "43.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bada5ca1cc47df7d14100e2254e187c2486b426df813cea2dd2553a7469f7674" +checksum = "9b3112806515fac8495883885eb8dbdde849988ae91fe6beb544c0d7c0f4c9aa" dependencies = [ - "anyhow", "cfg-if", "libc", + "wasmtime-internal-core", "windows-sys 0.61.2", ] -[[package]] -name = "wasmtime-internal-math" -version = "41.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf6f615d528eda9adc6eefb062135f831b5215c348f4c3ec3e143690c730605b" -dependencies = [ - "libm", -] - -[[package]] -name = "wasmtime-internal-slab" -version = "41.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da169d4f789b586e1b2612ba8399c653ed5763edf3e678884ba785bb151d018f" - [[package]] name = "wasmtime-internal-unwinder" -version = "41.0.3" +version = "43.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4888301f3393e4e8c75c938cce427293fade300fee3fc8fd466fdf3e54ae068e" +checksum = "dafc29c6e538273fda8409335137654751bdf24beab65702b7866b0a85ee108a" dependencies = [ "cfg-if", "cranelift-codegen", "log", - "object", + "object 0.38.1", "wasmtime-environ", ] [[package]] name = "wasmtime-internal-versioned-export-macros" -version = "41.0.3" +version = "43.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63ba3124cc2cbcd362672f9f077303ccc4cd61daa908f73447b7fdaece75ff9f" +checksum = "772f2b105b7fdd3dfb2cdf70c297baaeb96fe76a95cdc6fa516f713f04090c73" dependencies = [ "proc-macro2", "quote", @@ -3473,16 +3470,16 @@ dependencies = [ [[package]] name = "wasmtime-internal-winch" -version = "41.0.3" +version = "43.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90a4182515dabba776656de4ebd62efad03399e261cf937ecccb838ce8823534" +checksum = "d556c3b176aba3cce565b2bafcdc049e7410ac1d86bf1ef663a035d9ded0dddc" dependencies = [ "cranelift-codegen", - "gimli", + "gimli 0.33.1", "log", - "object", + "object 0.38.1", "target-lexicon", - "wasmparser 0.243.0", + "wasmparser 0.245.1", "wasmtime-environ", "wasmtime-internal-cranelift", "winch-codegen", @@ -3490,24 +3487,23 @@ dependencies = [ [[package]] name = "wasmtime-internal-wit-bindgen" -version = "41.0.3" +version = "43.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87acbd416227cdd279565ba49e57cf7f08d112657c3b3f39b70250acdfd094fe" +checksum = "c47507f09e68462a0ed9f351ef410584a52e01d7ec92bc588bf7fa597ce528ef" dependencies = [ "anyhow", "bitflags", "heck", "indexmap", - "wit-parser 0.243.0", + "wit-parser 0.245.1", ] [[package]] name = "wasmtime-wasi" -version = "41.0.3" +version = "43.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9a1bdb4948463ed22559a640e687fed0df50b66353144aa6a9496c041ecd927" +checksum = "cf7fc1eb83dd0d5a368c78d2bad2660f69c03e3c07ce2dd6d1e50fc2b9ff14db" dependencies = [ - "anyhow", "async-trait", "bitflags", "bytes", @@ -3534,14 +3530,14 @@ dependencies = [ [[package]] name = "wasmtime-wasi-io" -version = "41.0.3" +version = "43.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7873d8b990d3cf1105ef491abf2b3cf1e19ff6722d24d5ca662026ea082cdff" +checksum = "315fd7192148233c2c61753b5e8e2456e0ff96dd649f079148977554139ea4dc" dependencies = [ - "anyhow", "async-trait", "bytes", "futures", + "tracing", "wasmtime", ] @@ -3556,24 +3552,24 @@ dependencies = [ [[package]] name = "wast" -version = "244.0.0" +version = "245.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2e7b9f9e23311275920e3d6b56d64137c160cf8af4f84a7283b36cfecbf4acb" +checksum = "28cf1149285569120b8ce39db8b465e8a2b55c34cbb586bd977e43e2bc7300bf" dependencies = [ "bumpalo", "leb128fmt", "memchr", "unicode-width", - "wasm-encoder 0.244.0", + "wasm-encoder 0.245.1", ] [[package]] name = "wat" -version = "1.244.0" +version = "1.245.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbf35b87ed352f9ab6cd0732abde5a67dd6153dfd02c493e61459218b19456fa" +checksum = "cd48d1679b6858988cb96b154dda0ec5bbb09275b71db46057be37332d5477be" dependencies = [ - "wast 244.0.0", + "wast 245.0.1", ] [[package]] @@ -3607,37 +3603,37 @@ dependencies = [ [[package]] name = "wiggle" -version = "41.0.3" +version = "43.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f05d2a9932ca235984248dc98471ae83d1985e095682d049af4c296f54f0fb4" +checksum = "c4e79079e7f5a8c034307bb5e61b2e63bc668e17d139705a7dea5afceab02510" dependencies = [ - "anyhow", "bitflags", "thiserror 2.0.18", "tracing", "wasmtime", + "wasmtime-environ", "wiggle-macro", ] [[package]] name = "wiggle-generate" -version = "41.0.3" +version = "43.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57f773d51c1696bd7d028aa35c884d9fc58f48d79a1176dfbad6c908de314235" +checksum = "9165e5b08a6463d247b5c1292aaab16b103d0d8f5941b60d7bc0c38125eb9ffe" dependencies = [ - "anyhow", "heck", "proc-macro2", "quote", "syn", + "wasmtime-environ", "witx", ] [[package]] name = "wiggle-macro" -version = "41.0.3" +version = "43.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e976fe0cecd60041f66b15ad45ebc997952af13da9bf9d90261c7b025057edc" +checksum = "5fb0a5b9476150428eead9ce1a5c83e8fd6aac29806f48c6dbf77d50a067473a" dependencies = [ "proc-macro2", "quote", @@ -3678,22 +3674,21 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "winch-codegen" -version = "41.0.3" +version = "43.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4f31dcfdfaf9d6df9e1124d7c8ee6fc29af5b99b89d11ae731c138e0f5bd77b" +checksum = "1ca3d76763e4ddc48ede73792d067396ba5ee74c3c581db90e6638fe6b46bf52" dependencies = [ - "anyhow", "cranelift-assembler-x64", "cranelift-codegen", - "gimli", + "gimli 0.33.1", "regalloc2", "smallvec", "target-lexicon", "thiserror 2.0.18", - "wasmparser 0.243.0", + "wasmparser 0.245.1", "wasmtime-environ", + "wasmtime-internal-core", "wasmtime-internal-cranelift", - "wasmtime-internal-math", ] [[package]] @@ -4062,24 +4057,6 @@ name = "wit-dylib-ffi" version = "0.1.0" source = "git+https://github.com/dicej/wasm-tools?rev=b072b0ca#b072b0caa8307779558d96a62bc9522abda6a7fb" -[[package]] -name = "wit-parser" -version = "0.243.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df983a8608e513d8997f435bb74207bf0933d0e49ca97aa9d8a6157164b9b7fc" -dependencies = [ - "anyhow", - "id-arena", - "indexmap", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser 0.243.0", -] - [[package]] name = "wit-parser" version = "0.244.0" diff --git a/Cargo.toml b/Cargo.toml index 5089b7b..ebdb7d1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "componentize-py" -version = "0.21.0" +version = "0.22.0" edition = "2024" exclude = ["cpython"] @@ -26,8 +26,8 @@ pyo3 = { version = "0.26.0", features = [ "abi3-py39", "extension-module", ], optional = true } -wasmtime = "41.0.3" -wasmtime-wasi = { version = "41.0.3", features = [ "p3" ] } +wasmtime = "43.0.0" +wasmtime-wasi = { version = "43.0.0", features = [ "p3" ] } once_cell = "1.20.2" component-init-transform = { git = "https://github.com/dicej/component-init", rev = "1de5906c" } async-trait = "0.1.83" diff --git a/bundled/poll_loop.py b/bundled/poll_loop.py index c067a0f..504c958 100644 --- a/bundled/poll_loop.py +++ b/bundled/poll_loop.py @@ -2,9 +2,8 @@ This also includes helper classes and functions for working with `wasi:http`. -As of WASI Preview 2, there is not yet a standard for first-class, composable -asynchronous functions and streams. We expect that little or none of this -boilerplate will be needed once those features arrive in Preview 3. +This is only useful for `wasi:http@0.2.x`; `wasi:http@0.3.x` uses a different +mechanism to model concurrency. """ import asyncio diff --git a/examples/cli-p3/README.md b/examples/cli-p3/README.md index c53587d..ff1c148 100644 --- a/examples/cli-p3/README.md +++ b/examples/cli-p3/README.md @@ -1,31 +1,31 @@ # Example: `cli-p3` This is an example of how to use [componentize-py] and [Wasmtime] to build and -run a Python-based component targetting version `0.3.0-rc-2026-01-06` of the +run a Python-based component targetting version `0.3.0-rc-2026-03-15` of the [wasi-cli] `command` world. [componentize-py]: https://github.com/bytecodealliance/componentize-py [Wasmtime]: https://github.com/bytecodealliance/wasmtime -[wasi-cli]: https://github.com/WebAssembly/WASI/tree/v0.3.0-rc-2026-01-06/proposals/cli/wit-0.3.0-draft +[wasi-cli]: https://github.com/WebAssembly/WASI/tree/v0.3.0-rc-2026-03-15/proposals/cli/wit-0.3.0-draft ## Prerequisites -* `Wasmtime` 41.0.3 -* `componentize-py` 0.21.0 +* `Wasmtime` 43.0.0 +* `componentize-py` 0.22.0 Below, we use [Rust](https://rustup.rs/)'s `cargo` to install `Wasmtime`. If you don't have `cargo`, you can download and install from -https://github.com/bytecodealliance/wasmtime/releases/tag/v41.0.3. +https://github.com/bytecodealliance/wasmtime/releases/tag/v43.0.0. ``` -cargo install --version 41.0.3 wasmtime-cli -pip install componentize-py==0.21.0 +cargo install --version 43.0.0 wasmtime-cli +pip install componentize-py==0.22.0 ``` ## Running the demo ``` -componentize-py -d ../../wit -w wasi:cli/command@0.3.0-rc-2026-01-06 componentize app -o cli.wasm +componentize-py -d ../../wit -w wasi:cli/command@0.3.0-rc-2026-03-15 componentize app -o cli.wasm wasmtime run -Sp3 -Wcomponent-model-async cli.wasm ``` diff --git a/examples/cli/README.md b/examples/cli/README.md index 8ad7938..3ffc1e0 100644 --- a/examples/cli/README.md +++ b/examples/cli/README.md @@ -10,7 +10,7 @@ run a Python-based component targetting the [wasi-cli] `command` world. ## Prerequisites * `Wasmtime` 38.0.0 or later -* `componentize-py` 0.21.0 +* `componentize-py` 0.22.0 Below, we use [Rust](https://rustup.rs/)'s `cargo` to install `Wasmtime`. If you don't have `cargo`, you can download and install from @@ -18,7 +18,7 @@ https://github.com/bytecodealliance/wasmtime/releases/tag/v38.0.0. ``` cargo install --version 38.0.0 wasmtime-cli -pip install componentize-py==0.21.0 +pip install componentize-py==0.22.0 ``` ## Running the demo diff --git a/examples/http-p3/README.md b/examples/http-p3/README.md index 6df34f0..dfafb2e 100644 --- a/examples/http-p3/README.md +++ b/examples/http-p3/README.md @@ -1,7 +1,7 @@ # Example: `http-p3` This is an example of how to use [componentize-py] and [Wasmtime] to build and -run a Python-based component targetting version `0.3.0-rc-2026-01-06` of the +run a Python-based component targetting version `0.3.0-rc-2026-03-15` of the [wasi-http] `service` world. [componentize-py]: https://github.com/bytecodealliance/componentize-py @@ -10,16 +10,16 @@ run a Python-based component targetting version `0.3.0-rc-2026-01-06` of the ## Prerequisites -* `Wasmtime` 41.0.3 -* `componentize-py` 0.21.0 +* `Wasmtime` 43.0.0 +* `componentize-py` 0.22.0 Below, we use [Rust](https://rustup.rs/)'s `cargo` to install `Wasmtime`. If you don't have `cargo`, you can download and install from -https://github.com/bytecodealliance/wasmtime/releases/tag/v41.0.3. +https://github.com/bytecodealliance/wasmtime/releases/tag/v43.0.0. ``` -cargo install --version 41.0.3 wasmtime-cli -pip install componentize-py==0.21.0 +cargo install --version 43.0.0 wasmtime-cli +pip install componentize-py==0.22.0 ``` ## Running the demo @@ -27,7 +27,7 @@ pip install componentize-py==0.21.0 First, build the app and run it: ``` -componentize-py -d ../../wit -w wasi:http/service@0.3.0-rc-2026-01-06 componentize app -o http.wasm +componentize-py -d ../../wit -w wasi:http/service@0.3.0-rc-2026-03-15 componentize app -o http.wasm wasmtime serve -Sp3,common -Wcomponent-model-async http.wasm ``` diff --git a/examples/http/README.md b/examples/http/README.md index 9142b81..0f7ae37 100644 --- a/examples/http/README.md +++ b/examples/http/README.md @@ -10,7 +10,7 @@ run a Python-based component targetting the [wasi-http] `proxy` world. ## Prerequisites * `Wasmtime` 38.0.0 or later -* `componentize-py` 0.21.0 +* `componentize-py` 0.22.0 Below, we use [Rust](https://rustup.rs/)'s `cargo` to install `Wasmtime`. If you don't have `cargo`, you can download and install from @@ -18,7 +18,7 @@ https://github.com/bytecodealliance/wasmtime/releases/tag/v38.0.0. ``` cargo install --version 38.0.0 wasmtime-cli -pip install componentize-py==0.21.0 +pip install componentize-py==0.22.0 ``` ## Running the demo diff --git a/examples/matrix-math/README.md b/examples/matrix-math/README.md index 679974b..a015808 100644 --- a/examples/matrix-math/README.md +++ b/examples/matrix-math/README.md @@ -11,7 +11,7 @@ within a guest component. ## Prerequisites * `wasmtime` 38.0.0 or later -* `componentize-py` 0.21.0 +* `componentize-py` 0.22.0 * `NumPy`, built for WASI Note that we use an unofficial build of NumPy since the upstream project does @@ -23,7 +23,7 @@ https://github.com/bytecodealliance/wasmtime/releases/tag/v38.0.0. ``` cargo install --version 38.0.0 wasmtime-cli -pip install componentize-py==0.21.0 +pip install componentize-py==0.22.0 curl -OL https://github.com/dicej/wasi-wheels/releases/download/v0.0.2/numpy-wasi.tar.gz tar xf numpy-wasi.tar.gz ``` diff --git a/examples/sandbox/README.md b/examples/sandbox/README.md index 997b267..d1fcbd0 100644 --- a/examples/sandbox/README.md +++ b/examples/sandbox/README.md @@ -12,10 +12,10 @@ versions have a different API for working with components, and this example has not yet been updated to use it. * `wasmtime-py` 38.0.0 -* `componentize-py` 0.21.0 +* `componentize-py` 0.22.0 ``` -pip install componentize-py==0.21.0 wasmtime==38.0.0 +pip install componentize-py==0.22.0 wasmtime==38.0.0 ``` ## Running the demo diff --git a/examples/tcp-p3/README.md b/examples/tcp-p3/README.md index 3e744b3..ad72799 100644 --- a/examples/tcp-p3/README.md +++ b/examples/tcp-p3/README.md @@ -1,26 +1,26 @@ # Example: `tcp-p3` This is an example of how to use [componentize-py] and [Wasmtime] to build and -run a Python-based component targetting version `0.3.0-rc-2026-01-06` of the +run a Python-based component targetting version `0.3.0-rc-2026-03-15` of the [wasi-cli] `command` world and making an outbound TCP request using [wasi-sockets]. [componentize-py]: https://github.com/bytecodealliance/componentize-py [Wasmtime]: https://github.com/bytecodealliance/wasmtime -[wasi-cli]: https://github.com/WebAssembly/WASI/tree/v0.3.0-rc-2026-01-06/proposals/cli/wit-0.3.0-draft -[wasi-sockets]: https://github.com/WebAssembly/WASI/tree/v0.3.0-rc-2026-01-06/proposals/sockets/wit-0.3.0-draft +[wasi-cli]: https://github.com/WebAssembly/WASI/tree/v0.3.0-rc-2026-03-15/proposals/cli/wit-0.3.0-draft +[wasi-sockets]: https://github.com/WebAssembly/WASI/tree/v0.3.0-rc-2026-03-15/proposals/sockets/wit-0.3.0-draft ## Prerequisites -* `Wasmtime` 41.0.3 -* `componentize-py` 0.21.0 +* `Wasmtime` 43.0.0 +* `componentize-py` 0.22.0 Below, we use [Rust](https://rustup.rs/)'s `cargo` to install `Wasmtime`. If you don't have `cargo`, you can download and install from -https://github.com/bytecodealliance/wasmtime/releases/tag/v41.0.3. +https://github.com/bytecodealliance/wasmtime/releases/tag/v43.0.0. ``` -cargo install --version 41.0.3 wasmtime-cli -pip install componentize-py==0.21.0 +cargo install --version 43.0.0 wasmtime-cli +pip install componentize-py==0.22.0 ``` ## Running the demo @@ -35,7 +35,7 @@ nc -l 127.0.0.1 3456 Now, build and run the example, using the same port you gave to `netcat`. ``` -componentize-py -d ../../wit -w wasi:cli/command@0.3.0-rc-2026-01-06 componentize app -o tcp.wasm +componentize-py -d ../../wit -w wasi:cli/command@0.3.0-rc-2026-03-15 componentize app -o tcp.wasm wasmtime run -Sp3 -Sinherit-network -Wcomponent-model-async tcp.wasm 127.0.0.1:3456 ``` diff --git a/examples/tcp-p3/app.py b/examples/tcp-p3/app.py index 35410eb..4b3a308 100644 --- a/examples/tcp-p3/app.py +++ b/examples/tcp-p3/app.py @@ -77,4 +77,4 @@ async def read() -> None: data = await recv_rx.read(1024) print(f"received: {str(data)}") send_tx.__exit__(None, None, None) - await asyncio.gather(recv_fut.read(), read(), sock.send(send_rx), write()) + await asyncio.gather(recv_fut.read(), read(), sock.send(send_rx).read(), write()) diff --git a/examples/tcp/README.md b/examples/tcp/README.md index 9cd0f6c..d1dad14 100644 --- a/examples/tcp/README.md +++ b/examples/tcp/README.md @@ -11,7 +11,7 @@ making an outbound TCP request using `wasi-sockets`. ## Prerequisites * `Wasmtime` 38.0.0 or later -* `componentize-py` 0.21.0 +* `componentize-py` 0.22.0 Below, we use [Rust](https://rustup.rs/)'s `cargo` to install `Wasmtime`. If you don't have `cargo`, you can download and install from @@ -19,7 +19,7 @@ https://github.com/bytecodealliance/wasmtime/releases/tag/v38.0.0. ``` cargo install --version 38.0.0 wasmtime-cli -pip install componentize-py==0.21.0 +pip install componentize-py==0.22.0 ``` ## Running the demo diff --git a/pyproject.toml b/pyproject.toml index 4e3c4b5..8cdb5c0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ features = ["pyo3/extension-module"] [project] name = "componentize-py" -version = "0.21.0" +version = "0.22.0" description = "Tool to package Python applications as WebAssembly components" readme = "README.md" license = { file = "LICENSE" } diff --git a/src/command.rs b/src/command.rs index 980f3ff..e519cab 100644 --- a/src/command.rs +++ b/src/command.rs @@ -1,4 +1,5 @@ use { + crate::{BindingsGenerator, ComponentGenerator}, anyhow::{Context, Result}, clap::Parser as _, std::{ @@ -31,9 +32,12 @@ pub struct Common { #[arg(short = 'd', long)] 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. #[arg(short = 'w', long)] - pub world: Option, + pub world: Vec, /// Disable non-error output #[arg(short = 'q', long)] @@ -79,6 +83,16 @@ pub struct Common { /// If this is not specified, the module name will default to "wit_world". #[arg(long)] pub world_module: Option, + + /// When generating Python module names, include the WIT package name and + /// version even if only one version of that package is referenced by the + /// specified world or only one package uses that name. + /// + /// By default, the package name and version will only be included in the + /// name if the world references more than one version of the WIT package or + /// the name is used by more than one package. + #[arg(long)] + pub full_names: bool, } #[derive(clap::Subcommand, Debug)] @@ -168,24 +182,34 @@ pub fn run + Clone, I: IntoIterator>(args: I) -> Res } fn generate_bindings(common: Common, bindings: Bindings) -> Result<()> { - crate::generate_bindings( - &common.wit_path, - common.world.as_deref(), - &common.features, - common.all_features, - common.world_module.as_deref(), - &bindings.output_dir, - &common + BindingsGenerator { + wit_path: &common + .wit_path + .iter() + .map(|v| v.as_path()) + .collect::>(), + worlds: &common.world.iter().map(|v| v.as_str()).collect::>(), + features: &common + .features + .iter() + .map(|v| v.as_str()) + .collect::>(), + all_features: common.all_features, + world_module: common.world_module.as_deref(), + output_dir: &bindings.output_dir, + import_interface_names: &common .import_interface_name .iter() .map(|(a, b)| (a.as_str(), b.as_str())) .collect(), - &common + export_interface_names: &common .export_interface_name .iter() .map(|(a, b)| (a.as_str(), b.as_str())) .collect(), - ) + full_names: common.full_names, + } + .generate() } fn componentize(common: Common, componentize: Componentize) -> Result<()> { @@ -200,33 +224,48 @@ fn componentize(common: Common, componentize: Componentize) -> Result<()> { ); } - Runtime::new()?.block_on(crate::componentize( - &common.wit_path, - common.world.as_deref(), - &common.features, - common.all_features, - common.world_module.as_deref(), - &python_path.iter().map(|s| s.as_str()).collect::>(), - &componentize - .module_worlds - .iter() - .map(|(k, v)| (k.as_str(), v.as_str())) - .collect::>(), - &componentize.app_name, - &componentize.output, - None, - componentize.stub_wasi, - &common - .import_interface_name - .iter() - .map(|(a, b)| (a.as_str(), b.as_str())) - .collect(), - &common - .export_interface_name - .iter() - .map(|(a, b)| (a.as_str(), b.as_str())) - .collect(), - ))?; + Runtime::new()?.block_on( + ComponentGenerator { + wit_path: &common + .wit_path + .iter() + .map(|v| v.as_path()) + .collect::>(), + worlds: &common.world.iter().map(|v| v.as_str()).collect::>(), + features: &common + .features + .iter() + .map(|v| v.as_str()) + .collect::>(), + all_features: common.all_features, + world_module: common.world_module.as_deref(), + python_path: &python_path.iter().map(|s| s.as_str()).collect::>(), + module_worlds: &componentize + .module_worlds + .iter() + .map(|(k, v)| (k.as_str(), [v.as_str()])) + .collect::>() + .iter() + .map(|(k, v)| (*k, v as &[_])) + .collect::>(), + app_name: &componentize.app_name, + output_path: &componentize.output, + add_to_linker: None, + stub_wasi: componentize.stub_wasi, + import_interface_names: &common + .import_interface_name + .iter() + .map(|(a, b)| (a.as_str(), b.as_str())) + .collect(), + export_interface_names: &common + .export_interface_name + .iter() + .map(|(a, b)| (a.as_str(), b.as_str())) + .collect(), + full_names: common.full_names, + } + .generate(), + )?; if !common.quiet { println!("Component built successfully"); @@ -355,13 +394,14 @@ mod tests { // When generating the bindings for this WIT world let common = Common { wit_path: vec![wit.path().into()], - world: None, + world: Vec::new(), world_module: Some("bindings".into()), quiet: false, features: vec![], all_features: false, import_interface_name: Vec::new(), export_interface_name: Vec::new(), + full_names: false, }; let bindings = Bindings { output_dir: out_dir.path().into(), @@ -385,13 +425,14 @@ mod tests { // When generating the bindings for this WIT world let common = Common { wit_path: vec![wit.path().into()], - world: None, + world: Vec::new(), world_module: Some("bindings".into()), quiet: false, features: vec!["x".to_owned()], all_features: false, import_interface_name: Vec::new(), export_interface_name: Vec::new(), + full_names: false, }; let bindings = Bindings { output_dir: out_dir.path().into(), @@ -415,13 +456,14 @@ mod tests { // When generating the bindings for this WIT world let common = Common { wit_path: vec![wit.path().into()], - world: None, + world: Vec::new(), world_module: Some("bindings".into()), quiet: false, features: vec![], all_features: true, import_interface_name: Vec::new(), export_interface_name: Vec::new(), + full_names: false, }; let bindings = Bindings { output_dir: out_dir.path().into(), @@ -444,13 +486,14 @@ mod tests { let out_dir = tempfile::tempdir()?; let common = Common { wit_path: vec![wit.path().into()], - world: None, + world: Vec::new(), world_module: Some("bindings".into()), quiet: false, features: vec!["x".to_owned()], all_features: false, import_interface_name: Vec::new(), export_interface_name: Vec::new(), + full_names: false, }; let bindings = Bindings { output_dir: out_dir.path().into(), diff --git a/src/lib.rs b/src/lib.rs index ba72b0d..a479057 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,7 +6,6 @@ use { bytes::Bytes, component_init_transform::Invoker, futures::future::FutureExt, - heck::ToSnakeCase, indexmap::{IndexMap, IndexSet}, serde::Deserialize, std::{ @@ -19,7 +18,7 @@ use { path::{Path, PathBuf}, str, }, - summary::{Escape, Locations, Summary}, + summary::{Locations, Summary}, tar::Archive, wasm_encoder::{CustomSection, Section as _}, wasmtime::{ @@ -33,8 +32,8 @@ use { wit_component::metadata, wit_dylib::DylibOpts, wit_parser::{ - CloneMaps, FunctionKind, Package, PackageName, Resolve, Stability, TypeDefKind, - UnresolvedPackageGroup, World, WorldId, WorldItem, WorldKey, + CloneMaps, FunctionKind, Package, PackageName, Resolve, Stability, TypeDefKind, World, + WorldId, WorldItem, WorldKey, }, zstd::Decoder, }; @@ -91,6 +90,8 @@ struct RawComponentizePyConfig { import_interface_names: HashMap, #[serde(default)] export_interface_names: HashMap, + #[serde(default)] + full_names: bool, } #[derive(Debug)] @@ -99,6 +100,7 @@ struct ComponentizePyConfig { wit_directory: Option, import_interface_names: HashMap, export_interface_names: HashMap, + full_names: bool, } impl TryFrom<(&Path, RawComponentizePyConfig)> for ComponentizePyConfig { @@ -119,6 +121,7 @@ impl TryFrom<(&Path, RawComponentizePyConfig)> for ComponentizePyConfig { wit_directory: raw.wit_directory.map(convert).transpose()?, import_interface_names: raw.import_interface_names, export_interface_names: raw.export_interface_names, + full_names: raw.full_names, }) } } @@ -142,588 +145,610 @@ impl Invoker for MyInvoker { let func = self .instance .get_typed_func::<(), (i32,)>(&mut self.store, function)?; - let result = func.call_async(&mut self.store, ()).await?.0; - func.post_return_async(&mut self.store).await?; - Ok(result) + Ok(func.call_async(&mut self.store, ()).await?.0) } async fn call_s64(&mut self, function: &str) -> Result { let func = self .instance .get_typed_func::<(), (i64,)>(&mut self.store, function)?; - let result = func.call_async(&mut self.store, ()).await?.0; - func.post_return_async(&mut self.store).await?; - Ok(result) + Ok(func.call_async(&mut self.store, ()).await?.0) } async fn call_f32(&mut self, function: &str) -> Result { let func = self .instance .get_typed_func::<(), (f32,)>(&mut self.store, function)?; - let result = func.call_async(&mut self.store, ()).await?.0; - func.post_return_async(&mut self.store).await?; - Ok(result) + Ok(func.call_async(&mut self.store, ()).await?.0) } async fn call_f64(&mut self, function: &str) -> Result { let func = self .instance .get_typed_func::<(), (f64,)>(&mut self.store, function)?; - let result = func.call_async(&mut self.store, ()).await?.0; - func.post_return_async(&mut self.store).await?; - Ok(result) + Ok(func.call_async(&mut self.store, ()).await?.0) } async fn call_list_u8(&mut self, function: &str) -> Result> { let func = self .instance .get_typed_func::<(), (Vec,)>(&mut self.store, function)?; - let result = func.call_async(&mut self.store, ()).await?.0; - func.post_return_async(&mut self.store).await?; - Ok(result) + Ok(func.call_async(&mut self.store, ()).await?.0) } } -#[allow(clippy::too_many_arguments)] -pub fn generate_bindings( - wit_path: &[impl AsRef], - world: Option<&str>, - features: &[String], - all_features: bool, - world_module: Option<&str>, - output_dir: &Path, - import_interface_names: &HashMap<&str, &str>, - export_interface_names: &HashMap<&str, &str>, -) -> Result<()> { - // TODO: Split out and reuse the code responsible for finding and using - // componentize-py.toml files in the `componentize` function below, since - // that can affect the bindings we should be generating. - - let (resolve, world) = parse_wit(wit_path, world, features, all_features)?; - let import_function_indexes = &HashMap::new(); - let export_function_indexes = &HashMap::new(); - let stream_and_future_indexes = &HashMap::new(); - let summary = Summary::try_new( - &resolve, - &iter::once(world).collect(), - import_interface_names, - export_interface_names, - import_function_indexes, - export_function_indexes, - stream_and_future_indexes, - )?; - let world_module = world_module.unwrap_or(DEFAULT_WORLD_MODULE); - let world_dir = output_dir.join(world_module.replace('.', "/")); - fs::create_dir_all(&world_dir)?; - summary.generate_code( - &world_dir, - world, - world_module, - &mut Locations::default(), - true, - )?; - - Archive::new(Decoder::new(Cursor::new(include_bytes!(concat!( - env!("OUT_DIR"), - "/bundled.tar.zst" - ))))?) - .unpack(output_dir) - .unwrap(); - - Ok(()) +pub struct BindingsGenerator<'a> { + pub wit_path: &'a [&'a Path], + pub worlds: &'a [&'a str], + pub features: &'a [&'a str], + pub all_features: bool, + pub world_module: Option<&'a str>, + pub output_dir: &'a Path, + pub import_interface_names: &'a HashMap<&'a str, &'a str>, + pub export_interface_names: &'a HashMap<&'a str, &'a str>, + pub full_names: bool, } -#[allow(clippy::type_complexity, clippy::too_many_arguments)] -pub async fn componentize( - wit_path: &[impl AsRef], - world: Option<&str>, - features: &[String], - all_features: bool, - world_module: Option<&str>, - python_path: &[&str], - module_worlds: &[(&str, &str)], - app_name: &str, - output_path: &Path, - add_to_linker: Option<&dyn Fn(&mut Linker) -> Result<()>>, - stub_wasi: bool, - import_interface_names: &HashMap<&str, &str>, - export_interface_names: &HashMap<&str, &str>, -) -> Result<()> { - // Remove non-existent elements from `python_path` so we don't choke on them - // later: - let python_path = &python_path - .iter() - .filter_map(|&s| Path::new(s).exists().then_some(s)) - .collect::>(); - - let embedded_python_standard_lib = prelink::embedded_python_standard_library()?; - let embedded_helper_utils = prelink::embedded_helper_utils()?; - - let (configs, libraries) = - prelink::search_for_libraries_and_configs(python_path, module_worlds, world)?; - - // Next, iterate over all the WIT directories, merging them into a single - // `Resolve`, and matching Python packages to `WorldId`s. - let (mut resolve, mut main_world) = match wit_path { - [] => (None, None), - paths => { - let (resolve, world) = parse_wit(paths, world, features, all_features)?; - (Some(resolve), Some(world)) - } - }; +impl BindingsGenerator<'_> { + pub fn generate(&self) -> Result<()> { + // TODO: Split out and reuse the code responsible for finding and using + // componentize-py.toml files in the `componentize` function below, since + // that can affect the bindings we should be generating. + + let (resolve, world) = parse_wit( + self.wit_path, + self.worlds, + self.features, + self.all_features, + "union", + )?; + let import_function_indexes = &HashMap::new(); + let export_function_indexes = &HashMap::new(); + let stream_and_future_indexes = &HashMap::new(); + let summary = Summary::try_new( + &resolve, + &iter::once(world).collect(), + self.import_interface_names, + self.export_interface_names, + import_function_indexes, + export_function_indexes, + stream_and_future_indexes, + self.full_names, + )?; + let world_module = self.world_module.unwrap_or(DEFAULT_WORLD_MODULE); + let world_dir = self.output_dir.join(world_module.replace('.', "/")); + fs::create_dir_all(&world_dir)?; + summary.generate_code( + &world_dir, + world, + world_module, + &mut Locations::default(), + true, + )?; - let import_interface_names = import_interface_names - .iter() - .map(|(a, b)| (*a, *b)) - .chain(configs.iter().flat_map(|(_, (config, _))| { - config - .config - .import_interface_names - .iter() - .map(|(a, b)| (a.as_str(), b.as_str())) - })) - .collect(); + Archive::new(Decoder::new(Cursor::new(include_bytes!(concat!( + env!("OUT_DIR"), + "/bundled.tar.zst" + ))))?) + .unpack(self.output_dir) + .unwrap(); - let export_interface_names = export_interface_names - .iter() - .map(|(a, b)| (*a, *b)) - .chain(configs.iter().flat_map(|(_, (config, _))| { - config - .config - .export_interface_names - .iter() - .map(|(a, b)| (a.as_str(), b.as_str())) - })) - .collect(); + Ok(()) + } +} - let configs = configs - .iter() - .map(|(module, (config, world))| { - Ok((module, match (world, config.config.wit_directory.as_deref()) { - (_, Some(wit_path)) => { - let paths = &[config.path.join(wit_path)]; - let (my_resolve, mut world) = parse_wit(paths, *world, features, all_features)?; - - if let Some(resolve) = &mut resolve { - let remap = resolve.merge(my_resolve)?; - world = remap.worlds[world.index()].expect("missing world"); - } else { - resolve = Some(my_resolve); - } +pub type AddToLinker<'a> = Option<&'a dyn Fn(&mut Linker) -> Result<()>>; + +pub struct ComponentGenerator<'a> { + pub wit_path: &'a [&'a Path], + pub worlds: &'a [&'a str], + pub features: &'a [&'a str], + pub all_features: bool, + pub world_module: Option<&'a str>, + pub python_path: &'a [&'a str], + pub module_worlds: &'a [(&'a str, &'a [&'a str])], + pub app_name: &'a str, + pub output_path: &'a Path, + pub add_to_linker: AddToLinker<'a>, + pub stub_wasi: bool, + pub import_interface_names: &'a HashMap<&'a str, &'a str>, + pub export_interface_names: &'a HashMap<&'a str, &'a str>, + pub full_names: bool, +} - (config, Some(world)) - } - (None, None) => (config, None), - (Some(_), None) => { - bail!("no `wit-directory` specified in `componentize-py.toml` for module `{module}`"); - } - })) - }) - .collect::>>()?; +impl ComponentGenerator<'_> { + pub async fn generate(&self) -> Result<()> { + // Remove non-existent elements from `python_path` so we don't choke on them + // later: + let python_path = &self + .python_path + .iter() + .filter_map(|&s| Path::new(s).exists().then_some(s)) + .collect::>(); - let mut resolve = if let Some(resolve) = resolve { - resolve - } else { - // If no WIT directory was provided as a parameter and none were - // referenced by Python packages, use the default values. - let paths: &[&Path] = &[]; - let (my_resolve, world) = parse_wit(paths, world, features, all_features).context( - "no WIT files found; please specify the directory or file \ - containing the WIT world you wish to target", + let embedded_python_standard_lib = prelink::embedded_python_standard_library()?; + let embedded_helper_utils = prelink::embedded_helper_utils()?; + + let (configs, libraries) = prelink::search_for_libraries_and_configs( + python_path, + self.module_worlds, + self.worlds, )?; - main_world = Some(world); - my_resolve - }; - // Extract relevant metadata from the `Resolve` into a `Summary` instance, - // which we'll use to generate Wasm- and Python-level bindings. + let mut union_number = 0; + let mut next_union_name = move || { + let name = format!("union{union_number}"); + union_number += 1; + name + }; - let worlds = configs - .values() - .filter_map(|(_, world)| *world) - .chain(main_world) - .collect::>(); + // Next, iterate over all the WIT directories, merging them into a single + // `Resolve`, and matching Python packages to `WorldId`s. + let (mut resolve, mut main_world) = match self.wit_path { + [] => (None, None), + paths => { + let (resolve, world) = parse_wit( + paths, + self.worlds, + self.features, + self.all_features, + &next_union_name(), + )?; + (Some(resolve), Some(world)) + } + }; - if worlds - .iter() - .any(|&id| app_name == resolve.worlds[id].name.to_snake_case().escape()) - { - bail!( - "App name `{app_name}` conflicts with world name; please rename your application module." - ); - } + let import_interface_names = self + .import_interface_names + .iter() + .map(|(a, b)| (*a, *b)) + .chain(configs.iter().flat_map(|(_, (config, _))| { + config + .config + .import_interface_names + .iter() + .map(|(a, b)| (a.as_str(), b.as_str())) + })) + .collect(); - let union_package = resolve.packages.alloc(Package { - name: PackageName { - namespace: "componentize-py".into(), - name: "union".into(), - version: None, - }, - docs: Default::default(), - interfaces: Default::default(), - worlds: Default::default(), - }); + let export_interface_names = self + .export_interface_names + .iter() + .map(|(a, b)| (*a, *b)) + .chain(configs.iter().flat_map(|(_, (config, _))| { + config + .config + .export_interface_names + .iter() + .map(|(a, b)| (a.as_str(), b.as_str())) + })) + .collect(); - 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(), - }); + let configs = configs + .iter() + .map(|(module, (config, worlds))| { + Ok(( + module, + match (worlds, config.config.wit_directory.as_deref()) { + (_, Some(wit_path)) => { + let path = config.path.join(wit_path); + let paths = &[path.as_path()]; + let (my_resolve, mut world) = parse_wit( + paths, + worlds, + self.features, + self.all_features, + &next_union_name(), + )?; + + if let Some(resolve) = &mut resolve { + let remap = resolve.merge(my_resolve)?; + world = remap.worlds[world.index()].expect("missing world"); + } else { + resolve = Some(my_resolve); + } - resolve.packages[union_package] - .worlds - .insert("union".into(), union_world); + (config, Some(world)) + } + ([], None) => (config, None), + (_, None) => { + bail!( + "no `wit-directory` specified in \ + `componentize-py.toml` for module `{module}`" + ); + } + }, + )) + }) + .collect::>>()?; - let mut clone_maps = CloneMaps::default(); - for &world in &worlds { - resolve.merge_worlds(world, union_world, &mut clone_maps)?; - } + let mut resolve = if let Some(resolve) = resolve { + resolve + } else { + // If no WIT directory was provided as a parameter and none were + // referenced by Python packages, use the default values. + let paths: &[&Path] = &[]; + let (my_resolve, world) = parse_wit( + paths, + self.worlds, + self.features, + self.all_features, + &next_union_name(), + ) + .context( + "no WIT files found; please specify the directory or file \ + containing the WIT world you wish to target", + )?; + main_world = Some(world); + my_resolve + }; + + // Extract relevant metadata from the `Resolve` into a `Summary` instance, + // which we'll use to generate Wasm- and Python-level bindings. + + let worlds = configs + .values() + .filter_map(|(_, world)| *world) + .chain(main_world) + .collect::>(); + + let mut clone_maps = CloneMaps::default(); + let union_world = union_world( + &mut resolve, + &next_union_name(), + &worlds.iter().copied().collect::>(), + &mut clone_maps, + )?; - let (mut bindings, metadata) = wit_dylib::create_with_metadata( - &resolve, - union_world, - Some(&mut DylibOpts { - interpreter: Some("libcomponentize_py_runtime.so".into()), - async_: Default::default(), - }), - ); - - CustomSection { - name: Cow::Borrowed("component-type:componentize-py-union"), - data: Cow::Owned(metadata::encode( + let (mut bindings, metadata) = wit_dylib::create_with_metadata( &resolve, union_world, - wit_component::StringEncoding::UTF8, - None, - )?), - } - .append_to(&mut bindings); - - let imported_function_indexes = metadata - .import_funcs - .iter() - .enumerate() - .map(|(index, func)| ((func.interface.as_deref(), func.name.as_str()), index)) - .collect(); - - let exported_function_indexes = metadata - .export_funcs - .iter() - .enumerate() - .map(|(index, func)| ((func.interface.as_deref(), func.name.as_str()), index)) - .collect(); - - let mut reverse_cloned_types = HashMap::new(); - for (&original, &clone) in clone_maps.types() { - assert!(reverse_cloned_types.insert(clone, original).is_none()); - } + Some(&mut DylibOpts { + interpreter: Some("libcomponentize_py_runtime.so".into()), + async_: Default::default(), + }), + ); - let original = |ty| { - if let Some(&original) = reverse_cloned_types.get(&ty) { - original - } else { - ty + CustomSection { + name: Cow::Borrowed("component-type:componentize-py-union"), + data: Cow::Owned(metadata::encode( + &resolve, + union_world, + wit_component::StringEncoding::UTF8, + None, + )?), } - }; + .append_to(&mut bindings); - let stream_and_future_indexes = metadata - .streams - .iter() - .enumerate() - .map(|(index, stream)| (original(stream.id), index)) - .chain( - metadata - .futures - .iter() - .enumerate() - .map(|(index, future)| (original(future.id), index)), - ) - .collect(); - - let summary = Summary::try_new( - &resolve, - &worlds, - &import_interface_names, - &export_interface_names, - &imported_function_indexes, - &exported_function_indexes, - &stream_and_future_indexes, - )?; - - let need_async = summary.need_async(); - - // Now that we know whether to use the sync or async version of - // `libcomponentize_py_runtime.so`, update `libraries` accordingly. - // - // Note that we have two separate versions because older runtimes don't - // understand the new async ABI, so we only use the async version if it's - // actually needed. - let mut libraries = libraries - .into_iter() - .filter_map(|library| match (need_async, library.name.as_str()) { - (true, "libcomponentize_py_runtime_sync.so") - | (false, "libcomponentize_py_runtime_async.so") => None, - (true, "libcomponentize_py_runtime_async.so") - | (false, "libcomponentize_py_runtime_sync.so") => Some(Library { - name: "libcomponentize_py_runtime.so".into(), - ..library - }), - _ => Some(library), - }) - .collect::>(); + let imported_function_indexes = metadata + .import_funcs + .iter() + .enumerate() + .map(|(index, func)| ((func.interface.as_deref(), func.name.as_str()), index)) + .collect(); - libraries.push(Library { - name: "libcomponentize_py_bindings.so".into(), - module: bindings, - dl_openable: false, - }); + let exported_function_indexes = metadata + .export_funcs + .iter() + .enumerate() + .map(|(index, func)| ((func.interface.as_deref(), func.name.as_str()), index)) + .collect(); - let component = link::link_libraries(&libraries)?; + let mut reverse_cloned_types = HashMap::new(); + for (&original, &clone) in clone_maps.types() { + assert!(reverse_cloned_types.insert(clone, original).is_none()); + } - let stubbed_component = if stub_wasi { - stubwasi::link_stub_modules(libraries)? - } else { - None - }; + let original = |ty| { + if let Some(&original) = reverse_cloned_types.get(&ty) { + original + } else { + ty + } + }; - // Pre-initialize the component by running it through - // `component_init_transform::initialize`. Currently, this is the - // application's first and only chance to load any standard or third-party - // modules since we do not yet include a virtual filesystem in the component - // to make those modules available at runtime. - - let stdout = MemoryOutputPipe::new(10000); - let stderr = MemoryOutputPipe::new(10000); - - let mut wasi = WasiCtxBuilder::new(); - wasi.stdin(MemoryInputPipe::new(Bytes::new())) - .stdout(stdout.clone()) - .stderr(stderr.clone()) - .env("PYTHONUNBUFFERED", "1") - .env("PYTHONHOME", "/python") - .preopened_dir( - embedded_python_standard_lib.path(), - "python", - DirPerms::all(), - FilePerms::all(), - )? - .preopened_dir( - embedded_helper_utils.path(), - "bundled", - DirPerms::all(), - FilePerms::all(), + let stream_and_future_indexes = metadata + .streams + .iter() + .enumerate() + .map(|(index, stream)| (original(stream.id), index)) + .chain( + metadata + .futures + .iter() + .enumerate() + .map(|(index, future)| (original(future.id), index)), + ) + .collect(); + + let summary = Summary::try_new( + &resolve, + &worlds, + &import_interface_names, + &export_interface_names, + &imported_function_indexes, + &exported_function_indexes, + &stream_and_future_indexes, + // TODO: We should restrict the `full_names` setting found in a give + // config file to only the world(s) covered by that config file, if + // feasible. + self.full_names + || configs + .values() + .any(|(config, ..)| config.config.full_names), )?; - // Generate guest mounts for each host directory in `python_path`. - for (index, path) in python_path.iter().enumerate() { - wasi.preopened_dir(path, index.to_string(), DirPerms::all(), FilePerms::all())?; - } - - // For each Python package with a `componentize-py.toml` file that specifies - // where generated bindings for that package should be placed, generate the - // bindings and place them as indicated. + let need_async = summary.need_async(); + + // Now that we know whether to use the sync or async version of + // `libcomponentize_py_runtime.so`, update `libraries` accordingly. + // + // Note that we have two separate versions because older runtimes don't + // understand the new async ABI, so we only use the async version if it's + // actually needed. + let mut libraries = libraries + .into_iter() + .filter_map(|library| match (need_async, library.name.as_str()) { + (true, "libcomponentize_py_runtime_sync.so") + | (false, "libcomponentize_py_runtime_async.so") => None, + (true, "libcomponentize_py_runtime_async.so") + | (false, "libcomponentize_py_runtime_sync.so") => Some(Library { + name: "libcomponentize_py_runtime.so".into(), + ..library + }), + _ => Some(library), + }) + .collect::>(); - let mut world_dir_mounts = Vec::new(); - let mut locations = Locations::default(); - let mut saw_main_world = false; + libraries.push(Library { + name: "libcomponentize_py_bindings.so".into(), + module: bindings, + dl_openable: false, + }); - for (config, world, binding_path) in configs - .values() - .filter_map(|(config, world)| Some((config, world, config.config.bindings.as_deref()?))) - { - if *world == main_world { - saw_main_world = true; - } + let component = link::link_libraries(&libraries)?; - let Some(world) = *world else { - bail!("please specify a world for module `{}`", config.module); + let stubbed_component = if self.stub_wasi { + stubwasi::link_stub_modules(libraries)? + } else { + None }; - let paths = python_path - .iter() - .enumerate() - .map(|(index, dir)| { - let dir = Path::new(dir).canonicalize()?; - Ok(if config.root == dir { - config - .path - .join(binding_path) - .strip_prefix(dir) - .ok() - .map(|p| (index, p.to_str().unwrap().replace('\\', "/"))) - } else { - None - }) - }) - .filter_map(Result::transpose) - .collect::>>()?; + // Pre-initialize the component by running it through + // `component_init_transform::initialize`. Currently, this is the + // application's first and only chance to load any standard or third-party + // modules since we do not yet include a virtual filesystem in the component + // to make those modules available at runtime. + + let stdout = MemoryOutputPipe::new(10000); + let stderr = MemoryOutputPipe::new(10000); + + let mut wasi = WasiCtxBuilder::new(); + wasi.stdin(MemoryInputPipe::new(Bytes::new())) + .stdout(stdout.clone()) + .stderr(stderr.clone()) + .env("PYTHONUNBUFFERED", "1") + .env("PYTHONHOME", "/python") + .preopened_dir( + embedded_python_standard_lib.path(), + "python", + DirPerms::all(), + FilePerms::all(), + )? + .preopened_dir( + embedded_helper_utils.path(), + "bundled", + DirPerms::all(), + FilePerms::all(), + )?; + + // Generate guest mounts for each host directory in `python_path`. + for (index, path) in python_path.iter().enumerate() { + wasi.preopened_dir(path, index.to_string(), DirPerms::all(), FilePerms::all())?; + } - let binding_module = paths.first().unwrap().1.replace('/', "."); + // For each Python package with a `componentize-py.toml` file that specifies + // where generated bindings for that package should be placed, generate the + // bindings and place them as indicated. - let world_dir = tempfile::tempdir()?; + let mut world_dir_mounts = Vec::new(); + let mut locations = Locations::default(); + let mut saw_main_world = false; - summary.generate_code( - world_dir.path(), - world, - &binding_module, - &mut locations, - false, - )?; + for (config, world, binding_path) in configs + .values() + .filter_map(|(config, world)| Some((config, world, config.config.bindings.as_deref()?))) + { + if *world == main_world { + saw_main_world = true; + } + + let Some(world) = *world else { + bail!("please specify a world for module `{}`", config.module); + }; - world_dir_mounts.push(( - paths + let paths = python_path .iter() - .map(|(index, p)| format!("{index}/{p}")) - .collect(), - world_dir, - )); - } + .enumerate() + .map(|(index, dir)| { + let dir = Path::new(dir).canonicalize()?; + Ok(if config.root == dir { + config + .path + .join(binding_path) + .strip_prefix(dir) + .ok() + .map(|p| (index, p.to_str().unwrap().replace('\\', "/"))) + } else { + None + }) + }) + .filter_map(Result::transpose) + .collect::>>()?; + + let binding_module = paths.first().unwrap().1.replace('/', "."); + + let world_dir = tempfile::tempdir()?; + + summary.generate_code( + world_dir.path(), + world, + &binding_module, + &mut locations, + false, + )?; + + world_dir_mounts.push(( + paths + .iter() + .map(|(index, p)| format!("{index}/{p}")) + .collect(), + world_dir, + )); + } - // If the caller specified a world and we haven't already generated bindings - // for it above, do so now. - if let (Some(world), false) = (main_world, saw_main_world) { - let module = world_module.unwrap_or(DEFAULT_WORLD_MODULE); - let world_dir = tempfile::tempdir()?; - let module_path = world_dir.path().join(module); - fs::create_dir_all(&module_path)?; - summary.generate_code(&module_path, world, module, &mut locations, false)?; - world_dir_mounts.push((vec!["world".to_owned()], world_dir)); - - // The helper utilities are hard-coded to assume the world module is - // named `wit_world`. Here we replace that with the actual world module - // name. - fn replace(path: &Path, pattern: &str, replacement: &str) -> Result<()> { - if path.is_dir() { - for entry in fs::read_dir(path)? { - replace(&entry?.path(), pattern, replacement)?; + // If the caller specified a world and we haven't already generated bindings + // for it above, do so now. + if let (Some(world), false) = (main_world, saw_main_world) { + let module = self.world_module.unwrap_or(DEFAULT_WORLD_MODULE); + let world_dir = tempfile::tempdir()?; + let module_path = world_dir.path().join(module); + fs::create_dir_all(&module_path)?; + summary.generate_code(&module_path, world, module, &mut locations, false)?; + world_dir_mounts.push((vec!["world".to_owned()], world_dir)); + + // The helper utilities are hard-coded to assume the world module is + // named `wit_world`. Here we replace that with the actual world module + // name. + fn replace(path: &Path, pattern: &str, replacement: &str) -> Result<()> { + if path.is_dir() { + for entry in fs::read_dir(path)? { + replace(&entry?.path(), pattern, replacement)?; + } + } else { + fs::write( + path, + fs::read_to_string(path)? + .replace(pattern, replacement) + .as_bytes(), + )?; } - } else { - fs::write( - path, - fs::read_to_string(path)? - .replace(pattern, replacement) - .as_bytes(), - )?; - } - Ok(()) - } - replace(embedded_helper_utils.path(), "wit_world", module)?; - }; + Ok(()) + } + replace(embedded_helper_utils.path(), "wit_world", module)?; + }; - for (mounts, world_dir) in world_dir_mounts.iter() { - for mount in mounts { - if DEBUG_PYTHON_BINDINGS { - eprintln!("world dir path: {}", world_dir.path().display()); + for (mounts, world_dir) in world_dir_mounts.iter() { + for mount in mounts { + if DEBUG_PYTHON_BINDINGS { + eprintln!("world dir path: {}", world_dir.path().display()); + } + wasi.preopened_dir(world_dir.path(), mount, DirPerms::all(), FilePerms::all())?; } - wasi.preopened_dir(world_dir.path(), mount, DirPerms::all(), FilePerms::all())?; } - } - - if DEBUG_PYTHON_BINDINGS { - // Prevent temporary directories from being deleted: - std::mem::forget(world_dir_mounts); - } - - // Generate a `Symbols` object containing metadata to be passed to the - // pre-init function. The runtime library will use this to look up types - // and functions that will later be referenced by the generated Wasm code. - let symbols = summary.collect_symbols(&locations, &metadata, &clone_maps); - - // Finally, pre-initialize the component, writing the result to - // `output_path`. - let python_path = (0..python_path.len()) - .map(|index| format!("/{index}")) - .collect::>() - .join(":"); + if DEBUG_PYTHON_BINDINGS { + // Prevent temporary directories from being deleted: + std::mem::forget(world_dir_mounts); + } - let table = ResourceTable::new(); - let wasi = wasi - .env( - "PYTHONPATH", - format!("/python:/world:{python_path}:/bundled"), - ) - .build(); + // Generate a `Symbols` object containing metadata to be passed to the + // pre-init function. The runtime library will use this to look up types + // and functions that will later be referenced by the generated Wasm code. + let symbols = summary.collect_symbols(&locations, &metadata, &clone_maps); + + // Finally, pre-initialize the component, writing the result to + // `self.output_path`. + + let python_path = (0..python_path.len()) + .map(|index| format!("/{index}")) + .collect::>() + .join(":"); + + let table = ResourceTable::new(); + let wasi = wasi + .env( + "PYTHONPATH", + format!("/python:/world:{python_path}:/bundled"), + ) + .build(); + + let mut config = Config::new(); + config.wasm_component_model(true); + config.wasm_component_model_async(true); + + let engine = Engine::new(&config)?; + + let mut linker = Linker::new(&engine); + let added_to_linker = if let Some(add_to_linker) = self.add_to_linker { + add_to_linker(&mut linker)?; + true + } else { + false + }; - let mut config = Config::new(); - config.wasm_component_model(true); - config.wasm_component_model_async(true); - config.async_support(true); + let mut store = Store::new(&engine, Ctx { wasi, table }); + + let stub_wasi = self.stub_wasi; + let app_name = self.app_name.to_owned(); + let component = component_init_transform::initialize_staged( + &component, + stubbed_component + .as_ref() + .map(|(component, map)| (component.deref(), map as &dyn Fn(u32) -> u32)), + move |instrumented| { + async move { + let component = &Component::new(&engine, instrumented)?; + if !added_to_linker { + add_wasi_and_stubs(&resolve, &worlds, &mut linker)?; + } - let engine = Engine::new(&config)?; + let pre = InitPre::new(linker.instantiate_pre(component)?)?; + let instance = pre.instance_pre.instantiate_async(&mut store).await?; + let guest = pre.indices.interface0.load(&mut store, &instance)?; - let mut linker = Linker::new(&engine); - let added_to_linker = if let Some(add_to_linker) = add_to_linker { - add_to_linker(&mut linker)?; - true - } else { - false - }; + guest + .call_init(&mut store, &app_name, &symbols, stub_wasi) + .await? + .map_err(|e| anyhow!("{e}"))?; - let mut store = Store::new(&engine, Ctx { wasi, table }); - - let app_name = app_name.to_owned(); - let component = component_init_transform::initialize_staged( - &component, - stubbed_component - .as_ref() - .map(|(component, map)| (component.deref(), map as &dyn Fn(u32) -> u32)), - move |instrumented| { - async move { - let component = &Component::new(&engine, instrumented)?; - if !added_to_linker { - add_wasi_and_stubs(&resolve, &worlds, &mut linker)?; + Ok(Box::new(MyInvoker { store, instance }) as Box) } - - let pre = InitPre::new(linker.instantiate_pre(component)?)?; - let instance = pre.instance_pre.instantiate_async(&mut store).await?; - let guest = pre.indices.interface0.load(&mut store, &instance)?; - - guest - .call_init(&mut store, &app_name, &symbols, stub_wasi) - .await? - .map_err(|e| anyhow!("{e}"))?; - - Ok(Box::new(MyInvoker { store, instance }) as Box) - } - .boxed() - }, - ) - .await - .with_context(move || { - format!( - "{}{}", - String::from_utf8_lossy(&stdout.try_into_inner().unwrap()), - String::from_utf8_lossy(&stderr.try_into_inner().unwrap()) + .boxed() + }, ) - })?; + .await + .with_context(move || { + format!( + "{}{}", + String::from_utf8_lossy(&stdout.try_into_inner().unwrap()), + String::from_utf8_lossy(&stderr.try_into_inner().unwrap()) + ) + })?; + + // Checks if the output directory exists, and creates it if it doesn't. + if let Some(parent) = self.output_path.parent() { + fs::create_dir_all(parent)?; + } + fs::write(self.output_path, component)?; - // Checks if the output directory exists, and creates it if it doesn't. - if let Some(parent) = output_path.parent() { - fs::create_dir_all(parent)?; + Ok(()) } - fs::write(output_path, component)?; - - Ok(()) } fn parse_wit( - paths: &[impl AsRef], - world: Option<&str>, - features: &[String], + paths: &[&Path], + worlds: &[&str], + features: &[&str], all_features: bool, + union_name: &str, ) -> Result<(Resolve, WorldId)> { // If no WIT directory was provided as a parameter and none were referenced // by Python 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, features, all_features, union_name); } debug_assert!(!paths.is_empty(), "The paths should not be empty"); @@ -741,23 +766,119 @@ fn parse_wit( } } - let mut last_pkg = None; - for path in paths.iter().map(AsRef::as_ref) { - let pkg = if path.is_dir() { - resolve.push_dir(path)?.0 - } else { - let pkg = UnresolvedPackageGroup::parse_file(path)?; - resolve.push_group(pkg)? - }; - last_pkg = Some(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 available_worlds = |resolve: &Resolve| { + resolve + .worlds + .iter() + .map(|(_, world)| { + if let Some(package) = world.package { + let package = &resolve.packages[package].name; + let version = if let Some(version) = &package.version { + format!("@{version}") + } else { + String::new() + }; + let package_namespace = &package.namespace; + let package_name = &package.name; + let name = &world.name; + format!("{package_namespace}:{package_name}/{name}{version}") + } else { + world.name.clone() + } + }) + .collect::>() + }; - let pkg = last_pkg.unwrap(); // The paths should not be empty - let world = resolve.select_world(&[pkg], world)?; + let worlds = worlds + .iter() + .map(|world| { + packages + .iter() + .find_map(|&pkg| resolve.select_world(&[pkg], Some(world)).ok()) + .ok_or_else(|| { + let worlds = available_worlds(&resolve); + anyhow!( + "No world named `{world}` found in any of the loaded WIT packages.\n\ + Available worlds: {worlds:#?}\n\ + WIT paths: {paths:#?}" + ) + }) + }) + .collect::>>()?; + + let world = match &worlds[..] { + [] => packages + .iter() + .find_map(|&pkg| resolve.select_world(&[pkg], None).ok()) + .ok_or_else(|| { + let worlds = available_worlds(&resolve); + anyhow!( + "No default world found in any of the loaded WIT packages.\n\ + Available worlds: {worlds:#?}\n\ + WIT paths: {paths:#?}" + ) + })?, + &[world] => world, + worlds => union_world(&mut resolve, union_name, worlds, &mut CloneMaps::default())?, + }; Ok((resolve, world)) } +fn union_world( + resolve: &mut Resolve, + name: &str, + worlds: &[WorldId], + clone_maps: &mut CloneMaps, +) -> Result { + let union_package = resolve.packages.alloc(Package { + name: PackageName { + namespace: "componentize-py".into(), + name: name.into(), + version: None, + }, + docs: Default::default(), + interfaces: Default::default(), + worlds: Default::default(), + }); + + let union_world = resolve.worlds.alloc(World { + name: name.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(name.into(), union_world); + + for &world in worlds { + resolve.merge_worlds(world, union_world, clone_maps)?; + } + + Ok(union_world) +} + fn add_wasi_and_stubs( resolve: &Resolve, worlds: &IndexSet, @@ -836,7 +957,7 @@ fn add_wasi_and_stubs( let interface_name = interface_name.clone(); let name = name.clone(); Box::pin(async move { - Err(anyhow!( + Err(wasmtime::format_err!( "called trapping stub: {interface_name}#{name}" )) }) @@ -846,7 +967,7 @@ fn add_wasi_and_stubs( instance.func_new(name, { let name = name.clone(); move |_, _, _, _| { - Err(anyhow!( + Err(wasmtime::format_err!( "called trapping stub: {interface_name}#{name}" )) } @@ -857,7 +978,9 @@ fn add_wasi_and_stubs( .resource(name, ResourceType::host::<()>(), { let name = name.clone(); move |_, _| { - Err(anyhow!("called trapping stub: {interface_name}#{name}")) + Err(wasmtime::format_err!( + "called trapping stub: {interface_name}#{name}" + )) } }) .map(drop), @@ -874,22 +997,24 @@ fn add_wasi_and_stubs( let name = name.clone(); move |_, _, _, _| { let name = name.clone(); - Box::pin( - async move { Err(anyhow!("called trapping stub: {name}")) }, - ) + Box::pin(async move { + Err(wasmtime::format_err!("called trapping stub: {name}")) + }) } }) } else { instance.func_new(name, { let name = name.clone(); - move |_, _, _, _| Err(anyhow!("called trapping stub: {name}")) + move |_, _, _, _| { + Err(wasmtime::format_err!("called trapping stub: {name}")) + } }) } } Stub::Resource(name) => instance .resource(name, ResourceType::host::<()>(), { let name = name.clone(); - move |_, _| Err(anyhow!("called trapping stub: {name}")) + move |_, _| Err(wasmtime::format_err!("called trapping stub: {name}")) }) .map(drop), }?; diff --git a/src/prelink.rs b/src/prelink.rs index 330b0f1..3aac57a 100644 --- a/src/prelink.rs +++ b/src/prelink.rs @@ -26,7 +26,7 @@ fn is_wasm_file(path: &Path) -> bool { } type ConfigsMatchedWorlds<'a> = - IndexMap, Option<&'a str>)>; + IndexMap, &'a [&'a str])>; pub fn embedded_python_standard_library() -> Result { // Untar the embedded copy of the Python standard library into a temporary @@ -164,8 +164,8 @@ pub fn bundle_libraries(library_path: Vec<(&str, Vec)>) -> Result( python_path: &'a Vec<&'a str>, - module_worlds: &'a [(&'a str, &'a str)], - world: Option<&'a str>, + module_worlds: &'a [(&'a str, &'a [&'a str])], + worlds: &'a [&'a str], ) -> Result<(ConfigsMatchedWorlds<'a>, Vec)> { let mut raw_configs: Vec> = Vec::new(); let mut library_path: Vec<(&str, Vec)> = Vec::with_capacity(python_path.len()); @@ -190,9 +190,9 @@ pub fn search_for_libraries_and_configs<'a>( // dependencies. // // For any packages which contain componentize-py.toml files but no - // corresponding `module_worlds` entry, we use the `world` parameter as a + // corresponding `module_worlds` entry, we use the `worlds` parameter as a // default. - let configs: IndexMap, Option<&str>)> = { + let configs: IndexMap, &[&str])> = { let mut configs = raw_configs .into_iter() .map(|raw_config| { @@ -212,16 +212,16 @@ pub fn search_for_libraries_and_configs<'a>( .collect::>>()?; let mut ordered = IndexMap::new(); - for (module, world) in module_worlds { - if let Some(config) = configs.remove(*module) { - ordered.insert((*module).to_owned(), (config, Some(*world))); + for &(module, worlds) in module_worlds { + if let Some(config) = configs.remove(module) { + ordered.insert(module.to_owned(), (config, worlds)); } else { bail!("no `componentize-py.toml` file found for module `{module}`"); } } for (module, config) in configs { - ordered.insert(module, (config, world)); + ordered.insert(module, (config, worlds)); } ordered diff --git a/src/python.rs b/src/python.rs index 3d2dfe5..515a785 100644 --- a/src/python.rs +++ b/src/python.rs @@ -4,6 +4,7 @@ )] use { + crate::{BindingsGenerator, ComponentGenerator}, pyo3::{ Bound, PyResult, Python, exceptions::PyAssertionError, @@ -17,46 +18,54 @@ use { #[allow(clippy::too_many_arguments)] #[pyo3::pyfunction] #[pyo3(name = "componentize")] -#[pyo3(signature = (wit_path, world, features, all_features, world_module, python_path, module_worlds, app_name, output_path, stub_wasi, import_interface_names, export_interface_names))] +#[pyo3(signature = (wit_path, worlds, features, all_features, world_module, python_path, module_worlds, app_name, output_path, stub_wasi, import_interface_names, export_interface_names, full_names))] fn python_componentize( wit_path: Vec, - world: Option<&str>, + worlds: Vec, features: Vec, all_features: bool, world_module: Option<&str>, python_path: Vec, - module_worlds: Vec<(PyBackedStr, PyBackedStr)>, + module_worlds: Vec<(PyBackedStr, Vec)>, app_name: &str, output_path: PathBuf, stub_wasi: bool, import_interface_names: Vec<(PyBackedStr, PyBackedStr)>, export_interface_names: Vec<(PyBackedStr, PyBackedStr)>, + full_names: bool, ) -> PyResult<()> { (|| { - Runtime::new()?.block_on(crate::componentize( - &wit_path, - world, - &features, - all_features, - world_module, - &python_path.iter().map(|s| s.as_ref()).collect::>(), - &module_worlds - .iter() - .map(|(a, b)| (a.as_ref(), b.as_ref())) - .collect::>(), - app_name, - &output_path, - None, - stub_wasi, - &import_interface_names - .iter() - .map(|(a, b)| (a.as_ref(), b.as_ref())) - .collect(), - &export_interface_names - .iter() - .map(|(a, b)| (a.as_ref(), b.as_ref())) - .collect(), - )) + Runtime::new()?.block_on( + ComponentGenerator { + wit_path: &wit_path.iter().map(|v| v.as_path()).collect::>(), + worlds: &worlds.iter().map(|v| v.as_str()).collect::>(), + features: &features.iter().map(|v| v.as_str()).collect::>(), + all_features, + world_module, + python_path: &python_path.iter().map(|s| s.as_ref()).collect::>(), + module_worlds: &module_worlds + .iter() + .map(|(a, b)| (a.as_ref(), b.iter().map(|v| v.as_ref()).collect::>())) + .collect::>() + .iter() + .map(|(k, v)| (*k, v as &[_])) + .collect::>(), + app_name, + output_path: &output_path, + add_to_linker: None, + stub_wasi, + import_interface_names: &import_interface_names + .iter() + .map(|(a, b)| (a.as_ref(), b.as_ref())) + .collect(), + export_interface_names: &export_interface_names + .iter() + .map(|(a, b)| (a.as_ref(), b.as_ref())) + .collect(), + full_names, + } + .generate(), + ) })() .map_err(|e| PyAssertionError::new_err(format!("{e:?}"))) } @@ -64,33 +73,36 @@ fn python_componentize( #[allow(clippy::too_many_arguments)] #[pyo3::pyfunction] #[pyo3(name = "generate_bindings")] -#[pyo3(signature = (wit_path, world, features, all_features, world_module, output_dir, import_interface_names, export_interface_names))] +#[pyo3(signature = (wit_path, worlds, features, all_features, world_module, output_dir, import_interface_names, export_interface_names, full_names))] fn python_generate_bindings( wit_path: Vec, - world: Option<&str>, + worlds: Vec, features: Vec, all_features: bool, world_module: Option<&str>, output_dir: PathBuf, import_interface_names: Vec<(PyBackedStr, PyBackedStr)>, export_interface_names: Vec<(PyBackedStr, PyBackedStr)>, + full_names: bool, ) -> PyResult<()> { - crate::generate_bindings( - &wit_path, - world, - &features, + BindingsGenerator { + wit_path: &wit_path.iter().map(|v| v.as_path()).collect::>(), + worlds: &worlds.iter().map(|v| v.as_str()).collect::>(), + features: &features.iter().map(|v| v.as_str()).collect::>(), all_features, world_module, - &output_dir, - &import_interface_names + output_dir: &output_dir, + import_interface_names: &import_interface_names .iter() .map(|(a, b)| (a.as_ref(), b.as_ref())) .collect(), - &export_interface_names + export_interface_names: &export_interface_names .iter() .map(|(a, b)| (a.as_ref(), b.as_ref())) .collect(), - ) + full_names, + } + .generate() .map_err(|e| PyAssertionError::new_err(format!("{e:?}"))) } diff --git a/src/summary.rs b/src/summary.rs index 38639b9..9429fff 100644 --- a/src/summary.rs +++ b/src/summary.rs @@ -145,10 +145,12 @@ pub struct Summary<'a> { imported_function_indexes: &'a HashMap<(Option<&'a str>, &'a str), usize>, exported_function_indexes: &'a HashMap<(Option<&'a str>, &'a str), usize>, stream_and_future_indexes: &'a HashMap, + full_names: bool, need_async: bool, } impl<'a> Summary<'a> { + #[expect(clippy::too_many_arguments)] pub fn try_new( resolve: &'a Resolve, worlds: &IndexSet, @@ -157,6 +159,7 @@ impl<'a> Summary<'a> { imported_function_indexes: &'a HashMap<(Option<&'a str>, &'a str), usize>, exported_function_indexes: &'a HashMap<(Option<&'a str>, &'a str), usize>, stream_and_future_indexes: &'a HashMap, + full_names: bool, ) -> Result { let mut me = Self { resolve, @@ -178,6 +181,7 @@ impl<'a> Summary<'a> { imported_function_indexes, exported_function_indexes, stream_and_future_indexes, + full_names, need_async: false, }; @@ -1205,30 +1209,31 @@ impl<'a> Summary<'a> { *id, if let Some(version) = version { if let Some(name) = interface_names.get( - format!( - "{package_namespace}:{package_name}/{name}@{version}" - ) - .as_str(), - ) { - (*name).to_owned() - } else if versions.len() == 1 { - if packages.len() == 1 { + format!( + "{package_namespace}:{package_name}\ + /{name}@{version}" + ) + .as_str(), + ) { (*name).to_owned() + } else if versions.len() == 1 && !self.full_names { + if packages.len() == 1 { + (*name).to_owned() + } else { + format!("{package_namespace}-{package_name}-{name}") + } } else { - format!("{package_namespace}-{package_name}-{name}") + format!( + "{package_namespace}-{package_name}-{name}-{}", + version.to_string().replace('.', "-") + ) } - } else { - format!( - "{package_namespace}-{package_name}-{name}-{}", - version.to_string().replace('.', "-") - ) - } } else if let Some(name) = interface_names.get( format!("{package_namespace}:{package_name}/{name}") .as_str() ) { (*name).to_owned() - } else if packages.len() == 1 { + } else if packages.len() == 1 && !self.full_names { (*name).to_owned() } else { format!("{package_namespace}-{package_name}-{name}",) diff --git a/src/test.rs b/src/test.rs index d7e548e..ed0d9af 100644 --- a/src/test.rs +++ b/src/test.rs @@ -1,7 +1,7 @@ #![deny(warnings)] use { - crate::Ctx, + crate::{ComponentGenerator, Ctx}, anyhow::{Result, anyhow}, once_cell::sync::Lazy, proptest::{ @@ -36,7 +36,6 @@ static SEED: Lazy<[u8; 32]> = Lazy::new(|| get_seed().unwrap()); static ENGINE: Lazy = Lazy::new(|| { let mut config = Config::new(); - config.async_support(true); config.wasm_component_model(true); config.wasm_component_model_async(true); @@ -49,7 +48,7 @@ async fn make_component( world_module: Option<&str>, guest_code: &[(&str, &str)], python_path: &[&str], - module_worlds: &[(&str, &str)], + module_worlds: &[(&str, &[&str])], add_to_linker: Option<&dyn Fn(&mut Linker) -> Result<()>>, ) -> Result> { let tempdir = tempfile::tempdir()?; @@ -61,13 +60,13 @@ async fn make_component( fs::write(&path, content)?; } - crate::componentize( - &[tempdir.path().join("app.wit")], - None, - &[], - false, + ComponentGenerator { + wit_path: &[&tempdir.path().join("app.wit")], + worlds: &[], + features: &[], + all_features: false, world_module, - &python_path + python_path: &python_path .iter() .copied() .chain(iter::once(tempdir.path().to_str().ok_or_else(|| { @@ -75,13 +74,15 @@ async fn make_component( })?)) .collect::>(), module_worlds, - "app", - &tempdir.path().join("app.wasm"), + app_name: "app", + output_path: &tempdir.path().join("app.wasm"), add_to_linker, - false, - &HashMap::new(), - &HashMap::new(), - ) + stub_wasi: false, + import_interface_names: &HashMap::new(), + export_interface_names: &HashMap::new(), + full_names: false, + } + .generate() .await?; Ok(fs::read(tempdir.path().join("app.wasm"))?) @@ -125,7 +126,7 @@ impl Tester { world_module: Option<&str>, guest_code: &[(&str, &str)], python_path: &[&str], - module_worlds: &[(&str, &str)], + module_worlds: &[(&str, &[&str])], seed: [u8; 32], ) -> Result { // TODO: create two versions of the component -- one with and one diff --git a/src/test/echoes.rs b/src/test/echoes.rs index e927d2d..9795586 100644 --- a/src/test/echoes.rs +++ b/src/test/echoes.rs @@ -17,127 +17,133 @@ wasmtime::component::bindgen!({ }); impl componentize_py::test::echoes::Host for Ctx { - async fn echo_nothing(&mut self) -> Result<()> { + async fn echo_nothing(&mut self) -> wasmtime::Result<()> { Ok(()) } - async fn echo_bool(&mut self, v: bool) -> Result { + async fn echo_bool(&mut self, v: bool) -> wasmtime::Result { Ok(v) } - async fn echo_u8(&mut self, v: u8) -> Result { + async fn echo_u8(&mut self, v: u8) -> wasmtime::Result { Ok(v) } - async fn echo_s8(&mut self, v: i8) -> Result { + async fn echo_s8(&mut self, v: i8) -> wasmtime::Result { Ok(v) } - async fn echo_u16(&mut self, v: u16) -> Result { + async fn echo_u16(&mut self, v: u16) -> wasmtime::Result { Ok(v) } - async fn echo_s16(&mut self, v: i16) -> Result { + async fn echo_s16(&mut self, v: i16) -> wasmtime::Result { Ok(v) } - async fn echo_u32(&mut self, v: u32) -> Result { + async fn echo_u32(&mut self, v: u32) -> wasmtime::Result { Ok(v) } - async fn echo_s32(&mut self, v: i32) -> Result { + async fn echo_s32(&mut self, v: i32) -> wasmtime::Result { Ok(v) } - async fn echo_char(&mut self, v: char) -> Result { + async fn echo_char(&mut self, v: char) -> wasmtime::Result { Ok(v) } - async fn echo_u64(&mut self, v: u64) -> Result { + async fn echo_u64(&mut self, v: u64) -> wasmtime::Result { Ok(v) } - async fn echo_s64(&mut self, v: i64) -> Result { + async fn echo_s64(&mut self, v: i64) -> wasmtime::Result { Ok(v) } - async fn echo_f32(&mut self, v: f32) -> Result { + async fn echo_f32(&mut self, v: f32) -> wasmtime::Result { Ok(v) } - async fn echo_f64(&mut self, v: f64) -> Result { + async fn echo_f64(&mut self, v: f64) -> wasmtime::Result { Ok(v) } - async fn echo_string(&mut self, v: String) -> Result { + async fn echo_string(&mut self, v: String) -> wasmtime::Result { Ok(v) } - async fn echo_list_bool(&mut self, v: Vec) -> Result> { + async fn echo_list_bool(&mut self, v: Vec) -> wasmtime::Result> { Ok(v) } - async fn echo_list_u8(&mut self, v: Vec) -> Result> { + async fn echo_list_u8(&mut self, v: Vec) -> wasmtime::Result> { Ok(v) } - async fn echo_list_s8(&mut self, v: Vec) -> Result> { + async fn echo_list_s8(&mut self, v: Vec) -> wasmtime::Result> { Ok(v) } - async fn echo_list_u16(&mut self, v: Vec) -> Result> { + async fn echo_list_u16(&mut self, v: Vec) -> wasmtime::Result> { Ok(v) } - async fn echo_list_s16(&mut self, v: Vec) -> Result> { + async fn echo_list_s16(&mut self, v: Vec) -> wasmtime::Result> { Ok(v) } - async fn echo_list_u32(&mut self, v: Vec) -> Result> { + async fn echo_list_u32(&mut self, v: Vec) -> wasmtime::Result> { Ok(v) } - async fn echo_list_s32(&mut self, v: Vec) -> Result> { + async fn echo_list_s32(&mut self, v: Vec) -> wasmtime::Result> { Ok(v) } - async fn echo_list_char(&mut self, v: Vec) -> Result> { + async fn echo_list_char(&mut self, v: Vec) -> wasmtime::Result> { Ok(v) } - async fn echo_list_u64(&mut self, v: Vec) -> Result> { + async fn echo_list_u64(&mut self, v: Vec) -> wasmtime::Result> { Ok(v) } - async fn echo_list_s64(&mut self, v: Vec) -> Result> { + async fn echo_list_s64(&mut self, v: Vec) -> wasmtime::Result> { Ok(v) } - async fn echo_list_f32(&mut self, v: Vec) -> Result> { + async fn echo_list_f32(&mut self, v: Vec) -> wasmtime::Result> { Ok(v) } - async fn echo_list_f64(&mut self, v: Vec) -> Result> { + async fn echo_list_f64(&mut self, v: Vec) -> wasmtime::Result> { Ok(v) } - async fn echo_list_string(&mut self, v: Vec) -> Result> { + async fn echo_list_string(&mut self, v: Vec) -> wasmtime::Result> { Ok(v) } - async fn echo_list_list_u8(&mut self, v: Vec>) -> Result>> { + async fn echo_list_list_u8(&mut self, v: Vec>) -> wasmtime::Result>> { Ok(v) } - async fn echo_list_list_list_u8(&mut self, v: Vec>>) -> Result>>> { + async fn echo_list_list_list_u8( + &mut self, + v: Vec>>, + ) -> wasmtime::Result>>> { Ok(v) } - async fn echo_option_u8(&mut self, v: Option) -> Result> { + async fn echo_option_u8(&mut self, v: Option) -> wasmtime::Result> { Ok(v) } - async fn echo_option_option_u8(&mut self, v: Option>) -> Result>> { + async fn echo_option_option_u8( + &mut self, + v: Option>, + ) -> wasmtime::Result>> { Ok(v) } @@ -159,7 +165,7 @@ impl componentize_py::test::echoes::Host for Ctx { v14: Vec, v15: Vec, v16: Vec, - ) -> Result<( + ) -> wasmtime::Result<( bool, u8, u16, @@ -195,7 +201,7 @@ impl super::Host for Host { } async fn instantiate_pre(store: &mut Store, pre: InstancePre) -> Result { - EchoesTestPre::new(pre)?.instantiate_async(store).await + Ok(EchoesTestPre::new(pre)?.instantiate_async(store).await?) } } @@ -327,121 +333,121 @@ static TESTER: Lazy> = Lazy::new(|| { #[test] fn nothing() -> Result<()> { TESTER.all_eq(&Just(()), |(), instance, store, runtime| { - runtime.block_on( + Ok(runtime.block_on( instance .componentize_py_test_echoes() .call_echo_nothing(store), - ) + )?) }) } #[test] fn bools() -> Result<()> { TESTER.all_eq(&proptest::bool::ANY, |v, instance, store, runtime| { - runtime.block_on( + Ok(runtime.block_on( instance .componentize_py_test_echoes() .call_echo_bool(store, v), - ) + )?) }) } #[test] fn u8s() -> Result<()> { TESTER.all_eq(&proptest::num::u8::ANY, |v, instance, store, runtime| { - runtime.block_on( + Ok(runtime.block_on( instance .componentize_py_test_echoes() .call_echo_u8(store, v), - ) + )?) }) } #[test] fn s8s() -> Result<()> { TESTER.all_eq(&proptest::num::i8::ANY, |v, instance, store, runtime| { - runtime.block_on( + Ok(runtime.block_on( instance .componentize_py_test_echoes() .call_echo_s8(store, v), - ) + )?) }) } #[test] fn u16s() -> Result<()> { TESTER.all_eq(&proptest::num::u16::ANY, |v, instance, store, runtime| { - runtime.block_on( + Ok(runtime.block_on( instance .componentize_py_test_echoes() .call_echo_u16(store, v), - ) + )?) }) } #[test] fn s16s() -> Result<()> { TESTER.all_eq(&proptest::num::i16::ANY, |v, instance, store, runtime| { - runtime.block_on( + Ok(runtime.block_on( instance .componentize_py_test_echoes() .call_echo_s16(store, v), - ) + )?) }) } #[test] fn u32s() -> Result<()> { TESTER.all_eq(&proptest::num::u32::ANY, |v, instance, store, runtime| { - runtime.block_on( + Ok(runtime.block_on( instance .componentize_py_test_echoes() .call_echo_u32(store, v), - ) + )?) }) } #[test] fn s32s() -> Result<()> { TESTER.all_eq(&proptest::num::i32::ANY, |v, instance, store, runtime| { - runtime.block_on( + Ok(runtime.block_on( instance .componentize_py_test_echoes() .call_echo_s32(store, v), - ) + )?) }) } #[test] fn u64s() -> Result<()> { TESTER.all_eq(&proptest::num::u64::ANY, |v, instance, store, runtime| { - runtime.block_on( + Ok(runtime.block_on( instance .componentize_py_test_echoes() .call_echo_u64(store, v), - ) + )?) }) } #[test] fn s64s() -> Result<()> { TESTER.all_eq(&proptest::num::i64::ANY, |v, instance, store, runtime| { - runtime.block_on( + Ok(runtime.block_on( instance .componentize_py_test_echoes() .call_echo_s64(store, v), - ) + )?) }) } #[test] fn chars() -> Result<()> { TESTER.all_eq(&proptest::char::any(), |v, instance, store, runtime| { - runtime.block_on( + Ok(runtime.block_on( instance .componentize_py_test_echoes() .call_echo_char(store, v), - ) + )?) }) } @@ -484,11 +490,11 @@ fn strings() -> Result<()> { TESTER.all_eq( &proptest::string::string_regex(".*")?, |v, instance, store, runtime| { - runtime.block_on( + Ok(runtime.block_on( instance .componentize_py_test_echoes() .call_echo_string(store, &v), - ) + )?) }, ) } @@ -498,11 +504,11 @@ fn list_bools() -> Result<()> { TESTER.all_eq( &proptest::collection::vec(proptest::bool::ANY, 0..MAX_SIZE), |v, instance, store, runtime| { - runtime.block_on( + Ok(runtime.block_on( instance .componentize_py_test_echoes() .call_echo_list_bool(store, &v), - ) + )?) }, ) } @@ -512,11 +518,11 @@ fn list_u8s() -> Result<()> { TESTER.all_eq( &proptest::collection::vec(proptest::num::u8::ANY, 0..MAX_SIZE), |v, instance, store, runtime| { - runtime.block_on( + Ok(runtime.block_on( instance .componentize_py_test_echoes() .call_echo_list_u8(store, &v), - ) + )?) }, ) } @@ -529,11 +535,11 @@ fn list_list_u8s() -> Result<()> { 0..MAX_SIZE, ), |v, instance, store, runtime| { - runtime.block_on( + Ok(runtime.block_on( instance .componentize_py_test_echoes() .call_echo_list_list_u8(store, &v), - ) + )?) }, ) } @@ -549,11 +555,11 @@ fn list_list_list_u8s() -> Result<()> { 0..MAX_SIZE, ), |v, instance, store, runtime| { - runtime.block_on( + Ok(runtime.block_on( instance .componentize_py_test_echoes() .call_echo_list_list_list_u8(store, &v), - ) + )?) }, ) } @@ -563,11 +569,11 @@ fn option_u8s() -> Result<()> { TESTER.all_eq( &proptest::option::of(proptest::num::u8::ANY), |v, instance, store, runtime| { - runtime.block_on( + Ok(runtime.block_on( instance .componentize_py_test_echoes() .call_echo_option_u8(store, v), - ) + )?) }, ) } @@ -577,11 +583,11 @@ fn option_option_u8s() -> Result<()> { TESTER.all_eq( &proptest::option::of(proptest::option::of(proptest::num::u8::ANY)), |v, instance, store, runtime| { - runtime.block_on( + Ok(runtime.block_on( instance .componentize_py_test_echoes() .call_echo_option_option_u8(store, v), - ) + )?) }, ) } @@ -591,11 +597,11 @@ fn list_s8s() -> Result<()> { TESTER.all_eq( &proptest::collection::vec(proptest::num::i8::ANY, 0..MAX_SIZE), |v, instance, store, runtime| { - runtime.block_on( + Ok(runtime.block_on( instance .componentize_py_test_echoes() .call_echo_list_s8(store, &v), - ) + )?) }, ) } @@ -605,11 +611,11 @@ fn list_u16s() -> Result<()> { TESTER.all_eq( &proptest::collection::vec(proptest::num::u16::ANY, 0..MAX_SIZE), |v, instance, store, runtime| { - runtime.block_on( + Ok(runtime.block_on( instance .componentize_py_test_echoes() .call_echo_list_u16(store, &v), - ) + )?) }, ) } @@ -619,11 +625,11 @@ fn list_s16s() -> Result<()> { TESTER.all_eq( &proptest::collection::vec(proptest::num::i16::ANY, 0..MAX_SIZE), |v, instance, store, runtime| { - runtime.block_on( + Ok(runtime.block_on( instance .componentize_py_test_echoes() .call_echo_list_s16(store, &v), - ) + )?) }, ) } @@ -633,11 +639,11 @@ fn list_u32s() -> Result<()> { TESTER.all_eq( &proptest::collection::vec(proptest::num::u32::ANY, 0..MAX_SIZE), |v, instance, store, runtime| { - runtime.block_on( + Ok(runtime.block_on( instance .componentize_py_test_echoes() .call_echo_list_u32(store, &v), - ) + )?) }, ) } @@ -647,11 +653,11 @@ fn list_s32s() -> Result<()> { TESTER.all_eq( &proptest::collection::vec(proptest::num::i32::ANY, 0..MAX_SIZE), |v, instance, store, runtime| { - runtime.block_on( + Ok(runtime.block_on( instance .componentize_py_test_echoes() .call_echo_list_s32(store, &v), - ) + )?) }, ) } @@ -661,11 +667,11 @@ fn list_u64s() -> Result<()> { TESTER.all_eq( &proptest::collection::vec(proptest::num::u64::ANY, 0..MAX_SIZE), |v, instance, store, runtime| { - runtime.block_on( + Ok(runtime.block_on( instance .componentize_py_test_echoes() .call_echo_list_u64(store, &v), - ) + )?) }, ) } @@ -675,11 +681,11 @@ fn list_s64s() -> Result<()> { TESTER.all_eq( &proptest::collection::vec(proptest::num::i64::ANY, 0..MAX_SIZE), |v, instance, store, runtime| { - runtime.block_on( + Ok(runtime.block_on( instance .componentize_py_test_echoes() .call_echo_list_s64(store, &v), - ) + )?) }, ) } @@ -689,11 +695,11 @@ fn list_chars() -> Result<()> { TESTER.all_eq( &proptest::collection::vec(proptest::char::any(), 0..MAX_SIZE), |v, instance, store, runtime| { - runtime.block_on( + Ok(runtime.block_on( instance .componentize_py_test_echoes() .call_echo_list_char(store, &v), - ) + )?) }, ) } diff --git a/src/test/tests.rs b/src/test/tests.rs index c7665fa..0fdc614 100644 --- a/src/test/tests.rs +++ b/src/test/tests.rs @@ -2,9 +2,9 @@ use { super::{Ctx, SEED, Tester}, - anyhow::{Error, Result, anyhow}, + anyhow::{Result, anyhow}, exports::componentize_py::test::streams_and_futures, - futures::{FutureExt, TryStreamExt, stream::FuturesUnordered}, + futures::{FutureExt, TryStreamExt, channel::oneshot, stream::FuturesUnordered}, once_cell::sync::Lazy, std::{ collections::BTreeMap, @@ -31,7 +31,7 @@ wasmtime::component::bindgen!({ path: "src/test/wit", world: "tests", imports: { default: async | trappable }, - exports: { default: async | task_exit }, + exports: { default: async }, with: { "componentize-py:test/resource-import-and-export.thing": ThingU32, "componentize-py:test/resource-borrow-import.thing": ThingU32, @@ -74,17 +74,17 @@ pub struct ThingString(String); pub struct MyFloat(f64); impl TestsImports for Ctx { - async fn output(&mut self, _: Frame) -> Result<()> { + async fn output(&mut self, _: Frame) -> wasmtime::Result<()> { unreachable!() } - async fn get_bytes(&mut self, count: u32) -> Result> { + async fn get_bytes(&mut self, count: u32) -> wasmtime::Result> { Ok(vec![42u8; usize::try_from(count).unwrap()]) } } impl TestsImportsWithStore for HasSelf { - async fn sleep(_: &Accessor, delay_millis: u32) -> Result<()> { + async fn sleep(_: &Accessor, delay_millis: u32) -> wasmtime::Result<()> { tokio::time::sleep(Duration::from_millis(delay_millis as _)).await; Ok(()) } @@ -125,7 +125,7 @@ impl super::Host for Host { } async fn instantiate_pre(store: &mut Store, pre: InstancePre) -> Result { - TestsPre::new(pre)?.instantiate_async(store).await + Ok(TestsPre::new(pre)?.instantiate_async(store).await?) } } @@ -139,9 +139,9 @@ impl super::Host for FooHost { } async fn instantiate_pre(store: &mut Store, pre: InstancePre) -> Result { - foo_sdk::FooWorldPre::new(pre)? + Ok(foo_sdk::FooWorldPre::new(pre)? .instantiate_async(store) - .await + .await?) } } @@ -155,9 +155,9 @@ impl super::Host for BarHost { } async fn instantiate_pre(store: &mut Store, pre: InstancePre) -> Result { - bar_sdk::BarWorldPre::new(pre)? + Ok(bar_sdk::BarWorldPre::new(pre)? .instantiate_async(store) - .await + .await?) } } @@ -167,7 +167,7 @@ static TESTER: Lazy> = Lazy::new(|| { Some("tests"), GUEST_CODE, &["src/test"], - &[("foo_sdk", "foo-world"), ("bar_sdk", "bar-world")], + &[("foo_sdk", &["foo-world"]), ("bar_sdk", &["bar-world"])], *SEED, ) .unwrap() @@ -194,18 +194,16 @@ fn simple_async_export() -> Result<()> { TESTER.test(|world, store, runtime| { assert_eq!( 42 + 3, - runtime - .block_on(async { - store - .run_concurrent(async |store| { - world - .componentize_py_test_simple_async_export() - .call_foo(store, 42) - .await - }) - .await? - })? - .0 + runtime.block_on(async { + store + .run_concurrent(async |store| { + world + .componentize_py_test_simple_async_export() + .call_foo(store, 42) + .await + }) + .await? + })? ); Ok(()) @@ -215,7 +213,7 @@ fn simple_async_export() -> Result<()> { #[test] fn simple_import_and_export() -> Result<()> { impl componentize_py::test::simple_import_and_export::Host for Ctx { - async fn foo(&mut self, v: u32) -> Result { + async fn foo(&mut self, v: u32) -> wasmtime::Result { Ok(v + 2) } } @@ -239,7 +237,7 @@ fn simple_async_import_and_export() -> Result<()> { impl componentize_py::test::simple_async_import_and_export::Host for Ctx {} impl componentize_py::test::simple_async_import_and_export::HostWithStore for HasSelf { - async fn foo(_: &Accessor, v: u32) -> Result { + async fn foo(_: &Accessor, v: u32) -> wasmtime::Result { tokio::time::sleep(DELAY).await; Ok(v + 2) } @@ -248,18 +246,16 @@ fn simple_async_import_and_export() -> Result<()> { TESTER.test(|world, store, runtime| { assert_eq!( 42 + 2 + 3, - runtime - .block_on(async { - store - .run_concurrent(async |store| { - world - .componentize_py_test_simple_async_import_and_export() - .call_foo(store, 42) - .await - }) - .await? - })? - .0 + runtime.block_on(async { + store + .run_concurrent(async |store| { + world + .componentize_py_test_simple_async_import_and_export() + .call_foo(store, 42) + .await + }) + .await? + })? ); Ok(()) @@ -271,15 +267,15 @@ fn resource_import_and_export() -> Result<()> { use componentize_py::test::resource_import_and_export::{Host, HostThing}; impl HostThing for Ctx { - async fn new(&mut self, v: u32) -> Result> { + async fn new(&mut self, v: u32) -> wasmtime::Result> { Ok(self.ctx().table.push(ThingU32(v + 8))?) } - async fn foo(&mut self, this: Resource) -> Result { + async fn foo(&mut self, this: Resource) -> wasmtime::Result { Ok(self.ctx().table.get(&this)?.0 + 1) } - async fn bar(&mut self, this: Resource, v: u32) -> Result<()> { + async fn bar(&mut self, this: Resource, v: u32) -> wasmtime::Result<()> { self.ctx().table.get_mut(&this)?.0 = v + 5; Ok(()) } @@ -288,14 +284,14 @@ fn resource_import_and_export() -> Result<()> { &mut self, a: Resource, b: Resource, - ) -> Result> { + ) -> wasmtime::Result> { let a = self.ctx().table.get(&a)?.0; let b = self.ctx().table.get(&b)?.0; Ok(self.ctx().table.push(ThingU32(a + b + 6))?) } - async fn drop(&mut self, this: Resource) -> Result<()> { + async fn drop(&mut self, this: Resource) -> wasmtime::Result<()> { Ok(self.ctx().table.delete(this).map(|_| ())?) } } @@ -336,7 +332,7 @@ fn resource_import_and_export() -> Result<()> { #[test] fn refcounts() -> Result<()> { - TESTER.test(|world, store, runtime| runtime.block_on(world.call_test_refcounts(store))) + TESTER.test(|world, store, runtime| Ok(runtime.block_on(world.call_test_refcounts(store))?)) } #[test] @@ -344,17 +340,17 @@ fn resource_borrow_import() -> Result<()> { use componentize_py::test::resource_borrow_import::{Host, HostThing}; impl HostThing for Ctx { - async fn new(&mut self, v: u32) -> Result> { + async fn new(&mut self, v: u32) -> wasmtime::Result> { Ok(self.ctx().table.push(ThingU32(v + 2))?) } - async fn drop(&mut self, this: Resource) -> Result<()> { + async fn drop(&mut self, this: Resource) -> wasmtime::Result<()> { Ok(self.ctx().table.delete(this).map(|_| ())?) } } impl Host for Ctx { - async fn foo(&mut self, this: Resource) -> Result { + async fn foo(&mut self, this: Resource) -> wasmtime::Result { Ok(self.ctx().table.get(&this)?.0 + 3) } } @@ -389,29 +385,29 @@ fn resource_with_lists() -> Result<()> { use componentize_py::test::resource_with_lists::{Host, HostThing}; impl HostThing for Ctx { - async fn new(&mut self, mut v: Vec) -> Result> { + async fn new(&mut self, mut v: Vec) -> wasmtime::Result> { v.extend(b" HostThing.new"); Ok(self.ctx().table.push(ThingList(v))?) } - async fn foo(&mut self, this: Resource) -> Result> { + async fn foo(&mut self, this: Resource) -> wasmtime::Result> { let mut v = self.ctx().table.get(&this)?.0.clone(); v.extend(b" HostThing.foo"); Ok(v) } - async fn bar(&mut self, this: Resource, mut v: Vec) -> Result<()> { + async fn bar(&mut self, this: Resource, mut v: Vec) -> wasmtime::Result<()> { v.extend(b" HostThing.bar"); self.ctx().table.get_mut(&this)?.0 = v; Ok(()) } - async fn baz(&mut self, mut v: Vec) -> Result> { + async fn baz(&mut self, mut v: Vec) -> wasmtime::Result> { v.extend(b" HostThing.baz"); Ok(v) } - async fn drop(&mut self, this: Resource) -> Result<()> { + async fn drop(&mut self, this: Resource) -> wasmtime::Result<()> { Ok(self.ctx().table.delete(this).map(|_| ())?) } } @@ -454,11 +450,11 @@ fn resource_aggregates() -> Result<()> { }; impl HostThing for Ctx { - async fn new(&mut self, v: u32) -> Result> { + async fn new(&mut self, v: u32) -> wasmtime::Result> { Ok(self.ctx().table.push(ThingU32(v + 2))?) } - async fn drop(&mut self, this: Resource) -> Result<()> { + async fn drop(&mut self, this: Resource) -> wasmtime::Result<()> { Ok(self.ctx().table.delete(this).map(|_| ())?) } } @@ -479,7 +475,7 @@ fn resource_aggregates() -> Result<()> { o2: Option>, result1: Result, ()>, result2: Result, ()>, - ) -> Result { + ) -> wasmtime::Result { let V1::Thing(v1) = v1; let V2::Thing(v2) = v2; Ok(self.ctx().table.get(&r1.thing)?.0 @@ -491,19 +487,21 @@ fn resource_aggregates() -> Result<()> { + self.ctx().table.get(&t2.0)?.0 + self.ctx().table.get(&v1)?.0 + self.ctx().table.get(&v2)?.0 - + l1.into_iter() - .try_fold(0, |n, v| Ok::<_, Error>(self.ctx().table.get(&v)?.0 + n))? - + l2.into_iter() - .try_fold(0, |n, v| Ok::<_, Error>(self.ctx().table.get(&v)?.0 + n))? - + o1.map(|v| Ok::<_, Error>(self.ctx().table.get(&v)?.0)) + + l1.into_iter().try_fold(0, |n, v| { + Ok::<_, wasmtime::Error>(self.ctx().table.get(&v)?.0 + n) + })? + + l2.into_iter().try_fold(0, |n, v| { + Ok::<_, wasmtime::Error>(self.ctx().table.get(&v)?.0 + n) + })? + + o1.map(|v| Ok::<_, wasmtime::Error>(self.ctx().table.get(&v)?.0)) .unwrap_or(Ok(0))? - + o2.map(|v| Ok::<_, Error>(self.ctx().table.get(&v)?.0)) + + o2.map(|v| Ok::<_, wasmtime::Error>(self.ctx().table.get(&v)?.0)) .unwrap_or(Ok(0))? + result1 - .map(|v| Ok::<_, Error>(self.ctx().table.get(&v)?.0)) + .map(|v| Ok::<_, wasmtime::Error>(self.ctx().table.get(&v)?.0)) .unwrap_or(Ok(0))? + result2 - .map(|v| Ok::<_, Error>(self.ctx().table.get(&v)?.0)) + .map(|v| Ok::<_, wasmtime::Error>(self.ctx().table.get(&v)?.0)) .unwrap_or(Ok(0))? + 3) } @@ -558,21 +556,21 @@ fn resource_alias() -> Result<()> { use componentize_py::test::resource_alias1::{Foo, Host, HostThing}; impl HostThing for Ctx { - async fn new(&mut self, s: String) -> Result> { + async fn new(&mut self, s: String) -> wasmtime::Result> { Ok(self.ctx().table.push(ThingString(s + " HostThing::new"))?) } - async fn get(&mut self, this: Resource) -> Result { + async fn get(&mut self, this: Resource) -> wasmtime::Result { Ok(format!("{} HostThing.get", self.ctx().table.get(&this)?.0)) } - async fn drop(&mut self, this: Resource) -> Result<()> { + async fn drop(&mut self, this: Resource) -> wasmtime::Result<()> { Ok(self.ctx().table.delete(this).map(|_| ())?) } } impl Host for Ctx { - async fn a(&mut self, f: Foo) -> Result>> { + async fn a(&mut self, f: Foo) -> wasmtime::Result>> { Ok(vec![f.thing]) } } @@ -582,7 +580,7 @@ fn resource_alias() -> Result<()> { use componentize_py::test::resource_alias2::{Bar, Foo, Host, Thing}; impl Host for Ctx { - async fn b(&mut self, f: Foo, g: Bar) -> Result>> { + async fn b(&mut self, f: Foo, g: Bar) -> wasmtime::Result>> { Ok(vec![f.thing, g.thing]) } } @@ -676,20 +674,24 @@ fn resource_floats() -> Result<()> { use resource_floats_imports::{Host, HostFloat}; impl HostFloat for Ctx { - async fn new(&mut self, v: f64) -> Result> { + async fn new(&mut self, v: f64) -> wasmtime::Result> { Ok(self.ctx().table.push(MyFloat(v + 2_f64))?) } - async fn get(&mut self, this: Resource) -> Result { + async fn get(&mut self, this: Resource) -> wasmtime::Result { Ok(self.ctx().table.get(&this)?.0 + 4_f64) } - async fn add(&mut self, a: Resource, b: f64) -> Result> { + async fn add( + &mut self, + a: Resource, + b: f64, + ) -> wasmtime::Result> { let a = self.ctx().table.get(&a)?.0; Ok(self.ctx().table.push(MyFloat(a + b + 6_f64))?) } - async fn drop(&mut self, this: Resource) -> Result<()> { + async fn drop(&mut self, this: Resource) -> wasmtime::Result<()> { Ok(self.ctx().table.delete(this).map(|_| ())?) } } @@ -701,15 +703,15 @@ fn resource_floats() -> Result<()> { use componentize_py::test::resource_floats::{Host, HostFloat}; impl HostFloat for Ctx { - async fn new(&mut self, v: f64) -> Result> { + async fn new(&mut self, v: f64) -> wasmtime::Result> { Ok(self.ctx().table.push(MyFloat(v + 1_f64))?) } - async fn get(&mut self, this: Resource) -> Result { + async fn get(&mut self, this: Resource) -> wasmtime::Result { Ok(self.ctx().table.get(&this)?.0 + 3_f64) } - async fn drop(&mut self, this: Resource) -> Result<()> { + async fn drop(&mut self, this: Resource) -> wasmtime::Result<()> { Ok(self.ctx().table.delete(this).map(|_| ())?) } } @@ -765,21 +767,24 @@ fn resource_borrow_in_record() -> Result<()> { use componentize_py::test::resource_borrow_in_record::{Foo, Host, HostThing}; impl HostThing for Ctx { - async fn new(&mut self, v: String) -> Result> { + async fn new(&mut self, v: String) -> wasmtime::Result> { Ok(self.ctx().table.push(ThingString(v + " HostThing::new"))?) } - async fn get(&mut self, this: Resource) -> Result { + async fn get(&mut self, this: Resource) -> wasmtime::Result { Ok(format!("{} HostThing.get", self.ctx().table.get(&this)?.0)) } - async fn drop(&mut self, this: Resource) -> Result<()> { + async fn drop(&mut self, this: Resource) -> wasmtime::Result<()> { Ok(self.ctx().table.delete(this).map(|_| ())?) } } impl Host for Ctx { - async fn test(&mut self, list: Vec) -> Result>> { + async fn test( + &mut self, + list: Vec, + ) -> wasmtime::Result>> { list.into_iter() .map(|foo| { let value = self.ctx().table.get(&foo.thing)?.0.clone(); @@ -839,7 +844,7 @@ fn resource_borrow_in_record() -> Result<()> { #[test] fn multiworld() -> Result<()> { impl foo_sdk::foo::sdk::foo_interface::Host for Ctx { - fn test(&mut self, s: String) -> Result { + fn test(&mut self, s: String) -> wasmtime::Result { Ok(format!("{s} HostFoo::test")) } } @@ -926,7 +931,7 @@ impl StreamProducer for VecProducer { _: StoreContextMut, mut destination: Destination, _: bool, - ) -> Poll> { + ) -> Poll> { let sleep = &mut self.as_mut().get_mut().sleep; task::ready!(sleep.as_mut().poll(cx)); *sleep = async {}.boxed(); @@ -939,10 +944,11 @@ impl StreamProducer for VecProducer { struct VecConsumer { destination: Arc>>, sleep: Pin + Send>>, + _tx: oneshot::Sender<()>, } impl VecConsumer { - fn new(destination: Arc>>, delay: bool) -> Self { + fn new(destination: Arc>>, delay: bool, tx: oneshot::Sender<()>) -> Self { Self { destination, sleep: if delay { @@ -950,6 +956,7 @@ impl VecConsumer { } else { async {}.boxed() }, + _tx: tx, } } } @@ -963,7 +970,7 @@ impl StreamConsumer for VecConsumer { store: StoreContextMut, mut source: Source, _: bool, - ) -> Poll> { + ) -> Poll> { let sleep = &mut self.as_mut().get_mut().sleep; task::ready!(sleep.as_mut().poll(cx)); *sleep = async {}.boxed(); @@ -992,19 +999,20 @@ fn test_echo_stream_u8(delay: bool) -> Result<()> { b"Beware the Jubjub bird, and shun\n\tThe frumious Bandersnatch!"; let stream = store.with(|store| { StreamReader::new(store, VecProducer::new(expected.to_vec(), delay)) - }); + })?; - let (stream, task) = world + let stream = world .componentize_py_test_streams_and_futures() .call_echo_stream_u8(store, stream) .await?; let received = Arc::new(Mutex::new(Vec::with_capacity(expected.len()))); + let (tx, rx) = oneshot::channel(); store.with(|store| { - stream.pipe(store, VecConsumer::new(received.clone(), delay)) - }); + stream.pipe(store, VecConsumer::new(received.clone(), delay, tx)) + })?; - task.block(store).await; + _ = rx.await; assert_eq!(expected, &received.lock().unwrap()[..]); @@ -1043,7 +1051,7 @@ impl FutureProducer for OptionProducer { cx: &mut Context<'_>, _: StoreContextMut, _: bool, - ) -> Poll>> { + ) -> Poll>> { let sleep = &mut self.as_mut().get_mut().sleep; task::ready!(sleep.as_mut().poll(cx)); *sleep = async {}.boxed(); @@ -1055,10 +1063,11 @@ impl FutureProducer for OptionProducer { struct OptionConsumer { destination: Arc>>, sleep: Pin + Send>>, + _tx: oneshot::Sender<()>, } impl OptionConsumer { - fn new(destination: Arc>>, delay: bool) -> Self { + fn new(destination: Arc>>, delay: bool, tx: oneshot::Sender<()>) -> Self { Self { destination, sleep: if delay { @@ -1066,6 +1075,7 @@ impl OptionConsumer { } else { async {}.boxed() }, + _tx: tx, } } } @@ -1079,7 +1089,7 @@ impl FutureConsumer for OptionConsumer { store: StoreContextMut, mut source: Source, _: bool, - ) -> Poll> { + ) -> Poll> { let sleep = &mut self.as_mut().get_mut().sleep; task::ready!(sleep.as_mut().poll(cx)); *sleep = async {}.boxed(); @@ -1110,19 +1120,20 @@ fn test_echo_future_string(delay: bool) -> Result<()> { store, OptionProducer::new(Some(expected.to_string()), delay), ) - }); + })?; - let (future, task) = world + let future = world .componentize_py_test_streams_and_futures() .call_echo_future_string(store, future) .await?; let received = Arc::new(Mutex::new(None::)); + let (tx, rx) = oneshot::channel(); store.with(|store| { - future.pipe(store, OptionConsumer::new(received.clone(), delay)) - }); + future.pipe(store, OptionConsumer::new(received.clone(), delay, tx)) + })?; - task.block(store).await; + _ = rx.await; assert_eq!( expected, @@ -1142,10 +1153,11 @@ struct OneAtATime { destination: Arc>>, sleep: Pin + Send>>, delay: bool, + _tx: oneshot::Sender<()>, } impl OneAtATime { - fn new(destination: Arc>>, delay: bool) -> Self { + fn new(destination: Arc>>, delay: bool, tx: oneshot::Sender<()>) -> Self { Self { destination, sleep: if delay { @@ -1154,6 +1166,7 @@ impl OneAtATime { async {}.boxed() }, delay, + _tx: tx, } } } @@ -1167,7 +1180,7 @@ impl StreamConsumer for OneAtATime { store: StoreContextMut, mut source: Source, _: bool, - ) -> Poll> { + ) -> Poll> { let delay = self.delay; let sleep = &mut self.as_mut().get_mut().sleep; task::ready!(sleep.as_mut().poll(cx)); @@ -1213,20 +1226,21 @@ fn test_short_reads(delay: bool) -> Result<()> { // one at a time, forcing us to retake ownership of the unwritten // items between writes. let stream = store - .with(|store| StreamReader::new(store, VecProducer::new(things, delay))); + .with(|store| StreamReader::new(store, VecProducer::new(things, delay)))?; - let (stream, task) = instance.call_short_reads(store, stream).await?; + let stream = instance.call_short_reads(store, stream).await?; let received_things = Arc::new(Mutex::new( Vec::::with_capacity(count), )); + let (tx, rx) = oneshot::channel(); // Read only one item at a time, forcing the sender to retake // ownership of any unwritten items. store.with(|store| { - stream.pipe(store, OneAtATime::new(received_things.clone(), delay)) - }); + stream.pipe(store, OneAtATime::new(received_things.clone(), delay, tx)) + })?; - task.block(store).await; + _ = rx.await; assert_eq!(count, received_things.lock().unwrap().len()); @@ -1253,7 +1267,7 @@ fn test_short_reads(delay: bool) -> Result<()> { } let mut received_strings = BTreeMap::new(); - while let Some((index, (string, _))) = futures.try_next().await? { + while let Some((index, string)) = futures.try_next().await? { received_strings.insert(index, string); } @@ -1299,17 +1313,17 @@ fn test_short_reads_host(delay: bool) -> Result<()> { async fn get( accessor: &Accessor, this: Resource, - ) -> Result { + ) -> wasmtime::Result { accessor.with(|mut store| Ok(store.get().table.get(&this)?.0.clone())) } } impl HostHostThing for Ctx { - async fn new(&mut self, v: String) -> Result> { + async fn new(&mut self, v: String) -> wasmtime::Result> { Ok(self.ctx().table.push(ThingString(v))?) } - async fn drop(&mut self, this: Resource) -> Result<()> { + async fn drop(&mut self, this: Resource) -> wasmtime::Result<()> { Ok(self.ctx().table.delete(this).map(|_| ())?) } } @@ -1333,20 +1347,21 @@ fn test_short_reads_host(delay: bool) -> Result<()> { // one at a time, forcing us to retake ownership of the unwritten // items between writes. let stream = store - .with(|store| StreamReader::new(store, VecProducer::new(things, delay))); + .with(|store| StreamReader::new(store, VecProducer::new(things, delay)))?; - let (stream, task) = instance.call_short_reads_host(store, stream).await?; + let stream = instance.call_short_reads_host(store, stream).await?; let received_things = Arc::new(Mutex::new( Vec::>::with_capacity(count), )); + let (tx, rx) = oneshot::channel(); // Read only one item at a time, forcing the sender to retake // ownership of any unwritten items. store.with(|store| { - stream.pipe(store, OneAtATime::new(received_things.clone(), delay)) - }); + stream.pipe(store, OneAtATime::new(received_things.clone(), delay, tx)) + })?; - task.block(store).await; + _ = rx.await; assert_eq!(count, received_things.lock().unwrap().len()); @@ -1393,24 +1408,25 @@ fn test_dropped_future_reader(delay: bool) -> Result<()> { let it = store .run_concurrent(async |store| { let expected = "Beware the Jubjub bird, and shun\n\tThe frumious Bandersnatch!"; - let ((mut rx1, rx2), task) = instance + let (mut rx1, rx2) = instance .call_dropped_future_reader(store, expected.into()) .await?; // Close the future without reading the value. This will // force the sender to retake ownership of the value it // tried to write. - rx1.close_with(store); + rx1.close_with(store)?; let received = Arc::new(Mutex::new(None::)); + let (tx, rx) = oneshot::channel(); store.with(|store| { - rx2.pipe(store, OptionConsumer::new(received.clone(), delay)) - }); + rx2.pipe(store, OptionConsumer::new(received.clone(), delay, tx)) + })?; - task.block(store).await; + _ = rx.await; let it = received.lock().unwrap().take().unwrap(); - assert_eq!(expected, &thing.call_get(store, it, 0).await?.0); + assert_eq!(expected, &thing.call_get(store, it, 0).await?); anyhow::Ok(it) }) @@ -1443,20 +1459,21 @@ fn test_dropped_future_reader_host(delay: bool) -> Result<()> { store .run_concurrent(async |store| { let expected = "Beware the Jubjub bird, and shun\n\tThe frumious Bandersnatch!"; - let ((mut rx1, rx2), task) = instance + let (mut rx1, rx2) = instance .call_dropped_future_reader_host(store, expected.into()) .await?; // Close the future without reading the value. This will // force the sender to retake ownership of the value it // tried to write. - rx1.close_with(store); + rx1.close_with(store)?; let received = Arc::new(Mutex::new(None::>)); + let (tx, rx) = oneshot::channel(); store.with(|store| { - rx2.pipe(store, OptionConsumer::new(received.clone(), delay)) - }); + rx2.pipe(store, OptionConsumer::new(received.clone(), delay, tx)) + })?; - task.block(store).await; + _ = rx.await; let it = store.with(|mut store| { anyhow::Ok( diff --git a/test-generator/src/lib.rs b/test-generator/src/lib.rs index a38d4ef..e52d1a4 100644 --- a/test-generator/src/lib.rs +++ b/test-generator/src/lib.rs @@ -679,7 +679,7 @@ pub fn generate() -> Result<()> { writeln!( &mut host_functions, - "async fn echo{test_index}(&mut self, {params}) -> Result<{result_type}> {{ Ok({result}) }}" + "async fn echo{test_index}(&mut self, {params}) -> wasmtime::Result<{result_type}> {{ Ok({result}) }}" ) .unwrap(); } diff --git a/tests/bindings.rs b/tests/bindings.rs index f40b482..4d39e3c 100644 --- a/tests/bindings.rs +++ b/tests/bindings.rs @@ -39,7 +39,7 @@ fn lint_cli_p3_bindings() -> anyhow::Result<()> { )?; let path = dir.path().join("cli-p3"); - generate_bindings(&path, "wasi:cli/command@0.3.0-rc-2026-01-06")?; + generate_bindings(&path, "wasi:cli/command@0.3.0-rc-2026-03-15")?; assert!(predicate::path::is_dir().eval(&path.join("wit_world"))); @@ -91,7 +91,7 @@ fn lint_http_p3_bindings() -> anyhow::Result<()> { )?; let path = dir.path().join("http-p3"); - generate_bindings(&path, "wasi:http/service@0.3.0-rc-2026-01-06")?; + generate_bindings(&path, "wasi:http/service@0.3.0-rc-2026-03-15")?; assert!(predicate::path::is_dir().eval(&path.join("wit_world"))); @@ -181,7 +181,7 @@ fn lint_tcp_p3_bindings() -> anyhow::Result<()> { )?; let path = dir.path().join("tcp-p3"); - generate_bindings(&path, "wasi:cli/command@0.3.0-rc-2026-01-06")?; + generate_bindings(&path, "wasi:cli/command@0.3.0-rc-2026-03-15")?; assert!(predicate::path::is_dir().eval(&path.join("wit_world"))); @@ -211,7 +211,7 @@ where Command::new(venv_path(path).join("pip")) .current_dir(path) - .args(["install", "mypy"]) + .args(["install", "mypy==1.13.0"]) .assert() .success(); @@ -230,7 +230,7 @@ fn venv_path(path: &Path) -> PathBuf { fn install_numpy(path: &Path) -> anyhow::Result<()> { let bytes = reqwest::blocking::get( - "https://github.com/dicej/wasi-wheels/releases/download/v0.0.1/numpy-wasi.tar.gz", + "https://github.com/dicej/wasi-wheels/releases/download/v0.0.2/numpy-wasi.tar.gz", )? .error_for_status()? .bytes()?; diff --git a/tests/componentize.rs b/tests/componentize.rs index 215f354..4fb4e96 100644 --- a/tests/componentize.rs +++ b/tests/componentize.rs @@ -1,6 +1,6 @@ use core::net::Ipv4Addr; use std::{ - io::Write, + io::{Read as _, Write as _}, path::{Path, PathBuf}, process::Stdio, thread::sleep, @@ -20,7 +20,7 @@ fn cli_example() -> anyhow::Result<()> { #[test] fn cli_p3_example() -> anyhow::Result<()> { - test_cli_example("cli-p3", "wasi:cli/command@0.3.0-rc-2026-01-06") + test_cli_example("cli-p3", "wasi:cli/command@0.3.0-rc-2026-03-15") } fn test_cli_example(name: &str, world: &str) -> anyhow::Result<()> { @@ -65,7 +65,7 @@ fn http_example() -> anyhow::Result<()> { #[test] fn http_p3_example() -> anyhow::Result<()> { - test_http_example("http-p3", "wasi:http/service@0.3.0-rc-2026-01-06", 8081) + test_http_example("http-p3", "wasi:http/service@0.3.0-rc-2026-03-15", 8081) } fn test_http_example(name: &str, world: &str, port: u16) -> anyhow::Result<()> { @@ -247,7 +247,7 @@ fn tcp_example() -> anyhow::Result<()> { #[test] fn tcp_p3_example() -> anyhow::Result<()> { - test_tcp_example("tcp-p3", "wasi:cli/command@0.3.0-rc-2026-01-06") + test_tcp_example("tcp-p3", "wasi:cli/command@0.3.0-rc-2026-03-15") } fn test_tcp_example(name: &str, world: &str) -> anyhow::Result<()> { @@ -291,6 +291,11 @@ fn test_tcp_example(name: &str, world: &str) -> anyhow::Result<()> { .spawn()?; let (mut stream, _) = listener.accept()?; + + let mut buffer = vec![0; 256]; + let count = stream.read(&mut buffer)?; + assert_eq!(String::from_utf8_lossy(&buffer[..count]), "hello, world!"); + stream.write_all(b"hello")?; let output = tcp_handle.wait_with_output()?; @@ -300,6 +305,8 @@ fn test_tcp_example(name: &str, world: &str) -> anyhow::Result<()> { "received: b'hello'\n" ); + assert!(output.status.success()); + Ok(()) } diff --git a/wit/deps/cli-0.3.0-rc-2026-02-09.wit b/wit/deps/cli-0.3.0-rc-2026-02-09.wit new file mode 100644 index 0000000..4f8bbf8 --- /dev/null +++ b/wit/deps/cli-0.3.0-rc-2026-02-09.wit @@ -0,0 +1,256 @@ +package wasi:cli@0.3.0-rc-2026-02-09; + +@since(version = 0.3.0-rc-2026-02-09) +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-02-09) + get-environment: func() -> list>; + + /// Get the POSIX-style arguments to the program. + @since(version = 0.3.0-rc-2026-02-09) + 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-02-09) + get-initial-cwd: func() -> option; +} + +@since(version = 0.3.0-rc-2026-02-09) +interface exit { + /// Exit the current instance and any linked instances. + @since(version = 0.3.0-rc-2026-02-09) + 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-02-09) +interface run { + /// Run the program. + @since(version = 0.3.0-rc-2026-02-09) + run: async func() -> result; +} + +@since(version = 0.3.0-rc-2026-02-09) +interface types { + @since(version = 0.3.0-rc-2026-02-09) + 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-02-09) +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-02-09) + read-via-stream: func() -> tuple, future>>; +} + +@since(version = 0.3.0-rc-2026-02-09) +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-02-09) + write-via-stream: func(data: stream) -> future>; +} + +@since(version = 0.3.0-rc-2026-02-09) +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-02-09) + 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-02-09) +interface terminal-input { + /// The input side of a terminal. + @since(version = 0.3.0-rc-2026-02-09) + 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-02-09) +interface terminal-output { + /// The output side of a terminal. + @since(version = 0.3.0-rc-2026-02-09) + resource terminal-output; +} + +/// An interface providing an optional `terminal-input` for stdin as a +/// link-time authority. +@since(version = 0.3.0-rc-2026-02-09) +interface terminal-stdin { + @since(version = 0.3.0-rc-2026-02-09) + 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-02-09) + 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-02-09) +interface terminal-stdout { + @since(version = 0.3.0-rc-2026-02-09) + 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-02-09) + 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-02-09) +interface terminal-stderr { + @since(version = 0.3.0-rc-2026-02-09) + 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-02-09) + get-terminal-stderr: func() -> option; +} + +@since(version = 0.3.0-rc-2026-02-09) +world imports { + @since(version = 0.3.0-rc-2026-02-09) + import environment; + @since(version = 0.3.0-rc-2026-02-09) + import exit; + @since(version = 0.3.0-rc-2026-02-09) + import types; + @since(version = 0.3.0-rc-2026-02-09) + import stdin; + @since(version = 0.3.0-rc-2026-02-09) + import stdout; + @since(version = 0.3.0-rc-2026-02-09) + import stderr; + @since(version = 0.3.0-rc-2026-02-09) + import terminal-input; + @since(version = 0.3.0-rc-2026-02-09) + import terminal-output; + @since(version = 0.3.0-rc-2026-02-09) + import terminal-stdin; + @since(version = 0.3.0-rc-2026-02-09) + import terminal-stdout; + @since(version = 0.3.0-rc-2026-02-09) + import terminal-stderr; + import wasi:clocks/types@0.3.0-rc-2026-02-09; + import wasi:clocks/monotonic-clock@0.3.0-rc-2026-02-09; + import wasi:clocks/system-clock@0.3.0-rc-2026-02-09; + @unstable(feature = clocks-timezone) + import wasi:clocks/timezone@0.3.0-rc-2026-02-09; + import wasi:filesystem/types@0.3.0-rc-2026-02-09; + import wasi:filesystem/preopens@0.3.0-rc-2026-02-09; + import wasi:sockets/types@0.3.0-rc-2026-02-09; + import wasi:sockets/ip-name-lookup@0.3.0-rc-2026-02-09; + import wasi:random/random@0.3.0-rc-2026-02-09; + import wasi:random/insecure@0.3.0-rc-2026-02-09; + import wasi:random/insecure-seed@0.3.0-rc-2026-02-09; +} +@since(version = 0.3.0-rc-2026-02-09) +world command { + @since(version = 0.3.0-rc-2026-02-09) + import environment; + @since(version = 0.3.0-rc-2026-02-09) + import exit; + @since(version = 0.3.0-rc-2026-02-09) + import types; + @since(version = 0.3.0-rc-2026-02-09) + import stdin; + @since(version = 0.3.0-rc-2026-02-09) + import stdout; + @since(version = 0.3.0-rc-2026-02-09) + import stderr; + @since(version = 0.3.0-rc-2026-02-09) + import terminal-input; + @since(version = 0.3.0-rc-2026-02-09) + import terminal-output; + @since(version = 0.3.0-rc-2026-02-09) + import terminal-stdin; + @since(version = 0.3.0-rc-2026-02-09) + import terminal-stdout; + @since(version = 0.3.0-rc-2026-02-09) + import terminal-stderr; + import wasi:clocks/types@0.3.0-rc-2026-02-09; + import wasi:clocks/monotonic-clock@0.3.0-rc-2026-02-09; + import wasi:clocks/system-clock@0.3.0-rc-2026-02-09; + @unstable(feature = clocks-timezone) + import wasi:clocks/timezone@0.3.0-rc-2026-02-09; + import wasi:filesystem/types@0.3.0-rc-2026-02-09; + import wasi:filesystem/preopens@0.3.0-rc-2026-02-09; + import wasi:sockets/types@0.3.0-rc-2026-02-09; + import wasi:sockets/ip-name-lookup@0.3.0-rc-2026-02-09; + import wasi:random/random@0.3.0-rc-2026-02-09; + import wasi:random/insecure@0.3.0-rc-2026-02-09; + import wasi:random/insecure-seed@0.3.0-rc-2026-02-09; + + @since(version = 0.3.0-rc-2026-02-09) + export run; +} diff --git a/wit/deps/cli-0.3.0-rc-2026-03-15.wit b/wit/deps/cli-0.3.0-rc-2026-03-15.wit new file mode 100644 index 0000000..8ba52c5 --- /dev/null +++ b/wit/deps/cli-0.3.0-rc-2026-03-15.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/wit/deps/clocks-0.3.0-rc-2026-02-09.wit b/wit/deps/clocks-0.3.0-rc-2026-02-09.wit new file mode 100644 index 0000000..71986ba --- /dev/null +++ b/wit/deps/clocks-0.3.0-rc-2026-02-09.wit @@ -0,0 +1,161 @@ +package wasi:clocks@0.3.0-rc-2026-02-09; + +/// This interface common types used throughout wasi:clocks. +@since(version = 0.3.0-rc-2026-02-09) +interface types { + /// A duration of time, in nanoseconds. + @since(version = 0.3.0-rc-2026-02-09) + 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-02-09) +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-02-09) + 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-02-09) + 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-02-09) + get-resolution: func() -> duration; + + /// Wait until the specified mark has occurred. + @since(version = 0.3.0-rc-2026-02-09) + wait-until: async func(when: mark); + + /// Wait for the specified duration to elapse. + @since(version = 0.3.0-rc-2026-02-09) + 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-02-09) +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-02-09) + 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-02-09) + 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-02-09) + 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-02-09) +world imports { + @since(version = 0.3.0-rc-2026-02-09) + import types; + @since(version = 0.3.0-rc-2026-02-09) + import monotonic-clock; + @since(version = 0.3.0-rc-2026-02-09) + import system-clock; + @unstable(feature = clocks-timezone) + import timezone; +} diff --git a/wit/deps/clocks-0.3.0-rc-2026-03-15.wit b/wit/deps/clocks-0.3.0-rc-2026-03-15.wit new file mode 100644 index 0000000..19fc4bc --- /dev/null +++ b/wit/deps/clocks-0.3.0-rc-2026-03-15.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/wit/deps/filesystem-0.3.0-rc-2026-02-09.wit b/wit/deps/filesystem-0.3.0-rc-2026-02-09.wit new file mode 100644 index 0000000..8fb8304 --- /dev/null +++ b/wit/deps/filesystem-0.3.0-rc-2026-02-09.wit @@ -0,0 +1,566 @@ +package wasi:filesystem@0.3.0-rc-2026-02-09; + +/// 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-02-09) +interface types { + @since(version = 0.3.0-rc-2026-02-09) + use wasi:clocks/system-clock@0.3.0-rc-2026-02-09.{instant}; + + /// File size or length of a region within a file. + @since(version = 0.3.0-rc-2026-02-09) + 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-02-09) + 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-2026-02-09) + 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-02-09) + 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-02-09) + 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-02-09) + type link-count = u64; + + /// File attributes. + /// + /// Note: This was called `filestat` in earlier versions of WASI. + @since(version = 0.3.0-rc-2026-02-09) + 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-02-09) + 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-02-09) + 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-02-09) + 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-2026-02-09) + 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-02-09) + 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-02-09) + 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-02-09) + 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-02-09) + 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-02-09) + 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-02-09) + 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-02-09) + 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-02-09) + 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-02-09) + 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-02-09) + 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-02-09) + 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-02-09) + 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-02-09) + sync: async func() -> result<_, error-code>; + /// Create a directory. + /// + /// Note: This is similar to `mkdirat` in POSIX. + @since(version = 0.3.0-rc-2026-02-09) + 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-02-09) + 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-02-09) + 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-02-09) + 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-02-09) + 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-02-09) + 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-02-09) + 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-02-09) + 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-02-09) + 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-02-09) + symlink-at: async func(old-path: string, 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-2026-02-09) + 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-02-09) + 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-02-09) + 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-02-09) + metadata-hash-at: async func(path-flags: path-flags, path: string) -> result; + } +} + +@since(version = 0.3.0-rc-2026-02-09) +interface preopens { + @since(version = 0.3.0-rc-2026-02-09) + use types.{descriptor}; + + /// Return the set of preopened directories, and their paths. + @since(version = 0.3.0-rc-2026-02-09) + get-directories: func() -> list>; +} + +@since(version = 0.3.0-rc-2026-02-09) +world imports { + @since(version = 0.3.0-rc-2026-02-09) + import wasi:clocks/types@0.3.0-rc-2026-02-09; + @since(version = 0.3.0-rc-2026-02-09) + import wasi:clocks/system-clock@0.3.0-rc-2026-02-09; + @since(version = 0.3.0-rc-2026-02-09) + import types; + @since(version = 0.3.0-rc-2026-02-09) + import preopens; +} diff --git a/wit/deps/filesystem-0.3.0-rc-2026-03-15.wit b/wit/deps/filesystem-0.3.0-rc-2026-03-15.wit new file mode 100644 index 0000000..697681f --- /dev/null +++ b/wit/deps/filesystem-0.3.0-rc-2026-03-15.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/wit/deps/http-0.3.0-rc-2026-02-09.wit b/wit/deps/http-0.3.0-rc-2026-02-09.wit new file mode 100644 index 0000000..442b18e --- /dev/null +++ b/wit/deps/http-0.3.0-rc-2026-02-09.wit @@ -0,0 +1,479 @@ +package wasi:http@0.3.0-rc-2026-02-09; + +/// 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-02-09) +interface types { + use wasi:clocks/types@0.3.0-rc-2026-02-09.{duration}; + + /// This type corresponds to HTTP standard Methods. + @since(version = 0.3.0-rc-2026-02-09) + 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-02-09) + variant scheme { + HTTP, + HTTPS, + other(string), + } + + /// Defines the case payload type for `DNS-error` above: + @since(version = 0.3.0-rc-2026-02-09) + 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-02-09) + 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-02-09) + 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-02-09) + 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-02-09) + 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 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-02-09) + 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, + } + + /// 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-02-09) + 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-02-09) + 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. + @since(version = 0.3.0-rc-2026-02-09) + 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, or if a header was forbidden. + 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. + 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. + 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-02-09) + type headers = fields; + + /// Trailers is an alias for Fields. + @since(version = 0.3.0-rc-2026-02-09) + type trailers = fields; + + /// Represents an HTTP Request. + @since(version = 0.3.0-rc-2026-02-09) + 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-02-09) + 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-02-09) + type status-code = u16; + + /// Represents an HTTP Response. + @since(version = 0.3.0-rc-2026-02-09) + 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-02-09) +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-02-09) +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-02-09) +world service { + import wasi:cli/types@0.3.0-rc-2026-02-09; + import wasi:cli/stdout@0.3.0-rc-2026-02-09; + import wasi:cli/stderr@0.3.0-rc-2026-02-09; + import wasi:cli/stdin@0.3.0-rc-2026-02-09; + import wasi:clocks/types@0.3.0-rc-2026-02-09; + import types; + import client; + import wasi:clocks/monotonic-clock@0.3.0-rc-2026-02-09; + import wasi:clocks/system-clock@0.3.0-rc-2026-02-09; + @unstable(feature = clocks-timezone) + import wasi:clocks/timezone@0.3.0-rc-2026-02-09; + import wasi:random/random@0.3.0-rc-2026-02-09; + import wasi:random/insecure@0.3.0-rc-2026-02-09; + import wasi:random/insecure-seed@0.3.0-rc-2026-02-09; + + 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-02-09) +world middleware { + import wasi:clocks/types@0.3.0-rc-2026-02-09; + import types; + import handler; + import wasi:cli/types@0.3.0-rc-2026-02-09; + import wasi:cli/stdout@0.3.0-rc-2026-02-09; + import wasi:cli/stderr@0.3.0-rc-2026-02-09; + import wasi:cli/stdin@0.3.0-rc-2026-02-09; + import client; + import wasi:clocks/monotonic-clock@0.3.0-rc-2026-02-09; + import wasi:clocks/system-clock@0.3.0-rc-2026-02-09; + @unstable(feature = clocks-timezone) + import wasi:clocks/timezone@0.3.0-rc-2026-02-09; + import wasi:random/random@0.3.0-rc-2026-02-09; + import wasi:random/insecure@0.3.0-rc-2026-02-09; + import wasi:random/insecure-seed@0.3.0-rc-2026-02-09; + + export handler; +} diff --git a/wit/deps/http-0.3.0-rc-2026-03-15.wit b/wit/deps/http-0.3.0-rc-2026-03-15.wit new file mode 100644 index 0000000..c1c1e68 --- /dev/null +++ b/wit/deps/http-0.3.0-rc-2026-03-15.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/wit/deps/random-0.3.0-rc-2026-02-09.wit b/wit/deps/random-0.3.0-rc-2026-02-09.wit new file mode 100644 index 0000000..521df6e --- /dev/null +++ b/wit/deps/random-0.3.0-rc-2026-02-09.wit @@ -0,0 +1,92 @@ +package wasi:random@0.3.0-rc-2026-02-09; + +/// 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-02-09) +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-02-09) + 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-02-09) +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-2026-02-09) + 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-2026-02-09) + 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-02-09) +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-2026-02-09) + 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-2026-02-09) + get-random-u64: func() -> u64; +} + +@since(version = 0.3.0-rc-2026-02-09) +world imports { + @since(version = 0.3.0-rc-2026-02-09) + import random; + @since(version = 0.3.0-rc-2026-02-09) + import insecure; + @since(version = 0.3.0-rc-2026-02-09) + import insecure-seed; +} diff --git a/wit/deps/random-0.3.0-rc-2026-03-15.wit b/wit/deps/random-0.3.0-rc-2026-03-15.wit new file mode 100644 index 0000000..026f44a --- /dev/null +++ b/wit/deps/random-0.3.0-rc-2026-03-15.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/wit/deps/sockets-0.3.0-rc-2026-02-09.wit b/wit/deps/sockets-0.3.0-rc-2026-02-09.wit new file mode 100644 index 0000000..aa9d4d7 --- /dev/null +++ b/wit/deps/sockets-0.3.0-rc-2026-02-09.wit @@ -0,0 +1,758 @@ +package wasi:sockets@0.3.0-rc-2026-02-09; + +@since(version = 0.3.0-rc-2026-02-09) +interface types { + @since(version = 0.3.0-rc-2026-02-09) + use wasi:clocks/types@0.3.0-rc-2026-02-09.{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-2026-02-09) + 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-2026-02-09) + 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-02-09) + type ipv4-address = tuple; + + @since(version = 0.3.0-rc-2026-02-09) + type ipv6-address = tuple; + + @since(version = 0.3.0-rc-2026-02-09) + variant ip-address { + ipv4(ipv4-address), + ipv6(ipv6-address), + } + + @since(version = 0.3.0-rc-2026-02-09) + record ipv4-socket-address { + /// sin_port + port: u16, + /// sin_addr + address: ipv4-address, + } + + @since(version = 0.3.0-rc-2026-02-09) + 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-02-09) + 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-2026-02-09) + 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-2026-02-09) + 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-2026-02-09) + 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-2026-02-09) + 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-02-09) + 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-2026-02-09) + send: func(data: stream) -> future>; + /// 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-2026-02-09) + 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-02-09) + 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-02-09) + 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-02-09) + 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-02-09) + 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-02-09) + 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-02-09) + get-keep-alive-enabled: func() -> result; + @since(version = 0.3.0-rc-2026-02-09) + 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-2026-02-09) + get-keep-alive-idle-time: func() -> result; + @since(version = 0.3.0-rc-2026-02-09) + 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-2026-02-09) + get-keep-alive-interval: func() -> result; + @since(version = 0.3.0-rc-2026-02-09) + 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-2026-02-09) + get-keep-alive-count: func() -> result; + @since(version = 0.3.0-rc-2026-02-09) + 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-02-09) + get-hop-limit: func() -> result; + @since(version = 0.3.0-rc-2026-02-09) + 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-2026-02-09) + get-receive-buffer-size: func() -> result; + @since(version = 0.3.0-rc-2026-02-09) + set-receive-buffer-size: func(value: u64) -> result<_, error-code>; + @since(version = 0.3.0-rc-2026-02-09) + get-send-buffer-size: func() -> result; + @since(version = 0.3.0-rc-2026-02-09) + set-send-buffer-size: func(value: u64) -> result<_, error-code>; + } + + /// A UDP socket handle. + @since(version = 0.3.0-rc-2026-02-09) + 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-02-09) + 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-02-09) + 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-02-09) + 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-2026-02-09) + 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-2026-02-09) + 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-02-09) + 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-02-09) + 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-02-09) + 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-02-09) + 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-02-09) + get-unicast-hop-limit: func() -> result; + @since(version = 0.3.0-rc-2026-02-09) + 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-2026-02-09) + get-receive-buffer-size: func() -> result; + @since(version = 0.3.0-rc-2026-02-09) + set-receive-buffer-size: func(value: u64) -> result<_, error-code>; + @since(version = 0.3.0-rc-2026-02-09) + get-send-buffer-size: func() -> result; + @since(version = 0.3.0-rc-2026-02-09) + set-send-buffer-size: func(value: u64) -> result<_, error-code>; + } +} + +@since(version = 0.3.0-rc-2026-02-09) +interface ip-name-lookup { + @since(version = 0.3.0-rc-2026-02-09) + use types.{ip-address}; + + /// Lookup error codes. + @since(version = 0.3.0-rc-2026-02-09) + 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-2026-02-09) + resolve-addresses: async func(name: string) -> result, error-code>; +} + +@since(version = 0.3.0-rc-2026-02-09) +world imports { + @since(version = 0.3.0-rc-2026-02-09) + import wasi:clocks/types@0.3.0-rc-2026-02-09; + @since(version = 0.3.0-rc-2026-02-09) + import types; + @since(version = 0.3.0-rc-2026-02-09) + import ip-name-lookup; +} diff --git a/wit/deps/sockets-0.3.0-rc-2026-03-15.wit b/wit/deps/sockets-0.3.0-rc-2026-03-15.wit new file mode 100644 index 0000000..cde2e4d --- /dev/null +++ b/wit/deps/sockets-0.3.0-rc-2026-03-15.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; +}