Skip to content

Commit eedf4a5

Browse files
committed
Include Rust panic messages in Python exception
When Python calls a Rust function, an unhandled Rust panic is turned into a Python `SystemError` exception. Previously, the value of that exception was just "Rust panic" without more details. This adds the panic message to that value. Normally this message is printed to stderr by the Rust standard library’s panic hook. In case that is not available for some reason, it can help to duplicate it in the exception.
1 parent e211a68 commit eedf4a5

File tree

2 files changed

+48
-3
lines changed

2 files changed

+48
-3
lines changed

src/function.rs

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@ where
224224
});
225225
match ret {
226226
Ok(r) => r,
227-
Err(ref err) => {
227+
Err(err) => {
228228
// Protect against panics in C::error_value() causing UB
229229
let guard = AbortOnDrop("handle_panic() / C::error_value()");
230230
handle_panic(Python::assume_gil_acquired(), err);
@@ -235,8 +235,29 @@ where
235235
}
236236
}
237237

238-
fn handle_panic(_py: Python, _panic: &dyn any::Any) {
239-
let msg = cstr!("Rust panic");
238+
// This only needs `&dyn Any`, but we keep a `Box` all the way to avoid the
239+
// risk of a subtle bug in the caller where `&Box<dyn Any>` is coerced to
240+
// `&dyn Any` by unsizing with another layer of vtable and wide pointer,
241+
// instead of the expected auto-deref.
242+
fn handle_panic(_py: Python, panic: Box<dyn any::Any>) {
243+
let panic_str = if let Some(s) = panic.downcast_ref::<String>() {
244+
Some(s.as_str())
245+
} else if let Some(s) = panic.downcast_ref::<&'static str>() {
246+
Some(*s)
247+
} else {
248+
None
249+
};
250+
let panic_cstring = panic_str.and_then(|s| {
251+
let result = CString::new(format!("Rust panic: {}", s));
252+
// Give up on representing the panic payload if it contains a null byte
253+
// TODO: use PyErr_SetObject instead, so a `char*` string isn’t needed?
254+
result.ok()
255+
});
256+
let msg = if let Some(s) = &panic_cstring {
257+
s.as_c_str()
258+
} else {
259+
cstr!("Rust panic")
260+
};
240261
unsafe {
241262
ffi::PyErr_SetString(ffi::PyExc_SystemError, msg.as_ptr());
242263
}

tests/test_function.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,30 @@ fn none_return() {
181181
assert_eq!(CALL_COUNT.load(Relaxed), 1);
182182
}
183183

184+
/// When Python calls a Rust function, an unhandled Rust panic is turned into
185+
/// a Python `SystemError` exception. The exception’s value is a string that
186+
/// contains the panic’s payload, if that payload was a string.
187+
#[test]
188+
fn panicking() {
189+
fn f(_py: Python) -> PyResult<PyNone> {
190+
panic!("panicking because {}", "reasons")
191+
}
192+
193+
let gil = Python::acquire_gil();
194+
let py = gil.python();
195+
let obj = py_fn!(py, f());
196+
197+
assert_eq!(
198+
obj.call(py, NoArgs, None)
199+
.unwrap_err() // Expect an exception
200+
.instance(py)
201+
.str(py)
202+
.unwrap()
203+
.to_string_lossy(py),
204+
"Rust panic: panicking because reasons"
205+
);
206+
}
207+
184208
/* TODO: reimplement flexible sig support
185209
#[test]
186210
fn flexible_sig() {

0 commit comments

Comments
 (0)