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}