Align ctypes call ownership and C error state with Python 3.15 (#9218)

* Align ctypes call ownership and C error state with Python 3.15

Keep native raised errors separate from handled exceptions and preserve them across teardown callbacks. Import the related canonical ctypes call and type contracts.

Assisted-by: OpenAI Codex: model version unavailable

* Enumerate ctypes shared libraries natively on Linux and Android

Copy loader-owned names before Python filesystem decoding and update the related canonical ctypes utility and discovery tests from CPython 3.15. Preserve existing platform paths and dllist test markers.

Assisted-by: OpenAI Codex: model version unavailable

* Enable canonical dllist system enumeration coverage on Linux

Regenerate the pinned test through update_lib patches with reviewed marker data. Preserve other platform conditions and the unavailable extension-fixture guard.

Assisted-by: OpenAI Codex: model version unavailable

* Match Python float and long-double C argument representations

Use the existing Python float formatter for float and double C argument representations. Keep long-double arguments on the canonical generic-address representation path. Preserve the unchanged canonical parameter repr assertions.

Assisted-by: OpenAI Codex: model version unavailable
This commit is contained in:
youknowdot authored and GitHub committed 2026-10-22 18:16:22 +09:00
1 parent 1ffba21557
commit 1ba1107115
26 files changed
+569 -232

No files matched your search

+13 -15
View File
@@ -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}")
+2 -1
View File
@@ -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
+10 -51
View File
@@ -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
+17
View File
@@ -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)
-1
View File
@@ -3,7 +3,6 @@
import ctypes
from _ctypes import Structure, Union, _Pointer, Array, _SimpleCData, CFuncPtr
import sys
from test import support
_CData = Structure.__base__
+4 -1
View File
@@ -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
+3 -5
View File
@@ -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
+15
View File
@@ -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()
+1 -1
View File
@@ -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()
+17 -1
View File
@@ -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:
+1 -1
View File
@@ -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
+27 -8
View File
@@ -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 "&<element_format>"
self.assertEqual(actual_format, expected_format)
if __name__ == '__main__':
unittest.main()
+11 -2
View File
@@ -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):
+8
View File
@@ -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"^<cparam 'z' \(0x[A-Fa-f0-9]+\)>$")
self.assertRegex(repr(c_wchar_p.from_param('hihi')), r"^<cparam 'Z' \(0x[A-Fa-f0-9]+\)>$")
self.assertRegex(repr(c_void_p.from_param(0x12)), r"^<cparam 'P' \(0x0*12\)>$")
if hasattr(ctypes, 'c_double_complex'):
self.assertRegex(repr(ctypes.c_double_complex.from_param(0)),
r"^<cparam 'Zd' at 0x[A-Fa-f0-9]+>$")
self.assertRegex(repr(ctypes.c_float_complex.from_param(0)),
r"^<cparam 'Zf' at 0x[A-Fa-f0-9]+>$")
self.assertRegex(repr(ctypes.c_longdouble_complex.from_param(0)),
r"^<cparam 'Zg' at 0x[A-Fa-f0-9]+>$")
@test.support.cpython_only
def test_from_param_result_refcount(self):
+7
View File
@@ -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.
+12 -2
View File
@@ -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::<PyType>()
&& cls.fast_issubclass(vm.ctx.exceptions.base_exception_type)
&& !exc_val
.downcast_ref::<PyBaseException>()
.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::<PyType>(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)
+2 -2
View File
@@ -186,7 +186,7 @@ impl FfiResult<()> for PyResult<Infallible> {
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),
+46 -2
View File
@@ -221,6 +221,36 @@ const LONG_DOUBLE_SIZE: usize = core::mem::size_of::<c_double>();
)))]
const LONG_DOUBLE_SIZE: usize = core::mem::size_of::<c_double>();
/// 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<std::ffi::OsString> {
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::<Vec<std::ffi::OsString>>() };
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<usize> {
match ty {
"c" | "b" => Some(core::mem::size_of::<c_schar>()),
@@ -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<usize>,
/// 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();
+5 -1
View File
@@ -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<PyMemoryView>) {
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 {}",
+6 -5
View File
@@ -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).
+12 -7
View File
@@ -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__))
{
+36 -6
View File
@@ -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<PyObjectRef>,
/// 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!("<cparam '{tag_char}' ({v})>"))
}
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!("<cparam '{tag_char}' ({v})>"))
}
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::<PyStr>().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<PyObjectRef> {
rustpython_host_env::ctypes::dllist()
.into_iter()
.map(|name| vm.fsdecode(name).into())
.collect()
}
#[cfg(windows)]
#[pyfunction(name = "LoadLibrary")]
fn load_library_windows(
+9 -6
View File
@@ -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<CArgObject>
/// PyCPointerType_paramfunc
fn pointer_paramfunc(obj: &PyObject, vm: &VirtualMachine) -> PyResult<CArgObject> {
use super::function::PyCFuncPtr;
use super::pointer::PyCPointer;
let ptr = obj
.downcast_ref::<PyCPointer>()
.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::<PyCPointer>() {
ptr.get_ptr_value()
} else if let Some(funcptr) = obj.downcast_ref::<PyCFuncPtr>() {
funcptr.get_func_ptr()
} else {
return Err(vm.new_type_error("expected pointer"));
};
Ok(CArgObject {
tag: b'P',
+237 -103
View File
@@ -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<CArgValue> {
fn convert_to_pointer(
value: &PyObject,
vm: &VirtualMachine,
) -> PyResult<(CArgValue, Option<PyObjectRef>)> {
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::<CArgObject>() {
// Get buffer address from the underlying object
let base_addr = if let Some(cdata) = carg.obj.downcast_ref::<PyCData>() {
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::<PyCArray>() {
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::<PyCPointer>() {
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::<PyCStructure>() {
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<CArgVal
let buffer = simple.0.buffer.read();
if has_pointer_width(&buffer) {
let addr = rustpython_host_env::ctypes::read_pointer_from_buffer(&buffer);
return Ok(CArgValue::pointer(addr));
return pointer(addr);
}
}
// 6. bytes -> buffer address (PyBytes_AsString)
// 6. bytes -> owned, null-terminated buffer
if let Some(bytes) = value.downcast_ref::<crate::builtins::PyBytes>() {
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<CArgVal
// Negative values: use signed conversion (allows -1 as 0xFFFF...)
if bigint.is_negative() {
if let Some(signed_val) = bigint.to_isize() {
return Ok(CArgValue::pointer(signed_val as usize));
return pointer(signed_val as usize);
}
} else if let Some(unsigned_val) = bigint.to_usize() {
return Ok(CArgValue::pointer(unsigned_val));
return pointer(unsigned_val);
}
// Value out of range - raise OverflowError
return Err(vm.new_overflow_error("int too large to convert to pointer"));
@@ -126,7 +120,9 @@ fn conv_param(value: &PyObject, vm: &VirtualMachine) -> PyResult<Argument> {
// 1. CArgObject (from byref() or paramfunc) -> use stored value
if let Some(carg) = value.downcast_ref::<CArgObject>() {
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<Argument> {
// 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<Argument> {
// `__int__` would accept a float and pass its truncated value where the
// callee expects a pointer.
if let Some(int_val) = value.downcast_ref::<PyInt>() {
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<Argument> {
)))
}
fn conv_param_for_call(value: &PyObject, index: usize, vm: &VirtualMachine) -> PyResult<Argument> {
match conv_param(value, vm) {
Ok(argument) => Ok(argument),
Err(error) => {
let error_type = vm
.import("_ctypes", 0)?
.get_attr("ArgumentError", vm)?
.downcast::<PyType>()
.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::<CArgObject>() {
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::<PyCPointer>() {
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<Argument> = args
.args
.iter()
.map(|arg| conv_param(arg, vm))
.enumerate()
.map(|(index, arg)| conv_param_for_call(arg, index, vm))
.collect::<PyResult<Vec<_>>>()?;
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<Argument>, 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<Argument> = 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::<PyResult<Vec<_>>>()?;
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<Argument>, 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<PyObjectRef> {
) -> PyResult<Option<PyObjectRef>> {
// 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::<usize>()),
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::<PyStr>()
&& 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::<PyCPointer>() {
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::<Self>::_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::<Self>::_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::<PyCPointer>()
.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<PyObjectRef> = 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<PyObjectRef> = userdata
.arg_types
.iter()
.enumerate()
.map(|(i, ty)| ffi_to_python(ty, args, i, vm))
.collect::<PyResult<Vec<_>>>()?;
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(|_| "<unknown>".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(|_| "<unknown>".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<FfiType> = 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();
+12
View File
@@ -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",
+56 -11
View File
@@ -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<PyBaseExceptionRef>,
/// 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<PyBaseExceptionRef> {
self.exceptions.borrow().raised.clone()
}
pub fn set_raised_exception(&self, exc: Option<PyBaseExceptionRef>) {
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<PyBaseExceptionRef> {
let mut excs = self.exceptions.borrow_mut();
if let Some(top) = excs.stack.last_mut() {
let exc = top.take();
drop(excs);
#[cfg(feature = "threading")]
thread::update_thread_exception(self.topmost_exception());
exc
} else {
None
}
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() {