Documentation
Language Guide
Rust
Tutorials
WASIX with gRPC

WASIX with gRPC

Introduction

This is a tutorial on how to use gRPC with https in WASIX. We will use the tonic (opens in a new tab) crate to implement a simple gRPC server and client in Rust.

โ—๏ธ

This tutorial does not focus on instantiating gRPC with rust but is rather focused on getting WASIX compatability in gRPC with tonic.

Prerequisites

โš ๏ธ

Please check that you have the latest version of wasmer runtime as this tutorial depends on version 4.1.1 or higher.

The project requires the following tools to be installed on your system:

Project Description

Weโ€™ll be following tls_rustls (opens in a new tab) example from tonicโ€™s official repo. This example implements a unary hello world example with https support.

Project Setup

Let's create a new project with cargo.

$ cargo new --bin wasix-grpc
    Created binary (application) `wasix-grpc` package

Your wasix-grpc directory structure should look like this:

As we'll place both server and the client in the same project. We won't be needing the main.rs file. So, let's delete it.

$ rm src/main.rs

Rather we'd need a server.rs and a client.rs file. So, let's create them.

$ touch src/server.rs src/client.rs

Both of these will be our entry points for the server and the client respectively. We'll also need a proto file to define our service. So, let's create a proto directory and a hello.proto file inside it.

$ mkdir proto
$ touch proto/helloworld.proto

Let's also add the following dependencies to our Cargo.toml file.

[dependencies]
tonic = { version = "0.9", features = ["tls"] }
prost = "0.11"
tokio = { version = "=1.24.2", default-features = false, features = [
    "full",
] }
 
tokio-stream = "0.1.14"
 
hyper-rustls = { version="0.24.1", features = [
    "http2",
] }
 
tokio-rustls = "0.24.1"
hyper = "0.14.27"
tower = "0.4.13"
http-body = "0.4.5"
tower-http = "0.4.3"
rustls-pemfile = "1.0.3"
rustls-native-certs = "0.6.3"
 
[build-dependencies]
tonic-build = "0.9"
โ„น๏ธ

The build-dependencies section is required to compile the proto file.

We need to also define our entry points in the Cargo.toml file.

[[bin]] # Bin to run the HelloWorld gRPC server
name = "helloworld-server"
path = "src/server.rs"
 
[[bin]] # Bin to run the HelloWorld gRPC client
name = "helloworld-client"
path = "src/client.rs"

While we're at it let's also add build.rs for compiling the proto file.

$ touch build.rs
// build.rs
fn main() -> Result<(), Box<dyn std::error::Error>> {
    tonic_build::compile_protos("proto/helloworld.proto")?;
    Ok(())
}

Okay, we're all set to start writing our proto file and then our server & client.

But first let's see our directory structure.

Proto file

Let's define our service in the proto/helloworld.proto file.

// proto/helloworld.proto
 
syntax = "proto3";
package helloworld;
 
service Greeter {
    rpc Send(HelloRequest) returns (HelloReply) {}
 
}
 
message HelloRequest {
   string name = 1;
}
 
message HelloReply {
    string message = 1;
}

Wiring up client and server

Let's start with the server.

// src/server.rs
use tonic::{transport::Server, Request, Response, Status};
 
use hello_world::greeter_server::{Greeter, GreeterServer};
use hello_world::{HelloReply, HelloRequest};
 
pub mod hello_world {
    tonic::include_proto!("helloworld"); // The string specified here must match the proto package name
}
 
#[derive(Debug, Default)]
pub struct MyGreeter {}
 
#[tonic::async_trait]
impl Greeter for MyGreeter {
    async fn send(
        &self,
        request: Request<HelloRequest>,
    ) -> Result<Response<HelloReply>, Status> {
        println!("Got a request: {:?}", request);
 
        let reply = HelloReply {
            message: format!("Hello {}!", request.into_inner().name),
        };
 
        Ok(Response::new(reply))
    }
}
 
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let addr = "127.0.0.1:50051".parse()?;
    let greeter = MyGreeter::default();
 
    println!("Server listening on {}", addr);
 
    Server::builder()
        .add_service(GreeterServer::new(greeter))
        .serve(addr)
        .await?;
 
    Ok(())
}

Now, let's write our client.

// src/client.rs
use hello_world::greeter_client::GreeterClient;
use hello_world::HelloRequest;
 
pub mod hello_world {
    tonic::include_proto!("helloworld");
}
 
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut client = GreeterClient::connect("http://127.0.0.1:50051").await?;
 
    let request = tonic::Request::new(HelloRequest {
        name: "Tonic".into(),
    });
 
    let response = client.say_hello(request).await?;
 
    println!("RESPONSE={:?}", response);
 
    Ok(())
}

