Crate wstd

Source
Expand description

An async standard library for Wasm Components and WASI 0.2

This is a minimal async standard library written exclusively to support Wasm Components. It exists primarily to enable people to write async-based applications in Rust before async-std, smol, or tokio land support for Wasm Components and WASI 0.2. Once those runtimes land support, it is recommended users switch to use those instead.

§Examples

TCP echo server

use wstd::io;
use wstd::iter::AsyncIterator;
use wstd::net::TcpListener;

#[wstd::main]
async fn main() -> io::Result<()> {
    let listener = TcpListener::bind("127.0.0.1:8080").await?;
    println!("Listening on {}", listener.local_addr()?);
    println!("type `nc localhost 8080` to create a TCP client");

    let mut incoming = listener.incoming();
    while let Some(stream) = incoming.next().await {
        let stream = stream?;
        println!("Accepted from: {}", stream.peer_addr()?);
        io::copy(&stream, &stream).await?;
    }
    Ok(())
}

HTTP Client

use std::error::Error;
use wstd::http::{Body, Client, HeaderValue, Request};
use wstd::io::{empty, AsyncRead};

#[wstd::test]
async fn main() -> Result<(), Box<dyn Error>> {
    let request = Request::get("https://postman-echo.com/get")
        .header("my-header", HeaderValue::from_str("my-value")?)
        .body(empty())?;

    let mut response = Client::new().send(request).await?;

    let content_type = response
        .headers()
        .get("Content-Type")
        .ok_or_else(|| "response expected to have Content-Type header")?;
    assert_eq!(content_type, "application/json; charset=utf-8");

    let body = response.body_mut();
    let body_len = body
        .len()
        .ok_or_else(|| "GET postman-echo.com/get is supposed to provide a content-length")?;

    let mut body_buf = Vec::new();
    body.read_to_end(&mut body_buf).await?;

    assert_eq!(
        body_buf.len(),
        body_len,
        "read_to_end length should match content-length"
    );

    let val: serde_json::Value = serde_json::from_slice(&body_buf)?;
    let body_url = val
        .get("url")
        .ok_or_else(|| "body json has url")?
        .as_str()
        .ok_or_else(|| "body json url is str")?;
    assert!(
        body_url.contains("postman-echo.com/get"),
        "expected body url to contain the authority and path, got: {body_url}"
    );

    assert_eq!(
        val.get("headers")
            .ok_or_else(|| "body json has headers")?
            .get("my-header")
            .ok_or_else(|| "headers contains my-header")?
            .as_str()
            .ok_or_else(|| "my-header is a str")?,
        "my-value"
    );

    Ok(())
}

HTTP Server

use wstd::http::body::{BodyForthcoming, IncomingBody, OutgoingBody};
use wstd::http::server::{Finished, Responder};
use wstd::http::{IntoBody, Request, Response, StatusCode};
use wstd::io::{copy, empty, AsyncWrite};
use wstd::time::{Duration, Instant};

#[wstd::http_server]
async fn main(request: Request<IncomingBody>, responder: Responder) -> Finished {
    match request.uri().path_and_query().unwrap().as_str() {
        "/wait" => http_wait(request, responder).await,
        "/echo" => http_echo(request, responder).await,
        "/echo-headers" => http_echo_headers(request, responder).await,
        "/echo-trailers" => http_echo_trailers(request, responder).await,
        "/fail" => http_fail(request, responder).await,
        "/bigfail" => http_bigfail(request, responder).await,
        "/" => http_home(request, responder).await,
        _ => http_not_found(request, responder).await,
    }
}

async fn http_home(_request: Request<IncomingBody>, responder: Responder) -> Finished {
    // To send a single string as the response body, use `Responder::respond`.
    responder
        .respond(Response::new("Hello, wasi:http/proxy world!\n".into_body()))
        .await
}

async fn http_wait(_request: Request<IncomingBody>, responder: Responder) -> Finished {
    // Get the time now
    let now = Instant::now();

    // Sleep for one second.
    wstd::task::sleep(Duration::from_secs(1)).await;

    // Compute how long we slept for.
    let elapsed = Instant::now().duration_since(now).as_millis();

    // To stream data to the response body, use `Responder::start_response`.
    let mut body = responder.start_response(Response::new(BodyForthcoming));
    let result = body
        .write_all(format!("slept for {elapsed} millis\n").as_bytes())
        .await;
    Finished::finish(body, result, None)
}

async fn http_echo(mut request: Request<IncomingBody>, responder: Responder) -> Finished {
    // Stream data from the request body to the response body.
    let mut body = responder.start_response(Response::new(BodyForthcoming));
    let result = copy(request.body_mut(), &mut body).await;
    Finished::finish(body, result, None)
}

async fn http_fail(_request: Request<IncomingBody>, responder: Responder) -> Finished {
    let body = responder.start_response(Response::new(BodyForthcoming));
    Finished::fail(body)
}

async fn http_bigfail(_request: Request<IncomingBody>, responder: Responder) -> Finished {
    async fn write_body(body: &mut OutgoingBody) -> wstd::io::Result<()> {
        for _ in 0..0x10 {
            body.write_all("big big big big\n".as_bytes()).await?;
        }
        body.flush().await?;
        Ok(())
    }

    let mut body = responder.start_response(Response::new(BodyForthcoming));
    let _ = write_body(&mut body).await;
    Finished::fail(body)
}

async fn http_echo_headers(request: Request<IncomingBody>, responder: Responder) -> Finished {
    let mut response = Response::builder();
    *response.headers_mut().unwrap() = request.into_parts().0.headers;
    let response = response.body(empty()).unwrap();
    responder.respond(response).await
}

async fn http_echo_trailers(request: Request<IncomingBody>, responder: Responder) -> Finished {
    let body = responder.start_response(Response::new(BodyForthcoming));
    let (trailers, result) = match request.into_body().finish().await {
        Ok(trailers) => (trailers, Ok(())),
        Err(err) => (Default::default(), Err(std::io::Error::other(err))),
    };
    Finished::finish(body, result, trailers)
}

async fn http_not_found(_request: Request<IncomingBody>, responder: Responder) -> Finished {
    let response = Response::builder()
        .status(StatusCode::NOT_FOUND)
        .body(empty())
        .unwrap();
    responder.respond(response).await
}

§Design Decisions

This library is entirely self-contained. This means that it does not share any traits or types with any other async runtimes. This means we’re trading in some compatibility for ease of maintenance. Because this library is not intended to be maintained in the long term, this seems like the right tradeoff to make.

WASI 0.2 does not yet support multi-threading. For that reason this library does not provide any multi-threaded primitives, and is free to make liberal use of Async Functions in Traits since no Send bounds are required. This makes for a simpler end-user experience, again at the cost of some compatibility. Though ultimately we do believe that using Async Functions is the right foundation for the standard library abstractions - meaning we may be trading in backward-compatibility for forward-compatibility.

This library also supports slightly more interfaces than the stdlib does. For example wstd::rand is a new module that provides access to random bytes. And wstd::runtime provides access to async runtime primitives. These are unique capabilities provided by WASI 0.2, and because this library is specific to that are exposed from here.

Modules§

future
Asynchronous values.
http
HTTP networking support
io
Async IO abstractions.
iter
Composable async iteration.
net
Async network abstractions.
prelude
rand
Random number generation.
runtime
Async event loop support.
task
Types and Traits for working with asynchronous tasks.
time
Async time interfaces.

Attribute Macros§

http_server
Enables a HTTP server main function, for creating HTTP servers.
main
test