Skip to content

Commit 7d9464b

Browse files
authored
use PyErr::write_unraisable when panicking (#5496)
* use `PyErr::write_unraisable` when panicking * add a test * fixup cfg for 3.7 * newsfragment * clippy * add panic sections
1 parent 0d4580c commit 7d9464b

File tree

3 files changed

+102
-33
lines changed

3 files changed

+102
-33
lines changed

newsfragments/5496.changed.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Call `sys.unraisablehook` instead of `PyErr_Print` if panicking on null FFI pointer in `Bound`, `Borrowed` and `Py` constructors.

src/err/mod.rs

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -709,14 +709,6 @@ where
709709
}
710710
}
711711

712-
#[track_caller]
713-
pub fn panic_after_error(_py: Python<'_>) -> ! {
714-
unsafe {
715-
ffi::PyErr_Print();
716-
}
717-
panic!("Python API call failed");
718-
}
719-
720712
/// Returns Ok if the error code is not -1.
721713
#[inline]
722714
pub(crate) fn error_on_minusone<T: SignedInteger>(py: Python<'_>, result: T) -> PyResult<()> {

src/instance.rs

Lines changed: 101 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -336,18 +336,18 @@ impl<'py> Bound<'py, PyAny> {
336336
///
337337
/// # Safety
338338
///
339-
/// - `ptr` must be a valid pointer to a Python object
339+
/// - `ptr` must be a valid pointer to a Python object (or null, which will cause a panic)
340340
/// - `ptr` must be an owned Python reference, as the `Bound<'py, PyAny>` will assume ownership
341+
///
342+
/// # Panics
343+
///
344+
/// Panics if `ptr` is null.
341345
#[inline]
342346
#[track_caller]
343347
pub unsafe fn from_owned_ptr(py: Python<'py>, ptr: *mut ffi::PyObject) -> Self {
344-
match NonNull::new(ptr) {
345-
Some(nonnull_ptr) => {
346-
// SAFETY: caller has upheld the safety contract, ptr is known to be non-null
347-
unsafe { Py::from_non_null(nonnull_ptr) }.into_bound(py)
348-
}
349-
None => crate::err::panic_after_error(py),
350-
}
348+
let non_null = NonNull::new(ptr).unwrap_or_else(|| panic_on_null(py));
349+
// SAFETY: caller has upheld the safety contract, ptr is known to be non-null
350+
unsafe { Py::from_non_null(non_null) }.into_bound(py)
351351
}
352352

353353
/// Constructs a new `Bound<'py, PyAny>` from a pointer. Returns `None` if `ptr` is null.
@@ -405,16 +405,16 @@ impl<'py> Bound<'py, PyAny> {
405405
/// # Safety
406406
///
407407
/// - `ptr` must be a valid pointer to a Python object
408+
///
409+
/// # Panics
410+
///
411+
/// Panics if `ptr` is null
408412
#[inline]
409413
#[track_caller]
410414
pub unsafe fn from_borrowed_ptr(py: Python<'py>, ptr: *mut ffi::PyObject) -> Self {
411-
match NonNull::new(ptr) {
412-
Some(nonnull_ptr) => {
413-
// SAFETY: caller has upheld the safety contract, ptr is known to be non-null
414-
unsafe { Py::from_borrowed_non_null(py, nonnull_ptr) }.into_bound(py)
415-
}
416-
None => crate::err::panic_after_error(py),
417-
}
415+
let non_null = NonNull::new(ptr).unwrap_or_else(|| panic_on_null(py));
416+
// SAFETY: caller has upheld the safety contract, ptr is known to be non-null
417+
unsafe { Py::from_borrowed_non_null(py, non_null) }.into_bound(py)
418418
}
419419

420420
/// Constructs a new `Bound<'py, PyAny>` from a pointer by creating a new Python reference.
@@ -1095,14 +1095,18 @@ impl<'a, 'py> Borrowed<'a, 'py, PyAny> {
10951095
///
10961096
/// # Safety
10971097
///
1098-
/// - `ptr` must be a valid pointer to a Python object
1098+
/// - `ptr` must be a valid pointer to a Python object (or null, which will cause a panic)
10991099
/// - similar to `std::slice::from_raw_parts`, the lifetime `'a` is completely defined by
11001100
/// the caller and it is the caller's responsibility to ensure that the reference this is
11011101
/// derived from is valid for the lifetime `'a`.
1102+
///
1103+
/// # Panics
1104+
///
1105+
/// Panics if `ptr` is null
11021106
#[inline]
11031107
#[track_caller]
11041108
pub unsafe fn from_ptr(py: Python<'py>, ptr: *mut ffi::PyObject) -> Self {
1105-
let non_null = NonNull::new(ptr).unwrap_or_else(|| crate::err::panic_after_error(py));
1109+
let non_null = NonNull::new(ptr).unwrap_or_else(|| panic_on_null(py));
11061110
// SAFETY: caller has upheld the safety contract
11071111
unsafe { Self::from_non_null(py, non_null) }
11081112
}
@@ -1965,12 +1969,12 @@ impl<T> Py<T> {
19651969
/// Create a `Py<T>` instance by taking ownership of the given FFI pointer.
19661970
///
19671971
/// # Safety
1968-
/// `ptr` must be a pointer to a Python object of type T.
19691972
///
1970-
/// Callers must own the object referred to by `ptr`, as this function
1971-
/// implicitly takes ownership of that object.
1973+
/// - `ptr` must be a valid pointer to a Python object (or null, which will cause a panic)
1974+
/// - `ptr` must be an owned Python reference, as the `Py<T>` will assume ownership
19721975
///
19731976
/// # Panics
1977+
///
19741978
/// Panics if `ptr` is null.
19751979
#[inline]
19761980
#[track_caller]
@@ -1981,7 +1985,7 @@ impl<T> Py<T> {
19811985
// SAFETY: caller has upheld the safety contract, ptr is known to be non-null
19821986
unsafe { Self::from_non_null(nonnull_ptr) }
19831987
}
1984-
None => crate::err::panic_after_error(py),
1988+
None => panic_on_null(py),
19851989
}
19861990
}
19871991

@@ -1990,7 +1994,9 @@ impl<T> Py<T> {
19901994
/// If `ptr` is null then the current Python exception is fetched as a [`PyErr`].
19911995
///
19921996
/// # Safety
1993-
/// If non-null, `ptr` must be a pointer to a Python object of type T.
1997+
///
1998+
/// - `ptr` must be a valid pointer to a Python object, or null
1999+
/// - a non-null `ptr` must be an owned Python reference, as the `Py<T>` will assume ownership
19942000
#[inline]
19952001
#[deprecated(note = "use `Bound::from_owned_ptr_or_err` instead", since = "0.28.0")]
19962002
pub unsafe fn from_owned_ptr_or_err(
@@ -2011,7 +2017,9 @@ impl<T> Py<T> {
20112017
/// If `ptr` is null then `None` is returned.
20122018
///
20132019
/// # Safety
2014-
/// If non-null, `ptr` must be a pointer to a Python object of type T.
2020+
///
2021+
/// - `ptr` must be a valid pointer to a Python object, or null
2022+
/// - a non-null `ptr` must be an owned Python reference, as the `Py<T>` will assume ownership
20152023
#[inline]
20162024
#[deprecated(note = "use `Bound::from_owned_ptr_or_opt` instead", since = "0.28.0")]
20172025
pub unsafe fn from_owned_ptr_or_opt(_py: Python<'_>, ptr: *mut ffi::PyObject) -> Option<Self> {
@@ -2027,15 +2035,15 @@ impl<T> Py<T> {
20272035
/// `ptr` must be a pointer to a Python object of type T.
20282036
///
20292037
/// # Panics
2038+
///
20302039
/// Panics if `ptr` is null.
20312040
#[inline]
20322041
#[track_caller]
20332042
#[deprecated(note = "use `Borrowed::from_borrowed_ptr` instead", since = "0.28.0")]
20342043
pub unsafe fn from_borrowed_ptr(py: Python<'_>, ptr: *mut ffi::PyObject) -> Py<T> {
20352044
// SAFETY: caller has upheld the safety contract
20362045
#[allow(deprecated)]
2037-
unsafe { Self::from_borrowed_ptr_or_opt(py, ptr) }
2038-
.unwrap_or_else(|| crate::err::panic_after_error(py))
2046+
unsafe { Self::from_borrowed_ptr_or_opt(py, ptr) }.unwrap_or_else(|| panic_on_null(py))
20392047
}
20402048

20412049
/// Create a `Py<T>` instance by creating a new reference from the given FFI pointer.
@@ -2437,10 +2445,23 @@ impl<T> Py<T> {
24372445
}
24382446
}
24392447

2448+
#[track_caller]
2449+
#[cold]
2450+
fn panic_on_null(py: Python<'_>) -> ! {
2451+
if let Some(err) = PyErr::take(py) {
2452+
err.write_unraisable(py, None);
2453+
}
2454+
panic!("PyObject pointer is null");
2455+
}
2456+
24402457
#[cfg(test)]
24412458
mod tests {
24422459
use super::{Bound, IntoPyObject, Py};
2460+
#[cfg(all(feature = "macros", Py_3_8))]
2461+
use crate::exceptions::PyValueError;
24432462
use crate::test_utils::generate_unique_module_name;
2463+
#[cfg(all(feature = "macros", Py_3_8))]
2464+
use crate::test_utils::UnraisableCapture;
24442465
use crate::types::{dict::IntoPyDict, PyAnyMethods, PyCapsule, PyDict, PyString};
24452466
use crate::{ffi, Borrowed, IntoPyObjectExt, PyAny, PyResult, Python};
24462467
use std::ffi::CStr;
@@ -2797,6 +2818,61 @@ a = A()
27972818
});
27982819
}
27992820

2821+
#[cfg(all(feature = "macros", Py_3_8))]
2822+
#[test]
2823+
fn test_constructors_panic_on_null() {
2824+
Python::attach(|py| {
2825+
const NULL: *mut ffi::PyObject = std::ptr::null_mut();
2826+
2827+
#[expect(deprecated, reason = "Py<T> constructors")]
2828+
// SAFETY: calling all constructors with null pointer to test panic behavior
2829+
for constructor in unsafe {
2830+
[
2831+
(|py| {
2832+
Py::<PyAny>::from_owned_ptr(py, NULL);
2833+
}) as fn(Python<'_>),
2834+
(|py| {
2835+
Py::<PyAny>::from_borrowed_ptr(py, NULL);
2836+
}) as fn(Python<'_>),
2837+
(|py| {
2838+
Bound::from_owned_ptr(py, NULL);
2839+
}) as fn(Python<'_>),
2840+
(|py| {
2841+
Bound::from_borrowed_ptr(py, NULL);
2842+
}) as fn(Python<'_>),
2843+
(|py| {
2844+
Borrowed::from_ptr(py, NULL);
2845+
}) as fn(Python<'_>),
2846+
]
2847+
} {
2848+
UnraisableCapture::enter(py, |capture| {
2849+
// panic without exception set, no unraisable hook called
2850+
let result = std::panic::catch_unwind(|| {
2851+
constructor(py);
2852+
});
2853+
assert_eq!(
2854+
result.unwrap_err().downcast_ref::<&str>(),
2855+
Some(&"PyObject pointer is null")
2856+
);
2857+
assert!(capture.take_capture().is_none());
2858+
2859+
// set an exception, panic, unraisable hook called
2860+
PyValueError::new_err("error").restore(py);
2861+
let result = std::panic::catch_unwind(|| {
2862+
constructor(py);
2863+
});
2864+
assert_eq!(
2865+
result.unwrap_err().downcast_ref::<&str>(),
2866+
Some(&"PyObject pointer is null")
2867+
);
2868+
assert!(capture.take_capture().is_some_and(|(err, obj)| {
2869+
err.is_instance_of::<PyValueError>(py) && obj.is_none()
2870+
}));
2871+
});
2872+
}
2873+
});
2874+
}
2875+
28002876
#[cfg(feature = "macros")]
28012877
mod using_macros {
28022878
use super::*;

0 commit comments

Comments
 (0)