Testing the server and client

Let's test our server and client.

$ cargo run --bin helloworld-server
    Finished dev [unoptimized + debuginfo] target(s) in 0.14s
     Running `target/debug/helloworld-server`
Server listening on 127.0.0.1:50051

Now, in another terminal run the client.

$ cargo run --bin helloworld-client
    Finished dev [unoptimized + debuginfo] target(s) in 0.15s
     Running `target/debug/helloworld-client`
RESPONSE=Response \{ metadata: MetadataMap { headers: {"content-type": "application/grpc", "date": "Mon, 28 Aug 2023 12:54:16 GMT", "grpc-status": "0"} }, message: HelloReply { message: "Hello Tonic!" }, extensions: Extensions }

Adding in https support

For adding https support we need server and client certificates. You can get them from tonic's repo (opens in a new tab) or you can generate them yourself.

Let's place the certificates in a tls directory.

$ mkdir tls

Copy the ca.pem, server.pem and server.key files from tonic's repo (opens in a new tab) to the tls directory.

โ„น๏ธ

Certificates Infomation:

  • ca.pem - Client Certificate
  • server.pem - Server certificate
  • server.key - Server private key

Modify the server.rs file to use the certificates.

Note: I'm just copy-pasting most of the code from the tonic repo's tls_rustls (opens in a new tab) example and modifying it according to our proto file.

use hyper::server::conn::Http;
use tokio_rustls::rustls::{OwnedTrustAnchor, RootCertStore, ServerConfig};
use tonic::{transport::Server, Request, Response, Status};
 
use hello_world::greeter_server::Greeter;
use hello_world::{HelloReply, HelloRequest};
 
use tokio::sync::mpsc;
use tokio_stream::{wrappers::ReceiverStream, Stream, StreamExt};
 
use std::error::Error;
use std::io::ErrorKind;
use std::pin::Pin;
use std::time::Duration;
 
use std::sync::Arc;
use tokio::net::TcpListener;
use tokio_rustls::{
    rustls::{Certificate, PrivateKey},
    TlsAcceptor,
};
use tower_http::ServiceBuilderExt;
 
pub mod hello_world {
    tonic::include_proto!("helloworld"); // The string specified here must match the proto package name
}
 
#[derive(Debug, Default)]
pub struct MyGreeter {}
 
#[tonic::async_trait]
impl Greeter for MyGreeter {
    async fn send(&self, request: Request<HelloRequest>) -> Result<Response<HelloReply>, Status> {
        Ok(Response::new(HelloReply {
            message: format!("hello {}", request.get_ref().name),
        }))
    }
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
     let data_dir = if cfg!(target_os = "wasi") {
        std::env::current_dir()?
    } else {
        std::path::PathBuf::from(std::env!("CARGO_MANIFEST_DIR"))
    };
    let certs = {
        let fd = std::fs::File::open(data_dir.join("tls/server.pem"))?;
        let mut buf = std::io::BufReader::new(&fd);
        rustls_pemfile::certs(&mut buf)?
            .into_iter()
            .map(Certificate)
            .collect()
    };
    let key = {
        let fd = std::fs::File::open(data_dir.join("tls/server.key"))?;
        let mut buf = std::io::BufReader::new(&fd);
        rustls_pemfile::pkcs8_private_keys(&mut buf)?
            .into_iter()
            .map(PrivateKey)
            .next()
            .unwrap()
    };
 
    let mut tls = ServerConfig::builder()
        .with_safe_defaults()
        .with_no_client_auth()
        .with_single_cert(certs, key)?;
    tls.alpn_protocols = vec![b"h2".to_vec()];
 
    let server = MyGreeter::default();
 
    let svc = Server::builder()
        .add_service(hello_world::greeter_server::GreeterServer::new(server))
        .into_service();
 
    let mut http = Http::new();
    http.http2_only(true);
 
    let listener = TcpListener::bind("127.0.0.1:50051").await?;
    let tls_acceptor = TlsAcceptor::from(Arc::new(tls));
 
    println!("Server listening on {}", listener.local_addr()?);
 
    loop {
        let (conn, addr) = match listener.accept().await {
            Ok(incoming) => incoming,
            Err(e) => {
                eprintln!("Error accepting connection: {}", e);
                continue;
            }
        };
 
        let http = http.clone();
        let tls_acceptor = tls_acceptor.clone();
        let svc = svc.clone();
 
        tokio::spawn(async move {
            let mut certificates = Vec::new();
 
            let conn = tls_acceptor
                .accept_with(conn, |info| {
                    if let Some(certs) = info.peer_certificates() {
                        for cert in certs {
                            certificates.push(cert.clone());
                        }
                    }
                })
                .await
                .unwrap();
 
            let svc = tower::ServiceBuilder::new()
                .add_extension(Arc::new(ConnInfo { addr, certificates }))
                .service(svc);
 
 
            http.serve_connection(conn, svc)
                .await
                .map_err(|e| {
                    eprintln!("Error serving connection: {}", e);
                    e
                })
                .unwrap();
        });
    }
}
 
