Skip to content

Commit cd3fda2

Browse files
committed
Add support for demangling C++ frames' symbols
This adds a new optional (but on-by-default) dependency on the `cpp_demangle` crate. When this feature is enabled, if demangling a frame's symbol as a Rust symbol fails, then we attempt to demangle it as a C++ symbol.
1 parent 4477f21 commit cd3fda2

File tree

6 files changed

+215
-19
lines changed

6 files changed

+215
-19
lines changed

Cargo.toml

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,15 @@ build = "build.rs"
1616
[dependencies]
1717
libc = "0.2"
1818
cfg-if = "0.1"
19-
rustc-demangle = "0.1"
19+
rustc-demangle = "0.1.4"
2020

2121
# Optionally enable the ability to serialize a `Backtrace`
2222
serde = { version = "0.8", optional = true }
2323
rustc-serialize = { version = "0.3", optional = true }
2424

25+
# Optionally demangle C++ frames' symbols in backtraces.
26+
cpp_demangle = { version = "0.2", optional = true }
27+
2528
[target.'cfg(windows)'.dependencies]
2629
dbghelp-sys = { version = "0.2", optional = true }
2730
kernel32-sys = { version = "0.2", optional = true }
@@ -32,6 +35,7 @@ backtrace-sys = { path = "backtrace-sys", version = "0.1.3", optional = true }
3235

3336
[build-dependencies]
3437
serde_codegen = { version = "0.8", optional = true }
38+
gcc = { version = "0.3.43", optional = true }
3539

3640
# Each feature controls the two phases of finding a backtrace: getting a
3741
# backtrace and then resolving instruction pointers to symbols. The default
@@ -42,7 +46,7 @@ serde_codegen = { version = "0.8", optional = true }
4246
# Note that not all features are available on all platforms, so even though a
4347
# feature is enabled some other feature may be used instead.
4448
[features]
45-
default = ["libunwind", "libbacktrace", "coresymbolication", "dladdr", "dbghelp"]
49+
default = ["libunwind", "libbacktrace", "coresymbolication", "dladdr", "dbghelp", "demangle_cpp"]
4650

4751
#=======================================
4852
# Methods of acquiring a backtrace
@@ -76,9 +80,12 @@ default = ["libunwind", "libbacktrace", "coresymbolication", "dladdr", "dbghelp"
7680
# enough on OSX.
7781
# - coresymbolication: this feature uses the undocumented core symbolication
7882
# framework on OS X to symbolize.
83+
# - demangle_cpp: this feature enables demangling C++ symbols from C++
84+
# frames.
7985
libbacktrace = ["backtrace-sys"]
8086
dladdr = []
8187
coresymbolication = []
88+
demangle_cpp = ["cpp_demangle", "gcc"]
8289

8390
#=======================================
8491
# Methods of serialization

build.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ fn main() {
2525

2626
expand_serde(out_dir);
2727
println!("cargo:rerun-if-changed=src/capture.rs");
28+
29+
compile_cpp();
30+
println!("cargo:rerun-if-changed=cpp/trampoline.cpp");
2831
}
2932

3033
#[cfg(not(feature = "serialize-serde"))]
@@ -49,3 +52,23 @@ fn expand_serde(out_dir: &Path) {
4952
serde_codegen::expand(&out_dir.join("tmp.rs"), &dst).unwrap();
5053
}).unwrap().join().unwrap();
5154
}
55+
56+
// TODO: how to only do this for tests? I tried to do:
57+
//
58+
// > #[cfg(all(test, feature = "demangle_cpp"))]
59+
//
60+
// but it did not work...
61+
#[cfg(feature = "demangle_cpp")]
62+
fn compile_cpp() {
63+
extern crate gcc;
64+
65+
gcc::Config::new()
66+
.cpp(true)
67+
.debug(true)
68+
.opt_level(0)
69+
.file("cpp/trampoline.cpp")
70+
.compile("libcpptrampoline.a");
71+
}
72+
73+
#[cfg(not(feature = "demangle_cpp"))]
74+
fn compile_cpp() {}

cpp/trampoline.cpp

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
#include <stdio.h>
2+
3+
namespace space {
4+
template <typename FuncT>
5+
void templated_trampoline(FuncT func) {
6+
func();
7+
}
8+
}
9+
10+
typedef void (*FuncPtr)();
11+
12+
extern "C" void cpp_trampoline(FuncPtr func) {
13+
space::templated_trampoline(func);
14+
}

src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,9 @@ extern crate cfg_if;
8585

8686
extern crate rustc_demangle;
8787

