diff --git a/.cspell.dict/cpython.txt b/.cspell.dict/cpython.txt index 3bbe7426c74..1fb1c56e029 100644 --- a/.cspell.dict/cpython.txt +++ b/.cspell.dict/cpython.txt @@ -157,6 +157,7 @@ pybuilddir pycore pyinner pydecimal +pyerrors Pyfunc pylifecycle pymain diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index cf31b2068fc..61e81a8e61d 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -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' diff --git a/crates/capi/src/lib.rs b/crates/capi/src/lib.rs index c6a250da724..4dc17536a8a 100644 --- a/crates/capi/src/lib.rs +++ b/crates/capi/src/lib.rs @@ -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> { @@ -17,3 +21,13 @@ pub fn get_main_interpreter() -> MutexGuard<'static, Option> { .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); +} diff --git a/crates/capi/src/object.rs b/crates/capi/src/object.rs new file mode 100644 index 00000000000..fb8fc3de54a --- /dev/null +++ b/crates/capi/src/object.rs @@ -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; + +#[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) + }) +} diff --git a/crates/capi/src/pyerrors.rs b/crates/capi/src/pyerrors.rs new file mode 100644 index 00000000000..4554757c66e --- /dev/null +++ b/crates/capi/src/pyerrors.rs @@ -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::, _>(|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::, _>(|vm| { + let exc_type = unsafe { &*exception }.try_downcast_ref::(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::() + .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::() { + vec![ty.to_owned()] + } else if let Some(tuple) = bases.downcast_ref::() { + tuple + .iter() + .map(|item| item.to_owned().downcast()) + .collect::, _>>() + .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::(py)); + }) + } +} diff --git a/crates/capi/src/pylifecycle.rs b/crates/capi/src/pylifecycle.rs index 6e986c98f4e..6760b2822a3 100644 --- a/crates/capi/src/pylifecycle.rs +++ b/crates/capi/src/pylifecycle.rs @@ -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> = 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(); diff --git a/crates/capi/src/pystate.rs b/crates/capi/src/pystate.rs index 107750be89e..97b29bcebe1 100644 --- a/crates/capi/src/pystate.rs +++ b/crates/capi/src/pystate.rs @@ -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, 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; diff --git a/crates/capi/src/util.rs b/crates/capi/src/util.rs new file mode 100644 index 00000000000..95e11ff576e --- /dev/null +++ b/crates/capi/src/util.rs @@ -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 { + 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 for () { + const ERR_VALUE: c_int = -1; + + fn into_output(self, _vm: &VirtualMachine) -> c_int { + 0 + } +} + +impl FfiResult<*mut PyObject> for PyRef +where + Self: Into, +{ + 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 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 for bool { + const ERR_VALUE: c_int = -1; + + fn into_output(self, _vm: &VirtualMachine) -> c_int { + self as c_int + } +} + +impl FfiResult<()> for PyResult { + const ERR_VALUE: () = (); + + fn into_output(self, vm: &VirtualMachine) { + match self { + Err(err) => vm.set_exception(Some(err)), + } + } +} + +impl FfiResult for PyResult +where + T: FfiResult, +{ + 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), + ) + } +} diff --git a/crates/vm/src/builtins/type.rs b/crates/vm/src/builtins/type.rs index 6208bc5ebfe..d535618982f 100644 --- a/crates/vm/src/builtins/type.rs +++ b/crates/vm/src/builtins/type.rs @@ -1263,7 +1263,7 @@ impl PyType { } impl Py { - 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) } diff --git a/crates/vm/src/vm/mod.rs b/crates/vm/src/vm/mod.rs index 3cfe4642907..e55a68fb805 100644 --- a/crates/vm/src/vm/mod.rs +++ b/crates/vm/src/vm/mod.rs @@ -2060,12 +2060,12 @@ impl VirtualMachine { exc } - pub(crate) fn current_exception(&self) -> Option { + pub fn current_exception(&self) -> Option { 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) { + pub fn set_exception(&self, exc: Option) { // 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 { + 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) { if let Some(context_exc) = self.topmost_exception() && !context_exc.is(exception) diff --git a/src/lib.rs b/src/lib.rs index a8fd5034d83..a8384244cfa 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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()) }},