nurmohammed840 / frpc

A high performance Remote Procedure Call system.
Apache License 2.0
17 stars 3 forks source link
rpc

A high performance Remote Procedure Call (RPC) system.

Usage

Add this to your Cargo.toml file.

[dependencies]
frpc = { git = "https://github.com/nurmohammed840/frpc" }
frpc-transport = { git = "https://github.com/nurmohammed840/frpc" }

# Required for codegen
frpc-codegen-client = { git = "https://github.com/nurmohammed840/frpc" }

Example

let's re-implement famous gRPC greeter example in rust!

use frpc::*;

/// The request message containing the user's name.
#[derive(Input)]
struct HelloRequest {
    name: String,
}

/// The response message containing the greetings.
#[derive(Output)]
struct HelloReply {
    message: String,
}

async fn SayHello(req: HelloRequest) -> HelloReply {
    HelloReply {
        message: format!("Hello {}", req.name),
    }
}

async fn SayHelloAgain(req: HelloRequest) -> HelloReply {
    HelloReply {
        message: format!("Hello Again, {}", req.name),
    }
}

declare! {
    /// The greeting service definition.
    service Greeter {
        /// Sends a greeting
        rpc SayHello = 1;

        /// Sends another greeting
        rpc SayHelloAgain = 2;
    }
}

A service is like a namespace. Greeter service has two functions with an unique u16 id. The ID is used to identify which function to call.

Function parameters have to derived from Input and output with Output macro.

Client then call those function as follow:

let greeter = new Greeter(new HttpTransport("<URL>"));
console.log(await greeter.SayHello({ name: "Foo!" })()); // { message: "Hello Foo!" }
console.log(await greeter.SayHelloAgain({ name: "Foo!" })()); // { message: "Hello Again, Foo!" }

You get the typesafe client API for free! Still not impressed ?!

Server Stream Example

In this example server send a stream of messages.

use frpc::*;
use std::time::{Duration, Instant};
use tokio::time::sleep;

#[derive(Output)]
struct Event {
    elapsed: u64,
}

fn get_events(count: u8) -> impl Output {
    sse! {
        if count > 10 {
            return Err(format!("count: {count} should be <= 10"));
        }
        let time = Instant::now();
        for _ in 0..count {
            sleep(Duration::from_secs(1)).await;
            yield Event { elapsed: time.elapsed().as_secs() }
        }
        Ok(())
    }
}

declare! {
    service ServerSentEvents {
        rpc get_events = 1;
    }
}

Here sse! macro create an async generator, impl Output is used omit return type, whereas return type would be: SSE<impl AsyncGenerator<Yield = Event, Return = Result<(), String>>>

The client then calls this function as follow:

let sse = new ServerSentEvents(new HttpTransport("<URL>"));
for await (const ev of sse.get_events(3)()) {
  console.log(ev);
}

// Cancelation example
let task = new AbortController();
setTimeout(() => task.abort(), 5000); // Abort the stream after 5 seconds

for await (const ev of sse.get_events(7)({ signal: task.signal })) {
  console.log(ev);
}

It's that easy!

See more examples

Motivation

The idea behind any RPC system is to communicate locally or remotely without writing any remote interactions.

This is usually done by generation some glue code, also known as Stub, which is responsible for network communication, conversion types and serializing function parameters. Stub usually generatied from Domain Specific Language (DSL), for example gRPC use Protocol Buffers, which describe an interface.

This library doesn't use any Interface Description Language (IDL), and the interface is described from the Rust codebase using macros.