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//! This crate currently only supports x86_64 architecture, and requires nightly.
58
59use std::sync::OnceLock;
60
61#[cfg(not(target_arch = "x86_64"))]
62compile_error!("only supported on x86_64");
63
64struct Pos(*const u8);
65unsafe impl Send for Pos {}
66unsafe impl Sync for Pos {}
67static MATCHES: OnceLock<Vec<Pos>> = OnceLock::new();
68
69
70/// Enables or disables the patch.
71///
72/// # Panics
73/// Panics if the function does not look like expected, which is most likely to happen if `std`
74/// changes something internally, or if the compiler finds a better way to optimize it.
75///
76/// # Safety
77/// Aside from the whole concept being inherently unsafe, this will probably have unexpected
78/// consequences if called in multi-threaded contexts.
79pub unsafe fn enable(on: bool) {
80	unsafe {
81		let matches = MATCHES.get_or_init(find_all);
82
83		for Pos(ptr) in matches {
84			let ptr = ptr.cast_mut();
85			let _prot = region::protect_with_handle(ptr, 1, region::Protection::READ_WRITE_EXECUTE)
86				.unwrap();
87			ptr.write(if on { 0 } else { 4 });
88		}
89	}
90}
91
92fn find_all() -> Vec<Pos> {
93	unsafe {
94		let mut out = Vec::new();
95		macro_rules! find {
96			($name:path) => {
97				do_find(&mut out, stringify!($name), $name as *const () as *const u8);
98			};
99		}
100		find!(std::fmt::DebugTuple::field);
101		find!(std::fmt::DebugTuple::finish);
102		find!(std::fmt::DebugTuple::finish_non_exhaustive);
103		find!(std::fmt::Formatter::debug_tuple_field1_finish);
104		find!(std::fmt::Formatter::debug_tuple_field2_finish);
105		find!(std::fmt::Formatter::debug_tuple_field3_finish);
106		find!(std::fmt::Formatter::debug_tuple_field4_finish);
107		find!(std::fmt::Formatter::debug_tuple_field5_finish);
108		find!(std::fmt::Formatter::debug_tuple_fields_finish);
109
110		// Check that all the field offsets are the same
111		assert!(out.iter().all(|x| x.0 == out[0].0), "field offsets differ");
112
113		out.into_iter().map(|x| Pos(x.1)).collect()
114	}
115}
116
117// Find pattern f6 4x ?? 04, and record the value of the ?? and the position of the 04.
118// End when we find a C3 (ret) or CC (int3) on a position that ends with 0xF.
119unsafe fn do_find(out: &mut Vec<(u8, *const u8)>, name: &str, mut ptr: *const u8) {
120	let n = out.len();
121	loop {
122		if (ptr as usize & 0xF) == 0xF && (*ptr == 0xC3 || *ptr == 0xCC) {
123			break;
124		}
125		if *ptr == 0xF6 && *ptr.add(1) & 0xF0 == 0x40 && *ptr.add(3) == 0x04 {
126			out.push((*ptr.add(2), ptr.add(3)));
127		}
128		ptr = ptr.add(1);
129	}
130	assert!(out.len() > n, "no matches found for {name}");
131}
132
133#[test]
134fn test() {
135	#[derive(Debug)]
136	#[allow(dead_code)]
137	struct A(u32, u32);
138
139	#[allow(dead_code)]
140	#[derive(Debug)]
141	struct B {
142		x: u32,
143		y: u32,
144	}
145
146	let a = A(8, 32);
147	let b = B { x: 8, y: 32 };
148
149	assert_eq!(format!("{a:?}"), "A(8, 32)");
150	assert_eq!(format!("{a:#?}"), "A(\n    8,\n    32,\n)");
151	assert_eq!(format!("{b:?}"), "B { x: 8, y: 32 }");
152	assert_eq!(format!("{b:#?}"), "B {\n    x: 8,\n    y: 32,\n}");
153
154	unsafe { enable(true) };
155
156	assert_eq!(format!("{a:?}"), "A(8, 32)");
157	assert_eq!(format!("{a:#?}"), "A(8, 32)");
158	assert_eq!(format!("{b:?}"), "B { x: 8, y: 32 }");
159	assert_eq!(format!("{b:#?}"), "B {\n    x: 8,\n    y: 32,\n}");
160
161	unsafe { enable(false) };
162
163	assert_eq!(format!("{a:?}"), "A(8, 32)");
164	assert_eq!(format!("{a:#?}"), "A(\n    8,\n    32,\n)");
165	assert_eq!(format!("{b:?}"), "B { x: 8, y: 32 }");
166	assert_eq!(format!("{b:#?}"), "B {\n    x: 8,\n    y: 32,\n}");
167}