diff --git a/Lib/ctypes/__init__.py b/Lib/ctypes/__init__.py index 7223fc499..0b84a27f8 100644 --- a/Lib/ctypes/__init__.py +++ b/Lib/ctypes/__init__.py @@ -2,15 +2,13 @@ import os as _os import sys as _sys -import sysconfig as _sysconfig import types as _types -__version__ = "1.1.0" +lazy import sysconfig as _sysconfig from _ctypes import Union, Structure, Array from _ctypes import _Pointer from _ctypes import CFuncPtr as _CFuncPtr -from _ctypes import __version__ as _ctypes_version from _ctypes import RTLD_LOCAL, RTLD_GLOBAL from _ctypes import ArgumentError from _ctypes import SIZEOF_TIME_T @@ -18,9 +16,6 @@ from _ctypes import CField from struct import calcsize as _calcsize -if __version__ != _ctypes_version: - raise Exception("Version number mismatch", __version__, _ctypes_version) - if _os.name == "nt": from _ctypes import COMError, CopyComPointer, FormatError @@ -211,13 +206,13 @@ if sizeof(c_longdouble) == sizeof(c_double): try: class c_double_complex(_SimpleCData): - _type_ = "D" + _type_ = "Zd" _check_size(c_double_complex) class c_float_complex(_SimpleCData): - _type_ = "F" + _type_ = "Zf" _check_size(c_float_complex) class c_longdouble_complex(_SimpleCData): - _type_ = "G" + _type_ = "Zg" except AttributeError: pass @@ -381,12 +376,6 @@ def create_unicode_buffer(init, size=None): return buf raise TypeError(init) - -def SetPointerType(pointer, cls): - import warnings - warnings._deprecated("ctypes.SetPointerType", remove=(3, 15)) - pointer.set_type(cls) - def ARRAY(typ, len): return typ * len @@ -681,3 +670,12 @@ else: raise SystemError(f"Unexpected sizeof(time_t): {SIZEOF_TIME_T=}") _reset_cache() + + +def __getattr__(name): + if name == "__version__": + from warnings import _deprecated + + _deprecated("__version__", remove=(3, 20)) + return "1.1.0" # Do not change + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/Lib/ctypes/_layout.py b/Lib/ctypes/_layout.py index 2048ccb6a..61b4c6c70 100644 --- a/Lib/ctypes/_layout.py +++ b/Lib/ctypes/_layout.py @@ -5,11 +5,12 @@ may change at any time. """ import sys -import warnings from _ctypes import CField, buffer_info import ctypes +lazy import warnings + def round_down(n, multiple): assert n >= 0 assert multiple > 0 diff --git a/Lib/ctypes/util.py b/Lib/ctypes/util.py index 3b2165843..015c62b31 100644 --- a/Lib/ctypes/util.py +++ b/Lib/ctypes/util.py @@ -1,8 +1,9 @@ import os -import shutil -import subprocess import sys +lazy import shutil +lazy import subprocess + # find_library(name) returns the pathname of a library, or None. if os.name == "nt": @@ -421,8 +422,7 @@ elif os.name == "posix": result = None try: p = subprocess.Popen(cmd, stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - universal_newlines=True) + stderr=subprocess.PIPE) out, _ = p.communicate() res = re.findall(expr, os.fsdecode(out)) for file in res: @@ -442,53 +442,12 @@ elif os.name == "posix": _get_soname(_findLib_gcc(name)) or _get_soname(_findLib_ld(name)) -# Listing loaded libraries on other systems will try to use -# functions common to Linux and a few other Unix-like systems. -# See the following for several platforms' documentation of the same API: -# https://man7.org/linux/man-pages/man3/dl_iterate_phdr.3.html -# https://man.freebsd.org/cgi/man.cgi?query=dl_iterate_phdr -# https://man.openbsd.org/dl_iterate_phdr -# https://docs.oracle.com/cd/E88353_01/html/E37843/dl-iterate-phdr-3c.html -if (os.name == "posix" and - sys.platform not in {"darwin", "ios", "tvos", "watchos"}): - import ctypes - if hasattr((_libc := ctypes.CDLL(None)), "dl_iterate_phdr"): - - class _dl_phdr_info(ctypes.Structure): - _fields_ = [ - ("dlpi_addr", ctypes.c_void_p), - ("dlpi_name", ctypes.c_char_p), - ("dlpi_phdr", ctypes.c_void_p), - ("dlpi_phnum", ctypes.c_ushort), - ] - - _dl_phdr_callback = ctypes.CFUNCTYPE( - ctypes.c_int, - ctypes.POINTER(_dl_phdr_info), - ctypes.c_size_t, - ctypes.POINTER(ctypes.py_object), - ) - - @_dl_phdr_callback - def _info_callback(info, _size, data): - libraries = data.contents.value - name = os.fsdecode(info.contents.dlpi_name) - libraries.append(name) - return 0 - - _dl_iterate_phdr = _libc["dl_iterate_phdr"] - _dl_iterate_phdr.argtypes = [ - _dl_phdr_callback, - ctypes.POINTER(ctypes.py_object), - ] - _dl_iterate_phdr.restype = ctypes.c_int - - def dllist(): - """Return a list of loaded shared libraries in the current process.""" - libraries = [] - _dl_iterate_phdr(_info_callback, - ctypes.byref(ctypes.py_object(libraries))) - return libraries +# On platforms which provide dl_iterate_phdr(), dllist() is implemented +# in _ctypes. +try: + from _ctypes import dllist +except ImportError: + pass ################################################################ # test code diff --git a/Lib/test/test_ctypes/__init__.py b/Lib/test/test_ctypes/__init__.py index eb9126cbe..d848beb1f 100644 --- a/Lib/test/test_ctypes/__init__.py +++ b/Lib/test/test_ctypes/__init__.py @@ -1,4 +1,5 @@ import os +import unittest from test import support from test.support import import_helper @@ -6,5 +7,21 @@ from test.support import import_helper # skip tests if the _ctypes extension was not built import_helper.import_module('ctypes') + +class TestModule(unittest.TestCase): + def test_deprecated__version__(self): + import ctypes + import _ctypes + + for mod in (ctypes, _ctypes): + with self.subTest(mod=mod): + with self.assertWarnsRegex( + DeprecationWarning, + "'__version__' is deprecated and slated for removal in Python 3.20", + ) as cm: + getattr(mod, "__version__") + self.assertEqual(cm.filename, __file__) + + def load_tests(*args): return support.load_package_tests(os.path.dirname(__file__), *args) diff --git a/Lib/test/test_ctypes/_support.py b/Lib/test/test_ctypes/_support.py index 946d654a1..700657a4e 100644 --- a/Lib/test/test_ctypes/_support.py +++ b/Lib/test/test_ctypes/_support.py @@ -3,7 +3,6 @@ import ctypes from _ctypes import Structure, Union, _Pointer, Array, _SimpleCData, CFuncPtr import sys -from test import support _CData = Structure.__base__ diff --git a/Lib/test/test_ctypes/test_as_parameter.py b/Lib/test/test_ctypes/test_as_parameter.py index 2da1acfcf..abbfb6efd 100644 --- a/Lib/test/test_ctypes/test_as_parameter.py +++ b/Lib/test/test_ctypes/test_as_parameter.py @@ -5,7 +5,8 @@ from ctypes import (Structure, CDLL, CFUNCTYPE, c_short, c_int, c_long, c_longlong, c_byte, c_wchar, c_float, c_double, ArgumentError) -from test.support import import_helper, skip_if_sanitizer +from test.support import (import_helper, skip_if_sanitizer, + skip_emscripten_stack_overflow, skip_if_huge_c_stack) _ctypes_test = import_helper.import_module("_ctypes_test") @@ -193,6 +194,8 @@ class BasicWrapTestCase(unittest.TestCase): (9*2, 8*3, 7*4, 6*5, 5*6, 4*7, 3*8, 2*9)) @skip_if_sanitizer('requires deep stack', thread=True) + @skip_if_huge_c_stack() + @skip_emscripten_stack_overflow() def test_recursive_as_param(self): class A: pass diff --git a/Lib/test/test_ctypes/test_c_simple_type_meta.py b/Lib/test/test_ctypes/test_c_simple_type_meta.py index fd261acf4..3b4fcf9c4 100644 --- a/Lib/test/test_ctypes/test_c_simple_type_meta.py +++ b/Lib/test/test_ctypes/test_c_simple_type_meta.py @@ -213,14 +213,12 @@ class PyCSimpleTypeAsMetaclassTest(unittest.TestCase): class F(metaclass=PyCSimpleType): _type_ = "\0" message = str(cm.exception) - expected_type_chars = list('cbBhHiIlLdDFGfuzZqQPXOv?g') - if not hasattr(ctypes, 'c_float_complex'): - expected_type_chars.remove('F') - expected_type_chars.remove('D') - expected_type_chars.remove('G') + expected_type_chars = list('cbBhHiIlLdfuzZqQPXOv?g') if not MS_WINDOWS: expected_type_chars.remove('X') self.assertIn("'" + ''.join(expected_type_chars) + "'", message) + if hasattr(ctypes, 'c_float_complex'): + self.assertIn("'Zf', 'Zd', 'Zg'", message) def test_creating_pointer_in_dunder_init_3(self): """Check if interfcase subclasses properly creates according internal diff --git a/Lib/test/test_ctypes/test_callbacks.py b/Lib/test/test_ctypes/test_callbacks.py index 6c7c2e527..d1ac62816 100644 --- a/Lib/test/test_ctypes/test_callbacks.py +++ b/Lib/test/test_ctypes/test_callbacks.py @@ -328,6 +328,21 @@ class SampleCallbacksTestCase(unittest.TestCase): f"of ctypes callback function {func!r}") self.assertIsNone(cm.unraisable.object) + def test_narrow_int_return_widened(self): + # gh-156933: Narrow integers were not widened on s390x + CALLBACK = CFUNCTYPE(c_int) + + dll = CDLL(_ctypes_test.__file__) + _testfunc_callback_int_to_longlong = dll._testfunc_callback_int_to_longlong + _testfunc_callback_int_to_longlong.argtypes = [CALLBACK] + _testfunc_callback_int_to_longlong.restype = c_longlong + + @CALLBACK + def cb(): + return -1 + + self.assertEqual(_testfunc_callback_int_to_longlong(cb), -1) + if __name__ == '__main__': unittest.main() diff --git a/Lib/test/test_ctypes/test_dllist.py b/Lib/test/test_ctypes/test_dllist.py index 53f077be0..10e0f261b 100644 --- a/Lib/test/test_ctypes/test_dllist.py +++ b/Lib/test/test_ctypes/test_dllist.py @@ -26,7 +26,7 @@ else: ) class ListSharedLibraries(unittest.TestCase): - @unittest.skipIf(not APPLE, "TODO: RUSTPYTHON") + @unittest.skipIf(not APPLE and sys.platform != "linux", "TODO: RUSTPYTHON") def test_lists_system(self): dlls = ctypes.util.dllist() diff --git a/Lib/test/test_ctypes/test_find.py b/Lib/test/test_ctypes/test_find.py index 8bc84c3d2..92175cc38 100644 --- a/Lib/test/test_ctypes/test_find.py +++ b/Lib/test/test_ctypes/test_find.py @@ -1,4 +1,5 @@ import os.path +import subprocess import sys import test.support import unittest @@ -78,9 +79,24 @@ class Test_OpenGL_libs(unittest.TestCase): @unittest.skipUnless(sys.platform.startswith('linux'), 'Test only valid for Linux') class FindLibraryLinux(unittest.TestCase): + @classmethod + def setUpClass(cls): + try: + p = subprocess.run(['ld', '--version'], + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, + text=True) + except OSError: + pass + else: + if p.stdout.startswith('mold '): + # The mold linker is known to be incompatible with + # the soft-deprecated ctypes.util.find_library + # (which uses `ld -t`). + raise unittest.SkipTest('Fails when ld is mold') + @thread_unsafe('uses setenv') def test_find_on_libpath(self): - import subprocess import tempfile try: diff --git a/Lib/test/test_ctypes/test_generated_structs.py b/Lib/test/test_ctypes/test_generated_structs.py index aa448fad5..1cb46a827 100644 --- a/Lib/test/test_ctypes/test_generated_structs.py +++ b/Lib/test/test_ctypes/test_generated_structs.py @@ -10,7 +10,7 @@ Run this module to regenerate the files: """ import unittest -from test.support import import_helper, verbose +from test.support import import_helper import re from dataclasses import dataclass from functools import cached_property diff --git a/Lib/test/test_ctypes/test_incomplete.py b/Lib/test/test_ctypes/test_incomplete.py index fefdfe910..8f6316d6f 100644 --- a/Lib/test/test_ctypes/test_incomplete.py +++ b/Lib/test/test_ctypes/test_incomplete.py @@ -1,7 +1,6 @@ import ctypes import unittest -import warnings -from ctypes import Structure, POINTER, pointer, c_char_p +from ctypes import Structure, POINTER, pointer, c_char_p, c_int # String-based "incomplete pointers" were implemented in ctypes 0.6.3 (2003, when # ctypes was an external project). They made obsolete by the current @@ -21,9 +20,7 @@ class TestSetPointerType(unittest.TestCase): _fields_ = [("name", c_char_p), ("next", lpcell)] - with warnings.catch_warnings(): - warnings.simplefilter('ignore', DeprecationWarning) - ctypes.SetPointerType(lpcell, cell) + lpcell.set_type(cell) self.assertIs(POINTER(cell), lpcell) @@ -50,10 +47,32 @@ class TestSetPointerType(unittest.TestCase): _fields_ = [("name", c_char_p), ("next", lpcell)] - with self.assertWarns(DeprecationWarning): - ctypes.SetPointerType(lpcell, cell) - + lpcell.set_type(cell) self.assertIs(POINTER(cell), lpcell) + def test_set_type_updates_format(self): + # gh-142966: set_type should update StgInfo.format + # to match the element type's format + with self.assertWarns(DeprecationWarning): + lp = POINTER("node") + + class node(Structure): + _fields_ = [("value", c_int)] + + # Get the expected format before set_type + node_format = memoryview(node()).format + expected_format = "&" + node_format + + lp.set_type(node) + + # Create instance to check format via memoryview + n = node(42) + p = lp(n) + actual_format = memoryview(p).format + + # After set_type, the pointer's format should be "&" + self.assertEqual(actual_format, expected_format) + + if __name__ == '__main__': unittest.main() diff --git a/Lib/test/test_ctypes/test_numbers.py b/Lib/test/test_ctypes/test_numbers.py index c57c58eb0..b7df08f60 100644 --- a/Lib/test/test_ctypes/test_numbers.py +++ b/Lib/test/test_ctypes/test_numbers.py @@ -119,8 +119,11 @@ class NumberTestCase(unittest.TestCase, ComplexesAreIdenticalMixin): @unittest.skipUnless(hasattr(ctypes, "c_double_complex"), "requires C11 complex type") def test_complex(self): - for t in [ctypes.c_double_complex, ctypes.c_float_complex, - ctypes.c_longdouble_complex]: + for format, t in [ + ('Zd', ctypes.c_double_complex), + ('Zf', ctypes.c_float_complex), + ('Zg', ctypes.c_longdouble_complex), + ]: self.assertEqual(t(1).value, 1+0j) self.assertEqual(t(1.0).value, 1+0j) self.assertEqual(t(1+0.125j).value, 1+0.125j) @@ -128,6 +131,12 @@ class NumberTestCase(unittest.TestCase, ComplexesAreIdenticalMixin): self.assertEqual(t(FloatLike()).value, 2+0j) self.assertEqual(t(ComplexLike()).value, 1+1j) + prefix = '>' if sys.byteorder == 'big' else '<' + num = t(1.0) + self.assertEqual(memoryview(num).format, prefix + format) + array = (t * 3)() + self.assertEqual(memoryview(array).format, prefix + format) + @unittest.skipUnless(hasattr(ctypes, "c_double_complex"), "requires C11 complex type") def test_complex_round_trip(self): diff --git a/Lib/test/test_ctypes/test_parameters.py b/Lib/test/test_ctypes/test_parameters.py index 46f8ff93e..6dadb7b41 100644 --- a/Lib/test/test_ctypes/test_parameters.py +++ b/Lib/test/test_ctypes/test_parameters.py @@ -1,6 +1,7 @@ import sys import unittest import test.support +import ctypes from ctypes import (CDLL, PyDLL, ArgumentError, Structure, Array, Union, _Pointer, _SimpleCData, _CFuncPtr, @@ -247,6 +248,13 @@ class SimpleTypesTestCase(unittest.TestCase): self.assertRegex(repr(c_char_p.from_param(b'hihi')), r"^$") self.assertRegex(repr(c_wchar_p.from_param('hihi')), r"^$") self.assertRegex(repr(c_void_p.from_param(0x12)), r"^$") + if hasattr(ctypes, 'c_double_complex'): + self.assertRegex(repr(ctypes.c_double_complex.from_param(0)), + r"^$") + self.assertRegex(repr(ctypes.c_float_complex.from_param(0)), + r"^$") + self.assertRegex(repr(ctypes.c_longdouble_complex.from_param(0)), + r"^$") @test.support.cpython_only def test_from_param_result_refcount(self): diff --git a/Lib/test/test_ctypes/test_structures.py b/Lib/test/test_ctypes/test_structures.py index 92d4851d7..65ccc1262 100644 --- a/Lib/test/test_ctypes/test_structures.py +++ b/Lib/test/test_ctypes/test_structures.py @@ -299,9 +299,16 @@ class StructureTestCase(unittest.TestCase, StructCheckMixin): self.assertEqual(s.first, got.first) self.assertEqual(s.second, got.second) + @unittest.skipIf(support.is_wasm32, "wasm ABI is incompatible with test expectations") def _test_issue18060(self, Vector): # Regression tests for gh-62260 + # This test passes a struct of two doubles by value to atan2(), whose C + # signature is atan2(double, double), so it only works on platforms + # where the abi of a function that takes a struct with two doubles + # matches the abi of a function that takes two doubles. The wasm32 ABI + # does not satisfy this condition and the test breaks. + # The call to atan2() should succeed if the # class fields were correctly cloned in the # subclasses. Otherwise, it will segfault. diff --git a/crates/capi/src/pyerrors.rs b/crates/capi/src/pyerrors.rs index 0bf843aa4..715adb137 100644 --- a/crates/capi/src/pyerrors.rs +++ b/crates/capi/src/pyerrors.rs @@ -101,7 +101,7 @@ define_py_check!(fn PyExceptionInstance_Check, exceptions.base_exception_type); #[unsafe(no_mangle)] pub extern "C" fn PyErr_Occurred() -> *mut PyObject { with_vm(|vm| { - vm.current_exception() + vm.raised_exception() .map(|exc| exc.class().as_object().as_raw()) .unwrap_or_default() }) @@ -121,7 +121,7 @@ pub unsafe extern "C" fn PyErr_SetRaisedException(exc: *mut PyObject) { with_vm(|vm| { let exception = unsafe { exc.assume_owned_or_opt() }.map(|exc| unsafe { exc.downcast_unchecked() }); - vm.set_exception(exception); + vm.set_raised_exception(exception); }) } @@ -131,6 +131,15 @@ pub unsafe extern "C" fn PyErr_SetObject(exception: *mut PyObject, value: *mut P let exc_type = unsafe { exception.assume_borrowed() }.to_owned(); let exc_val = unsafe { value.assume_borrowed() }.to_owned(); + if let Some(cls) = exc_type.downcast_ref::() + && cls.fast_issubclass(vm.ctx.exceptions.base_exception_type) + && !exc_val + .downcast_ref::() + .is_some_and(|exc| exc.fast_isinstance(cls)) + { + // Exception constructors must run without a pending C error. + vm.set_raised_exception(None); + } let normalized = vm.normalize_exception(exc_type, exc_val, vm.ctx.none())?; Err(normalized) }) @@ -142,6 +151,7 @@ pub unsafe extern "C" fn PyErr_SetString(exception: *mut PyObject, message: *con let exc_type = unsafe { exception.assume_borrowed_and_cast::(vm) }?; let message = unsafe { message.try_as_str(vm) }?; + vm.set_raised_exception(None); let exc = vm.invoke_exception(exc_type, vec![vm.ctx.new_str(message).into_object()])?; Err(exc) diff --git a/crates/capi/src/util.rs b/crates/capi/src/util.rs index 45fc3c1e8..b3fc2c39a 100644 --- a/crates/capi/src/util.rs +++ b/crates/capi/src/util.rs @@ -186,7 +186,7 @@ impl FfiResult<()> for PyResult { fn into_output(self, vm: &VirtualMachine) { match self { - Err(err) => vm.set_exception(Some(err)), + Err(err) => vm.set_raised_exception(Some(err)), } } } @@ -200,7 +200,7 @@ where fn into_output(self, vm: &VirtualMachine) -> Output { self.map_or_else( |err| { - vm.set_exception(Some(err)); + vm.set_raised_exception(Some(err)); T::ERR_VALUE }, |obj| obj.into_output(vm), diff --git a/crates/host_env/src/ctypes.rs b/crates/host_env/src/ctypes.rs index 5c1a3f70b..25f921853 100644 --- a/crates/host_env/src/ctypes.rs +++ b/crates/host_env/src/ctypes.rs @@ -221,6 +221,36 @@ const LONG_DOUBLE_SIZE: usize = core::mem::size_of::(); )))] const LONG_DOUBLE_SIZE: usize = core::mem::size_of::(); +/// Return the loader's current shared-object names, including its empty main entry. +#[cfg(any(target_os = "linux", target_os = "android"))] +pub fn dllist() -> Vec { + use std::os::unix::ffi::OsStringExt; + + unsafe extern "C" fn collect( + info: *mut libc::dl_phdr_info, + _size: libc::size_t, + data: *mut c_void, + ) -> c_int { + // dl_iterate_phdr supplies a valid record and calls synchronously while + // the loader keeps the object and its NUL-terminated name alive. + let name = unsafe { (*info).dlpi_name }; + let names = unsafe { &mut *data.cast::>() }; + if !name.is_null() { + let bytes = unsafe { CStr::from_ptr(name) }.to_bytes().to_vec(); + names.push(std::ffi::OsString::from_vec(bytes)); + } + 0 + } + + let mut names = Vec::new(); + // The callback only copies loader-owned bytes into this live, exclusive + // vector. It never invokes Python or mutates the loader's object list. + unsafe { + libc::dl_iterate_phdr(Some(collect), core::ptr::from_mut(&mut names).cast()); + } + names +} + pub fn simple_type_size(ty: &str) -> Option { match ty { "c" | "b" => Some(core::mem::size_of::()), @@ -2011,7 +2041,7 @@ pub enum CallRet<'a> { Aggregate(&'a CTypeLayout), } -/// Per-call error-swapping options. +/// Per-call ABI and error-swapping options. #[cfg(all( any( target_os = "linux", @@ -2023,6 +2053,10 @@ pub enum CallRet<'a> { ))] #[derive(Debug, Clone, Copy, Default)] pub struct CallOptions { + /// Declared argument count for a variadic call. Values must already have + /// their promoted C types; libffi rejects explicit narrow ctypes values. + /// `None` uses a fixed-arity CIF. + pub fixed_arg_count: Option, /// Swap the ctypes-local errno around the raw call (unix; ignored on windows). pub use_errno: bool, /// Swap the ctypes-local last error around the raw call (windows; ignored @@ -2066,6 +2100,9 @@ pub enum CallValue { #[derive(Debug, Clone, PartialEq, Eq)] pub enum CallError { NullFunctionPointer, + InvalidCallInterface { + variadic: bool, + }, UnknownTypeCode(String), /// An aggregate argument's buffer was shorter than its layout size. BufferTooSmall { @@ -2156,7 +2193,13 @@ pub fn call( }) .collect(); - let cif = Cif::new(ffi_arg_types, ffi_return_type); + let cif = match options.fixed_arg_count { + Some(fixed) => Cif::try_new_variadic(ffi_arg_types, fixed, ffi_return_type), + None => Cif::try_new(ffi_arg_types, ffi_return_type), + } + .map_err(|_| CallError::InvalidCallInterface { + variadic: options.fixed_arg_count.is_some(), + })?; // Allocate the aggregate return buffer outside the error-swap window so no // allocation runs between the raw call and the errno/last-error capture. @@ -3673,6 +3716,7 @@ mod tests { CallOptions { use_errno: true, use_last_error: false, + ..CallOptions::default() }, ) .unwrap(); diff --git a/crates/vm/src/builtins/memory.rs b/crates/vm/src/builtins/memory.rs index 24edde5e4..41622c17a 100644 --- a/crates/vm/src/builtins/memory.rs +++ b/crates/vm/src/builtins/memory.rs @@ -1627,6 +1627,7 @@ pub(crate) fn release_buffer_from_python( // releasebuffer_call_python, for a buffer acquired from a native exporter pub(crate) fn release_buffer_call_python(buffer: &PyBuffer) { crate::vm::thread::try_with_current_vm(|vm| { + let _raised = vm.preserve_raised_exception(); let exporter = buffer.obj.clone(); let window = PyBufferWindow { source: buffer.detached(), @@ -1655,9 +1656,12 @@ pub(crate) fn release_buffer_call_python(buffer: &PyBuffer) { fn call_python_release_buffer(exporter: &PyObject, mv: PyRef) { crate::vm::thread::try_with_current_vm(|vm| { + let _raised = vm.preserve_raised_exception(); + // Drop the captured view before restoring the pending error. + let owned_view = mv; let method = vm.get_special_method(exporter, identifier!(vm, __release_buffer__)); if let Ok(Some(method)) = method - && let Err(exc) = method.invoke((mv,), vm) + && let Err(exc) = method.invoke((owned_view,), vm) { let msg = format!( "Exception ignored in __release_buffer__ of {}", diff --git a/crates/vm/src/gc_state.rs b/crates/vm/src/gc_state.rs index 70ee04d46..4bdfb00f8 100644 --- a/crates/vm/src/gc_state.rs +++ b/crates/vm/src/gc_state.rs @@ -827,11 +827,12 @@ impl GcState { all_callbacks.extend(callbacks); } for (wr, cb) in all_callbacks { - if let Some(Err(e)) = crate::vm::thread::with_vm(&cb, |vm| cb.call((wr.clone(),), vm)) { - crate::vm::thread::with_vm(&cb, |vm| { - vm.run_unraisable(e.clone(), Some("weakref callback".to_owned()), cb.clone()); - }); - } + crate::vm::thread::with_vm(&cb, |vm| { + let _raised = vm.preserve_raised_exception(); + if let Err(e) = cb.call((wr.clone(),), vm) { + vm.run_unraisable(e, Some("weakref callback".to_owned()), cb.clone()); + } + }); } // 6d: Call __del__ on unreachable objects (skip already-finalized). diff --git a/crates/vm/src/object/core.rs b/crates/vm/src/object/core.rs index d93cefbeb..72c15855c 100644 --- a/crates/vm/src/object/core.rs +++ b/crates/vm/src/object/core.rs @@ -924,6 +924,7 @@ impl WeakRefList { for (wr, callback) in callbacks { if let Some(cb) = callback { crate::vm::thread::with_vm(&cb, |vm| { + let _raised = vm.preserve_raised_exception(); let _ = cb.call((wr.clone(),), vm); }); } @@ -2154,13 +2155,16 @@ impl PyObject { // during __del__, preventing safe_inc from seeing 0. zelf.0.ref_count.inc_by(2); - let del_method = zelf.get_class_attr(identifier!(vm, __del__)).unwrap(); - if let Err(e) = slot_del(zelf, vm) { - let msg = del_method - .repr(vm) - .ok() - .map(|r| format!("Exception ignored while calling deallocator {r}")); - vm.run_unraisable(e, msg, del_method); + { + let _raised = vm.preserve_raised_exception(); + let del_method = zelf.get_class_attr(identifier!(vm, __del__)).unwrap(); + if let Err(e) = slot_del(zelf, vm) { + let msg = del_method + .repr(vm) + .ok() + .map(|r| format!("Exception ignored while calling deallocator {r}")); + vm.run_unraisable(e, msg, del_method); + } } // Undo the temporary resurrection. Always remove both @@ -2315,6 +2319,7 @@ impl PyObject { // This ensures drop_slow_inner() won't call __del__ again self.set_gc_finalized(); let result = crate::vm::thread::with_vm(self, |vm| { + let _raised = vm.preserve_raised_exception(); if let Err(e) = slot_del(self, vm) && let Some(del_method) = self.get_class_attr(identifier!(vm, __del__)) { diff --git a/crates/vm/src/stdlib/_ctypes.rs b/crates/vm/src/stdlib/_ctypes.rs index 5deb9b46b..4345b7fbf 100644 --- a/crates/vm/src/stdlib/_ctypes.rs +++ b/crates/vm/src/stdlib/_ctypes.rs @@ -98,7 +98,7 @@ pub(crate) use _ctypes::module_def; #[pymodule] pub(crate) mod _ctypes { use super::{PyCArray, PyCData, PyCPointer, PyCSimple, PyCStructure, PyCUnion}; - use crate::builtins::{PyType, PyTypeRef}; + use crate::builtins::{PyStr, PyType, PyTypeRef}; use crate::class::StaticType; #[cfg(windows)] use crate::convert::ToPyException; @@ -110,12 +110,14 @@ pub(crate) mod _ctypes { // CArgObject - returned by byref() and paramfunc // tagPyCArgObject - #[pyclass(name = "CArgObject", module = "_ctypes", no_attr)] + #[pyclass(name = "CArgObject", module = "_ctypes", no_attr, traverse)] #[derive(Debug, PyPayload)] pub(crate) struct CArgObject { /// Type tag ('P', 'V', 'i', 'd', etc.) + #[pytraverse(skip)] pub tag: u8, /// The actual foreign-call value (mirrors union value) + #[pytraverse(skip)] pub value: super::CArgValue, /// Reference to original object (for memory safety) pub obj: PyObjectRef, @@ -124,8 +126,11 @@ pub(crate) mod _ctypes { pub keep: Option, /// Size for struct/union ('V' tag) #[allow(dead_code)] + #[pytraverse(skip)] pub size: usize, - /// Offset for byref() + /// Offset for byref(), read by Windows CopyComPointer. + #[cfg_attr(not(windows), allow(dead_code))] + #[pytraverse(skip)] pub offset: isize, } @@ -183,14 +188,16 @@ pub(crate) mod _ctypes { FfiValue::F32(v) => v as f64, _ => 0.0, }; + let v = crate::literal::float::to_string(v); Ok(format!("")) } - b'd' | b'g' => { + b'd' => { let v = match ffi_val { FfiValue::F64(v) => v, FfiValue::F32(v) => v as f64, _ => 0.0, }; + let v = crate::literal::float::to_string(v); Ok(format!("")) } b'c' => { @@ -239,8 +246,22 @@ pub(crate) mod _ctypes { } } - #[pyattr(name = "__version__")] - const __VERSION__: &str = "1.1.0"; + #[pyfunction] + fn __getattr__(name: PyObjectRef, vm: &VirtualMachine) -> PyResult { + if name.downcast_ref::().and_then(|name| name.to_str()) == Some("__version__") { + crate::stdlib::_warnings::warn( + vm.ctx.exceptions.deprecation_warning, + "'__version__' is deprecated and slated for removal in Python 3.20".to_owned(), + 1, + vm, + )?; + return Ok(vm.ctx.new_str("1.1.0").into()); + } + Err(vm.new_attribute_error(format!( + "module '_ctypes' has no attribute {}", + name.repr(vm)? + ))) + } // TODO: get properly #[pyattr] @@ -406,6 +427,15 @@ pub(crate) mod _ctypes { Err(vm.new_type_error("this type has no size")) } + #[cfg(any(target_os = "linux", target_os = "android"))] + #[pyfunction] + fn dllist(vm: &VirtualMachine) -> Vec { + rustpython_host_env::ctypes::dllist() + .into_iter() + .map(|name| vm.fsdecode(name).into()) + .collect() + } + #[cfg(windows)] #[pyfunction(name = "LoadLibrary")] fn load_library_windows( diff --git a/crates/vm/src/stdlib/_ctypes/base.rs b/crates/vm/src/stdlib/_ctypes/base.rs index 09a44dc34..c729c510a 100644 --- a/crates/vm/src/stdlib/_ctypes/base.rs +++ b/crates/vm/src/stdlib/_ctypes/base.rs @@ -219,7 +219,7 @@ impl StgInfo { size, align, length, - proto: None, + proto: Some(element_type.clone()), flags, scalar_code: None, element_type: Some(element_type), @@ -2007,14 +2007,17 @@ fn array_paramfunc(obj: &PyObject, vm: &VirtualMachine) -> PyResult /// PyCPointerType_paramfunc fn pointer_paramfunc(obj: &PyObject, vm: &VirtualMachine) -> PyResult { + use super::function::PyCFuncPtr; use super::pointer::PyCPointer; - let ptr = obj - .downcast_ref::() - .ok_or_else(|| vm.new_type_error("expected pointer"))?; - // parg->value.p = *(void **)self->b_ptr - let ptr_val = ptr.get_ptr_value(); + let ptr_val = if let Some(ptr) = obj.downcast_ref::() { + ptr.get_ptr_value() + } else if let Some(funcptr) = obj.downcast_ref::() { + funcptr.get_func_ptr() + } else { + return Err(vm.new_type_error("expected pointer")); + }; Ok(CArgObject { tag: b'P', diff --git a/crates/vm/src/stdlib/_ctypes/function.rs b/crates/vm/src/stdlib/_ctypes/function.rs index e8537b412..7137f22d5 100644 --- a/crates/vm/src/stdlib/_ctypes/function.rs +++ b/crates/vm/src/stdlib/_ctypes/function.rs @@ -17,7 +17,7 @@ use crate::{ vm::thread::with_current_vm, }; use alloc::borrow::Cow; -use core::ffi::c_void; +use core::ffi::{c_int, c_long, c_ulong, c_void}; use core::fmt::Debug; use core::ptr::NonNull; use num_traits::{Signed, ToPrimitive}; @@ -41,42 +41,36 @@ pub(super) const INTERNAL_MEMORYVIEW_AT_ADDR: usize = 4; /// Convert any object to a pointer value for c_void_p arguments /// Follows ConvParam logic for pointer types -fn convert_to_pointer(value: &PyObject, vm: &VirtualMachine) -> PyResult { +fn convert_to_pointer( + value: &PyObject, + vm: &VirtualMachine, +) -> PyResult<(CArgValue, Option)> { + let pointer = |address| Ok((CArgValue::pointer(address), Some(value.to_owned()))); // 0. CArgObject (from byref()) -> buffer address + offset if let Some(carg) = value.downcast_ref::() { - // Get buffer address from the underlying object - let base_addr = if let Some(cdata) = carg.obj.downcast_ref::() { - cdata.buffer.read().as_ptr() as usize - } else { - return Err(vm.new_type_error(format!( - "byref() argument must be a ctypes instance, not '{}'", - carg.obj.class().name() - ))); - }; - let addr = (base_addr as isize + carg.offset) as usize; - return Ok(CArgValue::pointer(addr)); + return Ok((carg.value.clone(), Some(value.to_owned()))); } // 1. None -> NULL if value.is(&vm.ctx.none) { - return Ok(CArgValue::pointer(0)); + return Ok((CArgValue::pointer(0), None)); } // 2. PyCArray -> buffer address (PyCArrayType_paramfunc) if let Some(array) = value.downcast_ref::() { let addr = array.0.buffer.read().as_ptr() as usize; - return Ok(CArgValue::pointer(addr)); + return pointer(addr); } // 3. PyCPointer -> stored pointer value if let Some(ptr) = value.downcast_ref::() { - return Ok(CArgValue::pointer(ptr.get_ptr_value())); + return pointer(ptr.get_ptr_value()); } // 4. PyCStructure -> buffer address if let Some(struct_obj) = value.downcast_ref::() { let addr = struct_obj.0.buffer.read().as_ptr() as usize; - return Ok(CArgValue::pointer(addr)); + return pointer(addr); } // 5. PyCSimple (c_void_p, c_char_p, etc.) -> value from buffer @@ -84,14 +78,14 @@ fn convert_to_pointer(value: &PyObject, vm: &VirtualMachine) -> PyResult buffer address (PyBytes_AsString) + // 6. bytes -> owned, null-terminated buffer if let Some(bytes) = value.downcast_ref::() { - let addr = bytes.as_bytes().as_ptr() as usize; - return Ok(CArgValue::pointer(addr)); + let (owner, address) = super::base::ensure_z_null_terminated(bytes, vm); + return Ok((CArgValue::pointer(address), Some(owner))); } // 7. Integer -> direct value (PyLong_AsVoidPtr behavior) @@ -100,10 +94,10 @@ fn convert_to_pointer(value: &PyObject, vm: &VirtualMachine) -> PyResult PyResult { // 1. CArgObject (from byref() or paramfunc) -> use stored value if let Some(carg) = value.downcast_ref::() { return Ok(Argument { - keep: carg.keep.clone(), + // Keep the wrapper, which owns both the source and any auxiliary + // pointer storage. `_as_parameter_` may have created it on demand. + keep: Some(value.to_owned()), value: carg.value.clone(), }); } @@ -141,9 +137,10 @@ fn conv_param(value: &PyObject, vm: &VirtualMachine) -> PyResult { // 3. ctypes objects -> use paramfunc if let Ok(carg) = super::base::call_paramfunc(value, vm) { + let value = carg.value.clone(); return Ok(Argument { - keep: carg.keep, - value: carg.value, + keep: Some(carg.into_ref(&vm.ctx).into()), + value, }); } @@ -175,7 +172,26 @@ fn conv_param(value: &PyObject, vm: &VirtualMachine) -> PyResult { // `__int__` would accept a float and pass its truncated value where the // callee expects a pointer. if let Some(int_val) = value.downcast_ref::() { - let val = int_val.as_bigint().to_i32().unwrap_or(0); + // CPython first accepts the platform's unsigned long / signed long + // range, then stores the low C-int bits in the argument carrier. + let bigint = int_val.as_bigint(); + #[allow( + clippy::useless_conversion, + clippy::unnecessary_cast, + reason = "C long is 32 bits on Windows and 64 bits on LP64 targets" + )] + let val = if bigint.is_negative() { + bigint + .to_i64() + .and_then(|value| c_long::try_from(value).ok()) + .map(|value| value as c_int) + } else { + bigint + .to_u64() + .and_then(|value| c_ulong::try_from(value).ok()) + .map(|value| value as c_int) + } + .ok_or_else(|| vm.new_overflow_error("int too long to convert"))?; return Ok(Argument { keep: None, value: CArgValue::Int(val), @@ -193,6 +209,29 @@ fn conv_param(value: &PyObject, vm: &VirtualMachine) -> PyResult { ))) } +fn conv_param_for_call(value: &PyObject, index: usize, vm: &VirtualMachine) -> PyResult { + match conv_param(value, vm) { + Ok(argument) => Ok(argument), + Err(error) => { + let error_type = vm + .import("_ctypes", 0)? + .get_attr("ArgumentError", vm)? + .downcast::() + .map_err(|_| vm.new_type_error("ArgumentError is not a type"))?; + Err(vm.new_exception_msg( + error_type, + format!( + "argument {}: {}: {}", + index + 1, + error.class().name(), + error.as_object().str(vm)?, + ) + .into(), + )) + } + } +} + trait ArgumentType { /// Convert an argument for this type into a foreign-call value plus an /// optional owner keeping any referenced memory alive. @@ -214,6 +253,7 @@ impl ArgumentType for PyTypeRef { // type must carry a known _type_ code; anything else is unsupported. let type_code = if self.fast_issubclass(CArgObject::static_type()) || self.fast_issubclass(PyCPointer::static_type()) + || self.fast_issubclass(PyCFuncPtr::static_type()) || self.fast_issubclass(PyCStructure::static_type()) || self.fast_issubclass(PyCUnion::static_type()) { @@ -246,7 +286,7 @@ impl ArgumentType for PyTypeRef { // Then pass the converted value to ConvParam logic // CArgObject (from from_param) -> use stored value and keepalive directly if let Some(carg) = converted.downcast_ref::() { - return Ok((carg.value.clone(), carg.keep.clone())); + return Ok((carg.value.clone(), Some(converted.clone()))); } // None -> NULL pointer @@ -254,12 +294,20 @@ impl ArgumentType for PyTypeRef { return Ok((CArgValue::pointer(0), None)); } + if self.fast_issubclass(PyCFuncPtr::static_type()) { + let argument = conv_param(&converted, vm)?; + return Ok((argument.value, argument.keep)); + } + // For pointer types (POINTER(T)), we need to pass the pointer VALUE stored in buffer if self.fast_issubclass(PyCPointer::static_type()) { if let Some(pointer) = converted.downcast_ref::() { - return Ok((CArgValue::pointer(pointer.get_ptr_value()), None)); + return Ok(( + CArgValue::pointer(pointer.get_ptr_value()), + Some(converted.clone()), + )); } - return Ok((convert_to_pointer(&converted, vm)?, None)); + return convert_to_pointer(&converted, vm); } // For structure/union types, pass the aggregate by value: snapshot the @@ -279,12 +327,12 @@ impl ArgumentType for PyTypeRef { // owns, which must outlive the foreign call. return Ok((CArgValue::aggregate(layout, bytes), Some(converted.clone()))); } - return Ok((convert_to_pointer(&converted, vm)?, None)); + return convert_to_pointer(&converted, vm); } // For pointer types (c_void_p, c_char_p, c_wchar_p), handle as pointer if matches!(type_code.as_deref(), Some("P" | "z" | "Z")) { - return Ok((convert_to_pointer(&converted, vm)?, None)); + return convert_to_pointer(&converted, vm); } // PyCSimple (already a ctypes instance from from_param) @@ -293,7 +341,7 @@ impl ArgumentType for PyTypeRef { .as_deref() .and_then(|s| s.chars().next()) .ok_or_else(|| vm.new_type_error("Unsupported argument type"))?; - return Ok((simple.to_carg_value(code), None)); + return Ok((simple.to_carg_value(code), Some(simple.into()))); } Err(vm.new_type_error("Unsupported argument type")) @@ -333,6 +381,25 @@ impl Initializer for PyCFuncPtrType { #[pyclass(flags(IMMUTABLETYPE), with(AsSequence, Initializer))] impl PyCFuncPtrType { + #[pymethod] + fn from_param(zelf: PyTypeRef, value: PyObjectRef, vm: &VirtualMachine) -> PyResult { + if value.is_instance(zelf.as_object(), vm)? { + return Ok(value); + } + + if let Some(as_parameter) = vm.get_attribute_opt(&value, "_as_parameter_")? { + return vm.with_recursion(" while processing _as_parameter_", || { + Self::from_param(zelf, as_parameter, vm) + }); + } + + Err(vm.new_type_error(format!( + "expected {} instance instead of {}", + zelf.name(), + value.class().name() + ))) + } + #[pygetset(name = "__pointer_type__")] fn pointer_type(zelf: PyTypeRef, vm: &VirtualMachine) -> PyResult { super::base::pointer_type_get(&zelf, vm) @@ -664,7 +731,7 @@ impl PyCFuncPtr { } /// Get function pointer address from buffer - fn get_func_ptr(&self) -> usize { + pub(super) fn get_func_ptr(&self) -> usize { let buffer = self._base.buffer.read(); rustpython_host_env::ctypes::read_pointer_from_buffer(&buffer) } @@ -1216,7 +1283,8 @@ fn build_callargs_no_argtypes( let arguments: Vec = args .args .iter() - .map(|arg| conv_param(arg, vm)) + .enumerate() + .map(|(index, arg)| conv_param_for_call(arg, index, vm)) .collect::>>()?; Ok((arguments, Vec::new())) } @@ -1225,18 +1293,30 @@ fn build_callargs_no_argtypes( fn build_callargs_simple( args: &FuncArgs, arg_types: &[PyTypeRef], + allow_variadic: bool, vm: &VirtualMachine, ) -> PyResult<(Vec, OutBuffers)> { + if args.args.len() < arg_types.len() || (!allow_variadic && args.args.len() != arg_types.len()) + { + return Err(vm.new_type_error(format!( + "this function takes {}{} argument{} ({} given)", + if allow_variadic { "at least " } else { "" }, + arg_types.len(), + if arg_types.len() == 1 { "" } else { "s" }, + args.args.len(), + ))); + } let arguments: Vec = args .args .iter() .enumerate() .map(|(n, arg)| { - let arg_type = arg_types - .get(n) - .ok_or_else(|| vm.new_type_error("argument amount mismatch"))?; - let (value, keep) = arg_type.convert_object(arg.clone(), vm)?; - Ok(Argument { value, keep }) + if let Some(arg_type) = arg_types.get(n) { + let (value, keep) = arg_type.convert_object(arg.clone(), vm)?; + Ok(Argument { value, keep }) + } else { + conv_param_for_call(arg, n, vm) + } }) .collect::>>()?; Ok((arguments, Vec::new())) @@ -1312,6 +1392,7 @@ fn build_callargs( call_info: &CallInfo, paramflags: Option<&ParsedParamFlags>, is_com_method: bool, + allow_variadic: bool, vm: &VirtualMachine, ) -> PyResult<(Vec, OutBuffers)> { let Some(ref arg_types) = call_info.explicit_arg_types else { @@ -1338,7 +1419,7 @@ fn build_callargs( Ok((arguments, Vec::new())) } else { // Regular function - build_callargs_simple(args, arg_types, vm) + build_callargs_simple(args, arg_types, allow_variadic, vm) } } @@ -1410,11 +1491,11 @@ fn convert_raw_result( result: &CallValue, call_info: &CallInfo, vm: &VirtualMachine, -) -> Option { +) -> PyResult> { // Result register image as bytes + size (None for void): pointer/scalar // returns are pointer/register sized. let (result_bytes, result_size) = match result { - CallValue::Void => return None, + CallValue::Void => return Ok(None), CallValue::Pointer(ptr) => (ptr.to_ne_bytes().to_vec(), size_of::()), CallValue::Scalar(bytes) | CallValue::Aggregate(bytes) => (bytes.clone(), bytes.len()), }; @@ -1435,14 +1516,14 @@ fn convert_raw_result( let restype = match &call_info.restype_obj { None => { // Default: return as int - return Some(vm.ctx.new_int(result_word as isize).into()); + return Ok(Some(vm.ctx.new_int(result_word as isize).into())); } Some(r) => r, }; // 2. restype is None → return None if restype.is(&vm.ctx.none()) { - return None; + return Ok(None); } // 3. Get restype as PyType @@ -1450,7 +1531,7 @@ fn convert_raw_result( Ok(t) => t, Err(_) => { // Not a type, call it with int result - return restype.call((result_word as isize,), vm).ok(); + return restype.call((result_word as isize,), vm).map(Some); } }; @@ -1462,7 +1543,7 @@ fn convert_raw_result( return restype_type .as_object() .call((result_word as isize,), vm) - .ok(); + .map(Some); } let info = stg_info.unwrap(); @@ -1480,24 +1561,22 @@ fn convert_raw_result( && let Some(type_str) = type_attr.downcast_ref::() && type_str.to_str() == Some("O") { - let ptr = NonNull::new(result_word as *mut PyObject).or_else(|| { - vm.set_exception(Some(vm.new_value_error("PyObject is NULL"))); - None - })?; + let ptr = NonNull::new(result_word as *mut PyObject) + .ok_or_else(|| vm.new_value_error("PyObject is NULL"))?; unsafe { let obj = PyObjectRef::from_raw(ptr); - return Some(obj); + return Ok(Some(obj)); } } // 5. Simple type with getfunc → use bytes_to_pyobject (info->getfunc) // is_simple_instance returns TRUE for c_int, c_void_p, etc. if super::base::is_simple_instance(&restype_type) { - return Some(super::base::bytes_to_pyobject( + return Ok(Some(super::base::bytes_to_pyobject( &restype_type, &result_bytes, vm, - )); + ))); } // 6. Complex type → create ctypes instance (PyCData_FromBaseObj) @@ -1507,16 +1586,16 @@ fn convert_raw_result( if is_pointer_type && has_proto && let CallValue::Pointer(ptr) = result - && let Ok(instance) = restype_type.as_object().call((), vm) { + let instance = restype_type.as_object().call((), vm)?; if let Some(pointer) = instance.downcast_ref::() { pointer.set_ptr_value(*ptr); } - return Some(instance); + return Ok(Some(instance)); } // Create instance and copy result data - pycdata_from_ffi_result(&restype_type, &result_bytes, result_size, vm).ok() + pycdata_from_ffi_result(&restype_type, &result_bytes, result_size, vm).map(Some) } /// Create a ctypes instance from FFI result (PyCData_FromBaseObj equivalent) @@ -1575,7 +1654,7 @@ fn build_result( } // Convert the foreign-call result to a Python object - let mut result = convert_raw_result(&call_result, call_info, vm); + let mut result = convert_raw_result(&call_result, call_info, vm)?; // Apply errcheck if set if let Some(errcheck) = zelf.errcheck.read().as_ref() { @@ -1622,8 +1701,16 @@ impl Callable for PyCFuncPtr { let paramflags = parse_paramflags(zelf, vm)?; // 5. Build call arguments - let (arguments, out_buffers) = - build_callargs(&args, &call_info, paramflags.as_ref(), is_com_method, vm)?; + let flags = Py::::_flags_(zelf, vm); + let allow_variadic = flags & StgInfoFlags::FUNCFLAG_CDECL.bits() != 0; + let (arguments, out_buffers) = build_callargs( + &args, + &call_info, + paramflags.as_ref(), + is_com_method, + allow_variadic, + vm, + )?; // 6. Function address (usize); the unified `call` rejects a NULL address. let addr = match func_ptr { @@ -1632,24 +1719,47 @@ impl Callable for PyCFuncPtr { }; // 7. Errno / last-error swap options from flags - let flags = Py::::_flags_(zelf, vm); let options = CallOptions { + fixed_arg_count: call_info + .explicit_arg_types + .as_ref() + .map(Vec::len) + .filter(|&fixed| allow_variadic && fixed != 0 && arguments.len() > fixed), use_errno: flags & super::base::StgInfoFlags::FUNCFLAG_USE_ERRNO.bits() != 0, use_last_error: flags & super::base::StgInfoFlags::FUNCFLAG_USE_LASTERROR.bits() != 0, }; - // 8. Call the function through the unified entry point. - let call_result = ctypes_callproc(addr, &arguments, &call_info.ret, options).map_err( - |err| match err { - CallError::NullFunctionPointer => vm.new_value_error("NULL function pointer"), - CallError::UnknownTypeCode(code) => { - vm.new_type_error(format!("Unsupported argument type: {code}")) - } - CallError::BufferTooSmall { expected, got } => vm.new_value_error(format!( - "argument buffer too small: expected {expected}, got {got}" - )), - }, - )?; + // Native calls may block while another thread runs Python. Callbacks + // reattach before executing Python; PythonAPI calls stay attached. + let native_call = || ctypes_callproc(addr, &arguments, &call_info.ret, options); + let call_result = if flags & StgInfoFlags::FUNCFLAG_PYTHONAPI.bits() != 0 { + native_call() + } else { + vm.allow_threads(native_call) + } + .map_err(|err| match err { + CallError::NullFunctionPointer => vm.new_value_error("NULL function pointer"), + CallError::InvalidCallInterface { variadic } => vm.new_runtime_error(if variadic { + "ffi_prep_cif_var failed" + } else { + "ffi_prep_cif failed" + }), + CallError::UnknownTypeCode(code) => { + vm.new_type_error(format!("Unsupported argument type: {code}")) + } + CallError::BufferTooSmall { expected, got } => vm.new_value_error(format!( + "argument buffer too small: expected {expected}, got {got}" + )), + })?; + + // PythonAPI functions report errors through the native raised-error + // slot. Check it before a NULL PyObject* can be decoded, and consume + // the exception exactly once without touching handled exceptions. + if flags & StgInfoFlags::FUNCFLAG_PYTHONAPI.bits() != 0 + && let Some(exception) = vm.take_raised_exception() + { + return Err(exception); + } // 9. Build result build_result(call_result, &call_info, out_buffers, zelf, &args, vm) @@ -1808,7 +1918,23 @@ fn ffi_to_python( args: *const *const c_void, index: usize, vm: &VirtualMachine, -) -> PyObjectRef { +) -> PyResult { + if ty.fast_issubclass(PyCPointer::static_type()) { + // POINTER(T) uses a pointer argument in the CIF, even though its + // `_type_` is a type object rather than a simple type code. + let rustpython_host_env::ctypes::DecodedValue::Pointer(address) = + (unsafe { rustpython_host_env::ctypes::callback_arg_value_at(Some("P"), args, index) }) + else { + return Err(vm.new_type_error("invalid pointer callback argument")); + }; + let instance = ty.as_object().call((), vm)?; + let pointer = instance + .downcast_ref::() + .ok_or_else(|| vm.new_type_error("pointer constructor returned a non-pointer"))?; + pointer.set_ptr_value(address); + return Ok(instance); + } + let type_code = ty.type_code(vm); let raw_value: PyObjectRef = match unsafe { rustpython_host_env::ctypes::callback_arg_value_at(type_code.as_deref(), args, index) @@ -1824,11 +1950,12 @@ fn ffi_to_python( }; if !is_simple_subclass(ty, vm) { - return raw_value; + return Ok(raw_value); } - ty.as_object() + Ok(ty + .as_object() .call((raw_value.clone(),), vm) - .unwrap_or(raw_value) + .unwrap_or(raw_value)) } /// Convert a Python object to a C value and store it at the result pointer @@ -1910,32 +2037,35 @@ unsafe extern "C" fn thunk_callback( userdata: &ThunkUserData, ) { with_current_vm(|vm| { - let use_errno = userdata.flags & StgInfoFlags::FUNCFLAG_USE_ERRNO.bits() != 0; - let py_result = - rustpython_host_env::ctypes::with_callback_errno_preserved(use_errno, || { - let py_args: Vec = userdata - .arg_types - .iter() - .enumerate() - .map(|(i, ty)| ffi_to_python(ty, args, i, vm)) - .collect(); + vm.attach_for_callback(|| { + let use_errno = userdata.flags & StgInfoFlags::FUNCFLAG_USE_ERRNO.bits() != 0; + let py_result = + rustpython_host_env::ctypes::with_callback_errno_preserved(use_errno, || { + let py_args: Vec = userdata + .arg_types + .iter() + .enumerate() + .map(|(i, ty)| ffi_to_python(ty, args, i, vm)) + .collect::>>()?; - userdata.callable.call(py_args, vm) - }); + userdata.callable.call(py_args, vm) + }); - // Call unraisable hook if exception occurred - if let Err(exc) = &py_result { - let repr = userdata - .callable - .repr(vm) - .map_or_else(|_| "".to_string(), |s| s.to_string()); - let msg = format!("Exception ignored while calling ctypes callback function {repr}"); - vm.run_unraisable(exc.clone(), Some(msg), vm.ctx.none()); - } + // Call unraisable hook if exception occurred + if let Err(exc) = &py_result { + let repr = userdata + .callable + .repr(vm) + .map_or_else(|_| "".to_string(), |s| s.to_string()); + let msg = + format!("Exception ignored while calling ctypes callback function {repr}"); + vm.run_unraisable(exc.clone(), Some(msg), vm.ctx.none()); + } - if let Some(ref res_type) = userdata.res_type { - python_to_ffi(py_result, res_type, result as *mut c_void, vm); - } + if let Some(ref res_type) = userdata.res_type { + python_to_ffi(py_result, res_type, result as *mut c_void, vm); + } + }); }); } @@ -1982,9 +2112,13 @@ impl PyCThunk { let ffi_arg_types: Vec = arg_type_vec .iter() .map(|ty| { - ty.type_code(vm) - .and_then(|code| ffi_type_from_code(&code)) - .unwrap_or_else(ffi_pointer_type) + if ty.fast_issubclass(PyCPointer::static_type()) { + ffi_pointer_type() + } else { + ty.type_code(vm) + .and_then(|code| ffi_type_from_code(&code)) + .unwrap_or_else(ffi_pointer_type) + } }) .collect(); diff --git a/crates/vm/src/stdlib/_ctypes/simple.rs b/crates/vm/src/stdlib/_ctypes/simple.rs index 54ce7a1a3..c78fb81d4 100644 --- a/crates/vm/src/stdlib/_ctypes/simple.rs +++ b/crates/vm/src/stdlib/_ctypes/simple.rs @@ -59,6 +59,12 @@ fn new_simple_type( let tp_str = _type_.str(vm)?.to_string(); + // Complex ctypes storage and FFI are not implemented. Preserve the + // optional-type AttributeError contract for the PEP 3118 codes. + if matches!(tp_str.as_str(), "Zd" | "Zf" | "Zg") { + return Err(vm.new_attribute_error("complex ctypes types are not supported")); + } + if tp_str.len() != 1 { return Err(vm.new_value_error(format!( "class must define a '_type_' attribute which must be a string of length 1, str: {tp_str}" @@ -540,6 +546,12 @@ impl Initializer for PyCSimpleType { let type_str = type_attr.str(vm)?.to_string(); // Validate _type_ is a single character + // Complex ctypes storage and FFI are not implemented. Preserve the + // optional-type AttributeError contract for the PEP 3118 codes. + if matches!(type_str.as_str(), "Zd" | "Zf" | "Zg") { + return Err(vm.new_attribute_error("complex ctypes types are not supported")); + } + if type_str.len() != 1 { return Err(vm.new_value_error( "class must define a '_type_' attribute which must be a string of length 1", diff --git a/crates/vm/src/vm/mod.rs b/crates/vm/src/vm/mod.rs index 795f3db12..c899a8c2e 100644 --- a/crates/vm/src/vm/mod.rs +++ b/crates/vm/src/vm/mod.rs @@ -186,6 +186,8 @@ unsafe impl Send for FramePtr {} #[derive(Debug)] struct ExceptionStack { + /// Pending native C API error, independent of handled `sys.exception()`. + raised: Option, /// Linked list of handled-exception slots (`_PyErr_StackItem` chain). /// Bottom element is the thread's base slot; generator/coroutine resume /// pushes an additional slot. Normal frame calls do **not** push/pop. @@ -195,7 +197,10 @@ struct ExceptionStack { impl Default for ExceptionStack { fn default() -> Self { // Thread's base `_PyErr_StackItem` – always present. - Self { stack: vec![None] } + Self { + raised: None, + stack: vec![None], + } } } @@ -3957,17 +3962,27 @@ impl VirtualMachine { } } + /// Keep teardown callbacks from observing or replacing a pending C error. + /// Create this guard before callback-owned locals so they drop before restoration. + #[must_use] + pub(crate) fn preserve_raised_exception(&self) -> impl Drop + '_ { + scopeguard::guard(self.take_raised_exception(), |saved| { + self.set_raised_exception(saved); + }) + } + + pub fn raised_exception(&self) -> Option { + self.exceptions.borrow().raised.clone() + } + + pub fn set_raised_exception(&self, exc: Option) { + let previous = core::mem::replace(&mut self.exceptions.borrow_mut().raised, exc); + // Release the RefCell borrow before dropping a potentially final reference. + drop(previous); + } + 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 - } + self.exceptions.borrow_mut().raised.take() } /// `_PyErr_ChainStackItem`: if the current `exc_info` slot is occupied, @@ -4189,6 +4204,36 @@ pub fn resolve_frozen_alias(name: &str) -> &str { mod tests { use super::*; + #[test] + fn raised_error_guards_preserve_storage_on_unwind() { + Interpreter::builder(Default::default()) + .build() + .enter(|vm| { + let handled = vm.new_type_error("handled"); + let outer = vm.new_type_error("outer"); + let inner = vm.new_type_error("inner"); + vm.set_exception(Some(handled.clone())); + vm.set_raised_exception(Some(outer.clone())); + let result = std::panic::catch_unwind(core::panic::AssertUnwindSafe(|| { + let _outer_guard = vm.preserve_raised_exception(); + assert!(vm.raised_exception().is_none()); + vm.set_raised_exception(Some(inner.clone())); + { + let _inner_guard = vm.preserve_raised_exception(); + assert!(vm.raised_exception().is_none()); + assert!(vm.current_exception().unwrap().is(&handled)); + } + assert!(vm.raised_exception().unwrap().is(&inner)); + panic!("exercise raised-error guard unwinding"); + })); + assert!(result.is_err()); + assert!(vm.take_raised_exception().unwrap().is(&outer)); + assert!(vm.raised_exception().is_none()); + assert!(vm.current_exception().unwrap().is(&handled)); + vm.set_exception(None); + }); + } + #[cfg(not(feature = "stdio"))] #[test] fn no_stdio_streams_are_none_singleton() {