nprpc/lib.rs
1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5//! # Not Postcard-RPC
6//!
7//! It needs a better name. `nprpc` is a tool for communication that is:
8//!
9//! * Strongly typed (and Rust focused)
10//! * Uses `postcard-schema` for deriving payload schemas.
11//! * Messages are tagged with an eight-byte hash of the method name,
12//! request schema, and response schema. Hash collisions are caught at
13//! compile time, and Cool Schemas Never Change.
14//! * Uses `serde` and `postcard` are used for serialization and
15//! deserialization.
16//! * `postcard-dyn` can be used for dynamic ser/de, and transcoding frames
17//! to/from JSON (Coming Soon™️).
18//! * Strongly ordered
19//! * Client initiates request to Server, then Server replies to Client.
20//! * No concurrent in-flight requests.
21//! * No multi-part requests or responses.
22//! * Currently only blocking client/servers supported.
23//! * At least an async (Tokio) client Coming Soon™️.
24//! * Flexible transport and storage
25//! * Can be used with or without a heap, both for frame buffering as well
26//! as request/response data payloads.
27//! * Can work over any transport, clients and servers both have `Io` traits
28//! for custom plumbing.
29//! * For bounded types, maximum message sizes can be calculated, and
30//! necessary buffer sizes are generated as `const`s.
31//! * Support for flexible data payloads:
32//! * Variable length messages supported.
33//! * `&[u8]` or `&str`s can be borrowed from the incoming frame (for
34//! requests), or borrowed from the server for the outgoing frame (for
35//! responses).
36//! * Support for "type punning" on either side of the connection, e.g. the
37//! server can send a `&str` and the client can receive an owned `String`.
38//! * Doesn't concern itself with reliable delivery (and doesn't attempt to add
39//! reliability, That's On You).
40//!
41//! ## Lightning Tour
42//!
43//! We call "a set of remotely callable methods" an "interface". You define
44//! an interface using the [`interface!`] macro. You usually do this in a shared
45//! `example-api` crate.
46//!
47//! ```rust
48//! use nprpc::interface;
49//!
50//! interface! {
51//! /// This module named "example" contains everything you need for both
52//! /// the client and server.
53//! mod example {
54//! /// We support doc comments and `#[cfg]`s on methods. Methods are
55//! /// written using a mostly-rust-like syntax.
56//! fn echo(u32) -> u32;
57//! }
58//! }
59//! ```
60//!
61//! In order to implement the server for this interface, you need to implement
62//! the `example::Server` trait's methods. You can use rust-analyzer's
63//! "Implement missing methods" action to do this quickly.
64//!
65//! ```rust
66//! # use nprpc::interface;
67//! #
68//! # interface! {
69//! # /// This module named "example" contains everything you need for both
70//! # /// the client and server.
71//! # mod example {
72//! # /// We support doc comments and `#[cfg]`s on methods. Methods are
73//! # /// written using a mostly-rust-like syntax.
74//! # fn echo(u32) -> u32;
75//! # }
76//! # }
77//! use nprpc::Request;
78//! // From the `example-api` crate above
79//! use example::Server;
80//!
81//! // Not shown: impl Backend for WireBackend { .. }
82//! struct WireBackend {
83//! // ...
84//! # a: WireStorage,
85//! # b: WireIo,
86//! }
87//!
88//! // Server-specific type that contains necessary trait
89//! struct ServerImpl {
90//! // ...
91//! }
92//! # impl ServerImpl { pub fn new() -> Self { Self {} }}
93//!
94//! // Implementation of `example` defined by `interface!` above
95//! impl example::Server for ServerImpl {
96//! fn echo(&mut self, req: Request<u32>) -> u32 {
97//! // `req` contains both the header of the request as well as the
98//! // body, what we defined in the interface! macro. We just copy the
99//! // data back out.
100//! *req.body
101//! }
102//! }
103//!
104//! # struct WireIo;
105//! # struct WireStorage;
106//! # use nprpc::io::server::{ServerIoError, RawIoFrame};
107//! # impl nprpc::io::server::Io for WireIo {
108//! # type Error = ();
109//! # type Meta = ();
110//! # fn recv_one_frame_raw<'data>(&mut self, _: &'data mut [u8])
111//! # -> Result<Option<RawIoFrame<'data, ()>>, ServerIoError<()>>
112//! # {
113//! # Err(nprpc::io::server::ServerIoError::Io(()))
114//! # }
115//! # fn send_one_frame_raw(&mut self, _: RawIoFrame<'_, ()>)
116//! # -> Result<(), ServerIoError<()>> { todo!() }
117//! # }
118//! # impl nprpc::io::Storage for WireStorage {
119//! # fn buffers(&mut self) -> nprpc::io::StorageView<'_> {
120//! # nprpc::io::StorageView { rqst_buf: &mut [], resp_buf: &mut [] }
121//! # }
122//! # }
123//! # impl nprpc::io::server::Backend for WireBackend {
124//! # type Io = WireIo;
125//! # type Storage = WireStorage;
126//! # fn parts(&mut self) -> (&mut WireStorage, &mut WireIo) {
127//! # let Self { a, b } = self;
128//! # (a, b)
129//! # }
130//! # }
131//! #
132//! # impl WireBackend {
133//! # fn new() -> Self { Self { a: WireStorage, b: WireIo }}
134//! # }
135//! #
136//! fn main() {
137//! let mut wire = WireBackend::new();
138//! let mut server = ServerImpl::new();
139//!
140//! // Server one request, typically done in a loop
141//! let res = server.serve_one(&mut wire);
142//! if let Err(e) = res {
143//! println!("Err: {e:?}");
144//! }
145//! }
146//!
147//! ```
148//!
149//! As a client, you'll get an extension trait called `Client` that lets you
150//! call the methods you defined.
151//!
152//! ```rust,no_run
153//! # use nprpc::interface;
154//! #
155//! # interface! {
156//! # /// This module named "example" contains everything you need for both
157//! # /// the client and server.
158//! # mod example {
159//! # /// We support doc comments and `#[cfg]`s on methods. Methods are
160//! # /// written using a mostly-rust-like syntax.
161//! # fn echo(u32) -> u32;
162//! # }
163//! # }
164//! #
165//! # struct WireIo;
166//! # struct WireStorage;
167//! # impl nprpc::io::client::Io for WireIo {
168//! # type Error = ();
169//! # fn send_then_receive_raw_frames<'a>(&mut self, _: &[u8], _: &'a mut [u8])
170//! # -> Result<&'a [u8], ()> { todo!() }
171//! # }
172//! # impl nprpc::io::Storage for WireStorage {
173//! # fn buffers(&mut self) -> nprpc::io::StorageView<'_> { todo!() }
174//! # }
175//! # impl nprpc::io::client::Backend for WireBackend {
176//! # type Io = WireIo;
177//! # type Storage = WireStorage;
178//! # fn next_sequence_number(&mut self) -> u16 { todo!() }
179//! # fn parts(&mut self) -> (&mut WireStorage, &mut WireIo) { todo!() }
180//! # }
181//! // Not shown: impl Backend for WireBackend { .. }
182//! struct WireBackend;
183//!
184//! use nprpc::io::client::ClientIoError;
185//! use nprpc::Response;
186//!
187//! // Pull in the extension trait to get access to the methods for any type
188//! // that impls `Backend`
189//! use example::Client;
190//!
191//! fn main() {
192//! let mut backend = WireBackend;
193//! let res: Result<Response<u32>, ClientIoError<_>> = backend.echo(&123);
194//! # drop(res);
195//! }
196//!
197//! ```
198//!
199//! ## Naming Guide
200//!
201//! For the sake of consistency, we use the following naming scheme for types
202//! and lifetimes. Prefer the 4-letter abbreviations to keep pieces aligned in
203//! multi-line code.
204//!
205//! * "Request"
206//! * A message sent by a client, and received by a server
207//! * `request`, `rqst`, or `'rqst`
208//! * "Response"
209//! * A message sent by a server, and received by a client
210//! * `response`, `resp`, or `'resp`
211//! * "Header"
212//! * Included in every request and response, same type in all items
213//! * `header` or `hedr`
214//! * "Body"
215//! * The type-specific payload. Used for both the serialized form as well
216//! as the rust-type form.
217//! * Always `body`. May have the lifetime `'rqst` or `'resp`.
218
219#![cfg_attr(not(feature = "std"), no_std)]
220
221pub mod macros;
222
223pub mod interface;
224pub mod io;
225pub mod wire;
226
227pub use io::client::{ClientError, ClientIoError};
228pub use io::server::{ServerError, ServerIoError};
229
230// TODO: Should the `Key`s we generate also hash the Header and Error type to
231// ensure complete compatibility? Do we consider this part of the versioning?
232
233/// Not covered by semver, re-exported items for external macros
234#[doc(hidden)]
235pub mod __private {
236 pub use postcard_schema_ng::Schema;
237 pub use postcard_schema_ng::key::Key;
238 pub use postcard_schema_ng::schema::DataModelType;
239 pub use serde::{Deserialize, Serialize};
240}
241
242/// Borrowed view of a request
243///
244/// Contains a reference to a request header and request body.
245#[derive(Debug, Clone, Copy)]
246pub struct Request<'rqst, T> {
247 pub hedr: &'rqst wire::Header,
248 pub body: &'rqst T,
249}
250
251/// Borrowed view of a request
252///
253/// Like `Request`, but the body has not been deserialized.
254#[derive(Debug, Clone, Copy)]
255pub struct RequestRaw<'rqst> {
256 pub hedr: &'rqst wire::Header,
257 pub body: &'rqst [u8],
258}
259
260/// Owned view of a response
261#[derive(Debug)]
262pub struct Response<U> {
263 pub hedr: wire::Header,
264 pub body: U,
265}