Skip to content

Commit 4059a03

Browse files
Add basic capi error support (RustPython#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 bccd38e. * 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 b2c2f69. * `set_main_interpreter` -> `init_main_interpreter`
1 parent e8711ed commit 4059a03

11 files changed

Lines changed: 515 additions & 8 deletions

File tree

.cspell.dict/cpython.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@ pybuilddir
169169
pycore
170170
pyinner
171171
pydecimal
172+
pyerrors
172173
Pyfunc
173174
pylifecycle
174175
pymain

.github/workflows/ci.yaml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,10 +114,15 @@ jobs:
114114
uses: ./.github/actions/install-macos-deps
115115

116116
- name: run rust tests
117-
run: cargo test --workspace ${{ env.WORKSPACE_EXCLUDES }} --features threading ${{ env.CARGO_ARGS }}
117+
run: cargo test --workspace --exclude rustpython-capi ${{ env.WORKSPACE_EXCLUDES }} --features threading ${{ env.CARGO_ARGS }}
118118
env:
119119
INSTA_WORKSPACE_ROOT: ${{ github.workspace }}
120120

121+
- name: run c-api tests
122+
working-directory: crates/capi
123+
run: cargo test
124+
if: runner.os != 'Windows' # Requires pyo3 0.29+ on Windows
125+
121126
- run: cargo doc --locked
122127
if: runner.os == 'Linux'
123128

crates/capi/src/lib.rs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,33 @@
11
#![allow(clippy::missing_safety_doc)]
22

3+
use crate::pyerrors::init_exception_statics;
34
use crate::pylifecycle::MAIN_INTERP;
4-
use rustpython_vm::Interpreter;
55
pub use rustpython_vm::PyObject;
6+
use rustpython_vm::{Context, Interpreter};
67
use std::sync::MutexGuard;
78

89
extern crate alloc;
910

11+
pub mod object;
12+
pub mod pyerrors;
1013
pub mod pylifecycle;
1114
pub mod pystate;
1215
pub mod refcount;
16+
mod util;
1317

1418
/// Get main interpreter of this process. Will be None if it has not been initialized yet.
1519
pub fn get_main_interpreter() -> MutexGuard<'static, Option<Interpreter>> {
1620
MAIN_INTERP
1721
.lock()
1822
.expect("Failed to lock interpreter mutex")
1923
}
24+
25+
/// Set the main interpreter of this process. This method will panic when there is already an
26+
/// interpreter set.
27+
pub fn init_main_interpreter(interpreter: Interpreter) {
28+
let mut interp = get_main_interpreter();
29+
assert!(interp.is_none(), "Main interpreter is already set");
30+
// Safety: Interpreter was not initialized before, so we can safely assume the statics are not used
31+
unsafe { init_exception_statics(&Context::genesis().exceptions) };
32+
*interp = Some(interpreter);
33+
}

crates/capi/src/object.rs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
use crate::PyObject;
2+
use crate::pystate::with_vm;
3+
use core::ffi::{c_int, c_uint, c_ulong};
4+
use rustpython_vm::builtins::PyType;
5+
use rustpython_vm::{AsObject, Py};
6+
7+
pub type PyTypeObject = Py<PyType>;
8+
9+
#[unsafe(no_mangle)]
10+
pub unsafe extern "C" fn Py_TYPE(op: *mut PyObject) -> *const PyTypeObject {
11+
unsafe { (*op).class() }
12+
}
13+
14+
#[unsafe(no_mangle)]
15+
pub unsafe extern "C" fn Py_IS_TYPE(op: *mut PyObject, ty: *mut PyTypeObject) -> c_int {
16+
with_vm(|_vm| {
17+
let obj = unsafe { &*op };
18+
let ty = unsafe { &*ty };
19+
obj.class().is(ty)
20+
})
21+
}
22+
23+
#[unsafe(no_mangle)]
24+
pub unsafe extern "C" fn PyType_GetFlags(ptr: *const PyTypeObject) -> c_ulong {
25+
let ty = unsafe { &*ptr };
26+
ty.slots.flags.bits() as u32 as c_ulong
27+
}
28+
29+
#[unsafe(no_mangle)]
30+
pub extern "C" fn Py_GetConstantBorrowed(constant_id: c_uint) -> *mut PyObject {
31+
with_vm(|vm| {
32+
let ctx = &vm.ctx;
33+
let constant = match constant_id {
34+
0 => ctx.none.as_object(),
35+
1 => ctx.false_value.as_object(),
36+
2 => ctx.true_value.as_object(),
37+
3 => ctx.ellipsis.as_object(),
38+
4 => ctx.not_implemented.as_object(),
39+
_ => {
40+
return Err(
41+
vm.new_system_error("Invalid constant ID passed to Py_GetConstantBorrowed")
42+
);
43+
}
44+
}
45+
.as_raw();
46+
Ok(constant)
47+
})
48+
}

