Working with mRPC Library

This tutorial describes how to develop user applications with mRPC library.

Overview

mRPC library provides interfaces similar to tonic, which is a Rust implementation of gRPC. mRPC Rust library is built to have first class support of the async ecosystem. Protocol Buffers, which are used by gRPC to specify services and messages, are also applied by mRPC. mRPC library's codegen generates client and server stubs based on the protobuf specs.

Defining the HelloWorld service

The first step is to define a mRPC service with protobuf. Let us first create a new Cargo project for our hello world demo.

cargo new rpc_hello

Then, let's create the .proto file for the server and client to use.

cd rpc_hello
mkdir proto
touch proto/rpc_hello.proto

Now, we define a simple hello world service:

syntax = "proto3";

package rpc_hello;

// The greeting service definition.
service Greeter {
    // Sends a greeting
    rpc SayHello (HelloRequest) returns (HelloReply) {}
}

// The request message containing the user's name.
message HelloRequest {
    bytes name = 1;
}

// The response message containing the greetings
message HelloReply {
    bytes message = 1;
}

Here we defined a Greater service, with SayHello RPC call. SayHello takes a HelloRequest, which contains a series of bytes, and returns a HelloReply of bytes. Now, our .proto file is ready to go.

Setup applications

We setup Cargo.toml to include mRPC library and other required dependencies, and specify a client binary and a server binary to compile.

[package]
name = "rpc_hello"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[build-dependencies]
mrpc-build = { path = "../../mrpc/mrpc-build" }

[dependencies]
mrpc = { path = "../../mrpc" }
prost = { path = "../../3rdparty/prost", features = ["mrpc-frontend"] }
structopt = "0.3.23"
smol = "1.2.5"

[[bin]]
name = "rpc_hello_client"
path = "src/client.rs"

[[bin]]
name = "rpc_hello_server"
path = "src/server.rs"

Generate client and server stubs

Before implementing the client and server, we need to write a build script to invoke mrpc-build to compile client and server stubs. Create a build.rs in rpc_hello/ with the following code:

const PROTO: &str = "proto/rpc_hello.proto";
fn main() -> Result<(), Box<dyn std::error::Error>> {
    println!("cargo:rerun-if-changed={PROTO}");
    mrpc_build::compile_protos(PROTO)?;
    Ok(())
}

Writing the server

Create a file server.rs. We can start by including our hello world proto's messages types and server stub:

#![allow(unused)]
fn main() {
pub mod rpc_hello {
    // The string specified here must match the proto package name
    mrpc::include_proto!("rpc_hello");
}

use rpc_hello::greeter_server::{Greeter, GreeterServer};
use rpc_hello::{HelloReply, HelloRequest};
}

Now, we need to implement the Greeter service we defined. In particular, we need to implement how say_hello should be handled by the server.

#![allow(unused)]
fn main() {
#[mrpc::async_trait]
impl Greeter for MyGreeter {
    async fn say_hello(
    &self,
    request: RRef<HelloRequest>,
        ) -> Result<WRef<HelloReply>, mrpc::Status> {
            eprintln!("request: {:?}", request);

            let message = format!("Hello {}!", String::from_utf8_lossy(&request.name));
            let reply = WRef::new(HelloReply {
                message: message.as_bytes().into(),
            });

            Ok(reply)
        }
    }
}

Here, RRef<HelloRequest> wraps the received HelloRequest on the receive heap of the shared memory with the mRPC service. RRef provides read-only access to the wrapped message. Copy-on-write can be performed if the application wants to modify a received message. WRef, wraps a RPC message to be sent on the send heap of the shared memory.

With Greeter service implemented, we can implement a simple server that runs our Greeter service with smol runtime (which is a small and fast async runtime).

fn main() -> Result<(), Box<dyn std::error::Error>> {
    smol::block_on(async {
        let mut server = mrpc::stub::LocalServer::bind("0.0.0.0:5000")?;
        server
			.add_service(GreeterServer::new(MyGreeter::default()))
			.serve()
			.await?;
        Ok(())
    })
}

The complete code for the server is:

pub mod rpc_hello {
    // The string specified here must match the proto package name
    mrpc::include_proto!("rpc_hello");
}

use rpc_hello::greeter_server::{Greeter, GreeterServer};
use rpc_hello::{HelloReply, HelloRequest};

use mrpc::{RRef, WRef};

#[derive(Debug, Default)]
struct MyGreeter;

#[mrpc::async_trait]
impl Greeter for MyGreeter {
    async fn say_hello(
		&self,
		request: RRef<HelloRequest>,
    ) -> Result<WRef<HelloReply>, mrpc::Status> {
        println!("request: {:?}", request);

        let message = format!("Hello {}!", String::from_utf8_lossy(&request.name));
        let reply = WRef::new(HelloReply {
            message: message.as_bytes().into(),
        });

        Ok(reply)
    }
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    smol::block_on(async {
        let mut server = mrpc::stub::LocalServer::bind("0.0.0.0:5000")?;
        server
			.add_service(GreeterServer::new(MyGreeter::default()))
			.serve()
			.await?;
        Ok(())
    })
}

Writing the client

We can write a client to send a single request to server and get the reply.

pub mod rpc_hello {
    // The string specified here must match the proto package name
    mrpc::include_proto!("rpc_hello");
}

use rpc_hello::greeter_client::GreeterClient;
use rpc_hello::HelloRequest;


fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = GreeterClient::connect("server-addr:5000")?;
    let req = HelloRequest {
        name: "mRPC".into(),
    };
    let reply = smol::block_on(client.say_hello(req))?;
    println!("reply: {}", String::from_utf8_lossy(&reply.message));
    Ok(())
}

With the client stub, we can just directly call client.say_hello(req) to send a request. It will return a Future that we can await on, which resolves to a Result<RRef<HelloReply>, mrpc::Status>. The HelloRequest and HelloReply types in the generated code internally uses our customized Rust collection types, e.g., mrpc::alloc::Vec<u8>, where it provides similar API as its std counterpart but directly allocates buffers on shared memory.

Running the demo

First, start mRPC services on the machines that we will run the client and the server:

cd experimental/mrpc
cat load-mrpc-plugins.toml >> ../../phoenix.toml
cargo make

Note that mRPC service needs to run on both the client side and the server side. After spinning up mRPC services, we can run the client and server applications:

cd experimental/mrpc/examples/rpc_hello
cargo run --release --bin rpc_hello_server
cargo run --release --bin rpc_hello_client