Skip to content

Commit 11c19a2

Browse files
Inject rust stack frames into traceback
1 parent 6a243bf commit 11c19a2

File tree

5 files changed

+199
-6
lines changed

5 files changed

+199
-6
lines changed

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(debug_assertions)]
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(debug_assertions)]
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(debug_assertions)]
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(debug_assertions)]
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(debug_assertions)]
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(debug_assertions)]
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: 72 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,14 @@ use crate::types::any::PyAnyMethods;
1212
#[cfg(Py_3_11)]
1313
use crate::types::PyString;
1414
use crate::types::{
15-
string::PyStringMethods, traceback::PyTracebackMethods, typeobject::PyTypeMethods, PyTraceback,
16-
PyType,
15+
string::PyStringMethods, traceback::PyTracebackMethods, typeobject::PyTypeMethods, PyFrame,
16+
PyTraceback, PyType,
1717
};
1818
use crate::{exceptions::PyBaseException, ffi};
1919
use crate::{BoundObject, Py, PyAny, Python};
2020
use err_state::{PyErrState, PyErrStateLazyFnOutput, PyErrStateNormalized};
2121
use std::convert::Infallible;
22-
use std::ffi::CStr;
22+
use std::ffi::{CStr, CString};
2323

2424
mod cast_error;
2525
mod downcast_error;
@@ -127,10 +127,14 @@ impl PyErr {
127127
T: PyTypeInfo,
128128
A: PyErrArguments + Send + Sync + 'static,
129129
{
130+
#[cfg(debug_assertions)]
131+
let backtrace = backtrace::Backtrace::new_unresolved();
130132
PyErr::from_state(PyErrState::lazy(Box::new(move |py| {
131133
PyErrStateLazyFnOutput {
132134
ptype: T::type_object(py).into(),
133135
pvalue: args.arguments(py),
136+
#[cfg(debug_assertions)]
137+
backtrace,
134138
}
135139
})))
136140
}
@@ -289,7 +293,24 @@ impl PyErr {
289293
Self::print_panic_and_unwind(py, state)
290294
}
291295

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

295316
#[cold]
@@ -696,6 +717,53 @@ impl<'py> IntoPyObject<'py> for PyErr {
696717
}
697718
}
698719

720+
#[cfg(debug_assertions)]
721+
fn backtrace_to_frames<'py, 'a>(
722+
py: Python<'py>,
723+
backtrace: &'a mut backtrace::Backtrace,
724+
) -> impl Iterator<Item = Bound<'py, PyFrame>> + use<'py, 'a> {
725+
backtrace.resolve();
726+
backtrace
727+
.frames()
728+
.iter()
729+
.flat_map(|frame| frame.symbols())
730+
.take_while(|&symbol| {
731+
symbol
732+
.name()
733+
.map(|sym| sym.to_string())
734+
.map(|ref name| {
735+
dbg!(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 |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(
753+
symbol
754+
.name()
755+
.unwrap()
756+
.to_string()
757+
.trim_end_matches(char::is_alphanumeric)
758+
.trim_end_matches(':'),
759+
)
760+
.ok()?;
761+
let line = symbol.lineno().unwrap_or(0);
762+
763+
PyFrame::new(py, &file, &function, line as _).ok()
764+
})
765+
}
766+
699767
impl<'py> IntoPyObject<'py> for &PyErr {
700768
type Target = PyBaseException;
701769
type Output = Bound<'py, Self::Target>;
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 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_new_err_function_err_result::{{closure}}
10+
fn test_new_err_function_err_result() {
11+
File "./tests/test_backtrace.rs", line 16, in test_backtrace::test_new_err_function_err_result
12+
Python::attach(|py| {
13+
File "./src/marker.rs", line 415, in pyo3::marker::Python::attach
14+
f(guard.python())
15+
File "./tests/test_backtrace.rs", line 22, in test_backtrace::test_new_err_function_err_result::{{closure}}
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 352, in pyo3::err::PyErr::fetch
26+
PyErr::take(py).unwrap_or_else(failed_to_fetch)
27+
File "./src/err/mod.rs", line 300, 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_new_err_function_err_result::__pyfunction_produce_err_result
42+
#[pyfunction]
43+
File "./tests/test_backtrace.rs", line 13, in test_backtrace::test_new_err_function_err_result::produce_err_result
44+
Err(PyValueError::new_err("Error result"))
45+
File "./src/exceptions.rs", line 31, in pyo3::exceptions::PyValueError::new_err
46+
$crate::PyErr::new::<$name, A>(args)
47+
File "./src/err/mod.rs", line 131, in pyo3::err::PyErr::new
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(feature = "macros")]
2+
use insta::assert_snapshot;
3+
use pyo3::exceptions::PyValueError;
4+
use pyo3::prelude::*;
5+
6+
#[test]
7+
fn test_new_err_function_err_result() {
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)