Embed HTTP servers in WASM with Rust and CSharp

For a while, some WASM runtimes implement the Socket specification. So, let’s see how to code an HTTP server directly with WASM.

Quick reminder about WASM and WASI

First, WebAssembly (WASM) is a compilation target. Initially, designed for the browser (to augment JavaScript). This binary format is optimized for the size of the produced file and speed execution. A WASM runtime executes the WAM module (the compiled file) in an isolated sandbox (with no access to the resources of the host computer unless it’s explicitly allowed). From a browser perspective, the WASM runtime is the JavaScript VM.

Since 2019, WebAssembly is moving outside the browser. It’s the reason why a new standard was created (the specification is a work in progress): WASI (WebAssembly System Interface). WASI is an API for the WASM runtimes to define how to provide access to the host resources by the WASM modules.

Now runtimes can take several forms:

  • Applications that can load the WASM modules as plugins
  • CLI applications like WasmEdge, WasmTime, or Wasmer, (and the others). These Command line runtimes execute a wasm module regardless of the architecture.
  • Frameworks for building applications that can run WASM modules, like, for example, Spin (from Fermyon), Sat (from Suborbital), Wasm Workers Server (from Wasm Labs @ VMware OCTO), and even my own project Capsule using the Wazero runtime which are small applications servers serving the WASM modules as nano services (or functions) through HTTP.

As I said, the WASI specification is a work in progress. Every WASM runtime implements the most advanced WebAssembly proposal’s.

If there is no official specification for socket networking, WasmEdge and WasmTime already implement their own POSIX sockets.

So, what does this imply?. It means that we can now start to embed web servers inside the WebAssembly modules! (🖐 Disclaimer! It’s not ready for production).

I will show you two examples of WASM HTTP servers. One with WasmEdge and Rust, the other with WasmTime and the dotNet Core WASM support.

Create a WASM HTTP server with Rust and WasmEdge

The following example is inspired by this WasmEdge project.

Requirements

To use it, you need to install the Rust SDK and the WasmEdge Runtime.

Installing the WasmEdge Runtime:

curl -sSf https://raw.githubusercontent.com/WasmEdge/WasmEdge/master/utils/install.sh | bash -s -- -v 0.11.1
source /home/ubuntu/.wasmedge/env # the installer will give you the appropriate path)
# check
wasmedge --version

Installing the Rust SDK (+ wasm support):

curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh -s -- -y
source "$HOME/.cargo/env"
curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
rustup target add wasm32-wasi

Create a Rust project

First, create an http-service directory with two files: Cargo.toml at the root of the project and main.rs in the src subdirectory:

http-service
├── Cargo.toml
├── src
│  └── main.rs

This is the content of the two files:

main.rs

use std::net::SocketAddr;

use hyper::server::conn::Http;
use hyper::service::service_fn;
use hyper::{Body, Method, Request, Response};
use tokio::net::TcpListener;

async fn echo(req: Request<Body>) -> Result<Response<Body>, hyper::Error> {
  match (req.method(), req.uri().path()) {

    (&Method::GET, "/") => Ok(Response::new(Body::from(
          "👋 Hello World 🌍",
    ))),


    (&Method::POST, "/hello") => {
      let name = hyper::body::to_bytes(req.into_body()).await?;
      let name_string = String::from_utf8(name.to_vec()).unwrap();

      let answer = format!("{}{}", "👋 Hello ".to_owned(), name_string);

      Ok(Response::new(Body::from(answer)))
    }

    _ => {
        Ok(Response::new(Body::from("😡 try again")))
    }
  }
}

#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
  let addr = SocketAddr::from(([0, 0, 0, 0], 8080));

  let listener = TcpListener::bind(addr).await?;
  println!("Listening on http://{}", addr);
  loop {
    let (stream, _) = listener.accept().await?;

    tokio::task::spawn(async move {
        if let Err(err) = Http::new().serve_connection(stream, service_fn(echo)).await {
          println!("Error serving connection: {:?}", err);
        }
    });
  }
}

Cargo.toml

[package]
name = "http-service"
version = "0.1.0"
edition = "2021"

[dependencies]
hyper_wasi = { version = "0.15", features = ["full"]}
tokio_wasi = { version = "1.21", features = ["rt", "macros", "net", "time", "io-util"]}

Build and Serve the WASM service

To build the WASM service, type the below command:

cargo build --target wasm32-wasi
# The command will produce the file: target/wasm32-wasi/debug/http-service.wasm

Under Linux, if you get this error when building the project: error: linker cc not found, that means you need to install the build-essential package:

sudo apt-get update
sudo apt install build-essential -y

To start the HTTP server, type the below command:

wasmedge target/wasm32-wasi/debug/http-service.wasm

Then, you can test the WASM service with a curl command:

curl -d 'Bob Morane' -X POST http://127.0.0.1:8080/hello

And, you should get 👋 Hello Bob Morane.

The drawback is that, it seems to work with only WasmEdge; if I try with WasmTime (wich implements the socket networking too), I get an error:

wasmtime target/wasm32-wasi/debug/http-service.wasm --tcplisten localhost:8080

Error: failed to run main module `target/wasm32-wasi/debug/http-service.wasm`

Caused by:
   0: failed to instantiate "target/wasm32-wasi/debug/http-server.wasm"
   1: unknown import: `wasi_snapshot_preview1::sock_setsockopt` has not been defined

WasmEdge and Docker

