Skip to content

Commit fbb09f2

Browse files
Add basic capi error support
1 parent 22d4f43 commit fbb09f2

8 files changed

Lines changed: 445 additions & 8 deletions

File tree

crates/capi/src/lib.rs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,31 @@
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;
9-
10+
pub mod pyerrors;
1011
pub mod pylifecycle;
1112
pub mod pystate;
1213
pub mod refcount;
14+
mod util;
1315

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

crates/capi/src/pyerrors.rs

Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
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.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::<PyResult<Infallible>, _>(|_vm| {
120+
let exception =
121+
unsafe { PyObjectRef::from_raw(NonNull::new_unchecked(exc)).downcast_unchecked() };
122+
Err(exception)
123+
})
124+
}
125+
126+
#[unsafe(no_mangle)]
127+
pub unsafe extern "C" fn PyErr_SetObject(exception: *mut PyObject, value: *mut PyObject) {
128+
with_vm::<PyResult<Infallible>, _>(|vm| {
129+
let exc_type = unsafe { (&*exception).to_owned() };
130+
let exc_val = unsafe { (&*value).to_owned() };
131+
132+
let normalized = vm.normalize_exception(exc_type, exc_val, vm.ctx.none())?;
133+
Err(normalized)
134+
})
135+
}
136+
137+
#[unsafe(no_mangle)]
138+
pub unsafe extern "C" fn PyErr_SetString(exception: *mut PyObject, message: *const c_char) {
139+
with_vm::<PyResult<Infallible>, _>(|vm| {
140+
let exc_type = unsafe { &*exception }.try_downcast_ref::<PyType>(vm)?;
141+
142+
let message = unsafe { CStr::from_ptr(message) }
143+
.to_str()
144+
.expect("Exception message is not valid UTF-8");
145+
146+
let exc = vm.invoke_exception(
147+
exc_type.to_owned(),
148+
vec![vm.ctx.new_str(message).into_object()],
149+
)?;
150+
151+
Err(exc)
152+
})
153+
}
154+
155+
#[unsafe(no_mangle)]
156+
pub extern "C" fn PyErr_PrintEx(_set_sys_last_vars: c_int) {
157+
with_vm(|vm| {
158+
let exception = vm
159+
.take_raised_exception()
160+
.expect("No exception set in PyErr_PrintEx");
161+
162+
vm.print_exception(exception);
163+
})
164+
}
165+
166+
#[unsafe(no_mangle)]
167+
pub unsafe extern "C" fn PyErr_DisplayException(exc: *mut PyObject) {
168+
with_vm(|vm| {
169+
let exception = unsafe { &*exc }
170+
.downcast_ref::<PyBaseException>()
171+
.expect("PyErr_DisplayException exc must be an exception instance")
172+
.to_owned();
173+
174+
vm.print_exception(exception);
175+
})
176+
}
177+
178+
#[unsafe(no_mangle)]
179+
pub unsafe extern "C" fn PyErr_WriteUnraisable(obj: *mut PyObject) {
180+
with_vm(|vm| {
181+
let exception = vm
182+
.take_raised_exception()
183+
.expect("No exception set in PyErr_WriteUnraisable");
184+
185+
let object = unsafe { vm.unwrap_or_none(obj.as_ref().map(|obj| obj.to_owned())) };
186+
187+
vm.run_unraisable(exception, None, object)
188+
})
189+
}
190+
191+
#[unsafe(no_mangle)]
192+
pub unsafe extern "C" fn PyErr_NewException(
193+
name: *const c_char,
194+
base: *mut PyObject,
195+
dict: *mut PyObject,
196+
) -> *mut PyObject {
197+
with_vm(|vm| {
198+
let (module, name) = unsafe {
199+
CStr::from_ptr(name)
200+
.to_str()
201+
.expect("Exception name is not valid UTF-8")
202+
.rsplit_once('.')
203+
.expect("Exception name must be of the form 'module.ExceptionName'")
204+
};
205+
206+
let bases = unsafe { base.as_ref() }.map(|bases| {
207+
if let Some(ty) = bases.downcast_ref::<PyType>() {
208+
vec![ty.to_owned()]
209+
} else if let Some(tuple) = bases.downcast_ref::<PyTuple>() {
210+
tuple
211+
.iter()
212+
.map(|item| item.to_owned().downcast())
213+
.collect::<Result<Vec<_>, _>>()
214+
.expect("PyErr_NewException base tuple must contain only types")
215+
} else {
216+
panic!("PyErr_NewException base must be a type or a tuple of types");
217+
}
218+
});
219+
220+
assert!(
221+
dict.is_null(),
222+
"PyErr_NewException with non-null dict is not supported yet"
223+
);
224+
225+
vm.ctx.new_exception_type(module, name, bases)
226+
})
227+
}
228+
229+
#[unsafe(no_mangle)]
230+
pub unsafe extern "C" fn PyErr_NewExceptionWithDoc(
231+
name: *const c_char,
232+
_doc: *const c_char,
233+
base: *mut PyObject,
234+
dict: *mut PyObject,
235+
) -> *mut PyObject {
236+
unsafe { PyErr_NewException(name, base, dict) }
237+
}
238+
239+
#[unsafe(no_mangle)]
240+
pub unsafe extern "C" fn PyErr_GivenExceptionMatches(
241+
given: *mut PyObject,
242+
exc: *mut PyObject,
243+
) -> c_int {
244+
with_vm(|vm| {
245+
let given = unsafe { &*given };
246+
let exc = unsafe { &*exc };
247+
248+
given.is_subclass(exc, vm)
249+
})
250+
}
251+
252+
#[cfg(test)]
253+
mod tests {
254+
use pyo3::exceptions::PyTypeError;
255+
use pyo3::prelude::*;
256+
257+
#[test]
258+
fn test_raised_exception() {
259+
Python::attach(|py| {
260+
PyTypeError::new_err(py.None()).restore(py);
261+
assert!(PyErr::occurred(py));
262+
assert!(unsafe { !pyo3::ffi::PyErr_GetRaisedException().is_null() });
263+
assert!(!PyErr::occurred(py));
264+
})
265+
}
266+
}

