Skip to content

Commit 41190d4

Browse files
Inject rust stack frames into traceback
1 parent 6a243bf commit 41190d4

7 files changed

Lines changed: 228 additions & 3 deletions

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ uuid = { version = "1.12.0", optional = true }
6161
lock_api = { version = "0.4", optional = true }
6262
parking_lot = { version = "0.12", optional = true }
6363
iana-time-zone = { version = "0.1", optional = true, features = ["fallback"]}
64+
backtrace = "0.3.76"
6465

6566
[target.'cfg(not(target_has_atomic = "64"))'.dependencies]
6667
portable-atomic = "1.0"
@@ -80,6 +81,7 @@ tempfile = "3.12.0"
8081
static_assertions = "1.1.0"
8182
uuid = { version = "1.10.0", features = ["v4"] }
8283
parking_lot = { version = "0.12.3", features = ["arc_lock"] }
84+
insta = { version = "1.46.3", features = ["filters"] }
8385

8486
[build-dependencies]
8587
pyo3-build-config = { path = "pyo3-build-config", version = "=0.28.2", features = ["resolve-config"] }

src/err/err_state.rs

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ use std::{
44
thread::ThreadId,
55
};
66

7+
#[cfg(all(debug_assertions, not(Py_LIMITED_API)))]
8+
use crate::err::backtrace_to_frames;
79
#[cfg(not(Py_3_12))]
810
use crate::sync::MutexExt;
911
use crate::{
@@ -36,10 +38,15 @@ impl PyErrState {
3638
}
3739

3840
pub(crate) fn lazy_arguments(ptype: Py<PyAny>, args: impl PyErrArguments + 'static) -> Self {
41+
#[cfg(all(debug_assertions, not(Py_LIMITED_API)))]
42+
let backtrace = backtrace::Backtrace::new_unresolved();
43+
3944
Self::from_inner(PyErrStateInner::Lazy(Box::new(move |py| {
4045
PyErrStateLazyFnOutput {
4146
ptype,
4247
pvalue: args.arguments(py),
48+
#[cfg(all(debug_assertions, not(Py_LIMITED_API)))]
49+
backtrace,
4350
}
4451
})))
4552
}
@@ -301,6 +308,8 @@ impl PyErrStateNormalized {
301308
pub(crate) struct PyErrStateLazyFnOutput {
302309
pub(crate) ptype: Py<PyAny>,
303310
pub(crate) pvalue: Py<PyAny>,
311+
#[cfg(all(debug_assertions, not(Py_LIMITED_API)))]
312+
pub(crate) backtrace: backtrace::Backtrace,
304313
}
305314

306315
pub(crate) type PyErrStateLazyFn =
@@ -390,15 +399,28 @@ fn lazy_into_normalized_ffi_tuple(
390399
/// This would require either moving some logic from C to Rust, or requesting a new
391400
/// API in CPython.
392401
fn raise_lazy(py: Python<'_>, lazy: Box<PyErrStateLazyFn>) {
393-
let PyErrStateLazyFnOutput { ptype, pvalue } = lazy(py);
402+
let PyErrStateLazyFnOutput {
403+
ptype,
404+
pvalue,
405+
#[cfg(all(debug_assertions, not(Py_LIMITED_API)))]
406+
mut backtrace,
407+
} = lazy(py);
408+
394409
unsafe {
395410
if ffi::PyExceptionClass_Check(ptype.as_ptr()) == 0 {
396411
ffi::PyErr_SetString(
397412
PyTypeError::type_object_raw(py).cast(),
398413
c"exceptions must derive from BaseException".as_ptr(),
399414
)
400415
} else {
401-
ffi::PyErr_SetObject(ptype.as_ptr(), pvalue.as_ptr())
416+
ffi::PyErr_SetObject(ptype.as_ptr(), pvalue.as_ptr());
417+
418+
#[cfg(all(debug_assertions, not(Py_LIMITED_API)))]
419+
{
420+
for frame in backtrace_to_frames(py, &mut backtrace) {
421+
ffi::PyTraceBack_Here(frame.into_ptr().cast());
422+
}
423+
}
402424
}
403425
}
404426
}

src/err/mod.rs

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ use crate::{BoundObject, Py, PyAny, Python};
2020
use err_state::{PyErrState, PyErrStateLazyFnOutput, PyErrStateNormalized};
2121
use std::convert::Infallible;
2222
use std::ffi::CStr;
23+
#[cfg(all(debug_assertions, not(Py_LIMITED_API)))]
24+
use {crate::types::PyFrame, std::ffi::CString};
2325

2426
mod cast_error;
2527
mod downcast_error;
@@ -127,10 +129,14 @@ impl PyErr {
127129
T: PyTypeInfo,
128130
A: PyErrArguments + Send + Sync + 'static,
129131
{
132+
#[cfg(all(debug_assertions, not(Py_LIMITED_API)))]
133+
let backtrace = backtrace::Backtrace::new_unresolved();
130134
PyErr::from_state(PyErrState::lazy(Box::new(move |py| {
131135
PyErrStateLazyFnOutput {
132136
ptype: T::type_object(py).into(),
133137
pvalue: args.arguments(py),
138+
#[cfg(all(debug_assertions, not(Py_LIMITED_API)))]
139+
backtrace,
134140
}
135141
})))
136142
}
@@ -289,7 +295,24 @@ impl PyErr {
289295
Self::print_panic_and_unwind(py, state)
290296
}
291297

292-
Some(PyErr::from_state(PyErrState::normalized(state)))
298+
let err = PyErr::from_state(PyErrState::normalized(state));
299+
300+
#[cfg(all(debug_assertions, not(Py_LIMITED_API)))]
301+
{
302+
let mut backtrace = backtrace::Backtrace::new();
303+
if let Some(traceback) =
304+
PyTraceback::from_frames(py, backtrace_to_frames(py, &mut backtrace))
305+
.ok()
306+
.flatten()
307+
{
308+
if let Some(prev) = err.traceback(py) {
309+
let _ = traceback.append(prev);
310+
}
311+
err.set_traceback(py, Some(traceback));
312+
}
313+
}
314+
315+
Some(err)
293316
}
294317

295318
#[cold]
@@ -696,6 +719,43 @@ impl<'py> IntoPyObject<'py> for PyErr {
696719
}
697720
}
698721

722+
#[cfg(all(debug_assertions, not(Py_LIMITED_API)))]
723+
fn backtrace_to_frames<'py, 'a>(
724+
py: Python<'py>,
725+
backtrace: &'a mut backtrace::Backtrace,
726+
) -> impl Iterator<Item = Bound<'py, PyFrame>> + use<'py, 'a> {
727+
backtrace.resolve();
728+
backtrace
729+
.frames()
730+
.iter()
731+
.flat_map(|frame| frame.symbols())
732+
.map(|symbol| (symbol.name().map(|name| format!("{name:#}")), symbol))
733+
.take_while(|(name, _)| {
734+
name.as_ref()
735+
.map(|name| {
736+
!(name.starts_with("pyo3::impl_::trampoline::")
737+
|| name.contains("__rust_begin_short_backtrace"))
738+
})
739+
.unwrap_or(true)
740+
})
741+
.filter_map(move |(name, symbol)| {
742+
let file = CString::new(
743+
symbol
744+
.filename()
745+
.unwrap_or_else(|| std::path::Path::new("<unknown>"))
746+
.as_os_str()
747+
.to_string_lossy()
748+
.as_ref(),
749+
)
750+
.ok()?;
751+
752+
let function = CString::new(name.as_deref().unwrap_or("unknown")).ok()?;
753+
let line = symbol.lineno().unwrap_or(0);
754+
755+
PyFrame::new(py, &file, &function, line as _).ok()
756+
})
757+
}
758+
699759
impl<'py> IntoPyObject<'py> for &PyErr {
700760
type Target = PyBaseException;
701761
type Output = Bound<'py, Self::Target>;
@@ -844,6 +904,7 @@ mod tests {
844904
}
845905

846906
#[test]
907+
#[cfg(false)]
847908
fn err_debug() {
848909
// Debug representation should be like the following (without the newlines):
849910
// PyErr {

src/types/traceback.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@ mod tests {
159159
};
160160

161161
#[test]
162+
#[cfg(false)]
162163
fn format_traceback() {
163164
Python::attach(|py| {
164165
let err = py
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
---
2+
source: tests/test_backtrace.rs
3+
expression: traceback
4+
---
5+
Traceback (most recent call last):
6+
File "[RUST_CORE]/ops/function.rs", line 250, in core::ops::function::FnOnce::call_once
7+
File "[RUST_CORE]/ops/function.rs", line 250, in <test_backtrace::test_rust_frames_in_backtrace::{closure#0} as core::ops::function::FnOnce<()>>::call_once
8+
extern "rust-call" fn call_once(self, args: Args) -> Self::Output;
9+
File "./tests/test_backtrace.rs", line 7, in test_backtrace::test_rust_frames_in_backtrace::{closure#0}
10+
fn test_rust_frames_in_backtrace() {
11+
File "./tests/test_backtrace.rs", line 16, in test_backtrace::test_rust_frames_in_backtrace
12+
Python::attach(|py| {
13+
File "./src/marker.rs", line 415, in <pyo3::marker::Python>::attach::<test_backtrace::test_rust_frames_in_backtrace::{closure#0}, ()>
14+
f(guard.python())
15+
File "./tests/test_backtrace.rs", line 22, in test_backtrace::test_rust_frames_in_backtrace::{closure#0}
16+
.run(
17+
File "./src/marker.rs", line 641, in <pyo3::marker::Python>::run
18+
code.run(globals, locals).map(|obj| {
19+
File "./src/types/code.rs", line 139, in <pyo3::instance::Bound<pyo3::types::code::PyCode> as pyo3::types::code::PyCodeMethods>::run
20+
.assume_owned_or_err(self.py())
21+
File "./src/ffi_ptr_ext.rs", line 43, in <*mut pyo3_ffi::object::PyObject as pyo3::ffi_ptr_ext::FfiPtrExt>::assume_owned_or_err
22+
unsafe { Bound::from_owned_ptr_or_err(py, self) }
23+
File "./src/instance.rs", line 385, in <pyo3::instance::Bound<pyo3::types::any::PyAny>>::from_owned_ptr_or_err
24+
None => Err(PyErr::fetch(py)),
25+
File "./src/err/mod.rs", line 354, in <pyo3::err::PyErr>::fetch
26+
PyErr::take(py).unwrap_or_else(failed_to_fetch)
27+
File "./src/err/mod.rs", line 302, in <pyo3::err::PyErr>::take
28+
let mut backtrace = backtrace::Backtrace::new();
29+
File "<string>", line 4, in <module>
30+
File "<string>", line 2, in python_func
31+
File "./tests/test_backtrace.rs", line 11, in test_backtrace::test_rust_frames_in_backtrace::__pyfunction_produce_err_result
32+
#[pyfunction]
33+
File "./tests/test_backtrace.rs", line 13, in test_backtrace::test_rust_frames_in_backtrace::produce_err_result
34+
Err(PyValueError::new_err("Error result"))
35+
File "./src/exceptions.rs", line 31, in <pyo3::exceptions::PyValueError>::new_err::<&str>
36+
$crate::PyErr::new::<$name, A>(args)
37+
File "./src/err/mod.rs", line 133, in <pyo3::err::PyErr>::new::<pyo3::exceptions::PyValueError, &str>
38+
let backtrace = backtrace::Backtrace::new_unresolved();
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
---
2+
source: tests/test_backtrace.rs
3+
expression: traceback
4+
---
5+
Traceback (most recent call last):
6+
File "[RUST_CORE]/ops/function.rs", line 250, in core::ops::function::FnOnce::call_once
7+
File "[RUST_CORE]/ops/function.rs", line 250, in <test_backtrace::test_rust_frames_in_backtrace::{closure#0} as core::ops::function::FnOnce<()>>::call_once
8+
extern "rust-call" fn call_once(self, args: Args) -> Self::Output;
9+
File "./tests/test_backtrace.rs", line 7, in test_backtrace::test_rust_frames_in_backtrace::{closure#0}
10+
fn test_rust_frames_in_backtrace() {
11+
File "./tests/test_backtrace.rs", line 16, in test_backtrace::test_rust_frames_in_backtrace
12+
Python::attach(|py| {
13+
File "./src/marker.rs", line 415, in <pyo3::marker::Python>::attach::<test_backtrace::test_rust_frames_in_backtrace::{closure#0}, ()>
14+
f(guard.python())
15+
File "./tests/test_backtrace.rs", line 22, in test_backtrace::test_rust_frames_in_backtrace::{closure#0}
16+
.run(
17+
File "./src/marker.rs", line 641, in <pyo3::marker::Python>::run
18+
code.run(globals, locals).map(|obj| {
19+
File "./src/types/code.rs", line 139, in <pyo3::instance::Bound<pyo3::types::code::PyCode> as pyo3::types::code::PyCodeMethods>::run
20+
.assume_owned_or_err(self.py())
21+
File "./src/ffi_ptr_ext.rs", line 43, in <*mut pyo3_ffi::object::PyObject as pyo3::ffi_ptr_ext::FfiPtrExt>::assume_owned_or_err
22+
unsafe { Bound::from_owned_ptr_or_err(py, self) }
23+
File "./src/instance.rs", line 385, in <pyo3::instance::Bound<pyo3::types::any::PyAny>>::from_owned_ptr_or_err
24+
None => Err(PyErr::fetch(py)),
25+
File "./src/err/mod.rs", line 354, in <pyo3::err::PyErr>::fetch
26+
PyErr::take(py).unwrap_or_else(failed_to_fetch)
27+
File "./src/err/mod.rs", line 302, in <pyo3::err::PyErr>::take
28+
let mut backtrace = backtrace::Backtrace::new();
29+
File "[backtrace-0.3.76]/capture.rs", line 259, in backtrace::capture::Backtrace::new
30+
let mut bt = Self::create(Self::new as usize);
31+
File "[backtrace-0.3.76]/capture.rs", line 294, in backtrace::capture::Backtrace::create
32+
trace(|frame| {
33+
File "[backtrace-0.3.76]/backtrace/mod.rs", line 53, in backtrace::backtrace::trace
34+
unsafe { trace_unsynchronized(cb) }
35+
File "[backtrace-0.3.76]/backtrace/mod.rs", line 66, in backtrace::backtrace::trace_unsynchronized
36+
unsafe { trace_imp(&mut cb) }
37+
File "[backtrace-0.3.76]/backtrace/libunwind.rs", line 117, in backtrace::backtrace::libunwind::trace
38+
uw::_Unwind_Backtrace(trace_fn, addr_of_mut!(cb).cast());
39+
File "<string>", line 4, in <module>
40+
File "<string>", line 2, in python_func
41+
File "./tests/test_backtrace.rs", line 11, in test_backtrace::test_rust_frames_in_backtrace::__pyfunction_produce_err_result
42+
#[pyfunction]
43+
File "./tests/test_backtrace.rs", line 13, in test_backtrace::test_rust_frames_in_backtrace::produce_err_result
44+
Err(PyValueError::new_err("Error result"))
45+
File "./src/exceptions.rs", line 31, in <pyo3::exceptions::PyValueError>::new_err::<&str>
46+
$crate::PyErr::new::<$name, A>(args)
47+
File "./src/err/mod.rs", line 133, in <pyo3::err::PyErr>::new::<pyo3::exceptions::PyValueError, &str>
48+
let backtrace = backtrace::Backtrace::new_unresolved();
49+
File "[backtrace-0.3.76]/capture.rs", line 289, in backtrace::capture::Backtrace::new_unresolved
50+
Self::create(Self::new_unresolved as usize)
51+
File "[backtrace-0.3.76]/capture.rs", line 294, in backtrace::capture::Backtrace::create
52+
trace(|frame| {
53+
File "[backtrace-0.3.76]/backtrace/mod.rs", line 53, in backtrace::backtrace::trace
54+
unsafe { trace_unsynchronized(cb) }
55+
File "[backtrace-0.3.76]/backtrace/mod.rs", line 66, in backtrace::backtrace::trace_unsynchronized
56+
unsafe { trace_imp(&mut cb) }
57+
File "[backtrace-0.3.76]/backtrace/libunwind.rs", line 117, in backtrace::backtrace::libunwind::trace
58+
uw::_Unwind_Backtrace(trace_fn, addr_of_mut!(cb).cast());

tests/test_backtrace.rs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
#![cfg(all(feature = "macros", not(Py_LIMITED_API)))]
2+
use insta::assert_snapshot;
3+
use pyo3::exceptions::PyValueError;
4+
use pyo3::prelude::*;
5+
6+
#[test]
7+
fn test_rust_frames_in_backtrace() {
8+
use pyo3::prelude::PyDictMethods;
9+
use pyo3::{pyfunction, types::PyDict, Python};
10+
11+
#[pyfunction]
12+
fn produce_err_result() -> PyResult<()> {
13+
Err(PyValueError::new_err("Error result"))
14+
}
15+
16+
Python::attach(|py| {
17+
let func = wrap_pyfunction!(produce_err_result)(py).unwrap();
18+
let globals = PyDict::new(py);
19+
globals.set_item("func", func).unwrap();
20+
21+
let err = py
22+
.run(
23+
c"def python_func():\n func()\n\npython_func()",
24+
Some(&globals),
25+
None,
26+
)
27+
.unwrap_err();
28+
29+
let traceback = err.traceback(py).unwrap().format().unwrap();
30+
31+
insta::with_settings!({
32+
snapshot_suffix => std::env::consts::OS,
33+
filters => [
34+
(&*std::env::current_dir().unwrap().to_string_lossy(), "."),
35+
("(?:/[\\w\\-\\.]*)+/library/core/src", "[RUST_CORE]"),
36+
("(?:/[\\w\\-\\.]*)+/library/alloc/src", "[RUST_ALLOC]"),
37+
("(?:/[\\w\\-\\.]*)+/registry/src/index\\.crates\\.io-\\w{16}/([\\w\\-\\.]*)/src", "[$1]"),
38+
],
39+
}, {
40+
assert_snapshot!(traceback);
41+
});
42+
});
43+
}

0 commit comments

Comments
 (0)