Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions runtime/core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ license = "MPL-2.0"
[features]
default = ["std"]

# `contract` + `wire` need only `serde`'s core traits and no allocation.
# `contract` needs only `serde`'s core traits and no allocation.
# `alloc` adds owned generated types, the frame `transport`, and the `serve`
# loop. `std` adds the MessagePack `format` and the in-memory transport, plus
# the dylib ABI.
Expand All @@ -20,11 +20,12 @@ std = [
"dep:libloading",
"dep:rmp",
"dep:rmp-serde",
"dep:serde_json",
]


[dependencies]
# `contract` / `wire` — no_std, no alloc
# `contract` — no_std, no alloc
serde = { version = "1.0.190", default-features = false }
bytemuck = { version = "1.14.0", features = ["derive"] }

Expand All @@ -33,6 +34,7 @@ abi_stable = { version = "0.11.2", optional = true }
libloading = { version = "0.8.0", optional = true }
rmp = { version = "0.8.12", optional = true }
rmp-serde = { version = "1.1.2", optional = true }
serde_json = { version = "1.0.108", optional = true, features = ["raw_value"] }


[dev-dependencies]
Expand Down
140 changes: 79 additions & 61 deletions runtime/core/src/client.rs
Original file line number Diff line number Diff line change
@@ -1,111 +1,125 @@
//! Consumer-side calling: the mirror of [`Server`](crate::serve::Server).
//!
//! A [`Client`] owns a [`Transport`] and a [`WireFormat`], hands out request
//! ids, and reuses its send / receive buffers across calls (§4.6 — no per-call
//! allocation on the frame path). One call is: encode the params straight into
//! the frame, send, block for the response, hand back the [`Envelope`]
//! borrowing the receive buffer.
//! A [`Client`] owns a [`Transport`], a [`WireFormat`] and a [`Framing`], hands
//! out request ids, and reuses its send / receive buffers across calls (§4.6 —
//! no per-call allocation on the frame path). One call is: hand the params to
//! the framing (which serialises them with the format), send, block for the
//! response, hand back the [`Envelope`] borrowing the receive buffer.
//!
//! The generated `<Proto>Client` stub wraps this: it knows each function's
//! ordinal and its params / result / error types, so it calls [`Client::call`]
//! with a `u16` and turns the returned `(Envelope, &W)` into `Result<R,
//! CallError<E>>` — `Ok` payload decoded as `R`, an `Err` ordinal mapped through
//! its generated error table. This type stays protocol-agnostic; it does no
//! decoding, which is why `call` also hands back the format.
//! ordinal *and* name (it passes a [`Call`]), its params / result / error
//! types, and turns the returned `(Envelope, &W)` into `Result<R,
//! CallError<E>>`.

use alloc::vec::Vec;

use core::time::Duration;

use serde::Serialize;

use crate::contract::{Envelope, Handshake, RuntimeError, WireFormat};
use crate::contract::{
Call, DatagramFraming, Envelope, Framing, Handshake, RuntimeError, WireFormat,
};
use crate::transport::Transport;
use crate::wire;

