Crate wstd

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()?);
        wstd::runtime::spawn(async move {
            // If echo copy fails, we can ignore it.
            let _ = io::copy(&stream, &stream).await;
        })
        .detach();
    }
    Ok(())
}

HTTP Client

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

#[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(Body::empty())?;

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

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

    let mut body = response.into_body();
    let body_len = body
        .content_length()
        .ok_or("GET postman-echo.com/get is supposed to provide a content-length")?;

    let contents = body.contents().await?;

    assert_eq!(
        contents.len() as u64,
        body_len,
        "contents length should match content-length"
    );

    let val: serde_json::Value = serde_json::from_slice(contents)?;
    let body_url = val
        .get("url")
        .ok_or("body json has url")?
        .as_str()
        .ok_or("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("body json has headers")?
            .get("my-header")
            .ok_or("headers contains my-header")?
            .as_str()
            .ok_or("my-header is a str")?,
        "my-value"
    );

    Ok(())
}

HTTP Server

use anyhow::{Context, Result};
use futures_lite::stream::{once_future, unfold};
use http_body_util::{BodyExt, StreamBody};
use std::convert::Infallible;
use wstd::http::body::{Body, Bytes, Frame};
use wstd::http::{Error, HeaderMap, Request, Response, StatusCode};
use wstd::time::{Duration, Instant};

#[wstd::http_server]
async fn main(request: Request<Body>) -> Result<Response<Body>, Error> {
    let path = request.uri().path_and_query().unwrap().as_str();
    println!("serving {path}");
    match path {
        "/" => http_home(request).await,
        "/wait-response" => http_wait_response(request).await,
        "/wait-body" => http_wait_body(request).await,
        "/stream-body" => http_stream_body(request).await,
        "/echo" => http_echo(request).await,
        "/echo-headers" => http_echo_headers(request).await,
        "/echo-trailers" => http_echo_trailers(request).await,
        "/response-status" => http_response_status(request).await,
        "/response-fail" => http_response_fail(request).await,
        "/response-body-fail" => http_body_fail(request).await,
        _ => http_not_found(request).await,
    }
}

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

async fn http_wait_response(_request: Request<Body>) -> Result<Response<Body>> {
    // 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();

    Ok(Response::new(
        format!("slept for {elapsed} millis\n").into(),
    ))
}

async fn http_wait_body(_request: Request<Body>) -> Result<Response<Body>> {
    // Get the time now
    let now = Instant::now();

    let body = async move {
        // 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();
        Ok::<_, Infallible>(Bytes::from(format!("slept for {elapsed} millis\n")))
    };

    Ok(Response::new(Body::from_try_stream(once_future(body))))
}

async fn http_stream_body(_request: Request<Body>) -> Result<Response<Body>> {
    // Get the time now
    let start = Instant::now();

    let body = move |iters: usize| async move {
        if iters == 0 {
            return None;
        }
        // Sleep for 0.1 second.
        wstd::task::sleep(Duration::from_millis(100)).await;

        // Compute how long we slept for.
        let elapsed = Instant::now().duration_since(start).as_millis();
        Some((
            Ok::<_, Infallible>(Bytes::from(format!(
                "stream started {elapsed} millis ago\n"
            ))),
            iters - 1,
        ))
    };

    Ok(Response::new(Body::from_try_stream(unfold(5, body))))
}

async fn http_echo(request: Request<Body>) -> Result<Response<Body>> {
    let (_parts, body) = request.into_parts();
    Ok(Response::new(body))
}

async fn http_echo_headers(request: Request<Body>) -> Result<Response<Body>> {
    let mut response = Response::builder();
    *response.headers_mut().unwrap() = request.into_parts().0.headers;
    Ok(response.body("".to_owned().into())?)
}

async fn http_echo_trailers(request: Request<Body>) -> Result<Response<Body>> {
    let collected = request.into_body().into_boxed_body().collect().await?;
    let trailers = collected.trailers().cloned().unwrap_or_else(|| {
        let mut trailers = HeaderMap::new();
        trailers.insert("x-no-trailers", "1".parse().unwrap());
        trailers
    });

    let body = StreamBody::new(once_future(async move {
        anyhow::Ok(Frame::<Bytes>::trailers(trailers))
    }));
    Ok(Response::new(Body::from_http_body(body)))
}

async fn http_response_status(request: Request<Body>) -> Result<Response<Body>> {
    let status = if let Some(header_val) = request.headers().get("x-response-status") {
        header_val
            .to_str()
            .context("contents of x-response-status")?
            .parse::<u16>()
            .context("u16 value from x-response-status")?
    } else {
        500
    };
    let mut response = Response::builder().status(status);
    if status == 302 {
        response = response.header("Location", "http://localhost/response-status");
    }
    Ok(response.body(String::new().into())?)
}

async fn http_response_fail(_request: Request<Body>) -> Result<Response<Body>> {
    Err(anyhow::anyhow!("error creating response"))
}

async fn http_body_fail(_request: Request<Body>) -> Result<Response<Body>> {
    let body = StreamBody::new(once_future(async move {
        Err::<Frame<Bytes>, _>(anyhow::anyhow!("error creating body"))
    }));

    Ok(Response::new(Body::from_http_body(body)))
}

async fn http_not_found(_request: Request<Body>) -> Result<Response<Body>> {
    let response = Response::builder()
        .status(StatusCode::NOT_FOUND)
        .body(Body::empty())
        .unwrap();
    Ok(response)
}

§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