From 792f843d1b48abee6aa15e2529b77e4e2e59de25 Mon Sep 17 00:00:00 2001 From: Vlad Scherbich Date: Mon, 31 Aug 2026 14:28:02 -0400 Subject: [PATCH] chore(native): declare PyContextVar_* locally and skip GIL probe on limited-API PyContextVar_New/_Get/_Set are absent from pyo3-ffi limited-API bindings. PyGILState_Check is not limited-API; on those builds skip the probe rather than acquire or guess from a crash signal handler. --- src/native/contextvar.rs | 27 ++++++++++++++----- .../crashtracker_runtime_stacks.rs | 15 ++++++++++- 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/src/native/contextvar.rs b/src/native/contextvar.rs index c1ff6e008f5..22292960528 100644 --- a/src/native/contextvar.rs +++ b/src/native/contextvar.rs @@ -2,6 +2,23 @@ use pyo3::ffi; use pyo3::prelude::*; use std::ffi::CString; +/// PyContextVar_New/_Get/_Set are absent from pyo3-ffi's limited-API bindings (Py_LIMITED_API), +/// which can be enabled via PYO3_USE_ABI3_FORWARD_COMPATIBILITY. +/// Declare the CPython C-API entry points locally so this module builds either way. +/// Context variables were introduced in Python 3.7, so these symbols exist on every supported CPython. +unsafe extern "C" { + fn PyContextVar_New( + name: *const std::os::raw::c_char, + def: *mut ffi::PyObject, + ) -> *mut ffi::PyObject; + fn PyContextVar_Get( + var: *mut ffi::PyObject, + default_value: *mut ffi::PyObject, + value: *mut *mut ffi::PyObject, + ) -> std::os::raw::c_int; + fn PyContextVar_Set(var: *mut ffi::PyObject, value: *mut ffi::PyObject) -> *mut ffi::PyObject; +} + /// Create a new `contextvars.ContextVar` via the C API (`PyContextVar_New`) /// with `default` as its default value. Avoids importing the `contextvars` /// module from Rust. @@ -13,9 +30,7 @@ pub fn contextvar_new<'py>( let c_name = CString::new(name).expect("contextvar name must not contain NUL bytes"); // SAFETY: `c_name` is a valid NUL-terminated string for the duration of the call; // `default.as_ptr()` is a valid borrowed reference (PyContextVar_New takes its own ref). - unsafe { - Bound::from_owned_ptr_or_err(py, ffi::PyContextVar_New(c_name.as_ptr(), default.as_ptr())) - } + unsafe { Bound::from_owned_ptr_or_err(py, PyContextVar_New(c_name.as_ptr(), default.as_ptr())) } } /// Read the current value of `var` via `PyContextVar_Get`. @@ -28,7 +43,7 @@ pub fn contextvar_get<'py>( ) -> PyResult> { let mut value: *mut ffi::PyObject = std::ptr::null_mut(); // SAFETY: `var` is a valid ContextVar object; `value` is a valid out-pointer. - let rc = unsafe { ffi::PyContextVar_Get(var.as_ptr(), std::ptr::null_mut(), &mut value) }; + let rc = unsafe { PyContextVar_Get(var.as_ptr(), std::ptr::null_mut(), &mut value) }; if rc < 0 { return Err(PyErr::take(py).unwrap_or_else(|| { pyo3::exceptions::PyRuntimeError::new_err("PyContextVar_Get failed") @@ -80,7 +95,7 @@ pub fn safe_contextvar_set( })); } - let token = ffi::PyContextVar_Set(var.as_ptr(), value.as_ptr()); + let token = PyContextVar_Set(var.as_ptr(), value.as_ptr()); // Capture any error before the decrefs below can perturb interpreter state. let err = if token.is_null() { @@ -113,7 +128,7 @@ pub fn safe_contextvar_set( ) -> PyResult<()> { // SAFETY: the GIL (`py`) is held for the duration of the call. unsafe { - let token = ffi::PyContextVar_Set(var.as_ptr(), value.as_ptr()); + let token = PyContextVar_Set(var.as_ptr(), value.as_ptr()); if token.is_null() { return Err(PyErr::take(py).unwrap_or_else(|| { PyErr::new::("PyContextVar_Set failed") diff --git a/src/native/crashtracker/crashtracker_runtime_stacks.rs b/src/native/crashtracker/crashtracker_runtime_stacks.rs index f0bb2cb43b7..44b46796754 100644 --- a/src/native/crashtracker/crashtracker_runtime_stacks.rs +++ b/src/native/crashtracker/crashtracker_runtime_stacks.rs @@ -19,11 +19,24 @@ extern "C" { /// This is signal-safe: it's just a thread-local read + comparison. /// Returns false if the GIL is not held (e.g. during a ctypes foreign function call), /// in which case it's unsafe to call any Python C API functions. -#[cfg(unix)] +/// +/// `PyGILState_Check` is not limited-API: it lives in CPython's +/// `Include/cpython/pystate.h` and is absent from `Misc/stable_abi.toml` +/// (3.15 lists Ensure / Release / GetThisThreadState only). pyo3-ffi therefore +/// omits it under `Py_LIMITED_API` (3.15 via `PYO3_USE_ABI3_FORWARD_COMPATIBILITY`). +/// This runs in a crash signal handler, so `PyGILState_Ensure` is not a +/// substitute (not async-signal-safe). On limited-API builds we cannot probe, +/// so skip Python C API use rather than acquire or guess. +#[cfg(all(unix, not(Py_LIMITED_API)))] unsafe fn gil_is_held() -> bool { pyo3_ffi::PyGILState_Check() != 0 } +#[cfg(all(unix, Py_LIMITED_API))] +unsafe fn gil_is_held() -> bool { + false +} + /************************************************************ Emit runtime stacktrace as string using _Py_DumpTracebackThreads / PyUnstable_DumpTracebackThreads