From 4c9ff8218df7c518c355e8cb873fdc9f82abca34 Mon Sep 17 00:00:00 2001 From: Roman Volosatovs Date: Mon, 20 May 2024 17:49:34 +0200 Subject: [PATCH] feat: add binding crate integrations (#89) * feat(io): implement `Read` and `Write` for streams Signed-off-by: Roman Volosatovs * feat(rand): add `rand` crate integration Signed-off-by: Roman Volosatovs * chore: split examples into `std` and `no_std` Signed-off-by: Roman Volosatovs * ci: build all std and no_std examples Signed-off-by: Roman Volosatovs * refactor(rand): use `writeln` to write to stdout Signed-off-by: Roman Volosatovs * feat(wasi-ext): add workspace crate Signed-off-by: Roman Volosatovs * ci: build in `--workspace` mode Signed-off-by: Roman Volosatovs --------- Signed-off-by: Roman Volosatovs --- .github/workflows/main.yml | 34 +++++++++++++--- Cargo.toml | 32 +++++++++++++-- crates/wasi-ext/Cargo.toml | 21 ++++++++++ crates/wasi-ext/examples/rand.rs | 16 ++++++++ crates/wasi-ext/src/lib.rs | 2 + crates/wasi-ext/src/rand.rs | 69 ++++++++++++++++++++++++++++++++ examples/cli-command-no_std.rs | 11 +++++ examples/cli-command.rs | 7 +++- examples/hello-world-no_std.rs | 4 ++ examples/hello-world.rs | 7 +++- examples/http-proxy-no_std.rs | 22 ++++++++++ examples/http-proxy.rs | 7 +++- src/{errors.rs => ext/mod.rs} | 6 +-- src/ext/std.rs | 69 ++++++++++++++++++++++++++++++++ src/lib.rs | 2 +- 15 files changed, 290 insertions(+), 19 deletions(-) create mode 100644 crates/wasi-ext/Cargo.toml create mode 100644 crates/wasi-ext/examples/rand.rs create mode 100644 crates/wasi-ext/src/lib.rs create mode 100644 crates/wasi-ext/src/rand.rs create mode 100644 examples/cli-command-no_std.rs create mode 100644 examples/hello-world-no_std.rs create mode 100644 examples/http-proxy-no_std.rs rename src/{errors.rs => ext/mod.rs} (79%) create mode 100644 src/ext/std.rs diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 40268fb..da90d70 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -13,11 +13,11 @@ jobs: - name: Install Rust run: rustup update ${{ matrix.rust }} && rustup default ${{ matrix.rust }} && rustup component add rustfmt - run: rustup target add wasm32-wasi wasm32-unknown-unknown - - run: cargo build - - run: cargo build --no-default-features - - run: cargo build --target wasm32-wasi - - run: cargo build --target wasm32-wasi --no-default-features - - run: cargo test --doc + - run: cargo build --workspace + - run: cargo build --workspace --no-default-features + - run: cargo build --workspace --target wasm32-wasi + - run: cargo build --workspace --target wasm32-wasi --no-default-features + - run: cargo test --workspace --doc - name: Install Wasmtime uses: bytecodealliance/actions/wasmtime/setup@v1 with: @@ -26,16 +26,38 @@ jobs: uses: bytecodealliance/actions/wasm-tools/setup@v1 with: version: "1.202.0" - - run: cargo build --examples --target wasm32-wasi - run: curl -LO https://github.com/bytecodealliance/wasmtime/releases/download/v19.0.0/wasi_snapshot_preview1.command.wasm + + - run: cargo build --examples --target wasm32-wasi --no-default-features + + - run: wasm-tools component new ./target/wasm32-wasi/debug/examples/hello-world-no_std.wasm --adapt ./wasi_snapshot_preview1.command.wasm -o component.wasm + - run: wasmtime run component.wasm + + - run: cargo build --examples --target wasm32-unknown-unknown --no-default-features + + - run: wasm-tools component new ./target/wasm32-unknown-unknown/debug/examples/cli_command_no_std.wasm -o component.wasm + - run: wasmtime run component.wasm + + - run: wasm-tools component new ./target/wasm32-unknown-unknown/debug/examples/http_proxy_no_std.wasm -o component.wasm + - run: wasm-tools component targets wit component.wasm -w wasi:http/proxy + + - run: cargo build --examples --target wasm32-wasi + - run: wasm-tools component new ./target/wasm32-wasi/debug/examples/hello-world.wasm --adapt ./wasi_snapshot_preview1.command.wasm -o component.wasm - run: wasmtime run component.wasm + - run: cargo build --examples --target wasm32-unknown-unknown + - run: wasm-tools component new ./target/wasm32-unknown-unknown/debug/examples/cli_command.wasm -o component.wasm - run: wasmtime run component.wasm + - run: wasm-tools component new ./target/wasm32-unknown-unknown/debug/examples/http_proxy.wasm -o component.wasm - run: wasm-tools component targets wit component.wasm -w wasi:http/proxy + - run: cargo build --examples --workspace --target wasm32-wasi --features rand + + - run: wasm-tools component new ./target/wasm32-wasi/debug/examples/rand.wasm --adapt ./wasi_snapshot_preview1.command.wasm -o component.wasm + - run: wasmtime run component.wasm rustfmt: name: Rustfmt diff --git a/Cargo.toml b/Cargo.toml index dd0e9ef..266e5f0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,14 +2,26 @@ name = "wasi" version = "0.13.0+wasi-0.2.0" authors = ["The Cranelift Project Developers"] -license = "Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT" description = "WASI API bindings for Rust" -edition = "2021" categories = ["no-std", "wasm"] keywords = ["webassembly", "wasm"] -repository = "https://github.com/bytecodealliance/wasi-rs" readme = "README.md" documentation = "https://docs.rs/wasi" +license.workspace = true +edition.workspace = true +repository.workspace = true + +[workspace.package] +edition = "2021" +license = "Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT" +repository = "https://github.com/bytecodealliance/wasi-rs" + +[workspace.dependencies] +rand = { version = "0.8.5", default-features = false } +wasi = { version = "0.13", path = ".", default-features = false } + +[workspace] +members = ["./crates/*"] [dependencies] wit-bindgen-rt = { version = "0.23.0", features = ["bitflags"] } @@ -25,10 +37,24 @@ std = [] # Unstable feature to support being a libstd dependency rustc-dep-of-std = ["compiler_builtins", "core", "rustc-std-workspace-alloc"] +[[example]] +name = "cli-command-no_std" +crate-type = ["cdylib"] + [[example]] name = "cli-command" crate-type = ["cdylib"] +required-features = ["std"] + +[[example]] +name = "hello-world" +required-features = ["std"] + +[[example]] +name = "http-proxy-no_std" +crate-type = ["cdylib"] [[example]] name = "http-proxy" crate-type = ["cdylib"] +required-features = ["std"] diff --git a/crates/wasi-ext/Cargo.toml b/crates/wasi-ext/Cargo.toml new file mode 100644 index 0000000..e338185 --- /dev/null +++ b/crates/wasi-ext/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "wasi-ext" +version = "0.1.0" +authors = ["Roman Volosatovs "] +description = "Third-party crate integrations for WASI" + +license.workspace = true +edition.workspace = true +repository.workspace = true + +[features] +default = ["std"] +std = ["wasi/std"] + +[dependencies] +rand = { workspace = true, optional = true } +wasi = { workspace = true } + +[[example]] +name = "rand" +required-features = ["rand", "std"] diff --git a/crates/wasi-ext/examples/rand.rs b/crates/wasi-ext/examples/rand.rs new file mode 100644 index 0000000..3ab79e6 --- /dev/null +++ b/crates/wasi-ext/examples/rand.rs @@ -0,0 +1,16 @@ +use std::io::Write as _; + +use wasi_ext::rand::rand::Rng as _; +use wasi_ext::rand::{HostInsecureRng, HostRng}; + +fn main() { + let mut stdout = wasi::cli::stdout::get_stdout(); + + let r: u64 = HostRng.gen(); + writeln!(stdout, "Cryptographically-secure random u64 is {r}").unwrap(); + + let r: u64 = HostInsecureRng.gen(); + writeln!(stdout, "Pseudo-random u64 is {r}").unwrap(); + + stdout.flush().unwrap(); +} diff --git a/crates/wasi-ext/src/lib.rs b/crates/wasi-ext/src/lib.rs new file mode 100644 index 0000000..3428031 --- /dev/null +++ b/crates/wasi-ext/src/lib.rs @@ -0,0 +1,2 @@ +#[cfg(feature = "rand")] +pub mod rand; diff --git a/crates/wasi-ext/src/rand.rs b/crates/wasi-ext/src/rand.rs new file mode 100644 index 0000000..f4a3488 --- /dev/null +++ b/crates/wasi-ext/src/rand.rs @@ -0,0 +1,69 @@ +pub use rand; + +use rand::{CryptoRng, RngCore}; + +/// The secure interface for cryptographically-secure random numbers +pub struct HostRng; + +impl CryptoRng for HostRng {} + +impl RngCore for HostRng { + #[inline] + fn next_u32(&mut self) -> u32 { + wasi::random::random::get_random_u64() as _ + } + + #[inline] + fn next_u64(&mut self) -> u64 { + wasi::random::random::get_random_u64() + } + + fn fill_bytes(&mut self, dest: &mut [u8]) { + let n = dest.len(); + if usize::BITS <= u64::BITS || n <= u64::MAX as _ { + dest.copy_from_slice(&wasi::random::random::get_random_bytes(n as _)); + } else { + let (head, tail) = dest.split_at_mut(u64::MAX as _); + head.copy_from_slice(&wasi::random::random::get_random_bytes(u64::MAX)); + self.fill_bytes(tail); + } + } + + #[inline] + fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), rand::Error> { + self.fill_bytes(dest); + Ok(()) + } +} + +/// The insecure interface for insecure pseudo-random numbers +pub struct HostInsecureRng; + +impl RngCore for HostInsecureRng { + #[inline] + fn next_u32(&mut self) -> u32 { + wasi::random::insecure::get_insecure_random_u64() as _ + } + + #[inline] + fn next_u64(&mut self) -> u64 { + wasi::random::insecure::get_insecure_random_u64() + } + + fn fill_bytes(&mut self, dest: &mut [u8]) { + let n = dest.len(); + if usize::BITS <= u64::BITS || n <= u64::MAX as _ { + dest.copy_from_slice(&wasi::random::insecure::get_insecure_random_bytes(n as _)); + } else { + let (head, tail) = dest.split_at_mut(u64::MAX as _); + head.copy_from_slice(&wasi::random::insecure::get_insecure_random_bytes(u64::MAX)); + self.fill_bytes(tail); + } + } + + #[inline] + fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), rand::Error> { + self.fill_bytes(dest); + Ok(()) + } +} diff --git a/examples/cli-command-no_std.rs b/examples/cli-command-no_std.rs new file mode 100644 index 0000000..809d15a --- /dev/null +++ b/examples/cli-command-no_std.rs @@ -0,0 +1,11 @@ +wasi::cli::command::export!(Example); + +struct Example; + +impl wasi::exports::cli::run::Guest for Example { + fn run() -> Result<(), ()> { + let stdout = wasi::cli::stdout::get_stdout(); + stdout.blocking_write_and_flush(b"Hello, WASI!").unwrap(); + Ok(()) + } +} diff --git a/examples/cli-command.rs b/examples/cli-command.rs index 809d15a..e3932ab 100644 --- a/examples/cli-command.rs +++ b/examples/cli-command.rs @@ -1,11 +1,14 @@ +use std::io::Write as _; + wasi::cli::command::export!(Example); struct Example; impl wasi::exports::cli::run::Guest for Example { fn run() -> Result<(), ()> { - let stdout = wasi::cli::stdout::get_stdout(); - stdout.blocking_write_and_flush(b"Hello, WASI!").unwrap(); + let mut stdout = wasi::cli::stdout::get_stdout(); + stdout.write_all(b"Hello, WASI!").unwrap(); + stdout.flush().unwrap(); Ok(()) } } diff --git a/examples/hello-world-no_std.rs b/examples/hello-world-no_std.rs new file mode 100644 index 0000000..026e1bc --- /dev/null +++ b/examples/hello-world-no_std.rs @@ -0,0 +1,4 @@ +fn main() { + let stdout = wasi::cli::stdout::get_stdout(); + stdout.blocking_write_and_flush(b"Hello, world!\n").unwrap(); +} diff --git a/examples/hello-world.rs b/examples/hello-world.rs index 026e1bc..62d1210 100644 --- a/examples/hello-world.rs +++ b/examples/hello-world.rs @@ -1,4 +1,7 @@ +use std::io::Write as _; + fn main() { - let stdout = wasi::cli::stdout::get_stdout(); - stdout.blocking_write_and_flush(b"Hello, world!\n").unwrap(); + let mut stdout = wasi::cli::stdout::get_stdout(); + stdout.write_all(b"Hello, world!\n").unwrap(); + stdout.flush().unwrap(); } diff --git a/examples/http-proxy-no_std.rs b/examples/http-proxy-no_std.rs new file mode 100644 index 0000000..9a2e54a --- /dev/null +++ b/examples/http-proxy-no_std.rs @@ -0,0 +1,22 @@ +use wasi::http::types::{ + Fields, IncomingRequest, OutgoingBody, OutgoingResponse, ResponseOutparam, +}; + +wasi::http::proxy::export!(Example); + +struct Example; + +impl wasi::exports::http::incoming_handler::Guest for Example { + fn handle(_request: IncomingRequest, response_out: ResponseOutparam) { + let resp = OutgoingResponse::new(Fields::new()); + let body = resp.body().unwrap(); + + ResponseOutparam::set(response_out, Ok(resp)); + + let out = body.write().unwrap(); + out.blocking_write_and_flush(b"Hello, WASI!").unwrap(); + drop(out); + + OutgoingBody::finish(body, None).unwrap(); + } +} diff --git a/examples/http-proxy.rs b/examples/http-proxy.rs index 9a2e54a..8226e8b 100644 --- a/examples/http-proxy.rs +++ b/examples/http-proxy.rs @@ -1,3 +1,5 @@ +use std::io::Write as _; + use wasi::http::types::{ Fields, IncomingRequest, OutgoingBody, OutgoingResponse, ResponseOutparam, }; @@ -13,8 +15,9 @@ impl wasi::exports::http::incoming_handler::Guest for Example { ResponseOutparam::set(response_out, Ok(resp)); - let out = body.write().unwrap(); - out.blocking_write_and_flush(b"Hello, WASI!").unwrap(); + let mut out = body.write().unwrap(); + out.write_all(b"Hello, WASI!").unwrap(); + out.flush().unwrap(); drop(out); OutgoingBody::finish(body, None).unwrap(); diff --git a/src/errors.rs b/src/ext/mod.rs similarity index 79% rename from src/errors.rs rename to src/ext/mod.rs index 467e6fa..b581f22 100644 --- a/src/errors.rs +++ b/src/ext/mod.rs @@ -1,8 +1,8 @@ +#[cfg(feature = "std")] +mod std; + impl core::fmt::Display for crate::io::error::Error { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { f.write_str(&self.to_debug_string()) } } - -#[cfg(feature = "std")] -impl std::error::Error for crate::io::error::Error {} diff --git a/src/ext/std.rs b/src/ext/std.rs new file mode 100644 index 0000000..bd0178d --- /dev/null +++ b/src/ext/std.rs @@ -0,0 +1,69 @@ +use std::error::Error; +use std::io; +use std::num::NonZeroU64; + +use crate::io::streams::StreamError; + +impl Error for crate::io::error::Error {} + +impl io::Read for crate::io::streams::InputStream { + fn read(&mut self, buf: &mut [u8]) -> io::Result { + let n = buf + .len() + .try_into() + .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; + match self.blocking_read(n) { + Ok(chunk) => { + let n = chunk.len(); + if n > buf.len() { + return Err(io::Error::new( + io::ErrorKind::Other, + "more bytes read than requested", + )); + } + buf[..n].copy_from_slice(&chunk); + Ok(n) + } + Err(StreamError::Closed) => Ok(0), + Err(StreamError::LastOperationFailed(e)) => { + Err(io::Error::new(io::ErrorKind::Other, e.to_debug_string())) + } + } + } +} + +impl io::Write for crate::io::streams::OutputStream { + fn write(&mut self, buf: &[u8]) -> io::Result { + let n = loop { + match self.check_write().map(NonZeroU64::new) { + Ok(Some(n)) => { + break n; + } + Ok(None) => { + self.subscribe().block(); + } + Err(StreamError::Closed) => return Ok(0), + Err(StreamError::LastOperationFailed(e)) => { + return Err(io::Error::new(io::ErrorKind::Other, e.to_debug_string())) + } + }; + }; + let n = n + .get() + .try_into() + .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; + let n = buf.len().min(n); + crate::io::streams::OutputStream::write(self, &buf[..n]).map_err(|e| match e { + StreamError::Closed => io::ErrorKind::UnexpectedEof.into(), + StreamError::LastOperationFailed(e) => { + io::Error::new(io::ErrorKind::Other, e.to_debug_string()) + } + })?; + Ok(n) + } + + fn flush(&mut self) -> io::Result<()> { + self.blocking_flush() + .map_err(|e| io::Error::new(io::ErrorKind::Other, e)) + } +} diff --git a/src/lib.rs b/src/lib.rs index ad02f10..7b829c8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -179,7 +179,7 @@ #[cfg(feature = "std")] extern crate std; -mod errors; +pub mod ext; // These modules are all auto-generated by `./ci/regenerate.sh` mod bindings;