crates/capi/src/pylifecycle.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
use crate::get_main_interpreter;
2+
use crate::pyerrors::init_exception_statics;
23
use crate::pystate::ensure_thread_has_vm_attached;
34
use core::ffi::c_int;
4-
use rustpython_vm::Interpreter;
55
use rustpython_vm::vm::thread::ThreadedVirtualMachine;
6+
use rustpython_vm::{Context, Interpreter};
67
use std::sync::Mutex;
78

89
pub(crate) static MAIN_INTERP: Mutex<Option<Interpreter>> = Mutex::new(None);
@@ -29,6 +30,8 @@ pub extern "C" fn Py_Initialize() {
2930
pub extern "C" fn Py_InitializeEx(_initsigs: c_int) {
3031
let mut interp = get_main_interpreter();
3132
if interp.is_none() {
33+
// Safety: Interpreter was not initialized before, so we can safely assume the statics are not used
34+
unsafe { init_exception_statics(&Context::genesis().exceptions) };
3235
*interp = Interpreter::with_init(Default::default(), |_vm| {}).into();
3336
drop(interp);
3437
ensure_thread_has_vm_attached();

crates/capi/src/pystate.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
11
use crate::pylifecycle::request_vm_from_interpreter;
2+
use crate::util::FfiResult;
23
use core::ffi::c_int;
34
use core::ptr;
5+
use rustpython_vm::VirtualMachine;
46
use rustpython_vm::vm::thread::{
5-
CurrentVmAttachState, attach_current_thread, release_current_thread,
7+
CurrentVmAttachState, attach_current_thread, release_current_thread, with_current_vm,
68
};
79

10+
#[allow(dead_code)]
11+
pub(crate) fn with_vm<R: FfiResult<O>, O>(f: impl FnOnce(&VirtualMachine) -> R) -> O {
12+
with_current_vm(|vm| f(vm).into_output(vm))
13+
}
14+
815
#[allow(non_camel_case_types)]
916
type PyGILState_STATE = c_int;
1017
const PYGILSTATE_LOCKED: PyGILState_STATE = 0;

0 commit comments

Comments
 (0)