88+
#[cfg(feature = "cpp_demangle")]
89+
extern crate cpp_demangle;
90+
8891
#[allow(dead_code)] // not used everywhere
8992
#[cfg(unix)]
9093
#[macro_use]

src/symbolize/mod.rs

Lines changed: 98 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
use std::fmt;
2+
#[cfg(not(feature = "cpp_demangle"))]
3+
use std::marker::PhantomData;
24
use std::os::raw::c_void;
35
use std::path::Path;
46
use std::str;
5-
use rustc_demangle::{demangle, Demangle};
7+
use rustc_demangle::{try_demangle, Demangle};
68

79
/// Resolve an address to a symbol, passing the symbol to the specified
810
/// closure.
@@ -108,26 +110,74 @@ impl fmt::Debug for Symbol {
108110
}
109111
}
110112

113+
114+
cfg_if! {
115+
if #[cfg(feature = "cpp_demangle")] {
116+
// Maybe a parsed C++ symbol, if parsing the mangled symbol as Rust
117+
// failed.
118+
struct OptionCppSymbol<'a>(Option<::cpp_demangle::BorrowedSymbol<'a>>);
119+
120+
impl<'a> OptionCppSymbol<'a> {
121+
fn parse(input: &'a [u8]) -> OptionCppSymbol<'a> {
122+
OptionCppSymbol(::cpp_demangle::BorrowedSymbol::new(input).ok())
123+
}
124+
125+
fn none() -> OptionCppSymbol<'a> {
126+
OptionCppSymbol(None)
127+
}
128+
}
129+
} else {
130+
// Make sure to keep this zero-sized, so that the `cpp_demangle` feature
131+
// has no cost when disabled.
132+
struct OptionCppSymbol<'a>(PhantomData<&'a ()>);
133+
134+
impl<'a> OptionCppSymbol<'a> {
135+
fn parse(_: &'a [u8]) -> OptionCppSymbol<'a> {
136+
OptionCppSymbol(PhantomData)
137+
}
138+
139+
fn none() -> OptionCppSymbol<'a> {
140+
OptionCppSymbol(PhantomData)
141+
}
142+
}
143+
}
144+
}
145+
111146
/// A wrapper around a symbol name to provide ergonomic accessors to the
112147
/// demangled name, the raw bytes, the raw string, etc.
113148
pub struct SymbolName<'a> {
114149
bytes: &'a [u8],
115150
demangled: Option<Demangle<'a>>,
151+
cpp_demangled: OptionCppSymbol<'a>,
116152
}
117153

118154
impl<'a> SymbolName<'a> {
119155
/// Creates a new symbol name from the raw underlying bytes.
120156
pub fn new(bytes: &'a [u8]) -> SymbolName<'a> {
121-
let demangled = str::from_utf8(bytes).ok().map(demangle);
157+
let str_bytes = str::from_utf8(bytes).ok();
158+
let demangled = str_bytes.and_then(|s| try_demangle(s).ok());
159+
160+
let cpp = if demangled.is_none() {
161+
OptionCppSymbol::parse(bytes)
162+
} else {
163+
OptionCppSymbol::none()
164+
};
165+
122166
SymbolName {
123167
bytes: bytes,
124168
demangled: demangled,
169+
cpp_demangled: cpp,
125170
}
126171
}
127172

128-
/// Returns the raw symbol name as `&str` if the symbols is valid utf-8.
173+
/// Returns the raw symbol name as a `str` if the symbols is valid utf-8.
129174
pub fn as_str(&self) -> Option<&'a str> {
130-
self.demangled.as_ref().map(|s| s.as_str())
175+
self.demangled
176+
.as_ref()
177+
.map(|s| s.as_str())
178+
.or_else(|| {
179+
str::from_utf8(self.bytes).ok()
180+
})
131181
}
132182

133183
/// Returns the raw symbol name as a list of bytes
@@ -136,22 +186,54 @@ impl<'a> SymbolName<'a> {
136186
}
137187
}
138188

139-
impl<'a> fmt::Display for SymbolName<'a> {
140-
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
141-
if let Some(ref s) = self.demangled {
142-
s.fmt(f)
143-
} else {
144-
String::from_utf8_lossy(self.bytes).fmt(f)
189+
cfg_if! {
190+
if #[cfg(feature = "cpp_demangle")] {
191+
impl<'a> fmt::Display for SymbolName<'a> {
192+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
193+
if let Some(ref s) = self.demangled {
194+
s.fmt(f)
195+
} else if let Some(ref cpp) = self.cpp_demangled.0 {
196+
cpp.fmt(f)
197+
} else {
198+
String::from_utf8_lossy(self.bytes).fmt(f)
199+
}
200+
}
201+
}
202+
} else {
203+
impl<'a> fmt::Display for SymbolName<'a> {
204+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
205+
if let Some(ref s) = self.demangled {
206+
s.fmt(f)
207+
} else {
208+
String::from_utf8_lossy(self.bytes).fmt(f)
209+
}
210+
}
145211
}
146212
}
147213
}
148214

