diff --git a/.github/workflows/python-ci.yml b/.github/workflows/python-ci.yml index c4aae64..88a12df 100644 --- a/.github/workflows/python-ci.yml +++ b/.github/workflows/python-ci.yml @@ -15,16 +15,6 @@ jobs: runs-on: forge-amd64-medium steps: - uses: actions/checkout@v4 - - name: Get wasiless submodule - # Org-internal submodules are very tricky to check out. - env: - SSHK: ${{ secrets.WASILESS_REPO_READ_KEY }} - run: | - mkdir -p $HOME/.ssh - echo "$SSHK" > $HOME/.ssh/ssh.key - chmod 600 $HOME/.ssh/ssh.key - export GIT_SSH_COMMAND="ssh -i $HOME/.ssh/ssh.key" - git submodule update --init --recursive - name: Set up Rust toolchain uses: actions-rust-lang/setup-rust-toolchain@v1 with: diff --git a/.gitmodules b/.gitmodules index 233453b..8b13789 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1 @@ -[submodule "crates/wasiless"] - path = crates/wasiless - url = git@github.com:fastly/wasiless.git + diff --git a/crates/wasiless b/crates/wasiless deleted file mode 160000 index 4c9ac50..0000000 --- a/crates/wasiless +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 4c9ac507bae44e7964c0f689cb67c0f505e210a7 diff --git a/crates/wasiless/.cargo/config.toml b/crates/wasiless/.cargo/config.toml new file mode 100644 index 0000000..435ed75 --- /dev/null +++ b/crates/wasiless/.cargo/config.toml @@ -0,0 +1,2 @@ +[build] +target = "wasm32-unknown-unknown" \ No newline at end of file diff --git a/crates/wasiless/.gitignore b/crates/wasiless/.gitignore new file mode 100644 index 0000000..c7b9bef --- /dev/null +++ b/crates/wasiless/.gitignore @@ -0,0 +1,3 @@ +/target +/wasiless.rs +/wasiless.wasm diff --git a/crates/wasiless/Cargo.lock b/crates/wasiless/Cargo.lock new file mode 100644 index 0000000..4146831 --- /dev/null +++ b/crates/wasiless/Cargo.lock @@ -0,0 +1,433 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anyhow" +version = "1.0.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" + +[[package]] +name = "bitflags" +version = "2.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "id-arena" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25a2bc672d1148e28034f176e01fffebb08b35768468cc954630da77a1449005" + +[[package]] +name = "indexmap" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "206a8042aec68fa4a62e8d3f7aa4ceb508177d9324faf261e1959e495b7a1921" +dependencies = [ + "equivalent", + "hashbrown", + "serde", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "log" +version = "0.4.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" + +[[package]] +name = "memchr" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "serde" +version = "1.0.224" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6aaeb1e94f53b16384af593c71e20b095e958dab1d26939c1b70645c5cfbcc0b" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.224" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32f39390fa6346e24defbcdd3d9544ba8a19985d0af74df8501fbfe9a64341ab" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.224" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ff78ab5e8561c9a675bfc1785cb07ae721f0ee53329a595cefd8c04c2ac4e0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", + "serde_core", +] + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "syn" +version = "2.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "wasiless" +version = "0.1.0" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-encoder" +version = "0.239.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be00faa2b4950c76fe618c409d2c3ea5a3c9422013e079482d78544bb2d184c" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.239.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20b3ec880a9ac69ccd92fbdbcf46ee833071cf09f82bb005b2327c7ae6025ae2" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.239.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c9d90bb93e764f6beabf1d02028c70a2156a6583e63ac4218dd07ef733368b0" +dependencies = [ + "bitflags", + "hashbrown", + "indexmap", + "semver", +] + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +dependencies = [ + "bitflags", + "futures", + "once_cell", + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cabd629f94da277abc739c71353397046401518efb2c707669f805205f0b9890" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a4232e841089fa5f3c4fc732a92e1c74e1a3958db3b12f1de5934da2027f1f4" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0d4698c2913d8d9c2b220d116409c3f51a7aa8d7765151b886918367179ee9" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.239.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a866b19dba2c94d706ec58c92a4c62ab63e482b4c935d2a085ac94caecb136" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.239.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55c92c939d667b7bf0c6bf2d1f67196529758f99a2a45a3355cc56964fd5315d" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] diff --git a/crates/wasiless/Cargo.toml b/crates/wasiless/Cargo.toml new file mode 100644 index 0000000..bd9e937 --- /dev/null +++ b/crates/wasiless/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "wasiless" +version = "0.1.0" +edition = "2024" +description = "Minimal or trapping implementations of WASI interfaces, allowing ports from non-WASI environments to link and even sometimes run" + +[dependencies] +wit-bindgen = "0.46.0" + +[lib] +crate-type = ["cdylib"] diff --git a/crates/wasiless/Makefile b/crates/wasiless/Makefile new file mode 100755 index 0000000..5ed2069 --- /dev/null +++ b/crates/wasiless/Makefile @@ -0,0 +1,9 @@ +wasiless.wasm: target/wasm32-unknown-unknown/debug/wasiless.wasm + wasm-tools component new target/wasm32-unknown-unknown/release/wasiless.wasm -o wasiless.wasm + +target/wasm32-unknown-unknown/debug/wasiless.wasm: $(shell find src -name '*.rs') + cargo build --release + +# `wasm-tools component wit some_componentize_py_output.wasm --out-dir wit` +# handily extracts the wit for a new version of Python/componentize-py into this +# tree. \ No newline at end of file diff --git a/crates/wasiless/README.md b/crates/wasiless/README.md new file mode 100644 index 0000000..2d7e9e9 --- /dev/null +++ b/crates/wasiless/README.md @@ -0,0 +1,34 @@ +# wasiless + +Wasiless is a WebAssembly component that provides minimal or trapping implementations of all WASI interfaces, meant to allow the porting of dynamic runtimes like CPython which expect a normal OS, with normal affordances like filesystems and sockets. Build CPython (or some other runtime) as a component, satisfy its imports with wasiless, and you should be able to run it in an environment that provides only a subset of WASI, like [Viceroy](https://github.com/fastly/Viceroy). + +## Build + +Build wasiless as a WASIp2 component using `make`. + +## Use + +Here is an example composition of wasiless and a Python component (built using componentize-py) for use with Viceroy: + +``` +package fastly:python-wasiless; + +// Instantiate wasiless to satisfy irrelevant WASI interfaces: +let wasiless = new fastly:wasiless { ... }; + +// Instantiate the Python component: +let app = new app:component { ...wasiless, ... }; + +// Export only the HTTP handler, not the extraneous `exports` bundle: +export app["fastly:compute/http-incoming"]; +``` + +To apply this, save it as `wrap_app_in_wasiless.wac`, then invoke wac like... + +``` +wac compose --dep fastly:wasiless=wasiless.wasm --dep app:component=python_app.wasm -o composed.wasm wrap_app_in_wasiless.wac +``` + +## Caveats and philosophy + +Many of wasiless’ functions panic immediately. This is a nonissue if they are never actually called, which appears to be the common case. Where this is not true, we strive instead to return error codes like `ENOTSUP`, which allows more graceful recovery or error reporting by the guest language (e.g. Python tracebacks). diff --git a/crates/wasiless/src/bindings.rs b/crates/wasiless/src/bindings.rs new file mode 100644 index 0000000..5f79300 --- /dev/null +++ b/crates/wasiless/src/bindings.rs @@ -0,0 +1,5 @@ +wit_bindgen::generate!({ + world: "wasiless", + path: "wit", + generate_all, +}); diff --git a/crates/wasiless/src/cli.rs b/crates/wasiless/src/cli.rs new file mode 100644 index 0000000..d1e8919 --- /dev/null +++ b/crates/wasiless/src/cli.rs @@ -0,0 +1,38 @@ +use crate::Wasiless; +use crate::bindings::exports::wasi::cli::terminal_input::{ + self, GuestTerminalInput, TerminalInput, +}; +use crate::bindings::exports::wasi::cli::terminal_output::{ + self, GuestTerminalOutput, TerminalOutput, +}; +use crate::bindings::exports::wasi::cli::{terminal_stderr, terminal_stdin, terminal_stdout}; + +impl GuestTerminalInput for TerminalInput {} + +impl terminal_input::Guest for Wasiless { + type TerminalInput = TerminalInput; +} + +impl GuestTerminalOutput for TerminalOutput {} + +impl terminal_output::Guest for Wasiless { + type TerminalOutput = TerminalOutput; +} + +impl terminal_stdin::Guest for Wasiless { + fn get_terminal_stdin() -> Option<::TerminalInput> { + None + } +} + +impl terminal_stdout::Guest for Wasiless { + fn get_terminal_stdout() -> Option<::TerminalOutput> { + None + } +} + +impl terminal_stderr::Guest for Wasiless { + fn get_terminal_stderr() -> Option<::TerminalOutput> { + None + } +} diff --git a/crates/wasiless/src/filesystem.rs b/crates/wasiless/src/filesystem.rs new file mode 100644 index 0000000..1fb0d87 --- /dev/null +++ b/crates/wasiless/src/filesystem.rs @@ -0,0 +1,176 @@ +use crate::Wasiless; +use crate::bindings::exports::wasi::filesystem::{ + self, + types::{ + Advice, Descriptor, DescriptorBorrow, DescriptorFlags, DescriptorStat, DescriptorType, + DirectoryEntry, DirectoryEntryStream, Error, ErrorCode, Filesize, GuestDescriptor, + GuestDirectoryEntryStream, MetadataHashValue, NewTimestamp, OpenFlags, PathFlags, + }, +}; +use crate::bindings::wasi::io::streams::{InputStream, OutputStream}; + +impl GuestDescriptor for Descriptor { + fn read_via_stream(&self, _offset: Filesize) -> Result { + Err(ErrorCode::Unsupported) + } + + fn write_via_stream(&self, _offset: Filesize) -> Result { + Err(ErrorCode::Unsupported) + } + + fn append_via_stream(&self) -> Result { + Err(ErrorCode::Unsupported) + } + + fn advise( + &self, + _offset: Filesize, + _length: Filesize, + _advice: Advice, + ) -> Result<(), ErrorCode> { + Err(ErrorCode::Unsupported) + } + + fn sync_data(&self) -> Result<(), ErrorCode> { + Err(ErrorCode::Unsupported) + } + + fn get_flags(&self) -> Result { + Err(ErrorCode::Unsupported) + } + + fn get_type(&self) -> Result { + Err(ErrorCode::Unsupported) + } + + fn set_size(&self, _size: Filesize) -> Result<(), ErrorCode> { + Err(ErrorCode::Unsupported) + } + + fn set_times( + &self, + _data_access_timestamp: NewTimestamp, + _data_modification_timestamp: NewTimestamp, + ) -> Result<(), ErrorCode> { + Err(ErrorCode::Unsupported) + } + + fn read(&self, _length: Filesize, _offset: Filesize) -> Result<(Vec, bool), ErrorCode> { + Err(ErrorCode::Unsupported) + } + + fn write(&self, _buffer: Vec, _offset: Filesize) -> Result { + Err(ErrorCode::Unsupported) + } + + fn read_directory(&self) -> Result { + Err(ErrorCode::Unsupported) + } + + fn sync(&self) -> Result<(), ErrorCode> { + Err(ErrorCode::Unsupported) + } + + fn create_directory_at(&self, _path: String) -> Result<(), ErrorCode> { + Err(ErrorCode::Unsupported) + } + + fn stat(&self) -> Result { + Err(ErrorCode::Unsupported) + } + + fn stat_at(&self, _path_flags: PathFlags, _path: String) -> Result { + Err(ErrorCode::Unsupported) + } + + fn set_times_at( + &self, + _path_flags: PathFlags, + _path: String, + _data_access_timestamp: NewTimestamp, + _data_modification_timestamp: NewTimestamp, + ) -> Result<(), ErrorCode> { + Err(ErrorCode::Unsupported) + } + + fn link_at( + &self, + _old_path_flags: PathFlags, + _old_path: String, + _new_descriptor: DescriptorBorrow<'_>, + _new_path: String, + ) -> Result<(), ErrorCode> { + Err(ErrorCode::Unsupported) + } + + fn open_at( + &self, + _path_flags: PathFlags, + _path: String, + _open_flags: OpenFlags, + _flags: DescriptorFlags, + ) -> Result { + Err(ErrorCode::Unsupported) + } + + fn readlink_at(&self, _path: String) -> Result { + Err(ErrorCode::Unsupported) + } + + fn remove_directory_at(&self, _path: String) -> Result<(), ErrorCode> { + Err(ErrorCode::Unsupported) + } + + fn rename_at( + &self, + _old_path: String, + _new_descriptor: DescriptorBorrow<'_>, + _new_path: String, + ) -> Result<(), ErrorCode> { + Err(ErrorCode::Unsupported) + } + + fn symlink_at(&self, _old_path: String, _new_path: String) -> Result<(), ErrorCode> { + Err(ErrorCode::Unsupported) + } + + fn unlink_file_at(&self, _path: String) -> Result<(), ErrorCode> { + Err(ErrorCode::Unsupported) + } + + fn is_same_object(&self, _other: DescriptorBorrow<'_>) -> bool { + false // arbitrary + } + + fn metadata_hash(&self) -> Result { + Err(ErrorCode::Unsupported) + } + + fn metadata_hash_at( + &self, + _path_flags: PathFlags, + _path: String, + ) -> Result { + Err(ErrorCode::Unsupported) + } +} + +impl GuestDirectoryEntryStream for DirectoryEntryStream { + fn read_directory_entry(&self) -> Result, ErrorCode> { + Err(ErrorCode::Unsupported) + } +} + +impl filesystem::types::Guest for Wasiless { + type Descriptor = Descriptor; + type DirectoryEntryStream = DirectoryEntryStream; + fn filesystem_error_code(_err: &Error) -> Option { + None + } +} + +impl filesystem::preopens::Guest for Wasiless { + fn get_directories() -> Vec<(Descriptor, String)> { + Vec::new() + } +} diff --git a/crates/wasiless/src/lib.rs b/crates/wasiless/src/lib.rs new file mode 100644 index 0000000..ba1b19d --- /dev/null +++ b/crates/wasiless/src/lib.rs @@ -0,0 +1,21 @@ +// General philosophy thus far: Avoid returning error conditions; appear to +// succeed. But lie as little as possible beyond that: IO read and write +// routines claim 0 bytes were written, "successfully". This is in service of +// creating as little surprise for the caller as possible. Keep in mind this +// philosophy may be proven unhelpful through actual experience with the +// behavior of real-world clients. It may be helpful (and even less surprising) +// to crash as early as possible. + +mod bindings; +mod cli; +mod filesystem; +mod random; +mod sockets; + +use bindings::export; + +/// Wasm component implementing WASI with as little functionality as possible +/// without trapping +struct Wasiless; + +export!(Wasiless with_types_in bindings); diff --git a/crates/wasiless/src/random.rs b/crates/wasiless/src/random.rs new file mode 100644 index 0000000..678b537 --- /dev/null +++ b/crates/wasiless/src/random.rs @@ -0,0 +1,20 @@ +use crate::Wasiless; +use crate::bindings::exports::wasi::random; + +impl random::insecure::Guest for Wasiless { + #[allow(unused_variables)] + fn get_insecure_random_bytes(len: u64) -> Vec { + unreachable!() + } + #[allow(unused_variables)] + fn get_insecure_random_u64() -> u64 { + unreachable!() + } +} + +impl random::insecure_seed::Guest for Wasiless { + #[allow(unused_variables)] + fn insecure_seed() -> (u64, u64) { + unreachable!() + } +} diff --git a/crates/wasiless/src/sockets.rs b/crates/wasiless/src/sockets.rs new file mode 100644 index 0000000..acd2b25 --- /dev/null +++ b/crates/wasiless/src/sockets.rs @@ -0,0 +1,281 @@ +use crate::Wasiless; +use crate::bindings::exports::wasi::sockets::instance_network; +use crate::bindings::exports::wasi::sockets::ip_name_lookup; +use crate::bindings::exports::wasi::sockets::network::{ + self, ErrorCode, GuestNetwork, IpAddressFamily, IpSocketAddress, Network, NetworkBorrow, +}; +use crate::bindings::exports::wasi::sockets::tcp::{self, GuestTcpSocket, TcpSocket}; +use crate::bindings::exports::wasi::sockets::tcp_create_socket; +use crate::bindings::exports::wasi::sockets::udp::{ + self, GuestIncomingDatagramStream, GuestOutgoingDatagramStream, GuestUdpSocket, + IncomingDatagram, IncomingDatagramStream, OutgoingDatagram, OutgoingDatagramStream, UdpSocket, +}; +use crate::bindings::exports::wasi::sockets::udp_create_socket; +use crate::bindings::wasi::io::poll::Pollable; + +impl GuestNetwork for Network {} + +impl network::Guest for Wasiless { + type Network = Network; +} + +impl instance_network::Guest for Wasiless { + fn instance_network() -> Network { + unreachable!() + } +} + +impl GuestUdpSocket for UdpSocket { + fn start_bind( + &self, + _network: NetworkBorrow, + _local_address: IpSocketAddress, + ) -> Result<(), ErrorCode> { + unimplemented!() + } + + fn finish_bind(&self) -> Result<(), ErrorCode> { + unimplemented!() + } + + fn stream( + &self, + _remote_address: Option, + ) -> Result<(IncomingDatagramStream, OutgoingDatagramStream), ErrorCode> { + unimplemented!() + } + + fn local_address(&self) -> Result { + unreachable!() + } + + fn remote_address(&self) -> Result { + unreachable!() + } + + fn address_family(&self) -> IpAddressFamily { + unreachable!() + } + + fn unicast_hop_limit(&self) -> Result { + unreachable!() + } + + fn set_unicast_hop_limit(&self, _value: u8) -> Result<(), ErrorCode> { + unreachable!() + } + + fn receive_buffer_size(&self) -> Result { + unreachable!() + } + + fn set_receive_buffer_size(&self, _value: u64) -> Result<(), ErrorCode> { + unreachable!() + } + + fn send_buffer_size(&self) -> Result { + unreachable!() + } + + fn set_send_buffer_size(&self, _value: u64) -> Result<(), ErrorCode> { + unreachable!() + } + + fn subscribe(&self) -> Pollable { + unreachable!() + } +} + +impl GuestIncomingDatagramStream for IncomingDatagramStream { + fn receive(&self, _max_results: u64) -> Result, ErrorCode> { + unreachable!() + } + + fn subscribe(&self) -> Pollable { + unreachable!() + } +} + +impl GuestOutgoingDatagramStream for OutgoingDatagramStream { + fn check_send(&self) -> Result { + unreachable!() + } + fn send(&self, _datagrams: Vec) -> Result { + unreachable!() + } + fn subscribe(&self) -> Pollable { + unreachable!() + } +} + +impl udp::Guest for Wasiless { + type UdpSocket = UdpSocket; + type IncomingDatagramStream = IncomingDatagramStream; + type OutgoingDatagramStream = OutgoingDatagramStream; +} + +impl udp_create_socket::Guest for Wasiless { + fn create_udp_socket(_address_family: IpAddressFamily) -> Result { + unimplemented!() + } +} + +impl GuestTcpSocket for TcpSocket { + fn start_bind( + &self, + _network: tcp::NetworkBorrow<'_>, + _local_address: tcp::IpSocketAddress, + ) -> Result<(), tcp::ErrorCode> { + unreachable!() + } + + fn finish_bind(&self) -> Result<(), tcp::ErrorCode> { + unreachable!() + } + + fn start_connect( + &self, + _network: tcp::NetworkBorrow<'_>, + _remote_address: tcp::IpSocketAddress, + ) -> Result<(), tcp::ErrorCode> { + unreachable!() + } + + fn finish_connect(&self) -> Result<(tcp::InputStream, tcp::OutputStream), tcp::ErrorCode> { + unreachable!() + } + + fn start_listen(&self) -> Result<(), tcp::ErrorCode> { + unreachable!() + } + + fn finish_listen(&self) -> Result<(), tcp::ErrorCode> { + unreachable!() + } + + fn accept( + &self, + ) -> Result<(tcp::TcpSocket, tcp::InputStream, tcp::OutputStream), tcp::ErrorCode> { + unreachable!() + } + + fn local_address(&self) -> Result { + unreachable!() + } + + fn remote_address(&self) -> Result { + unreachable!() + } + + fn is_listening(&self) -> bool { + unreachable!() + } + + fn address_family(&self) -> tcp::IpAddressFamily { + unreachable!() + } + + fn set_listen_backlog_size(&self, _value: u64) -> Result<(), tcp::ErrorCode> { + unreachable!() + } + + fn keep_alive_enabled(&self) -> Result { + unreachable!() + } + + fn set_keep_alive_enabled(&self, _value: bool) -> Result<(), tcp::ErrorCode> { + unreachable!() + } + + fn keep_alive_idle_time(&self) -> Result { + unreachable!() + } + + fn set_keep_alive_idle_time(&self, _value: tcp::Duration) -> Result<(), tcp::ErrorCode> { + unreachable!() + } + + fn keep_alive_interval(&self) -> Result { + unreachable!() + } + + fn set_keep_alive_interval(&self, _value: tcp::Duration) -> Result<(), tcp::ErrorCode> { + unreachable!() + } + + fn keep_alive_count(&self) -> Result { + unreachable!() + } + + fn set_keep_alive_count(&self, _value: u32) -> Result<(), tcp::ErrorCode> { + unreachable!() + } + + fn hop_limit(&self) -> Result { + unreachable!() + } + + fn set_hop_limit(&self, _value: u8) -> Result<(), tcp::ErrorCode> { + unreachable!() + } + + fn receive_buffer_size(&self) -> Result { + unreachable!() + } + + fn set_receive_buffer_size(&self, _value: u64) -> Result<(), tcp::ErrorCode> { + unreachable!() + } + + fn send_buffer_size(&self) -> Result { + unreachable!() + } + + fn set_send_buffer_size(&self, _value: u64) -> Result<(), tcp::ErrorCode> { + unreachable!() + } + + fn subscribe(&self) -> tcp::Pollable { + unreachable!() + } + + fn shutdown(&self, _shutdown_type: tcp::ShutdownType) -> Result<(), tcp::ErrorCode> { + unreachable!() + } +} + +impl tcp::Guest for Wasiless { + type TcpSocket = TcpSocket; +} + +impl tcp_create_socket::Guest for Wasiless { + fn create_tcp_socket( + _address_family: tcp_create_socket::IpAddressFamily, + ) -> Result { + unreachable!() + } +} + +impl ip_name_lookup::GuestResolveAddressStream for Wasiless { + #[allow(unused_variables)] + fn resolve_next_address( + &self, + ) -> Result, ip_name_lookup::ErrorCode> { + unreachable!() + } + #[allow(unused_variables)] + fn subscribe(&self) -> ip_name_lookup::Pollable { + unreachable!() + } +} + +impl ip_name_lookup::Guest for Wasiless { + type ResolveAddressStream = Wasiless; + #[allow(unused_variables)] + fn resolve_addresses( + network: ip_name_lookup::NetworkBorrow<'_>, + name: String, + ) -> Result { + unreachable!() + } +} diff --git a/crates/wasiless/wit/deps/cli.wit b/crates/wasiless/wit/deps/cli.wit new file mode 100644 index 0000000..5f1811b --- /dev/null +++ b/crates/wasiless/wit/deps/cli.wit @@ -0,0 +1,28 @@ +package wasi:cli@0.2.0; + +interface terminal-input { + resource terminal-input; +} + +interface terminal-output { + resource terminal-output; +} + +interface terminal-stdin { + use terminal-input.{terminal-input}; + + get-terminal-stdin: func() -> option; +} + +interface terminal-stdout { + use terminal-output.{terminal-output}; + + get-terminal-stdout: func() -> option; +} + +interface terminal-stderr { + use terminal-output.{terminal-output}; + + get-terminal-stderr: func() -> option; +} + diff --git a/crates/wasiless/wit/deps/clocks.wit b/crates/wasiless/wit/deps/clocks.wit new file mode 100644 index 0000000..7fb41eb --- /dev/null +++ b/crates/wasiless/wit/deps/clocks.wit @@ -0,0 +1,29 @@ +package wasi:clocks@0.2.6; + +interface wall-clock { + record datetime { + seconds: u64, + nanoseconds: u32, + } + + now: func() -> datetime; + + resolution: func() -> datetime; +} + +interface monotonic-clock { + use wasi:io/poll@0.2.6.{pollable}; + + type instant = u64; + + type duration = u64; + + now: func() -> instant; + + resolution: func() -> duration; + + subscribe-instant: func(when: instant) -> pollable; + + subscribe-duration: func(when: duration) -> pollable; +} + diff --git a/crates/wasiless/wit/deps/filesystem.wit b/crates/wasiless/wit/deps/filesystem.wit new file mode 100644 index 0000000..9a2f885 --- /dev/null +++ b/crates/wasiless/wit/deps/filesystem.wit @@ -0,0 +1,159 @@ +package wasi:filesystem@0.2.0; + +interface types { + use wasi:io/streams@0.2.6.{input-stream, output-stream}; + use wasi:clocks/wall-clock@0.2.6.{datetime}; + use wasi:io/streams@0.2.6.{error}; + + resource descriptor { + read-via-stream: func(offset: filesize) -> result; + write-via-stream: func(offset: filesize) -> result; + append-via-stream: func() -> result; + advise: func(offset: filesize, length: filesize, advice: advice) -> result<_, error-code>; + sync-data: func() -> result<_, error-code>; + get-flags: func() -> result; + get-type: func() -> result; + set-size: func(size: filesize) -> result<_, error-code>; + set-times: func(data-access-timestamp: new-timestamp, data-modification-timestamp: new-timestamp) -> result<_, error-code>; + read: func(length: filesize, offset: filesize) -> result, bool>, error-code>; + write: func(buffer: list, offset: filesize) -> result; + read-directory: func() -> result; + sync: func() -> result<_, error-code>; + create-directory-at: func(path: string) -> result<_, error-code>; + stat: func() -> result; + stat-at: func(path-flags: path-flags, path: string) -> result; + set-times-at: func(path-flags: path-flags, path: string, data-access-timestamp: new-timestamp, data-modification-timestamp: new-timestamp) -> result<_, error-code>; + link-at: func(old-path-flags: path-flags, old-path: string, new-descriptor: borrow, new-path: string) -> result<_, error-code>; + open-at: func(path-flags: path-flags, path: string, open-flags: open-flags, %flags: descriptor-flags) -> result; + readlink-at: func(path: string) -> result; + remove-directory-at: func(path: string) -> result<_, error-code>; + rename-at: func(old-path: string, new-descriptor: borrow, new-path: string) -> result<_, error-code>; + symlink-at: func(old-path: string, new-path: string) -> result<_, error-code>; + unlink-file-at: func(path: string) -> result<_, error-code>; + is-same-object: func(other: borrow) -> bool; + metadata-hash: func() -> result; + metadata-hash-at: func(path-flags: path-flags, path: string) -> result; + } + + type filesize = u64; + + enum error-code { + access, + would-block, + already, + bad-descriptor, + busy, + deadlock, + quota, + exist, + file-too-large, + illegal-byte-sequence, + in-progress, + interrupted, + invalid, + io, + is-directory, + loop, + too-many-links, + message-size, + name-too-long, + no-device, + no-entry, + no-lock, + insufficient-memory, + insufficient-space, + not-directory, + not-empty, + not-recoverable, + unsupported, + no-tty, + no-such-device, + overflow, + not-permitted, + pipe, + read-only, + invalid-seek, + text-file-busy, + cross-device, + } + + enum advice { + normal, + sequential, + random, + will-need, + dont-need, + no-reuse, + } + + flags descriptor-flags { + read, + write, + file-integrity-sync, + data-integrity-sync, + requested-write-sync, + mutate-directory, + } + + enum descriptor-type { + unknown, + block-device, + character-device, + directory, + fifo, + symbolic-link, + regular-file, + socket, + } + + variant new-timestamp { + no-change, + now, + timestamp(datetime), + } + + resource directory-entry-stream { + read-directory-entry: func() -> result, error-code>; + } + + type link-count = u64; + + record descriptor-stat { + %type: descriptor-type, + link-count: link-count, + size: filesize, + data-access-timestamp: option, + data-modification-timestamp: option, + status-change-timestamp: option, + } + + flags path-flags { + symlink-follow, + } + + flags open-flags { + create, + directory, + exclusive, + truncate, + } + + record metadata-hash-value { + lower: u64, + upper: u64, + } + + record directory-entry { + %type: descriptor-type, + name: string, + } + + filesystem-error-code: func(err: borrow) -> option; +} + +interface preopens { + use types.{descriptor}; + + get-directories: func() -> list>; +} + diff --git a/crates/wasiless/wit/deps/io.wit b/crates/wasiless/wit/deps/io.wit new file mode 100644 index 0000000..11b7a7f --- /dev/null +++ b/crates/wasiless/wit/deps/io.wit @@ -0,0 +1,48 @@ +package wasi:io@0.2.6; + +interface error { + resource error { + to-debug-string: func() -> string; + } +} + +interface poll { + resource pollable { + ready: func() -> bool; + block: func(); + } + + poll: func(in: list>) -> list; +} + +interface streams { + use error.{error}; + use poll.{pollable}; + + resource input-stream { + read: func(len: u64) -> result, stream-error>; + blocking-read: func(len: u64) -> result, stream-error>; + skip: func(len: u64) -> result; + blocking-skip: func(len: u64) -> result; + subscribe: func() -> pollable; + } + + variant stream-error { + last-operation-failed(error), + closed, + } + + resource output-stream { + check-write: func() -> result; + write: func(contents: list) -> result<_, stream-error>; + blocking-write-and-flush: func(contents: list) -> result<_, stream-error>; + flush: func() -> result<_, stream-error>; + blocking-flush: func() -> result<_, stream-error>; + subscribe: func() -> pollable; + write-zeroes: func(len: u64) -> result<_, stream-error>; + blocking-write-zeroes-and-flush: func(len: u64) -> result<_, stream-error>; + splice: func(src: borrow, len: u64) -> result; + blocking-splice: func(src: borrow, len: u64) -> result; + } +} + diff --git a/crates/wasiless/wit/deps/random.wit b/crates/wasiless/wit/deps/random.wit new file mode 100644 index 0000000..eb3d27d --- /dev/null +++ b/crates/wasiless/wit/deps/random.wit @@ -0,0 +1,12 @@ +package wasi:random@0.2.0; + +interface insecure { + get-insecure-random-bytes: func(len: u64) -> list; + + get-insecure-random-u64: func() -> u64; +} + +interface insecure-seed { + insecure-seed: func() -> tuple; +} + diff --git a/crates/wasiless/wit/deps/sockets.wit b/crates/wasiless/wit/deps/sockets.wit new file mode 100644 index 0000000..fc71f10 --- /dev/null +++ b/crates/wasiless/wit/deps/sockets.wit @@ -0,0 +1,183 @@ +package wasi:sockets@0.2.0; + +interface network { + resource network; + + type ipv4-address = tuple; + + record ipv4-socket-address { + port: u16, + address: ipv4-address, + } + + type ipv6-address = tuple; + + record ipv6-socket-address { + port: u16, + flow-info: u32, + address: ipv6-address, + scope-id: u32, + } + + variant ip-socket-address { + ipv4(ipv4-socket-address), + ipv6(ipv6-socket-address), + } + + enum error-code { + unknown, + access-denied, + not-supported, + invalid-argument, + out-of-memory, + timeout, + concurrency-conflict, + not-in-progress, + would-block, + invalid-state, + new-socket-limit, + address-not-bindable, + address-in-use, + remote-unreachable, + connection-refused, + connection-reset, + connection-aborted, + datagram-too-large, + name-unresolvable, + temporary-resolver-failure, + permanent-resolver-failure, + } + + enum ip-address-family { + ipv4, + ipv6, + } + + variant ip-address { + ipv4(ipv4-address), + ipv6(ipv6-address), + } +} + +interface instance-network { + use network.{network}; + + instance-network: func() -> network; +} + +interface udp { + use network.{network, ip-socket-address, error-code, ip-address-family}; + use wasi:io/poll@0.2.6.{pollable}; + + resource udp-socket { + start-bind: func(network: borrow, local-address: ip-socket-address) -> result<_, error-code>; + finish-bind: func() -> result<_, error-code>; + %stream: func(remote-address: option) -> result, error-code>; + local-address: func() -> result; + remote-address: func() -> result; + address-family: func() -> ip-address-family; + unicast-hop-limit: func() -> result; + set-unicast-hop-limit: func(value: u8) -> result<_, error-code>; + receive-buffer-size: func() -> result; + set-receive-buffer-size: func(value: u64) -> result<_, error-code>; + send-buffer-size: func() -> result; + set-send-buffer-size: func(value: u64) -> result<_, error-code>; + subscribe: func() -> pollable; + } + + resource incoming-datagram-stream { + receive: func(max-results: u64) -> result, error-code>; + subscribe: func() -> pollable; + } + + resource outgoing-datagram-stream { + check-send: func() -> result; + send: func(datagrams: list) -> result; + subscribe: func() -> pollable; + } + + record incoming-datagram { + data: list, + remote-address: ip-socket-address, + } + + record outgoing-datagram { + data: list, + remote-address: option, + } +} + +interface udp-create-socket { + use network.{ip-address-family}; + use udp.{udp-socket}; + use network.{error-code}; + + create-udp-socket: func(address-family: ip-address-family) -> result; +} + +interface tcp { + use network.{network, ip-socket-address, error-code}; + use wasi:io/streams@0.2.6.{input-stream, output-stream}; + use network.{ip-address-family}; + use wasi:clocks/monotonic-clock@0.2.6.{duration}; + use wasi:io/poll@0.2.6.{pollable}; + + resource tcp-socket { + start-bind: func(network: borrow, local-address: ip-socket-address) -> result<_, error-code>; + finish-bind: func() -> result<_, error-code>; + start-connect: func(network: borrow, remote-address: ip-socket-address) -> result<_, error-code>; + finish-connect: func() -> result, error-code>; + start-listen: func() -> result<_, error-code>; + finish-listen: func() -> result<_, error-code>; + accept: func() -> result, error-code>; + local-address: func() -> result; + remote-address: func() -> result; + is-listening: func() -> bool; + address-family: func() -> ip-address-family; + set-listen-backlog-size: func(value: u64) -> result<_, error-code>; + keep-alive-enabled: func() -> result; + set-keep-alive-enabled: func(value: bool) -> result<_, error-code>; + keep-alive-idle-time: func() -> result; + set-keep-alive-idle-time: func(value: duration) -> result<_, error-code>; + keep-alive-interval: func() -> result; + set-keep-alive-interval: func(value: duration) -> result<_, error-code>; + keep-alive-count: func() -> result; + set-keep-alive-count: func(value: u32) -> result<_, error-code>; + hop-limit: func() -> result; + set-hop-limit: func(value: u8) -> result<_, error-code>; + receive-buffer-size: func() -> result; + set-receive-buffer-size: func(value: u64) -> result<_, error-code>; + send-buffer-size: func() -> result; + set-send-buffer-size: func(value: u64) -> result<_, error-code>; + subscribe: func() -> pollable; + shutdown: func(shutdown-type: shutdown-type) -> result<_, error-code>; + } + + enum shutdown-type { + receive, + send, + both, + } +} + +interface tcp-create-socket { + use network.{ip-address-family}; + use tcp.{tcp-socket}; + use network.{error-code}; + + create-tcp-socket: func(address-family: ip-address-family) -> result; +} + +interface ip-name-lookup { + use network.{ip-address, error-code}; + use wasi:io/poll@0.2.6.{pollable}; + use network.{network}; + + resource resolve-address-stream { + resolve-next-address: func() -> result, error-code>; + subscribe: func() -> pollable; + } + + resolve-addresses: func(network: borrow, name: string) -> result; +} + diff --git a/crates/wasiless/wit/wasiless.wit b/crates/wasiless/wit/wasiless.wit new file mode 100644 index 0000000..2731b11 --- /dev/null +++ b/crates/wasiless/wit/wasiless.wit @@ -0,0 +1,22 @@ +package fastly:wasiless; + +// Interfaces that are imported by the Python+app component but are not provided +// by Viceroy (ignoring versions): +world wasiless { + export wasi:cli/terminal-input@0.2.0; + export wasi:cli/terminal-output@0.2.0; + export wasi:cli/terminal-stdin@0.2.0; + export wasi:cli/terminal-stdout@0.2.0; + export wasi:cli/terminal-stderr@0.2.0; + export wasi:filesystem/types@0.2.0; + export wasi:filesystem/preopens@0.2.0; + export wasi:sockets/network@0.2.0; + export wasi:sockets/instance-network@0.2.0; + export wasi:sockets/udp@0.2.0; + export wasi:sockets/udp-create-socket@0.2.0; + export wasi:sockets/tcp@0.2.0; + export wasi:sockets/tcp-create-socket@0.2.0; + export wasi:sockets/ip-name-lookup@0.2.0; + export wasi:random/insecure@0.2.0; + export wasi:random/insecure-seed@0.2.0; +}