#[derive(Debug)]
struct ConnInfo {
    addr: std::net::SocketAddr,
    certificates: Vec<Certificate>,
}

Now the client.

 
use std::{sync::Arc, time::Duration};
 
use hello_world::greeter_client::GreeterClient;
use hello_world::HelloRequest;
 
use http_body::{combinators::UnsyncBoxBody, Body};
use hyper::{body::Bytes, client::HttpConnector, Client, Request, Uri};
use hyper_rustls::HttpsConnector;
use tokio_rustls::rustls::{ClientConfig, ConfigBuilder, OwnedTrustAnchor, RootCertStore};
use tokio_stream::{Stream, StreamExt};
use tonic::{
    body::BoxBody,
    client::GrpcService,
    transport::{Channel, ClientTlsConfig},
    Status,
};
use tower::{util::MapRequest, ServiceExt};
 
pub mod hello_world {
    tonic::include_proto!("helloworld");
}
 
async fn say_hello<T>(client: &mut GreeterClient<T>)
where
    T: tonic::client::GrpcService<tonic::body::BoxBody> + Send + 'static,
    T::ResponseBody: Body<Data = Bytes> + Send + 'static,
    <T::ResponseBody as Body>::Error: Into<Box<dyn std::error::Error + Send + Sync>> + Send,
{
    let request = tonic::Request::new(HelloRequest {
        name: "Alice".into(),
    });
 
    let response = client.send(request).await.unwrap();
 
    println!("RESPONSE={:?}", response);
}
 
 
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
     let data_dir = if cfg!(target_os = "wasi") {
        std::env::current_dir()?
    } else {
        std::path::PathBuf::from(std::env!("CARGO_MANIFEST_DIR"))
    };
    let fd = std::fs::File::open(data_dir.join("tls/ca.pem"))?;
 
    let mut roots = RootCertStore::empty();
 
    let mut buf = std::io::BufReader::new(&fd);
    let certs = rustls_pemfile::certs(&mut buf)?;
    roots.add_parsable_certificates(&certs);
 
    let tls = ClientConfig::builder()
        .with_safe_defaults()
        .with_root_certificates(roots)
        .with_no_client_auth();
 
    let mut http = HttpConnector::new();
    http.enforce_http(false);
 
    let connector = tower::ServiceBuilder::new()
        .layer_fn(move |s| {
            let tls = tls.clone();
 
            hyper_rustls::HttpsConnectorBuilder::new()
                .with_tls_config(tls)
                .https_or_http()
                .enable_http2()
                .wrap_connector(s)
        })
        .map_request(|_: Uri| Uri::from_static("https://127.0.0.1:50051"))
        .service(http);
 
    let client = hyper::Client::builder().build(connector);
 
    // Using `with_origin` will let the codegenerated client set the `scheme` and
    // `authority` from the provided `Uri`.
    let uri = Uri::from_static("https://example.com");
 
    let mut client = GreeterClient::with_origin(client, uri);
 
    say_hello(&mut client).await;
 
    Ok(())
}

Testing the server and client

In a terminal run the server.

$ cargo run --bin helloworld-server
    Finished dev [unoptimized + debuginfo] target(s) in 1.77s
     Running `target/debug/helloworld-server`
Server listening on 127.0.0.1:50051

Now, in another terminal run the client.

$ cargo run --bin helloworld-client
    Finished dev [unoptimized + debuginfo] target(s) in 2.55s
     Running `target/debug/helloworld-client`
RESPONSE=Response \{ metadata: MetadataMap { headers: {"content-type": "application/grpc", "date": "Mon, 28 Aug 2023 13:29:27 GMT", "grpc-status": "0"} }, message: HelloReply { message: "hello Alice" }, extensions: Extensions }

Yup, it works.

WASIX Compatability

For compiling our project to wasix, we need to pin and patch some dependencies in our Cargo.toml file.

