tracy_client/
demangle.rs

1//! Custom symbol demangling support.
2//!
3//! By default, Tracy demangles symbols using the C++ ABI, which is not fully compatible with Rust
4//! symbol mangling.
5//!
6//! With the `demangle` feature enabled, clients must register a custom demangling function.
7//! This can be done by calling the [`register_demangler!`][macro] macro with either no arguments
8//! to use the [default demangler](default), or with a path to a custom demangler function. See
9//! [its documentation][macro] for how to use it.
10//!
11//! Note that only one demangler can be registered at a time. Attempting to register multiple
12//! demanglers will result in a linking failure due to multiple definitions of the underlying
13//! `extern "C"` function.
14//!
15//! [macro]: crate::register_demangler
16
17use std::fmt;
18
19/// Opaque buffer used to write demangled symbols.
20///
21/// The only exposed API is currently [`fmt::Write`].
22///
23/// See [the module-level documentation](self) for more information.
24pub struct Buffer(String);
25
26impl fmt::Write for Buffer {
27    #[inline]
28    fn write_str(&mut self, s: &str) -> fmt::Result {
29        self.0.write_str(s)
30    }
31
32    #[inline]
33    fn write_char(&mut self, c: char) -> fmt::Result {
34        self.0.write_char(c)
35    }
36}
37
38impl Buffer {
39    const fn new() -> Self {
40        Self(String::new())
41    }
42
43    fn clear_on_err<T, E>(&mut self, f: impl FnOnce(&mut Self) -> Result<T, E>) -> Result<T, E> {
44        let r = f(self);
45        if r.is_err() {
46            self.0.clear();
47        }
48        r
49    }
50}
51
52/// Demangles a Rust symbol using [`rustc_demangle`].
53///
54/// See [the module-level documentation](self) for more information.
55pub fn default(s: &str, buffer: &mut impl fmt::Write) -> fmt::Result {
56    let Ok(demangled) = rustc_demangle::try_demangle(s) else {
57        return Err(fmt::Error);
58    };
59    // Use `:#` formatting to elide the hash.
60    write!(buffer, "{demangled:#}")
61}
62
63/// Symbol demangler that does nothing.
64///
65/// See [the module-level documentation](self) for more information.
66pub fn noop(_: &str, _: &mut impl fmt::Write) -> fmt::Result {
67    Err(fmt::Error)
68}
69
70pub(super) mod internal {
71    use super::Buffer;
72    use std::ffi::c_char;
73    use std::fmt::{self, Write};
74    use std::ptr::null;
75
76    /// Demangling glue.
77    pub unsafe fn implementation<F>(mangled: *const c_char, run: F) -> *const c_char
78    where
79        F: FnOnce(&str, &mut Buffer) -> fmt::Result,
80    {
81        // https://github.com/wolfpld/tracy/blob/d4a4b623968d99a7403cd93bae5247ed0735680a/public/client/TracyCallstack.cpp#L57-L67
82        // > The demangling function is responsible for managing memory for this string.
83        // > It is expected that it will be internally reused.
84        // > When a call to ___tracy_demangle is made, previous contents of the string memory
85        // > do not need to be preserved.
86        static mut BUFFER: Buffer = Buffer::new();
87
88        if mangled.is_null() {
89            return null();
90        }
91        let cstr = unsafe { std::ffi::CStr::from_ptr(mangled) };
92        let Ok(str) = cstr.to_str() else {
93            return null();
94        };
95
96        let buffer = unsafe { &mut *std::ptr::addr_of_mut!(BUFFER) };
97        buffer.0.clear();
98        let result = buffer.clear_on_err(|buffer| {
99            run(str, buffer)?;
100            match buffer.0.as_bytes().split_last() {
101                None | Some((&0, [])) => return Err(fmt::Error),
102                Some((_, v)) if v.contains(&0) => return Err(fmt::Error),
103                Some((&0, _)) => return Ok(()),
104                _ => (),
105            }
106            buffer.write_char('\0')?;
107            Ok(())
108        });
109        match result {
110            Ok(()) => {
111                debug_assert_eq!(buffer.0.as_bytes().last().copied(), Some(0));
112                buffer.0.as_ptr().cast()
113            }
114            Err(fmt::Error) => null(),
115        }
116    }
117}
118
119/// Registers a custom demangler function.
120///
121/// A [default implementation](default) for demangling Rust symbols can be registered by passing no
122/// arguments.
123///
124/// Custom implementations can be registered by passing a function with the following signature:
125/// `fn(mangled: &str, buffer: &mut tracy_client::demangle::Buffer) -> std::fmt::Result`
126///
127/// Custom demanglers:
128/// - Must not write null bytes to the buffer.
129/// - Returning `Err` or leaving the buffer unchanged will result in the symbol being displayed as-is.
130///
131/// See [the module-level documentation](self) for more information.
132///
133/// # Examples
134///
135/// ```
136/// use tracy_client::{demangle, register_demangler};
137///
138/// // Register the default demangler.
139/// # #[cfg(any())]
140/// register_demangler!();
141///
142/// // Register a noop demangler.
143/// # #[cfg(any())]
144/// register_demangler!(demangle::noop);
145///
146/// // Register a custom demangler.
147/// # #[cfg(any())]
148/// register_demangler!(my_demangler);
149///
150/// fn my_demangler(s: &str, buffer: &mut demangle::Buffer) -> std::fmt::Result {
151///     // Custom demangling logic...
152///     use std::fmt::Write;
153///     write!(buffer, "{s}")?;
154///     Ok(())
155/// }
156/// ```
157#[macro_export]
158macro_rules! register_demangler {
159    () => {
160        $crate::register_demangler!($crate::internal::demangle::default);
161    };
162
163    ($path:path) => {
164        const _: () = {
165            #[no_mangle]
166            unsafe extern "C" fn ___tracy_demangle(
167                mangled: *const std::ffi::c_char,
168            ) -> *const std::ffi::c_char {
169                unsafe { $crate::internal::demangle::implementation(mangled, $path) }
170            }
171        };
172    };
173}