tor_memquota_cost/memory_cost_derive.rs
1//! Deriving `HasMemoryCost`
2
3use crate::{EnabledToken, HasMemoryCost};
4use derive_deftly::define_derive_deftly;
5use itertools::chain;
6use paste::paste;
7
8//---------- main public items ----------
9
10/// Types whose `HasMemoryCost` is derived structurally
11///
12/// Usually implemented using
13/// [`#[derive_deftly(HasMemoryCost)]`](crate::derive_deftly_template_HasMemoryCost).
14///
15/// For `Copy` types, it can also be implemented with
16/// `memory_cost_structural_copy!`.
17///
18/// When this trait is implemented, a blanket impl provides [`HasMemoryCost`].
19///
20/// ### Structural memory cost
21///
22/// We call the memory cost "structural"
23/// when it is derived from the type's structure.
24///
25/// The memory cost of a `HasMemoryCostStructural` type is:
26///
27/// - The number of bytes in its own size [`size_of`]; plus
28///
29/// - The (structural) memory cost of all the out-of-line data that it owns;
30/// that's what's returned by
31/// [`indirect_memory_cost`](HasMemoryCostStructural::indirect_memory_cost)
32///
33/// For example, `String`s out-of-line memory cost is just its capacity,
34/// so its memory cost is the size of its three word layout plus its capacity.
35///
36/// This calculation is performed by the blanket impl of `HasMemoryCost`.
37///
38/// ### Shared data - non-`'static` types, `Arc`
39///
40/// It is probably a mistake to implement this trait (or `HasMemoryCost`)
41/// for types with out-of-line data that they don't exclusively own.
42/// After all, the memory cost must be known and fixed,
43/// and if there is shared data it's not clear how it should be accounted.
44pub trait HasMemoryCostStructural {
45 /// Memory cost of data stored out-of-line
46 ///
47 /// The total memory cost is the cost of the layout of `self` plus this.
48 fn indirect_memory_cost(&self, _: EnabledToken) -> usize;
49}
50
51/// Compile-time check for `Copy + 'static` - helper for macros
52///
53/// Used by `#[deftly(has_memory_cost(copy))]`
54/// and `memory_cost_structural_copy!`
55/// to check that the type really is suitable.
56pub fn assert_copy_static<T: Copy + 'static>(_: &T) {}
57
58impl<T: HasMemoryCostStructural> HasMemoryCost for T {
59 fn memory_cost(&self, et: EnabledToken) -> usize {
60 size_of::<T>() //
61 .saturating_add(
62 //
63 <T as HasMemoryCostStructural>::indirect_memory_cost(self, et),
64 )
65 }
66}
67
68//---------- specific implementations ----------
69
70/// Implement [`HasMemoryCostStructural`] for `Copy` types
71///
72/// The [`indirect_memory_cost`](HasMemoryCostStructural::indirect_memory_cost)
73/// of a `Copy + 'static` type is zero.
74///
75/// This macro implements that.
76///
77/// Ideally, we would `impl <T: Copy + 'static> MemoryCostStructural for T`.
78/// But that falls foul of trait coherence rules.
79/// So instead we provide `memory_cost_structural_copy!`
80/// and the `#[deftly(has_memory_cost(copy))]` attribute.
81///
82/// This macro can only be used within `tor-memquota`, or for types local to your crate.
83/// For other types, use `#[deftly(has_memory_cost(copy))]` on each field of that type.
84//
85// Unfortunately we can't provide a blanket impl of `HasMemoryCostStructural`
86// for all `Copy` types, because we want to provide `HasMemoryCostStructural`
87// for `Vec` and `Box` -
88// and rustic thinks that those might become `Copy` in the future.
89#[macro_export]
90macro_rules! memory_cost_structural_copy { { $($ty:ty),* $(,)? } => { $(
91 impl $crate::HasMemoryCostStructural for $ty {
92 fn indirect_memory_cost(&self, _et: $crate::EnabledToken) -> usize {
93 $crate::assert_copy_static::<$ty>(self);
94 0
95 }
96 }
97)* } }
98
99memory_cost_structural_copy! {
100 u8, u16, u32, u64, usize,
101 i8, i16, i32, i64, isize,
102 // Generic NonZero<T> impl isn't possible, so we use qualified types. See:
103 // https://github.com/rust-lang/rust/issues/142966
104 std::num::NonZeroU8, std::num::NonZeroU16, std::num::NonZeroU32, std::num::NonZeroU64,
105 std::num::NonZeroI8, std::num::NonZeroI16, std::num::NonZeroI32, std::num::NonZeroI64,
106 std::num::NonZeroUsize,
107 std::num::NonZeroIsize,
108 std::net::IpAddr, std::net::Ipv4Addr, std::net::Ipv6Addr,
109}
110
111/// Implement HasMemoryCost for tuples
112macro_rules! memory_cost_structural_tuples { {
113 // Recursive case: do base case for this input, and then the next inputs
114 $($T:ident)* - $U0:ident $($UN:ident)*
115} => {
116 memory_cost_structural_tuples! { $($T)* - }
117 memory_cost_structural_tuples! { $($T)* $U0 - $($UN)* }
118}; {
119 // Base case, implement for the tuple with contents types $T
120 $($T:ident)* -
121} => { paste! {
122 impl < $(
123 $T: HasMemoryCostStructural,
124 )* > HasMemoryCostStructural for ( $(
125 $T,
126 )* ) {
127 fn indirect_memory_cost(&self, #[allow(unused)] et: EnabledToken) -> usize {
128 let ( $(
129 [< $T:lower >],
130 )* ) = self;
131 0_usize $(
132 .saturating_add([< $T:lower >].indirect_memory_cost(et))
133 )*
134 }
135 }
136} } }
137memory_cost_structural_tuples! { - A B C D E F G H I J K L M N O P Q R S T U V W X Y Z }
138
139impl<T: HasMemoryCostStructural> HasMemoryCostStructural for Option<T> {
140 fn indirect_memory_cost(&self, et: EnabledToken) -> usize {
141 if let Some(t) = self {
142 <T as HasMemoryCostStructural>::indirect_memory_cost(t, et)
143 } else {
144 0
145 }
146 }
147}
148
149impl<T: HasMemoryCostStructural, const N: usize> HasMemoryCostStructural for [T; N] {
150 fn indirect_memory_cost(&self, et: EnabledToken) -> usize {
151 self.iter()
152 .map(|t| t.indirect_memory_cost(et))
153 .fold(0, usize::saturating_add)
154 }
155}
156
157impl<T: HasMemoryCostStructural> HasMemoryCostStructural for Box<T> {
158 fn indirect_memory_cost(&self, et: EnabledToken) -> usize {
159 <T as HasMemoryCost>::memory_cost(&**self, et)
160 }
161}
162
163impl<T: HasMemoryCostStructural> HasMemoryCostStructural for Vec<T> {
164 fn indirect_memory_cost(&self, et: EnabledToken) -> usize {
165 chain!(
166 [size_of::<T>().saturating_mul(self.capacity())],
167 self.iter().map(|t| t.indirect_memory_cost(et)),
168 )
169 .fold(0, usize::saturating_add)
170 }
171}
172
173impl HasMemoryCostStructural for String {
174 fn indirect_memory_cost(&self, _et: EnabledToken) -> usize {
175 self.capacity()
176 }
177}
178
179//------------------- derive macro ----------
180
181define_derive_deftly! {
182 /// Derive `HasMemoryCost`
183 ///
184 /// Each field must implement [`HasMemoryCostStructural`].
185 ///
186 /// Valid for structs and enums.
187 ///
188 /// ### Top-level attributes
189 ///
190 /// * **`#[deftly(has_memory_cost(bounds = "BOUNDS"))]`**:
191 /// Additional bounds to apply to the implementation.
192 ///
193 /// ### Field attributes
194 ///
195 /// * **`#[deftly(has_memory_cost(copy))]`**:
196 /// This field is `Copy + 'static` so does not reference any data that should be accounted.
197 /// * **`#[deftly(has_memory_cost(indirect_fn = "FUNCTION"))]`**:
198 /// `FUNCTION` is a function with the signature and semantics of
199 /// [`HasMemoryCostStructural::indirect_memory_cost`],
200 /// * **`#[deftly(has_memory_cost(indirect_size = "EXPR"))]`**:
201 /// `EXPR` is an expression of type usize with the semantics of a return value from
202 /// [`HasMemoryCostStructural::indirect_memory_cost`].
203 ///
204 /// With one of these, the field doesn't need to implement `HasMemoryCostStructural`.
205 ///
206 /// # Example
207 ///
208 /// ```
209 /// use derive_deftly::Deftly;
210 /// use std::mem::size_of;
211 /// use tor_memquota_cost::{HasMemoryCost, HasMemoryCostStructural};
212 /// use tor_memquota_cost::derive_deftly_template_HasMemoryCost;
213 ///
214 /// #[derive(Deftly)]
215 /// #[derive_deftly(HasMemoryCost)]
216 /// #[deftly(has_memory_cost(bounds = "Data: HasMemoryCostStructural"))]
217 /// struct Struct<Data> {
218 /// data: Data,
219 ///
220 /// #[deftly(has_memory_cost(indirect_size = "0"))] // this is a good guess
221 /// num: serde_json::Number,
222 ///
223 /// #[deftly(has_memory_cost(copy))]
224 /// msg: &'static str,
225 ///
226 /// #[deftly(has_memory_cost(indirect_fn = "|info, _et| String::capacity(info)"))]
227 /// info: safelog::Sensitive<String>,
228 /// }
229 ///
230 /// let s = Struct {
231 /// data: String::with_capacity(33),
232 /// num: serde_json::Number::from_f64(0.0).unwrap(),
233 /// msg: "hello",
234 /// info: String::with_capacity(12).into(),
235 /// };
236 ///
237 /// let Some(et) = tor_memquota_cost::EnabledToken::new_if_compiled_in() else { return };
238 ///
239 /// assert_eq!(
240 /// s.memory_cost(et),
241 /// size_of::<Struct<String>>() + 33 + 12,
242 /// );
243 /// ```
244 export HasMemoryCost expect items:
245
246 impl<$tgens> $crate::HasMemoryCostStructural for $ttype
247 where $twheres ${if tmeta(has_memory_cost(bounds)) {
248 ${tmeta(has_memory_cost(bounds)) as token_stream}
249 }}
250 {
251 fn indirect_memory_cost(&self, #[allow(unused)] et: $crate::EnabledToken) -> usize {
252 ${define F_INDIRECT_COST {
253 ${select1
254 fmeta(has_memory_cost(copy)) {
255 {
256 $crate::assert_copy_static::<$ftype>(&$fpatname);
257 0
258 }
259 }
260 fmeta(has_memory_cost(indirect_fn)) {
261 ${fmeta(has_memory_cost(indirect_fn)) as expr}(&$fpatname, et)
262 }
263 fmeta(has_memory_cost(indirect_size)) {
264 ${fmeta(has_memory_cost(indirect_size)) as expr}
265 }
266 else {
267 <$ftype as $crate::HasMemoryCostStructural>::indirect_memory_cost(&$fpatname, et)
268 }
269 }
270 }}
271
272 match self {
273 $(
274 $vpat => {
275 0_usize
276 ${for fields {
277 .saturating_add( $F_INDIRECT_COST )
278 }}
279 }
280 )
281 }
282 }
283 }
284}
285
286#[cfg(all(test, feature = "memquota"))]
287mod test {
288 // @@ begin test lint list maintained by maint/add_warning @@
289 #![allow(clippy::bool_assert_comparison)]
290 #![allow(clippy::clone_on_copy)]
291 #![allow(clippy::dbg_macro)]
292 #![allow(clippy::mixed_attributes_style)]
293 #![allow(clippy::print_stderr)]
294 #![allow(clippy::print_stdout)]
295 #![allow(clippy::single_char_pattern)]
296 #![allow(clippy::unwrap_used)]
297 #![allow(clippy::unchecked_time_subtraction)]
298 #![allow(clippy::useless_vec)]
299 #![allow(clippy::needless_pass_by_value)]
300 //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
301 #![allow(clippy::arithmetic_side_effects)] // don't mind potential panicking ops in tests
302
303 use super::*;
304 use derive_deftly::Deftly;
305
306 #[derive(Deftly)]
307 #[derive_deftly(HasMemoryCost)]
308 enum E {
309 U(usize),
310 B(Box<u32>),
311 }
312
313 #[derive(Deftly, Default)]
314 #[derive_deftly(HasMemoryCost)]
315 struct S {
316 u: usize,
317 b: Box<u32>,
318 v: Vec<u32>,
319 ev: Vec<E>,
320 }
321
322 const ET: EnabledToken = EnabledToken::new();
323
324 // The size of a u32 is always 4 bytes, so we just write "4" rather than u32::SIZE.
325
326 #[test]
327 fn structs() {
328 assert_eq!(S::default().memory_cost(ET), size_of::<S>() + 4);
329 assert_eq!(E::U(0).memory_cost(ET), size_of::<E>());
330 assert_eq!(E::B(Box::default()).memory_cost(ET), size_of::<E>() + 4);
331 }
332
333 #[test]
334 fn values() {
335 let mut v: Vec<u32> = Vec::with_capacity(10);
336 v.push(1);
337
338 let s = S {
339 u: 0,
340 b: Box::new(42),
341 v,
342 ev: vec![],
343 };
344
345 assert_eq!(
346 s.memory_cost(ET),
347 size_of::<S>() + 4 /* b */ + 10 * 4, /* v buffer */
348 );
349 }
350
351 #[test]
352 #[allow(clippy::identity_op)]
353 fn nest() {
354 let mut ev = Vec::with_capacity(10);
355 ev.push(E::U(42));
356 ev.push(E::B(Box::new(42)));
357
358 let s = S { ev, ..S::default() };
359
360 assert_eq!(
361 s.memory_cost(ET),
362 size_of::<S>() + 4 /* b */ + 0 /* v */ + size_of::<E>() * 10 /* ev buffer */ + 4 /* E::B */
363 );
364 }
365}