Skip to content

Commit 938c86b

Browse files
authored
Merge pull request #264 from SimonSapin/panic-msg
Include Rust panic messages in Python exception
2 parents e211a68 + eedf4a5 commit 938c86b

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)