Define Service

There are two ways (but three attribute macros) to help you define the the RPC service.

  • The attribute macro #[export_impl] can be used when the implementation and the service definition are located in the same file/project. The name of the struct will be used as the default service name with #[export_impl] in a case sensitive manner.
  • The attribute macros #[export_trait] and #[export_trait_impl] should be used when an abstract service definition (a trait) will be shared among different projects. The name of the trait will be used as the default service name with #[export_trait] in a case sensitive manner.

Both #[export_impl] and #[export_trait] will generate the client stub traits/methods when the "client" feature flag is enabled and a runtime feature flag is enabled on toy-rpc.

Inside the impl block or trait definition block, you should then use the attribute #[export_method] to mark which method(s) should be "exported" as RPC method(s). The methods to export must meet the following criteria on the server side

  • the method resides in an impl block marked with #[export_impl] or #[export_trait]
  • the method is marked with #[export_method] attribute
  • the method takes one argument other than &self and returns a Result<T, E>
    • the argument must implement trait serde::Deserialize
    • the Ok type T of the result must implement trait serde::Serialize
    • the Err type E of the result must implement trait ToString

The method is essentially in the form

#[export_method]
async fn method_name(&self, args: Req) -> Result<Res, ErrorMsg>
where
    Req: serde::Deserialize,
    Res: serde::Serialize,
    ErrorMsg: ToString,
{
    // ...
}

Example Usage

Use the following dependencies to work with the examples below

[dependencies]
async-trait = "0.1.50"
toy-rpc = "0.7.5"

#[export_impl]

When you have both the service definition and the implementation in the same file, you can use #[export_impl] on the impl block. This will also use the name of the struct as the default service name.

File structure

./src
├── /bin
│   ├── server.rs
│   ├── client.rs
└── lib.rs

Suppose that both the service definitions and implementations are placed in src/lib.rs.

// src/lib.rs
use toy_rpc::macros::export_impl;

pub struct Foo { }

#[export_impl] // The default service name will be "Foo"
impl Foo {
    // use attribute `#[export_method]` to mark which method to "export"
    #[export_method]
    async fn exported_method(&self, args: ()) -> Result<String, String> {
        Ok("exported method".into())
    }

    async fn not_exported_method(&self, args: ()) -> Result<String, String> {
        Ok("not exported method".into())
    }
}

You may also define a separate trait in src/lib.rs which is implemented by some struct in the same file.

// continuing in src/lib.rs

use async_trait::async_trait;

#[async_trait]
pub trait Arith {
    async fn add(&self, args: (i32, i32)) -> Result<i32, String>;
    async fn subtract(&self, args: (i32, i32)) -> Result<i32, String>;
}

pub struct Bar { }
// implement the Arith trait for `Bar { }`and then mark the implementation as "exported" for RPC

#[async_trait]
// Place `#[export_impl] ` after `#[async_trait]`
#[export_impl] // The default service name will be "Bar"
impl Arith for Bar {
    // Only mark `add(...)` as RPC method
    #[export_method]
    async fn add(&self, args: (i32, i32)) -> Result<i32, String> {
        Ok(args.0 + args.1)
    }

    // `subtract(...)` will not be accessible from RPC calls
    async fn subtract(&self, args: (i32, i32)) -> Result<i32, String> {
        Ok(args.0 - args.1)
    }
}

We will continue to use this example in the Server and Client chapters.

#[export_trait] and #[export_trait_impl]

When you want the abstract service definition to be shared but without concreate implementations, you should use #[export_trait] on the trait definition and #[export_trait_impl] on the concrete trait implementation. Please note that the default service name hence will be the name of the trait NOT that of the struct.

Suppose we will have three separate crates

  • "example-service" as the service definition,
  • "example-server" acting as the server,
  • and "example-client" acting as the client

which can also be found in the GitHub examples (service, server, client).

We are going to define the RPC service just as a trait in "example-service".

// example-service/src/lib.rs
use async_trait::async_trait;
use toy_rpc::macros::export_trait;

#[async_trait]
#[export_trait] // The default service name will be "Arith"
pub trait Arith {
    // let's mark both `add(...)` and `subtract(...)` as RPC method
    #[export_method]
    async fn add(&self, args: (i32, i32)) -> Result<i32, String>;

    #[export_method]
    async fn subtract(&self, args: (i32, i32)) -> Result<i32, String>;

    // some method that we don't want to export
    fn say_hi(&self);
}

We will continue to use this example in the Server and Client chapters.

If you want to the Client implements the RPC trait and don't care about cancelling the RPC call, the implementation can be conveniently generated with an argument (impl_for_client) in the attribute macro #[export_trait]. The usage will demonstrated below.


#![allow(unused)]
fn main() {
#[async_trait]
#[export_trait(impl_for_client)] // This will generate `Arith` trait implementation for `toy_rpc::Client`
pub trait Arith {
    // let's mark both `add(...)` and `subtract(...)` as RPC method
    #[export_method]
    async fn add(&self, args: (i32, i32)) -> Result<i32, anyhow::Error>;

    #[export_method]
    async fn subtract(&self, args: (i32, i32)) -> Result<i32, toy_rpc::Error>;

    // All methods must be exported if trait implementation generation is enabled
    // fn say_hi(&self);
}
}

This will allow convenient client usage like


#![allow(unused)]
fn main() {
// client.rs

let reply = Arith::add(&client, (3,4)).await.unwrap();
}

More can be found here in the tokio_tcp example