tokio_quiche/buf_factory.rs
1// Copyright (C) 2025, Cloudflare, Inc.
2// All rights reserved.
3//
4// Redistribution and use in source and binary forms, with or without
5// modification, are permitted provided that the following conditions are
6// met:
7//
8// * Redistributions of source code must retain the above copyright notice,
9// this list of conditions and the following disclaimer.
10//
11// * Redistributions in binary form must reproduce the above copyright
12// notice, this list of conditions and the following disclaimer in the
13// documentation and/or other materials provided with the distribution.
14//
15// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
16// IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
17// THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
18// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
19// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
20// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
21// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
22// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
23// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
24// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
25// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
27//! Pooled buffers for zero-copy packet handling.
28//!
29//! tokio-quiche maintains multiple [`buffer_pool::Pool`] instances for the
30//! lifetime of the program. Buffers from those pools are used for received
31//! network packets and HTTP/3 data, which is passed directly to users of the
32//! crate. Outbound HTTP/3 data (like a message body or a datagram) is provided
33//! by users in the same format.
34//!
35//! [`BufFactory`] provides access to the crate's pools to create outbound
36//! buffers, but users can also use their own custom [`buffer_pool::Pool`]s.
37//! There are two types of built-in pools:
38//! - The generic buffer pool with very large buffers, which is used for stream
39//! data such as HTTP bodies.
40//! - The datagram pool, which retains buffers the size of a single UDP packet.
41
42use buffer_pool::ConsumeBuffer;
43use buffer_pool::Pool;
44use buffer_pool::Pooled;
45use datagram_socket::MAX_DATAGRAM_SIZE;
46
47const POOL_SHARDS: usize = 8;
48const POOL_SIZE: usize = 16 * 1024;
49const DATAGRAM_POOL_SIZE: usize = 64 * 1024;
50
51const TINY_BUF_SIZE: usize = 64;
52const SMALL_BUF_SIZE: usize = 1024;
53const MEDIUM_BUF_SIZE: usize = 4096;
54const MAX_POOL_BUF_SIZE: usize = 64 * 1024;
55
56type BufPool = Pool<POOL_SHARDS, ConsumeBuffer>;
57
58static TINY_POOL: BufPool =
59 BufPool::new(TINY_BUF_SIZE, TINY_BUF_SIZE, "tiny_pool");
60static SMALL_POOL: BufPool =
61 BufPool::new(SMALL_BUF_SIZE, SMALL_BUF_SIZE, "small_pool");
62static MEDIUM_POOL: BufPool =
63 BufPool::new(MEDIUM_BUF_SIZE, MEDIUM_BUF_SIZE, "medium_pool");
64
65/// A generic buffer pool used to pass data around without copying.
66static BUF_POOL: BufPool =
67 BufPool::new(POOL_SIZE, MAX_POOL_BUF_SIZE, "generic_pool");
68
69/// A datagram pool shared for both UDP streams, and incoming QUIC packets.
70static DATAGRAM_POOL: BufPool =
71 BufPool::new(DATAGRAM_POOL_SIZE, MAX_DATAGRAM_SIZE, "datagram_pool");
72
73/// A pooled byte buffer to pass stream data around without copying.
74pub type PooledBuf = Pooled<ConsumeBuffer>;
75/// A pooled byte buffer to pass datagrams around without copying.
76///
77/// The buffer type records a head offset, which allows cheaply inserting
78/// data at the front given sufficient capacity.
79pub type PooledDgram = Pooled<ConsumeBuffer>;
80
81#[cfg(feature = "zero-copy")]
82pub use self::zero_copy::QuicheBuf;
83
84/// Prefix size to reserve in a [`PooledDgram`]. Up to 8 bytes for the flow ID
85/// plus 1 byte for the flow context.
86const DGRAM_PREFIX: usize = 8 + 1;
87
88/// Handle to the crate's static buffer pools.
89#[derive(Default, Clone, Debug)]
90pub struct BufFactory;
91
92impl BufFactory {
93 /// The maximum size of the buffers in the generic pool. Larger buffers
94 /// will shrink to this size before returning to the pool.
95 pub const MAX_BUF_SIZE: usize = MAX_POOL_BUF_SIZE;
96 /// The maximum size of the buffers in the datagram pool.
97 pub const MAX_DGRAM_SIZE: usize = MAX_DATAGRAM_SIZE;
98
99 /// Creates an empty [`PooledBuf`] which is not taken from the pool. When
100 /// dropped, it may be assigned to the generic pool if no longer empty.
101 pub fn get_empty_buf() -> PooledBuf {
102 BUF_POOL.get_empty()
103 }
104
105 /// Creates an empty [`PooledDgram`] which is not taken from the pool. When
106 /// dropped, it may be assigned to the datagram pool if no longer empty.
107 pub fn get_empty_datagram() -> PooledDgram {
108 DATAGRAM_POOL.get_empty()
109 }
110
111 /// Fetches a `MAX_BUF_SIZE` sized [`PooledBuf`] from the generic pool.
112 pub fn get_max_buf() -> PooledBuf {
113 BUF_POOL.get_with(|d| d.expand(MAX_POOL_BUF_SIZE))
114 }
115
116 /// Fetches a `MAX_DATAGRAM_SIZE` sized [`PooledDgram`] from the datagram
117 /// pool.
118 pub fn get_max_datagram() -> PooledDgram {
119 DATAGRAM_POOL.get_with(|d| {
120 d.expand(MAX_DATAGRAM_SIZE);
121 // Make room to inject a prefix
122 d.pop_front(DGRAM_PREFIX);
123 })
124 }
125
126 /// Adds `dgram` to the datagram pool without copying it.
127 pub fn dgram_from_vec(dgram: Vec<u8>) -> PooledDgram {
128 DATAGRAM_POOL.from_owned(ConsumeBuffer::from_vec(dgram))
129 }
130
131 /// Fetches a [`PooledBuf`] from the generic pool and initializes it
132 /// with the contents of `slice`.
133 pub fn buf_from_slice(slice: &[u8]) -> PooledBuf {
134 #[allow(clippy::match_overlapping_arm)]
135 match slice.len() {
136 0 => TINY_POOL.get_empty(),
137 ..=TINY_BUF_SIZE => TINY_POOL.with_slice(slice),
138 ..=SMALL_BUF_SIZE => SMALL_POOL.with_slice(slice),
139 ..=MEDIUM_BUF_SIZE => MEDIUM_POOL.with_slice(slice),
140 _ => BUF_POOL.with_slice(slice),
141 }
142 }
143
144 /// Fetches a [`PooledDgram`] from the datagram pool and initializes it
145 /// with the contents of `slice`.
146 pub fn dgram_from_slice(slice: &[u8]) -> PooledDgram {
147 let mut dgram = Self::get_max_datagram();
148 dgram.truncate(0);
149 dgram.extend(slice);
150 dgram
151 }
152}
153
154#[cfg(feature = "zero-copy")]
155mod zero_copy {
156 use super::PooledBuf;
157 use quiche::BufSplit;
158
159 /// A pooled, splittable byte buffer for zero-copy [`quiche`] calls.
160 #[derive(Clone, Debug)]
161 pub struct QuicheBuf {
162 inner: triomphe::Arc<PooledBuf>,
163 start: usize,
164 end: usize,
165 }
166
167 impl QuicheBuf {
168 pub(crate) fn new(inner: PooledBuf) -> Self {
169 QuicheBuf {
170 start: 0,
171 end: inner.len(),
172 inner: triomphe::Arc::new(inner),
173 }
174 }
175 }
176
177 impl AsRef<[u8]> for QuicheBuf {
178 fn as_ref(&self) -> &[u8] {
179 &self.inner[self.start..self.end]
180 }
181 }
182
183 impl BufSplit for QuicheBuf {
184 fn split_at(&mut self, at: usize) -> Self {
185 assert!(self.start + at <= self.end);
186
187 let split = QuicheBuf {
188 inner: self.inner.clone(),
189 start: self.start + at,
190 end: self.end,
191 };
192
193 self.end = self.start + at;
194
195 split
196 }
197
198 fn try_add_prefix(&mut self, prefix: &[u8]) -> bool {
199 if self.start != 0 {
200 return false;
201 }
202
203 if let Some(unique) = triomphe::Arc::get_mut(&mut self.inner) {
204 if unique.add_prefix(prefix) {
205 self.end += prefix.len();
206 return true;
207 }
208 }
209
210 false
211 }
212 }
213
214 impl quiche::BufFactory for super::BufFactory {
215 type Buf = QuicheBuf;
216
217 fn buf_from_slice(buf: &[u8]) -> Self::Buf {
218 QuicheBuf::new(Self::buf_from_slice(buf))
219 }
220 }
221}