At KubeCon NA 2022, Docker announced Docker+Wasm technical preview in partnership with WasmEdge. Docker developers can simply build and run a complete Wasm application, thanks to a containerd shim developed in collaboration with Docker and WasmEdge. This shim extracts the Wasm module from the OCI artifact and runs it using the WasmEdge runtime.

Build the image and serve the service

You need to install the Docker+Wasm preview

Create a Dockerfile:

FROM scratch
COPY ./target/wasm32-wasi/debug/http-service.wasm /http-service.wasm
EXPOSE 8080
ENTRYPOINT [ "http-service.wasm" ]

Type the below command:

docker buildx build --platform wasi/wasm32 -t wasmservice .

The size image of the image is under 9MB.

To run the service with Docker, type the below command:

docker run -dp 8080:8080 --name=wasmservice --runtime=io.containerd.wasmedge.v1 --platform=wasi/wasm32 wasmservice

You can choose the runtime with the --runtime option, but right now, the option exits only for the WasmEdge runtime.

And now you can call the service like this: curl -d 'Bob Morane' -X POST http://127.0.0.1:8080/hello, and of course you'll get 👋 Hello Bob Morane.

I said that WasmTime implements the socket API, so let’s see how to use it with the WASM support of the dotNet framework.

Create a WASM HTTP server with ASP.Net, CSharp and WasmTime

I didn’t find clear and understandable (for me) documentation about the socket support with WasmTime, but the Mio project provides an example of a WASM TCP server runnable with WasmTime.

However, when I was at WasmDay at Kubecon Valencia in May 2022, I attended to an awesome presentation by Steve Sanderson from Microsoft: "Bringing WebAssembly to the .NET Mainstream". Steve was running an embedded ASP.Net HTTP server in a wasm module with WasmTime.

So, let’s try to reproduce this experiment.

Requirements

Installing the WasmTime Runtime:

curl https://wasmtime.dev/install.sh -sSf | bash
# check
wasmtime --version

Installing the dotNet Core Preview SDK (+ wasm support):

wget https://download.visualstudio.microsoft.com/download/pr/f5c74056-330b-452b-915e-d98fda75024e/18076ca3b89cd362162bbd0cbf9b2ca5/dotnet-sdk-7.0.100-rc.2.22477.23-linux-x64.tar.gz
mkdir -p $HOME/dotnet && tar zxf dotnet-sdk-7.0.100-rc.2.22477.23-linux-x64.tar.gz -C $HOME/dotnet
rm dotnet-sdk-7.0.100-rc.2.22477.23-linux-x64.tar.gz

export DOTNET_ROOT=$HOME/dotnet
export PATH=$PATH:$HOME/dotnet
dotnet workload install wasm-tools

To get the last version of the dotNet Core Preview SDK, visit this page https://dotnet.microsoft.com/en-us/download/dotnet/7.0

Create an ASP.Net project

The below commands will generate an ASP.Net project and add the WASI support to the project:

dotnet new web -o hello
cd hello
dotnet add package Wasi.Sdk --prerelease
dotnet add package Wasi.AspNetCore.Server.Native --prerelease

Change the code of Program.cs

Change the code of hello/Program.cs by the following one:

using System.Runtime.InteropServices;

var builder = WebApplication.CreateBuilder(args).UseWasiConnectionListener();

var app = builder.Build();

app.MapGet("/", () => {
  return $"👋 Hello, World! 🌍 🖥️: {RuntimeInformation.OSArchitecture} ⏳: {DateTime.UtcNow.ToLongTimeString()} (UTC)";
});

app.Run();

Build and serve the service

Type the below commands to build the service:

cd hello
dotnet build

Start the service with the below commands:

cd hello
wasmtime bin/Debug/net7.0/hello.wasm --tcplisten localhost:8080

Call the service: curl http://localhost:8080 and you'll get something like this: 👋 Hello, World! 🌍 🖥️: Wasm ⏳: 14:09:12 (UTC). My only regret is that does not yet run with the Docker+Wasm preview.

I tried with WasmEdge: wasmedge bin/Debug/net7.0/hello.wasm, but unsurprisingly it didn't work:

[2022-11-06 14:11:54.358] [error] instantiation failed: incompatible import type, Code: 0x61
[2022-11-06 14:11:54.358] [error]     Mismatched function type. Expected: FuncType {params{i32 , i32 , i32} returns{i32}} , Got: FuncType {params{i32 , i32} returns{i32}}
[2022-11-06 14:11:54.358] [error]     When linking module: "wasi_snapshot_preview1" , function name: "sock_accept"
[2022-11-06 14:11:54.358] [error]     At AST node: import description
[2022-11-06 14:11:54.358] [error]     At AST node: import section
[2022-11-06 14:11:54.358] [error]     At AST node: module

But, it should work: https://twitter.com/juntao/status/1589323762901843971?s=20&t=fvjV0JSBXZgn0Px5f22IlA - stay tuned, I created an issue at the WasmEdge project.

It's only a start, but the WASI specification seems to be moving in the right direction (even if the implementations sometimes diverge). It is already possible to offer "nano services" with no dependency other than the runtime. And I find dotNet Core's WASI support particularly impressive (and even more ASP.Net).

You can find the source code of the examples at https://github.com/wasm-university/wasm-hosted-web-servers. This is a Gitpod, so you can test the samples without installing anything. Open this URL with your browser: https://gitpod.io/#https://github.com/wasm-university/wasm-hosted-web-servers.

Follow Kubesimplify on Hashnode, Twitter and Linkedin. Join our Discord server to learn with us.

Did you find this article valuable?

Support Kubesimplify by becoming a sponsor. Any amount is appreciated!