├── aws ├── .gitignore ├── Cargo.toml ├── README.md ├── src │ └── lib.rs └── examples │ └── s3.rs ├── axum ├── macro │ ├── .gitignore │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── examples │ ├── hello_world.rs │ ├── hello_world_nomacro.rs │ └── weather.rs ├── Cargo.toml └── src │ └── lib.rs ├── .gitignore ├── .gitattributes ├── rust-toolchain.toml ├── ci └── print-current-version.sh ├── src ├── time │ ├── utils.rs │ ├── instant.rs │ ├── mod.rs │ └── duration.rs ├── iter │ └── mod.rs ├── task.rs ├── http │ ├── error.rs │ ├── mod.rs │ ├── scheme.rs │ ├── fields.rs │ ├── response.rs │ ├── method.rs │ ├── server.rs │ ├── request.rs │ └── client.rs ├── io │ ├── mod.rs │ ├── empty.rs │ ├── copy.rs │ ├── write.rs │ ├── read.rs │ ├── seek.rs │ ├── cursor.rs │ └── stdio.rs ├── rand │ └── mod.rs ├── future │ ├── mod.rs │ ├── timeout.rs │ ├── delay.rs │ └── future_ext.rs ├── net │ ├── mod.rs │ ├── tcp_listener.rs │ └── tcp_stream.rs ├── runtime │ ├── mod.rs │ └── block_on.rs └── lib.rs ├── tests ├── sleep.rs ├── http_timeout.rs ├── http_get_json.rs ├── http_first_byte_timeout.rs ├── http_post_json.rs ├── http_post.rs └── http_get.rs ├── .vscode └── settings.json ├── .cargo └── config.toml ├── ADOPTERS.md ├── test-programs ├── Cargo.toml ├── tests │ ├── http_server_proxy.rs │ ├── axum_hello_world.rs │ ├── axum_weather.rs │ ├── tcp_stream_client.rs │ ├── aws_s3.rs │ ├── tcp_echo_server.rs │ └── http_server.rs ├── src │ └── lib.rs └── build.rs ├── macro ├── Cargo.toml └── src │ └── lib.rs ├── examples ├── tcp_stream_client.rs ├── tcp_echo_server.rs ├── http_server_proxy.rs ├── http_client.rs ├── complex_http_client.rs └── http_server.rs ├── CONTRIBUTING.md ├── .github ├── actions │ └── install-rust │ │ └── action.yml ├── workflows │ ├── publish.yml │ ├── ci.yaml │ └── release-process.yml ├── CONTRIBUTING.md └── CODE_OF_CONDUCT.md ├── Cargo.toml ├── CODE_OF_CONDUCT.md ├── README.md └── LICENSE-Apache-2.0_WITH_LLVM-exception /aws/.gitignore: -------------------------------------------------------------------------------- 1 | .environment 2 | -------------------------------------------------------------------------------- /axum/macro/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | tmp/ 3 | Cargo.lock 4 | .DS_Store 5 | publish 6 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "stable" 3 | targets = ["wasm32-wasip2"] 4 | -------------------------------------------------------------------------------- /ci/print-current-version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | grep '^version =' Cargo.toml | head -n 1 | sed 's/.*"\(.*\)"/\1/' 3 | -------------------------------------------------------------------------------- /src/time/utils.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | 3 | pub(crate) fn timeout_err(msg: &'static str) -> io::Error { 4 | io::Error::new(io::ErrorKind::TimedOut, msg) 5 | } 6 | -------------------------------------------------------------------------------- /tests/sleep.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | use wstd::task::sleep; 3 | use wstd::time::Duration; 4 | 5 | #[wstd::test] 6 | async fn just_sleep() -> Result<(), Box> { 7 | sleep(Duration::from_secs(1)).await; 8 | Ok(()) 9 | } 10 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "rust-analyzer.check.overrideCommand": [ 3 | "cargo", 4 | "component", 5 | "check", 6 | "--workspace", 7 | "--all-targets", 8 | "--message-format=json" 9 | ], 10 | } 11 | -------------------------------------------------------------------------------- /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [target.wasm32-wasip2] 2 | # wasmtime is given: 3 | # * AWS auth environment variables, for running the wstd-aws integration tests. 4 | # * . directory is available at . 5 | runner = "wasmtime run -Shttp --env AWS_ACCESS_KEY_ID --env AWS_SECRET_ACCESS_KEY --env AWS_SESSION_TOKEN --dir .::." 6 | -------------------------------------------------------------------------------- /ADOPTERS.md: -------------------------------------------------------------------------------- 1 | # Wstd adopters 2 | 3 | _If you are using wstd in your project, please add your project to this list. 4 | The list is in alphabetical order._ 5 | 6 | * Bytecode Alliance's [sample-wasi-http-rust] template 7 | 8 | [sample-wasi-http-rust]: https://github.com/bytecodealliance/sample-wasi-http-rust 9 | -------------------------------------------------------------------------------- /src/iter/mod.rs: -------------------------------------------------------------------------------- 1 | //! Composable async iteration. 2 | 3 | /// A trait for dealing with async iterators. 4 | pub trait AsyncIterator { 5 | /// The type of the elements being iterated over. 6 | type Item; 7 | 8 | /// Advances the iterator and returns the next value. 9 | async fn next(&mut self) -> Option; 10 | } 11 | -------------------------------------------------------------------------------- /src/task.rs: -------------------------------------------------------------------------------- 1 | //! Types and Traits for working with asynchronous tasks. 2 | 3 | use crate::time::{Duration, Instant, Timer, Wait}; 4 | 5 | /// Sleeps for the specified amount of time. 6 | pub fn sleep(dur: Duration) -> Wait { 7 | Timer::after(dur).wait() 8 | } 9 | 10 | /// Sleeps until the specified instant. 11 | pub fn sleep_until(deadline: Instant) -> Wait { 12 | Timer::at(deadline).wait() 13 | } 14 | -------------------------------------------------------------------------------- /test-programs/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "test-programs" 3 | version = "0.1.0" 4 | edition.workspace = true 5 | license.workspace = true 6 | rust-version.workspace = true 7 | publish = false 8 | 9 | [dev-dependencies] 10 | anyhow.workspace = true 11 | test-log.workspace = true 12 | serde_json.workspace = true 13 | ureq.workspace = true 14 | 15 | [build-dependencies] 16 | cargo_metadata.workspace = true 17 | heck.workspace = true 18 | 19 | [features] 20 | default = [] 21 | no-aws = [] 22 | -------------------------------------------------------------------------------- /macro/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wstd-macro" 3 | version.workspace = true 4 | edition.workspace = true 5 | license.workspace = true 6 | authors.workspace = true 7 | keywords.workspace = true 8 | categories.workspace = true 9 | rust-version.workspace = true 10 | description = "proc-macros for the wstd crate" 11 | documentation = "https://docs.rs/wstd-macro" 12 | 13 | [lib] 14 | proc-macro = true 15 | 16 | [dependencies] 17 | syn = { workspace = true, features = ["full"] } 18 | quote.workspace = true 19 | -------------------------------------------------------------------------------- /axum/macro/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wstd-axum-macro" 3 | description = "Proc-macro support for axum as a wasi http server via wstd" 4 | version.workspace = true 5 | license.workspace = true 6 | edition.workspace = true 7 | authors.workspace = true 8 | keywords.workspace = true 9 | categories.workspace = true 10 | repository.workspace = true 11 | rust-version.workspace = true 12 | 13 | [lib] 14 | proc-macro = true 15 | 16 | [dependencies] 17 | syn = { workspace = true, features = ["full"] } 18 | quote.workspace = true 19 | -------------------------------------------------------------------------------- /src/http/error.rs: -------------------------------------------------------------------------------- 1 | //! The http portion of wstd uses `anyhow::Error` as its `Error` type. 2 | //! 3 | //! There are various concrete error types 4 | 5 | pub use crate::http::body::InvalidContentLength; 6 | pub use anyhow::Context; 7 | pub use http::header::{InvalidHeaderName, InvalidHeaderValue}; 8 | pub use http::method::InvalidMethod; 9 | pub use wasip2::http::types::{ErrorCode, HeaderError}; 10 | 11 | pub type Error = anyhow::Error; 12 | /// The `http` result type. 13 | pub type Result = std::result::Result; 14 | -------------------------------------------------------------------------------- /axum/examples/hello_world.rs: -------------------------------------------------------------------------------- 1 | //! Run with 2 | //! 3 | //! ```sh 4 | //! cargo build -p wstd-axum --examples --target wasm32-wasip2 5 | //! wasmtime serve -Scli target/wasm32-wasip2/debug/examples/hello-world.wasm 6 | //! ``` 7 | 8 | use axum::{Router, response::Html, routing::get}; 9 | 10 | #[wstd_axum::http_server] 11 | fn main() -> Router { 12 | // build our application with a route 13 | Router::new().route("/", get(handler)) 14 | } 15 | 16 | async fn handler() -> Html<&'static str> { 17 | Html("

Hello, World!

") 18 | } 19 | -------------------------------------------------------------------------------- /src/io/mod.rs: -------------------------------------------------------------------------------- 1 | //! Async IO abstractions. 2 | 3 | mod copy; 4 | mod cursor; 5 | mod empty; 6 | mod read; 7 | mod seek; 8 | mod stdio; 9 | mod streams; 10 | mod write; 11 | 12 | pub use crate::runtime::AsyncPollable; 13 | pub use copy::*; 14 | pub use cursor::*; 15 | pub use empty::*; 16 | pub use read::*; 17 | pub use seek::*; 18 | pub use stdio::*; 19 | pub use streams::*; 20 | pub use write::*; 21 | 22 | /// The error type for I/O operations. 23 | /// 24 | pub use std::io::Error; 25 | 26 | /// A specialized Result type for I/O operations. 27 | /// 28 | pub use std::io::Result; 29 | -------------------------------------------------------------------------------- /src/io/empty.rs: -------------------------------------------------------------------------------- 1 | use super::{AsyncRead, AsyncWrite}; 2 | 3 | #[non_exhaustive] 4 | pub struct Empty; 5 | 6 | impl AsyncRead for Empty { 7 | async fn read(&mut self, _buf: &mut [u8]) -> super::Result { 8 | Ok(0) 9 | } 10 | } 11 | 12 | impl AsyncWrite for Empty { 13 | async fn write(&mut self, buf: &[u8]) -> super::Result { 14 | Ok(buf.len()) 15 | } 16 | 17 | async fn flush(&mut self) -> super::Result<()> { 18 | Ok(()) 19 | } 20 | } 21 | 22 | /// Creates a value that is always at EOF for reads, and ignores all data written. 23 | pub fn empty() -> Empty { 24 | Empty {} 25 | } 26 | -------------------------------------------------------------------------------- /src/http/mod.rs: -------------------------------------------------------------------------------- 1 | //! HTTP networking support 2 | //! 3 | pub use http::status::StatusCode; 4 | pub use http::uri::{Authority, PathAndQuery, Uri}; 5 | 6 | #[doc(inline)] 7 | pub use body::{Body, util::BodyExt}; 8 | pub use client::Client; 9 | pub use error::{Error, ErrorCode, Result}; 10 | pub use fields::{HeaderMap, HeaderName, HeaderValue}; 11 | pub use method::Method; 12 | pub use request::Request; 13 | pub use response::Response; 14 | pub use scheme::{InvalidUri, Scheme}; 15 | 16 | pub mod body; 17 | 18 | mod client; 19 | pub mod error; 20 | mod fields; 21 | mod method; 22 | pub mod request; 23 | pub mod response; 24 | mod scheme; 25 | pub mod server; 26 | -------------------------------------------------------------------------------- /axum/examples/hello_world_nomacro.rs: -------------------------------------------------------------------------------- 1 | //! Run with 2 | //! 3 | //! ```sh 4 | //! cargo build -p wstd-axum --examples --target wasm32-wasip2 5 | //! wasmtime serve -Scli target/wasm32-wasip2/debug/examples/hello-world-nomacro.wasm 6 | //! ``` 7 | 8 | use axum::{Router, response::Html, routing::get}; 9 | use wstd::http::{Body, Error, Request, Response}; 10 | 11 | #[wstd::http_server] 12 | async fn main(request: Request) -> Result, Error> { 13 | let service = Router::new().route("/", get(handler)); 14 | wstd_axum::serve(request, service).await 15 | } 16 | 17 | async fn handler() -> Html<&'static str> { 18 | Html("

Hello, World!

") 19 | } 20 | -------------------------------------------------------------------------------- /examples/tcp_stream_client.rs: -------------------------------------------------------------------------------- 1 | use wstd::io::{self, AsyncRead, AsyncWrite}; 2 | use wstd::net::TcpStream; 3 | 4 | #[wstd::main] 5 | async fn main() -> io::Result<()> { 6 | let mut args = std::env::args(); 7 | 8 | let _ = args.next(); 9 | 10 | let addr = args.next().ok_or_else(|| { 11 | io::Error::new( 12 | std::io::ErrorKind::InvalidInput, 13 | "address argument required", 14 | ) 15 | })?; 16 | 17 | let mut stream = TcpStream::connect(addr).await?; 18 | 19 | stream.write_all(b"ping\n").await?; 20 | 21 | let mut reply = Vec::new(); 22 | stream.read_to_end(&mut reply).await?; 23 | 24 | Ok(()) 25 | } 26 | -------------------------------------------------------------------------------- /src/http/scheme.rs: -------------------------------------------------------------------------------- 1 | use wasip2::http::types::Scheme as WasiScheme; 2 | 3 | pub use http::uri::{InvalidUri, Scheme}; 4 | use std::str::FromStr; 5 | 6 | pub(crate) fn to_wasi_scheme(value: &Scheme) -> WasiScheme { 7 | match value.as_str() { 8 | "http" => WasiScheme::Http, 9 | "https" => WasiScheme::Https, 10 | other => WasiScheme::Other(other.to_owned()), 11 | } 12 | } 13 | 14 | pub(crate) fn from_wasi_scheme(value: WasiScheme) -> Result { 15 | Ok(match value { 16 | WasiScheme::Http => Scheme::HTTP, 17 | WasiScheme::Https => Scheme::HTTPS, 18 | WasiScheme::Other(other) => Scheme::from_str(&other)?, 19 | }) 20 | } 21 | -------------------------------------------------------------------------------- /axum/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wstd-axum" 3 | description = "Support for axum as a wasi http server via wstd" 4 | version.workspace = true 5 | license.workspace = true 6 | edition.workspace = true 7 | authors.workspace = true 8 | keywords.workspace = true 9 | categories.workspace = true 10 | repository.workspace = true 11 | rust-version.workspace = true 12 | 13 | [dependencies] 14 | axum.workspace = true 15 | tower-service.workspace = true 16 | wstd.workspace = true 17 | wstd-axum-macro.workspace = true 18 | 19 | [dev-dependencies] 20 | anyhow.workspace = true 21 | futures-concurrency.workspace = true 22 | serde = { workspace = true, features = ["derive"] } 23 | serde_qs.workspace = true 24 | axum = { workspace = true, features = ["query", "json", "macros"] } 25 | -------------------------------------------------------------------------------- /src/rand/mod.rs: -------------------------------------------------------------------------------- 1 | //! Random number generation. 2 | 3 | use wasip2::random; 4 | 5 | /// Fill the slice with cryptographically secure random bytes. 6 | pub fn get_random_bytes(buf: &mut [u8]) { 7 | match buf.len() { 8 | 0 => {} 9 | _ => { 10 | let output = random::random::get_random_bytes(buf.len() as u64); 11 | buf.copy_from_slice(&output[..]); 12 | } 13 | } 14 | } 15 | 16 | /// Fill the slice with insecure random bytes. 17 | pub fn get_insecure_random_bytes(buf: &mut [u8]) { 18 | match buf.len() { 19 | 0 => {} 20 | _ => { 21 | let output = random::insecure::get_insecure_random_bytes(buf.len() as u64); 22 | buf.copy_from_slice(&output[..]); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /test-programs/tests/http_server_proxy.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | 3 | #[test_log::test] 4 | fn http_server_proxy() -> Result<()> { 5 | // Run wasmtime serve for the proxy and the target HTTP server. 6 | let _serve_target = test_programs::WasmtimeServe::new(test_programs::HTTP_SERVER)?; 7 | let _serve_proxy = test_programs::WasmtimeServe::new_with_config( 8 | test_programs::HTTP_SERVER_PROXY, 9 | 8082, 10 | &["TARGET_URL=http://127.0.0.1:8081"], 11 | )?; 12 | 13 | // TEST / of the `http_server` example through the proxy 14 | let body: String = ureq::get("http://127.0.0.1:8082/proxy/") 15 | .call()? 16 | .body_mut() 17 | .read_to_string()?; 18 | assert_eq!(body, "Hello, wasi:http/proxy world!\n"); 19 | Ok(()) 20 | } 21 | -------------------------------------------------------------------------------- /examples/tcp_echo_server.rs: -------------------------------------------------------------------------------- 1 | use wstd::io; 2 | use wstd::iter::AsyncIterator; 3 | use wstd::net::TcpListener; 4 | 5 | #[wstd::main] 6 | async fn main() -> io::Result<()> { 7 | let listener = TcpListener::bind("127.0.0.1:8080").await?; 8 | println!("Listening on {}", listener.local_addr()?); 9 | println!("type `nc localhost 8080` to create a TCP client"); 10 | 11 | let mut incoming = listener.incoming(); 12 | while let Some(stream) = incoming.next().await { 13 | let stream = stream?; 14 | println!("Accepted from: {}", stream.peer_addr()?); 15 | wstd::runtime::spawn(async move { 16 | // If echo copy fails, we can ignore it. 17 | let _ = io::copy(&stream, &stream).await; 18 | }) 19 | .detach(); 20 | } 21 | Ok(()) 22 | } 23 | -------------------------------------------------------------------------------- /tests/http_timeout.rs: -------------------------------------------------------------------------------- 1 | use wstd::future::FutureExt; 2 | use wstd::http::{Body, Client, Request}; 3 | use wstd::time::Duration; 4 | 5 | #[wstd::test] 6 | async fn http_timeout() -> Result<(), Box> { 7 | // This get request will connect to the server, which will then wait 1 second before 8 | // returning a response. 9 | let request = Request::get("https://postman-echo.com/delay/1").body(Body::empty())?; 10 | let result = Client::new() 11 | .send(request) 12 | .timeout(Duration::from_millis(500)) 13 | .await; 14 | 15 | assert!(result.is_err(), "response should be an error"); 16 | let error = result.unwrap_err(); 17 | assert!( 18 | matches!(error.kind(), std::io::ErrorKind::TimedOut), 19 | "expected TimedOut error, got: {error:?>}" 20 | ); 21 | 22 | Ok(()) 23 | } 24 | -------------------------------------------------------------------------------- /aws/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wstd-aws" 3 | description = "AWS rust sdk support for Wasm Components and WASI 0.2, based on wstd" 4 | version.workspace = true 5 | edition.workspace = true 6 | license.workspace = true 7 | repository.workspace = true 8 | keywords.workspace = true 9 | categories.workspace = true 10 | rust-version.workspace = true 11 | authors.workspace = true 12 | 13 | [dependencies] 14 | anyhow.workspace = true 15 | aws-smithy-async = { workspace = true } 16 | aws-smithy-types = { workspace = true, features = ["http-body-1-x"] } 17 | aws-smithy-runtime-api = { workspace = true, features = ["client", "http-1x"] } 18 | http-body-util.workspace = true 19 | sync_wrapper = { workspace = true, features = ["futures"] } 20 | wstd.workspace = true 21 | 22 | [dev-dependencies] 23 | aws-config.workspace = true 24 | aws-sdk-s3.workspace = true 25 | clap.workspace = true 26 | -------------------------------------------------------------------------------- /test-programs/tests/axum_hello_world.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | 3 | #[test_log::test] 4 | fn hello_world() -> Result<()> { 5 | run(test_programs::axum::HELLO_WORLD) 6 | } 7 | 8 | #[test_log::test] 9 | fn hello_world_nomacro() -> Result<()> { 10 | run(test_programs::axum::HELLO_WORLD_NOMACRO) 11 | } 12 | 13 | // The hello_world.rs and hello_world_nomacro.rs are identical in 14 | // functionality 15 | fn run(guest: &str) -> Result<()> { 16 | // Run wasmtime serve. 17 | let _serve = test_programs::WasmtimeServe::new(guest)?; 18 | 19 | // Test each path in the server: 20 | 21 | // TEST / handler 22 | // Response body is the hard-coded default 23 | let body: String = ureq::get("http://127.0.0.1:8081") 24 | .call()? 25 | .body_mut() 26 | .read_to_string()?; 27 | assert!(body.contains("

Hello, World!

")); 28 | 29 | Ok(()) 30 | } 31 | -------------------------------------------------------------------------------- /src/io/copy.rs: -------------------------------------------------------------------------------- 1 | use crate::io::{AsyncRead, AsyncWrite}; 2 | 3 | /// Copy bytes from a reader to a writer. 4 | pub async fn copy(mut reader: R, mut writer: W) -> crate::io::Result<()> 5 | where 6 | R: AsyncRead, 7 | W: AsyncWrite, 8 | { 9 | // Optimized path when we have an `AsyncInputStream` and an 10 | // `AsyncOutputStream`. 11 | if let Some(reader) = reader.as_async_input_stream() 12 | && let Some(writer) = writer.as_async_output_stream() 13 | { 14 | reader.copy_to(writer).await?; 15 | return Ok(()); 16 | } 17 | 18 | // Unoptimized case: read the input and then write it. 19 | let mut buf = [0; 1024]; 20 | 'read: loop { 21 | let bytes_read = reader.read(&mut buf).await?; 22 | if bytes_read == 0 { 23 | break 'read Ok(()); 24 | } 25 | writer.write_all(&buf[0..bytes_read]).await?; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/http_get_json.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | use std::error::Error; 3 | use wstd::http::{Body, Client, Request}; 4 | 5 | #[derive(Deserialize)] 6 | struct Echo { 7 | url: String, 8 | } 9 | 10 | #[wstd::test] 11 | async fn main() -> Result<(), Box> { 12 | let request = Request::get("https://postman-echo.com/get").body(Body::empty())?; 13 | 14 | let response = Client::new().send(request).await?; 15 | 16 | let content_type = response 17 | .headers() 18 | .get("Content-Type") 19 | .ok_or("response expected to have Content-Type header")?; 20 | assert_eq!(content_type, "application/json; charset=utf-8"); 21 | 22 | let Echo { url } = response.into_body().json::().await?; 23 | assert!( 24 | url.contains("postman-echo.com/get"), 25 | "expected body url to contain the authority and path, got: {url}" 26 | ); 27 | 28 | Ok(()) 29 | } 30 | -------------------------------------------------------------------------------- /tests/http_first_byte_timeout.rs: -------------------------------------------------------------------------------- 1 | use wstd::http::{Body, Client, Request, error::ErrorCode}; 2 | 3 | #[wstd::main] 4 | async fn main() -> Result<(), Box> { 5 | // Set first byte timeout to 1/2 second. 6 | let mut client = Client::new(); 7 | client.set_first_byte_timeout(std::time::Duration::from_millis(500)); 8 | // This get request will connect to the server, which will then wait 1 second before 9 | // returning a response. 10 | let request = Request::get("https://postman-echo.com/delay/1").body(Body::empty())?; 11 | let result = client.send(request).await; 12 | 13 | assert!(result.is_err(), "response should be an error"); 14 | let error = result.unwrap_err(); 15 | assert!( 16 | matches!( 17 | error.downcast_ref::(), 18 | Some(ErrorCode::ConnectionReadTimeout) 19 | ), 20 | "expected ConnectionReadTimeout error, got: {error:?>}" 21 | ); 22 | 23 | Ok(()) 24 | } 25 | -------------------------------------------------------------------------------- /src/future/mod.rs: -------------------------------------------------------------------------------- 1 | //! Asynchronous values. 2 | //! 3 | //! # Cancellation 4 | //! 5 | //! Futures can be cancelled by dropping them before they finish executing. This 6 | //! is useful when we're no longer interested in the result of an operation, as 7 | //! it allows us to stop doing needless work. This also means that a future may cancel at any `.await` point, and so just 8 | //! like with `?` we have to be careful to roll back local state if our future 9 | //! halts there. 10 | //! 11 | //! 12 | //! ```no_run 13 | //! use futures_lite::prelude::*; 14 | //! use wstd::prelude::*; 15 | //! use wstd::time::Duration; 16 | //! 17 | //! #[wstd::main] 18 | //! async fn main() { 19 | //! let mut counter = 0; 20 | //! let value = async { "meow" } 21 | //! .delay(Duration::from_millis(100)) 22 | //! .timeout(Duration::from_millis(200)) 23 | //! .await; 24 | //! 25 | //! assert_eq!(value.unwrap(), "meow"); 26 | //! } 27 | //! ``` 28 | 29 | mod delay; 30 | mod future_ext; 31 | mod timeout; 32 | 33 | pub use delay::Delay; 34 | pub use future_ext::FutureExt; 35 | pub use timeout::Timeout; 36 | -------------------------------------------------------------------------------- /src/http/fields.rs: -------------------------------------------------------------------------------- 1 | pub use http::header::{HeaderMap, HeaderName, HeaderValue}; 2 | 3 | use super::{Error, error::Context}; 4 | use wasip2::http::types::Fields; 5 | 6 | pub(crate) fn header_map_from_wasi(wasi_fields: Fields) -> Result { 7 | let mut output = HeaderMap::new(); 8 | for (key, value) in wasi_fields.entries() { 9 | let key = 10 | HeaderName::from_bytes(key.as_bytes()).with_context(|| format!("header name {key}"))?; 11 | let value = 12 | HeaderValue::from_bytes(&value).with_context(|| format!("header value for {key}"))?; 13 | output.append(key, value); 14 | } 15 | Ok(output) 16 | } 17 | 18 | pub(crate) fn header_map_to_wasi(header_map: &HeaderMap) -> Result { 19 | let wasi_fields = Fields::new(); 20 | for (key, value) in header_map { 21 | // Unwrap because `HeaderMap` has already validated the headers. 22 | wasi_fields 23 | .append(key.as_str(), value.as_bytes()) 24 | .with_context(|| format!("wasi rejected header `{key}: {value:?}`"))? 25 | } 26 | Ok(wasi_fields) 27 | } 28 | -------------------------------------------------------------------------------- /src/net/mod.rs: -------------------------------------------------------------------------------- 1 | //! Async network abstractions. 2 | 3 | use std::io::{self, ErrorKind}; 4 | use wasip2::sockets::network::ErrorCode; 5 | 6 | mod tcp_listener; 7 | mod tcp_stream; 8 | 9 | pub use tcp_listener::*; 10 | pub use tcp_stream::*; 11 | 12 | fn to_io_err(err: ErrorCode) -> io::Error { 13 | match err { 14 | ErrorCode::Unknown => ErrorKind::Other.into(), 15 | ErrorCode::AccessDenied => ErrorKind::PermissionDenied.into(), 16 | ErrorCode::NotSupported => ErrorKind::Unsupported.into(), 17 | ErrorCode::InvalidArgument => ErrorKind::InvalidInput.into(), 18 | ErrorCode::OutOfMemory => ErrorKind::OutOfMemory.into(), 19 | ErrorCode::Timeout => ErrorKind::TimedOut.into(), 20 | ErrorCode::WouldBlock => ErrorKind::WouldBlock.into(), 21 | ErrorCode::InvalidState => ErrorKind::InvalidData.into(), 22 | ErrorCode::AddressInUse => ErrorKind::AddrInUse.into(), 23 | ErrorCode::ConnectionRefused => ErrorKind::ConnectionRefused.into(), 24 | ErrorCode::ConnectionReset => ErrorKind::ConnectionReset.into(), 25 | ErrorCode::ConnectionAborted => ErrorKind::ConnectionAborted.into(), 26 | ErrorCode::ConcurrencyConflict => ErrorKind::AlreadyExists.into(), 27 | _ => ErrorKind::Other.into(), 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/http_post_json.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use std::error::Error; 3 | use wstd::http::{Body, Client, HeaderValue, Request}; 4 | 5 | #[derive(Serialize)] 6 | struct TestData { 7 | test: String, 8 | } 9 | 10 | #[derive(Deserialize)] 11 | struct Echo { 12 | url: String, 13 | } 14 | 15 | #[wstd::test] 16 | async fn main() -> Result<(), Box> { 17 | let test_data = TestData { 18 | test: "data".to_string(), 19 | }; 20 | let mut request = 21 | Request::post("https://postman-echo.com/post").body(Body::from_json(&test_data)?)?; 22 | 23 | request.headers_mut().insert( 24 | "Content-Type", 25 | HeaderValue::from_static("application/json; charset=utf-8"), 26 | ); 27 | 28 | let response = Client::new().send(request).await?; 29 | 30 | let content_type = response 31 | .headers() 32 | .get("Content-Type") 33 | .ok_or("response expected to have Content-Type header")?; 34 | assert_eq!(content_type, "application/json; charset=utf-8"); 35 | 36 | let Echo { url } = response.into_body().json::().await?; 37 | assert!( 38 | url.contains("postman-echo.com/post"), 39 | "expected body url to contain the authority and path, got: {url}" 40 | ); 41 | 42 | Ok(()) 43 | } 44 | -------------------------------------------------------------------------------- /src/http/response.rs: -------------------------------------------------------------------------------- 1 | use http::StatusCode; 2 | use wasip2::http::types::IncomingResponse; 3 | 4 | use crate::http::body::{Body, BodyHint}; 5 | use crate::http::error::{Context, Error}; 6 | use crate::http::fields::{HeaderMap, header_map_from_wasi}; 7 | 8 | pub use http::response::{Builder, Response}; 9 | 10 | pub(crate) fn try_from_incoming(incoming: IncomingResponse) -> Result, Error> { 11 | let headers: HeaderMap = header_map_from_wasi(incoming.headers())?; 12 | // TODO: Does WASI guarantee that the incoming status is valid? 13 | let status = StatusCode::from_u16(incoming.status()) 14 | .map_err(|err| anyhow::anyhow!("wasi provided invalid status code ({err})"))?; 15 | 16 | let hint = BodyHint::from_headers(&headers)?; 17 | // `body_stream` is a child of `incoming_body` which means we cannot 18 | // drop the parent before we drop the child 19 | let incoming_body = incoming 20 | .consume() 21 | .expect("cannot call `consume` twice on incoming response"); 22 | let body = Body::from_incoming(incoming_body, hint); 23 | 24 | let mut builder = Response::builder().status(status); 25 | 26 | if let Some(headers_mut) = builder.headers_mut() { 27 | *headers_mut = headers; 28 | } 29 | 30 | builder.body(body).context("building response") 31 | } 32 | -------------------------------------------------------------------------------- /src/runtime/mod.rs: -------------------------------------------------------------------------------- 1 | //! Async event loop support. 2 | //! 3 | //! The way to use this is to call [`block_on()`]. Inside the future, [`Reactor::current`] 4 | //! will give an instance of the [`Reactor`] running the event loop, which can be 5 | //! to [`AsyncPollable::wait_for`] instances of 6 | //! [`wasip2::Pollable`](https://docs.rs/wasi/latest/wasi/io/poll/struct.Pollable.html). 7 | //! This will automatically wait for the futures to resolve, and call the 8 | //! necessary wakers to work. 9 | 10 | #![deny(missing_debug_implementations, nonstandard_style)] 11 | #![warn(missing_docs, unreachable_pub)] 12 | 13 | mod block_on; 14 | mod reactor; 15 | 16 | pub use ::async_task::Task; 17 | pub use block_on::block_on; 18 | pub use reactor::{AsyncPollable, Reactor, WaitFor}; 19 | use std::cell::RefCell; 20 | 21 | // There are no threads in WASI 0.2, so this is just a safe way to thread a single reactor to all 22 | // use sites in the background. 23 | std::thread_local! { 24 | pub(crate) static REACTOR: RefCell> = const { RefCell::new(None) }; 25 | } 26 | 27 | /// Spawn a `Future` as a `Task` on the current `Reactor`. 28 | /// 29 | /// Panics if called from outside `block_on`. 30 | pub fn spawn(fut: F) -> Task 31 | where 32 | F: std::future::Future + 'static, 33 | T: 'static, 34 | { 35 | Reactor::current().spawn(fut) 36 | } 37 | -------------------------------------------------------------------------------- /src/http/method.rs: -------------------------------------------------------------------------------- 1 | use wasip2::http::types::Method as WasiMethod; 2 | 3 | pub use http::Method; 4 | use http::method::InvalidMethod; 5 | 6 | pub(crate) fn to_wasi_method(value: Method) -> WasiMethod { 7 | match value { 8 | Method::GET => WasiMethod::Get, 9 | Method::HEAD => WasiMethod::Head, 10 | Method::POST => WasiMethod::Post, 11 | Method::PUT => WasiMethod::Put, 12 | Method::DELETE => WasiMethod::Delete, 13 | Method::CONNECT => WasiMethod::Connect, 14 | Method::OPTIONS => WasiMethod::Options, 15 | Method::TRACE => WasiMethod::Trace, 16 | Method::PATCH => WasiMethod::Patch, 17 | other => WasiMethod::Other(other.as_str().to_owned()), 18 | } 19 | } 20 | 21 | pub(crate) fn from_wasi_method(value: WasiMethod) -> Result { 22 | Ok(match value { 23 | WasiMethod::Get => Method::GET, 24 | WasiMethod::Head => Method::HEAD, 25 | WasiMethod::Post => Method::POST, 26 | WasiMethod::Put => Method::PUT, 27 | WasiMethod::Delete => Method::DELETE, 28 | WasiMethod::Connect => Method::CONNECT, 29 | WasiMethod::Options => Method::OPTIONS, 30 | WasiMethod::Trace => Method::TRACE, 31 | WasiMethod::Patch => Method::PATCH, 32 | WasiMethod::Other(s) => Method::from_bytes(s.as_bytes())?, 33 | }) 34 | } 35 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to wstd 2 | 3 | Wstd is a [Bytecode Alliance] project. It follows the Bytecode Alliance's 4 | [Code of Conduct] and [Organizational Code of Conduct]. 5 | 6 | [Bytecode Alliance]: https://bytecodealliance.org/ 7 | [Code of Conduct]: CODE_OF_CONDUCT.md 8 | [Organizational Code of Conduct]: ORG_CODE_OF_CONDUCT.md 9 | 10 | ## File an Issue 11 | 12 | Please [file an issue] to report bugs in wstd. Please provide the smallest 13 | reasonable context to reproduce your bug. If you cannot share the context of 14 | your bug, you may still open a bug, but we may end up needing to close it if 15 | you can't provide a way to reproduce. 16 | 17 | Please [file an issue] if the [rust docs] or [examples] are incomplete, ambigious, 18 | misleading, or incorrect. 19 | 20 | Before creating a nontrivial PR to wstd, we suggest you first [file an issue] 21 | to propose and discuss the change. 22 | 23 | [file an issue]: https://github.com/bytecodealliance/wstd/issues 24 | [rust docs]: https://docs.rs/wstd/latest/wstd 25 | [examples]: https://github.com/bytecodealliance/wstd/tree/main/examples 26 | 27 | ## Join Our Chat 28 | 29 | We chat about wstd development on the Bytecode Alliance Zulip -- [join us!][zulip]. You 30 | can also join the [wstd stream] directly. 31 | 32 | [zulip]: https://bytecodealliance.zulipchat.com/ 33 | [wstd stream]: https://bytecodealliance.zulipchat.com/#narrow/channel/514491-wstd 34 | -------------------------------------------------------------------------------- /src/io/write.rs: -------------------------------------------------------------------------------- 1 | use crate::io; 2 | 3 | /// Write bytes to a sink. 4 | pub trait AsyncWrite { 5 | // Required methods 6 | async fn write(&mut self, buf: &[u8]) -> io::Result; 7 | async fn flush(&mut self) -> io::Result<()>; 8 | 9 | async fn write_all(&mut self, buf: &[u8]) -> io::Result<()> { 10 | let mut to_write = &buf[0..]; 11 | loop { 12 | let bytes_written = self.write(to_write).await?; 13 | to_write = &to_write[bytes_written..]; 14 | if to_write.is_empty() { 15 | return Ok(()); 16 | } 17 | } 18 | } 19 | 20 | // If the `AsyncWrite` implementation is an unbuffered wrapper around an 21 | // `AsyncOutputStream`, some I/O operations can be more efficient. 22 | #[inline] 23 | fn as_async_output_stream(&self) -> Option<&io::AsyncOutputStream> { 24 | None 25 | } 26 | } 27 | 28 | impl AsyncWrite for &mut W { 29 | #[inline] 30 | async fn write(&mut self, buf: &[u8]) -> io::Result { 31 | (**self).write(buf).await 32 | } 33 | 34 | #[inline] 35 | async fn flush(&mut self) -> io::Result<()> { 36 | (**self).flush().await 37 | } 38 | 39 | #[inline] 40 | async fn write_all(&mut self, buf: &[u8]) -> io::Result<()> { 41 | (**self).write_all(buf).await 42 | } 43 | 44 | #[inline] 45 | fn as_async_output_stream(&self) -> Option<&io::AsyncOutputStream> { 46 | (**self).as_async_output_stream() 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /.github/actions/install-rust/action.yml: -------------------------------------------------------------------------------- 1 | name: 'Install Rust toolchain' 2 | description: 'Install a rust toolchain' 3 | 4 | inputs: 5 | toolchain: 6 | description: 'Default toolchan to install' 7 | required: false 8 | default: 'stable' 9 | 10 | runs: 11 | using: composite 12 | steps: 13 | - name: Install Rust 14 | shell: bash 15 | id: select 16 | run: | 17 | # Determine MSRV as N in `1.N.0` by looking at the `rust-version` 18 | # located in the root `Cargo.toml`. 19 | msrv=$(grep 'rust-version.*1' Cargo.toml | sed 's/.*\.\([0-9]*\)\..*/\1/') 20 | 21 | if [ "${{ inputs.toolchain }}" = "msrv" ]; then 22 | echo "version=1.$msrv.0" >> "$GITHUB_OUTPUT" 23 | else 24 | echo "version=${{ inputs.toolchain }}" >> "$GITHUB_OUTPUT" 25 | fi 26 | 27 | - name: Install Rust 28 | shell: bash 29 | run: | 30 | rustup set profile minimal 31 | rustup update "${{ steps.select.outputs.version }}" --no-self-update 32 | rustup default "${{ steps.select.outputs.version }}" 33 | 34 | # Save disk space by avoiding incremental compilation. Also turn down 35 | # debuginfo from 2 to 0 to help save disk space. 36 | cat >> "$GITHUB_ENV" <> "$GITHUB_ENV" 44 | -------------------------------------------------------------------------------- /src/io/read.rs: -------------------------------------------------------------------------------- 1 | use crate::io; 2 | 3 | const CHUNK_SIZE: usize = 2048; 4 | 5 | /// Read bytes from a source. 6 | pub trait AsyncRead { 7 | async fn read(&mut self, buf: &mut [u8]) -> io::Result; 8 | async fn read_to_end(&mut self, buf: &mut Vec) -> io::Result { 9 | // total bytes written to buf 10 | let mut n = 0; 11 | 12 | loop { 13 | // grow buf if empty 14 | if buf.len() == n { 15 | buf.resize(n + CHUNK_SIZE, 0u8); 16 | } 17 | 18 | let len = self.read(&mut buf[n..]).await?; 19 | if len == 0 { 20 | buf.truncate(n); 21 | return Ok(n); 22 | } 23 | 24 | n += len; 25 | } 26 | } 27 | 28 | // If the `AsyncRead` implementation is an unbuffered wrapper around an 29 | // `AsyncInputStream`, some I/O operations can be more efficient. 30 | #[inline] 31 | fn as_async_input_stream(&self) -> Option<&io::AsyncInputStream> { 32 | None 33 | } 34 | } 35 | 36 | impl AsyncRead for &mut R { 37 | #[inline] 38 | async fn read(&mut self, buf: &mut [u8]) -> io::Result { 39 | (**self).read(buf).await 40 | } 41 | 42 | #[inline] 43 | async fn read_to_end(&mut self, buf: &mut Vec) -> io::Result { 44 | (**self).read_to_end(buf).await 45 | } 46 | 47 | #[inline] 48 | fn as_async_input_stream(&self) -> Option<&io::AsyncInputStream> { 49 | (**self).as_async_input_stream() 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /axum/macro/src/lib.rs: -------------------------------------------------------------------------------- 1 | use proc_macro::TokenStream; 2 | use quote::{quote, quote_spanned}; 3 | use syn::{ItemFn, parse_macro_input, spanned::Spanned}; 4 | 5 | #[proc_macro_attribute] 6 | pub fn attr_macro_http_server(_attr: TokenStream, item: TokenStream) -> TokenStream { 7 | let input = parse_macro_input!(item as ItemFn); 8 | 9 | if input.sig.ident != "main" { 10 | return quote_spanned! { input.sig.ident.span()=> 11 | compile_error!("only `fn main` can be used for #[wstd_axum::http_server]"); 12 | } 13 | .into(); 14 | } 15 | 16 | if !input.sig.inputs.is_empty() { 17 | return quote_spanned! { input.sig.inputs.span()=> 18 | compile_error!("arguments to main are not supported"); 19 | } 20 | .into(); 21 | } 22 | let (async_, call) = if input.sig.asyncness.is_some() { 23 | (quote!(async), quote!(__make_service().await)) 24 | } else { 25 | (quote!(), quote!(__make_service())) 26 | }; 27 | let attrs = input.attrs; 28 | let output = input.sig.output; 29 | let block = input.block; 30 | quote! { 31 | #[::wstd::http_server] 32 | pub async fn main( 33 | __request: ::wstd::http::Request<::wstd::http::Body> 34 | ) -> ::wstd::http::error::Result<::wstd::http::Response<::wstd::http::Body>> { 35 | 36 | #(#attrs)* 37 | #async_ fn __make_service() #output { 38 | #block 39 | } 40 | 41 | let __service = #call; 42 | 43 | ::wstd_axum::serve(__request, __service).await 44 | } 45 | } 46 | .into() 47 | } 48 | -------------------------------------------------------------------------------- /tests/http_post.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | use wstd::http::{Client, HeaderValue, Request}; 3 | 4 | #[wstd::test] 5 | async fn main() -> Result<(), Box> { 6 | let request = Request::post("https://postman-echo.com/post") 7 | .header( 8 | "content-type", 9 | HeaderValue::from_str("application/json; charset=utf-8")?, 10 | ) 11 | .body("{\"test\": \"data\"}")?; 12 | 13 | let response = Client::new().send(request).await?; 14 | 15 | let content_type = response 16 | .headers() 17 | .get("Content-Type") 18 | .ok_or("response expected to have Content-Type header")?; 19 | assert_eq!(content_type, "application/json; charset=utf-8"); 20 | 21 | let mut body = response.into_body(); 22 | let val: serde_json::Value = body.json().await?; 23 | 24 | let body_url = val 25 | .get("url") 26 | .ok_or("body json has url")? 27 | .as_str() 28 | .ok_or("body json url is str")?; 29 | assert!( 30 | body_url.contains("postman-echo.com/post"), 31 | "expected body url to contain the authority and path, got: {body_url}" 32 | ); 33 | 34 | let posted_json = val 35 | .get("json") 36 | .ok_or("body json has 'json' key")? 37 | .as_object() 38 | .ok_or_else(|| format!("body json 'json' is object. got {val:?}"))?; 39 | 40 | assert_eq!(posted_json.len(), 1); 41 | assert_eq!( 42 | posted_json 43 | .get("test") 44 | .ok_or("returned json has 'test' key")? 45 | .as_str() 46 | .ok_or("returned json 'test' key should be str value")?, 47 | "data" 48 | ); 49 | 50 | Ok(()) 51 | } 52 | -------------------------------------------------------------------------------- /test-programs/tests/axum_weather.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use serde_json::Value; 3 | 4 | const CITY: &str = "portland"; 5 | const COUNT: usize = 2; 6 | 7 | #[test_log::test] 8 | fn weather() -> Result<()> { 9 | // Run wasmtime serve. 10 | let _serve = test_programs::WasmtimeServe::new(test_programs::axum::WEATHER)?; 11 | 12 | // TEST /weather weather handler 13 | let body = ureq::get(format!( 14 | "http://127.0.0.1:8081/weather?city={CITY}&count={COUNT}" 15 | )) 16 | .call()? 17 | .body_mut() 18 | .read_json::()?; 19 | let array = body.as_array().expect("json body is an array"); 20 | assert_eq!(array.len(), COUNT); 21 | let item_0 = &array[0]; 22 | let loc_0 = item_0 23 | .get("location") 24 | .expect("item 0 has `location`") 25 | .as_object() 26 | .expect("location 0 is object"); 27 | let qn_0 = loc_0 28 | .get("qualified_name") 29 | .expect("location has qualified name") 30 | .as_str() 31 | .expect("name is string"); 32 | assert!( 33 | qn_0.contains("Multnomah"), 34 | "{qn_0:?} should contain substring 'Multnomah'" 35 | ); 36 | 37 | let item_1 = &array[1]; 38 | let loc_1 = item_1 39 | .get("location") 40 | .expect("item 1 has `location`") 41 | .as_object() 42 | .expect("location 1 is object"); 43 | let qn_1 = loc_1 44 | .get("qualified_name") 45 | .expect("location has qualified name") 46 | .as_str() 47 | .expect("name is string"); 48 | assert!( 49 | qn_1.contains("Cumberland"), 50 | "{qn_1:?} should contain substring 'Cumberland'" 51 | ); 52 | 53 | Ok(()) 54 | } 55 | -------------------------------------------------------------------------------- /test-programs/tests/tcp_stream_client.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Context, Result}; 2 | use std::net::{Shutdown, TcpListener}; 3 | use std::process::{Command, Stdio}; 4 | 5 | #[test_log::test] 6 | fn tcp_stream_client() -> Result<()> { 7 | use std::io::{Read, Write}; 8 | 9 | let server = TcpListener::bind("127.0.0.1:8082").context("binding temporary test server")?; 10 | let addr = server 11 | .local_addr() 12 | .context("getting local listener address")?; 13 | 14 | let child = Command::new("wasmtime") 15 | .arg("run") 16 | .arg("-Sinherit-network") 17 | .arg(test_programs::TCP_STREAM_CLIENT) 18 | .arg(addr.to_string()) 19 | .stdout(Stdio::piped()) 20 | .spawn() 21 | .context("spawning wasmtime component")?; 22 | 23 | let (mut server_stream, _addr) = server 24 | .accept() 25 | .context("accepting TCP connection from component")?; 26 | 27 | let mut buf = [0u8; 5]; 28 | server_stream 29 | .read_exact(&mut buf) 30 | .context("reading ping message")?; 31 | assert_eq!(&buf, b"ping\n", "expected ping from component"); 32 | 33 | server_stream 34 | .write_all(b"pong\n") 35 | .context("writing reply")?; 36 | server_stream.flush().context("flushing")?; 37 | 38 | server_stream 39 | .shutdown(Shutdown::Both) 40 | .context("shutting down connection")?; 41 | 42 | let output = child 43 | .wait_with_output() 44 | .context("waiting for component exit")?; 45 | 46 | assert!( 47 | output.status.success(), 48 | "\nComponent exited abnormally (stderr:\n{})", 49 | String::from_utf8_lossy(&output.stderr) 50 | ); 51 | 52 | Ok(()) 53 | } 54 | -------------------------------------------------------------------------------- /tests/http_get.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | use wstd::http::{Body, Client, HeaderValue, Request}; 3 | 4 | #[wstd::test] 5 | async fn main() -> Result<(), Box> { 6 | let request = Request::get("https://postman-echo.com/get") 7 | .header("my-header", HeaderValue::from_str("my-value")?) 8 | .body(Body::empty())?; 9 | 10 | let response = Client::new().send(request).await?; 11 | 12 | let content_type = response 13 | .headers() 14 | .get("Content-Type") 15 | .ok_or("response expected to have Content-Type header")?; 16 | assert_eq!(content_type, "application/json; charset=utf-8"); 17 | 18 | let mut body = response.into_body(); 19 | let body_len = body 20 | .content_length() 21 | .ok_or("GET postman-echo.com/get is supposed to provide a content-length")?; 22 | 23 | let contents = body.contents().await?; 24 | 25 | assert_eq!( 26 | contents.len() as u64, 27 | body_len, 28 | "contents length should match content-length" 29 | ); 30 | 31 | let val: serde_json::Value = serde_json::from_slice(contents)?; 32 | let body_url = val 33 | .get("url") 34 | .ok_or("body json has url")? 35 | .as_str() 36 | .ok_or("body json url is str")?; 37 | assert!( 38 | body_url.contains("postman-echo.com/get"), 39 | "expected body url to contain the authority and path, got: {body_url}" 40 | ); 41 | 42 | assert_eq!( 43 | val.get("headers") 44 | .ok_or("body json has headers")? 45 | .get("my-header") 46 | .ok_or("headers contains my-header")? 47 | .as_str() 48 | .ok_or("my-header is a str")?, 49 | "my-value" 50 | ); 51 | 52 | Ok(()) 53 | } 54 | -------------------------------------------------------------------------------- /src/future/timeout.rs: -------------------------------------------------------------------------------- 1 | use crate::time::utils::timeout_err; 2 | 3 | use std::future::Future; 4 | use std::io; 5 | use std::pin::Pin; 6 | use std::task::{Context, Poll}; 7 | 8 | use pin_project_lite::pin_project; 9 | 10 | pin_project! { 11 | /// A future that times out after a duration of time. 12 | /// 13 | /// This `struct` is created by the [`timeout`] method on [`FutureExt`]. See its 14 | /// documentation for more. 15 | /// 16 | /// [`timeout`]: crate::future::FutureExt::timeout 17 | /// [`FutureExt`]: crate::future::futureExt 18 | #[must_use = "futures do nothing unless polled or .awaited"] 19 | pub struct Timeout { 20 | #[pin] 21 | future: F, 22 | #[pin] 23 | deadline: D, 24 | completed: bool, 25 | } 26 | } 27 | 28 | impl Timeout { 29 | pub(super) fn new(future: F, deadline: D) -> Self { 30 | Self { 31 | future, 32 | deadline, 33 | completed: false, 34 | } 35 | } 36 | } 37 | 38 | impl Future for Timeout { 39 | type Output = io::Result; 40 | 41 | fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { 42 | let this = self.project(); 43 | 44 | assert!(!*this.completed, "future polled after completing"); 45 | 46 | match this.future.poll(cx) { 47 | Poll::Ready(v) => { 48 | *this.completed = true; 49 | Poll::Ready(Ok(v)) 50 | } 51 | Poll::Pending => match this.deadline.poll(cx) { 52 | Poll::Ready(_) => { 53 | *this.completed = true; 54 | Poll::Ready(Err(timeout_err("future timed out"))) 55 | } 56 | Poll::Pending => Poll::Pending, 57 | }, 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/future/delay.rs: -------------------------------------------------------------------------------- 1 | use std::future::Future; 2 | use std::pin::Pin; 3 | use std::task::{Context, Poll, ready}; 4 | 5 | use pin_project_lite::pin_project; 6 | 7 | pin_project! { 8 | /// Suspends a future until the specified deadline. 9 | /// 10 | /// This `struct` is created by the [`delay`] method on [`FutureExt`]. See its 11 | /// documentation for more. 12 | /// 13 | /// [`delay`]: crate::future::FutureExt::delay 14 | /// [`FutureExt`]: crate::future::futureExt 15 | #[must_use = "futures do nothing unless polled or .awaited"] 16 | pub struct Delay { 17 | #[pin] 18 | future: F, 19 | #[pin] 20 | deadline: D, 21 | state: State, 22 | } 23 | } 24 | 25 | /// The internal state 26 | #[derive(Debug)] 27 | enum State { 28 | Started, 29 | PollFuture, 30 | Completed, 31 | } 32 | 33 | impl Delay { 34 | pub(super) fn new(future: F, deadline: D) -> Self { 35 | Self { 36 | future, 37 | deadline, 38 | state: State::Started, 39 | } 40 | } 41 | } 42 | 43 | impl Future for Delay { 44 | type Output = F::Output; 45 | 46 | fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { 47 | let mut this = self.project(); 48 | loop { 49 | match this.state { 50 | State::Started => { 51 | ready!(this.deadline.as_mut().poll(cx)); 52 | *this.state = State::PollFuture; 53 | } 54 | State::PollFuture => { 55 | let value = ready!(this.future.as_mut().poll(cx)); 56 | *this.state = State::Completed; 57 | return Poll::Ready(value); 58 | } 59 | State::Completed => panic!("future polled after completing"), 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/io/seek.rs: -------------------------------------------------------------------------------- 1 | /// The `Seek` trait provides a cursor which can be moved within a stream of 2 | /// bytes. 3 | pub trait AsyncSeek { 4 | /// Seek to an offset, in bytes, in a stream. 5 | async fn seek(&mut self, pos: SeekFrom) -> super::Result; 6 | 7 | /// Rewind to the beginning of a stream. 8 | async fn rewind(&mut self) -> super::Result<()> { 9 | self.seek(SeekFrom::Start(0)).await?; 10 | Ok(()) 11 | } 12 | 13 | /// Returns the length of this stream (in bytes). 14 | async fn stream_len(&mut self) -> super::Result { 15 | let old_pos = self.stream_position().await?; 16 | let len = self.seek(SeekFrom::End(0)).await?; 17 | 18 | // Avoid seeking a third time when we were already at the end of the 19 | // stream. The branch is usually way cheaper than a seek operation. 20 | if old_pos != len { 21 | self.seek(SeekFrom::Start(old_pos)).await?; 22 | } 23 | 24 | Ok(len) 25 | } 26 | 27 | /// Returns the current seek position from the start of the stream. 28 | async fn stream_position(&mut self) -> super::Result { 29 | self.seek(SeekFrom::Current(0)).await 30 | } 31 | 32 | /// Seeks relative to the current position. 33 | async fn seek_relative(&mut self, offset: i64) -> super::Result<()> { 34 | self.seek(SeekFrom::Current(offset)).await?; 35 | Ok(()) 36 | } 37 | } 38 | 39 | /// Enumeration of possible methods to seek within an I/O object. 40 | /// 41 | /// It is used by the [`AsyncSeek`] trait. 42 | #[derive(Copy, PartialEq, Eq, Clone, Debug)] 43 | pub enum SeekFrom { 44 | /// Sets the offset to the provided number of bytes. 45 | Start(u64), 46 | 47 | /// Sets the offset to the size of this object plus the specified number of 48 | /// bytes. 49 | End(i64), 50 | 51 | /// Sets the offset to the current position plus the specified number of 52 | /// bytes. 53 | Current(i64), 54 | } 55 | -------------------------------------------------------------------------------- /test-programs/tests/aws_s3.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use std::path::Path; 3 | use std::process::Command; 4 | 5 | fn run_s3_example() -> Command { 6 | let mut command = Command::new("wasmtime"); 7 | command.arg("run"); 8 | command.arg("-Shttp"); 9 | command.args(["--env", "AWS_ACCESS_KEY_ID"]); 10 | command.args(["--env", "AWS_SECRET_ACCESS_KEY"]); 11 | command.args(["--env", "AWS_SESSION_TOKEN"]); 12 | command.args(["--dir", ".::."]); 13 | command.arg(test_programs::aws::S3); 14 | command 15 | } 16 | 17 | #[test_log::test] 18 | #[cfg_attr(feature = "no-aws", ignore)] 19 | fn aws_s3() -> Result<()> { 20 | // bucket list command 21 | let output = run_s3_example() 22 | .arg(format!( 23 | "--region={}", 24 | std::env::var("AWS_REGION").unwrap_or_else(|_| "us-west-2".to_owned()) 25 | )) 26 | .arg(format!( 27 | "--bucket={}", 28 | std::env::var("WSTD_EXAMPLE_BUCKET") 29 | .unwrap_or_else(|_| "wstd-example-bucket".to_owned()) 30 | )) 31 | .arg("list") 32 | .output()?; 33 | println!("{:?}", output); 34 | assert!(output.status.success()); 35 | let stdout = String::from_utf8_lossy(&output.stdout); 36 | assert!(stdout.contains("fluff.jpg")); 37 | assert!(stdout.contains("shoug.jpg")); 38 | 39 | // bucket get command 40 | let output = run_s3_example() 41 | .arg(format!( 42 | "--region={}", 43 | std::env::var("AWS_REGION").unwrap_or_else(|_| "us-west-2".to_owned()) 44 | )) 45 | .arg(format!( 46 | "--bucket={}", 47 | std::env::var("WSTD_EXAMPLE_BUCKET") 48 | .unwrap_or_else(|_| "wstd-example-bucket".to_owned()) 49 | )) 50 | .arg("get") 51 | .arg("shoug.jpg") 52 | .output()?; 53 | println!("{:?}", output); 54 | assert!(output.status.success()); 55 | 56 | assert!(Path::new("shoug.jpg").exists()); 57 | 58 | Ok(()) 59 | } 60 | -------------------------------------------------------------------------------- /src/runtime/block_on.rs: -------------------------------------------------------------------------------- 1 | use super::{REACTOR, Reactor}; 2 | 3 | use std::future::Future; 4 | use std::pin::pin; 5 | use std::task::{Context, Poll, Waker}; 6 | 7 | /// Start the event loop. Blocks until the future 8 | pub fn block_on(fut: F) -> F::Output 9 | where 10 | F: Future + 'static, 11 | F::Output: 'static, 12 | { 13 | // Construct the reactor 14 | let reactor = Reactor::new(); 15 | // Store a copy as a singleton to be used elsewhere: 16 | let prev = REACTOR.replace(Some(reactor.clone())); 17 | if prev.is_some() { 18 | panic!("cannot wstd::runtime::block_on inside an existing block_on!") 19 | } 20 | 21 | // Spawn the task onto the reactor. 22 | let root_task = reactor.spawn(fut); 23 | 24 | loop { 25 | match reactor.pop_ready_list() { 26 | // No more work is possible - only a pending pollable could 27 | // possibly create a runnable, and there are none. 28 | None if reactor.pending_pollables_is_empty() => break, 29 | // Block until a pending pollable puts something on the ready 30 | // list. 31 | None => reactor.block_on_pollables(), 32 | Some(runnable) => { 33 | // Run the task popped from the head of the ready list. If the 34 | // task re-inserts itself onto the runlist during execution, 35 | // last_run_awake is a hint that guarantees us the runlist is 36 | // nonempty. 37 | let last_run_awake = runnable.run(); 38 | 39 | // If any task is ready for running, we perform a nonblocking 40 | // check of pollables, giving any tasks waiting on a pollable 41 | // a chance to wake. 42 | if last_run_awake || !reactor.ready_list_is_empty() { 43 | reactor.nonblock_check_pollables(); 44 | } 45 | } 46 | } 47 | } 48 | // Clear the singleton 49 | REACTOR.replace(None); 50 | // Get the result out of the root task 51 | let mut root_task = pin!(root_task); 52 | let mut noop_context = Context::from_waker(Waker::noop()); 53 | match root_task.as_mut().poll(&mut noop_context) { 54 | Poll::Ready(res) => res, 55 | Poll::Pending => { 56 | unreachable!( 57 | "ready list empty, therefore root task should be ready. malformed root task?" 58 | ) 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /test-programs/src/lib.rs: -------------------------------------------------------------------------------- 1 | include!(concat!(env!("OUT_DIR"), "/gen.rs")); 2 | 3 | use std::fs::File; 4 | use std::net::TcpStream; 5 | use std::process::{Child, Command}; 6 | use std::thread::sleep; 7 | use std::time::Duration; 8 | 9 | const DEFAULT_SERVER_PORT: u16 = 8081; 10 | 11 | /// Manages exclusive access to port 8081, and kills the process when dropped 12 | pub struct WasmtimeServe { 13 | #[expect(dead_code, reason = "exists to live for as long as wasmtime process")] 14 | lockfile: File, 15 | process: Child, 16 | } 17 | 18 | impl WasmtimeServe { 19 | /// Run `wasmtime serve -Scli --addr=127.0.0.1:8081` for a given wasm 20 | /// guest filepath. 21 | /// 22 | /// Takes exclusive access to a lockfile so that only one test on a host 23 | /// can use port 8081 at a time. 24 | /// 25 | /// Kills the wasmtime process, and releases the lock, once dropped. 26 | pub fn new(guest: &str) -> std::io::Result { 27 | Self::new_with_config(guest, DEFAULT_SERVER_PORT, &[]) 28 | } 29 | 30 | pub fn new_with_config(guest: &str, port: u16, env_vars: &[&str]) -> std::io::Result { 31 | let mut lockfile = std::env::temp_dir(); 32 | lockfile.push(format!("TEST_PROGRAMS_WASMTIME_SERVE_{port}.lock")); 33 | let lockfile = File::create(&lockfile)?; 34 | lockfile.lock()?; 35 | 36 | // Run wasmtime serve. 37 | // Enable -Scli because we currently don't have a way to build with the 38 | // proxy adapter, so we build with the default adapter. 39 | let mut process = Command::new("wasmtime"); 40 | let listening_addr = format!("127.0.0.1:{port}"); 41 | process 42 | .arg("serve") 43 | .arg("-Scli") 44 | .arg("--addr") 45 | .arg(&listening_addr); 46 | for env_var in env_vars { 47 | process.arg("--env").arg(env_var); 48 | } 49 | let process = process.arg(guest).spawn()?; 50 | let w = WasmtimeServe { lockfile, process }; 51 | 52 | // Clumsily wait for the server to accept connections. 53 | 'wait: loop { 54 | sleep(Duration::from_millis(100)); 55 | if TcpStream::connect(&listening_addr).is_ok() { 56 | break 'wait; 57 | } 58 | } 59 | Ok(w) 60 | } 61 | } 62 | // Wasmtime serve will run until killed. Kill it in a drop impl so the process 63 | // isnt orphaned when the test suite ends (successfully, or unsuccessfully) 64 | impl Drop for WasmtimeServe { 65 | fn drop(&mut self) { 66 | let _ = self.process.kill(); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/time/instant.rs: -------------------------------------------------------------------------------- 1 | use super::{Duration, Wait}; 2 | use std::future::IntoFuture; 3 | use std::ops::{Add, AddAssign, Sub, SubAssign}; 4 | use wasip2::clocks::monotonic_clock; 5 | 6 | /// A measurement of a monotonically nondecreasing clock. Opaque and useful only 7 | /// with Duration. 8 | /// 9 | /// This type wraps `std::time::Duration` so we can implement traits on it 10 | /// without coherence issues, just like if we were implementing this in the 11 | /// stdlib. 12 | #[derive(Debug, PartialEq, PartialOrd, Ord, Eq, Hash, Clone, Copy)] 13 | pub struct Instant(pub(crate) monotonic_clock::Instant); 14 | 15 | impl Instant { 16 | /// Returns an instant corresponding to "now". 17 | /// 18 | /// # Examples 19 | /// 20 | /// ```no_run 21 | /// use wstd::time::Instant; 22 | /// 23 | /// let now = Instant::now(); 24 | /// ``` 25 | #[must_use] 26 | pub fn now() -> Self { 27 | Instant(wasip2::clocks::monotonic_clock::now()) 28 | } 29 | 30 | /// Returns the amount of time elapsed from another instant to this one, or zero duration if 31 | /// that instant is later than this one. 32 | pub fn duration_since(&self, earlier: Instant) -> Duration { 33 | Duration::from_nanos(self.0.checked_sub(earlier.0).unwrap_or_default()) 34 | } 35 | 36 | /// Returns the amount of time elapsed since this instant. 37 | pub fn elapsed(&self) -> Duration { 38 | Instant::now().duration_since(*self) 39 | } 40 | } 41 | 42 | impl Add for Instant { 43 | type Output = Self; 44 | 45 | fn add(self, rhs: Duration) -> Self::Output { 46 | Self(self.0 + rhs.0) 47 | } 48 | } 49 | 50 | impl AddAssign for Instant { 51 | fn add_assign(&mut self, rhs: Duration) { 52 | *self = Self(self.0 + rhs.0) 53 | } 54 | } 55 | 56 | impl Sub for Instant { 57 | type Output = Self; 58 | 59 | fn sub(self, rhs: Duration) -> Self::Output { 60 | Self(self.0 - rhs.0) 61 | } 62 | } 63 | 64 | impl SubAssign for Instant { 65 | fn sub_assign(&mut self, rhs: Duration) { 66 | *self = Self(self.0 - rhs.0) 67 | } 68 | } 69 | 70 | impl IntoFuture for Instant { 71 | type Output = Instant; 72 | 73 | type IntoFuture = Wait; 74 | 75 | fn into_future(self) -> Self::IntoFuture { 76 | crate::task::sleep_until(self) 77 | } 78 | } 79 | 80 | #[cfg(test)] 81 | mod tests { 82 | use super::*; 83 | 84 | #[test] 85 | fn test_duration_since() { 86 | let x = Instant::now(); 87 | let d = Duration::new(456, 789); 88 | let y = x + d; 89 | assert_eq!(y.duration_since(x), d); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /examples/http_server_proxy.rs: -------------------------------------------------------------------------------- 1 | //! Run the example with: 2 | //! ```sh 3 | //! cargo build --example http_server_proxy --target=wasm32-wasip2 4 | //! wasmtime serve -Scli -Shttp --env TARGET_URL=https://example.com/ target/wasm32-wasip2/debug/examples/http_server_proxy.wasm 5 | //! curl --no-buffer -v 127.0.0.1:8080/proxy/ 6 | //! ``` 7 | use wstd::http::body::Body; 8 | use wstd::http::{Client, Error, Request, Response, StatusCode, Uri}; 9 | 10 | const PROXY_PREFIX: &str = "/proxy/"; 11 | 12 | #[wstd::http_server] 13 | async fn main(server_req: Request) -> Result, Error> { 14 | match server_req.uri().path_and_query().unwrap().as_str() { 15 | api_prefixed_path if api_prefixed_path.starts_with(PROXY_PREFIX) => { 16 | // Remove PROXY_PREFIX 17 | let target_url = 18 | std::env::var("TARGET_URL").expect("missing environment variable TARGET_URL"); 19 | let target_url: Uri = format!( 20 | "{target_url}{}", 21 | api_prefixed_path 22 | .strip_prefix(PROXY_PREFIX) 23 | .expect("checked above") 24 | ) 25 | .parse() 26 | .expect("final target url should be parseable"); 27 | println!("Proxying to {target_url}"); 28 | proxy(server_req, target_url).await 29 | } 30 | _ => Ok(http_not_found(server_req)), 31 | } 32 | } 33 | 34 | async fn proxy(server_req: Request, target_url: Uri) -> Result, Error> { 35 | let client = Client::new(); 36 | let mut client_req = Request::builder(); 37 | client_req = client_req.uri(target_url).method(server_req.method()); 38 | 39 | // Copy headers from `server_req` to the `client_req`. 40 | for (key, value) in server_req.headers() { 41 | client_req = client_req.header(key, value); 42 | } 43 | 44 | // Stream the request body. 45 | let client_req = client_req.body(server_req.into_body())?; 46 | // Send the request. 47 | let client_resp = client.send(client_req).await?; 48 | // Copy headers from `client_resp` to `server_resp`. 49 | let mut server_resp = Response::builder(); 50 | for (key, value) in client_resp.headers() { 51 | server_resp 52 | .headers_mut() 53 | .expect("no errors could be in ResponseBuilder") 54 | .append(key, value.clone()); 55 | } 56 | Ok(server_resp.body(client_resp.into_body())?) 57 | } 58 | 59 | fn http_not_found(_request: Request) -> Response { 60 | Response::builder() 61 | .status(StatusCode::NOT_FOUND) 62 | .body(Body::empty()) 63 | .unwrap() 64 | } 65 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | # Publication half of the release process for this repository. This runs on 2 | # pushes to `main` and will detect a magical string in commit messages. When 3 | # found a tag will be created, pushed, and then everything is published. 4 | 5 | name: Publish Artifacts 6 | on: 7 | push: 8 | branches: [main] 9 | 10 | permissions: 11 | contents: write 12 | id-token: write 13 | 14 | jobs: 15 | create_tag: 16 | name: Publish artifacts of build 17 | runs-on: ubuntu-latest 18 | environment: release 19 | if: | 20 | github.repository_owner == 'bytecodealliance' 21 | && github.event_name == 'push' 22 | && github.ref == 'refs/heads/main' 23 | steps: 24 | - uses: actions/checkout@v4 25 | with: 26 | submodules: true 27 | fetch-depth: 0 28 | 29 | - run: rustup update stable && rustup default stable 30 | 31 | # If this is a push to `main` see if the push has an indicator saying that 32 | # a tag should be made. If so create one and push it. 33 | - name: Test if tag is needed 34 | run: | 35 | git log ${{ github.event.before }}...${{ github.event.after }} | tee main.log 36 | version=$(./ci/print-current-version.sh) 37 | echo "version: $version" 38 | echo "version=$version" >> $GITHUB_OUTPUT 39 | echo "sha=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT 40 | if grep -q "automatically-tag-and-release-this-commit" main.log; then 41 | echo push-tag 42 | echo "push_tag=yes" >> $GITHUB_OUTPUT 43 | else 44 | echo no-push-tag 45 | echo "push_tag=no" >> $GITHUB_OUTPUT 46 | fi 47 | id: tag 48 | 49 | - name: Push the tag 50 | run: | 51 | git_refs_url=$(jq .repository.git_refs_url $GITHUB_EVENT_PATH | tr -d '"' | sed 's/{\/sha}//g') 52 | curl -iX POST $git_refs_url \ 53 | -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ 54 | -d @- << EOF 55 | { 56 | "ref": "refs/tags/v${{ steps.tag.outputs.version }}", 57 | "sha": "${{ steps.tag.outputs.sha }}" 58 | } 59 | EOF 60 | if: steps.tag.outputs.push_tag == 'yes' 61 | 62 | - uses: softprops/action-gh-release@v1 63 | if: steps.tag.outputs.push_tag == 'yes' 64 | with: 65 | tag_name: v${{ steps.tag.outputs.version }} 66 | 67 | - uses: rust-lang/crates-io-auth-action@v1 68 | id: auth 69 | if: steps.tag.outputs.push_tag == 'yes' 70 | 71 | - run: | 72 | rm -rf main.log 73 | rustc ci/publish.rs 74 | ./publish publish 75 | env: 76 | CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }} 77 | if: steps.tag.outputs.push_tag == 'yes' 78 | -------------------------------------------------------------------------------- /axum/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Support for the [`axum`] web server framework in wasi-http components, via 2 | //! [`wstd`]. 3 | //! 4 | //! This crate is a pretty thin wrapper on [`wstd`] that allows users to 5 | //! use the [`axum`] crate on top of wstd's http support. This means that 6 | //! axum services can run anywhere the [wasi-http proxy world] is supported, 7 | //! e.g. in [`wasmtime serve`]. 8 | //! 9 | //! Users of this crate should depend on `axum` with `default-features = 10 | //! false`, and opt in to any features that they require (e.g. form, json, 11 | //! matched-path, original-uri, query, tower-log, tracing). The axum crate 12 | //! features that require `hyper` or `tokio` are NOT supported (e.g. http1, 13 | //! http2, ws), because unlike in native applications, wasi-http components 14 | //! have an http implementation provided as imported interfaces (i.e. 15 | //! implemented the Wasm host), and do not use raw sockets inside of this 16 | //! program. 17 | //! 18 | //! # Examples 19 | //! 20 | //! The simplest use is via the `wstd_axum::http_server` proc macro. 21 | //! This macro can be applied to a sync or `async` `fn main` which returns 22 | //! an impl of the `tower_service::Service` trait, typically an 23 | //! `axum::Router`: 24 | //! 25 | //! ```rust,no_run 26 | #![doc = include_str!("../examples/hello_world.rs")] 27 | //! ``` 28 | //! 29 | //! If users desire, they can instead use a `wstd::http_server` entry point 30 | //! and then use `wstd_axum::serve` directly. The following is equivelant 31 | //! to the above example: 32 | //! 33 | //! ```rust,no_run 34 | #![doc = include_str!("../examples/hello_world_nomacro.rs")] 35 | //! ``` 36 | //! 37 | //! [`axum`]: https://docs.rs/axum/latest/axum/ 38 | //! [`wstd`]: https://docs.rs/wstd/latest/wstd/ 39 | //! [wasi-http proxy world]: https://github.com/WebAssembly/wasi-http 40 | //! [`wasmtime serve`]: https://wasmtime.dev/ 41 | 42 | use axum::extract::Request; 43 | use axum::response::Response; 44 | use std::convert::Infallible; 45 | use tower_service::Service; 46 | 47 | pub use wstd_axum_macro::attr_macro_http_server as http_server; 48 | 49 | pub async fn serve( 50 | request: wstd::http::Request, 51 | mut service: S, 52 | ) -> wstd::http::error::Result> 53 | where 54 | S: Service + Clone + Send + 'static, 55 | S::Future: Send, 56 | { 57 | let resp = service 58 | .call( 59 | request.map(|incoming: wstd::http::Body| -> axum::body::Body { 60 | axum::body::Body::new(incoming.into_boxed_body()) 61 | }), 62 | ) 63 | .await 64 | .unwrap_or_else(|err| match err {}); 65 | Ok(resp.map(|body: axum::body::Body| -> wstd::http::Body { 66 | wstd::http::Body::from_http_body(body) 67 | })) 68 | } 69 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - staging 8 | - trying 9 | 10 | env: 11 | RUSTFLAGS: -Dwarnings 12 | 13 | # required for AWS oidc 14 | permissions: 15 | id-token: write 16 | 17 | jobs: 18 | build_and_test: 19 | name: Build and test 20 | runs-on: ${{ matrix.os }} 21 | strategy: 22 | matrix: 23 | os: [ubuntu-latest, windows-latest, macOS-latest] 24 | 25 | steps: 26 | - uses: actions/checkout@master 27 | 28 | - uses: ./.github/actions/install-rust 29 | 30 | - name: Install wasmtime 31 | uses: bytecodealliance/actions/wasmtime/setup@v1 32 | 33 | # pchickey made a role `wstd-aws-ci-role` and bucket `wstd-example-bucket` 34 | # on his personal aws account 313377415443. The role only provides 35 | # ListBucket and GetObject for the example bucket, which is enough to pass 36 | # the single integration test. The role is configured to trust GitHub 37 | # actions for the bytecodealliance/wstd repo. This action will set the 38 | # AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, and AWS_SESSION_TOKEN 39 | # environment variables. 40 | - name: get aws credentials 41 | id: creds 42 | uses: aws-actions/configure-aws-credentials@v5.1.0 43 | continue-on-error: true 44 | with: 45 | aws-region: us-west-2 46 | role-to-assume: arn:aws:iam::313377415443:role/wstd-aws-ci-role 47 | role-session-name: github-ci 48 | 49 | - name: check 50 | run: cargo check --workspace --all --bins --examples 51 | 52 | - name: wstd tests 53 | run: cargo test -p wstd -p wstd-axum -p wstd-aws --target wasm32-wasip2 -- --nocapture 54 | 55 | - name: test-programs tests 56 | run: cargo test -p test-programs -- --nocapture 57 | if: steps.creds.outcome == 'success' 58 | 59 | - name: test-programs tests (no aws) 60 | run: cargo test -p test-programs --features no-aws -- --nocapture 61 | if: steps.creds.outcome != 'success' 62 | 63 | check_fmt_and_docs: 64 | name: Checking fmt and docs 65 | runs-on: ubuntu-latest 66 | steps: 67 | - uses: actions/checkout@master 68 | - uses: ./.github/actions/install-rust 69 | 70 | - name: fmt 71 | run: cargo fmt --all -- --check 72 | 73 | - name: Docs 74 | run: cargo doc 75 | 76 | - name: Clippy 77 | run: cargo clippy --all 78 | 79 | verify-publish: 80 | name: Verify publish 81 | if: github.repository_owner == 'bytecodealliance' 82 | runs-on: ubuntu-latest 83 | steps: 84 | - uses: actions/checkout@master 85 | - uses: ./.github/actions/install-rust 86 | - run: rustc ci/publish.rs 87 | # Make sure we can bump version numbers for the next release 88 | - run: ./publish bump 89 | # Make sure the tree is publish-able as-is 90 | - run: ./publish verify 91 | 92 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | Contributions include code, documentation, answering user questions, running the 3 | project's infrastructure, and advocating for all types of users. 4 | 5 | The project welcomes all contributions from anyone willing to work in good faith 6 | with other contributors and the community. No contribution is too small and all 7 | contributions are valued. 8 | 9 | This guide explains the process for contributing to the project's GitHub 10 | Repository. 11 | 12 | - [Code of Conduct](#code-of-conduct) 13 | - [Bad Actors](#bad-actors) 14 | 15 | ## Code of Conduct 16 | The project has a [Code of Conduct](./CODE_OF_CONDUCT.md) that *all* 17 | contributors are expected to follow. This code describes the *minimum* behavior 18 | expectations for all contributors. 19 | 20 | As a contributor, how you choose to act and interact towards your 21 | fellow contributors, as well as to the community, will reflect back not only 22 | on yourself but on the project as a whole. The Code of Conduct is designed and 23 | intended, above all else, to help establish a culture within the project that 24 | allows anyone and everyone who wants to contribute to feel safe doing so. 25 | 26 | Should any individual act in any way that is considered in violation of the 27 | [Code of Conduct](./CODE_OF_CONDUCT.md), corrective actions will be taken. It is 28 | possible, however, for any individual to *act* in such a manner that is not in 29 | violation of the strict letter of the Code of Conduct guidelines while still 30 | going completely against the spirit of what that Code is intended to accomplish. 31 | 32 | Open, diverse, and inclusive communities live and die on the basis of trust. 33 | Contributors can disagree with one another so long as they trust that those 34 | disagreements are in good faith and everyone is working towards a common 35 | goal. 36 | 37 | ## Bad Actors 38 | All contributors to tacitly agree to abide by both the letter and 39 | spirit of the [Code of Conduct](./CODE_OF_CONDUCT.md). Failure, or 40 | unwillingness, to do so will result in contributions being respectfully 41 | declined. 42 | 43 | A *bad actor* is someone who repeatedly violates the *spirit* of the Code of 44 | Conduct through consistent failure to self-regulate the way in which they 45 | interact with other contributors in the project. In doing so, bad actors 46 | alienate other contributors, discourage collaboration, and generally reflect 47 | poorly on the project as a whole. 48 | 49 | Being a bad actor may be intentional or unintentional. Typically, unintentional 50 | bad behavior can be easily corrected by being quick to apologize and correct 51 | course *even if you are not entirely convinced you need to*. Giving other 52 | contributors the benefit of the doubt and having a sincere willingness to admit 53 | that you *might* be wrong is critical for any successful open collaboration. 54 | 55 | Don't be a bad actor. 56 | -------------------------------------------------------------------------------- /aws/README.md: -------------------------------------------------------------------------------- 1 | 2 | # wstd-aws: wstd support for the AWS Rust SDK 3 | 4 | This crate provides support for using the AWS Rust SDK for the `wasm32-wasip2` 5 | target using the [`wstd`] crate. 6 | 7 | In many wasi settings, its necessary or desirable to use the wasi-http 8 | interface to make http requests. Wasi-http interfaces provide an http 9 | implementation, including the sockets layer and TLS, outside of the user's 10 | component. `wstd` provides user-friendly async Rust interfaces to all of the 11 | standardized wasi interfaces, including wasi-http. 12 | 13 | The AWS Rust SDK, by default, depends on `tokio`, `hyper`, and either `rustls` 14 | or `s2n_tls`, and makes http requests over sockets (which can be provided as 15 | wasi-sockets). Those dependencies may not work correctly under `wasm32-wasip2`, 16 | and if they do, they will not use the wasi-http interfaces. To avoid using 17 | http over sockets, make sure to set the `default-features = false` setting 18 | when depending on any `aws-*` crates in your project. 19 | 20 | To configure `wstd`'s wasi-http client for the AWS Rust SDK, provide 21 | `wstd_aws::sleep_impl()` and `wstd_aws::http_client()` to your 22 | [`aws_config::ConfigLoader`]: 23 | 24 | ``` 25 | let config = aws_config::defaults(BehaviorVersion::latest()) 26 | .sleep_impl(wstd_aws::sleep_impl()) 27 | .http_client(wstd_aws::http_client()) 28 | ...; 29 | ``` 30 | 31 | [`wstd`]: https://docs.rs/wstd/latest/wstd 32 | [`aws_config::ConfigLoader`]: https://docs.rs/aws-config/1.8.8/aws_config/struct.ConfigLoader.html 33 | 34 | ## Example 35 | 36 | An example s3 client is provided as a wasi cli command. It accepts command 37 | line arguments with the subcommand `list` to list a bucket's contents, and 38 | `get ` to get an object from a bucket and write it to the filesystem. 39 | 40 | This example *must be compiled in release mode* - in debug mode, the aws 41 | sdk's generated code will overflow the maximum permitted wasm locals in 42 | a single function. 43 | 44 | Compile it with: 45 | 46 | ```sh 47 | cargo build -p wstd-aws --target wasm32-wasip2 --release --examples 48 | ``` 49 | 50 | When running this example, you will need AWS credentials provided in environment 51 | variables. 52 | 53 | Run it with: 54 | ```sh 55 | wasmtime run -Shttp \ 56 | --env AWS_ACCESS_KEY_ID \ 57 | --env AWS_SECRET_ACCESS_KEY \ 58 | --env AWS_SESSION_TOKEN \ 59 | --dir .::. \ 60 | target/wasm32-wasip2/release/examples/s3.wasm 61 | ``` 62 | 63 | or alternatively run it with: 64 | ```sh 65 | cargo run --target wasm32-wasip2 -p wstd-aws --example s3 66 | ``` 67 | 68 | which uses the wasmtime cli, as above, via configiration found in this 69 | workspace's `.cargo/config`. 70 | 71 | By default, this script accesses the `wstd-example-bucket` in `us-west-2`. 72 | To change the bucket or region, use the `--bucket` and `--region` cli 73 | flags before the subcommand. 74 | 75 | 76 | -------------------------------------------------------------------------------- /src/future/future_ext.rs: -------------------------------------------------------------------------------- 1 | use super::{Delay, Timeout}; 2 | use std::future::{Future, IntoFuture}; 3 | 4 | /// Extend `Future` with time-based operations. 5 | pub trait FutureExt: Future { 6 | /// Return an error if a future does not complete within a given time span. 7 | /// 8 | /// Typically timeouts are, as the name implies, based on _time_. However 9 | /// this method can time out based on any future. This can be useful in 10 | /// combination with channels, as it allows (long-lived) futures to be 11 | /// cancelled based on some external event. 12 | /// 13 | /// When a timeout is returned, the future will be dropped and destructors 14 | /// will be run. 15 | /// 16 | /// # Example 17 | /// 18 | /// ```no_run 19 | /// use wstd::prelude::*; 20 | /// use wstd::time::{Instant, Duration}; 21 | /// use std::io; 22 | /// 23 | /// #[wstd::main] 24 | /// async fn main() { 25 | /// let res = async { "meow" } 26 | /// .delay(Duration::from_millis(100)) // longer delay 27 | /// .timeout(Duration::from_millis(50)) // shorter timeout 28 | /// .await; 29 | /// assert_eq!(res.unwrap_err().kind(), io::ErrorKind::TimedOut); // error 30 | /// 31 | /// let res = async { "meow" } 32 | /// .delay(Duration::from_millis(50)) // shorter delay 33 | /// .timeout(Duration::from_millis(100)) // longer timeout 34 | /// .await; 35 | /// assert_eq!(res.unwrap(), "meow"); // success 36 | /// } 37 | /// ``` 38 | fn timeout(self, deadline: D) -> Timeout 39 | where 40 | Self: Sized, 41 | D: IntoFuture, 42 | { 43 | Timeout::new(self, deadline.into_future()) 44 | } 45 | 46 | /// Delay resolving the future until the given deadline. 47 | /// 48 | /// The underlying future will not be polled until the deadline has expired. In addition 49 | /// to using a time source as a deadline, any future can be used as a 50 | /// deadline too. When used in combination with a multi-consumer channel, 51 | /// this method can be used to synchronize the start of multiple futures and streams. 52 | /// 53 | /// # Example 54 | /// 55 | /// ```no_run 56 | /// use wstd::prelude::*; 57 | /// use wstd::time::{Instant, Duration}; 58 | /// 59 | /// #[wstd::main] 60 | /// async fn main() { 61 | /// let now = Instant::now(); 62 | /// let delay = Duration::from_millis(100); 63 | /// let _ = async { "meow" }.delay(delay).await; 64 | /// assert!(now.elapsed() >= delay); 65 | /// } 66 | /// ``` 67 | fn delay(self, deadline: D) -> Delay 68 | where 69 | Self: Sized, 70 | D: IntoFuture, 71 | { 72 | Delay::new(self, deadline.into_future()) 73 | } 74 | } 75 | 76 | impl FutureExt for T where T: Future {} 77 | -------------------------------------------------------------------------------- /.github/workflows/release-process.yml: -------------------------------------------------------------------------------- 1 | # Initiation half of the release process for this repository. 2 | # 3 | # This is triggered manually through the github actions UI and will execute 4 | # `./publish bump` (or `bump-patch`). Afterwards the result will be pushed to a 5 | # branch in the main repository and a PR will be opened. This PR, when merged, 6 | # will trigger the second half in `publish.yml`. 7 | 8 | name: "Automated Release Process" 9 | on: 10 | # Allow manually triggering this request via the button on the action 11 | # workflow page. 12 | workflow_dispatch: 13 | inputs: 14 | action: 15 | description: 'Publish script argument: "bump", or "bump-patch"' 16 | required: false 17 | default: 'bump' 18 | 19 | permissions: 20 | contents: write 21 | pull-requests: write 22 | 23 | jobs: 24 | release_process: 25 | name: Run the release process 26 | runs-on: ubuntu-latest 27 | steps: 28 | - uses: actions/checkout@v4 29 | with: 30 | submodules: true 31 | - name: Setup 32 | run: | 33 | rustc ci/publish.rs 34 | git config user.name 'Auto Release Process' 35 | git config user.email 'auto-release-process@users.noreply.github.com' 36 | git remote set-url origin https://git:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }} 37 | 38 | - name: Bump version number 39 | run: ./publish ${{ github.event.inputs.action }} 40 | 41 | - name: Prep PR metadata 42 | run: | 43 | set -ex 44 | git fetch origin 45 | 46 | cur=$(./ci/print-current-version.sh) 47 | 48 | git commit --allow-empty -a -F-<> $GITHUB_ENV 58 | echo "PR_TITLE=Release ${{ github.event.repository.name }} $cur" >> $GITHUB_ENV 59 | echo "PR_BASE=main" >> $GITHUB_ENV 60 | cat > pr-body <<-EOF 61 | This is an automated pull request from CI to release 62 | ${{ github.event.repository.name }} $cur when merged. The commit 63 | message for this PR has a marker that is detected by CI to create 64 | tags and publish crate artifacts. 65 | 66 | When first opened this PR will not have CI run because it is generated 67 | by a bot. A maintainer should close this PR and then reopen it to 68 | trigger CI to execute which will then enable merging this PR. 69 | EOF 70 | 71 | - name: Make a PR 72 | run: gh pr create -B "$PR_BASE" -H "$PR_HEAD" --title "$PR_TITLE" --body "$(cat ./pr-body)" 73 | env: 74 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 75 | -------------------------------------------------------------------------------- /src/io/cursor.rs: -------------------------------------------------------------------------------- 1 | use crate::io::{self, AsyncRead, AsyncSeek, AsyncWrite}; 2 | 3 | use super::SeekFrom; 4 | 5 | /// A `Cursor` wraps an in-memory buffer and provides it with a 6 | /// [`AsyncSeek`] implementation. 7 | #[derive(Clone, Debug, Default)] 8 | pub struct Cursor { 9 | inner: std::io::Cursor, 10 | } 11 | 12 | impl Cursor { 13 | /// Creates a new cursor wrapping the provided underlying in-memory buffer. 14 | pub fn new(inner: T) -> Cursor { 15 | Cursor { 16 | inner: std::io::Cursor::new(inner), 17 | } 18 | } 19 | 20 | /// Consumes this cursor, returning the underlying value. 21 | pub fn into_inner(self) -> T { 22 | self.inner.into_inner() 23 | } 24 | 25 | /// Gets a reference to the underlying value in this cursor. 26 | pub fn get_ref(&self) -> &T { 27 | self.inner.get_ref() 28 | } 29 | 30 | /// Gets a mutable reference to the underlying value in this cursor. 31 | pub fn get_mut(&mut self) -> &mut T { 32 | self.inner.get_mut() 33 | } 34 | 35 | /// Returns the current position of this cursor. 36 | pub fn position(&self) -> u64 { 37 | self.inner.position() 38 | } 39 | 40 | /// Sets the position of this cursor. 41 | pub fn set_position(&mut self, pos: u64) { 42 | self.inner.set_position(pos) 43 | } 44 | } 45 | 46 | impl AsyncSeek for Cursor 47 | where 48 | T: AsRef<[u8]>, 49 | { 50 | async fn seek(&mut self, pos: SeekFrom) -> io::Result { 51 | let pos = match pos { 52 | SeekFrom::Start(pos) => std::io::SeekFrom::Start(pos), 53 | SeekFrom::End(pos) => std::io::SeekFrom::End(pos), 54 | SeekFrom::Current(pos) => std::io::SeekFrom::Current(pos), 55 | }; 56 | std::io::Seek::seek(&mut self.inner, pos) 57 | } 58 | } 59 | 60 | impl AsyncRead for Cursor 61 | where 62 | T: AsRef<[u8]>, 63 | { 64 | async fn read(&mut self, buf: &mut [u8]) -> io::Result { 65 | std::io::Read::read(&mut self.inner, buf) 66 | } 67 | } 68 | 69 | impl AsyncWrite for Cursor<&mut [u8]> { 70 | async fn write(&mut self, buf: &[u8]) -> io::Result { 71 | std::io::Write::write(&mut self.inner, buf) 72 | } 73 | async fn flush(&mut self) -> io::Result<()> { 74 | std::io::Write::flush(&mut self.inner) 75 | } 76 | } 77 | 78 | impl AsyncWrite for Cursor<&mut Vec> { 79 | async fn write(&mut self, buf: &[u8]) -> io::Result { 80 | std::io::Write::write(&mut self.inner, buf) 81 | } 82 | async fn flush(&mut self) -> io::Result<()> { 83 | std::io::Write::flush(&mut self.inner) 84 | } 85 | } 86 | 87 | impl AsyncWrite for Cursor> { 88 | async fn write(&mut self, buf: &[u8]) -> io::Result { 89 | std::io::Write::write(&mut self.inner, buf) 90 | } 91 | async fn flush(&mut self) -> io::Result<()> { 92 | std::io::Write::flush(&mut self.inner) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(async_fn_in_trait)] 2 | #![warn(future_incompatible, unreachable_pub)] 3 | #![deny(unsafe_code)] 4 | //#![deny(missing_debug_implementations)] 5 | //#![warn(missing_docs)] 6 | //#![forbid(rustdoc::missing_doc_code_examples)] 7 | 8 | //! An async standard library for Wasm Components and WASI 0.2 9 | //! 10 | //! This is a minimal async standard library written exclusively to support Wasm 11 | //! Components. It exists primarily to enable people to write async-based 12 | //! applications in Rust before async-std, smol, or tokio land support for Wasm 13 | //! Components and WASI 0.2. Once those runtimes land support, it is recommended 14 | //! users switch to use those instead. 15 | //! 16 | //! # Examples 17 | //! 18 | //! **TCP echo server** 19 | //! 20 | //! ```rust,no_run 21 | #![doc = include_str!("../examples/tcp_echo_server.rs")] 22 | //! ``` 23 | //! 24 | //! **HTTP Client** 25 | //! 26 | //! ```rust,ignore 27 | #![doc = include_str!("../tests/http_get.rs")] 28 | //! ``` 29 | //! 30 | //! **HTTP Server** 31 | //! 32 | //! ```rust,no_run 33 | #![doc = include_str!("../examples/http_server.rs")] 34 | //! ``` 35 | //! 36 | //! # Design Decisions 37 | //! 38 | //! This library is entirely self-contained. This means that it does not share 39 | //! any traits or types with any other async runtimes. This means we're trading 40 | //! in some compatibility for ease of maintenance. Because this library is not 41 | //! intended to be maintained in the long term, this seems like the right 42 | //! tradeoff to make. 43 | //! 44 | //! WASI 0.2 does not yet support multi-threading. For that reason this library 45 | //! does not provide any multi-threaded primitives, and is free to make liberal 46 | //! use of Async Functions in Traits since no `Send` bounds are required. This 47 | //! makes for a simpler end-user experience, again at the cost of some 48 | //! compatibility. Though ultimately we do believe that using Async Functions is 49 | //! the right foundation for the standard library abstractions - meaning we may 50 | //! be trading in backward-compatibility for forward-compatibility. 51 | //! 52 | //! This library also supports slightly more interfaces than the stdlib does. 53 | //! For example `wstd::rand` is a new module that provides access to random 54 | //! bytes. And `wstd::runtime` provides access to async runtime primitives. 55 | //! These are unique capabilities provided by WASI 0.2, and because this library 56 | //! is specific to that are exposed from here. 57 | 58 | pub mod future; 59 | #[macro_use] 60 | pub mod http; 61 | pub mod io; 62 | pub mod iter; 63 | pub mod net; 64 | pub mod rand; 65 | pub mod runtime; 66 | pub mod task; 67 | pub mod time; 68 | 69 | pub use wstd_macro::attr_macro_http_server as http_server; 70 | pub use wstd_macro::attr_macro_main as main; 71 | pub use wstd_macro::attr_macro_test as test; 72 | 73 | // Re-export the wasip2 crate for use only by `wstd-macro` macros. The proc 74 | // macros need to generate code that uses these definitions, but we don't want 75 | // to treat it as part of our public API with regards to semver, so we keep it 76 | // under `__internal` as well as doc(hidden) to indicate it is private. 77 | #[doc(hidden)] 78 | pub mod __internal { 79 | pub use wasip2; 80 | } 81 | 82 | pub mod prelude { 83 | pub use crate::future::FutureExt as _; 84 | pub use crate::io::AsyncRead as _; 85 | pub use crate::io::AsyncWrite as _; 86 | } 87 | -------------------------------------------------------------------------------- /src/http/server.rs: -------------------------------------------------------------------------------- 1 | //! HTTP servers 2 | //! 3 | //! The WASI HTTP server uses the [typed main] idiom, with a `main` function 4 | //! that takes a [`Request`] and succeeds with a [`Response`], using the 5 | //! [`http_server`] macro: 6 | //! 7 | //! ```no_run 8 | //! use wstd::http::{Request, Response, Body, Error}; 9 | //! #[wstd::http_server] 10 | //! async fn main(_request: Request) -> Result, Error> { 11 | //! Ok(Response::new("Hello!\n".into())) 12 | //! } 13 | //! ``` 14 | //! 15 | //! [typed main]: https://sunfishcode.github.io/typed-main-wasi-presentation/chapter_1.html 16 | //! [`Request`]: crate::http::Request 17 | //! [`Responder`]: crate::http::server::Responder 18 | //! [`Response`]: crate::http::Response 19 | //! [`http_server`]: crate::http_server 20 | 21 | use super::{Body, Error, Response, error::ErrorCode, fields::header_map_to_wasi}; 22 | use http::header::CONTENT_LENGTH; 23 | use wasip2::exports::http::incoming_handler::ResponseOutparam; 24 | use wasip2::http::types::OutgoingResponse; 25 | 26 | /// For use by the [`http_server`] macro only. 27 | /// 28 | /// [`http_server`]: crate::http_server 29 | #[doc(hidden)] 30 | #[must_use] 31 | pub struct Responder { 32 | outparam: ResponseOutparam, 33 | } 34 | 35 | impl Responder { 36 | /// This is used by the `http_server` macro. 37 | #[doc(hidden)] 38 | pub async fn respond>(self, response: Response) -> Result<(), Error> { 39 | let headers = response.headers(); 40 | let status = response.status().as_u16(); 41 | 42 | let wasi_headers = header_map_to_wasi(headers).expect("header error"); 43 | 44 | // Consume the `response` and prepare to write the body. 45 | let body = response.into_body().into(); 46 | 47 | // Automatically add a Content-Length header. 48 | if let Some(len) = body.content_length() { 49 | let mut buffer = itoa::Buffer::new(); 50 | wasi_headers 51 | .append(CONTENT_LENGTH.as_str(), buffer.format(len).as_bytes()) 52 | .unwrap(); 53 | } 54 | 55 | let wasi_response = OutgoingResponse::new(wasi_headers); 56 | 57 | // Unwrap because `StatusCode` has already validated the status. 58 | wasi_response.set_status_code(status).unwrap(); 59 | 60 | // Unwrap because we can be sure we only call these once. 61 | let wasi_body = wasi_response.body().unwrap(); 62 | 63 | // Set the outparam to the response, which allows wasi-http to send 64 | // the response status and headers. 65 | ResponseOutparam::set(self.outparam, Ok(wasi_response)); 66 | 67 | // Then send the body. The response will be fully sent once this 68 | // future is ready. 69 | body.send(wasi_body).await 70 | } 71 | 72 | /// This is used by the `http_server` macro. 73 | #[doc(hidden)] 74 | pub fn new(outparam: ResponseOutparam) -> Self { 75 | Self { outparam } 76 | } 77 | 78 | /// This is used by the `http_server` macro. 79 | #[doc(hidden)] 80 | pub fn fail(self, err: Error) -> Result<(), Error> { 81 | let e = match err.downcast_ref::() { 82 | Some(e) => e.clone(), 83 | None => ErrorCode::InternalError(Some(format!("{err:?}"))), 84 | }; 85 | ResponseOutparam::set(self.outparam, Err(e)); 86 | Err(err) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of 9 | experience, 10 | education, socio-economic status, nationality, personal appearance, race, 11 | religion, or sexual identity and orientation. 12 | 13 | ## Our Standards 14 | 15 | Examples of behavior that contributes to creating a positive environment 16 | include: 17 | 18 | - Using welcoming and inclusive language 19 | - Being respectful of differing viewpoints and experiences 20 | - Gracefully accepting constructive criticism 21 | - Focusing on what is best for the community 22 | - Showing empathy towards other community members 23 | 24 | Examples of unacceptable behavior by participants include: 25 | 26 | - The use of sexualized language or imagery and unwelcome sexual attention or 27 | advances 28 | - Trolling, insulting/derogatory comments, and personal or political attacks 29 | - Public or private harassment 30 | - Publishing others' private information, such as a physical or electronic 31 | address, without explicit permission 32 | - Other conduct which could reasonably be considered inappropriate in a 33 | professional setting 34 | 35 | 36 | ## Our Responsibilities 37 | 38 | Project maintainers are responsible for clarifying the standards of acceptable 39 | behavior and are expected to take appropriate and fair corrective action in 40 | response to any instances of unacceptable behavior. 41 | 42 | Project maintainers have the right and responsibility to remove, edit, or 43 | reject comments, commits, code, wiki edits, issues, and other contributions 44 | that are not aligned to this Code of Conduct, or to ban temporarily or 45 | permanently any contributor for other behaviors that they deem inappropriate, 46 | threatening, offensive, or harmful. 47 | 48 | ## Scope 49 | 50 | This Code of Conduct applies both within project spaces and in public spaces 51 | when an individual is representing the project or its community. Examples of 52 | representing a project or community include using an official project e-mail 53 | address, posting via an official social media account, or acting as an appointed 54 | representative at an online or offline event. Representation of a project may be 55 | further defined and clarified by project maintainers. 56 | 57 | ## Enforcement 58 | 59 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 60 | reported by contacting the project team at conduct@yosh.is, or through 61 | IRC. All complaints will be reviewed and investigated and will result in a 62 | response that is deemed necessary and appropriate to the circumstances. The 63 | project team is obligated to maintain confidentiality with regard to the 64 | reporter of an incident. 65 | Further details of specific enforcement policies may be posted separately. 66 | 67 | Project maintainers who do not follow or enforce the Code of Conduct in good 68 | faith may face temporary or permanent repercussions as determined by other 69 | members of the project's leadership. 70 | 71 | ## Attribution 72 | 73 | This Code of Conduct is adapted from the Contributor Covenant, version 1.4, 74 | available at 75 | https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 76 | -------------------------------------------------------------------------------- /test-programs/build.rs: -------------------------------------------------------------------------------- 1 | use cargo_metadata::{MetadataCommand, Package, TargetKind}; 2 | use heck::ToShoutySnakeCase; 3 | use std::env::var_os; 4 | use std::path::{Path, PathBuf}; 5 | use std::process::Command; 6 | 7 | fn main() { 8 | let out_dir = PathBuf::from(var_os("OUT_DIR").expect("OUT_DIR env var exists")); 9 | 10 | let meta = MetadataCommand::new().exec().expect("cargo metadata"); 11 | 12 | println!( 13 | "cargo:rerun-if-changed={}", 14 | meta.workspace_root.as_os_str().to_str().unwrap() 15 | ); 16 | 17 | fn build_examples(pkg: &str, out_dir: &PathBuf) { 18 | // release build is required for aws sdk to not overflow wasm locals 19 | let status = Command::new("cargo") 20 | .arg("build") 21 | .arg("--examples") 22 | .arg("--release") 23 | .arg("--target=wasm32-wasip2") 24 | .arg(format!("--package={pkg}")) 25 | .env("CARGO_TARGET_DIR", out_dir) 26 | .env("CARGO_PROFILE_DEV_DEBUG", "2") 27 | .env("RUSTFLAGS", rustflags()) 28 | .env_remove("CARGO_ENCODED_RUSTFLAGS") 29 | .status() 30 | .expect("cargo build wstd examples"); 31 | assert!(status.success()); 32 | } 33 | build_examples("wstd", &out_dir); 34 | build_examples("wstd-axum", &out_dir); 35 | build_examples("wstd-aws", &out_dir); 36 | 37 | let mut generated_code = "// THIS FILE IS GENERATED CODE\n".to_string(); 38 | 39 | fn module_for(name: &str, out_dir: &Path, meta: &Package) -> String { 40 | let mut generated_code = String::new(); 41 | generated_code += &format!("pub mod {name} {{"); 42 | for binary in meta 43 | .targets 44 | .iter() 45 | .filter(|t| t.kind == [TargetKind::Example]) 46 | { 47 | let component_path = out_dir 48 | .join("wasm32-wasip2") 49 | .join("release") 50 | .join("examples") 51 | .join(format!("{}.wasm", binary.name)); 52 | 53 | let const_name = binary.name.to_shouty_snake_case(); 54 | generated_code += &format!( 55 | "pub const {const_name}: &str = {:?};\n", 56 | component_path.as_os_str().to_str().expect("path is str") 57 | ); 58 | } 59 | generated_code += "}\n\n"; // end `pub mod {name}` 60 | generated_code 61 | } 62 | 63 | generated_code += &module_for( 64 | "_wstd", 65 | &out_dir, 66 | meta.packages 67 | .iter() 68 | .find(|p| *p.name == "wstd") 69 | .expect("wstd is in cargo metadata"), 70 | ); 71 | generated_code += "pub use _wstd::*;\n\n"; 72 | generated_code += &module_for( 73 | "axum", 74 | &out_dir, 75 | meta.packages 76 | .iter() 77 | .find(|p| *p.name == "wstd-axum") 78 | .expect("wstd-axum is in cargo metadata"), 79 | ); 80 | generated_code += &module_for( 81 | "aws", 82 | &out_dir, 83 | meta.packages 84 | .iter() 85 | .find(|p| *p.name == "wstd-aws") 86 | .expect("wstd-aws is in cargo metadata"), 87 | ); 88 | 89 | std::fs::write(out_dir.join("gen.rs"), generated_code).unwrap(); 90 | } 91 | 92 | fn rustflags() -> &'static str { 93 | match option_env!("RUSTFLAGS") { 94 | Some(s) if s.contains("-D warnings") => "-D warnings", 95 | _ => "", 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wstd" 3 | version.workspace = true 4 | license.workspace = true 5 | documentation = "https://docs.rs/wstd" 6 | description = "An async standard library for Wasm Components and WASI 0.2" 7 | readme = "README.md" 8 | edition.workspace = true 9 | authors.workspace = true 10 | keywords.workspace = true 11 | categories.workspace = true 12 | repository.workspace = true 13 | rust-version.workspace = true 14 | 15 | [features] 16 | default = ["json"] 17 | json = ["dep:serde", "dep:serde_json"] 18 | 19 | [dependencies] 20 | anyhow.workspace = true 21 | async-task.workspace = true 22 | bytes.workspace = true 23 | futures-lite.workspace = true 24 | http-body-util.workspace = true 25 | http-body.workspace = true 26 | http.workspace = true 27 | itoa.workspace = true 28 | pin-project-lite.workspace = true 29 | slab.workspace = true 30 | wasip2.workspace = true 31 | wstd-macro.workspace = true 32 | 33 | # optional 34 | serde = { workspace = true, optional = true } 35 | serde_json = { workspace = true, optional = true } 36 | 37 | [dev-dependencies] 38 | anyhow.workspace = true 39 | clap.workspace = true 40 | http-body-util.workspace = true 41 | futures-concurrency.workspace = true 42 | humantime.workspace = true 43 | serde = { workspace = true, features = ["derive"] } 44 | serde_json.workspace = true 45 | 46 | [workspace] 47 | members = ["aws", 48 | "axum", 49 | "axum/macro", 50 | "macro", 51 | "test-programs", 52 | ] 53 | resolver = "2" 54 | 55 | [workspace.package] 56 | version = "0.6.3" 57 | edition = "2024" 58 | license = "Apache-2.0 WITH LLVM-exception" 59 | repository = "https://github.com/bytecodealliance/wstd" 60 | keywords = ["WebAssembly", "async", "stdlib", "Components"] 61 | categories = ["wasm", "asynchronous"] 62 | # Rust-version policy: stable N-2, same as wasmtime. 63 | rust-version = "1.89" 64 | authors = [ 65 | "Yoshua Wuyts ", 66 | "Pat Hickey ", 67 | "Dan Gohman ", 68 | ] 69 | 70 | [workspace.dependencies] 71 | anyhow = "1" 72 | async-task = "4.7" 73 | aws-config = { version = "1.8.8", default-features = false } 74 | aws-sdk-s3 = { version = "1.108.0", default-features = false } 75 | aws-smithy-async = { version = "1.2.6", default-features = false } 76 | aws-smithy-types = { version = "1.3.3", default-features = false } 77 | aws-smithy-runtime-api = { version = "1.9.1", default-features = false } 78 | axum = { version = "0.8.6", default-features = false } 79 | bytes = "1.10.1" 80 | cargo_metadata = "0.22" 81 | clap = { version = "4.5.26", features = ["derive"] } 82 | futures-core = "0.3.19" 83 | futures-lite = "1.12.0" 84 | futures-concurrency = "7.6" 85 | humantime = "2.1.0" 86 | heck = "0.5" 87 | http = "1.1" 88 | http-body = "1.0.1" 89 | http-body-util = "0.1.3" 90 | itoa = "1" 91 | pin-project-lite = "0.2.8" 92 | quote = "1.0" 93 | serde= "1" 94 | serde_json = "1" 95 | serde_qs = "0.15" 96 | sync_wrapper = "1" 97 | slab = "0.4.9" 98 | syn = "2.0" 99 | test-log = { version = "0.2", features = ["trace"] } 100 | test-programs = { path = "test-programs" } 101 | tower-service = "0.3.3" 102 | ureq = { version = "3.1", default-features = false, features = ["json"] } 103 | wasip2 = "1.0" 104 | wstd = { path = ".", version = "=0.6.3" } 105 | wstd-axum = { path = "./axum", version = "=0.6.3" } 106 | wstd-axum-macro = { path = "./axum/macro", version = "=0.6.3" } 107 | wstd-macro = { path = "./macro", version = "=0.6.3" } 108 | 109 | [package.metadata.docs.rs] 110 | all-features = true 111 | targets = [ 112 | "wasm32-wasip2" 113 | ] 114 | -------------------------------------------------------------------------------- /examples/http_client.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Result, anyhow}; 2 | use clap::{ArgAction, Parser}; 3 | use wstd::http::{Body, BodyExt, Client, Method, Request, Uri}; 4 | use wstd::io::AsyncWrite; 5 | 6 | /// Simple HTTP client 7 | /// 8 | /// A simple command-line HTTP client, implemented using `wstd`, using WASI. 9 | #[derive(Parser, Debug)] 10 | #[command(version, about)] 11 | struct Args { 12 | /// The URL to request 13 | url: Uri, 14 | 15 | /// Forward stdin to the request body 16 | #[arg(long)] 17 | body: bool, 18 | 19 | /// Add a header to the request 20 | #[arg(long = "header", action = ArgAction::Append, value_name = "HEADER")] 21 | headers: Vec, 22 | 23 | /// Method of the request 24 | #[arg(long, default_value = "GET")] 25 | method: Method, 26 | 27 | /// Set the connect timeout 28 | #[arg(long, value_name = "DURATION")] 29 | connect_timeout: Option, 30 | 31 | /// Set the first-byte timeout 32 | #[arg(long, value_name = "DURATION")] 33 | first_byte_timeout: Option, 34 | 35 | /// Set the between-bytes timeout 36 | #[arg(long, value_name = "DURATION")] 37 | between_bytes_timeout: Option, 38 | } 39 | 40 | #[wstd::main] 41 | async fn main() -> Result<()> { 42 | let args = Args::parse(); 43 | 44 | // Create and configure the `Client` 45 | 46 | let mut client = Client::new(); 47 | 48 | if let Some(connect_timeout) = args.connect_timeout { 49 | client.set_connect_timeout(*connect_timeout); 50 | } 51 | if let Some(first_byte_timeout) = args.first_byte_timeout { 52 | client.set_first_byte_timeout(*first_byte_timeout); 53 | } 54 | if let Some(between_bytes_timeout) = args.between_bytes_timeout { 55 | client.set_between_bytes_timeout(*between_bytes_timeout); 56 | } 57 | 58 | // Create and configure the request. 59 | 60 | let mut request = Request::builder(); 61 | 62 | request = request.uri(args.url).method(args.method); 63 | 64 | for header in args.headers { 65 | let mut parts = header.splitn(2, ": "); 66 | let key = parts.next().unwrap(); 67 | let value = parts 68 | .next() 69 | .ok_or_else(|| anyhow!("headers must be formatted like \"key: value\""))?; 70 | request = request.header(key, value); 71 | } 72 | 73 | // Send the request. 74 | 75 | let body = if args.body { 76 | Body::from_try_stream(wstd::io::stdin().into_inner().into_stream()) 77 | } else { 78 | Body::empty() 79 | }; 80 | 81 | let request = request.body(body)?; 82 | 83 | eprintln!("> {} / {:?}", request.method(), request.version()); 84 | for (key, value) in request.headers().iter() { 85 | let value = String::from_utf8_lossy(value.as_bytes()); 86 | eprintln!("> {key}: {value}"); 87 | } 88 | 89 | let response = client.send(request).await?; 90 | 91 | // Print the response. 92 | eprintln!("< {:?} {}", response.version(), response.status()); 93 | for (key, value) in response.headers().iter() { 94 | let value = String::from_utf8_lossy(value.as_bytes()); 95 | eprintln!("< {key}: {value}"); 96 | } 97 | 98 | let body = response.into_body().into_boxed_body().collect().await?; 99 | let trailers = body.trailers().cloned(); 100 | wstd::io::stdout() 101 | .write_all(body.to_bytes().as_ref()) 102 | .await?; 103 | 104 | if let Some(trailers) = trailers { 105 | for (key, value) in trailers.iter() { 106 | let value = String::from_utf8_lossy(value.as_bytes()); 107 | eprintln!("< {key}: {value}"); 108 | } 109 | } 110 | 111 | Ok(()) 112 | } 113 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | *Note*: this Code of Conduct pertains to individuals' behavior. Please also see the [Organizational Code of Conduct][OCoC]. Additionally, individuals must also adhere to the [Bytecode Alliance Code of Conduct][BACoC]. 4 | 5 | ## Our Pledge 6 | 7 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 8 | 9 | ## Our Standards 10 | 11 | Examples of behavior that contributes to creating a positive environment include: 12 | 13 | * Using welcoming and inclusive language 14 | * Being respectful of differing viewpoints and experiences 15 | * Gracefully accepting constructive criticism 16 | * Focusing on what is best for the community 17 | * Showing empathy towards other community members 18 | 19 | Examples of unacceptable behavior by participants include: 20 | 21 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 22 | * Trolling, insulting/derogatory comments, and personal or political attacks 23 | * Public or private harassment 24 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 25 | * Other conduct which could reasonably be considered inappropriate in a professional setting 26 | 27 | ## Our Responsibilities 28 | 29 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 30 | 31 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 32 | 33 | ## Scope 34 | 35 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 36 | 37 | ## Enforcement 38 | 39 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the Bytecode Alliance CoC team at [report@bytecodealliance.org](mailto:report@bytecodealliance.org). The CoC team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The CoC team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 40 | 41 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the Bytecode Alliance's leadership. 42 | 43 | ## Attribution 44 | 45 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 46 | 47 | [OCoC]: https://github.com/bytecodealliance/wasmtime/blob/main/ORG_CODE_OF_CONDUCT.md 48 | [BACoC]: https://github.com/bytecodealliance/governance/blob/main/CODE_OF_CONDUCT.md 49 | [homepage]: https://www.contributor-covenant.org 50 | [version]: https://www.contributor-covenant.org/version/1/4/ 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

wstd

2 |
3 | 4 | An async Rust standard library for Wasm Components and WASI 0.2 5 | 6 |
7 | 8 |
9 | A Bytecode Alliance project 10 |
11 | 12 |
13 | 14 |
15 | 16 | 17 | Crates.io version 19 | 20 | 21 | 22 | Download 24 | 25 | 26 | 27 | docs.rs docs 29 | 30 |
31 | 32 | 47 | 48 | 49 | This is a minimal async Rust standard library written exclusively to support 50 | Wasm Components. It exists primarily to enable people to write async-based 51 | applications in Rust before async-std, smol, or tokio land support for Wasm 52 | Components and WASI 0.2. Once those runtimes land support, it is recommended 53 | users switch to use those instead. 54 | 55 | ## Examples 56 | 57 | **TCP echo server** 58 | 59 | ```rust 60 | use wstd::io; 61 | use wstd::iter::AsyncIterator; 62 | use wstd::net::TcpListener; 63 | use wstd::runtime::block_on; 64 | 65 | fn main() -> io::Result<()> { 66 | block_on(async move { 67 | let listener = TcpListener::bind("127.0.0.1:8080").await?; 68 | println!("Listening on {}", listener.local_addr()?); 69 | println!("type `nc localhost 8080` to create a TCP client"); 70 | 71 | let mut incoming = listener.incoming(); 72 | while let Some(stream) = incoming.next().await { 73 | let stream = stream?; 74 | println!("Accepted from: {}", stream.peer_addr()?); 75 | io::copy(&stream, &stream).await?; 76 | } 77 | Ok(()) 78 | }) 79 | } 80 | ``` 81 | 82 | ## Installation 83 | ```sh 84 | $ cargo add wstd 85 | ``` 86 | 87 | ## Safety 88 | This crate uses ``#![deny(unsafe_code)]``, and in the very small number of 89 | exceptional cases where ``#[allow(unsafe_code)]`` is required, documentation 90 | is provided justifying its use. 91 | 92 | ## Contributing 93 | Want to join us? Check out our ["Contributing" guide][contributing] and take a 94 | look at some of these issues: 95 | 96 | - [Issues labeled "good first issue"][good-first-issue] 97 | - [Issues labeled "help wanted"][help-wanted] 98 | 99 | [contributing]: https://github.com/bytecodealliance/wstd/blob/main/CONTRIBUTING.md 100 | [good-first-issue]: https://github.com/bytecodealliance/wstd/labels/good%20first%20issue 101 | [help-wanted]: https://github.com/bytecodealliance/wstd/labels/help%20wanted 102 | 103 | ## License 104 | 105 | 106 | Licensed under Apache 107 | License, Version 2.0 with LLVM Exception. 108 | 109 | 110 |
111 | 112 | 113 | Unless you explicitly state otherwise, any contribution intentionally submitted 114 | for inclusion in this crate by you, as defined in the Apache-2.0 license with 115 | LLVM Exception, shall be licensed as above, without any additional terms or 116 | conditions. 117 | 118 | -------------------------------------------------------------------------------- /src/time/mod.rs: -------------------------------------------------------------------------------- 1 | //! Async time interfaces. 2 | 3 | pub(crate) mod utils; 4 | 5 | mod duration; 6 | mod instant; 7 | pub use duration::Duration; 8 | pub use instant::Instant; 9 | 10 | use pin_project_lite::pin_project; 11 | use std::future::Future; 12 | use std::pin::Pin; 13 | use std::task::{Context, Poll}; 14 | use wasip2::clocks::{ 15 | monotonic_clock::{subscribe_duration, subscribe_instant}, 16 | wall_clock, 17 | }; 18 | 19 | use crate::{ 20 | iter::AsyncIterator, 21 | runtime::{AsyncPollable, Reactor}, 22 | }; 23 | 24 | /// A measurement of the system clock, useful for talking to external entities 25 | /// like the file system or other processes. 26 | #[derive(Debug, Clone, Copy)] 27 | #[allow(dead_code)] 28 | pub struct SystemTime(wall_clock::Datetime); 29 | 30 | impl SystemTime { 31 | pub fn now() -> Self { 32 | Self(wall_clock::now()) 33 | } 34 | } 35 | 36 | /// An async iterator representing notifications at fixed interval. 37 | pub fn interval(duration: Duration) -> Interval { 38 | Interval { duration } 39 | } 40 | 41 | /// An async iterator representing notifications at fixed interval. 42 | /// 43 | /// See the [`interval`] function for more. 44 | #[derive(Debug)] 45 | pub struct Interval { 46 | duration: Duration, 47 | } 48 | impl AsyncIterator for Interval { 49 | type Item = Instant; 50 | 51 | async fn next(&mut self) -> Option { 52 | Some(Timer::after(self.duration).wait().await) 53 | } 54 | } 55 | 56 | #[derive(Debug)] 57 | pub struct Timer(Option); 58 | 59 | impl Timer { 60 | pub fn never() -> Timer { 61 | Timer(None) 62 | } 63 | pub fn at(deadline: Instant) -> Timer { 64 | let pollable = Reactor::current().schedule(subscribe_instant(deadline.0)); 65 | Timer(Some(pollable)) 66 | } 67 | pub fn after(duration: Duration) -> Timer { 68 | let pollable = Reactor::current().schedule(subscribe_duration(duration.0)); 69 | Timer(Some(pollable)) 70 | } 71 | pub fn set_after(&mut self, duration: Duration) { 72 | *self = Self::after(duration); 73 | } 74 | pub fn wait(&self) -> Wait { 75 | let wait_for = self.0.as_ref().map(AsyncPollable::wait_for); 76 | Wait { wait_for } 77 | } 78 | } 79 | 80 | pin_project! { 81 | /// Future created by [`Timer::wait`] 82 | #[must_use = "futures do nothing unless polled or .awaited"] 83 | pub struct Wait { 84 | #[pin] 85 | wait_for: Option 86 | } 87 | } 88 | 89 | impl Future for Wait { 90 | type Output = Instant; 91 | 92 | fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { 93 | let this = self.project(); 94 | match this.wait_for.as_pin_mut() { 95 | None => Poll::Pending, 96 | Some(f) => match f.poll(cx) { 97 | Poll::Pending => Poll::Pending, 98 | Poll::Ready(()) => Poll::Ready(Instant::now()), 99 | }, 100 | } 101 | } 102 | } 103 | 104 | #[cfg(test)] 105 | mod test { 106 | use super::*; 107 | 108 | async fn debug_duration(what: &str, f: impl Future) { 109 | let start = Instant::now(); 110 | let now = f.await; 111 | let d = now.duration_since(start); 112 | let d: std::time::Duration = d.into(); 113 | println!("{what} awaited for {} s", d.as_secs_f32()); 114 | } 115 | 116 | #[test] 117 | fn timer_now() { 118 | crate::runtime::block_on(debug_duration("timer_now", async { 119 | Timer::at(Instant::now()).wait().await 120 | })); 121 | } 122 | 123 | #[test] 124 | fn timer_after_100_milliseconds() { 125 | crate::runtime::block_on(debug_duration("timer_after_100_milliseconds", async { 126 | Timer::after(Duration::from_millis(100)).wait().await 127 | })); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/http/request.rs: -------------------------------------------------------------------------------- 1 | use super::{ 2 | Authority, HeaderMap, PathAndQuery, Uri, 3 | body::{Body, BodyHint}, 4 | error::{Context, Error, ErrorCode}, 5 | fields::{header_map_from_wasi, header_map_to_wasi}, 6 | method::{from_wasi_method, to_wasi_method}, 7 | scheme::{from_wasi_scheme, to_wasi_scheme}, 8 | }; 9 | use wasip2::http::outgoing_handler::OutgoingRequest; 10 | use wasip2::http::types::IncomingRequest; 11 | 12 | pub use http::request::{Builder, Request}; 13 | 14 | // TODO: go back and add json stuff??? 15 | 16 | pub(crate) fn try_into_outgoing(request: Request) -> Result<(OutgoingRequest, T), Error> { 17 | let wasi_req = OutgoingRequest::new(header_map_to_wasi(request.headers())?); 18 | 19 | let (parts, body) = request.into_parts(); 20 | 21 | // Set the HTTP method 22 | let method = to_wasi_method(parts.method); 23 | wasi_req 24 | .set_method(&method) 25 | .map_err(|()| anyhow::anyhow!("method rejected by wasi-http: {method:?}"))?; 26 | 27 | // Set the url scheme 28 | let scheme = parts 29 | .uri 30 | .scheme() 31 | .map(to_wasi_scheme) 32 | .unwrap_or(wasip2::http::types::Scheme::Https); 33 | wasi_req 34 | .set_scheme(Some(&scheme)) 35 | .map_err(|()| anyhow::anyhow!("scheme rejected by wasi-http: {scheme:?}"))?; 36 | 37 | // Set authority 38 | let authority = parts.uri.authority().map(Authority::as_str); 39 | wasi_req 40 | .set_authority(authority) 41 | .map_err(|()| anyhow::anyhow!("authority rejected by wasi-http {authority:?}"))?; 42 | 43 | // Set the url path + query string 44 | if let Some(p_and_q) = parts.uri.path_and_query() { 45 | wasi_req 46 | .set_path_with_query(Some(p_and_q.as_str())) 47 | .map_err(|()| anyhow::anyhow!("path and query rejected by wasi-http {p_and_q:?}"))?; 48 | } 49 | 50 | // All done; request is ready for send-off 51 | Ok((wasi_req, body)) 52 | } 53 | 54 | /// This is used by the `http_server` macro. 55 | #[doc(hidden)] 56 | pub fn try_from_incoming(incoming: IncomingRequest) -> Result, Error> { 57 | let headers: HeaderMap = header_map_from_wasi(incoming.headers()) 58 | .context("headers provided by wasi rejected by http::HeaderMap")?; 59 | 60 | let method = 61 | from_wasi_method(incoming.method()).map_err(|_| ErrorCode::HttpRequestMethodInvalid)?; 62 | let scheme = incoming 63 | .scheme() 64 | .map(|scheme| { 65 | from_wasi_scheme(scheme).context("scheme provided by wasi rejected by http::Scheme") 66 | }) 67 | .transpose()?; 68 | let authority = incoming 69 | .authority() 70 | .map(|authority| { 71 | Authority::from_maybe_shared(authority) 72 | .context("authority provided by wasi rejected by http::Authority") 73 | }) 74 | .transpose()?; 75 | let path_and_query = incoming 76 | .path_with_query() 77 | .map(|path_and_query| { 78 | PathAndQuery::from_maybe_shared(path_and_query) 79 | .context("path and query provided by wasi rejected by http::PathAndQuery") 80 | }) 81 | .transpose()?; 82 | 83 | let hint = BodyHint::from_headers(&headers)?; 84 | 85 | // `body_stream` is a child of `incoming_body` which means we cannot 86 | // drop the parent before we drop the child 87 | let incoming_body = incoming 88 | .consume() 89 | .expect("`consume` should not have been called previously on this incoming-request"); 90 | let body = Body::from_incoming(incoming_body, hint); 91 | 92 | let mut uri = Uri::builder(); 93 | if let Some(scheme) = scheme { 94 | uri = uri.scheme(scheme); 95 | } 96 | if let Some(authority) = authority { 97 | uri = uri.authority(authority); 98 | } 99 | if let Some(path_and_query) = path_and_query { 100 | uri = uri.path_and_query(path_and_query); 101 | } 102 | let uri = uri.build().context("building uri from wasi")?; 103 | 104 | let mut request = Request::builder().method(method).uri(uri); 105 | if let Some(headers_mut) = request.headers_mut() { 106 | *headers_mut = headers; 107 | } 108 | request.body(body).context("building request from wasi") 109 | } 110 | -------------------------------------------------------------------------------- /aws/src/lib.rs: -------------------------------------------------------------------------------- 1 | use anyhow::anyhow; 2 | use aws_smithy_async::rt::sleep::{AsyncSleep, Sleep}; 3 | use aws_smithy_runtime_api::client::http::{ 4 | HttpClient, HttpConnector, HttpConnectorFuture, HttpConnectorSettings, SharedHttpConnector, 5 | }; 6 | use aws_smithy_runtime_api::client::orchestrator::HttpRequest; 7 | use aws_smithy_runtime_api::client::result::ConnectorError; 8 | use aws_smithy_runtime_api::client::retries::ErrorKind; 9 | use aws_smithy_runtime_api::client::runtime_components::RuntimeComponents; 10 | use aws_smithy_runtime_api::http::Response; 11 | use aws_smithy_types::body::SdkBody; 12 | use http_body_util::{BodyStream, StreamBody}; 13 | use std::time::Duration; 14 | use sync_wrapper::SyncStream; 15 | use wstd::http::{Body as WstdBody, BodyExt, Client}; 16 | 17 | pub fn sleep_impl() -> impl AsyncSleep + 'static { 18 | WstdSleep 19 | } 20 | 21 | #[derive(Debug)] 22 | struct WstdSleep; 23 | impl AsyncSleep for WstdSleep { 24 | fn sleep(&self, duration: Duration) -> Sleep { 25 | Sleep::new(async move { 26 | wstd::task::sleep(wstd::time::Duration::from(duration)).await; 27 | }) 28 | } 29 | } 30 | 31 | pub fn http_client() -> impl HttpClient + 'static { 32 | WstdHttpClient 33 | } 34 | 35 | #[derive(Debug)] 36 | struct WstdHttpClient; 37 | 38 | impl HttpClient for WstdHttpClient { 39 | fn http_connector( 40 | &self, 41 | settings: &HttpConnectorSettings, 42 | // afaict, none of these components are relevant to this 43 | // implementation. 44 | _components: &RuntimeComponents, 45 | ) -> SharedHttpConnector { 46 | let mut client = Client::new(); 47 | if let Some(timeout) = settings.connect_timeout() { 48 | client.set_connect_timeout(timeout); 49 | } 50 | if let Some(timeout) = settings.read_timeout() { 51 | client.set_first_byte_timeout(timeout); 52 | } 53 | SharedHttpConnector::new(WstdHttpConnector(client)) 54 | } 55 | } 56 | 57 | #[derive(Debug)] 58 | struct WstdHttpConnector(Client); 59 | 60 | impl HttpConnector for WstdHttpConnector { 61 | fn call(&self, request: HttpRequest) -> HttpConnectorFuture { 62 | let client = self.0.clone(); 63 | HttpConnectorFuture::new(async move { 64 | let request = request 65 | .try_into_http1x() 66 | // This can only fail if the Extensions fail to convert 67 | .map_err(|e| ConnectorError::other(Box::new(e), None))?; 68 | // smithy's SdkBody Error is a non-'static boxed dyn stderror. 69 | // Anyhow can't represent that, so convert it to the debug impl. 70 | let request = 71 | request.map(|body| WstdBody::from_http_body(body.map_err(|e| anyhow!("{e:?}")))); 72 | // Any error given by send is considered a "ClientError" kind 73 | // which should prevent smithy from retrying like it would for a 74 | // throttling error 75 | let response = client 76 | .send(request) 77 | .await 78 | .map_err(|e| ConnectorError::other(e.into(), Some(ErrorKind::ClientError)))?; 79 | 80 | Response::try_from(response.map(|wstd_body| { 81 | // You'd think that an SdkBody would just be an impl Body with 82 | // the usual error type dance. 83 | let nonsync_body = wstd_body 84 | .into_boxed_body() 85 | .map_err(|e| e.into_boxed_dyn_error()); 86 | // But we have to do this weird dance: because Axum insists 87 | // bodies are not Sync, wstd settled on non-Sync bodies. 88 | // Smithy insists on Sync bodies. The SyncStream type exists 89 | // to assert, because all Stream operations are on &mut self, 90 | // all Streams are Sync. So, turn the Body into a Stream, make 91 | // it sync, then back to a Body. 92 | let nonsync_stream = BodyStream::new(nonsync_body); 93 | let sync_stream = SyncStream::new(nonsync_stream); 94 | let sync_body = StreamBody::new(sync_stream); 95 | SdkBody::from_body_1_x(sync_body) 96 | })) 97 | // This can only fail if the Extensions fail to convert 98 | .map_err(|e| ConnectorError::other(Box::new(e), None)) 99 | }) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /test-programs/tests/tcp_echo_server.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Context, Result}; 2 | use std::process::Command; 3 | 4 | #[test_log::test] 5 | fn tcp_echo_server() -> Result<()> { 6 | use std::io::{Read, Write}; 7 | use std::net::{Shutdown, TcpStream}; 8 | 9 | println!("testing {}", test_programs::TCP_ECHO_SERVER); 10 | 11 | // Run the component in wasmtime 12 | // -Sinherit-network required for sockets to work 13 | let mut wasmtime_process = Command::new("wasmtime") 14 | .arg("run") 15 | .arg("-Sinherit-network") 16 | .arg(test_programs::TCP_ECHO_SERVER) 17 | .stdout(std::process::Stdio::piped()) 18 | .spawn()?; 19 | 20 | let addr = get_listening_address(wasmtime_process.stdout.take().expect("stdout is piped"))?; 21 | 22 | println!("tcp echo server is listening on {addr:?}"); 23 | 24 | let mut sock1 = TcpStream::connect(addr).context("connect sock1")?; 25 | println!("sock1 connected"); 26 | 27 | let mut sock2 = TcpStream::connect(addr).context("connect sock2")?; 28 | println!("sock2 connected"); 29 | 30 | const MESSAGE1: &[u8] = b"hello, echoserver!\n"; 31 | 32 | sock1.write_all(MESSAGE1).context("write to sock1")?; 33 | println!("sock1 wrote to echo server"); 34 | 35 | let mut sock3 = TcpStream::connect(addr).context("connect sock3")?; 36 | println!("sock3 connected"); 37 | 38 | const MESSAGE2: &[u8] = b"hello, gussie!\n"; 39 | sock2.write_all(MESSAGE2).context("write to sock1")?; 40 | println!("sock2 wrote to echo server"); 41 | 42 | sock1.shutdown(Shutdown::Write)?; 43 | sock2.shutdown(Shutdown::Write)?; 44 | 45 | let mut readback2 = Vec::new(); 46 | sock2 47 | .read_to_end(&mut readback2) 48 | .context("read from sock2")?; 49 | println!("read from sock2"); 50 | 51 | let mut readback1 = Vec::new(); 52 | sock1 53 | .read_to_end(&mut readback1) 54 | .context("read from sock1")?; 55 | println!("read from sock1"); 56 | 57 | assert_eq!(MESSAGE1, readback1, "readback of sock1"); 58 | assert_eq!(MESSAGE2, readback2, "readback of sock2"); 59 | 60 | let mut sock4 = TcpStream::connect(addr).context("connect sock4")?; 61 | println!("sock4 connected"); 62 | const MESSAGE4: &[u8] = b"hello, sparky!\n"; 63 | sock4.write_all(MESSAGE4).context("write to sock4")?; 64 | // Hang up - demonstrate that a failure on this connection doesn't affect 65 | // others. 66 | drop(sock4); 67 | println!("sock4 hung up"); 68 | 69 | const MESSAGE3: &[u8] = b"hello, willa!\n"; 70 | sock3.write_all(MESSAGE3).context("write to sock3")?; 71 | println!("sock3 wrote to echo server"); 72 | sock3.shutdown(Shutdown::Write)?; 73 | 74 | let mut readback3 = Vec::new(); 75 | sock3 76 | .read_to_end(&mut readback3) 77 | .context("read from sock3")?; 78 | println!("read from sock3"); 79 | assert_eq!(MESSAGE3, readback3, "readback of sock3"); 80 | 81 | wasmtime_process.kill()?; 82 | 83 | Ok(()) 84 | } 85 | 86 | fn get_listening_address( 87 | mut wasmtime_stdout: std::process::ChildStdout, 88 | ) -> Result { 89 | use std::io::Read; 90 | use std::thread::sleep; 91 | use std::time::Duration; 92 | 93 | // Gather complete contents of stdout here 94 | let mut stdout_contents = String::new(); 95 | loop { 96 | // Wait for process to print 97 | sleep(Duration::from_millis(100)); 98 | 99 | // Read more that the process printed, append to contents 100 | let mut buf = vec![0; 4096]; 101 | let len = wasmtime_stdout 102 | .read(&mut buf) 103 | .context("reading wasmtime stdout")?; 104 | buf.truncate(len); 105 | stdout_contents 106 | .push_str(std::str::from_utf8(&buf).context("wasmtime stdout should be string")?); 107 | 108 | // Parse out the line where guest program says where it is listening 109 | for line in stdout_contents.lines() { 110 | if let Some(rest) = line.strip_prefix("Listening on ") { 111 | // Forget wasmtime_stdout, rather than drop it, so that any 112 | // subsequent stdout from wasmtime doesn't panic on a broken 113 | // pipe. 114 | std::mem::forget(wasmtime_stdout); 115 | return rest 116 | .parse() 117 | .with_context(|| format!("parsing socket addr from line: {line:?}")); 118 | } 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/http/client.rs: -------------------------------------------------------------------------------- 1 | use super::{Body, Error, Request, Response}; 2 | use crate::http::request::try_into_outgoing; 3 | use crate::http::response::try_from_incoming; 4 | use crate::io::AsyncPollable; 5 | use crate::time::Duration; 6 | use wasip2::http::types::RequestOptions as WasiRequestOptions; 7 | 8 | /// An HTTP client. 9 | #[derive(Debug, Clone)] 10 | pub struct Client { 11 | options: Option, 12 | } 13 | 14 | impl Default for Client { 15 | fn default() -> Self { 16 | Self::new() 17 | } 18 | } 19 | 20 | impl Client { 21 | /// Create a new instance of `Client` 22 | pub fn new() -> Self { 23 | Self { options: None } 24 | } 25 | 26 | /// Send an HTTP request. 27 | pub async fn send>(&self, req: Request) -> Result, Error> { 28 | let (wasi_req, body) = try_into_outgoing(req)?; 29 | let body = body.into(); 30 | let wasi_body = wasi_req.body().unwrap(); 31 | 32 | // 1. Start sending the request head 33 | let res = wasip2::http::outgoing_handler::handle(wasi_req, self.wasi_options()?).unwrap(); 34 | 35 | let ((), body) = futures_lite::future::try_zip( 36 | async move { 37 | // 3. send the body: 38 | body.send(wasi_body).await 39 | }, 40 | async move { 41 | // 4. Receive the response 42 | AsyncPollable::new(res.subscribe()).wait_for().await; 43 | 44 | // NOTE: the first `unwrap` is to ensure readiness, the second `unwrap` 45 | // is to trap if we try and get the response more than once. The final 46 | // `?` is to raise the actual error if there is one. 47 | let res = res.get().unwrap().unwrap()?; 48 | try_from_incoming(res) 49 | }, 50 | ) 51 | .await?; 52 | Ok(body) 53 | } 54 | 55 | /// Set timeout on connecting to HTTP server 56 | pub fn set_connect_timeout(&mut self, d: impl Into) { 57 | self.options_mut().connect_timeout = Some(d.into()); 58 | } 59 | 60 | /// Set timeout on recieving first byte of the Response body 61 | pub fn set_first_byte_timeout(&mut self, d: impl Into) { 62 | self.options_mut().first_byte_timeout = Some(d.into()); 63 | } 64 | 65 | /// Set timeout on recieving subsequent chunks of bytes in the Response body stream 66 | pub fn set_between_bytes_timeout(&mut self, d: impl Into) { 67 | self.options_mut().between_bytes_timeout = Some(d.into()); 68 | } 69 | 70 | fn options_mut(&mut self) -> &mut RequestOptions { 71 | match &mut self.options { 72 | Some(o) => o, 73 | uninit => { 74 | *uninit = Some(RequestOptions::default()); 75 | uninit.as_mut().unwrap() 76 | } 77 | } 78 | } 79 | 80 | fn wasi_options(&self) -> Result, crate::http::Error> { 81 | self.options 82 | .as_ref() 83 | .map(RequestOptions::to_wasi) 84 | .transpose() 85 | } 86 | } 87 | 88 | #[derive(Default, Debug, Clone)] 89 | struct RequestOptions { 90 | connect_timeout: Option, 91 | first_byte_timeout: Option, 92 | between_bytes_timeout: Option, 93 | } 94 | 95 | impl RequestOptions { 96 | fn to_wasi(&self) -> Result { 97 | let wasi = WasiRequestOptions::new(); 98 | if let Some(timeout) = self.connect_timeout { 99 | wasi.set_connect_timeout(Some(timeout.0)).map_err(|()| { 100 | anyhow::Error::msg( 101 | "wasi-http implementation does not support connect timeout option", 102 | ) 103 | })?; 104 | } 105 | if let Some(timeout) = self.first_byte_timeout { 106 | wasi.set_first_byte_timeout(Some(timeout.0)).map_err(|()| { 107 | anyhow::Error::msg( 108 | "wasi-http implementation does not support first byte timeout option", 109 | ) 110 | })?; 111 | } 112 | if let Some(timeout) = self.between_bytes_timeout { 113 | wasi.set_between_bytes_timeout(Some(timeout.0)) 114 | .map_err(|()| { 115 | anyhow::Error::msg( 116 | "wasi-http implementation does not support between byte timeout option", 117 | ) 118 | })?; 119 | } 120 | Ok(wasi) 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /examples/complex_http_client.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Result, anyhow}; 2 | use clap::{ArgAction, Parser}; 3 | use std::str::FromStr; 4 | use wstd::http::{Body, BodyExt, Client, HeaderMap, HeaderName, HeaderValue, Method, Request, Uri}; 5 | use wstd::io::AsyncWrite; 6 | 7 | /// Complex HTTP client 8 | /// 9 | /// A somewhat more complex command-line HTTP client, implemented using 10 | /// `wstd`, using WASI. 11 | #[derive(Parser, Debug)] 12 | #[command(version, about)] 13 | struct Args { 14 | /// The URL to request 15 | url: Uri, 16 | 17 | /// Forward stdin to the request body 18 | #[arg(long)] 19 | body: bool, 20 | 21 | /// Add a header to the request 22 | #[arg(long = "header", action = ArgAction::Append, value_name = "HEADER")] 23 | headers: Vec, 24 | 25 | /// Add a trailer to the request 26 | #[arg(long = "trailer", action = ArgAction::Append, value_name = "TRAILER")] 27 | trailers: Vec, 28 | 29 | /// Method of the request 30 | #[arg(long, default_value = "GET")] 31 | method: Method, 32 | 33 | /// Set the connect timeout 34 | #[arg(long, value_name = "DURATION")] 35 | connect_timeout: Option, 36 | 37 | /// Set the first-byte timeout 38 | #[arg(long, value_name = "DURATION")] 39 | first_byte_timeout: Option, 40 | 41 | /// Set the between-bytes timeout 42 | #[arg(long, value_name = "DURATION")] 43 | between_bytes_timeout: Option, 44 | } 45 | 46 | #[wstd::main] 47 | async fn main() -> Result<()> { 48 | let args = Args::parse(); 49 | 50 | // Create and configure the `Client` 51 | 52 | let mut client = Client::new(); 53 | 54 | if let Some(connect_timeout) = args.connect_timeout { 55 | client.set_connect_timeout(*connect_timeout); 56 | } 57 | if let Some(first_byte_timeout) = args.first_byte_timeout { 58 | client.set_first_byte_timeout(*first_byte_timeout); 59 | } 60 | if let Some(between_bytes_timeout) = args.between_bytes_timeout { 61 | client.set_between_bytes_timeout(*between_bytes_timeout); 62 | } 63 | 64 | // Create and configure the request. 65 | 66 | let mut request = Request::builder(); 67 | 68 | request = request.uri(args.url).method(args.method); 69 | 70 | for header in args.headers { 71 | let mut parts = header.splitn(2, ": "); 72 | let key = parts.next().unwrap(); 73 | let value = parts 74 | .next() 75 | .ok_or_else(|| anyhow!("headers must be formatted like \"key: value\""))?; 76 | request = request.header(key, value); 77 | } 78 | let mut trailers = HeaderMap::new(); 79 | for trailer in args.trailers { 80 | let mut parts = trailer.splitn(2, ": "); 81 | let key = parts.next().unwrap(); 82 | let value = parts 83 | .next() 84 | .ok_or_else(|| anyhow!("trailers must be formatted like \"key: value\""))?; 85 | trailers.insert(HeaderName::from_str(key)?, HeaderValue::from_str(value)?); 86 | } 87 | 88 | let body = if args.body { 89 | Body::from_try_stream(wstd::io::stdin().into_inner().into_stream()).into_boxed_body() 90 | } else { 91 | Body::empty().into_boxed_body() 92 | }; 93 | let t = trailers.clone(); 94 | let body = body.with_trailers(async move { if t.is_empty() { None } else { Some(Ok(t)) } }); 95 | let request = request.body(Body::from_http_body(body))?; 96 | 97 | // Send the request. 98 | eprintln!("> {} / {:?}", request.method(), request.version()); 99 | for (key, value) in request.headers().iter() { 100 | let value = String::from_utf8_lossy(value.as_bytes()); 101 | eprintln!("> {key}: {value}"); 102 | } 103 | 104 | let response = client.send(request).await?; 105 | 106 | if !trailers.is_empty() { 107 | eprintln!("..."); 108 | } 109 | for (key, value) in trailers.iter() { 110 | let value = String::from_utf8_lossy(value.as_bytes()); 111 | eprintln!("> {key}: {value}"); 112 | } 113 | 114 | // Print the response. 115 | 116 | eprintln!("< {:?} {}", response.version(), response.status()); 117 | for (key, value) in response.headers().iter() { 118 | let value = String::from_utf8_lossy(value.as_bytes()); 119 | eprintln!("< {key}: {value}"); 120 | } 121 | 122 | let body = response.into_body().into_boxed_body().collect().await?; 123 | let trailers = body.trailers().cloned(); 124 | wstd::io::stdout() 125 | .write_all(body.to_bytes().as_ref()) 126 | .await?; 127 | 128 | if let Some(trailers) = trailers { 129 | for (key, value) in trailers.iter() { 130 | let value = String::from_utf8_lossy(value.as_bytes()); 131 | eprintln!("< {key}: {value}"); 132 | } 133 | } 134 | 135 | Ok(()) 136 | } 137 | -------------------------------------------------------------------------------- /src/net/tcp_listener.rs: -------------------------------------------------------------------------------- 1 | use wasip2::sockets::network::Ipv4SocketAddress; 2 | use wasip2::sockets::tcp::{IpAddressFamily, IpSocketAddress, TcpSocket}; 3 | 4 | use crate::io; 5 | use crate::iter::AsyncIterator; 6 | use std::net::SocketAddr; 7 | 8 | use super::{TcpStream, to_io_err}; 9 | use crate::runtime::AsyncPollable; 10 | 11 | /// A TCP socket server, listening for connections. 12 | #[derive(Debug)] 13 | pub struct TcpListener { 14 | // Field order matters: must drop this child before parent below 15 | pollable: AsyncPollable, 16 | socket: TcpSocket, 17 | } 18 | 19 | impl TcpListener { 20 | /// Creates a new TcpListener which will be bound to the specified address. 21 | /// 22 | /// The returned listener is ready for accepting connections. 23 | pub async fn bind(addr: &str) -> io::Result { 24 | let addr: SocketAddr = addr 25 | .parse() 26 | .map_err(|_| io::Error::other("failed to parse string to socket addr"))?; 27 | let family = match addr { 28 | SocketAddr::V4(_) => IpAddressFamily::Ipv4, 29 | SocketAddr::V6(_) => IpAddressFamily::Ipv6, 30 | }; 31 | let socket = 32 | wasip2::sockets::tcp_create_socket::create_tcp_socket(family).map_err(to_io_err)?; 33 | let network = wasip2::sockets::instance_network::instance_network(); 34 | 35 | let local_address = sockaddr_to_wasi(addr); 36 | 37 | socket 38 | .start_bind(&network, local_address) 39 | .map_err(to_io_err)?; 40 | let pollable = AsyncPollable::new(socket.subscribe()); 41 | pollable.wait_for().await; 42 | socket.finish_bind().map_err(to_io_err)?; 43 | 44 | socket.start_listen().map_err(to_io_err)?; 45 | pollable.wait_for().await; 46 | socket.finish_listen().map_err(to_io_err)?; 47 | Ok(Self { pollable, socket }) 48 | } 49 | 50 | /// Returns the local socket address of this listener. 51 | pub fn local_addr(&self) -> io::Result { 52 | self.socket 53 | .local_address() 54 | .map_err(to_io_err) 55 | .map(sockaddr_from_wasi) 56 | } 57 | 58 | /// Returns an iterator over the connections being received on this listener. 59 | pub fn incoming(&self) -> Incoming<'_> { 60 | Incoming { listener: self } 61 | } 62 | } 63 | 64 | /// An iterator that infinitely accepts connections on a TcpListener. 65 | #[derive(Debug)] 66 | pub struct Incoming<'a> { 67 | listener: &'a TcpListener, 68 | } 69 | 70 | impl<'a> AsyncIterator for Incoming<'a> { 71 | type Item = io::Result; 72 | 73 | async fn next(&mut self) -> Option { 74 | self.listener.pollable.wait_for().await; 75 | let (socket, input, output) = match self.listener.socket.accept().map_err(to_io_err) { 76 | Ok(accepted) => accepted, 77 | Err(err) => return Some(Err(err)), 78 | }; 79 | Some(Ok(TcpStream::new(input, output, socket))) 80 | } 81 | } 82 | 83 | fn sockaddr_from_wasi(addr: IpSocketAddress) -> std::net::SocketAddr { 84 | use wasip2::sockets::network::Ipv6SocketAddress; 85 | match addr { 86 | IpSocketAddress::Ipv4(Ipv4SocketAddress { address, port }) => { 87 | std::net::SocketAddr::V4(std::net::SocketAddrV4::new( 88 | std::net::Ipv4Addr::new(address.0, address.1, address.2, address.3), 89 | port, 90 | )) 91 | } 92 | IpSocketAddress::Ipv6(Ipv6SocketAddress { 93 | address, 94 | port, 95 | flow_info, 96 | scope_id, 97 | }) => std::net::SocketAddr::V6(std::net::SocketAddrV6::new( 98 | std::net::Ipv6Addr::new( 99 | address.0, address.1, address.2, address.3, address.4, address.5, address.6, 100 | address.7, 101 | ), 102 | port, 103 | flow_info, 104 | scope_id, 105 | )), 106 | } 107 | } 108 | 109 | fn sockaddr_to_wasi(addr: std::net::SocketAddr) -> IpSocketAddress { 110 | use wasip2::sockets::network::Ipv6SocketAddress; 111 | match addr { 112 | std::net::SocketAddr::V4(addr) => { 113 | let ip = addr.ip().octets(); 114 | IpSocketAddress::Ipv4(Ipv4SocketAddress { 115 | address: (ip[0], ip[1], ip[2], ip[3]), 116 | port: addr.port(), 117 | }) 118 | } 119 | std::net::SocketAddr::V6(addr) => { 120 | let ip = addr.ip().segments(); 121 | IpSocketAddress::Ipv6(Ipv6SocketAddress { 122 | address: (ip[0], ip[1], ip[2], ip[3], ip[4], ip[5], ip[6], ip[7]), 123 | port: addr.port(), 124 | flow_info: addr.flowinfo(), 125 | scope_id: addr.scope_id(), 126 | }) 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /aws/examples/s3.rs: -------------------------------------------------------------------------------- 1 | //! Example s3 client running on `wstd` via `wstd_aws` 2 | //! 3 | //! This example is a wasi cli command. It accepts command line arguments 4 | //! with the subcommand `list` to list a bucket's contents, and `get ` 5 | //! to get an object from a bucket and write it to the filesystem. 6 | //! 7 | //! This example *must be compiled in release mode* - in debug mode, the aws 8 | //! sdk's generated code will overflow the maximum permitted wasm locals in 9 | //! a single function. 10 | //! 11 | //! Compile it with: 12 | //! 13 | //! ```sh 14 | //! cargo build -p wstd-aws --target wasm32-wasip2 --release --examples 15 | //! ``` 16 | //! 17 | //! When running this example, you will need AWS credentials provided in environment 18 | //! variables. 19 | //! 20 | //! Run it with: 21 | //! ```sh 22 | //! wasmtime run -Shttp \ 23 | //! --env AWS_ACCESS_KEY_ID \ 24 | //! --env AWS_SECRET_ACCESS_KEY \ 25 | //! --env AWS_SESSION_TOKEN \ 26 | //! --dir .::. \ 27 | //! target/wasm22-wasip2/release/examples/s3.wasm 28 | //! ``` 29 | //! 30 | //! or alternatively run it with: 31 | //! ```sh 32 | //! cargo run --target wasm32-wasip2 -p wstd-aws --example s3 33 | //! ``` 34 | //! 35 | //! which uses the wasmtime cli, as above, via configiration found in this 36 | //! workspace's `.cargo/config`. 37 | //! 38 | //! By default, this script accesses the `wstd-example-bucket` in `us-west-2`. 39 | //! To change the bucket or region, use the `--bucket` and `--region` cli 40 | //! flags before the subcommand. 41 | 42 | use anyhow::Result; 43 | use clap::{Parser, Subcommand}; 44 | 45 | use aws_config::{BehaviorVersion, Region}; 46 | use aws_sdk_s3::Client; 47 | 48 | #[derive(Debug, Parser)] 49 | #[command(version, about, long_about = None)] 50 | struct Opts { 51 | /// The AWS Region. Defaults to us-west-2 if not provided. 52 | #[arg(short, long)] 53 | region: Option, 54 | /// The name of the bucket. Defaults to wstd-example-bucket if not 55 | /// provided. 56 | #[arg(short, long)] 57 | bucket: Option, 58 | 59 | #[command(subcommand)] 60 | command: Option, 61 | } 62 | 63 | #[derive(Subcommand, Debug)] 64 | enum Command { 65 | List, 66 | Get { 67 | key: String, 68 | #[arg(short, long)] 69 | out: Option, 70 | }, 71 | } 72 | 73 | #[wstd::main] 74 | async fn main() -> Result<()> { 75 | let opts = Opts::parse(); 76 | let region = opts 77 | .region 78 | .clone() 79 | .unwrap_or_else(|| "us-west-2".to_owned()); 80 | let bucket = opts 81 | .bucket 82 | .clone() 83 | .unwrap_or_else(|| "wstd-example-bucket".to_owned()); 84 | 85 | let config = aws_config::defaults(BehaviorVersion::latest()) 86 | .region(Region::new(region)) 87 | .sleep_impl(wstd_aws::sleep_impl()) 88 | .http_client(wstd_aws::http_client()) 89 | .load() 90 | .await; 91 | 92 | let client = Client::new(&config); 93 | 94 | match opts.command.as_ref().unwrap_or(&Command::List) { 95 | Command::List => { 96 | let output = list(&bucket, &client).await?; 97 | print!("{}", output); 98 | } 99 | Command::Get { key, out } => { 100 | let contents = get(&bucket, &client, key).await?; 101 | let output: &str = if let Some(out) = out { 102 | out.as_str() 103 | } else { 104 | key.as_str() 105 | }; 106 | std::fs::write(output, contents)?; 107 | } 108 | } 109 | Ok(()) 110 | } 111 | 112 | async fn list(bucket: &str, client: &Client) -> Result { 113 | let mut listing = client 114 | .list_objects_v2() 115 | .bucket(bucket.to_owned()) 116 | .into_paginator() 117 | .send(); 118 | 119 | let mut output = String::new(); 120 | output += "key\tetag\tlast_modified\tstorage_class\n"; 121 | while let Some(res) = listing.next().await { 122 | let object = res?; 123 | for item in object.contents() { 124 | output += &format!( 125 | "{}\t{}\t{}\t{}\n", 126 | item.key().unwrap_or_default(), 127 | item.e_tag().unwrap_or_default(), 128 | item.last_modified() 129 | .map(|lm| format!("{lm}")) 130 | .unwrap_or_default(), 131 | item.storage_class() 132 | .map(|sc| format!("{sc}")) 133 | .unwrap_or_default(), 134 | ); 135 | } 136 | } 137 | Ok(output) 138 | } 139 | 140 | async fn get(bucket: &str, client: &Client, key: &str) -> Result> { 141 | let object = client 142 | .get_object() 143 | .bucket(bucket.to_owned()) 144 | .key(key) 145 | .send() 146 | .await?; 147 | let data = object.body.collect().await?; 148 | Ok(data.to_vec()) 149 | } 150 | -------------------------------------------------------------------------------- /src/io/stdio.rs: -------------------------------------------------------------------------------- 1 | use super::{AsyncInputStream, AsyncOutputStream, AsyncRead, AsyncWrite, Result}; 2 | use std::cell::LazyCell; 3 | use wasip2::cli::terminal_input::TerminalInput; 4 | use wasip2::cli::terminal_output::TerminalOutput; 5 | 6 | /// Use the program's stdin as an `AsyncInputStream`. 7 | #[derive(Debug)] 8 | pub struct Stdin { 9 | stream: AsyncInputStream, 10 | terminput: LazyCell>, 11 | } 12 | 13 | /// Get the program's stdin for use as an `AsyncInputStream`. 14 | pub fn stdin() -> Stdin { 15 | let stream = AsyncInputStream::new(wasip2::cli::stdin::get_stdin()); 16 | Stdin { 17 | stream, 18 | terminput: LazyCell::new(wasip2::cli::terminal_stdin::get_terminal_stdin), 19 | } 20 | } 21 | 22 | impl Stdin { 23 | /// Check if stdin is a terminal. 24 | pub fn is_terminal(&self) -> bool { 25 | LazyCell::force(&self.terminput).is_some() 26 | } 27 | 28 | /// Get the `AsyncInputStream` used to implement `Stdin` 29 | pub fn into_inner(self) -> AsyncInputStream { 30 | self.stream 31 | } 32 | } 33 | 34 | impl AsyncRead for Stdin { 35 | #[inline] 36 | async fn read(&mut self, buf: &mut [u8]) -> Result { 37 | self.stream.read(buf).await 38 | } 39 | 40 | #[inline] 41 | async fn read_to_end(&mut self, buf: &mut Vec) -> Result { 42 | self.stream.read_to_end(buf).await 43 | } 44 | 45 | #[inline] 46 | fn as_async_input_stream(&self) -> Option<&AsyncInputStream> { 47 | Some(&self.stream) 48 | } 49 | } 50 | 51 | /// Use the program's stdout as an `AsyncOutputStream`. 52 | #[derive(Debug)] 53 | pub struct Stdout { 54 | stream: AsyncOutputStream, 55 | termoutput: LazyCell>, 56 | } 57 | 58 | /// Get the program's stdout for use as an `AsyncOutputStream`. 59 | pub fn stdout() -> Stdout { 60 | let stream = AsyncOutputStream::new(wasip2::cli::stdout::get_stdout()); 61 | Stdout { 62 | stream, 63 | termoutput: LazyCell::new(wasip2::cli::terminal_stdout::get_terminal_stdout), 64 | } 65 | } 66 | 67 | impl Stdout { 68 | /// Check if stdout is a terminal. 69 | pub fn is_terminal(&self) -> bool { 70 | LazyCell::force(&self.termoutput).is_some() 71 | } 72 | 73 | /// Get the `AsyncOutputStream` used to implement `Stdout` 74 | pub fn into_inner(self) -> AsyncOutputStream { 75 | self.stream 76 | } 77 | } 78 | 79 | impl AsyncWrite for Stdout { 80 | #[inline] 81 | async fn write(&mut self, buf: &[u8]) -> Result { 82 | self.stream.write(buf).await 83 | } 84 | 85 | #[inline] 86 | async fn flush(&mut self) -> Result<()> { 87 | self.stream.flush().await 88 | } 89 | 90 | #[inline] 91 | async fn write_all(&mut self, buf: &[u8]) -> Result<()> { 92 | self.stream.write_all(buf).await 93 | } 94 | 95 | #[inline] 96 | fn as_async_output_stream(&self) -> Option<&AsyncOutputStream> { 97 | self.stream.as_async_output_stream() 98 | } 99 | } 100 | 101 | /// Use the program's stdout as an `AsyncOutputStream`. 102 | #[derive(Debug)] 103 | pub struct Stderr { 104 | stream: AsyncOutputStream, 105 | termoutput: LazyCell>, 106 | } 107 | 108 | /// Get the program's stdout for use as an `AsyncOutputStream`. 109 | pub fn stderr() -> Stderr { 110 | let stream = AsyncOutputStream::new(wasip2::cli::stderr::get_stderr()); 111 | Stderr { 112 | stream, 113 | termoutput: LazyCell::new(wasip2::cli::terminal_stderr::get_terminal_stderr), 114 | } 115 | } 116 | 117 | impl Stderr { 118 | /// Check if stderr is a terminal. 119 | pub fn is_terminal(&self) -> bool { 120 | LazyCell::force(&self.termoutput).is_some() 121 | } 122 | 123 | /// Get the `AsyncOutputStream` used to implement `Stderr` 124 | pub fn into_inner(self) -> AsyncOutputStream { 125 | self.stream 126 | } 127 | } 128 | 129 | impl AsyncWrite for Stderr { 130 | #[inline] 131 | async fn write(&mut self, buf: &[u8]) -> Result { 132 | self.stream.write(buf).await 133 | } 134 | 135 | #[inline] 136 | async fn flush(&mut self) -> Result<()> { 137 | self.stream.flush().await 138 | } 139 | 140 | #[inline] 141 | async fn write_all(&mut self, buf: &[u8]) -> Result<()> { 142 | self.stream.write_all(buf).await 143 | } 144 | 145 | #[inline] 146 | fn as_async_output_stream(&self) -> Option<&AsyncOutputStream> { 147 | self.stream.as_async_output_stream() 148 | } 149 | } 150 | 151 | #[cfg(test)] 152 | mod test { 153 | use crate::io::AsyncWrite; 154 | use crate::runtime::block_on; 155 | #[test] 156 | // No internal predicate. Run test with --nocapture and inspect output manually. 157 | fn stdout_println_hello_world() { 158 | block_on(async { 159 | let mut stdout = super::stdout(); 160 | let term = if stdout.is_terminal() { "is" } else { "is not" }; 161 | stdout 162 | .write_all(format!("hello, world! stdout {term} a terminal\n",).as_bytes()) 163 | .await 164 | .unwrap(); 165 | }) 166 | } 167 | #[test] 168 | // No internal predicate. Run test with --nocapture and inspect output manually. 169 | fn stderr_println_hello_world() { 170 | block_on(async { 171 | let mut stdout = super::stdout(); 172 | let term = if stdout.is_terminal() { "is" } else { "is not" }; 173 | stdout 174 | .write_all(format!("hello, world! stderr {term} a terminal\n",).as_bytes()) 175 | .await 176 | .unwrap(); 177 | }) 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /examples/http_server.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Context, Result}; 2 | use futures_lite::stream::{once_future, unfold}; 3 | use http_body_util::{BodyExt, StreamBody}; 4 | use std::convert::Infallible; 5 | use wstd::http::body::{Body, Bytes, Frame}; 6 | use wstd::http::{Error, HeaderMap, Request, Response, StatusCode}; 7 | use wstd::time::{Duration, Instant}; 8 | 9 | #[wstd::http_server] 10 | async fn main(request: Request) -> Result, Error> { 11 | let path = request.uri().path_and_query().unwrap().as_str(); 12 | println!("serving {path}"); 13 | match path { 14 | "/" => http_home(request).await, 15 | "/wait-response" => http_wait_response(request).await, 16 | "/wait-body" => http_wait_body(request).await, 17 | "/stream-body" => http_stream_body(request).await, 18 | "/echo" => http_echo(request).await, 19 | "/echo-headers" => http_echo_headers(request).await, 20 | "/echo-trailers" => http_echo_trailers(request).await, 21 | "/response-status" => http_response_status(request).await, 22 | "/response-fail" => http_response_fail(request).await, 23 | "/response-body-fail" => http_body_fail(request).await, 24 | _ => http_not_found(request).await, 25 | } 26 | } 27 | 28 | async fn http_home(_request: Request) -> Result> { 29 | // To send a single string as the response body, use `Responder::respond`. 30 | Ok(Response::new( 31 | "Hello, wasi:http/proxy world!\n".to_owned().into(), 32 | )) 33 | } 34 | 35 | async fn http_wait_response(_request: Request) -> Result> { 36 | // Get the time now 37 | let now = Instant::now(); 38 | 39 | // Sleep for one second. 40 | wstd::task::sleep(Duration::from_secs(1)).await; 41 | 42 | // Compute how long we slept for. 43 | let elapsed = Instant::now().duration_since(now).as_millis(); 44 | 45 | Ok(Response::new( 46 | format!("slept for {elapsed} millis\n").into(), 47 | )) 48 | } 49 | 50 | async fn http_wait_body(_request: Request) -> Result> { 51 | // Get the time now 52 | let now = Instant::now(); 53 | 54 | let body = async move { 55 | // Sleep for one second. 56 | wstd::task::sleep(Duration::from_secs(1)).await; 57 | 58 | // Compute how long we slept for. 59 | let elapsed = Instant::now().duration_since(now).as_millis(); 60 | Ok::<_, Infallible>(Bytes::from(format!("slept for {elapsed} millis\n"))) 61 | }; 62 | 63 | Ok(Response::new(Body::from_try_stream(once_future(body)))) 64 | } 65 | 66 | async fn http_stream_body(_request: Request) -> Result> { 67 | // Get the time now 68 | let start = Instant::now(); 69 | 70 | let body = move |iters: usize| async move { 71 | if iters == 0 { 72 | return None; 73 | } 74 | // Sleep for 0.1 second. 75 | wstd::task::sleep(Duration::from_millis(100)).await; 76 | 77 | // Compute how long we slept for. 78 | let elapsed = Instant::now().duration_since(start).as_millis(); 79 | Some(( 80 | Ok::<_, Infallible>(Bytes::from(format!( 81 | "stream started {elapsed} millis ago\n" 82 | ))), 83 | iters - 1, 84 | )) 85 | }; 86 | 87 | Ok(Response::new(Body::from_try_stream(unfold(5, body)))) 88 | } 89 | 90 | async fn http_echo(request: Request) -> Result> { 91 | let (_parts, body) = request.into_parts(); 92 | Ok(Response::new(body)) 93 | } 94 | 95 | async fn http_echo_headers(request: Request) -> Result> { 96 | let mut response = Response::builder(); 97 | *response.headers_mut().unwrap() = request.into_parts().0.headers; 98 | Ok(response.body("".to_owned().into())?) 99 | } 100 | 101 | async fn http_echo_trailers(request: Request) -> Result> { 102 | let collected = request.into_body().into_boxed_body().collect().await?; 103 | let trailers = collected.trailers().cloned().unwrap_or_else(|| { 104 | let mut trailers = HeaderMap::new(); 105 | trailers.insert("x-no-trailers", "1".parse().unwrap()); 106 | trailers 107 | }); 108 | 109 | let body = StreamBody::new(once_future(async move { 110 | anyhow::Ok(Frame::::trailers(trailers)) 111 | })); 112 | Ok(Response::new(Body::from_http_body(body))) 113 | } 114 | 115 | async fn http_response_status(request: Request) -> Result> { 116 | let status = if let Some(header_val) = request.headers().get("x-response-status") { 117 | header_val 118 | .to_str() 119 | .context("contents of x-response-status")? 120 | .parse::() 121 | .context("u16 value from x-response-status")? 122 | } else { 123 | 500 124 | }; 125 | let mut response = Response::builder().status(status); 126 | if status == 302 { 127 | response = response.header("Location", "http://localhost/response-status"); 128 | } 129 | Ok(response.body(String::new().into())?) 130 | } 131 | 132 | async fn http_response_fail(_request: Request) -> Result> { 133 | Err(anyhow::anyhow!("error creating response")) 134 | } 135 | 136 | async fn http_body_fail(_request: Request) -> Result> { 137 | let body = StreamBody::new(once_future(async move { 138 | Err::, _>(anyhow::anyhow!("error creating body")) 139 | })); 140 | 141 | Ok(Response::new(Body::from_http_body(body))) 142 | } 143 | 144 | async fn http_not_found(_request: Request) -> Result> { 145 | let response = Response::builder() 146 | .status(StatusCode::NOT_FOUND) 147 | .body(Body::empty()) 148 | .unwrap(); 149 | Ok(response) 150 | } 151 | -------------------------------------------------------------------------------- /test-programs/tests/http_server.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use std::time::{Duration, Instant}; 3 | 4 | #[test_log::test] 5 | fn http_server() -> Result<()> { 6 | // Run wasmtime serve. 7 | // Enable -Scli because we currently don't have a way to build with the 8 | // proxy adapter, so we build with the default adapter. 9 | let _serve = test_programs::WasmtimeServe::new(test_programs::HTTP_SERVER)?; 10 | 11 | // Test each path in the server: 12 | 13 | // TEST / http_home 14 | // Response body is the hard-coded default 15 | let body: String = ureq::get("http://127.0.0.1:8081") 16 | .call()? 17 | .body_mut() 18 | .read_to_string()?; 19 | assert_eq!(body, "Hello, wasi:http/proxy world!\n"); 20 | 21 | // TEST /wait-response http_wait_response 22 | // Sleeps for 1 second, then sends a response with body containing 23 | // internally measured sleep time. 24 | let start = Instant::now(); 25 | let body: String = ureq::get("http://127.0.0.1:8081/wait-response") 26 | .call()? 27 | .body_mut() 28 | .read_to_string()?; 29 | let duration = start.elapsed(); 30 | let sleep_report = body 31 | .split(' ') 32 | .find_map(|s| s.parse::().ok()) 33 | .expect("body should print 'slept for 10xx millis'"); 34 | assert!( 35 | sleep_report >= 1000, 36 | "should have slept for 1000 or more millis, got {sleep_report}" 37 | ); 38 | assert!(duration >= Duration::from_secs(1)); 39 | 40 | // TEST /wait-body http_wait_body 41 | // Sends response status and headers, then sleeps for 1 second, then sends 42 | // body with internally measured sleep time. 43 | // With ureq we can't tell that the response status and headers were sent 44 | // with a delay in the body. Additionally, the implementation MAY buffer up the 45 | // entire response and body before sending it, though wasmtime does not. 46 | let start = Instant::now(); 47 | let body: String = ureq::get("http://127.0.0.1:8081/wait-body") 48 | .call()? 49 | .body_mut() 50 | .read_to_string()?; 51 | let duration = start.elapsed(); 52 | let sleep_report = body 53 | .split(' ') 54 | .find_map(|s| s.parse::().ok()) 55 | .expect("body should print 'slept for 10xx millis'"); 56 | assert!( 57 | sleep_report >= 1000, 58 | "should have slept for 1000 or more millis, got {sleep_report}" 59 | ); 60 | assert!(duration >= Duration::from_secs(1)); 61 | 62 | // TEST /stream-body http_stream_body 63 | // Sends response status and headers, then unfolds 5 iterations of a 64 | // stream that sleeps for 100ms and then prints the time since stream 65 | // started. 66 | // With ureq we can't tell that the response status and headers were sent 67 | // with a delay in the body. Additionally, the implementation MAY buffer up the 68 | // entire response and body before sending it, though wasmtime does not. 69 | let start = Instant::now(); 70 | let body: String = ureq::get("http://127.0.0.1:8081/stream-body") 71 | .call()? 72 | .body_mut() 73 | .read_to_string()?; 74 | let duration = start.elapsed(); 75 | assert_eq!(body.lines().count(), 5, "body has 5 lines"); 76 | for (iter, line) in body.lines().enumerate() { 77 | let sleep_report = line 78 | .split(' ') 79 | .find_map(|s| s.parse::().ok()) 80 | .expect("body should print 'stream started Nxx millis ago'"); 81 | assert!( 82 | sleep_report >= (iter * 100), 83 | "should have slept for {iter} * 100 or more millis, got {sleep_report}" 84 | ); 85 | } 86 | assert!(duration >= Duration::from_millis(500)); 87 | 88 | // TEST /echo htto_echo 89 | // Send a request body, see that we got the same back in response body. 90 | const MESSAGE: &[u8] = b"hello, echoserver!\n"; 91 | let body: String = ureq::get("http://127.0.0.1:8081/echo") 92 | .force_send_body() 93 | .send(MESSAGE)? 94 | .body_mut() 95 | .read_to_string()?; 96 | assert_eq!(body.as_bytes(), MESSAGE); 97 | 98 | // TEST /echo-headers htto_echo_headers 99 | // Send request with headers, see that all of those headers are present in 100 | // response headers 101 | let test_headers = [ 102 | ("Red", "Rhubarb"), 103 | ("Orange", "Carrots"), 104 | ("Yellow", "Bananas"), 105 | ("Green", "Broccoli"), 106 | ("Blue", "Blueberries"), 107 | ("Purple", "Beets"), 108 | ]; 109 | let mut request = ureq::get("http://127.0.0.1:8081/echo-headers"); 110 | for (name, value) in test_headers { 111 | request = request.header(name, value); 112 | } 113 | let response = request.call()?; 114 | assert!(response.headers().len() >= test_headers.len()); 115 | for (name, value) in test_headers { 116 | assert_eq!( 117 | response.headers().get(name), 118 | Some(&ureq::http::HeaderValue::from_str(value).unwrap()) 119 | ); 120 | } 121 | 122 | // NOT TESTED /echo-trailers htto_echo_trailers 123 | // ureq doesn't support trailers 124 | 125 | // TEST /response-code http_response_code 126 | // Send request with `X-Request-Code: `. Should get back that 127 | // status. 128 | let response = ureq::get("http://127.0.0.1:8081/response-status") 129 | .header("X-Response-Status", "401") 130 | .call(); 131 | // ureq gives us a 401 in an Error::StatusCode 132 | match response { 133 | Err(ureq::Error::StatusCode(401)) => {} 134 | result => { 135 | panic!("/response-code expected status 302, got: {result:?}"); 136 | } 137 | } 138 | 139 | // TEST /response-fail http_response_fail 140 | // Wasmtime gives a 500 error when wasi-http guest gives error instead of 141 | // response 142 | match ureq::get("http://127.0.0.1:8081/response-fail").call() { 143 | Err(ureq::Error::StatusCode(500)) => {} 144 | result => { 145 | panic!("/response-fail expected status 500 error, got: {result:?}"); 146 | } 147 | } 148 | 149 | // TEST /response-body-fail http_body_fail 150 | // Response status and headers sent off, then error in body will close 151 | // connection 152 | match ureq::get("http://127.0.0.1:8081/response-body-fail").call() { 153 | Err(ureq::Error::Io(_transport)) => {} 154 | result => { 155 | panic!("/response-body-fail expected io error, got: {result:?}") 156 | } 157 | } 158 | 159 | Ok(()) 160 | } 161 | -------------------------------------------------------------------------------- /src/net/tcp_stream.rs: -------------------------------------------------------------------------------- 1 | use std::io::ErrorKind; 2 | use std::net::{SocketAddr, ToSocketAddrs}; 3 | use wasip2::sockets::instance_network::instance_network; 4 | use wasip2::sockets::network::Ipv4SocketAddress; 5 | use wasip2::sockets::tcp::{IpAddressFamily, IpSocketAddress}; 6 | use wasip2::sockets::tcp_create_socket::create_tcp_socket; 7 | use wasip2::{ 8 | io::streams::{InputStream, OutputStream}, 9 | sockets::tcp::TcpSocket, 10 | }; 11 | 12 | use super::to_io_err; 13 | use crate::io::{self, AsyncInputStream, AsyncOutputStream}; 14 | use crate::runtime::AsyncPollable; 15 | 16 | /// A TCP stream between a local and a remote socket. 17 | pub struct TcpStream { 18 | input: AsyncInputStream, 19 | output: AsyncOutputStream, 20 | socket: TcpSocket, 21 | } 22 | 23 | impl TcpStream { 24 | pub(crate) fn new(input: InputStream, output: OutputStream, socket: TcpSocket) -> Self { 25 | TcpStream { 26 | input: AsyncInputStream::new(input), 27 | output: AsyncOutputStream::new(output), 28 | socket, 29 | } 30 | } 31 | 32 | /// Opens a TCP connection to a remote host. 33 | /// 34 | /// `addr` is an address of the remote host. Anything which implements the 35 | /// [`ToSocketAddrs`] trait can be supplied as the address. If `addr` 36 | /// yields multiple addresses, connect will be attempted with each of the 37 | /// addresses until a connection is successful. If none of the addresses 38 | /// result in a successful connection, the error returned from the last 39 | /// connection attempt (the last address) is returned. 40 | pub async fn connect(addr: impl ToSocketAddrs) -> io::Result { 41 | let addrs = addr.to_socket_addrs()?; 42 | let mut last_err = None; 43 | for addr in addrs { 44 | match TcpStream::connect_addr(addr).await { 45 | Ok(stream) => return Ok(stream), 46 | Err(e) => last_err = Some(e), 47 | } 48 | } 49 | 50 | Err(last_err.unwrap_or_else(|| { 51 | io::Error::new(ErrorKind::InvalidInput, "could not resolve to any address") 52 | })) 53 | } 54 | 55 | /// Establishes a connection to the specified `addr`. 56 | pub async fn connect_addr(addr: SocketAddr) -> io::Result { 57 | let family = match addr { 58 | SocketAddr::V4(_) => IpAddressFamily::Ipv4, 59 | SocketAddr::V6(_) => IpAddressFamily::Ipv6, 60 | }; 61 | let socket = create_tcp_socket(family).map_err(to_io_err)?; 62 | let network = instance_network(); 63 | 64 | let remote_address = match addr { 65 | SocketAddr::V4(addr) => { 66 | let ip = addr.ip().octets(); 67 | let address = (ip[0], ip[1], ip[2], ip[3]); 68 | let port = addr.port(); 69 | IpSocketAddress::Ipv4(Ipv4SocketAddress { port, address }) 70 | } 71 | SocketAddr::V6(_) => todo!("IPv6 not yet supported in `wstd::net::TcpStream`"), 72 | }; 73 | socket 74 | .start_connect(&network, remote_address) 75 | .map_err(to_io_err)?; 76 | let pollable = AsyncPollable::new(socket.subscribe()); 77 | pollable.wait_for().await; 78 | let (input, output) = socket.finish_connect().map_err(to_io_err)?; 79 | 80 | Ok(TcpStream::new(input, output, socket)) 81 | } 82 | 83 | /// Returns the socket address of the remote peer of this TCP connection. 84 | pub fn peer_addr(&self) -> io::Result { 85 | let addr = self.socket.remote_address().map_err(to_io_err)?; 86 | Ok(format!("{addr:?}")) 87 | } 88 | 89 | pub fn split(&self) -> (ReadHalf<'_>, WriteHalf<'_>) { 90 | (ReadHalf(self), WriteHalf(self)) 91 | } 92 | } 93 | 94 | impl Drop for TcpStream { 95 | fn drop(&mut self) { 96 | let _ = self 97 | .socket 98 | .shutdown(wasip2::sockets::tcp::ShutdownType::Both); 99 | } 100 | } 101 | 102 | impl io::AsyncRead for TcpStream { 103 | async fn read(&mut self, buf: &mut [u8]) -> io::Result { 104 | self.input.read(buf).await 105 | } 106 | 107 | fn as_async_input_stream(&self) -> Option<&AsyncInputStream> { 108 | Some(&self.input) 109 | } 110 | } 111 | 112 | impl io::AsyncRead for &TcpStream { 113 | async fn read(&mut self, buf: &mut [u8]) -> io::Result { 114 | self.input.read(buf).await 115 | } 116 | 117 | fn as_async_input_stream(&self) -> Option<&AsyncInputStream> { 118 | (**self).as_async_input_stream() 119 | } 120 | } 121 | 122 | impl io::AsyncWrite for TcpStream { 123 | async fn write(&mut self, buf: &[u8]) -> io::Result { 124 | self.output.write(buf).await 125 | } 126 | 127 | async fn flush(&mut self) -> io::Result<()> { 128 | self.output.flush().await 129 | } 130 | 131 | fn as_async_output_stream(&self) -> Option<&AsyncOutputStream> { 132 | Some(&self.output) 133 | } 134 | } 135 | 136 | impl io::AsyncWrite for &TcpStream { 137 | async fn write(&mut self, buf: &[u8]) -> io::Result { 138 | self.output.write(buf).await 139 | } 140 | 141 | async fn flush(&mut self) -> io::Result<()> { 142 | self.output.flush().await 143 | } 144 | 145 | fn as_async_output_stream(&self) -> Option<&AsyncOutputStream> { 146 | (**self).as_async_output_stream() 147 | } 148 | } 149 | 150 | pub struct ReadHalf<'a>(&'a TcpStream); 151 | impl<'a> io::AsyncRead for ReadHalf<'a> { 152 | async fn read(&mut self, buf: &mut [u8]) -> io::Result { 153 | self.0.read(buf).await 154 | } 155 | 156 | fn as_async_input_stream(&self) -> Option<&AsyncInputStream> { 157 | self.0.as_async_input_stream() 158 | } 159 | } 160 | 161 | impl<'a> Drop for ReadHalf<'a> { 162 | fn drop(&mut self) { 163 | let _ = self 164 | .0 165 | .socket 166 | .shutdown(wasip2::sockets::tcp::ShutdownType::Receive); 167 | } 168 | } 169 | 170 | pub struct WriteHalf<'a>(&'a TcpStream); 171 | impl<'a> io::AsyncWrite for WriteHalf<'a> { 172 | async fn write(&mut self, buf: &[u8]) -> io::Result { 173 | self.0.write(buf).await 174 | } 175 | 176 | async fn flush(&mut self) -> io::Result<()> { 177 | self.0.flush().await 178 | } 179 | 180 | fn as_async_output_stream(&self) -> Option<&AsyncOutputStream> { 181 | self.0.as_async_output_stream() 182 | } 183 | } 184 | 185 | impl<'a> Drop for WriteHalf<'a> { 186 | fn drop(&mut self) { 187 | let _ = self 188 | .0 189 | .socket 190 | .shutdown(wasip2::sockets::tcp::ShutdownType::Send); 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /macro/src/lib.rs: -------------------------------------------------------------------------------- 1 | use proc_macro::TokenStream; 2 | use quote::{quote, quote_spanned}; 3 | use syn::{ItemFn, parse_macro_input, spanned::Spanned}; 4 | 5 | #[proc_macro_attribute] 6 | pub fn attr_macro_main(_attr: TokenStream, item: TokenStream) -> TokenStream { 7 | let input = parse_macro_input!(item as ItemFn); 8 | 9 | if input.sig.asyncness.is_none() { 10 | return quote_spanned! { input.sig.fn_token.span()=> 11 | compile_error!("fn must be `async fn`"); 12 | } 13 | .into(); 14 | } 15 | 16 | if input.sig.ident != "main" { 17 | return quote_spanned! { input.sig.ident.span()=> 18 | compile_error!("only `async fn main` can be used for #[wstd::main]"); 19 | } 20 | .into(); 21 | } 22 | 23 | if !input.sig.inputs.is_empty() { 24 | return quote_spanned! { input.sig.inputs.span()=> 25 | compile_error!("arguments to main are not supported"); 26 | } 27 | .into(); 28 | } 29 | let attrs = input.attrs; 30 | let output = input.sig.output; 31 | let block = input.block; 32 | quote! { 33 | pub fn main() #output { 34 | 35 | #(#attrs)* 36 | async fn __run() #output { 37 | #block 38 | } 39 | 40 | ::wstd::runtime::block_on(async { 41 | __run().await 42 | }) 43 | } 44 | } 45 | .into() 46 | } 47 | 48 | #[proc_macro_attribute] 49 | pub fn attr_macro_test(_attr: TokenStream, item: TokenStream) -> TokenStream { 50 | let input = parse_macro_input!(item as ItemFn); 51 | 52 | if input.sig.asyncness.is_none() { 53 | return quote_spanned! { input.sig.fn_token.span()=> 54 | compile_error!("fn must be `async fn`"); 55 | } 56 | .into(); 57 | } 58 | 59 | let name = input.sig.ident; 60 | 61 | if !input.sig.inputs.is_empty() { 62 | return quote_spanned! { input.sig.inputs.span()=> 63 | compile_error!("arguments to main are not supported"); 64 | } 65 | .into(); 66 | } 67 | let attrs = input.attrs; 68 | let output = input.sig.output; 69 | let block = input.block; 70 | quote! { 71 | #[::core::prelude::v1::test] 72 | pub fn #name() #output { 73 | 74 | #(#attrs)* 75 | async fn __run() #output { 76 | #block 77 | } 78 | 79 | ::wstd::runtime::block_on(async { 80 | __run().await 81 | }) 82 | } 83 | } 84 | .into() 85 | } 86 | 87 | /// Enables a HTTP server main function, for creating [HTTP servers]. 88 | /// 89 | /// [HTTP servers]: https://docs.rs/wstd/latest/wstd/http/server/index.html 90 | /// 91 | /// # Examples 92 | /// 93 | /// ```ignore 94 | /// #[wstd::http_server] 95 | /// async fn main(request: Request) -> Result> { 96 | /// Ok(Response::new("Hello!\n".into())) 97 | /// } 98 | /// ``` 99 | #[proc_macro_attribute] 100 | pub fn attr_macro_http_server(_attr: TokenStream, item: TokenStream) -> TokenStream { 101 | let input = parse_macro_input!(item as ItemFn); 102 | 103 | let (run_async, run_await) = if input.sig.asyncness.is_some() { 104 | (quote!(async), quote!(.await)) 105 | } else { 106 | (quote!(), quote!()) 107 | }; 108 | 109 | let output = &input.sig.output; 110 | let inputs = &input.sig.inputs; 111 | let name = &input.sig.ident; 112 | let body = &input.block; 113 | let attrs = &input.attrs; 114 | let vis = &input.vis; 115 | 116 | if name != "main" { 117 | return quote_spanned! { input.sig.ident.span()=> 118 | compile_error!("only `async fn main` can be used for #[wstd::http_server]"); 119 | } 120 | .into(); 121 | } 122 | 123 | quote! { 124 | struct TheServer; 125 | 126 | impl ::wstd::__internal::wasip2::exports::http::incoming_handler::Guest for TheServer { 127 | fn handle( 128 | request: ::wstd::__internal::wasip2::http::types::IncomingRequest, 129 | response_out: ::wstd::__internal::wasip2::http::types::ResponseOutparam 130 | ) { 131 | #(#attrs)* 132 | #vis #run_async fn __run(#inputs) #output { 133 | #body 134 | } 135 | 136 | let responder = ::wstd::http::server::Responder::new(response_out); 137 | ::wstd::runtime::block_on(async move { 138 | match ::wstd::http::request::try_from_incoming(request) { 139 | Ok(request) => match __run(request) #run_await { 140 | Ok(response) => { responder.respond(response).await.unwrap() }, 141 | Err(err) => responder.fail(err).unwrap(), 142 | } 143 | Err(err) => responder.fail(err).unwrap(), 144 | } 145 | }) 146 | } 147 | } 148 | 149 | ::wstd::__internal::wasip2::http::proxy::export!(TheServer with_types_in ::wstd::__internal::wasip2); 150 | 151 | // Provide an actual function named `main`. 152 | // 153 | // WASI HTTP server components don't use a traditional `main` function. 154 | // They export a function named `handle` which takes a `Request` 155 | // argument, and which may be called multiple times on the same 156 | // instance. To let users write a familiar `fn main` in a file 157 | // named src/main.rs, we provide this `wstd::http_server` macro, which 158 | // transforms the user's `fn main` into the appropriate `handle` 159 | // function. 160 | // 161 | // However, when the top-level file is named src/main.rs, rustc 162 | // requires there to be a function named `main` somewhere in it. This 163 | // requirement can be disabled using `#![no_main]`, however we can't 164 | // use that automatically because macros can't contain inner 165 | // attributes, and we don't want to require users to add `#![no_main]` 166 | // in their own code. 167 | // 168 | // So, we include a definition of a function named `main` here, which 169 | // isn't intended to ever be called, and exists just to satify the 170 | // requirement for a `main` function. 171 | // 172 | // Users could use `#![no_main]` if they want to. Or, they could name 173 | // their top-level file src/lib.rs and add 174 | // ```toml 175 | // [lib] 176 | // crate-type = ["cdylib"] 177 | // ``` 178 | // to their Cargo.toml. With either of these, this "main" function will 179 | // be ignored as dead code. 180 | fn main() { 181 | unreachable!("HTTP server components should be run with `handle` rather than `run`") 182 | } 183 | } 184 | .into() 185 | } 186 | -------------------------------------------------------------------------------- /src/time/duration.rs: -------------------------------------------------------------------------------- 1 | use super::{Instant, Wait}; 2 | use std::future::IntoFuture; 3 | use std::ops::{Add, AddAssign, Sub, SubAssign}; 4 | use wasip2::clocks::monotonic_clock; 5 | 6 | /// A Duration type to represent a span of time, typically used for system 7 | /// timeouts. 8 | /// 9 | /// This type wraps `std::time::Duration` so we can implement traits on it 10 | /// without coherence issues, just like if we were implementing this in the 11 | /// stdlib. 12 | #[derive(Debug, PartialEq, PartialOrd, Ord, Eq, Hash, Clone, Copy)] 13 | pub struct Duration(pub(crate) monotonic_clock::Duration); 14 | impl Duration { 15 | /// Creates a new `Duration` from the specified number of whole seconds and 16 | /// additional nanoseconds. 17 | #[must_use] 18 | #[inline] 19 | pub fn new(secs: u64, nanos: u32) -> Duration { 20 | std::time::Duration::new(secs, nanos).into() 21 | } 22 | 23 | /// Creates a new `Duration` from the specified number of whole seconds. 24 | #[must_use] 25 | #[inline] 26 | pub fn from_secs(secs: u64) -> Duration { 27 | std::time::Duration::from_secs(secs).into() 28 | } 29 | 30 | /// Creates a new `Duration` from the specified number of milliseconds. 31 | #[must_use] 32 | #[inline] 33 | pub fn from_millis(millis: u64) -> Self { 34 | std::time::Duration::from_millis(millis).into() 35 | } 36 | 37 | /// Creates a new `Duration` from the specified number of microseconds. 38 | #[must_use] 39 | #[inline] 40 | pub fn from_micros(micros: u64) -> Self { 41 | std::time::Duration::from_micros(micros).into() 42 | } 43 | 44 | /// Creates a new `Duration` from the specified number of nanoseconds. 45 | #[must_use] 46 | #[inline] 47 | pub fn from_nanos(nanos: u64) -> Self { 48 | std::time::Duration::from_nanos(nanos).into() 49 | } 50 | 51 | /// Creates a new `Duration` from the specified number of seconds represented 52 | /// as `f64`. 53 | /// 54 | /// # Panics 55 | /// This constructor will panic if `secs` is not finite, negative or overflows `Duration`. 56 | /// 57 | /// # Examples 58 | /// ```no_run 59 | /// use wstd::time::Duration; 60 | /// 61 | /// let dur = Duration::from_secs_f64(2.7); 62 | /// assert_eq!(dur, Duration::new(2, 700_000_000)); 63 | /// ``` 64 | #[must_use] 65 | #[inline] 66 | pub fn from_secs_f64(secs: f64) -> Duration { 67 | std::time::Duration::from_secs_f64(secs).into() 68 | } 69 | 70 | /// Creates a new `Duration` from the specified number of seconds represented 71 | /// as `f32`. 72 | /// 73 | /// # Panics 74 | /// This constructor will panic if `secs` is not finite, negative or overflows `Duration`. 75 | #[must_use] 76 | #[inline] 77 | pub fn from_secs_f32(secs: f32) -> Duration { 78 | std::time::Duration::from_secs_f32(secs).into() 79 | } 80 | 81 | /// Returns the number of whole seconds contained by this `Duration`. 82 | #[must_use] 83 | #[inline] 84 | pub const fn as_secs(&self) -> u64 { 85 | self.0 / 1_000_000_000 86 | } 87 | 88 | /// Returns the number of whole milliseconds contained by this `Duration`. 89 | #[must_use] 90 | #[inline] 91 | pub const fn as_millis(&self) -> u128 { 92 | (self.0 / 1_000_000) as u128 93 | } 94 | 95 | /// Returns the number of whole microseconds contained by this `Duration`. 96 | #[must_use] 97 | #[inline] 98 | pub const fn as_micros(&self) -> u128 { 99 | (self.0 / 1_000) as u128 100 | } 101 | 102 | /// Returns the total number of nanoseconds contained by this `Duration`. 103 | #[must_use] 104 | #[inline] 105 | pub const fn as_nanos(&self) -> u128 { 106 | self.0 as u128 107 | } 108 | } 109 | 110 | impl From for Duration { 111 | fn from(inner: std::time::Duration) -> Self { 112 | Self( 113 | inner 114 | .as_nanos() 115 | .try_into() 116 | .expect("only dealing with durations that can fit in u64"), 117 | ) 118 | } 119 | } 120 | 121 | impl From for std::time::Duration { 122 | fn from(duration: Duration) -> Self { 123 | Self::from_nanos(duration.0) 124 | } 125 | } 126 | 127 | impl Add for Duration { 128 | type Output = Self; 129 | 130 | fn add(self, rhs: Duration) -> Self::Output { 131 | Self(self.0 + rhs.0) 132 | } 133 | } 134 | 135 | impl AddAssign for Duration { 136 | fn add_assign(&mut self, rhs: Duration) { 137 | *self = Self(self.0 + rhs.0) 138 | } 139 | } 140 | 141 | impl Sub for Duration { 142 | type Output = Self; 143 | 144 | fn sub(self, rhs: Duration) -> Self::Output { 145 | Self(self.0 - rhs.0) 146 | } 147 | } 148 | 149 | impl SubAssign for Duration { 150 | fn sub_assign(&mut self, rhs: Duration) { 151 | *self = Self(self.0 - rhs.0) 152 | } 153 | } 154 | 155 | impl IntoFuture for Duration { 156 | type Output = Instant; 157 | 158 | type IntoFuture = Wait; 159 | 160 | fn into_future(self) -> Self::IntoFuture { 161 | crate::task::sleep(self) 162 | } 163 | } 164 | 165 | #[cfg(test)] 166 | mod tests { 167 | use super::*; 168 | 169 | #[test] 170 | fn test_new_from_as() { 171 | assert_eq!(Duration::new(456, 864209753).as_secs(), 456); 172 | assert_eq!(Duration::new(456, 864209753).as_millis(), 456864); 173 | assert_eq!(Duration::new(456, 864209753).as_micros(), 456864209); 174 | assert_eq!(Duration::new(456, 864209753).as_nanos(), 456864209753); 175 | 176 | assert_eq!(Duration::from_secs(9876543210).as_secs(), 9876543210); 177 | assert_eq!(Duration::from_secs(9876543210).as_millis(), 9876543210_000); 178 | assert_eq!( 179 | Duration::from_secs(9876543210).as_micros(), 180 | 9876543210_000000 181 | ); 182 | assert_eq!( 183 | Duration::from_secs(9876543210).as_nanos(), 184 | 9876543210_000000000 185 | ); 186 | 187 | assert_eq!(Duration::from_millis(9876543210).as_secs(), 9876543); 188 | assert_eq!(Duration::from_millis(9876543210).as_millis(), 9876543210); 189 | assert_eq!( 190 | Duration::from_millis(9876543210).as_micros(), 191 | 9876543210_000 192 | ); 193 | assert_eq!( 194 | Duration::from_millis(9876543210).as_nanos(), 195 | 9876543210_000000 196 | ); 197 | 198 | assert_eq!(Duration::from_micros(9876543210).as_secs(), 9876); 199 | assert_eq!(Duration::from_micros(9876543210).as_millis(), 9876543); 200 | assert_eq!(Duration::from_micros(9876543210).as_micros(), 9876543210); 201 | assert_eq!(Duration::from_micros(9876543210).as_nanos(), 9876543210_000); 202 | 203 | assert_eq!(Duration::from_nanos(9876543210).as_secs(), 9); 204 | assert_eq!(Duration::from_nanos(9876543210).as_millis(), 9876); 205 | assert_eq!(Duration::from_nanos(9876543210).as_micros(), 9876543); 206 | assert_eq!(Duration::from_nanos(9876543210).as_nanos(), 9876543210); 207 | } 208 | 209 | #[test] 210 | fn test_from_secs_float() { 211 | assert_eq!(Duration::from_secs_f64(158.9).as_secs(), 158); 212 | assert_eq!(Duration::from_secs_f32(158.9).as_secs(), 158); 213 | assert_eq!(Duration::from_secs_f64(159.1).as_secs(), 159); 214 | assert_eq!(Duration::from_secs_f32(159.1).as_secs(), 159); 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /axum/examples/weather.rs: -------------------------------------------------------------------------------- 1 | //! This demo app shows a Axum based wasi-http server making an arbitrary 2 | //! number of http requests as part of serving a single response. 3 | //! 4 | //! Use the request query string to pass the parameters `city`, and optionally 5 | //! `count` (defaults to 10). This app will tell you the current weather in 6 | //! a set of `count` locations matching the `city` name. For example, when 7 | //! searching for `city=portland&count=2`, it will return Portland, OR and 8 | //! then Portland, ME - location results are sorted by population. 9 | //! 10 | //! This app first makes a request to `geocoding-api.open-meteo.com` to search 11 | //! for a set of `count` locations for a given `city` name. 12 | //! 13 | //! Then, it makes `count` requests to `api.open-meteo.com`'s forecast api to 14 | //! get the current temperature and rain accumulation in each of those 15 | //! locations. 16 | //! 17 | //! The complete set of locations and weather reports are retuned as a json 18 | //! array of records. 19 | 20 | use anyhow::{Context, Result, anyhow}; 21 | use axum::extract::{Json, Query}; 22 | use axum::http::StatusCode as AxumStatusCode; 23 | use axum::routing::{Router, get}; 24 | use serde::{Deserialize, Serialize}; 25 | use wstd::http::{Client, Request, StatusCode, Uri}; 26 | 27 | /// Be polite: user-agent tells server where these results came from, so they 28 | /// can easily block abuse 29 | const USER_AGENT: &str = "wstd-axum weather example (https://github.com/bytecodealliance/wstd)"; 30 | 31 | /// The axum http server serves just one route for get requests at /weather 32 | #[wstd_axum::http_server] 33 | fn main() -> Router { 34 | Router::new().route("/weather", get(weather)) 35 | } 36 | 37 | /// Named pair used as JSON response 38 | #[derive(Serialize)] 39 | struct LocationWeather { 40 | location: Location, 41 | weather: Weather, 42 | } 43 | 44 | /// Whole demo app lives at this one endpoint. 45 | async fn weather( 46 | Query(query): Query, 47 | ) -> axum::response::Result>> { 48 | if query.count == 0 { 49 | Err((AxumStatusCode::BAD_REQUEST, "nonzero count required"))?; 50 | } 51 | // Search for the locations in the query 52 | let location_results = fetch_locations(&query) 53 | .await 54 | .context("searching for location") 55 | .map_err(anyhow_response)?; 56 | 57 | use futures_concurrency::future::TryJoin; 58 | let results = location_results 59 | .into_iter() 60 | // For each location found, constuct a future which fetches the 61 | // weather, then returns the record of location, weather 62 | .map(|location| async move { 63 | let weather = fetch_weather(&location) 64 | .await 65 | .with_context(|| format!("fetching weather for {}", location.qualified_name))?; 66 | Ok::<_, anyhow::Error>(LocationWeather { location, weather }) 67 | }) 68 | // Collect a vec of futures 69 | .collect::>() 70 | // TryJoin::try_join takes a vec of futures which return a 71 | // result, and gives a future which returns a 72 | // result, error> 73 | .try_join() 74 | // Get all of the successful items, or else the first error to 75 | // resolve. 76 | .await 77 | .map_err(anyhow_response)?; 78 | Ok(Json(results)) 79 | } 80 | 81 | /// The query string given to this server contains a city, and optionally a 82 | /// count. 83 | #[derive(Deserialize)] 84 | struct WeatherQuery { 85 | city: String, 86 | #[serde(default = "default_count")] 87 | count: u32, 88 | } 89 | /// When the count is not given in the query string, it defaults to this number 90 | const fn default_count() -> u32 { 91 | 10 92 | } 93 | /// Default WeatherQuery for when none is given. Portland is a good enough location 94 | /// for me, so its good enough for the demo. 95 | impl Default for WeatherQuery { 96 | fn default() -> Self { 97 | WeatherQuery { 98 | city: "Portland".to_string(), 99 | count: default_count(), 100 | } 101 | } 102 | } 103 | 104 | /// Location struct contains the fields we care from the location search. We 105 | /// massage the geolocation API response down to these fields because we dont 106 | /// care about a bunch of its contents. The Serialize allows us to return this 107 | /// value in our server response json. 108 | #[derive(Debug, Serialize)] 109 | struct Location { 110 | name: String, 111 | qualified_name: String, 112 | population: Option, 113 | latitude: f64, 114 | longitude: f64, 115 | } 116 | 117 | /// Fetch the locations corresponding to the query from the open-meteo 118 | /// geocoding API. 119 | async fn fetch_locations(query: &WeatherQuery) -> Result> { 120 | // Utility struct describes the fields we use in the geocoding api's query 121 | // string 122 | #[derive(Serialize)] 123 | struct GeoQuery { 124 | name: String, 125 | count: u32, 126 | language: String, 127 | format: String, 128 | } 129 | // Value of the fields in the query string: 130 | let geo_query = GeoQuery { 131 | name: query.city.clone(), 132 | count: query.count, 133 | language: "en".to_string(), 134 | format: "json".to_string(), 135 | }; 136 | 137 | // Construct the request uri using serde_qs to serialize GeoQuery into a query string. 138 | let uri = Uri::builder() 139 | .scheme("http") 140 | .authority("geocoding-api.open-meteo.com") 141 | .path_and_query(format!( 142 | "/v1/search?{}", 143 | serde_qs::to_string(&geo_query).context("serialize query string")? 144 | )) 145 | .build()?; 146 | // Request is a GET request with no body. User agent is polite to provide. 147 | let request = Request::get(uri) 148 | .header("User-Agent", USER_AGENT) 149 | .body(())?; 150 | 151 | // Make the request 152 | let resp = Client::new() 153 | .send(request) 154 | .await 155 | .context("request to geocoding-api.open-meteo.com") 156 | .context(AxumStatusCode::SERVICE_UNAVAILABLE)?; 157 | // Die with 503 if geocoding api fails for some reason 158 | if resp.status() != StatusCode::OK { 159 | return Err(anyhow!("geocoding-api returned status {:?}", resp.status()) 160 | .context(AxumStatusCode::SERVICE_UNAVAILABLE)); 161 | } 162 | 163 | // Utility structs with Deserialize impls to extract the fields we care 164 | // about from the API's json response. 165 | #[derive(Deserialize)] 166 | struct Contents { 167 | results: Vec, 168 | } 169 | #[derive(Deserialize)] 170 | struct Item { 171 | name: String, 172 | latitude: f64, 173 | longitude: f64, 174 | population: Option, 175 | // There are up to 4 admin region strings provided, only the first one 176 | // seems to be guaranteed to be delivered. If it was my API, I would 177 | // have made a single field `admin` which has a list of strings, but 178 | // its not my API! 179 | admin1: String, 180 | admin2: Option, 181 | admin3: Option, 182 | admin4: Option, 183 | } 184 | impl Item { 185 | /// The API returns a set of "admin" names (for administrative 186 | /// regions), pretty-print them from most specific to least specific: 187 | fn qualified_name(&self) -> String { 188 | let mut n = String::new(); 189 | if let Some(name) = &self.admin4 { 190 | n.push_str(name); 191 | n.push_str(", "); 192 | } 193 | if let Some(name) = &self.admin3 { 194 | n.push_str(name); 195 | n.push_str(", "); 196 | } 197 | if let Some(name) = &self.admin2 { 198 | n.push_str(name); 199 | n.push_str(", "); 200 | } 201 | n.push_str(&self.admin1); 202 | n 203 | } 204 | } 205 | 206 | // Collect the response body and parse the Contents field out of the json: 207 | let contents: Contents = resp 208 | .into_body() 209 | .json() 210 | .await 211 | .context("parsing geocoding-api response")?; 212 | 213 | // Massage the Contents into a Vec. 214 | let mut results = contents 215 | .results 216 | .into_iter() 217 | .map(|item| { 218 | let qualified_name = item.qualified_name(); 219 | Location { 220 | name: item.name, 221 | latitude: item.latitude, 222 | longitude: item.longitude, 223 | population: item.population, 224 | qualified_name, 225 | } 226 | }) 227 | .collect::>(); 228 | // Sort by highest population first. 229 | results.sort_by(|a, b| b.population.partial_cmp(&a.population).unwrap()); 230 | Ok(results) 231 | } 232 | 233 | /// Weather struct contains the items in the weather report we care about: the 234 | /// temperature, how much rain, and the units for each. The Serialize allows 235 | /// us to return this value in our server response json. 236 | #[derive(Debug, Serialize)] 237 | struct Weather { 238 | temp: f64, 239 | temp_unit: String, 240 | rain: f64, 241 | rain_unit: String, 242 | } 243 | 244 | /// Fetch the weather for a given location from the open-meto forecast API. 245 | async fn fetch_weather(location: &Location) -> Result { 246 | // Utility struct for the query string expected by the forecast API 247 | #[derive(Serialize)] 248 | struct ForecastQuery { 249 | latitude: f64, 250 | longitude: f64, 251 | current: String, 252 | } 253 | // Value used for the forecast api query string 254 | let query = ForecastQuery { 255 | latitude: location.latitude, 256 | longitude: location.longitude, 257 | current: "temperature_2m,rain".to_string(), 258 | }; 259 | // Construct the uri to the forecast api, serializing the query string 260 | // with serde_qs. 261 | let uri = Uri::builder() 262 | .scheme("http") 263 | .authority("api.open-meteo.com") 264 | .path_and_query(format!( 265 | "/v1/forecast?{}", 266 | serde_qs::to_string(&query).context("serialize query string")? 267 | )) 268 | .build()?; 269 | // Make the GET request, attaching user-agent, empty body. 270 | let request = Request::get(uri) 271 | .header("User-Agent", USER_AGENT) 272 | .body(())?; 273 | let mut resp = Client::new() 274 | .send(request) 275 | .await 276 | .context("request to api.open-meteo.com") 277 | .context(AxumStatusCode::SERVICE_UNAVAILABLE)?; 278 | 279 | // Bubble up error if forecast api failed 280 | if resp.status() != StatusCode::OK { 281 | return Err(anyhow!("forecast api returned status {:?}", resp.status()) 282 | .context(AxumStatusCode::SERVICE_UNAVAILABLE)); 283 | } 284 | 285 | // Utility structs for extracting fields from the forecast api's json 286 | // response. 287 | #[derive(Deserialize)] 288 | struct Contents { 289 | current_units: Units, 290 | current: Data, 291 | } 292 | #[derive(Deserialize)] 293 | struct Units { 294 | temperature_2m: String, 295 | rain: String, 296 | } 297 | #[derive(Deserialize)] 298 | struct Data { 299 | temperature_2m: f64, 300 | rain: f64, 301 | } 302 | 303 | // Parse the contents of the json response 304 | let contents: Contents = resp.body_mut().json().await?; 305 | // Massage those structs into a single Weather 306 | let weather = Weather { 307 | temp: contents.current.temperature_2m, 308 | temp_unit: contents.current_units.temperature_2m, 309 | rain: contents.current.rain, 310 | rain_unit: contents.current_units.rain, 311 | }; 312 | Ok(weather) 313 | } 314 | 315 | fn anyhow_response(e: anyhow::Error) -> axum::response::ErrorResponse { 316 | let code = e 317 | .downcast_ref::() 318 | .cloned() 319 | .unwrap_or(AxumStatusCode::INTERNAL_SERVER_ERROR); 320 | (code, format!("{e:?}")).into() 321 | } 322 | -------------------------------------------------------------------------------- /LICENSE-Apache-2.0_WITH_LLVM-exception: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | 204 | 205 | --- LLVM Exceptions to the Apache 2.0 License ---- 206 | 207 | As an exception, if, as a result of your compiling your source code, portions 208 | of this Software are embedded into an Object form of such source code, you 209 | may redistribute such embedded portions in such Object form without complying 210 | with the conditions of Sections 4(a), 4(b) and 4(d) of the License. 211 | 212 | In addition, if you combine or link compiled forms of this Software with 213 | software that is licensed under the GPLv2 ("Combined Software") and if a 214 | court of competent jurisdiction determines that the patent provision (Section 215 | 3), the indemnity provision (Section 9) or other Section of the License 216 | conflicts with the conditions of the GPLv2, you may retroactively and 217 | prospectively choose to deem waived or otherwise exclude such Section(s) of 218 | the License, but only in their entirety and only with respect to the Combined 219 | Software. 220 | 221 | --------------------------------------------------------------------------------