/// A calling endpoint bound to one transport.
pub struct Client<T, W> {
/// A calling endpoint bound to one transport. Generic over the [`Framing`];
/// defaults to the Comline datagram framing.
pub struct Client<T, W, F = DatagramFraming> {
transport: T,
format: W,
framing: F,
next_id: u64,
request: Vec<u8>,
response: Vec<u8>,
}

impl<T: Transport, W: WireFormat> Client<T, W> {
impl<T: Transport, W: WireFormat> Client<T, W, DatagramFraming> {
/// Bind a client to a transport **without a handshake** — "misaligned
/// mode". Use when the peer can't take part (a legacy server); a
/// wire-format or schema mismatch then surfaces later as a decode / framing
/// error instead of up front. [`connect`](Self::connect) is the checked
/// path.
pub fn new(transport: T, format: W) -> Self {
Self::with_framing(transport, format, DatagramFraming)
}

/// Bind + run the connection [`Handshake`] with the datagram framing.
pub fn connect(transport: T, format: W, local: Handshake) -> Result<Self, RuntimeError> {
Self::connect_with_framing(transport, format, DatagramFraming, local)
}
}

impl<T: Transport, W: WireFormat, F: Framing> Client<T, W, F> {
pub fn with_framing(transport: T, format: W, framing: F) -> Self {
Self {
transport,
format,
framing,
next_id: 0,
request: Vec::new(),
response: Vec::new(),
}
}

/// Bind a client and run the connection [`Handshake`]: send `local`, read
/// the peer's, and refuse (`RuntimeError::Handshake`) if they disagree on
/// schema hash, wire format, or framing.
pub fn connect(transport: T, format: W, local: Handshake) -> Result<Self, RuntimeError> {
let mut client = Self::new(transport, format);
client.exchange_handshake(local)?;
/// [`with_framing`](Self::with_framing) + run the connection [`Handshake`]:
/// send `local`, read the peer's, refuse (`RuntimeError::Handshake`) on a
/// schema / wire-format / framing mismatch.
pub fn connect_with_framing(
transport: T,
format: W,
framing: F,
local: Handshake,
) -> Result<Self, RuntimeError> {
let mut client = Self::with_framing(transport, format, framing);
client.request.clear();
local.encode(&mut client.request);
client.transport.send(&client.request)?;
client.response.clear();
client.transport.recv(&mut client.response)?;
let peer = Handshake::decode(&client.response).ok_or(RuntimeError::Handshake)?;
local.check(&peer)?;
Ok(client)
}

fn exchange_handshake(&mut self, local: Handshake) -> Result<(), RuntimeError> {
self.request.clear();
local.encode(&mut self.request);
self.transport.send(&self.request)?;

self.response.clear();
self.transport.recv(&mut self.response)?;
let peer = Handshake::decode(&self.response).ok_or(RuntimeError::Handshake)?;
local.check(&peer)
}

/// Make call `call_id` with `params`, block for the response, and return
/// its [`Envelope`] (borrowing this client's receive buffer) together with
/// the format to decode it: `Ok(payload)` for the stub to read as `R`, or
/// Make `call` with `params`, block for the response, and return its
/// [`Envelope`] (borrowing this client's receive buffer) with the format
/// to decode it: `Ok(payload)` for the stub to read as `R`, or
/// `Err { id, body }` to map through the generated error table.
///
/// The format rides along because both views come out of the same
/// `&mut self` borrow — the stub can't reach back into the client for it
/// while holding the envelope.
/// `call` is `impl Into<Call>` — a bare `u16` for datagram-only callers,
/// or `Call::new(id, name)` from a generated stub (a name-oriented framing
/// needs the name).
///
/// Borrows `self` mutably for as long as the result is held: the previous
/// response must be decoded (or dropped) before the next call. A pipelined
/// / multiplexed client is a later, additive layer.
pub fn call<P>(&mut self, call_id: u16, params: &P) -> Result<(Envelope<'_>, &W), RuntimeError>
/// response must be decoded (or dropped) before the next call.
pub fn call<C, P>(&mut self, call: C, params: &P) -> Result<(Envelope<'_>, &W), RuntimeError>
where
C: Into<Call>,
P: Serialize + ?Sized,
{
self.request_response(call_id, params, None)
self.request_response(call.into(), params, None)
}

/// [`call`](Self::call), but give up after `timeout` waiting for the
/// response — `Err(RuntimeError::Timeout)`. What a generated stub emits for
/// a `@timeout_ms` function annotation. Honoured only by transports that
/// override [`Transport::recv_timeout`] (the `std` ones do); others block.
pub fn call_with_timeout<P>(
pub fn call_with_timeout<C, P>(
&mut self,
call_id: u16,
call: C,
params: &P,
timeout: Duration,
) -> Result<(Envelope<'_>, &W), RuntimeError>
where
C: Into<Call>,
P: Serialize + ?Sized,
{
self.request_response(call_id, params, Some(timeout))
self.request_response(call.into(), params, Some(timeout))
}

fn request_response<P>(
&mut self,
call_id: u16,
call: Call,
params: &P,
timeout: Option<Duration>,
) -> Result<(Envelope<'_>, &W), RuntimeError>
Expand All @@ -116,8 +130,8 @@ impl<T: Transport, W: WireFormat> Client<T, W> {
self.next_id = self.next_id.wrapping_add(1);

self.request.clear();
wire::encode_request_header(call_id, request_id, &mut self.request);
self.format.encode(params, &mut self.request)?;
self.framing
.encode_request(call, request_id, params, &self.format, &mut self.request)?;
self.transport.send(&self.request)?;

self.response.clear();
Expand All @@ -130,34 +144,38 @@ impl<T: Transport, W: WireFormat> Client<T, W> {
}
}

let (echoed, envelope) =
wire::decode_response(&self.response).ok_or(RuntimeError::Framing)?;
let (echoed, envelope) = self
.framing
.decode_response(&self.response)
.ok_or(RuntimeError::Framing)?;
if echoed != request_id {
// One outstanding call at a time, so a mismatched id is a stale or
// corrupt frame, not reordering.
return Err(RuntimeError::Framing);
}
let envelope = Envelope::decode(envelope).ok_or(RuntimeError::Framing)?;
Ok((envelope, &self.format))
}

/// Fire a **one-way** call: frame `call_id` + `params`, send, return. No
/// response is awaited — for `_return: None` schema functions, whose
/// generated dispatcher writes no [`Envelope`] and whose peer [`Server`]
/// therefore sends nothing back. `Ok(())` means the frame left the
/// transport, never a remote outcome.
pub fn notify<P>(&mut self, call_id: u16, params: &P) -> Result<(), RuntimeError>
/// Fire a **one-way** call: frame it, send, return. No response is awaited
/// — for `_return: None` schema functions, whose generated dispatcher
/// writes no [`Envelope`] and whose peer [`Server`] therefore sends nothing
/// back. `Ok(())` means the frame left the transport.
pub fn notify<C, P>(&mut self, call: C, params: &P) -> Result<(), RuntimeError>
where
C: Into<Call>,
P: Serialize + ?Sized,
{
// Keep request ids monotonic across mixed call / notify use, even
// though nothing reads this one back.
let request_id = self.next_id;
self.next_id = self.next_id.wrapping_add(1);

self.request.clear();
wire::encode_request_header(call_id, request_id, &mut self.request);
self.format.encode(params, &mut self.request)?;
self.framing.encode_request(
call.into(),
request_id,
params,
&self.format,
&mut self.request,
)?;
self.transport.send(&self.request)
}

Expand Down
60 changes: 53 additions & 7 deletions runtime/core/src/contract/dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,23 +28,69 @@ impl Kind {
}
}

/// What a dispatched call produced. Framing-agnostic: the dispatcher calls
/// [`ok`](Reply::ok) or [`err`](Reply::err) exactly once, or neither for a
/// one-way call. The framing then wraps the accumulated body.
pub struct Reply<'a> {
body: &'a mut dyn BufMut,
outcome: Outcome,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Outcome {
/// One-way — nothing written, no response frame.
None,
/// A success payload was written.
Ok,
/// A schema error `body` was written, keyed by ordinal `id`.
Err(u16),
}

impl<'a> Reply<'a> {
pub fn new(body: &'a mut dyn BufMut) -> Self {
Self {
body,
outcome: Outcome::None,
}
}

/// Record a success: `payload` is the serialised return value.
pub fn ok(&mut self, payload: &[u8]) {
self.body.put_slice(payload);
self.outcome = Outcome::Ok;
}

/// Record a raised schema error: `body` is the serialised error struct,
/// `id` its schema-global ordinal.
pub fn err(&mut self, id: u16, body: &[u8]) {
self.body.put_slice(body);
self.outcome = Outcome::Err(id);
}

pub fn outcome(&self) -> Outcome {
self.outcome
}
}

/// The provider side of a protocol: given an inbound call and its encoded
/// params, run the user's handler and write the response [`Envelope`] into
/// `out`.
/// params, run the user's handler and record the outcome on `reply`.
///
/// Sync — no boxed futures on the `no_std` path. An async server layer wraps
/// this behind the `std` feature. The generated `<Proto>Dispatcher` implements
/// it; the call system holds one by value / `&D` (generic — no vtable, no
/// `dyn`) and calls it with the format it was configured with.
///
/// [`Envelope`]: crate::contract::Envelope
/// it; [`Server`](crate::serve::Server) holds one and routes inbound frames
/// to it.
pub trait Dispatch {
/// The protocol's function names, in declaration order — so a name-oriented
/// framing's method name can be resolved to an ordinal. The generated
/// dispatcher returns its `<PROTO>_CALLS` constant.
fn calls(&self) -> &'static [&'static str];

fn dispatch<W: WireFormat>(
&self,
call: Kind,
params: &[u8],
format: &W,
out: &mut dyn BufMut,
reply: &mut Reply,
) -> Result<(), RuntimeError>;
}

Expand Down
Loading
Loading