crates/capi/src/pyerrors.rs

Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
1+
use crate::PyObject;
2+
use crate::pystate::with_vm;
3+
use core::convert::Infallible;
4+
use core::ffi::{CStr, c_char, c_int};
5+
use core::ptr::NonNull;
6+
use rustpython_vm::builtins::{PyBaseException, PyTuple, PyType};
7+
use rustpython_vm::convert::IntoObject;
8+
use rustpython_vm::exceptions::ExceptionZoo;
9+
use rustpython_vm::{AsObject, PyObjectRef, PyResult};
10+
11+
macro_rules! define_exception_statics {
12+
($( $(#[$meta:meta])* $export:ident => $exc:ident ),* $(,)?) => {
13+
$(
14+
$(#[$meta])*
15+
#[unsafe(no_mangle)]
16+
pub static mut $export: *mut PyObject = core::ptr::null_mut();
17+
)*
18+
19+
#[allow(static_mut_refs)]
20+
pub(crate) unsafe fn init_exception_statics(zoo: &'static ExceptionZoo) {
21+
unsafe {
22+
$(
23+
$export = zoo.$exc.as_object().as_raw().cast_mut();
24+
)*
25+
}
26+
}
27+
};
28+
}
29+
30+
define_exception_statics! {
31+
PyExc_BaseException => base_exception_type,
32+
PyExc_BaseExceptionGroup => base_exception_group,
33+
PyExc_SystemExit => system_exit,
34+
PyExc_KeyboardInterrupt => keyboard_interrupt,
35+
PyExc_GeneratorExit => generator_exit,
36+
PyExc_Exception => exception_type,
37+
PyExc_StopIteration => stop_iteration,
38+
PyExc_StopAsyncIteration => stop_async_iteration,
39+
PyExc_ArithmeticError => arithmetic_error,
40+
PyExc_FloatingPointError => floating_point_error,
41+
PyExc_SystemError => system_error,
42+
PyExc_TypeError => type_error,
43+
PyExc_OverflowError => overflow_error,
44+
PyExc_ZeroDivisionError => zero_division_error,
45+
PyExc_AssertionError => assertion_error,
46+
PyExc_IndexError => index_error,
47+
PyExc_KeyError => key_error,
48+
PyExc_LookupError => lookup_error,
49+
PyExc_AttributeError => attribute_error,
50+
PyExc_BufferError => buffer_error,
51+
PyExc_EOFError => eof_error,
52+
PyExc_ImportError => import_error,
53+
PyExc_ModuleNotFoundError => module_not_found_error,
54+
PyExc_MemoryError => memory_error,
55+
PyExc_NameError => name_error,
56+
PyExc_UnboundLocalError => unbound_local_error,
57+
PyExc_OSError => os_error,
58+
PyExc_BlockingIOError => blocking_io_error,
59+
PyExc_ChildProcessError => child_process_error,
60+
PyExc_ConnectionError => connection_error,
61+
PyExc_BrokenPipeError => broken_pipe_error,
62+
PyExc_ConnectionAbortedError => connection_aborted_error,
63+
PyExc_ConnectionRefusedError => connection_refused_error,
64+
PyExc_ConnectionResetError => connection_reset_error,
65+
PyExc_FileExistsError => file_exists_error,
66+
PyExc_FileNotFoundError => file_not_found_error,
67+
PyExc_InterruptedError => interrupted_error,
68+
PyExc_IsADirectoryError => is_a_directory_error,
69+
PyExc_NotADirectoryError => not_a_directory_error,
70+
PyExc_PermissionError => permission_error,
71+
PyExc_ProcessLookupError => process_lookup_error,
72+
PyExc_TimeoutError => timeout_error,
73+
PyExc_ReferenceError => reference_error,
74+
PyExc_RuntimeError => runtime_error,
75+
PyExc_NotImplementedError => not_implemented_error,
76+
PyExc_RecursionError => recursion_error,
77+
PyExc_SyntaxError => syntax_error,
78+
PyExc_IndentationError => indentation_error,
79+
PyExc_TabError => tab_error,
80+
PyExc_ValueError => value_error,
81+
PyExc_UnicodeError => unicode_error,
82+
PyExc_UnicodeDecodeError => unicode_decode_error,
83+
PyExc_UnicodeEncodeError => unicode_encode_error,
84+
PyExc_UnicodeTranslateError => unicode_translate_error,
85+
PyExc_Warning => warning,
86+
PyExc_DeprecationWarning => deprecation_warning,
87+
PyExc_PendingDeprecationWarning => pending_deprecation_warning,
88+
PyExc_RuntimeWarning => runtime_warning,
89+
PyExc_SyntaxWarning => syntax_warning,
90+
PyExc_UserWarning => user_warning,
91+
PyExc_FutureWarning => future_warning,
92+
PyExc_ImportWarning => import_warning,
93+
PyExc_UnicodeWarning => unicode_warning,
94+
PyExc_BytesWarning => bytes_warning,
95+
PyExc_ResourceWarning => resource_warning,
96+
PyExc_EncodingWarning => encoding_warning,
97+
}
98+
99+
#[unsafe(no_mangle)]
100+
pub extern "C" fn PyErr_Occurred() -> *mut PyObject {
101+
with_vm(|vm| {
102+
vm.current_exception()
103+
.map(|exc| exc.class().as_object().as_raw())
104+
.unwrap_or_default()
105+
})
106+
}
107+
108+
#[unsafe(no_mangle)]
109+
pub extern "C" fn PyErr_GetRaisedException() -> *mut PyObject {
110+
with_vm(|vm| {
111+
vm.take_raised_exception()
112+
.map(|exc| exc.into_object().into_raw().as_ptr())
113+
.unwrap_or_default()
114+
})
115+
}
116+
117+
#[unsafe(no_mangle)]
118+
pub unsafe extern "C" fn PyErr_SetRaisedException(exc: *mut PyObject) {
119+
with_vm(|vm| {
120+
if let Some(exc) = NonNull::new(exc) {
121+
let exception = unsafe { PyObjectRef::from_raw(exc).downcast_unchecked() };
122+
vm.set_exception(Some(exception));
123+
} else {
124+
vm.set_exception(None);
125+
}
126+
})
127+
}
128+
129+
#[unsafe(no_mangle)]
130+
pub unsafe extern "C" fn PyErr_SetObject(exception: *mut PyObject, value: *mut PyObject) {
131+
with_vm::<PyResult<Infallible>, _>(|vm| {
132+
let exc_type = unsafe { (&*exception).to_owned() };
133+
let exc_val = unsafe { (&*value).to_owned() };
134+
135+
let normalized = vm.normalize_exception(exc_type, exc_val, vm.ctx.none())?;
136+
Err(normalized)
137+
})
138+
}
139+
140+
#[unsafe(no_mangle)]
141+
pub unsafe extern "C" fn PyErr_SetString(exception: *mut PyObject, message: *const c_char) {
142+
with_vm::<PyResult<Infallible>, _>(|vm| {
143+
let exc_type = unsafe { &*exception }.try_downcast_ref::<PyType>(vm)?;
144+
145+
let Ok(message) = unsafe { CStr::from_ptr(message) }.to_str() else {
146+
return Err(vm.new_type_error("Exception message is not valid UTF-8"));
147+
};
148+
149+
let exc = vm.invoke_exception(
150+
exc_type.to_owned(),
151+
vec![vm.ctx.new_str(message).into_object()],
152+
)?;
153+
154+
Err(exc)
155+
})
156+
}
157+
158+
#[unsafe(no_mangle)]
159+
pub extern "C" fn PyErr_PrintEx(_set_sys_last_vars: c_int) {
160+
with_vm(|vm| {
161+
let exception = vm
162+
.take_raised_exception()
163+
.expect("No exception set in PyErr_PrintEx");
164+
165+
vm.print_exception(exception);
166+
})
167+
}
168+
169+
#[unsafe(no_mangle)]
170+
pub unsafe extern "C" fn PyErr_DisplayException(exc: *mut PyObject) {
171+
with_vm(|vm| {
172+
let exception = unsafe { &*exc }
173+
.downcast_ref::<PyBaseException>()
174+
.expect("PyErr_DisplayException exc must be an exception instance")
175+
.to_owned();
176+
177+
vm.print_exception(exception);
178+
})
179+
}
180+
181+
#[unsafe(no_mangle)]
182+
pub unsafe extern "C" fn PyErr_WriteUnraisable(obj: *mut PyObject) {
183+
with_vm(|vm| {
184+
let exception = vm
185+
.take_raised_exception()
186+
.expect("No exception set in PyErr_WriteUnraisable");
187+
188+
let object = unsafe { vm.unwrap_or_none(obj.as_ref().map(|obj| obj.to_owned())) };
189+
190+
vm.run_unraisable(exception, None, object)
191+
})
192+
}
193+
194+
#[unsafe(no_mangle)]
195+
pub unsafe extern "C" fn PyErr_NewException(
196+
name: *const c_char,
197+
base: *mut PyObject,
198+
dict: *mut PyObject,
199+
) -> *mut PyObject {
200+
with_vm(|vm| {
201+
let (module, name) = unsafe {
202+
CStr::from_ptr(name)
203+
.to_str()
204+
.expect("Exception name is not valid UTF-8")
205+
.rsplit_once('.')
206+
.expect("Exception name must be of the form 'module.ExceptionName'")
207+
};
208+
209+
let bases = unsafe { base.as_ref() }.map(|bases| {
210+
if let Some(ty) = bases.downcast_ref::<PyType>() {
211+
vec![ty.to_owned()]
212+
} else if let Some(tuple) = bases.downcast_ref::<PyTuple>() {
213+
tuple
214+
.iter()
215+
.map(|item| item.to_owned().downcast())
216+
.collect::<Result<Vec<_>, _>>()
217+
.expect("PyErr_NewException base tuple must contain only types")
218+
} else {
219+
panic!("PyErr_NewException base must be a type or a tuple of types");
220+
}
221+
});
222+
223+
assert!(
224+
dict.is_null(),
225+
"PyErr_NewException with non-null dict is not supported yet"
226+
);
227+
228+
vm.ctx.new_exception_type(module, name, bases)
229+
})
230+
}
231+
232+
#[unsafe(no_mangle)]
233+
pub unsafe extern "C" fn PyErr_NewExceptionWithDoc(
234+
name: *const c_char,
235+
_doc: *const c_char,
236+
base: *mut PyObject,
237+
dict: *mut PyObject,
238+
) -> *mut PyObject {
239+
unsafe { PyErr_NewException(name, base, dict) }
240+
}
241+
242+
#[unsafe(no_mangle)]
243+
pub unsafe extern "C" fn PyErr_GivenExceptionMatches(
244+
given: *mut PyObject,
245+
exc: *mut PyObject,
246+
) -> c_int {
247+
with_vm(|vm| {
248+
let given = unsafe { &*given };
249+
let exc = unsafe { &*exc };
250+
251+
given.is_subclass(exc, vm)
252+
})
253+
}
254+
255+
#[cfg(test)]
256+
mod tests {
257+
use pyo3::exceptions::PyTypeError;
258+
use pyo3::prelude::*;
259+
260+
#[test]
261+
fn test_raised_exception() {
262+
Python::attach(|py| {
263+
PyTypeError::new_err(py.None()).restore(py);
264+
assert!(PyErr::occurred(py));
265+
assert!(unsafe { !pyo3::ffi::PyErr_GetRaisedException().is_null() });
266+
assert!(!PyErr::occurred(py));
267+
})
268+
}
269+
270+
#[test]
271+
fn test_error_is_instance() {
272+
Python::attach(|py| {
273+
let err = PyTypeError::new_err(py.None());
274+
assert!(err.is_instance_of::<PyTypeError>(py));
275+
})
276+
}
277+
}

0 commit comments

Comments
 (0)