Skip to main content

stable_type_layout/
lib.rs

1//! Layout descriptors for struct or enum types with an inventory registry.
2//!
3//! [`TypeLayout`] is derivable and exposes a `const` [`TypeLayoutInfo`] holding a type's name, size, minimum alignment,
4//! field offsets and enum variant discriminants.
5//!
6//! ```
7//! use stable_type_layout::{Field, TypeLayout, TypeStructure};
8//!
9//! #[derive(TypeLayout)]
10//! #[repr(C)]
11//! struct Foo {
12//!     a: u8,
13//!     b: u32,
14//! }
15//!
16//! const LAYOUT: stable_type_layout::TypeLayoutInfo = <Foo as TypeLayout>::TYPE_LAYOUT;
17//! assert_eq!(LAYOUT.name, "Foo");
18//! assert_eq!((LAYOUT.size, LAYOUT.align), (8, 4));
19//! assert_eq!(
20//!     LAYOUT.structure,
21//!     TypeStructure::Struct { fields: &[Field { name: "a", offset: 0 }, Field { name: "b", offset: 4 }] }
22//! );
23//! ```
24//!
25//! Types can additionally [`register!`] themselves into a crate-wide [`inventory`] registry, which can be used for
26//! snapshot testing, so a field, variant reorder, or a size change can be caught.
27
28extern crate self as stable_type_layout;
29
30pub use inventory;
31pub use stable_type_layout_derive::TypeLayout;
32
33/// A field's name and byte offset within its type.
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub struct Field {
36	pub name: &'static str,
37	pub offset: usize,
38}
39
40/// An enum variant's name, declaration index and discriminant.
41///
42/// `discriminant` is the compiler-assigned tag value, recorded only for fieldless enums where it can be read on stable
43/// via an `as` cast. Data-carrying variants record `None`; their `index` still pins declaration order, which is what a
44/// `#[repr(C, uN)]` tag is derived from.
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub struct Variant {
47	pub name: &'static str,
48	pub index: usize,
49	pub discriminant: Option<i64>,
50}
51
52/// Whether a type is a struct, union or enum, and the fields or variants it holds.
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub enum TypeStructure {
55	Struct { fields: &'static [Field] },
56	Union { fields: &'static [Field] },
57	Enum { variants: &'static [Variant] },
58}
59
60/// The concrete memory layout of a type: its name, size, minimum alignment and
61/// [`TypeStructure`].
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63pub struct TypeLayoutInfo {
64	pub name: &'static str,
65	pub size: usize,
66	pub align: usize,
67	pub structure: TypeStructure,
68}
69
70/// A type whose memory layout is known at compile time.
71pub trait TypeLayout: Sized {
72	const TYPE_LAYOUT: TypeLayoutInfo;
73}
74
75inventory::collect!(TypeLayoutInfo);
76
77/// Registers a type's [`TypeLayoutInfo`] with the registry read by [`all`] and [`render`].
78///
79/// ```
80/// use stable_type_layout::TypeLayout;
81///
82/// #[derive(TypeLayout)]
83/// #[repr(C)]
84/// struct Registered(u32);
85///
86/// stable_type_layout::register!(Registered);
87/// assert!(stable_type_layout::all().iter().any(|info| info.name == "Registered"));
88/// ```
89#[macro_export]
90macro_rules! register {
91	($ty:ty) => {
92		$crate::inventory::submit! { <$ty as $crate::TypeLayout>::TYPE_LAYOUT }
93	};
94}
95
96/// Every registered [`TypeLayoutInfo`], sorted by type name (deterministic order for snapshotting).
97pub fn all() -> Vec<&'static TypeLayoutInfo> {
98	let mut infos: Vec<&'static TypeLayoutInfo> = inventory::iter::<TypeLayoutInfo>.into_iter().collect();
99	infos.sort_by_key(|info| info.name);
100	infos
101}
102
103/// Render every registered layout as a stable, human-readable string for snapshot assertions.
104pub fn render() -> String {
105	let mut out = String::new();
106	for info in all() {
107		out.push_str(info.name);
108		out.push_str(&format!(": size={} align={}", info.size, info.align));
109		let fields = match info.structure {
110			TypeStructure::Struct { fields } | TypeStructure::Union { fields } => fields,
111			TypeStructure::Enum { variants } => {
112				for variant in variants {
113					match variant.discriminant {
114						Some(discriminant) => {
115							out.push_str(&format!("\n    {}: {} = {}", variant.index, variant.name, discriminant))
116						}
117						None => out.push_str(&format!("\n    {}: {}", variant.index, variant.name)),
118					}
119				}
120				&[]
121			}
122		};
123		if fields.is_empty() {
124			out.push('\n');
125		} else {
126			out.push_str(" {\n");
127			for field in fields {
128				out.push_str(&format!("    {}: {}\n", field.name, field.offset));
129			}
130			out.push_str("}\n");
131		}
132	}
133	out
134}
135
136#[cfg(test)]
137mod tests {
138	use super::*;
139
140	#[derive(TypeLayout)]
141	#[repr(C)]
142	struct SmallFirst {
143		small: u8,
144		large: u64,
145	}
146
147	#[derive(TypeLayout)]
148	#[repr(C)]
149	struct LargeFirst {
150		large: u64,
151		small: u8,
152	}
153
154	#[derive(TypeLayout)]
155	#[repr(u8)]
156	enum Fieldless {
157		Zero,
158		Seven = 7,
159		Eight,
160	}
161
162	#[allow(dead_code)]
163	#[derive(TypeLayout)]
164	#[repr(C, u8)]
165	enum DataCarrying {
166		Unit,
167		Payload(u32),
168	}
169
170	#[derive(TypeLayout)]
171	#[repr(C)]
172	struct Tuple<'a>(&'a str, u8);
173
174	#[derive(TypeLayout)]
175	#[repr(C)]
176	struct Unit;
177
178	register!(Fieldless);
179	register!(Tuple<'static>);
180	register!(Unit);
181
182	fn fields_of(info: TypeLayoutInfo) -> &'static [Field] {
183		match info.structure {
184			TypeStructure::Struct { fields } | TypeStructure::Union { fields } => fields,
185			TypeStructure::Enum { .. } => panic!("not a struct"),
186		}
187	}
188
189	#[test]
190	fn reorder_changes_field_offsets() {
191		let small_first = fields_of(SmallFirst::TYPE_LAYOUT);
192		let large_first = fields_of(LargeFirst::TYPE_LAYOUT);
193		assert_eq!(small_first, &[Field { name: "small", offset: 0 }, Field { name: "large", offset: 8 }]);
194		assert_eq!(large_first, &[Field { name: "large", offset: 0 }, Field { name: "small", offset: 8 }]);
195	}
196
197	#[test]
198	fn records_explicit_and_implicit_discriminants() {
199		assert_eq!(
200			Fieldless::TYPE_LAYOUT.structure,
201			TypeStructure::Enum {
202				variants: &[
203					Variant { name: "Zero", index: 0, discriminant: Some(0) },
204					Variant { name: "Seven", index: 1, discriminant: Some(7) },
205					Variant { name: "Eight", index: 2, discriminant: Some(8) },
206				]
207			}
208		);
209	}
210
211	#[test]
212	fn data_carrying_variants_pin_index_only() {
213		assert_eq!(
214			DataCarrying::TYPE_LAYOUT.structure,
215			TypeStructure::Enum {
216				variants: &[
217					Variant { name: "Unit", index: 0, discriminant: None },
218					Variant { name: "Payload", index: 1, discriminant: None },
219				]
220			}
221		);
222	}
223
224	#[test]
225	fn tuple_fields_are_named_by_index() {
226		assert_eq!(fields_of(Tuple::TYPE_LAYOUT), &[Field { name: "0", offset: 0 }, Field { name: "1", offset: 16 }]);
227	}
228
229	#[test]
230	fn renders_every_registered_type() {
231		assert_eq!(
232			render(),
233			"Fieldless: size=1 align=1\n    0: Zero = 0\n    1: Seven = 7\n    2: Eight = 8\nTuple: size=24 align=8 {\n    0: 0\n    1: 16\n}\nUnit: size=0 align=1\n"
234		);
235	}
236}