compact_debug/
lib.rs

1#![feature(fmt_helpers_for_derive)]
2//! `{:#?}` formatting, and the `dbg!()` macro, sound nice on paper. But once you try using them...
3//!
4//! ```text
5//! Goto(
6//!     Address(
7//!         30016,
8//!     ),
9//! ),
10//! Label(
11//!     Address(
12//!         29990,
13//!     ),
14//! ),
15//! Expr(
16//!     Expr(
17//!         Expr(
18//!             [
19//!                 Var(
20//!                     0,
21//!                 ),
22//!                 Const(
23//!                     0,
24//!                 ),
25//!                 Op(
26//!                     Ne,
27//!                 ),
28//!             ],
29//!         ),
30//!     ),
31//!     Address(
32//!         30016,
33//!     ),
34//! ),
35//! ```
36//!
37//! Your dreams of nice and readable output are shattered by a chunk of output more porous than cotton
38//! candy, with approximately two tokens of content on each line. Screenful upon screenful of vacuous
39//! output for even a moderately complex type. Upset, you reluctantly replace your derived `Debug`
40//! implementation with a manual one that eschews `DebugTuple` in favor of `write_str`. However, this
41//! results in a catastrophic amount of boilerplate code, and doesn't affect types outside of your
42//! control, like the ubiquitous `Option`.
43//!
44//! That's where this crate comes in. It monkey-patches the pretty-printing machinery so that
45//! `DebugTuple` is printed on a single line regardless of `#` flag. The above snippet is printed as:
46//!
47//! ```text
48//! Goto(Address(30016)),
49//! Label(Address(29990)),
50//! Expr(Expr(Expr([
51//!     Var(0),
52//!     Const(0),
53//!     Op(Ne),
54//! ])), Address(30016)),
55//! ```
56//!
57//! In addition, it also patches slices of primitive types, `&str`, and `String` to print on a single line.
58//!
59//! This crate currently only supports x86_64 architecture, and requires nightly.
60
61use std::sync::{LazyLock, OnceLock};
62
63#[cfg(not(target_arch = "x86_64"))]
64compile_error!("only supported on x86_64");
65
66struct Pos(*const u8);
67unsafe impl Send for Pos {}
68unsafe impl Sync for Pos {}
69static MATCHES: OnceLock<Vec<Pos>> = OnceLock::new();
70
71macro_rules! patch_slice {
72	($ty:ty, $on:expr, $check:literal) => { {
73		static DETOUR: LazyLock<retour::RawDetour> = LazyLock::new(|| unsafe {
74			type Hook = for<'a, 'b, 'c> fn(&'a [$ty], &'b mut std::fmt::Formatter<'c>) -> std::fmt::Result;
75			#[cfg($check)]
76			#[expect(unused)]
77			static ORIG_FUNC: Hook = <[$ty] as std::fmt::Debug>::fmt;
78			static HOOK: Hook = |elf, f| {
79				write!(f, "[")?;
80				for (i, v) in elf.iter().enumerate() {
81					if i != 0 {
82						write!(f, ", ")?;
83					}
84					std::fmt::Debug::fmt(v, f)?;
85				}
86				write!(f, "]")
87			};
88			retour::RawDetour::new(<[$ty] as std::fmt::Debug>::fmt as *const (), HOOK as *const ()).unwrap()
89		});
90		if $on {
91			DETOUR.enable().unwrap();
92		} else {
93			DETOUR.disable().unwrap();
94		}
95	} }
96}
97
98/// Enables or disables the patch.
99///
100/// # Panics
101/// Panics if the function does not look like expected, which is most likely to happen if `std`
102/// changes something internally, or if the compiler finds a better way to optimize it.
103///
104/// # Safety
105/// Aside from the whole concept being inherently unsafe, this will probably have unexpected
106/// consequences if called in multi-threaded contexts.
107pub unsafe fn enable(on: bool) {
108	unsafe {
109		let matches = MATCHES.get_or_init(find_all);
110
111		for Pos(ptr) in matches {
112			let ptr = ptr.cast_mut();
113			let _prot = region::protect_with_handle(ptr, 1, region::Protection::READ_WRITE_EXECUTE)
114				.unwrap();
115			ptr.write(if on { 0 } else { 0x80 });
116		}
117
118		patch_slice!(u8, on, true);
119		patch_slice!(u16, on, true);
120		patch_slice!(u32, on, true);
121		patch_slice!(u64, on, true);
122		patch_slice!(u128, on, true);
123		patch_slice!(usize, on, true);
124		patch_slice!(i8, on, true);
125		patch_slice!(i16, on, true);
126		patch_slice!(i32, on, true);
127		patch_slice!(i64, on, true);
128		patch_slice!(i128, on, true);
129		patch_slice!(isize, on, true);
130		patch_slice!(f32, on, true);
131		patch_slice!(f64, on, true);
132		patch_slice!(bool, on, true);
133		patch_slice!(char, on, true);
134		patch_slice!(&str, on, false); // can't get the lifetimes to work out, so skipping that check
135		patch_slice!(String, on, true);
136	}
137}
138
139fn find_all() -> Vec<Pos> {
140	unsafe {
141		let mut out = Vec::new();
142		macro_rules! find {
143			($name:path) => {
144				do_find(&mut out, stringify!($name), $name as *const () as *const u8);
145			};
146		}
147		find!(std::fmt::DebugTuple::field);
148		find!(std::fmt::DebugTuple::finish);
149		find!(std::fmt::DebugTuple::finish_non_exhaustive);
150		find!(std::fmt::Formatter::debug_tuple_field1_finish);
151		find!(std::fmt::Formatter::debug_tuple_field2_finish);
152		find!(std::fmt::Formatter::debug_tuple_field3_finish);
153		find!(std::fmt::Formatter::debug_tuple_field4_finish);
154		find!(std::fmt::Formatter::debug_tuple_field5_finish);
155		find!(std::fmt::Formatter::debug_tuple_fields_finish);
156
157		// Check that all the field offsets are the same
158		assert!(out.iter().all(|x| x.0 == out[0].0), "field offsets differ");
159
160		out.into_iter().map(|x| Pos(x.1)).collect()
161	}
162}
163
164// Find pattern f6 4x ?? 04, and record the value of the ?? and the position of the 04.
165// End when we find a C3 (ret) or CC (int3) on a position that ends with 0xF.
166unsafe fn do_find(out: &mut Vec<(u8, *const u8)>, name: &str, mut ptr: *const u8) {
167	let n = out.len();
168	loop {
169		if (ptr as usize & 0xF) == 0xF && (*ptr == 0xC3 || *ptr == 0xCC) {
170			break;
171		}
172		if *ptr == 0xF6 && *ptr.add(1) & 0xF0 == 0x40 && *ptr.add(3) == 0x80 {
173			out.push((*ptr.add(2), ptr.add(3)));
174		}
175		ptr = ptr.add(1);
176	}
177	assert!(out.len() > n, "no matches found for {name}");
178}
179
180// NOTE: there must only be one test, since enable() is thread-unsafe and global.
181
182#[test]
183fn test() {
184	#[derive(Debug)]
185	#[allow(dead_code)]
186	struct A(u32, u32);
187
188	#[allow(dead_code)]
189	#[derive(Debug)]
190	struct B {
191		x: u32,
192		y: u32,
193	}
194
195	#[allow(dead_code)]
196	#[derive(Debug)]
197	enum Enum {
198		A,
199		B(u32),
200		C(u32, u32),
201		D(u32, u32, u32),
202		E(u32, u32, u32, u32),
203		F(u32, u32, u32, u32, u32),
204	}
205
206	let a = A(8, 32);
207	let b = B { x: 8, y: 32 };
208
209	assert_eq!(format!("{a:?}"), "A(8, 32)");
210	assert_eq!(format!("{a:#?}"), "A(\n    8,\n    32,\n)");
211	assert_eq!(format!("{b:?}"), "B { x: 8, y: 32 }");
212	assert_eq!(format!("{b:#?}"), "B {\n    x: 8,\n    y: 32,\n}");
213
214	unsafe { enable(true) };
215
216	assert_eq!(format!("{a:?}"), "A(8, 32)");
217	assert_eq!(format!("{a:#?}"), "A(8, 32)");
218	assert_eq!(format!("{b:?}"), "B { x: 8, y: 32 }");
219	assert_eq!(format!("{b:#?}"), "B {\n    x: 8,\n    y: 32,\n}");
220
221	assert!(!format!("{:#?}", Enum::A).contains('\n'));
222	assert!(!format!("{:#?}", Enum::B(0)).contains('\n'));
223	assert!(!format!("{:#?}", Enum::C(0, 0)).contains('\n'));
224	assert!(!format!("{:#?}", Enum::D(0, 0, 0)).contains('\n'));
225	assert!(!format!("{:#?}", Enum::E(0, 0, 0, 0)).contains('\n'));
226	assert!(!format!("{:#?}", Enum::F(0, 0, 0, 0, 0)).contains('\n'));
227
228	unsafe { enable(false) };
229
230	assert_eq!(format!("{a:?}"), "A(8, 32)");
231	assert_eq!(format!("{a:#?}"), "A(\n    8,\n    32,\n)");
232	assert_eq!(format!("{b:?}"), "B { x: 8, y: 32 }");
233	assert_eq!(format!("{b:#?}"), "B {\n    x: 8,\n    y: 32,\n}");
234
235	test_list([1u8, 2, 3, 4, 5], "[\n    1,\n    2,\n    3,\n    4,\n    5,\n]", "[1, 2, 3, 4, 5]");
236	test_list([123u16], "[\n    123,\n]", "[123]");
237	test_list([123u32, 456], "[\n    123,\n    456,\n]", "[123, 456]");
238	test_list([123u64, 456, 789], "[\n    123,\n    456,\n    789,\n]", "[123, 456, 789]");
239	test_list([123u128, 456, 789, 101112], "[\n    123,\n    456,\n    789,\n    101112,\n]", "[123, 456, 789, 101112]");
240	test_list([1i32, 2, 3, 4, 5], "[\n    1,\n    2,\n    3,\n    4,\n    5,\n]", "[1, 2, 3, 4, 5]");
241	test_list(["one", "two", "three", "four", "five"], "[\n    \"one\",\n    \"two\",\n    \"three\",\n    \"four\",\n    \"five\",\n]", "[\"one\", \"two\", \"three\", \"four\", \"five\"]");
242	test_list([(1, 2), (3, 4), (5, 6)], "[\n    (\n        1,\n        2,\n    ),\n    (\n        3,\n        4,\n    ),\n    (\n        5,\n        6,\n    ),\n]", "[\n    (1, 2),\n    (3, 4),\n    (5, 6),\n]");
243}
244
245#[cfg(test)]
246fn test_list<T: std::fmt::Debug + Clone, const N: usize>(value: [T; N], long: &str, short: &str) {
247	let array = value;
248	let array_ref = &array;
249	let slice_ref = &array as &[_];
250	let vec = array.to_vec();
251
252	assert_eq!(format!("{array:#?}"), long);
253	assert_eq!(format!("{array_ref:#?}"), long);
254	assert_eq!(format!("{slice_ref:#?}"), long);
255	assert_eq!(format!("{vec:#?}"), long);
256
257	unsafe { enable(true) };
258
259	assert_eq!(format!("{array:#?}"), short);
260	assert_eq!(format!("{array_ref:#?}"), short);
261	assert_eq!(format!("{slice_ref:#?}"), short);
262	assert_eq!(format!("{vec:#?}"), short);
263
264	unsafe { enable(false) };
265
266	assert_eq!(format!("{array:#?}"), long);
267	assert_eq!(format!("{array_ref:#?}"), long);
268	assert_eq!(format!("{slice_ref:#?}"), long);
269	assert_eq!(format!("{vec:#?}"), long);
270}