aws_smithy_schema/schema/
extension.rs1use crate::Schema;
70use std::any::Any;
71use std::cell::Cell;
72use std::fmt;
73use std::marker::PhantomData;
74use std::sync::atomic::{AtomicUsize, Ordering};
75use std::sync::OnceLock;
76
77pub struct SchemaExtensionKey<T> {
93 slot: AtomicUsize,
95 provider: fn(&Schema<'_>) -> T,
96 _value: PhantomData<fn() -> T>,
97}
98
99impl<T> SchemaExtensionKey<T> {
100 pub const fn new(provider: fn(&Schema<'_>) -> T) -> Self {
102 Self {
103 slot: AtomicUsize::new(0),
104 provider,
105 _value: PhantomData,
106 }
107 }
108
109 #[inline]
110 fn slot(&self) -> usize {
111 match self.slot.load(Ordering::Relaxed) {
112 0 => self.assign_slot(),
113 encoded => encoded - 1,
114 }
115 }
116
117 #[cold]
118 fn assign_slot(&self) -> usize {
119 static NEXT_SLOT: AtomicUsize = AtomicUsize::new(0);
123 let candidate = NEXT_SLOT.fetch_add(1, Ordering::Relaxed) + 1;
124 match self
125 .slot
126 .compare_exchange(0, candidate, Ordering::Relaxed, Ordering::Relaxed)
127 {
128 Ok(_) => candidate - 1,
129 Err(existing) => existing - 1,
130 }
131 }
132}
133
134impl<T> fmt::Debug for SchemaExtensionKey<T> {
135 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
136 f.debug_struct("SchemaExtensionKey")
137 .field("value_type", &std::any::type_name::<T>())
138 .finish_non_exhaustive()
139 }
140}
141
142const CHUNK_LEN: usize = 8;
152
153type ExtensionCell = OnceLock<Box<dyn Any + Send + Sync>>;
154
155struct Chunk {
156 cells: [ExtensionCell; CHUNK_LEN],
157 next: OnceLock<Box<Chunk>>,
158}
159
160impl Chunk {
161 fn new() -> Box<Self> {
162 Box::new(Self {
163 cells: [const { OnceLock::new() }; CHUNK_LEN],
164 next: OnceLock::new(),
165 })
166 }
167}
168
169pub(crate) struct ExtensionStorage(OnceLock<Box<Chunk>>);
172
173impl ExtensionStorage {
174 pub(crate) const fn new() -> Self {
175 Self(OnceLock::new())
176 }
177
178 #[inline]
179 pub(crate) fn get_or_compute<'s, T: Send + Sync + 'static>(
180 &'s self,
181 schema: &Schema<'_>,
182 key: &SchemaExtensionKey<T>,
183 ) -> &'s T {
184 let mut slot = key.slot();
185 let mut chunk: &Chunk = self.0.get_or_init(Chunk::new);
186 while slot >= CHUNK_LEN {
187 chunk = chunk.next.get_or_init(Chunk::new);
188 slot -= CHUNK_LEN;
189 }
190 let cell = &chunk.cells[slot];
191 let value: &(dyn Any + Send + Sync) = match cell.get() {
192 Some(value) => &**value,
195 None => initialize(cell, schema, key),
196 };
197 value
198 .downcast_ref::<T>()
199 .expect("an extension slot is only ever filled by the key that owns it")
200 }
201}
202
203thread_local! {
204 static IN_PROVIDER: Cell<bool> = const { Cell::new(false) };
206}
207
208struct ProviderGuard;
210
211impl Drop for ProviderGuard {
212 fn drop(&mut self) {
213 IN_PROVIDER.with(|in_provider| in_provider.set(false));
214 }
215}
216
217#[cold]
225#[inline(never)]
226fn initialize<'c, T: Send + Sync + 'static>(
227 cell: &'c ExtensionCell,
228 schema: &Schema<'_>,
229 key: &SchemaExtensionKey<T>,
230) -> &'c (dyn Any + Send + Sync) {
231 if IN_PROVIDER.with(Cell::get) {
232 panic!(
233 "a schema extension provider requested the uncached extension `{}` for schema `{}`; \
234 providers must derive their value from the schema without requesting extensions",
235 std::any::type_name::<T>(),
236 schema.shape_id().as_str(),
237 );
238 }
239 &**cell.get_or_init(|| {
240 IN_PROVIDER.with(|in_provider| in_provider.set(true));
241 let _guard = ProviderGuard;
242 Box::new((key.provider)(schema))
243 })
244}
245
246impl fmt::Debug for ExtensionStorage {
247 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
248 f.write_str("..")
250 }
251}
252
253#[cfg(test)]
254mod tests {
255 use super::SchemaExtensionKey;
256 use crate::{shape_id, Schema, ShapeType};
257 use std::sync::atomic::{AtomicUsize, Ordering};
258
259 static CALLS: AtomicUsize = AtomicUsize::new(0);
260
261 static LEN: SchemaExtensionKey<usize> = SchemaExtensionKey::new(|schema| {
262 CALLS.fetch_add(1, Ordering::SeqCst);
263 schema.member_name().map_or(0, str::len)
264 });
265
266 static MEMBER: Schema<'static> =
267 Schema::new_member(shape_id!("ns", "S", "abc"), ShapeType::String, "abc", 0);
268
269 #[test]
270 fn computes_once_per_schema() {
271 let before = CALLS.load(Ordering::SeqCst);
272 assert_eq!(*MEMBER.extension(&LEN), 3);
273 assert_eq!(*MEMBER.extension(&LEN), 3);
274 assert_eq!(CALLS.load(Ordering::SeqCst) - before, 1);
275 }
276
277 #[test]
278 fn works_on_runtime_schemas() {
279 let name = String::from("runtime");
280 let id = crate::ShapeId::from_parts("ns#S$runtime", "ns", "S");
281 let schema = Schema::new_member(id, ShapeType::String, &name, 0);
282 assert_eq!(*schema.extension(&LEN), 7);
283 }
284
285 #[test]
286 fn values_are_dropped_with_their_schema() {
287 static DROPS: AtomicUsize = AtomicUsize::new(0);
288 struct Tracked;
289 impl Drop for Tracked {
290 fn drop(&mut self) {
291 DROPS.fetch_add(1, Ordering::SeqCst);
292 }
293 }
294 static KEY: SchemaExtensionKey<Tracked> = SchemaExtensionKey::new(|_| Tracked);
295
296 let schema = Schema::new(shape_id!("ns", "Dropped"), ShapeType::String);
297 let _ = schema.extension(&KEY);
298 assert_eq!(DROPS.load(Ordering::SeqCst), 0);
299 drop(schema);
300 assert_eq!(DROPS.load(Ordering::SeqCst), 1);
301 }
302
303 #[test]
304 fn a_panicking_provider_caches_nothing() {
305 static ATTEMPTS: AtomicUsize = AtomicUsize::new(0);
306 static KEY: SchemaExtensionKey<usize> = SchemaExtensionKey::new(|_| {
307 if ATTEMPTS.fetch_add(1, Ordering::SeqCst) == 0 {
308 panic!("first attempt fails");
309 }
310 7
311 });
312 static S: Schema<'static> = Schema::new(shape_id!("ns", "Panics"), ShapeType::String);
313
314 let first = std::panic::catch_unwind(|| *S.extension(&KEY));
315 assert!(first.is_err());
316 assert_eq!(*S.extension(&KEY), 7);
317 assert_eq!(*S.extension(&KEY), 7);
318 assert_eq!(ATTEMPTS.load(Ordering::SeqCst), 2);
319 }
320
321 #[test]
322 #[cfg(target_pointer_width = "64")]
323 fn chunk_size() {
324 assert_eq!(std::mem::size_of::<super::Chunk>(), 208);
327 }
328
329 #[test]
330 fn many_keys_spill_into_later_chunks() {
331 static KEYS: [SchemaExtensionKey<usize>; 9] = [
332 SchemaExtensionKey::new(|_| 0),
333 SchemaExtensionKey::new(|_| 1),
334 SchemaExtensionKey::new(|_| 2),
335 SchemaExtensionKey::new(|_| 3),
336 SchemaExtensionKey::new(|_| 4),
337 SchemaExtensionKey::new(|_| 5),
338 SchemaExtensionKey::new(|_| 6),
339 SchemaExtensionKey::new(|_| 7),
340 SchemaExtensionKey::new(|_| 8),
341 ];
342 static S: Schema<'static> = Schema::new(shape_id!("ns", "T"), ShapeType::String);
343 for (i, key) in KEYS.iter().enumerate() {
344 assert_eq!(*S.extension(key), i);
345 }
346 for (i, key) in KEYS.iter().enumerate().rev() {
347 assert_eq!(*S.extension(key), i);
348 }
349 }
350
351 #[test]
352 fn concurrent_first_access_computes_once() {
353 static N: AtomicUsize = AtomicUsize::new(0);
354 static KEY: SchemaExtensionKey<u64> = SchemaExtensionKey::new(|_| {
355 N.fetch_add(1, Ordering::SeqCst);
356 42
357 });
358 static S: Schema<'static> = Schema::new(shape_id!("ns", "U"), ShapeType::String);
359 std::thread::scope(|scope| {
360 for _ in 0..8 {
361 scope.spawn(|| assert_eq!(*S.extension(&KEY), 42));
362 }
363 });
364 assert_eq!(N.load(Ordering::SeqCst), 1);
365 }
366
367 fn panic_message(f: impl FnOnce() + std::panic::UnwindSafe) -> String {
369 let payload = std::panic::catch_unwind(f).expect_err("expected a panic");
370 payload
371 .downcast_ref::<String>()
372 .cloned()
373 .or_else(|| payload.downcast_ref::<&str>().map(|s| s.to_string()))
374 .unwrap_or_default()
375 }
376
377 #[test]
378 fn a_provider_requesting_its_own_key_panics() {
379 static KEY: SchemaExtensionKey<u8> =
380 SchemaExtensionKey::new(|schema| *schema.extension(&KEY));
381 static S: Schema<'static> = Schema::new(shape_id!("ns", "SelfRef"), ShapeType::String);
382
383 let message = panic_message(|| {
384 let _ = S.extension(&KEY);
385 });
386 assert!(message.contains("uncached extension"), "{message}");
387 assert!(message.contains("ns#SelfRef"), "{message}");
388 let message = panic_message(|| {
390 let _ = S.extension(&KEY);
391 });
392 assert!(message.contains("uncached extension"), "{message}");
393 }
394
395 #[test]
398 fn a_recursive_provider_over_a_recursive_schema_panics() {
399 use crate::prelude;
400
401 static AV_S: Schema<'static> = Schema::new_member(
402 shape_id!("com.amazonaws.dynamodb", "AttributeValue", "S"),
403 ShapeType::String,
404 "S",
405 0,
406 );
407 static AV_L: Schema<'static> = Schema::new_member(
408 shape_id!("com.amazonaws.dynamodb", "AttributeValue", "L"),
409 ShapeType::List,
410 "L",
411 1,
412 )
413 .with_list_member(&ATTRIBUTE_VALUE);
414 static AV_M: Schema<'static> = Schema::new_member(
415 shape_id!("com.amazonaws.dynamodb", "AttributeValue", "M"),
416 ShapeType::Map,
417 "M",
418 2,
419 )
420 .with_map_members(&prelude::STRING, &ATTRIBUTE_VALUE);
421 static ATTRIBUTE_VALUE: Schema<'static> = Schema::new_struct(
422 shape_id!("com.amazonaws.dynamodb", "AttributeValue"),
423 ShapeType::Union,
424 &[&AV_S, &AV_L, &AV_M],
425 );
426 static CONTAINS_SENSITIVE: SchemaExtensionKey<bool> = SchemaExtensionKey::new(|schema| {
427 schema.sensitive().is_some()
428 || schema
429 .members()
430 .iter()
431 .any(|m| *m.extension(&CONTAINS_SENSITIVE))
432 || schema
433 .member()
434 .is_some_and(|t| *t.extension(&CONTAINS_SENSITIVE))
435 || schema
436 .key()
437 .is_some_and(|t| *t.extension(&CONTAINS_SENSITIVE))
438 });
439
440 let message = panic_message(|| {
441 let _ = ATTRIBUTE_VALUE.extension(&CONTAINS_SENSITIVE);
442 });
443 assert!(message.contains("uncached extension"), "{message}");
444 }
445
446 #[test]
447 fn a_cycle_through_two_keys_panics() {
448 static A: SchemaExtensionKey<u8> = SchemaExtensionKey::new(|schema| *schema.extension(&B));
449 static B: SchemaExtensionKey<u8> = SchemaExtensionKey::new(|schema| *schema.extension(&A));
450 static S: Schema<'static> = Schema::new(shape_id!("ns", "TwoKeys"), ShapeType::String);
451
452 let message = panic_message(|| {
453 let _ = S.extension(&A);
454 });
455 assert!(message.contains("uncached extension"), "{message}");
456 }
457
458 #[test]
461 fn a_cycle_across_threads_panics_instead_of_deadlocking() {
462 use std::sync::Barrier;
463
464 static BOTH_IN_PROVIDERS: Barrier = Barrier::new(2);
465 static S1: Schema<'static> = Schema::new(shape_id!("ns", "Thread1"), ShapeType::String);
466 static S2: Schema<'static> = Schema::new(shape_id!("ns", "Thread2"), ShapeType::String);
467 static A: SchemaExtensionKey<u8> = SchemaExtensionKey::new(|_| {
468 BOTH_IN_PROVIDERS.wait();
469 *S2.extension(&B)
470 });
471 static B: SchemaExtensionKey<u8> = SchemaExtensionKey::new(|_| {
472 BOTH_IN_PROVIDERS.wait();
473 *S1.extension(&A)
474 });
475
476 std::thread::scope(|scope| {
477 let first = scope.spawn(|| *S1.extension(&A));
478 let second = scope.spawn(|| *S2.extension(&B));
479 assert!(first.join().is_err());
480 assert!(second.join().is_err());
481 });
482 }
483
484 #[test]
488 fn a_provider_may_read_an_already_cached_extension() {
489 static BASE: SchemaExtensionKey<u8> = SchemaExtensionKey::new(|_| 2);
490 static DERIVED: SchemaExtensionKey<u8> =
491 SchemaExtensionKey::new(|schema| *schema.extension(&BASE) * 3);
492 static S: Schema<'static> = Schema::new(shape_id!("ns", "Cached"), ShapeType::String);
493
494 assert_eq!(*S.extension(&BASE), 2);
495 assert_eq!(*S.extension(&DERIVED), 6);
496 }
497
498 #[test]
499 fn a_panicking_provider_clears_the_guard() {
500 static FAILS: SchemaExtensionKey<u8> =
501 SchemaExtensionKey::new(|_| panic!("provider failed"));
502 static OK: SchemaExtensionKey<u8> = SchemaExtensionKey::new(|_| 5);
503 static S: Schema<'static> = Schema::new(shape_id!("ns", "Cleared"), ShapeType::String);
504
505 assert_eq!(
506 panic_message(|| {
507 let _ = S.extension(&FAILS);
508 }),
509 "provider failed"
510 );
511 assert_eq!(*S.extension(&OK), 5);
513 }
514}