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:
- Rust (opens in a new tab)
- Wasmer (opens in a new tab)
- WASIX
- protoc (opens in a new tab) — the protobuf compiler,
needed by
tonic-prost-build(e.g.apt install protobuf-compilerorbrew install protobuf)
Project Description
We'll implement a unary hello world service with https support, based on the examples from tonic's official repo.
Project Setup
Let's create a new project with cargo.
$ cargo new --bin wasix-grpc
Created binary (application) `wasix-grpc` packageYour 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.rsRather we'd need a server.rs and a client.rs file. So, let's create them.
$ touch src/server.rs src/client.rsBoth 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.protoLet's also add the following dependencies to our Cargo.toml file.
[dependencies]
tonic = { version = "0.14", features = ["tls-ring"] }
tonic-prost = "0.14"
prost = "0.14"
tokio = { version = "1", features = ["full"] }
[build-dependencies]
tonic-prost-build = "0.14"Plain versions from crates.io — no git URLs, no pins, no
[patch.crates-io] section. The WASIX forks of tonic and the stack
underneath it (tokio, hyper, h2, ring, …) resolve automatically
through the WASIX registry.
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_prost_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.send(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` profile [unoptimized + debuginfo] target(s) in 0.14s
Running `target/debug/helloworld-server`
Server listening on 127.0.0.1:50051Now, in another terminal run the client.
$ cargo run --bin helloworld-client
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.15s
Running `target/debug/helloworld-client`
RESPONSE=Response \{ metadata: MetadataMap { headers: {"content-type": "application/grpc", "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 tlsCopy 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- The CA certificate the client trustsserver.pem- Server certificateserver.key- Server private key
tonic's built-in TLS support (the tls-ring feature we enabled earlier)
makes this a small change on both sides. Modify the server.rs main
function to load the certificates:
// src/server.rs
use tonic::transport::{Identity, Server, ServerTlsConfig};
#[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 cert = std::fs::read_to_string(data_dir.join("tls/server.pem"))?;
let key = std::fs::read_to_string(data_dir.join("tls/server.key"))?;
let identity = Identity::from_pem(cert, key);
let addr = "127.0.0.1:50051".parse()?;
println!("Server listening on {}", addr);
Server::builder()
.tls_config(ServerTlsConfig::new().identity(identity))?
.add_service(GreeterServer::new(MyGreeter::default()))
.serve(addr)
.await?;
Ok(())
}The data_dir dance lets the same binary find the certificates both
natively (relative to the crate) and under WASIX (relative to the mounted
working directory).
Now the client: trust our CA and connect over https.
// src/client.rs
use hello_world::greeter_client::GreeterClient;
use hello_world::HelloRequest;
use tonic::transport::{Certificate, Channel, ClientTlsConfig};
pub mod hello_world {
tonic::include_proto!("helloworld");
}
#[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"))
};
// The server's certificate is signed by our own CA, so trust that CA.
let ca = Certificate::from_pem(std::fs::read_to_string(data_dir.join("tls/ca.pem"))?);
let tls = ClientTlsConfig::new()
.ca_certificate(ca)
.domain_name("localhost");
let channel = Channel::from_static("https://127.0.0.1:50051")
.tls_config(tls)?
.connect()
.await?;
let mut client = GreeterClient::new(channel);
let request = tonic::Request::new(HelloRequest {
name: "Alice".into(),
});
let response = client.send(request).await?;
println!("RESPONSE={:?}", response);
Ok(())
}Testing the server and client
In a terminal run the server.
$ cargo run --bin helloworld-server
Running `target/debug/helloworld-server`
Server listening on 127.0.0.1:50051Now, in another terminal run the client.
$ cargo run --bin helloworld-client
Running `target/debug/helloworld-client`
RESPONSE=Response \{ metadata: MetadataMap { headers: {"content-type": "application/grpc", "grpc-status": "0"} }, message: HelloReply { message: "hello Alice" }, extensions: Extensions }Yup, it works.
Compiling to WASIX
$ cargo wasix build --release
Updating `.cargo/config.toml` to resolve crates through the WASIX registry
Compiling tokio v1.47.0+wasix.1
Compiling tonic v0.14.2+wasix.1
...
Finished `release` profile [optimized] target(s)
info: Post-processing WebAssembly files
Optimizing with wasm-opt
Optimizing with wasm-optThat's all it takes: on the first build, cargo wasix writes the WASIX
registry config into
.cargo/config.toml, and the WASIX forks — tonic, tokio, and the TLS
stack underneath — resolve transparently as +wasix.N builds.
Commit the generated .cargo/config.toml so that other checkouts and CI
build through the registry too.
Testing the server and client
The binaries read the certificates from the tls/ directory relative to
their working directory, so mount it into the sandbox by passing --volume
through to the runtime with -W, (see passing runtime
flags).
In a terminal run the server.
$ cargo wasix run --release --bin helloworld-server -W,--net,--volume,./tls:/tls
Server listening on 127.0.0.1:50051Now, in another terminal run the client.
$ cargo wasix run --release --bin helloworld-client -W,--net,--volume,./tls:/tls
RESPONSE=Response \{ metadata: MetadataMap { headers: {"content-type": "application/grpc", "grpc-status": "0"} }, message: HelloReply { message: "hello Alice" }, extensions: {} }gRPC over https, running under WASIX. 🎉
Conclusion
In this tutorial, we learned:
- How to build a gRPC server and client with tonic and compile them with wasix.
- How to enable https with tonic's built-in TLS support.
- How WASIX crate forks resolve automatically through the WASIX registry.
- How to run wasix based
.wasmfiles with Wasmer, including mounting a directory for certificates.