[dependencies]
tonic = { version = "0.9", features = ["tls"] }
prost = "0.11"
tokio = { version = "=1.24.2", git = "https://github.com/wasix-org/tokio.git", branch = "epoll", default-features = false, features = [
    "full",
] } # ๐Ÿ‘ˆ๐Ÿผ pinned to wasix fork
libc = { version = "0.2.139", git = "https://github.com/wasix-org/libc.git", branch = "master" } # ๐Ÿ‘ˆ๐Ÿผ pinned to wasix fork
tokio-stream = "0.1.14"
hyper-rustls = { git = "https://github.com/wasix-org/hyper-rustls.git", branch = "main", features = [
    "http2",
] } # ๐Ÿ‘ˆ๐Ÿผ pinned to wasix fork
tokio-rustls = { version = "0.24.1", git = "https://github.com/wasix-org/tokio-rustls.git", branch = "main" } # ๐Ÿ‘ˆ๐Ÿผ pinned to wasix fork
hyper = { git = "https://github.com/wasix-org/hyper.git", branch = "v0.14.27" } # ๐Ÿ‘ˆ๐Ÿผ pinned to wasix fork
tower = "0.4.13"
http-body = "0.4.5"
tower-http = { version = "0.4.3", features = ["util", "add-extension"] }
rustls-pemfile = "1.0.3"
rustls-native-certs = { git = "https://github.com/wasix-org/rustls-native-certs.git" } # ๐Ÿ‘ˆ๐Ÿผ pinned to wasix fork
 
[build-dependencies]
tonic-build = "0.9"
 
[patch.crates-io]
rustls-native-certs = { git = "https://github.com/wasix-org/rustls-native-certs.git" } # ๐Ÿ‘ˆ๐Ÿผ patched to wasix fork
socket2 = { git = "https://github.com/wasix-org/socket2.git", branch = "v0.4.9" } # ๐Ÿ‘ˆ๐Ÿผ patched to wasix fork
tokio = { git = "https://github.com/wasix-org/tokio.git", branch = "epoll" } # ๐Ÿ‘ˆ๐Ÿผ patched to wasix fork
hyper = { git = "https://github.com/wasix-org/hyper.git", branch = "v0.14.27" } # ๐Ÿ‘ˆ๐Ÿผ patched to wasix fork
ring = { git = "https://github.com/wasix-org/ring.git", branch = "wasix" } # ๐Ÿ‘ˆ๐Ÿผ patched to wasix fork
rustls = { git = "https://github.com/wasix-org/rustls.git", branch = "v0.21.5" } # ๐Ÿ‘ˆ๐Ÿผ patched to wasix fork

For a list of all of our forks, you can checkout the patched repos page.

Compiling the project to WASIX

$ cargo wasix build --release
...
warning: `wasix-grpc` (bin "helloworld-server") generated 2 warnings (run `cargo fix --bin "helloworld-server"` to apply 1 suggestion)
warning: `wasix-grpc` (bin "helloworld-client") generated 6 warnings (run `cargo fix --bin "helloworld-client"` to apply 3 suggestions)
    Finished release [optimized] target(s) in 26.41s
info: Post-processing WebAssembly files
  Optimizing with wasm-opt
  Optimizing with wasm-opt

Testing the server and client

โ„น๏ธ

In the client and server code you'll find this snippet.

let data_dir = if cfg!(target_os = "wasi") {
    std::env::current_dir()?
} else {
    std::path::PathBuf::from(std::env!("CARGO_MANIFEST_DIR"))
};

This code maps the data_dir to current directory if the target_os is wasi. This is important because CARGO_MANIFEST_DIR is not available in WASIX.

Running the server
$ wasmer run target/wasm32-wasmer-wasi/release/helloworld-server.wasm --net --mapdir /tls:./tls
Server listening on 127.0.0.1:50051
Running the client
$ wasmer run target/wasm32-wasmer-wasi/release/helloworld-client.wasm --net --mapdir /tls:./tls
RESPONSE=Response \{ metadata: MetadataMap { headers: {"content-type": "application/grpc", "date": "Mon, 28 Aug 2023 15:01:09 GMT", "grpc-status": "0"} }, message: HelloReply { message: "hello Alice" }, extensions: Extensions }

Yup, works the same.

Flags Information
  • --net - enables networking support
  • --mapdir /tls:./tls - maps the /tls directory in the WASIX filesystem to the ./tls directory in the host filesystem.

To know more about the flags run wasmer run --help.

๐Ÿคธ๐Ÿผ Exercise Time

  • Try to implement a server streaming example with WASIX.
  • Try to implement a client streaming example with WASIX.
  • Try to implement a bidirectional streaming example with WASIX.

Conclusion

In this tutorial we learned:

  • how to use gRPC with https in WASIX. We used the tonic (opens in a new tab) crate to implement a simple gRPC server and client in Rust
  • how to compile the project to WASIX
  • how to run the gRPC server and client in WASIX
  • how to use the --net and --mapdir flags in Wasmer to enable networking support and map directories respectively.

For the full example code, you can checkout the repository below.

wasix-rust-examples/wasix-grpc