149-
impl<'a> fmt::Debug for SymbolName<'a> {
150-
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
151-
if let Some(ref s) = self.demangled {
152-
s.fmt(f)
153-
} else {
154-
String::from_utf8_lossy(self.bytes).fmt(f)
215+
cfg_if! {
216+
if #[cfg(feature = "cpp_demangle")] {
217+
impl<'a> fmt::Debug for SymbolName<'a> {
218+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
219+
if let Some(ref s) = self.demangled {
220+
s.fmt(f)
221+
} else if let Some(ref cpp) = self.cpp_demangled.0 {
222+
fmt::Display::fmt(cpp, f)
223+
} else {
224+
String::from_utf8_lossy(self.bytes).fmt(f)
225+
}
226+
}
227+
}
228+
} else {
229+
impl<'a> fmt::Debug for SymbolName<'a> {
230+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
231+
if let Some(ref s) = self.demangled {
232+
s.fmt(f)
233+
} else {
234+
String::from_utf8_lossy(self.bytes).fmt(f)
235+
}
236+
}
155237
}
156238
}
157239
}
@@ -185,4 +267,3 @@ cfg_if! {
185267
use self::noop::Symbol as SymbolImp;
186268
}
187269
}
188-

tests/smoke.rs

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,3 +165,71 @@ fn is_serde() {
165165
is_serialize::<backtrace::Backtrace>();
166166
is_deserialize::<backtrace::Backtrace>();
167167
}
168+
169+
#[test]
170+
#[cfg(all(not(target_os = "windows"), feature = "demangle_cpp"))]
171+
fn smoke_test_cpp() {
172+
use std::sync::atomic::{ATOMIC_BOOL_INIT, AtomicBool, Ordering};
173+
174+
extern "C" {
175+
fn cpp_trampoline(func: extern "C" fn()) -> ();
176+
}
177+
178+
static RAN_ASSERTS: AtomicBool = ATOMIC_BOOL_INIT;
179+
180+
extern "C" fn assert_cpp_frames() {
181+
let mut physical_frames = Vec::new();
182+
backtrace::trace(|cx| {
183+
physical_frames.push(cx.ip());
184+
185+
// We only want to capture this closure's frame, assert_cpp_frames,
186+
// space::templated_trampoline, and cpp_trampoline. Those are
187+
// logical frames, which might be inlined into fewer physical
188+
// frames, so we may end up with extra logical frames after
189+
// resolving these.
190+
physical_frames.len() < 4
191+
});
192+
193+
let names: Vec<_> = physical_frames.into_iter()
194+
.flat_map(|ip| {
195+
let mut logical_frame_names = vec![];
196+
197+
backtrace::resolve(ip, |sym| {
198+
let sym_name = sym.name().expect("Should have a symbol name");
199+
let demangled = sym_name.to_string();
200+
logical_frame_names.push(demangled);
201+
});
202+
203+
assert!(!logical_frame_names.is_empty(),
204+
"Should have resolved at least one symbol for the physical frame");
205+
206+
logical_frame_names
207+
})
208+
// Skip the backtrace::trace closure and assert_cpp_frames, and then
209+
// take the two C++ frame names.
210+
.skip(2)
211+
.take(2)
212+
.collect();
213+
214+
println!("actual names = {:#?}", names);
215+
216+
let expected = [
217+
"void space::templated_trampoline<void (*)()>(void (*)())",
218+
"cpp_trampoline",
219+
];
220+
println!("expected names = {:#?}", expected);
221+
222+
assert_eq!(names.len(), expected.len());
223+
for (actual, expected) in names.iter().zip(expected.iter()) {
224+
assert_eq!(actual, expected);
225+
}
226+
227+
RAN_ASSERTS.store(true, Ordering::SeqCst);
228+
}
229+
230+
assert!(!RAN_ASSERTS.load(Ordering::SeqCst));
231+
unsafe {
232+
cpp_trampoline(assert_cpp_frames);
233+
}
234+
assert!(RAN_ASSERTS.load(Ordering::SeqCst));
235+
}

0 commit comments

Comments
 (0)