8.2 KiB
irpc: The rpc_requests Macro
The #[rpc_requests] attribute macro is the primary way to define an irpc protocol. It generates the boilerplate for channel typing, message wrapping, and service trait implementations.
Basic Usage
use irpc::{channel::{mpsc, oneshot}, rpc_requests, Client, WithChannels};
use serde::{Deserialize, Serialize};
#[rpc_requests(message = ComputeMessage)]
#[derive(Debug, Serialize, Deserialize)]
enum ComputeProtocol {
/// Unary RPC: one request, one response
#[rpc(tx=oneshot::Sender<i64>)]
#[wrap(Multiply)]
Multiply(i64, i64),
/// Bidirectional streaming
#[rpc(tx=mpsc::Sender<i64>, rx=mpsc::Receiver<i64>)]
#[wrap(Sum)]
Sum,
}
This single macro invocation generates:
- Wrapper structs (from
#[wrap]):MultiplyandSumstruct types Channels<ComputeProtocol>impls: For each variant's inner type, specifyingTxandRxServiceimpl:impl Service for ComputeProtocol { type Message = ComputeMessage; }RemoteServiceimpl (rpc feature): Maps protocol variants + QUIC streams to messagesComputeMessageenum: Wraps each request inWithChannelsFromconversions: Between inner types,ComputeProtocol, andComputeMessage
Macro Arguments
Top-level (on the enum)
| Argument | Required | Description |
|---|---|---|
message = Name |
Recommended | Name of the generated message enum. Also generates Service and RemoteService impls. |
alias = "Suffix" |
Optional | Generates type aliases like MultiplyMsg = WithChannels<Multiply, ComputeProtocol> |
rpc_feature = "feat" |
Optional | Feature-gates the RemoteService impl with #[cfg(feature = "feat")] |
no_rpc |
Optional | Skips generating RemoteService impl entirely |
no_spans |
Optional | Skips span-related code (for use without the spans feature) |
Per-variant
#[rpc(tx=Type, rx=Type)]
Specifies channel types for each request:
tx— response channel type (server → client). Defaults toNoSender.rx— update channel type (client → server). Defaults toNoReceiver.
Valid types:
oneshot::Sender<T>— single responsempsc::Sender<T>— streaming responseoneshot::Receiver<T>— not valid as tx (use for rx pattern)mpsc::Receiver<T>— streaming updates (client → server)NoSender/NoReceiver— no channel in that direction
#[wrap(TypeName, derive(Traits))]
Generates a struct from the variant's fields:
TypeName— name of the generated struct- Optional visibility prefix (e.g.,
pub(crate) TypeName) derive(...)— additional derive macros beyond the defaultSerialize, Deserialize, Debug
If #[wrap] is not used, each variant must have exactly one unnamed field (a named type).
Generated Code Walkthrough
Given this input:
#[rpc_requests(message = StoreMessage)]
#[derive(Debug, Serialize, Deserialize)]
enum StoreProtocol {
#[rpc(tx=oneshot::Sender<String>)]
#[wrap(GetRequest, derive(Clone))]
Get(String),
#[rpc(tx=oneshot::Sender<()>)]
#[wrap(SetRequest)]
Set { key: String, value: String },
}
The macro generates:
1. Wrapper Structs
#[derive(Debug, Serialize, Deserialize, Clone)]
pub GetRequest(pub String);
#[derive(Debug, Serialize, Deserialize)]
pub SetRequest { pub key: String, pub value: String }
The variants are rewritten to use these:
enum StoreProtocol {
Get(GetRequest),
Set(SetRequest),
}
2. Channels Implementations
impl Channels<StoreProtocol> for GetRequest {
type Tx = oneshot::Sender<String>;
type Rx = NoReceiver;
}
impl Channels<StoreProtocol> for SetRequest {
type Tx = oneshot::Sender<()>;
type Rx = NoReceiver;
}
3. Message Enum
#[doc = "Message enum for [`StoreProtocol`]"]
#[allow(missing_docs)]
#[derive(Debug)]
pub enum StoreMessage {
Get(WithChannels<GetRequest, StoreProtocol>),
Set(WithChannels<SetRequest, StoreProtocol>),
}
4. Service Implementation
impl Service for StoreProtocol {
type Message = StoreMessage;
}
5. RemoteService Implementation (rpc feature)
impl RemoteService for StoreProtocol {
fn with_remote_channels(
self,
rx: noq::RecvStream,
tx: noq::SendStream,
) -> Self::Message {
match self {
StoreProtocol::Get(msg) => {
StoreMessage::from(WithChannels::from((msg, tx, rx)))
}
StoreProtocol::Set(msg) => {
StoreMessage::from(WithChannels::from((msg, tx, rx)))
}
}
}
}
6. From Conversions
// Inner type → Protocol enum
impl From<GetRequest> for StoreProtocol { ... }
impl From<SetRequest> for StoreProtocol { ... }
// WithChannels → Message enum
impl From<WithChannels<GetRequest, StoreProtocol>> for StoreMessage { ... }
impl From<WithChannels<SetRequest, StoreProtocol>> for StoreMessage { ... }
7. parent_span Method (spans feature)
impl StoreMessage {
pub fn parent_span(&self) -> tracing::Span {
let span = match self {
StoreMessage::Get(inner) => inner.parent_span_opt(),
StoreMessage::Set(inner) => inner.parent_span_opt(),
};
span.cloned().unwrap_or_else(|| tracing::Span::current())
}
}
Interaction Pattern Mapping
The #[rpc] attribute maps directly to gRPC-like patterns:
| Pattern | tx type |
rx type |
Example |
|---|---|---|---|
| Unary RPC | oneshot::Sender<R> |
NoReceiver |
Get by key, return value |
| Server streaming | mpsc::Sender<R> |
NoReceiver |
List all items |
| Client streaming | oneshot::Sender<R> |
mpsc::Receiver<U> |
Upload items, get count |
| Bidirectional | mpsc::Sender<R> |
mpsc::Receiver<U> |
Chat, live updates |
| Notify (fire & forget) | NoSender |
NoReceiver |
Log event |
Client Methods Generated by Patterns
The Client<S> methods correspond to channel types:
// Unary RPC: tx=oneshot::Sender<Res>, rx=NoReceiver
client.rpc(Get { key: "x" }).await // → Result<Res>
// Server streaming: tx=mpsc::Sender<Res>, rx=NoReceiver
client.server_streaming(List, 16).await // → Result<mpsc::Receiver<Res>>
// Client streaming: tx=oneshot::Sender<Res>, rx=mpsc::Receiver<Update>
client.client_streaming(SetMany, 4).await // → Result<(mpsc::Sender<Update>, oneshot::Receiver<Res>)>
// Bidirectional: tx=mpsc::Sender<Res>, rx=mpsc::Receiver<Update>
client.bidi_streaming(Sum, 4, 4).await // → Result<(mpsc::Sender<Update>, mpsc::Receiver<Res>)>
// Notify: tx=NoSender, rx=NoReceiver
client.notify(Log { msg: "hi" }).await // → Result<()>
Manual Protocol Definition (Without Macro)
You can define protocols manually instead of using the macro:
use irpc::{channel::{mpsc, none::NoReceiver, oneshot}, Channels, Service, WithChannels};
use serde::{Deserialize, Serialize};
// 1. Define request types
#[derive(Debug, Serialize, Deserialize)]
struct Get { key: String }
#[derive(Debug, Serialize, Deserialize)]
struct Set { key: String, value: String }
// 2. Implement Channels for each type
impl Channels<StorageProtocol> for Get {
type Tx = oneshot::Sender<Option<String>>;
type Rx = NoReceiver;
}
impl Channels<StorageProtocol> for Set {
type Tx = oneshot::Sender<()>;
type Rx = NoReceiver;
}
// 3. Define protocol enum
#[derive(derive_more::From, Serialize, Deserialize, Debug)]
enum StorageProtocol {
Get(Get),
Set(Set),
}
// 4. Define message enum
#[derive(derive_more::From)]
enum StorageMessage {
Get(WithChannels<Get, StorageProtocol>),
Set(WithChannels<Set, StorageProtocol>),
}
// 5. Implement Service
impl Service for StorageProtocol {
type Message = StorageMessage;
}
// 6. Implement RemoteService (rpc feature)
impl RemoteService for StorageProtocol {
fn with_remote_channels(self, rx: noq::RecvStream, tx: noq::SendStream) -> Self::Message {
match self {
StorageProtocol::Get(msg) => WithChannels::from((msg, tx, rx)).into(),
StorageProtocol::Set(msg) => WithChannels::from((msg, tx, rx)).into(),
}
}
}
This manual approach gives full control but requires more boilerplate. The macro generates all of this automatically.