Skip to main content

dolang_rpc/
unbound.rs

1//! Staged construction: negotiate first, choose a concrete [`Protocol`]
2//! afterward.
3//!
4//! [`Client<P>`](crate::client::Client)/[`Server<P>`](crate::server::Server) are generic over
5//! a statically known `P`, but which concrete `P` to use can depend on the
6//! *negotiated* application-protocol version (e.g. a future protocol
7//! revision might be represented as a distinct Rust type). The client and
8//! server [`Unbound`](crate::client::Unbound) endpoints negotiate an
9//! application protocol first, expose what was negotiated, and only then let
10//! the caller bind to a concrete `P`.
11//!
12//! [`Builder`] is the sole entry point for constructing either one: it takes
13//! the mandatory application-protocol descriptor up front, offers chainable
14//! setters for individual size/concurrency limits, and a terminal method per
15//! transport shape (`client`/`client_split`/... or `server`/`server_split`/...).
16
17use tokio::io::{AsyncRead, AsyncWrite};
18
19#[cfg(unix)]
20use std::{os::unix::net::UnixStream, result};
21
22#[cfg(windows)]
23use std::os::windows::io::OwnedHandle;
24#[cfg(all(docsrs, not(windows)))]
25struct OwnedHandle;
26
27#[cfg(windows)]
28use tokio::net::windows::named_pipe::{NamedPipeClient, NamedPipeServer};
29#[cfg(all(docsrs, not(windows)))]
30struct NamedPipeClient;
31#[cfg(all(docsrs, not(windows)))]
32struct NamedPipeServer;
33
34use crate::{
35    Error, Limits, Protocol,
36    auth::{Auth, AuthKey},
37    client::Client,
38    fragment,
39    server::Server,
40    transport,
41};
42
43/// Builds an unbound client or server endpoint.
44///
45/// A builder advertises one application-protocol name and supported versions.
46/// Its terminal `client*` or `server*` method consumes it, performs the
47/// handshake, and returns an unbound endpoint that can be inspected before
48/// binding it to a concrete [`Protocol`]. Limit setters override defaults;
49/// size and concurrency limits are negotiated to the more conservative value,
50/// while copy thresholds are local performance settings.
51pub struct Builder {
52    name: String,
53    versions: Vec<u16>,
54    limits: Limits,
55    key: Option<AuthKey>,
56}
57
58impl Builder {
59    /// Starts a builder for an application-protocol name and supported versions.
60    ///
61    /// `versions` must be nonempty and unique.
62    pub fn new(name: &str, versions: &[u16]) -> Self {
63        Self {
64            name: name.to_owned(),
65            versions: versions.to_vec(),
66            limits: Limits::default(),
67            key: None,
68        }
69    }
70
71    /// Requires mutual proof of a pre-shared key during negotiation.
72    ///
73    /// Both endpoints must be configured with the same key, or neither: a
74    /// mismatch in either direction aborts the handshake. See [`crate::auth`]
75    /// for what this does and does not protect against.
76    pub fn key(mut self, key: AuthKey) -> Self {
77        self.key = Some(key);
78        self
79    }
80
81    /// Sets the maximum complete wire-fragment size, including its header.
82    ///
83    /// This bounds one round-robin write of a fragmented message. Defaults to
84    /// 512 KiB; the peer and local endpoint use the smaller advertised value.
85    pub fn max_fragment_size(mut self, value: usize) -> Self {
86        self.limits.max_fragment_size = value;
87        self
88    }
89
90    /// Sets the maximum reassembled postcard payload, excluding a trailer.
91    ///
92    /// Defaults to 2 MiB; the peer and local endpoint use the smaller
93    /// advertised value, and it is lowered further to the negotiated
94    /// [`max_outstanding_payload`](Self::max_outstanding_payload) if that
95    /// ends up smaller — a per-message cap above the aggregate pool would
96    /// describe a message that could never be sent.
97    pub fn max_payload_size(mut self, value: usize) -> Self {
98        self.limits.max_payload_size = value;
99        self
100    }
101
102    /// Sets the session-wide postcard payload quota, in bytes.
103    ///
104    /// This bounds the total charged payload bytes of every call that has not
105    /// yet released, across the whole connection. Unlike
106    /// [`max_payload_size`](Self::max_payload_size), which bounds one message,
107    /// this bounds the sum — and it is charged for the *entire call
108    /// lifecycle*, from the sender admitting the message to the receiving
109    /// application being done with it, not merely while the payload is being
110    /// reassembled. A payload's memory does not end at dispatch.
111    ///
112    /// The consequence is a contract worth knowing: a long-pending call with a
113    /// large payload holds its share of the pool for as long as it pends, and
114    /// throttles the connection accordingly. Indefinitely pending calls are
115    /// legitimate — an event poll is the usual shape — and a large payload on
116    /// one is unusual but not unthinkable. If you want both, release
117    /// explicitly (see
118    /// [`CallContext::release_payload`](crate::server::CallContext::release_payload)
119    /// and [`CallResult::take_payload_credit`](crate::client::CallResult::take_payload_credit))
120    /// once you no longer need the request. Violating it degrades throughput;
121    /// it does not hang anything.
122    ///
123    /// Trailer bytes are counted against
124    /// [`trailer_session_window`](Self::trailer_session_window) instead, and
125    /// the two pools are deliberately separate: a handler that must consume a
126    /// trailer before it can release its payload would deadlock against a
127    /// shared one.
128    ///
129    /// The pool measures wire bytes, and a struct-heavy payload can occupy
130    /// several times that once deserialized. Defaults to 16 MiB; the peer and
131    /// local endpoint use the smaller advertised value, raised to at least
132    /// this endpoint's own `max_payload_size` before it is advertised.
133    pub fn max_outstanding_payload(mut self, value: usize) -> Self {
134        self.limits.max_outstanding_payload = value;
135        self
136    }
137
138    /// Sets how much retired trailer credit accumulates before it is
139    /// returned to the peer, in bytes.
140    ///
141    /// Purely a local coalescing knob: it is not negotiated, the two ends
142    /// need not agree on it, and it bounds nothing — what bounds the peer is
143    /// [`trailer_session_window`](Self::trailer_session_window). Larger
144    /// values mean fewer credit fragments and a coarser feedback signal.
145    /// Credit is flushed regardless once the pool is exhausted or a trailer
146    /// ends, so no value can stall a sender.
147    ///
148    /// Defaults to 256 KiB.
149    pub fn trailer_credit_interval(mut self, value: usize) -> Self {
150        self.limits.trailer_credit_interval = value;
151        self
152    }
153
154    /// Sets the session-wide trailer credit pool, in bytes.
155    ///
156    /// This bounds trailer data the peer has sent but this end has not yet
157    /// retired — released by the consuming application, which is later than
158    /// merely reading it — across *all* trailers at once. It is the only
159    /// credit limit and the whole bound on receiver memory attributable to
160    /// trailers, however many are open. There is no separate cap on a
161    /// trailer's total size, so a trailer may stream indefinitely.
162    ///
163    /// There is deliberately no per-trailer subdivision: a sender that lets
164    /// one trailer consume the pool starves only its own other trailers, so
165    /// dividing it up is the sending end's local scheduling choice rather
166    /// than a protocol rule — and any division it chooses is safe, since
167    /// credit is flushed whenever a consumer is left waiting and not merely
168    /// at the coalescing threshold. The corollary is that a consumer which
169    /// stalls indefinitely can hold as much of the pool as the peer chose to
170    /// spend on it.
171    ///
172    /// Defaults to 16 MiB; the peer and local endpoint use the smaller
173    /// advertised value, floored at 1. A value below
174    /// [`max_fragment_size`](Self::max_fragment_size) is legal but merely
175    /// produces short fragments.
176    pub fn trailer_session_window(mut self, value: usize) -> Self {
177        self.limits.trailer_session_window = value;
178        self
179    }
180
181    /// Sets the maximum native handles carried by one wire fragment.
182    ///
183    /// Defaults to 8, is capped to the transport's operating-system limit,
184    /// and is negotiated down to the peer's advertised value.
185    pub fn max_handles_per_fragment(mut self, value: usize) -> Self {
186        self.limits.max_handles_per_fragment = value;
187        self
188    }
189
190    /// Sets the maximum native handles carried by one message.
191    ///
192    /// Defaults to 8; the peer and local endpoint use the smaller
193    /// advertised value.
194    pub fn max_handles_per_message(mut self, value: usize) -> Self {
195        self.limits.max_handles_per_message = value;
196        self
197    }
198
199    /// Sets the receive-side eager-copy threshold for an undemanded fragment.
200    ///
201    /// A fragment at or below this size is copied immediately, allowing the
202    /// connection receive loop to continue without waiting for the trailer
203    /// reader. Defaults to 64 KiB. Set zero to disable nonempty eager copies.
204    pub fn trailer_recv_copy_threshold(mut self, value: usize) -> Self {
205        self.limits.trailer_recv_copy_threshold = value;
206        self
207    }
208
209    /// Sets the receive-side eager-copy threshold for a demanded fragment.
210    ///
211    /// This applies when the trailer reader is already waiting for the next
212    /// fragment. Defaults to 256 KiB. Set zero to disable nonempty eager
213    /// copies on this path.
214    pub fn trailer_recv_demand_copy_threshold(mut self, value: usize) -> Self {
215        self.limits.trailer_recv_demand_copy_threshold = value;
216        self
217    }
218
219    /// Sets the send-side staging threshold for a trailer fragment.
220    ///
221    /// A write at or below this size is copied into staging without waiting
222    /// for a transport grant. Defaults to 64 KiB. Set zero to disable
223    /// nonempty eager staging.
224    pub fn trailer_send_copy_threshold(mut self, value: usize) -> Self {
225        self.limits.trailer_send_copy_threshold = value;
226        self
227    }
228
229    /// Sets the maximum number of concurrent calls, counted from a request's
230    /// first fragment to its response.
231    ///
232    /// Requests still being reassembled count against it alongside those
233    /// already dispatched, so the two together can never exceed this.
234    /// Messages that have entered their trailer phase are excluded — a
235    /// trailer may outlive its call, and is bounded by
236    /// [`trailer_session_window`](Self::trailer_session_window) instead.
237    ///
238    /// This is a count and not a memory bound; what bounds the memory those
239    /// calls hold is
240    /// [`max_outstanding_payload`](Self::max_outstanding_payload), which is
241    /// why the default is generous.
242    ///
243    /// Defaults to 1024; the peer and local endpoint use the smaller
244    /// advertised value.
245    pub fn max_concurrent_calls(mut self, value: usize) -> Self {
246        self.limits.max_concurrent_calls = value;
247        self
248    }
249
250    fn app_protocol(&self) -> (&str, &[u16]) {
251        (&self.name, &self.versions)
252    }
253
254    fn client_auth(&self) -> Option<Auth> {
255        self.key.map(|key| key.as_client())
256    }
257
258    fn server_auth(&self) -> Option<Auth> {
259        self.key.map(|key| key.as_server())
260    }
261
262    /// Negotiates a client session over a bidirectional byte stream.
263    pub async fn client<T>(self, stream: T) -> Result<crate::client::Unbound, Error>
264    where
265        T: AsyncRead + AsyncWrite + Unpin + Send + 'static,
266    {
267        let (sender, receiver) = transport::generic_duplex(stream);
268        negotiate_client(
269            transport::AnySender::Generic(sender),
270            transport::AnyReceiver::Generic(receiver),
271            self.limits,
272            #[cfg(windows)]
273            None,
274            self.app_protocol(),
275            self.client_auth(),
276        )
277        .await
278    }
279
280    /// Negotiates a client session over separate byte-stream reader and writer
281    /// halves.
282    pub async fn client_split<R, W>(
283        self,
284        reader: R,
285        writer: W,
286    ) -> Result<crate::client::Unbound, Error>
287    where
288        R: AsyncRead + Send + 'static,
289        W: AsyncWrite + Send + 'static,
290    {
291        let (sender, receiver) = transport::generic(reader, writer);
292        negotiate_client(
293            transport::AnySender::Generic(sender),
294            transport::AnyReceiver::Generic(receiver),
295            self.limits,
296            #[cfg(windows)]
297            None,
298            self.app_protocol(),
299            self.client_auth(),
300        )
301        .await
302    }
303
304    #[cfg(unix)]
305    /// Negotiates a client session over a connected Unix domain socket.
306    ///
307    /// Unlike [`client`](Self::client), this transport supports direct
308    /// [`OsHandle`](crate::handle::OsHandle) attachments.
309    pub async fn client_unix(self, stream: UnixStream) -> Result<crate::client::Unbound, Error> {
310        let (sender, receiver) = transport::unix::unix(stream)?;
311        negotiate_client(
312            transport::AnySender::Unix(sender),
313            transport::AnyReceiver::Unix(receiver),
314            self.limits,
315            #[cfg(windows)]
316            None,
317            self.app_protocol(),
318            self.client_auth(),
319        )
320        .await
321    }
322
323    #[cfg(any(windows, docsrs))]
324    #[cfg_attr(docsrs, doc(cfg(windows)))]
325    #[cfg_attr(all(docsrs, not(windows)), allow(private_interfaces))]
326    /// Starts a client session on the server end of a Windows named pipe.
327    ///
328    /// `peer_process` is retained for the lifetime of the session and must
329    /// grant process-query and synchronization access. Construction fails if
330    /// it does not identify the named-pipe peer.
331    ///
332    /// # Safety
333    ///
334    /// The identified peer must be trusted to send only handle values that it
335    /// created in this process with `DuplicateHandle`. A malicious peer can
336    /// otherwise cause this process to close arbitrary handles.
337    pub async unsafe fn client_named_pipe_server(
338        self,
339        pipe: NamedPipeServer,
340        peer_process: OwnedHandle,
341    ) -> Result<crate::client::Unbound, Error> {
342        #[cfg(windows)]
343        {
344            crate::client::validate_peer_process(
345                &peer_process,
346                transport::windows::server_pipe_peer_pid(&pipe)?,
347            )?;
348            let (sender, receiver) = transport::windows::server_pipe(pipe, false)?;
349            negotiate_client(
350                transport::AnySender::Windows(sender),
351                transport::AnyReceiver::Windows(receiver),
352                self.limits,
353                Some(peer_process),
354                self.app_protocol(),
355                self.client_auth(),
356            )
357            .await
358        }
359        #[cfg(all(docsrs, not(windows)))]
360        {
361            let _ = (self, pipe, peer_process);
362            unreachable!()
363        }
364    }
365
366    #[cfg(any(windows, docsrs))]
367    #[cfg_attr(docsrs, doc(cfg(windows)))]
368    #[cfg_attr(all(docsrs, not(windows)), allow(private_interfaces))]
369    /// Starts a client session on the client end of a Windows named pipe.
370    ///
371    /// `peer_process` is retained for the lifetime of the session and must
372    /// grant process-query and synchronization access. Construction fails if
373    /// it does not identify the named-pipe peer.
374    ///
375    /// # Safety
376    ///
377    /// The identified peer must be trusted to send only handle values that it
378    /// created in this process with `DuplicateHandle`. A malicious peer can
379    /// otherwise cause this process to close arbitrary handles.
380    pub async unsafe fn client_named_pipe_client(
381        self,
382        pipe: NamedPipeClient,
383        peer_process: OwnedHandle,
384    ) -> Result<crate::client::Unbound, Error> {
385        #[cfg(windows)]
386        {
387            crate::client::validate_peer_process(
388                &peer_process,
389                transport::windows::client_pipe_peer_pid(&pipe)?,
390            )?;
391            let (sender, receiver) = transport::windows::client_pipe(pipe, false)?;
392            negotiate_client(
393                transport::AnySender::Windows(sender),
394                transport::AnyReceiver::Windows(receiver),
395                self.limits,
396                Some(peer_process),
397                self.app_protocol(),
398                self.client_auth(),
399            )
400            .await
401        }
402        #[cfg(all(docsrs, not(windows)))]
403        {
404            let _ = (self, pipe, peer_process);
405            unreachable!()
406        }
407    }
408
409    /// Negotiates a server session over a bidirectional byte stream.
410    pub async fn server<T>(self, stream: T) -> Result<crate::server::Unbound, Error>
411    where
412        T: AsyncRead + AsyncWrite + Unpin + Send + 'static,
413    {
414        let (sender, receiver) = transport::generic_duplex(stream);
415        negotiate_server(
416            transport::AnySender::Generic(sender),
417            transport::AnyReceiver::Generic(receiver),
418            self.limits,
419            self.app_protocol(),
420            self.server_auth(),
421        )
422        .await
423    }
424
425    /// Negotiates a server session over separate byte-stream reader and writer
426    /// halves.
427    pub async fn server_split<R, W>(
428        self,
429        reader: R,
430        writer: W,
431    ) -> Result<crate::server::Unbound, Error>
432    where
433        R: AsyncRead + Send + 'static,
434        W: AsyncWrite + Send + 'static,
435    {
436        let (sender, receiver) = transport::generic(reader, writer);
437        negotiate_server(
438            transport::AnySender::Generic(sender),
439            transport::AnyReceiver::Generic(receiver),
440            self.limits,
441            self.app_protocol(),
442            self.server_auth(),
443        )
444        .await
445    }
446
447    #[cfg(unix)]
448    /// Negotiates a server session over a connected Unix domain socket.
449    ///
450    /// Unlike [`server`](Self::server), this transport supports direct
451    /// [`OsHandle`](crate::handle::OsHandle) attachments.
452    pub async fn server_unix(
453        self,
454        stream: UnixStream,
455    ) -> result::Result<crate::server::Unbound, Error> {
456        let (sender, receiver) = transport::unix::unix(stream)?;
457        negotiate_server(
458            transport::AnySender::Unix(sender),
459            transport::AnyReceiver::Unix(receiver),
460            self.limits,
461            self.app_protocol(),
462            self.server_auth(),
463        )
464        .await
465    }
466
467    #[cfg(any(windows, docsrs))]
468    #[cfg_attr(docsrs, doc(cfg(windows)))]
469    #[cfg_attr(all(docsrs, not(windows)), allow(private_interfaces))]
470    /// Creates a server on the server end of a Windows named pipe.
471    pub async fn server_named_pipe_server(
472        self,
473        pipe: NamedPipeServer,
474    ) -> Result<crate::server::Unbound, Error> {
475        #[cfg(windows)]
476        {
477            let (sender, receiver) = transport::windows::server_pipe(pipe, true)?;
478            negotiate_server(
479                transport::AnySender::Windows(sender),
480                transport::AnyReceiver::Windows(receiver),
481                self.limits,
482                self.app_protocol(),
483                self.server_auth(),
484            )
485            .await
486        }
487        #[cfg(all(docsrs, not(windows)))]
488        {
489            let _ = (self, pipe);
490            unreachable!()
491        }
492    }
493
494    #[cfg(any(windows, docsrs))]
495    #[cfg_attr(docsrs, doc(cfg(windows)))]
496    #[cfg_attr(all(docsrs, not(windows)), allow(private_interfaces))]
497    /// Creates a server on the client end of a Windows named pipe.
498    pub async fn server_named_pipe_client(
499        self,
500        pipe: NamedPipeClient,
501    ) -> Result<crate::server::Unbound, Error> {
502        #[cfg(windows)]
503        {
504            let (sender, receiver) = transport::windows::client_pipe(pipe, true)?;
505            negotiate_server(
506                transport::AnySender::Windows(sender),
507                transport::AnyReceiver::Windows(receiver),
508                self.limits,
509                self.app_protocol(),
510                self.server_auth(),
511            )
512            .await
513        }
514        #[cfg(all(docsrs, not(windows)))]
515        {
516            let _ = (self, pipe);
517            unreachable!()
518        }
519    }
520}
521
522async fn negotiate_client(
523    mut sender: transport::AnySender,
524    mut receiver: transport::AnyReceiver,
525    limits: Limits,
526    #[cfg(windows)] peer_process: Option<OwnedHandle>,
527    app_protocol: (&str, &[u16]),
528    auth: Option<Auth>,
529) -> Result<UnboundClient, Error> {
530    // The RPC framing version itself is an implementation detail,
531    // uninteresting once binding to `P` — only the application-protocol
532    // version negotiated below is surfaced.
533    let negotiated =
534        fragment::negotiate(&mut sender, &mut receiver, &limits, app_protocol, auth).await?;
535    receiver.set_max_handles_per_fragment(negotiated.limits.max_handles_per_fragment);
536    Ok(UnboundClient {
537        sender,
538        receiver,
539        limits: negotiated.limits,
540        #[cfg(windows)]
541        peer_process,
542        app_protocol: negotiated.app_protocol,
543    })
544}
545
546async fn negotiate_server(
547    mut sender: transport::AnySender,
548    mut receiver: transport::AnyReceiver,
549    limits: Limits,
550    app_protocol: (&str, &[u16]),
551    auth: Option<Auth>,
552) -> Result<UnboundServer, Error> {
553    // The RPC framing version itself is an implementation detail,
554    // uninteresting once binding to `P` — only the application-protocol
555    // version negotiated below is surfaced.
556    let negotiated =
557        fragment::negotiate(&mut sender, &mut receiver, &limits, app_protocol, auth).await?;
558    receiver.set_max_handles_per_fragment(negotiated.limits.max_handles_per_fragment);
559    Ok(UnboundServer {
560        sender,
561        receiver,
562        limits: negotiated.limits,
563        app_protocol: negotiated.app_protocol,
564    })
565}
566
567pub struct UnboundClient {
568    sender: transport::AnySender,
569    receiver: transport::AnyReceiver,
570    limits: Limits,
571    #[cfg(windows)]
572    peer_process: Option<OwnedHandle>,
573    app_protocol: (String, u16),
574}
575
576impl UnboundClient {
577    /// The negotiated application protocol name.
578    pub fn name(&self) -> &str {
579        &self.app_protocol.0
580    }
581
582    /// The negotiated application protocol version.
583    pub fn version(&self) -> u16 {
584        self.app_protocol.1
585    }
586
587    /// Consumes this endpoint and binds it to a concrete protocol type.
588    ///
589    /// The caller is responsible for choosing a `P` compatible with the
590    /// negotiated application protocol name and version.
591    pub fn bind<P: Protocol>(self) -> Client<P> {
592        Client::from_transport(
593            self.sender,
594            self.receiver,
595            self.limits,
596            #[cfg(windows)]
597            self.peer_process,
598        )
599    }
600}
601
602pub struct UnboundServer {
603    sender: transport::AnySender,
604    receiver: transport::AnyReceiver,
605    limits: Limits,
606    app_protocol: (String, u16),
607}
608
609impl UnboundServer {
610    /// The negotiated application protocol name.
611    pub fn name(&self) -> &str {
612        &self.app_protocol.0
613    }
614
615    /// The negotiated application protocol version.
616    pub fn version(&self) -> u16 {
617        self.app_protocol.1
618    }
619
620    /// Consumes this endpoint and binds it to a concrete protocol type.
621    ///
622    /// The caller is responsible for choosing a `P` compatible with the
623    /// negotiated application-protocol name and version.
624    pub fn bind<P: Protocol>(self) -> Server<P> {
625        Server::from_transport(self.sender, self.receiver, self.limits)
626    }
627}