Add basic capi error support (#7787)

* Add basic capi error support

* Add missing symbols to make tests compile again

* Add `pyerrors` to dictionary

* Return exception in `Py_GetConstantBorrowed`

* Remove `allow(dead_code)`

* Fix windows

* Load stdlib when calling `Py_InitializeEx`

* Debug tests

* Revert "Load stdlib when calling `Py_InitializeEx`"

This reverts commit bccd38e981.

* Disable tests on windows for now

* Truncate `PyType_GetFlags` to be always 32 bits

* Add test for exception type checking

* Remove subclass type flags

* Use latest pyo3 to make test work on windows

* Revert "Use latest pyo3 to make test work on windows"

This reverts commit b2c2f6913f.

* `set_main_interpreter` -> `init_main_interpreter`
This commit is contained in:
Bas Schoenmaeckers authored and GitHub committed 2026-05-13 23:19:58 +09:00
1 parent e8711edd2d
commit 4059a032a7
11 files changed
+515 -8

No files matched your search

+1
View File
@@ -169,6 +169,7 @@ pybuilddir
pycore
pyinner
pydecimal
pyerrors
Pyfunc
pylifecycle
pymain
+6 -1
View File
@@ -114,10 +114,15 @@ jobs:
uses: ./.github/actions/install-macos-deps
- name: run rust tests
run: cargo test --workspace ${{ env.WORKSPACE_EXCLUDES }} --features threading ${{ env.CARGO_ARGS }}
run: cargo test --workspace --exclude rustpython-capi ${{ env.WORKSPACE_EXCLUDES }} --features threading ${{ env.CARGO_ARGS }}
env:
INSTA_WORKSPACE_ROOT: ${{ github.workspace }}
- name: run c-api tests
working-directory: crates/capi
run: cargo test
if: runner.os != 'Windows' # Requires pyo3 0.29+ on Windows
- run: cargo doc --locked
if: runner.os == 'Linux'
+15 -1
View File
@@ -1,15 +1,19 @@
#![allow(clippy::missing_safety_doc)]
use crate::pyerrors::init_exception_statics;
use crate::pylifecycle::MAIN_INTERP;
use rustpython_vm::Interpreter;
pub use rustpython_vm::PyObject;
use rustpython_vm::{Context, Interpreter};
use std::sync::MutexGuard;
extern crate alloc;
pub mod object;
pub mod pyerrors;
pub mod pylifecycle;
pub mod pystate;
pub mod refcount;
mod util;
/// Get main interpreter of this process. Will be None if it has not been initialized yet.
pub fn get_main_interpreter() -> MutexGuard<'static, Option<Interpreter>> {
@@ -17,3 +21,13 @@ pub fn get_main_interpreter() -> MutexGuard<'static, Option<Interpreter>> {
.lock()
.expect("Failed to lock interpreter mutex")
}
/// Set the main interpreter of this process. This method will panic when there is already an
/// interpreter set.
pub fn init_main_interpreter(interpreter: Interpreter) {
let mut interp = get_main_interpreter();
assert!(interp.is_none(), "Main interpreter is already set");
// Safety: Interpreter was not initialized before, so we can safely assume the statics are not used
unsafe { init_exception_statics(&Context::genesis().exceptions) };
*interp = Some(interpreter);
}
+48
View File
@@ -0,0 +1,48 @@
use crate::PyObject;
use crate::pystate::with_vm;
use core::ffi::{c_int, c_uint, c_ulong};
use rustpython_vm::builtins::PyType;
use rustpython_vm::{AsObject, Py};
pub type PyTypeObject = Py<PyType>;
#[unsafe(no_mangle)]
pub unsafe extern "C" fn Py_TYPE(op: *mut PyObject) -> *const PyTypeObject {
unsafe { (*op).class() }
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn Py_IS_TYPE(op: *mut PyObject, ty: *mut PyTypeObject) -> c_int {
with_vm(|_vm| {
let obj = unsafe { &*op };
let ty = unsafe { &*ty };
obj.class().is(ty)
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn PyType_GetFlags(ptr: *const PyTypeObject) -> c_ulong {
let ty = unsafe { &*ptr };
ty.slots.flags.bits() as u32 as c_ulong
}
#[unsafe(no_mangle)]
pub extern "C" fn Py_GetConstantBorrowed(constant_id: c_uint) -> *mut PyObject {
with_vm(|vm| {
let ctx = &vm.ctx;
let constant = match constant_id {
0 => ctx.none.as_object(),
1 => ctx.false_value.as_object(),
2 => ctx.true_value.as_object(),
3 => ctx.ellipsis.as_object(),
4 => ctx.not_implemented.as_object(),
_ => {
return Err(
vm.new_system_error("Invalid constant ID passed to Py_GetConstantBorrowed")
);
}
}
.as_raw();
Ok(constant)
})
}
+277
View File
@@ -0,0 +1,277 @@
use crate::PyObject;
use crate::pystate::with_vm;
use core::convert::Infallible;
use core::ffi::{CStr, c_char, c_int};
use core::ptr::NonNull;
use rustpython_vm::builtins::{PyBaseException, PyTuple, PyType};
use rustpython_vm::convert::IntoObject;
use rustpython_vm::exceptions::ExceptionZoo;
use rustpython_vm::{AsObject, PyObjectRef, PyResult};
macro_rules! define_exception_statics {
($( $(#[$meta:meta])* $export:ident => $exc:ident ),* $(,)?) => {
$(
$(#[$meta])*
#[unsafe(no_mangle)]
pub static mut $export: *mut PyObject = core::ptr::null_mut();
)*
#[allow(static_mut_refs)]
pub(crate) unsafe fn init_exception_statics(zoo: &'static ExceptionZoo) {
unsafe {
$(
$export = zoo.$exc.as_object().as_raw().cast_mut();
)*
}
}
};
}
define_exception_statics! {
PyExc_BaseException => base_exception_type,
PyExc_BaseExceptionGroup => base_exception_group,
PyExc_SystemExit => system_exit,
PyExc_KeyboardInterrupt => keyboard_interrupt,
PyExc_GeneratorExit => generator_exit,
PyExc_Exception => exception_type,
PyExc_StopIteration => stop_iteration,
PyExc_StopAsyncIteration => stop_async_iteration,
PyExc_ArithmeticError => arithmetic_error,
PyExc_FloatingPointError => floating_point_error,
PyExc_SystemError => system_error,
PyExc_TypeError => type_error,
PyExc_OverflowError => overflow_error,
PyExc_ZeroDivisionError => zero_division_error,
PyExc_AssertionError => assertion_error,
PyExc_IndexError => index_error,
PyExc_KeyError => key_error,
PyExc_LookupError => lookup_error,
PyExc_AttributeError => attribute_error,
PyExc_BufferError => buffer_error,
PyExc_EOFError => eof_error,
PyExc_ImportError => import_error,
PyExc_ModuleNotFoundError => module_not_found_error,
PyExc_MemoryError => memory_error,
PyExc_NameError => name_error,
PyExc_UnboundLocalError => unbound_local_error,
PyExc_OSError => os_error,
PyExc_BlockingIOError => blocking_io_error,
PyExc_ChildProcessError => child_process_error,
PyExc_ConnectionError => connection_error,
PyExc_BrokenPipeError => broken_pipe_error,
PyExc_ConnectionAbortedError => connection_aborted_error,
PyExc_ConnectionRefusedError => connection_refused_error,
PyExc_ConnectionResetError => connection_reset_error,
PyExc_FileExistsError => file_exists_error,
PyExc_FileNotFoundError => file_not_found_error,
PyExc_InterruptedError => interrupted_error,
PyExc_IsADirectoryError => is_a_directory_error,
PyExc_NotADirectoryError => not_a_directory_error,
PyExc_PermissionError => permission_error,
PyExc_ProcessLookupError => process_lookup_error,
PyExc_TimeoutError => timeout_error,
PyExc_ReferenceError => reference_error,
PyExc_RuntimeError => runtime_error,
PyExc_NotImplementedError => not_implemented_error,
PyExc_RecursionError => recursion_error,
PyExc_SyntaxError => syntax_error,
PyExc_IndentationError => indentation_error,
PyExc_TabError => tab_error,
PyExc_ValueError => value_error,
PyExc_UnicodeError => unicode_error,
PyExc_UnicodeDecodeError => unicode_decode_error,
PyExc_UnicodeEncodeError => unicode_encode_error,
PyExc_UnicodeTranslateError => unicode_translate_error,
PyExc_Warning => warning,
PyExc_DeprecationWarning => deprecation_warning,
PyExc_PendingDeprecationWarning => pending_deprecation_warning,
PyExc_RuntimeWarning => runtime_warning,
PyExc_SyntaxWarning => syntax_warning,
PyExc_UserWarning => user_warning,
PyExc_FutureWarning => future_warning,
PyExc_ImportWarning => import_warning,
PyExc_UnicodeWarning => unicode_warning,
PyExc_BytesWarning => bytes_warning,
PyExc_ResourceWarning => resource_warning,
PyExc_EncodingWarning => encoding_warning,
}
#[unsafe(no_mangle)]
pub extern "C" fn PyErr_Occurred() -> *mut PyObject {
with_vm(|vm| {
vm.current_exception()
.map(|exc| exc.class().as_object().as_raw())
.unwrap_or_default()
})
}
#[unsafe(no_mangle)]
pub extern "C" fn PyErr_GetRaisedException() -> *mut PyObject {
with_vm(|vm| {
vm.take_raised_exception()
.map(|exc| exc.into_object().into_raw().as_ptr())
.unwrap_or_default()
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn PyErr_SetRaisedException(exc: *mut PyObject) {
with_vm(|vm| {
if let Some(exc) = NonNull::new(exc) {
let exception = unsafe { PyObjectRef::from_raw(exc).downcast_unchecked() };
vm.set_exception(Some(exception));
} else {
vm.set_exception(None);
}
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn PyErr_SetObject(exception: *mut PyObject, value: *mut PyObject) {
with_vm::<PyResult<Infallible>, _>(|vm| {
let exc_type = unsafe { (&*exception).to_owned() };
let exc_val = unsafe { (&*value).to_owned() };
let normalized = vm.normalize_exception(exc_type, exc_val, vm.ctx.none())?;
Err(normalized)
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn PyErr_SetString(exception: *mut PyObject, message: *const c_char) {
with_vm::<PyResult<Infallible>, _>(|vm| {
let exc_type = unsafe { &*exception }.try_downcast_ref::<PyType>(vm)?;
let Ok(message) = unsafe { CStr::from_ptr(message) }.to_str() else {
return Err(vm.new_type_error("Exception message is not valid UTF-8"));
};
let exc = vm.invoke_exception(
exc_type.to_owned(),
vec![vm.ctx.new_str(message).into_object()],
)?;
Err(exc)
})
}
#[unsafe(no_mangle)]
pub extern "C" fn PyErr_PrintEx(_set_sys_last_vars: c_int) {
with_vm(|vm| {
let exception = vm
.take_raised_exception()
.expect("No exception set in PyErr_PrintEx");
vm.print_exception(exception);
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn PyErr_DisplayException(exc: *mut PyObject) {
with_vm(|vm| {
let exception = unsafe { &*exc }
.downcast_ref::<PyBaseException>()
.expect("PyErr_DisplayException exc must be an exception instance")
.to_owned();
vm.print_exception(exception);
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn PyErr_WriteUnraisable(obj: *mut PyObject) {
with_vm(|vm| {
let exception = vm
.take_raised_exception()
.expect("No exception set in PyErr_WriteUnraisable");
let object = unsafe { vm.unwrap_or_none(obj.as_ref().map(|obj| obj.to_owned())) };
vm.run_unraisable(exception, None, object)
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn PyErr_NewException(
name: *const c_char,
base: *mut PyObject,
dict: *mut PyObject,
) -> *mut PyObject {
with_vm(|vm| {
let (module, name) = unsafe {
CStr::from_ptr(name)
.to_str()
.expect("Exception name is not valid UTF-8")
.rsplit_once('.')
.expect("Exception name must be of the form 'module.ExceptionName'")
};
let bases = unsafe { base.as_ref() }.map(|bases| {
if let Some(ty) = bases.downcast_ref::<PyType>() {
vec![ty.to_owned()]
} else if let Some(tuple) = bases.downcast_ref::<PyTuple>() {
tuple
.iter()
.map(|item| item.to_owned().downcast())
.collect::<Result<Vec<_>, _>>()
.expect("PyErr_NewException base tuple must contain only types")
} else {
panic!("PyErr_NewException base must be a type or a tuple of types");
}
});
assert!(
dict.is_null(),
"PyErr_NewException with non-null dict is not supported yet"
);
vm.ctx.new_exception_type(module, name, bases)
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn PyErr_NewExceptionWithDoc(
name: *const c_char,
_doc: *const c_char,
base: *mut PyObject,
dict: *mut PyObject,
) -> *mut PyObject {
unsafe { PyErr_NewException(name, base, dict) }
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn PyErr_GivenExceptionMatches(
given: *mut PyObject,
exc: *mut PyObject,
) -> c_int {
with_vm(|vm| {
let given = unsafe { &*given };
let exc = unsafe { &*exc };
given.is_subclass(exc, vm)
})
}
#[cfg(test)]
mod tests {
use pyo3::exceptions::PyTypeError;
use pyo3::prelude::*;
#[test]
fn test_raised_exception() {
Python::attach(|py| {
PyTypeError::new_err(py.None()).restore(py);
assert!(PyErr::occurred(py));
assert!(unsafe { !pyo3::ffi::PyErr_GetRaisedException().is_null() });
assert!(!PyErr::occurred(py));
})
}
#[test]
fn test_error_is_instance() {
Python::attach(|py| {
let err = PyTypeError::new_err(py.None());
assert!(err.is_instance_of::<PyTypeError>(py));
})
}
}
+4 -1
View File
@@ -1,8 +1,9 @@
use crate::get_main_interpreter;
use crate::pyerrors::init_exception_statics;
use crate::pystate::ensure_thread_has_vm_attached;
use core::ffi::c_int;
use rustpython_vm::Interpreter;
use rustpython_vm::vm::thread::ThreadedVirtualMachine;
use rustpython_vm::{Context, Interpreter};
use std::sync::Mutex;
pub(crate) static MAIN_INTERP: Mutex<Option<Interpreter>> = Mutex::new(None);
@@ -29,6 +30,8 @@ pub extern "C" fn Py_Initialize() {
pub extern "C" fn Py_InitializeEx(_initsigs: c_int) {
let mut interp = get_main_interpreter();
if interp.is_none() {
// Safety: Interpreter was not initialized before, so we can safely assume the statics are not used
unsafe { init_exception_statics(&Context::genesis().exceptions) };
*interp = Interpreter::with_init(Default::default(), |_vm| {}).into();
drop(interp);
ensure_thread_has_vm_attached();
+10 -1
View File
@@ -1,10 +1,16 @@
use crate::pylifecycle::request_vm_from_interpreter;
use crate::util::FfiResult;
use core::ffi::c_int;
use core::ptr;
use rustpython_vm::VirtualMachine;
use rustpython_vm::vm::thread::{
CurrentVmAttachState, attach_current_thread, release_current_thread,
CurrentVmAttachState, attach_current_thread, release_current_thread, with_current_vm,
};
pub(crate) fn with_vm<R: FfiResult<O>, O>(f: impl FnOnce(&VirtualMachine) -> R) -> O {
with_current_vm(|vm| f(vm).into_output(vm))
}
#[allow(non_camel_case_types)]
type PyGILState_STATE = c_int;
const PYGILSTATE_LOCKED: PyGILState_STATE = 0;
@@ -41,6 +47,9 @@ pub extern "C" fn PyEval_SaveThread() -> *mut PyThreadState {
ptr::null_mut()
}
#[unsafe(no_mangle)]
pub extern "C" fn PyEval_RestoreThread(_state: *mut PyThreadState) {}
#[cfg(test)]
mod tests {
use crate::get_main_interpreter;
+137
View File
@@ -0,0 +1,137 @@
use crate::PyObject;
use core::convert::Infallible;
use core::ffi::{c_char, c_double, c_int, c_long, c_void};
use rustpython_vm::{PyObjectRef, PyRef, PyResult, VirtualMachine};
pub(crate) trait FfiResult<Output = Self> {
const ERR_VALUE: Output;
fn into_output(self, vm: &VirtualMachine) -> Output;
}
impl FfiResult for () {
const ERR_VALUE: () = ();
fn into_output(self, _vm: &VirtualMachine) {
self
}
}
impl FfiResult<c_int> for () {
const ERR_VALUE: c_int = -1;
fn into_output(self, _vm: &VirtualMachine) -> c_int {
0
}
}
impl<T> FfiResult<*mut PyObject> for PyRef<T>
where
Self: Into<PyObjectRef>,
{
const ERR_VALUE: *mut PyObject = core::ptr::null_mut();
fn into_output(self, _vm: &VirtualMachine) -> *mut PyObject {
self.into().into_raw().as_ptr()
}
}
impl FfiResult<*mut PyObject> for PyObjectRef {
const ERR_VALUE: *mut PyObject = core::ptr::null_mut();
fn into_output(self, _vm: &VirtualMachine) -> *mut PyObject {
self.into_raw().as_ptr()
}
}
impl FfiResult for *mut PyObject {
const ERR_VALUE: *mut PyObject = core::ptr::null_mut();
fn into_output(self, _vm: &VirtualMachine) -> *mut PyObject {
self
}
}
impl FfiResult<*mut PyObject> for *const PyObject {
const ERR_VALUE: *mut PyObject = core::ptr::null_mut();
fn into_output(self, _vm: &VirtualMachine) -> *mut PyObject {
self.cast_mut()
}
}
impl FfiResult for *mut c_void {
const ERR_VALUE: *mut c_void = core::ptr::null_mut();
fn into_output(self, _vm: &VirtualMachine) -> *mut c_void {
self
}
}
impl FfiResult<*mut c_char> for *const u8 {
const ERR_VALUE: *mut c_char = core::ptr::null_mut();
fn into_output(self, _vm: &VirtualMachine) -> *mut c_char {
self.cast_mut().cast()
}
}
impl FfiResult<isize> for usize {
const ERR_VALUE: isize = -1;
fn into_output(self, _vm: &VirtualMachine) -> isize {
self.try_into()
.expect("Output value is too large to fit into target type")
}
}
impl FfiResult for c_long {
const ERR_VALUE: c_long = -1;
fn into_output(self, _vm: &VirtualMachine) -> c_long {
self
}
}
impl FfiResult for c_double {
const ERR_VALUE: c_double = -1.0;
fn into_output(self, _vm: &VirtualMachine) -> c_double {
self
}
}
impl FfiResult<c_int> for bool {
const ERR_VALUE: c_int = -1;
fn into_output(self, _vm: &VirtualMachine) -> c_int {
self as c_int
}
}
impl FfiResult<()> for PyResult<Infallible> {
const ERR_VALUE: () = ();
fn into_output(self, vm: &VirtualMachine) {
match self {
Err(err) => vm.set_exception(Some(err)),
}
}
}
impl<Output, T> FfiResult<Output> for PyResult<T>
where
T: FfiResult<Output>,
{
const ERR_VALUE: Output = T::ERR_VALUE;
fn into_output(self, vm: &VirtualMachine) -> Output {
self.map_or_else(
|err| {
vm.set_exception(Some(err));
T::ERR_VALUE
},
|obj| obj.into_output(vm),
)
}
}
+1 -1
View File
@@ -1259,7 +1259,7 @@ impl PyType {
}
impl Py<PyType> {
pub(crate) fn is_subtype(&self, other: &Self) -> bool {
pub fn is_subtype(&self, other: &Self) -> bool {
is_subtype_with_mro(&self.mro.read(), self, other)
}
+15 -2
View File
@@ -2060,12 +2060,12 @@ impl VirtualMachine {
exc
}
pub(crate) fn current_exception(&self) -> Option<PyBaseExceptionRef> {
pub fn current_exception(&self) -> Option<PyBaseExceptionRef> {
self.exceptions.borrow().stack.last().cloned().flatten()
}
/// Set the current exc_info slot value (PUSH_EXC_INFO / POP_EXCEPT).
pub(crate) fn set_exception(&self, exc: Option<PyBaseExceptionRef>) {
pub fn set_exception(&self, exc: Option<PyBaseExceptionRef>) {
// don't be holding the RefCell guard while __del__ is called
let mut excs = self.exceptions.borrow_mut();
debug_assert!(
@@ -2084,6 +2084,19 @@ impl VirtualMachine {
thread::update_thread_exception(self.topmost_exception());
}
pub fn take_raised_exception(&self) -> Option<PyBaseExceptionRef> {
let mut excs = self.exceptions.borrow_mut();
if let Some(top) = excs.stack.last_mut() {
let exc = top.take();
drop(excs);
#[cfg(feature = "threading")]
thread::update_thread_exception(self.topmost_exception());
exc
} else {
None
}
}
pub(crate) fn contextualize_exception(&self, exception: &Py<PyBaseException>) {
if let Some(context_exc) = self.topmost_exception()
&& !context_exc.is(exception)
+1 -1
View File
@@ -121,7 +121,7 @@ pub fn run(mut builder: InterpreterBuilder) -> ExitCode {
let exitcode = cfg_select! {
feature = "capi" => {{
let local_vm = interp.enter(|vm| vm.new_thread());
*(rustpython_capi::get_main_interpreter()) = Some(interp);
rustpython_capi::init_main_interpreter(interp);
let result = local_vm.run(|vm| run_rustpython(vm, run_mode));
rustpython_capi::get_main_interpreter().take().unwrap().finalize(result.err())
}},