Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Getting Started

This guide will walk you through starting your first modrpc project. It assumes you have already installed Rust - see rustup.rs.

1. Install the modrpc tooling

cargo install modrpcc

2. Create the structure of your modrpc project

mkdir my-modrpc-app
cd my-modrpc-app

cargo new --bin --name my-server server
cargo new --bin --name my-client client

3. Download the modrpc standard library

Currently the modrpc standard library is unversioned. Download the latest proof-of-concept schema straight from the GitHub repo - it will be kept in-sync with the latest std-modrpc on crates.io.

mkdir .modrpc
curl https://raw.githubusercontent.com/modrpc-org/modrpc/refs/heads/main/proto/std.modrpc -o .modrpc/std.modrpc

4. Define your first interface

Populate my-app.modrpc:

import std ".modrpc/std.modrpc"

interface MyApp @(Client, Server) {
    objects {
        compute_fizzbuzz: std.Request<
            ComputeFizzbuzzRequest,
            result<ComputeFizzbuzzSuccess, ComputeFizzbuzzError>,
        > @(Client, Server),
    }
}

struct ComputeFizzbuzzRequest {
    i: u64,
}

struct ComputeFizzbuzzSuccess {
    message: string,
}

enum ComputeFizzbuzzError {
    InvalidRequest,
}

5. Generate the modrpc glue library for your interface

This will generate a Rust crate at my-app-modrpc/rust:

modrpcc --language rust --output-dir . --name my-app my-app.modrpc

6. Implement your server

Move to the server's directory:

cd server

Populate Cargo.toml:

[package]
name = "my-server"
version = "0.1.0"
edition = "2024"

[dependencies]
modrpc = { version = "0.0", features = ["tcp-transport"] }
modrpc-executor = { version = "0.0", features = ["tokio"] }
my-app-modrpc = { path = "../my-app-modrpc/rust" }
std-modrpc = "0.0"
tokio = "1"

Populate src/main.rs:

use modrpc_executor::ModrpcExecutor;

fn main() {
    let mut ex = modrpc_executor::TokioExecutor::new();
    let _guard = ex.tokio_runtime().enter();

    let buffer_pool = modrpc::HeapBufferPool::new(256, 4, 4);
    let rt = modrpc::RuntimeBuilder::new_with_local(ex.spawner());
    let (rt, _rt_shutdown) = rt.start::<modrpc_executor::TokioExecutor>();

    ex.run_until(async move {
        let tcp_server = modrpc::TcpServer::new();
        let listener = tokio::net::TcpListener::bind("0.0.0.0:9090").await
            .expect("tcp listener");

        loop {
            println!("Waiting for client...");
            let (stream, client_addr) = match listener.accept().await {
                Ok(s) => s,
                Err(e) => {
                    println!("Failed to accept client: {}", e);
                    continue;
                }
            };
            stream.set_nodelay(true).unwrap();

            let _ = tcp_server.accept_local::<my_app_modrpc::MyAppServerRole>(
                &rt,
                buffer_pool.clone(),
                stream,
                start_my_app_server,
                my_app_modrpc::MyAppServerConfig { },
                my_app_modrpc::MyAppInitState { },
            )
            .await
            .unwrap();

            println!("Accepted client {}", client_addr);
        }
    });
}

fn start_my_app_server(
    cx: modrpc::RoleWorkerContext<my_app_modrpc::MyAppServerRole>,
) {
    cx.stubs.compute_fizzbuzz.build(cx.setup, async move |_source, request| {
        use my_app_modrpc::{ComputeFizzbuzzError, ComputeFizzbuzzSuccess};

        let Ok(i) = request.i() else {
            return Err(ComputeFizzbuzzError::InvalidRequest);
        };

        println!("Received request: {i}");

        let response = if i % 3 == 0 && i % 5 == 0 {
            ComputeFizzbuzzSuccess { message: "FizzBuzz".to_string() }
        } else if i % 3 == 0 {
            ComputeFizzbuzzSuccess { message: "Fizz".to_string() }
        } else if i % 5 == 0 {
            ComputeFizzbuzzSuccess { message: "Buzz".to_string() }
        } else {
            ComputeFizzbuzzSuccess { message: format!("{i}") }
        };

        println!("  response: {response:?}");

        Ok(response)
    });
}

Build and run the server:

cargo run --release

7. Implement your client

In another terminal session, move to the client's directory:

cd /path/to/my-modrpc-app/client

Populate Cargo.toml:

[package]
name = "my-client"
version = "0.1.0"
edition = "2024"

[dependencies]
modrpc = { version = "0.0", features = ["tcp-transport"] }
modrpc-executor = { version = "0.0", features = ["tokio"] }
my-app-modrpc = { path = "../my-app-modrpc/rust" }
std-modrpc = "0.0"
tokio = "1"

Populate src/main.rs:

use modrpc_executor::ModrpcExecutor;

fn main() {
    let mut ex = modrpc_executor::TokioExecutor::new();
    let _guard = ex.tokio_runtime().enter();

    let buffer_pool = modrpc::HeapBufferPool::new(256, 4, 4);
    let rt = modrpc::RuntimeBuilder::new_with_local(ex.spawner());
    let (rt, _rt_shutdown) = rt.start::<modrpc_executor::TokioExecutor>();

    ex.run_until(async move {
        let stream = tokio::net::TcpStream::connect("127.0.0.1:9090").await
            .expect("tcp stream connect");
        stream.set_nodelay(true).unwrap();

        println!("Connected to server");

        let connection =
            modrpc::tcp_connect::<my_app_modrpc::MyAppClientRole>(
                &rt,
                buffer_pool,
                modrpc::WorkerId::local(),
                my_app_modrpc::MyAppClientConfig { },
                stream,
            )
            .await
            .unwrap();
        let my_app_client = connection.role_handle;

        for i in 1..=15 {
            let response = my_app_client.compute_fizzbuzz.call(
                my_app_modrpc::ComputeFizzbuzzRequest { i }
            )
            .await
            .expect("fizzbuzz failed");

            println!("{}", response.message);
        }
    });
}

Build and run the client:

cargo run --release

The end!