diff --git a/Lib/ctypes/__init__.py b/Lib/ctypes/__init__.py index 3599e13ed2..80651dc64c 100644 --- a/Lib/ctypes/__init__.py +++ b/Lib/ctypes/__init__.py @@ -1,6 +1,8 @@ """create and manipulate C data types in Python""" -import os as _os, sys as _sys +import os as _os +import sys as _sys +import sysconfig as _sysconfig import types as _types __version__ = "1.1.0" @@ -36,9 +38,6 @@ FUNCFLAG_USE_ERRNO as _FUNCFLAG_USE_ERRNO, \ FUNCFLAG_USE_LASTERROR as _FUNCFLAG_USE_LASTERROR -# TODO: RUSTPYTHON remove this -from _ctypes import _non_existing_function - # WINOLEAPI -> HRESULT # WINOLEAPI_(type) # @@ -110,7 +109,7 @@ class CFunctionType(_CFuncPtr): return CFunctionType if _os.name == "nt": - from _ctypes import LoadLibrary as _dlopen + from _ctypes import LoadLibrary as _LoadLibrary from _ctypes import FUNCFLAG_STDCALL as _FUNCFLAG_STDCALL _win_functype_cache = {} @@ -305,8 +304,9 @@ def create_unicode_buffer(init, size=None): raise TypeError(init) -# XXX Deprecated def SetPointerType(pointer, cls): + import warnings + warnings._deprecated("ctypes.SetPointerType", remove=(3, 15)) if _pointer_type_cache.get(cls, None) is not None: raise RuntimeError("This type already exists in the cache") if id(pointer) not in _pointer_type_cache: @@ -315,7 +315,6 @@ def SetPointerType(pointer, cls): _pointer_type_cache[cls] = pointer del _pointer_type_cache[id(pointer)] -# XXX Deprecated def ARRAY(typ, len): return typ * len @@ -347,52 +346,59 @@ def __init__(self, name, mode=DEFAULT_MODE, handle=None, use_errno=False, use_last_error=False, winmode=None): + class _FuncPtr(_CFuncPtr): + _flags_ = self._func_flags_ + _restype_ = self._func_restype_ + if use_errno: + _flags_ |= _FUNCFLAG_USE_ERRNO + if use_last_error: + _flags_ |= _FUNCFLAG_USE_LASTERROR + + self._FuncPtr = _FuncPtr if name: name = _os.fspath(name) + self._handle = self._load_library(name, mode, handle, winmode) + + if _os.name == "nt": + def _load_library(self, name, mode, handle, winmode): + if winmode is None: + import nt as _nt + winmode = _nt._LOAD_LIBRARY_SEARCH_DEFAULT_DIRS + # WINAPI LoadLibrary searches for a DLL if the given name + # is not fully qualified with an explicit drive. For POSIX + # compatibility, and because the DLL search path no longer + # contains the working directory, begin by fully resolving + # any name that contains a path separator. + if name is not None and ('/' in name or '\\' in name): + name = _nt._getfullpathname(name) + winmode |= _nt._LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR + self._name = name + if handle is not None: + return handle + return _LoadLibrary(self._name, winmode) + + else: + def _load_library(self, name, mode, handle, winmode): # If the filename that has been provided is an iOS/tvOS/watchOS # .fwork file, dereference the location to the true origin of the # binary. - if name.endswith(".fwork"): + if name and name.endswith(".fwork"): with open(name) as f: name = _os.path.join( _os.path.dirname(_sys.executable), f.read().strip() ) - - self._name = name - flags = self._func_flags_ - if use_errno: - flags |= _FUNCFLAG_USE_ERRNO - if use_last_error: - flags |= _FUNCFLAG_USE_LASTERROR - if _sys.platform.startswith("aix"): - """When the name contains ".a(" and ends with ")", - e.g., "libFOO.a(libFOO.so)" - this is taken to be an - archive(member) syntax for dlopen(), and the mode is adjusted. - Otherwise, name is presented to dlopen() as a file argument. - """ - if name and name.endswith(")") and ".a(" in name: - mode |= ( _os.RTLD_MEMBER | _os.RTLD_NOW ) - if _os.name == "nt": - if winmode is not None: - mode = winmode - else: - import nt - mode = nt._LOAD_LIBRARY_SEARCH_DEFAULT_DIRS - if '/' in name or '\\' in name: - self._name = nt._getfullpathname(self._name) - mode |= nt._LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR - - class _FuncPtr(_CFuncPtr): - _flags_ = flags - _restype_ = self._func_restype_ - self._FuncPtr = _FuncPtr - - if handle is None: - self._handle = _dlopen(self._name, mode) - else: - self._handle = handle + if _sys.platform.startswith("aix"): + """When the name contains ".a(" and ends with ")", + e.g., "libFOO.a(libFOO.so)" - this is taken to be an + archive(member) syntax for dlopen(), and the mode is adjusted. + Otherwise, name is presented to dlopen() as a file argument. + """ + if name and name.endswith(")") and ".a(" in name: + mode |= _os.RTLD_MEMBER | _os.RTLD_NOW + self._name = name + return _dlopen(name, mode) def __repr__(self): return "<%s '%s', handle %x at %#x>" % \ @@ -480,10 +486,9 @@ def LoadLibrary(self, name): if _os.name == "nt": pythonapi = PyDLL("python dll", None, _sys.dllhandle) -elif _sys.platform == "android": - pythonapi = PyDLL("libpython%d.%d.so" % _sys.version_info[:2]) -elif _sys.platform == "cygwin": - pythonapi = PyDLL("libpython%d.%d.dll" % _sys.version_info[:2]) +elif _sys.platform in ["android", "cygwin"]: + # These are Unix-like platforms which use a dynamically-linked libpython. + pythonapi = PyDLL(_sysconfig.get_config_var("LDLIBRARY")) else: pythonapi = PyDLL(None) diff --git a/Lib/ctypes/_endian.py b/Lib/ctypes/_endian.py index 34dee64b1a..6382dd22b8 100644 --- a/Lib/ctypes/_endian.py +++ b/Lib/ctypes/_endian.py @@ -1,5 +1,5 @@ import sys -from ctypes import * +from ctypes import Array, Structure, Union _array_type = type(Array) @@ -15,8 +15,8 @@ def _other_endian(typ): # if typ is array if isinstance(typ, _array_type): return _other_endian(typ._type_) * typ._length_ - # if typ is structure - if issubclass(typ, Structure): + # if typ is structure or union + if issubclass(typ, (Structure, Union)): return typ raise TypeError("This type does not support other endian: %s" % typ) @@ -37,7 +37,7 @@ class _swapped_union_meta(_swapped_meta, type(Union)): pass ################################################################ # Note: The Structure metaclass checks for the *presence* (not the -# value!) of a _swapped_bytes_ attribute to determine the bit order in +# value!) of a _swappedbytes_ attribute to determine the bit order in # structures containing bit fields. if sys.byteorder == "little": diff --git a/Lib/ctypes/test/__init__.py b/Lib/ctypes/test/__init__.py deleted file mode 100644 index 6e496fa5a5..0000000000 --- a/Lib/ctypes/test/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -import os -import unittest -from test import support -from test.support import import_helper - - -# skip tests if _ctypes was not built -ctypes = import_helper.import_module('ctypes') -ctypes_symbols = dir(ctypes) - -def need_symbol(name): - return unittest.skipUnless(name in ctypes_symbols, - '{!r} is required'.format(name)) - -def load_tests(*args): - return support.load_package_tests(os.path.dirname(__file__), *args) diff --git a/Lib/ctypes/test/__main__.py b/Lib/ctypes/test/__main__.py deleted file mode 100644 index 362a9ec8cf..0000000000 --- a/Lib/ctypes/test/__main__.py +++ /dev/null @@ -1,4 +0,0 @@ -from ctypes.test import load_tests -import unittest - -unittest.main() diff --git a/Lib/ctypes/test/test_delattr.py b/Lib/ctypes/test/test_delattr.py deleted file mode 100644 index 0f4d58691b..0000000000 --- a/Lib/ctypes/test/test_delattr.py +++ /dev/null @@ -1,21 +0,0 @@ -import unittest -from ctypes import * - -class X(Structure): - _fields_ = [("foo", c_int)] - -class TestCase(unittest.TestCase): - def test_simple(self): - self.assertRaises(TypeError, - delattr, c_int(42), "value") - - def test_chararray(self): - self.assertRaises(TypeError, - delattr, (c_char * 5)(), "value") - - def test_struct(self): - self.assertRaises(TypeError, - delattr, X(), "foo") - -if __name__ == "__main__": - unittest.main() diff --git a/Lib/ctypes/test/test_simplesubclasses.py b/Lib/ctypes/test/test_simplesubclasses.py deleted file mode 100644 index 3da2794a79..0000000000 --- a/Lib/ctypes/test/test_simplesubclasses.py +++ /dev/null @@ -1,55 +0,0 @@ -import unittest -from ctypes import * - -class MyInt(c_int): - def __eq__(self, other): - if type(other) != MyInt: - return NotImplementedError - return self.value == other.value - -class Test(unittest.TestCase): - - def test_compare(self): - self.assertEqual(MyInt(3), MyInt(3)) - self.assertNotEqual(MyInt(42), MyInt(43)) - - def test_ignore_retval(self): - # Test if the return value of a callback is ignored - # if restype is None - proto = CFUNCTYPE(None) - def func(): - return (1, "abc", None) - - cb = proto(func) - self.assertEqual(None, cb()) - - - def test_int_callback(self): - args = [] - def func(arg): - args.append(arg) - return arg - - cb = CFUNCTYPE(None, MyInt)(func) - - self.assertEqual(None, cb(42)) - self.assertEqual(type(args[-1]), MyInt) - - cb = CFUNCTYPE(c_int, c_int)(func) - - self.assertEqual(42, cb(42)) - self.assertEqual(type(args[-1]), int) - - def test_int_struct(self): - class X(Structure): - _fields_ = [("x", MyInt)] - - self.assertEqual(X().x, MyInt()) - - s = X() - s.x = MyInt(42) - - self.assertEqual(s.x, MyInt(42)) - -if __name__ == "__main__": - unittest.main() diff --git a/Lib/ctypes/util.py b/Lib/ctypes/util.py index 0c2510e161..117bf06cb0 100644 --- a/Lib/ctypes/util.py +++ b/Lib/ctypes/util.py @@ -67,7 +67,7 @@ def find_library(name): return fname return None -elif os.name == "posix" and sys.platform == "darwin": +elif os.name == "posix" and sys.platform in {"darwin", "ios", "tvos", "watchos"}: from ctypes.macholib.dyld import dyld_find as _dyld_find def find_library(name): possible = ['lib%s.dylib' % name, @@ -89,6 +89,15 @@ def find_library(name): from ctypes._aix import find_library +elif sys.platform == "android": + def find_library(name): + directory = "/system/lib" + if "64" in os.uname().machine: + directory += "64" + + fname = f"{directory}/lib{name}.so" + return fname if os.path.isfile(fname) else None + elif os.name == "posix": # Andreas Degert's find functions, using gcc, /sbin/ldconfig, objdump import re, tempfile @@ -96,8 +105,11 @@ def find_library(name): def _is_elf(filename): "Return True if the given file is an ELF file" elf_header = b'\x7fELF' - with open(filename, 'br') as thefile: - return thefile.read(4) == elf_header + try: + with open(filename, 'br') as thefile: + return thefile.read(4) == elf_header + except FileNotFoundError: + return False def _findLib_gcc(name): # Run GCC's linker with the -t (aka --trace) option and examine the diff --git a/Lib/platform.py b/Lib/platform.py index 58b66078e1..c64c6d2c6f 100755 --- a/Lib/platform.py +++ b/Lib/platform.py @@ -1080,7 +1080,7 @@ def _sys_version(sys_version=None): match.groups() # XXX: RUSTPYTHON support - if "rustc" in sys_version: + if "RustPython" in sys_version: name = "RustPython" else: name = 'CPython' diff --git a/Lib/test/test_ctypes.py b/Lib/test/test_ctypes.py deleted file mode 100644 index b0a12c9734..0000000000 --- a/Lib/test/test_ctypes.py +++ /dev/null @@ -1,10 +0,0 @@ -import unittest -from test.support.import_helper import import_module - - -ctypes_test = import_module('ctypes.test') - -load_tests = ctypes_test.load_tests - -if __name__ == "__main__": - unittest.main() diff --git a/Lib/test/test_ctypes/__init__.py b/Lib/test/test_ctypes/__init__.py new file mode 100644 index 0000000000..eb9126cbe1 --- /dev/null +++ b/Lib/test/test_ctypes/__init__.py @@ -0,0 +1,10 @@ +import os +from test import support +from test.support import import_helper + + +# skip tests if the _ctypes extension was not built +import_helper.import_module('ctypes') + +def load_tests(*args): + return support.load_package_tests(os.path.dirname(__file__), *args) diff --git a/Lib/test/test_ctypes/__main__.py b/Lib/test/test_ctypes/__main__.py new file mode 100644 index 0000000000..3003d4db89 --- /dev/null +++ b/Lib/test/test_ctypes/__main__.py @@ -0,0 +1,4 @@ +from test.test_ctypes import load_tests +import unittest + +unittest.main() diff --git a/Lib/test/test_ctypes/_support.py b/Lib/test/test_ctypes/_support.py new file mode 100644 index 0000000000..e4c2b33825 --- /dev/null +++ b/Lib/test/test_ctypes/_support.py @@ -0,0 +1,24 @@ +# Some classes and types are not export to _ctypes module directly. + +import ctypes +from _ctypes import Structure, Union, _Pointer, Array, _SimpleCData, CFuncPtr + + +_CData = Structure.__base__ +assert _CData.__name__ == "_CData" + +class _X(Structure): + _fields_ = [("x", ctypes.c_int)] +CField = type(_X.x) + +# metaclasses +PyCStructType = type(Structure) +UnionType = type(Union) +PyCPointerType = type(_Pointer) +PyCArrayType = type(Array) +PyCSimpleType = type(_SimpleCData) +PyCFuncPtrType = type(CFuncPtr) + +# type flags +Py_TPFLAGS_DISALLOW_INSTANTIATION = 1 << 7 +Py_TPFLAGS_IMMUTABLETYPE = 1 << 8 diff --git a/Lib/test/test_ctypes/test_aligned_structures.py b/Lib/test/test_ctypes/test_aligned_structures.py new file mode 100644 index 0000000000..8e8ac42990 --- /dev/null +++ b/Lib/test/test_ctypes/test_aligned_structures.py @@ -0,0 +1,321 @@ +from ctypes import ( + c_char, c_uint32, c_uint16, c_ubyte, c_byte, alignment, sizeof, + BigEndianStructure, LittleEndianStructure, + BigEndianUnion, LittleEndianUnion, Structure +) +import struct +import unittest + + +class TestAlignedStructures(unittest.TestCase): + def test_aligned_string(self): + for base, e in ( + (LittleEndianStructure, "<"), + (BigEndianStructure, ">"), + ): + data = bytearray(struct.pack(f"{e}i12x16s", 7, b"hello world!")) + class Aligned(base): + _align_ = 16 + _fields_ = [ + ('value', c_char * 12) + ] + + class Main(base): + _fields_ = [ + ('first', c_uint32), + ('string', Aligned), + ] + + main = Main.from_buffer(data) + self.assertEqual(main.first, 7) + self.assertEqual(main.string.value, b'hello world!') + self.assertEqual(bytes(main.string), b'hello world!\0\0\0\0') + self.assertEqual(Main.string.offset, 16) + self.assertEqual(Main.string.size, 16) + self.assertEqual(alignment(main.string), 16) + self.assertEqual(alignment(main), 16) + + def test_aligned_structures(self): + for base, data in ( + (LittleEndianStructure, bytearray(b"\1\0\0\0\1\0\0\0\7\0\0\0")), + (BigEndianStructure, bytearray(b"\1\0\0\0\1\0\0\0\7\0\0\0")), + ): + class SomeBools(base): + _align_ = 4 + _fields_ = [ + ("bool1", c_ubyte), + ("bool2", c_ubyte), + ] + class Main(base): + _fields_ = [ + ("x", c_ubyte), + ("y", SomeBools), + ("z", c_ubyte), + ] + + main = Main.from_buffer(data) + self.assertEqual(alignment(SomeBools), 4) + self.assertEqual(alignment(main), 4) + self.assertEqual(alignment(main.y), 4) + self.assertEqual(Main.x.size, 1) + self.assertEqual(Main.y.offset, 4) + self.assertEqual(Main.y.size, 4) + self.assertEqual(main.y.bool1, True) + self.assertEqual(main.y.bool2, False) + self.assertEqual(Main.z.offset, 8) + self.assertEqual(main.z, 7) + + def test_oversized_structure(self): + data = bytearray(b"\0" * 8) + for base in (LittleEndianStructure, BigEndianStructure): + class SomeBoolsTooBig(base): + _align_ = 8 + _fields_ = [ + ("bool1", c_ubyte), + ("bool2", c_ubyte), + ("bool3", c_ubyte), + ] + class Main(base): + _fields_ = [ + ("y", SomeBoolsTooBig), + ("z", c_uint32), + ] + with self.assertRaises(ValueError) as ctx: + Main.from_buffer(data) + self.assertEqual( + ctx.exception.args[0], + 'Buffer size too small (4 instead of at least 8 bytes)' + ) + + def test_aligned_subclasses(self): + for base, e in ( + (LittleEndianStructure, "<"), + (BigEndianStructure, ">"), + ): + data = bytearray(struct.pack(f"{e}4i", 1, 2, 3, 4)) + class UnalignedSub(base): + x: c_uint32 + _fields_ = [ + ("x", c_uint32), + ] + + class AlignedStruct(UnalignedSub): + _align_ = 8 + _fields_ = [ + ("y", c_uint32), + ] + + class Main(base): + _fields_ = [ + ("a", c_uint32), + ("b", AlignedStruct) + ] + + main = Main.from_buffer(data) + self.assertEqual(alignment(main.b), 8) + self.assertEqual(alignment(main), 8) + self.assertEqual(sizeof(main.b), 8) + self.assertEqual(sizeof(main), 16) + self.assertEqual(main.a, 1) + self.assertEqual(main.b.x, 3) + self.assertEqual(main.b.y, 4) + self.assertEqual(Main.b.offset, 8) + self.assertEqual(Main.b.size, 8) + + def test_aligned_union(self): + for sbase, ubase, e in ( + (LittleEndianStructure, LittleEndianUnion, "<"), + (BigEndianStructure, BigEndianUnion, ">"), + ): + data = bytearray(struct.pack(f"{e}4i", 1, 2, 3, 4)) + class AlignedUnion(ubase): + _align_ = 8 + _fields_ = [ + ("a", c_uint32), + ("b", c_ubyte * 7), + ] + + class Main(sbase): + _fields_ = [ + ("first", c_uint32), + ("union", AlignedUnion), + ] + + main = Main.from_buffer(data) + self.assertEqual(main.first, 1) + self.assertEqual(main.union.a, 3) + self.assertEqual(bytes(main.union.b), data[8:-1]) + self.assertEqual(Main.union.offset, 8) + self.assertEqual(Main.union.size, 8) + self.assertEqual(alignment(main.union), 8) + self.assertEqual(alignment(main), 8) + + def test_aligned_struct_in_union(self): + for sbase, ubase, e in ( + (LittleEndianStructure, LittleEndianUnion, "<"), + (BigEndianStructure, BigEndianUnion, ">"), + ): + data = bytearray(struct.pack(f"{e}4i", 1, 2, 3, 4)) + class Sub(sbase): + _align_ = 8 + _fields_ = [ + ("x", c_uint32), + ("y", c_uint32), + ] + + class MainUnion(ubase): + _fields_ = [ + ("a", c_uint32), + ("b", Sub), + ] + + class Main(sbase): + _fields_ = [ + ("first", c_uint32), + ("union", MainUnion), + ] + + main = Main.from_buffer(data) + self.assertEqual(Main.first.size, 4) + self.assertEqual(alignment(main.union), 8) + self.assertEqual(alignment(main), 8) + self.assertEqual(Main.union.offset, 8) + self.assertEqual(Main.union.size, 8) + self.assertEqual(main.first, 1) + self.assertEqual(main.union.a, 3) + self.assertEqual(main.union.b.x, 3) + self.assertEqual(main.union.b.y, 4) + + def test_smaller_aligned_subclassed_union(self): + for sbase, ubase, e in ( + (LittleEndianStructure, LittleEndianUnion, "<"), + (BigEndianStructure, BigEndianUnion, ">"), + ): + data = bytearray(struct.pack(f"{e}H2xI", 1, 0xD60102D7)) + class SubUnion(ubase): + _align_ = 2 + _fields_ = [ + ("unsigned", c_ubyte), + ("signed", c_byte), + ] + + class MainUnion(SubUnion): + _fields_ = [ + ("num", c_uint32) + ] + + class Main(sbase): + _fields_ = [ + ("first", c_uint16), + ("union", MainUnion), + ] + + main = Main.from_buffer(data) + self.assertEqual(main.union.num, 0xD60102D7) + self.assertEqual(main.union.unsigned, data[4]) + self.assertEqual(main.union.signed, data[4] - 256) + self.assertEqual(alignment(main), 4) + self.assertEqual(alignment(main.union), 4) + self.assertEqual(Main.union.offset, 4) + self.assertEqual(Main.union.size, 4) + self.assertEqual(Main.first.size, 2) + + def test_larger_aligned_subclassed_union(self): + for ubase, e in ( + (LittleEndianUnion, "<"), + (BigEndianUnion, ">"), + ): + data = bytearray(struct.pack(f"{e}I4x", 0xD60102D6)) + class SubUnion(ubase): + _align_ = 8 + _fields_ = [ + ("unsigned", c_ubyte), + ("signed", c_byte), + ] + + class Main(SubUnion): + _fields_ = [ + ("num", c_uint32) + ] + + main = Main.from_buffer(data) + self.assertEqual(alignment(main), 8) + self.assertEqual(sizeof(main), 8) + self.assertEqual(main.num, 0xD60102D6) + self.assertEqual(main.unsigned, 0xD6) + self.assertEqual(main.signed, -42) + + def test_aligned_packed_structures(self): + for sbase, e in ( + (LittleEndianStructure, "<"), + (BigEndianStructure, ">"), + ): + data = bytearray(struct.pack(f"{e}B2H4xB", 1, 2, 3, 4)) + + class Inner(sbase): + _align_ = 8 + _fields_ = [ + ("x", c_uint16), + ("y", c_uint16), + ] + + class Main(sbase): + _pack_ = 1 + _fields_ = [ + ("a", c_ubyte), + ("b", Inner), + ("c", c_ubyte), + ] + + main = Main.from_buffer(data) + self.assertEqual(sizeof(main), 10) + self.assertEqual(Main.b.offset, 1) + # Alignment == 8 because _pack_ wins out. + self.assertEqual(alignment(main.b), 8) + # Size is still 8 though since inside this Structure, it will have + # effect. + self.assertEqual(sizeof(main.b), 8) + self.assertEqual(Main.c.offset, 9) + self.assertEqual(main.a, 1) + self.assertEqual(main.b.x, 2) + self.assertEqual(main.b.y, 3) + self.assertEqual(main.c, 4) + + def test_negative_align(self): + for base in (Structure, LittleEndianStructure, BigEndianStructure): + with ( + self.subTest(base=base), + self.assertRaisesRegex( + ValueError, + '_align_ must be a non-negative integer', + ) + ): + class MyStructure(base): + _align_ = -1 + _fields_ = [] + + def test_zero_align_no_fields(self): + for base in (Structure, LittleEndianStructure, BigEndianStructure): + with self.subTest(base=base): + class MyStructure(base): + _align_ = 0 + _fields_ = [] + + self.assertEqual(alignment(MyStructure), 1) + self.assertEqual(alignment(MyStructure()), 1) + + def test_zero_align_with_fields(self): + for base in (Structure, LittleEndianStructure, BigEndianStructure): + with self.subTest(base=base): + class MyStructure(base): + _align_ = 0 + _fields_ = [ + ("x", c_ubyte), + ] + + self.assertEqual(alignment(MyStructure), 1) + self.assertEqual(alignment(MyStructure()), 1) + + +if __name__ == '__main__': + unittest.main() diff --git a/Lib/ctypes/test/test_anon.py b/Lib/test/test_ctypes/test_anon.py similarity index 97% rename from Lib/ctypes/test/test_anon.py rename to Lib/test/test_ctypes/test_anon.py index d378392ebe..b36397b510 100644 --- a/Lib/ctypes/test/test_anon.py +++ b/Lib/test/test_ctypes/test_anon.py @@ -1,6 +1,7 @@ import unittest import test.support -from ctypes import * +from ctypes import c_int, Union, Structure, sizeof + class AnonTest(unittest.TestCase): @@ -69,5 +70,6 @@ class Y(Structure): self.assertEqual(Y._.offset, sizeof(c_int)) self.assertEqual(Y.y.offset, sizeof(c_int) * 2) + if __name__ == "__main__": unittest.main() diff --git a/Lib/ctypes/test/test_array_in_pointer.py b/Lib/test/test_ctypes/test_array_in_pointer.py similarity index 93% rename from Lib/ctypes/test/test_array_in_pointer.py rename to Lib/test/test_ctypes/test_array_in_pointer.py index ca1edcf621..b7c96b2fa4 100644 --- a/Lib/ctypes/test/test_array_in_pointer.py +++ b/Lib/test/test_ctypes/test_array_in_pointer.py @@ -1,21 +1,24 @@ -import unittest -from ctypes import * -from binascii import hexlify +import binascii import re +import unittest +from ctypes import c_byte, Structure, POINTER, cast + def dump(obj): # helper function to dump memory contents in hex, with a hyphen # between the bytes. - h = hexlify(memoryview(obj)).decode() + h = binascii.hexlify(memoryview(obj)).decode() return re.sub(r"(..)", r"\1-", h)[:-1] class Value(Structure): _fields_ = [("val", c_byte)] + class Container(Structure): _fields_ = [("pvalues", POINTER(Value))] + class Test(unittest.TestCase): def test(self): # create an array of 4 values @@ -60,5 +63,6 @@ def test_2(self): ([1, 2, 3, 4], "01-02-03-04") ) + if __name__ == "__main__": unittest.main() diff --git a/Lib/ctypes/test/test_arrays.py b/Lib/test/test_ctypes/test_arrays.py similarity index 77% rename from Lib/ctypes/test/test_arrays.py rename to Lib/test/test_ctypes/test_arrays.py index 14603b7049..c80fdff5de 100644 --- a/Lib/ctypes/test/test_arrays.py +++ b/Lib/test/test_ctypes/test_arrays.py @@ -1,16 +1,50 @@ +import ctypes +import sys import unittest +from ctypes import (Structure, Array, ARRAY, sizeof, addressof, + create_string_buffer, create_unicode_buffer, + c_char, c_wchar, c_byte, c_ubyte, c_short, c_ushort, c_int, c_uint, + c_long, c_ulonglong, c_float, c_double, c_longdouble) from test.support import bigmemtest, _2G -import sys -from ctypes import * +from ._support import (_CData, PyCArrayType, Py_TPFLAGS_DISALLOW_INSTANTIATION, + Py_TPFLAGS_IMMUTABLETYPE) -from ctypes.test import need_symbol formats = "bBhHiIlLqQfd" formats = c_byte, c_ubyte, c_short, c_ushort, c_int, c_uint, \ c_long, c_ulonglong, c_float, c_double, c_longdouble + class ArrayTestCase(unittest.TestCase): + def test_inheritance_hierarchy(self): + self.assertEqual(Array.mro(), [Array, _CData, object]) + + self.assertEqual(PyCArrayType.__name__, "PyCArrayType") + self.assertEqual(type(PyCArrayType), type) + + def test_type_flags(self): + for cls in Array, PyCArrayType: + with self.subTest(cls=cls): + self.assertTrue(cls.__flags__ & Py_TPFLAGS_IMMUTABLETYPE) + self.assertFalse(cls.__flags__ & Py_TPFLAGS_DISALLOW_INSTANTIATION) + + def test_metaclass_details(self): + # Abstract classes (whose metaclass __init__ was not called) can't be + # instantiated directly + NewArray = PyCArrayType.__new__(PyCArrayType, 'NewArray', (Array,), {}) + for cls in Array, NewArray: + with self.subTest(cls=cls): + with self.assertRaisesRegex(TypeError, "abstract class"): + obj = cls() + + # Cannot call the metaclass __init__ more than once + class T(Array): + _type_ = c_int + _length_ = 13 + with self.assertRaisesRegex(SystemError, "already initialized"): + PyCArrayType.__init__(T, 'ptr', (), {}) + def test_simple(self): # create classes holding simple numeric types, and check # various properties. @@ -34,9 +68,9 @@ def test_simple(self): with self.assertRaises(IndexError): ia[-alen-1] # change the items - from operator import setitem new_values = list(range(42, 42+alen)) - [setitem(ia, n, new_values[n]) for n in range(alen)] + for n in range(alen): + ia[n] = new_values[n] values = [ia[i] for i in range(alen)] self.assertEqual(values, new_values) @@ -66,8 +100,8 @@ def test_simple(self): self.assertEqual(len(ca), 3) # cannot delete items - from operator import delitem - self.assertRaises(TypeError, delitem, ca, 0) + with self.assertRaises(TypeError): + del ca[0] def test_step_overflow(self): a = (c_int * 5)() @@ -117,7 +151,6 @@ def test_from_address(self): self.assertEqual(sz[1:4:2], b"o") self.assertEqual(sz.value, b"foo") - @need_symbol('create_unicode_buffer') def test_from_addressW(self): p = create_unicode_buffer("foo") sz = (c_wchar * 3).from_address(addressof(p)) @@ -178,10 +211,10 @@ def test_bad_subclass(self): class T(Array): pass with self.assertRaises(AttributeError): - class T(Array): + class T2(Array): _type_ = c_int with self.assertRaises(AttributeError): - class T(Array): + class T3(Array): _length_ = 13 def test_bad_length(self): @@ -190,15 +223,15 @@ class T(Array): _type_ = c_int _length_ = - sys.maxsize * 2 with self.assertRaises(ValueError): - class T(Array): + class T2(Array): _type_ = c_int _length_ = -1 with self.assertRaises(TypeError): - class T(Array): + class T3(Array): _type_ = c_int _length_ = 1.87 with self.assertRaises(OverflowError): - class T(Array): + class T4(Array): _type_ = c_int _length_ = sys.maxsize * 2 @@ -212,7 +245,7 @@ def test_empty_element_struct(self): class EmptyStruct(Structure): _fields_ = [] - obj = (EmptyStruct * 2)() # bpo37188: Floating point exception + obj = (EmptyStruct * 2)() # bpo37188: Floating-point exception self.assertEqual(sizeof(obj), 0) def test_empty_element_array(self): @@ -220,7 +253,7 @@ class EmptyArray(Array): _type_ = c_int _length_ = 0 - obj = (EmptyArray * 2)() # bpo37188: Floating point exception + obj = (EmptyArray * 2)() # bpo37188: Floating-point exception self.assertEqual(sizeof(obj), 0) def test_bpo36504_signed_int_overflow(self): @@ -234,5 +267,6 @@ def test_bpo36504_signed_int_overflow(self): def test_large_array(self, size): c_char * size + if __name__ == '__main__': unittest.main() diff --git a/Lib/ctypes/test/test_as_parameter.py b/Lib/test/test_ctypes/test_as_parameter.py similarity index 84% rename from Lib/ctypes/test/test_as_parameter.py rename to Lib/test/test_ctypes/test_as_parameter.py index 9c39179d2a..c5e1840b0e 100644 --- a/Lib/ctypes/test/test_as_parameter.py +++ b/Lib/test/test_ctypes/test_as_parameter.py @@ -1,24 +1,31 @@ +import ctypes import unittest -from ctypes import * -from ctypes.test import need_symbol -import _ctypes_test +from ctypes import (Structure, CDLL, CFUNCTYPE, + POINTER, pointer, byref, + c_short, c_int, c_long, c_longlong, + c_byte, c_wchar, c_float, c_double, + ArgumentError) +from test.support import import_helper +_ctypes_test = import_helper.import_module("_ctypes_test") + dll = CDLL(_ctypes_test.__file__) try: - CALLBACK_FUNCTYPE = WINFUNCTYPE -except NameError: + CALLBACK_FUNCTYPE = ctypes.WINFUNCTYPE +except AttributeError: # fake to enable this test on Linux CALLBACK_FUNCTYPE = CFUNCTYPE + class POINT(Structure): _fields_ = [("x", c_int), ("y", c_int)] + class BasicWrapTestCase(unittest.TestCase): def wrap(self, param): return param - @need_symbol('c_wchar') def test_wchar_parm(self): f = dll._testfunc_i_bhilfd f.argtypes = [c_byte, c_wchar, c_int, c_long, c_float, c_double] @@ -67,8 +74,6 @@ def callback(v): f(self.wrap(2**18), self.wrap(cb)) self.assertEqual(args, expected) - ################################################################ - def test_callbacks(self): f = dll._testfunc_callback_i_if f.restype = c_int @@ -77,7 +82,6 @@ def test_callbacks(self): MyCallback = CFUNCTYPE(c_int, c_int) def callback(value): - #print "called back with", value return value cb = MyCallback(callback) @@ -114,7 +118,6 @@ def test_callbacks_2(self): f.argtypes = [c_int, MyCallback] def callback(value): - #print "called back with", value self.assertEqual(type(value), int) return value @@ -122,9 +125,7 @@ def callback(value): result = f(self.wrap(-10), self.wrap(cb)) self.assertEqual(result, -18) - @need_symbol('c_longlong') def test_longlong_callbacks(self): - f = dll._testfunc_callback_q_qf f.restype = c_longlong @@ -192,29 +193,32 @@ class S8I(Structure): (9*2, 8*3, 7*4, 6*5, 5*6, 4*7, 3*8, 2*9)) def test_recursive_as_param(self): - from ctypes import c_int - - class A(object): + class A: pass a = A() a._as_parameter_ = a - with self.assertRaises(RecursionError): - c_int.from_param(a) - - -#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -class AsParamWrapper(object): + for c_type in ( + ctypes.c_wchar_p, + ctypes.c_char_p, + ctypes.c_void_p, + ctypes.c_int, # PyCSimpleType + POINT, # CDataType + ): + with self.subTest(c_type=c_type): + with self.assertRaises(RecursionError): + c_type.from_param(a) + + +class AsParamWrapper: def __init__(self, param): self._as_parameter_ = param class AsParamWrapperTestCase(BasicWrapTestCase): wrap = AsParamWrapper -#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -class AsParamPropertyWrapper(object): +class AsParamPropertyWrapper: def __init__(self, param): self._param = param @@ -225,7 +229,17 @@ def getParameter(self): class AsParamPropertyWrapperTestCase(BasicWrapTestCase): wrap = AsParamPropertyWrapper -#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +class AsParamNestedWrapperTestCase(BasicWrapTestCase): + """Test that _as_parameter_ is evaluated recursively. + + The _as_parameter_ attribute can be another object which + defines its own _as_parameter_ attribute. + """ + + def wrap(self, param): + return AsParamWrapper(AsParamWrapper(AsParamWrapper(param))) + if __name__ == '__main__': unittest.main() diff --git a/Lib/ctypes/test/test_bitfields.py b/Lib/test/test_ctypes/test_bitfields.py similarity index 95% rename from Lib/ctypes/test/test_bitfields.py rename to Lib/test/test_ctypes/test_bitfields.py index 66acd62e68..0332544b58 100644 --- a/Lib/ctypes/test/test_bitfields.py +++ b/Lib/test/test_ctypes/test_bitfields.py @@ -1,10 +1,14 @@ -from ctypes import * -from ctypes.test import need_symbol -from test import support -import unittest import os +import unittest +from ctypes import (CDLL, Structure, sizeof, POINTER, byref, alignment, + LittleEndianStructure, BigEndianStructure, + c_byte, c_ubyte, c_char, c_char_p, c_void_p, c_wchar, + c_uint32, c_uint64, + c_short, c_ushort, c_int, c_uint, c_long, c_ulong, c_longlong, c_ulonglong) +from test import support +from test.support import import_helper +_ctypes_test = import_helper.import_module("_ctypes_test") -import _ctypes_test class BITS(Structure): _fields_ = [("A", c_int, 1), @@ -28,8 +32,6 @@ class BITS(Structure): func = CDLL(_ctypes_test.__file__).unpack_bitfields func.argtypes = POINTER(BITS), c_char -##for n in "ABCDEFGHIMNOPQRS": -## print n, hex(getattr(BITS, n).size), getattr(BITS, n).offset class C_Test(unittest.TestCase): @@ -53,6 +55,7 @@ def test_shorts(self): setattr(b, name, i) self.assertEqual(getattr(b, name), func(byref(b), name.encode('ascii'))) + signed_int_types = (c_byte, c_short, c_int, c_long, c_longlong) unsigned_int_types = (c_ubyte, c_ushort, c_uint, c_ulong, c_ulonglong) int_types = unsigned_int_types + signed_int_types @@ -114,7 +117,6 @@ class X(Structure): x.a, x.b = 0, -1 self.assertEqual((c_typ, x.a, x.b, x.c), (c_typ, 0, 7, 0)) - def fail_fields(self, *fields): return self.get_except(type(Structure), "X", (), {"_fields_": fields}) @@ -140,7 +142,6 @@ class Dummy(Structure): result = self.fail_fields(("a", Dummy, 1)) self.assertEqual(result, (TypeError, 'bit fields not allowed for type Dummy')) - @need_symbol('c_wchar') def test_c_wchar(self): result = self.fail_fields(("a", c_wchar, 1)) self.assertEqual(result, @@ -192,7 +193,6 @@ class X(Structure): self.assertEqual(X.b.offset, sizeof(c_short)*1) self.assertEqual(X.c.offset, sizeof(c_short)*2) - def get_except(self, func, *args, **kw): try: func(*args, **kw) @@ -245,7 +245,6 @@ class Y(Structure): _anonymous_ = ["_"] _fields_ = [("_", X)] - @need_symbol('c_uint32') def test_uint32(self): class X(Structure): _fields_ = [("a", c_uint32, 32)] @@ -255,7 +254,6 @@ class X(Structure): x.a = 0xFDCBA987 self.assertEqual(x.a, 0xFDCBA987) - @need_symbol('c_uint64') def test_uint64(self): class X(Structure): _fields_ = [("a", c_uint64, 64)] @@ -265,7 +263,6 @@ class X(Structure): x.a = 0xFEDCBA9876543211 self.assertEqual(x.a, 0xFEDCBA9876543211) - @need_symbol('c_uint32') def test_uint32_swap_little_endian(self): # Issue #23319 class Little(LittleEndianStructure): @@ -279,7 +276,6 @@ class Little(LittleEndianStructure): x.c = 2 self.assertEqual(b, b'\xef\xcd\xab\x21') - @need_symbol('c_uint32') def test_uint32_swap_big_endian(self): # Issue #23319 class Big(BigEndianStructure): @@ -293,5 +289,6 @@ class Big(BigEndianStructure): x.c = 2 self.assertEqual(b, b'\xab\xcd\xef\x12') + if __name__ == "__main__": unittest.main() diff --git a/Lib/ctypes/test/test_buffers.py b/Lib/test/test_ctypes/test_buffers.py similarity index 94% rename from Lib/ctypes/test/test_buffers.py rename to Lib/test/test_ctypes/test_buffers.py index 15782be757..468f41eb7c 100644 --- a/Lib/ctypes/test/test_buffers.py +++ b/Lib/test/test_ctypes/test_buffers.py @@ -1,9 +1,9 @@ -from ctypes import * -from ctypes.test import need_symbol import unittest +from ctypes import (create_string_buffer, create_unicode_buffer, sizeof, + c_char, c_wchar) -class StringBufferTestCase(unittest.TestCase): +class StringBufferTestCase(unittest.TestCase): def test_buffer(self): b = create_string_buffer(32) self.assertEqual(len(b), 32) @@ -27,7 +27,6 @@ def test_buffer_interface(self): self.assertEqual(len(bytearray(create_string_buffer(0))), 0) self.assertEqual(len(bytearray(create_string_buffer(1))), 1) - @need_symbol('c_wchar') def test_unicode_buffer(self): b = create_unicode_buffer(32) self.assertEqual(len(b), 32) @@ -47,7 +46,6 @@ def test_unicode_buffer(self): self.assertRaises(TypeError, create_unicode_buffer, b"abc") - @need_symbol('c_wchar') def test_unicode_conversion(self): b = create_unicode_buffer("abc") self.assertEqual(len(b), 4) # trailing nul char @@ -60,7 +58,6 @@ def test_unicode_conversion(self): self.assertEqual(b[::2], "ac") self.assertEqual(b[::5], "a") - @need_symbol('c_wchar') def test_create_unicode_buffer_non_bmp(self): expected = 5 if sizeof(c_wchar) == 2 else 3 for s in '\U00010000\U00100000', '\U00010000\U0010ffff': diff --git a/Lib/ctypes/test/test_bytes.py b/Lib/test/test_ctypes/test_bytes.py similarity index 95% rename from Lib/ctypes/test/test_bytes.py rename to Lib/test/test_ctypes/test_bytes.py index 092ec5af05..fa11e1bbd4 100644 --- a/Lib/ctypes/test/test_bytes.py +++ b/Lib/test/test_ctypes/test_bytes.py @@ -1,7 +1,9 @@ """Test where byte objects are accepted""" -import unittest import sys -from ctypes import * +import unittest +from _ctypes import _SimpleCData +from ctypes import Structure, c_char, c_char_p, c_wchar, c_wchar_p + class BytesTest(unittest.TestCase): def test_c_char(self): @@ -55,7 +57,6 @@ class X(Structure): @unittest.skipUnless(sys.platform == "win32", 'Windows-specific test') def test_BSTR(self): - from _ctypes import _SimpleCData class BSTR(_SimpleCData): _type_ = "X" diff --git a/Lib/ctypes/test/test_byteswap.py b/Lib/test/test_ctypes/test_byteswap.py similarity index 92% rename from Lib/ctypes/test/test_byteswap.py rename to Lib/test/test_ctypes/test_byteswap.py index 7e98559dfb..78eff0392c 100644 --- a/Lib/ctypes/test/test_byteswap.py +++ b/Lib/test/test_ctypes/test_byteswap.py @@ -1,10 +1,21 @@ -import sys, unittest, struct, math, ctypes -from binascii import hexlify +import binascii +import ctypes +import math +import struct +import sys +import unittest +from ctypes import (Structure, Union, LittleEndianUnion, BigEndianUnion, + BigEndianStructure, LittleEndianStructure, + POINTER, sizeof, cast, + c_byte, c_ubyte, c_char, c_wchar, c_void_p, + c_short, c_ushort, c_int, c_uint, + c_long, c_ulong, c_longlong, c_ulonglong, + c_uint32, c_float, c_double) -from ctypes import * def bin(s): - return hexlify(memoryview(s)).decode().upper() + return binascii.hexlify(memoryview(s)).decode().upper() + # Each *simple* type that supports different byte orders has an # __ctype_be__ attribute that specifies the same type in BIG ENDIAN @@ -14,14 +25,6 @@ def bin(s): # For Structures and Unions, these types are created on demand. class Test(unittest.TestCase): - @unittest.skip('test disabled') - def test_X(self): - print(sys.byteorder, file=sys.stderr) - for i in range(32): - bits = BITS() - setattr(bits, "i%s" % i, 1) - dump(bits) - def test_slots(self): class BigPoint(BigEndianStructure): __slots__ = () @@ -360,5 +363,24 @@ class TestUnion(parent): self.assertEqual(s.point.x, 1) self.assertEqual(s.point.y, 2) + def test_build_struct_union_opposite_system_byteorder(self): + # gh-105102 + if sys.byteorder == "little": + _Structure = BigEndianStructure + _Union = BigEndianUnion + else: + _Structure = LittleEndianStructure + _Union = LittleEndianUnion + + class S1(_Structure): + _fields_ = [("a", c_byte), ("b", c_byte)] + + class U1(_Union): + _fields_ = [("s1", S1), ("ab", c_short)] + + class S2(_Structure): + _fields_ = [("u1", U1), ("c", c_byte)] + + if __name__ == "__main__": unittest.main() diff --git a/Lib/test/test_ctypes/test_c_simple_type_meta.py b/Lib/test/test_ctypes/test_c_simple_type_meta.py new file mode 100644 index 0000000000..eb77d6d778 --- /dev/null +++ b/Lib/test/test_ctypes/test_c_simple_type_meta.py @@ -0,0 +1,152 @@ +import unittest +import ctypes +from ctypes import POINTER, c_void_p + +from ._support import PyCSimpleType + + +class PyCSimpleTypeAsMetaclassTest(unittest.TestCase): + def tearDown(self): + # to not leak references, we must clean _pointer_type_cache + ctypes._reset_cache() + + def test_creating_pointer_in_dunder_new_1(self): + # Test metaclass whose instances are C types; when the type is + # created it automatically creates a pointer type for itself. + # The pointer type is also an instance of the metaclass. + # Such an implementation is used in `IUnknown` of the `comtypes` + # project. See gh-124520. + + class ct_meta(type): + def __new__(cls, name, bases, namespace): + self = super().__new__(cls, name, bases, namespace) + + # Avoid recursion: don't set up a pointer to + # a pointer (to a pointer...) + if bases == (c_void_p,): + # When creating PtrBase itself, the name + # is not yet available + return self + if issubclass(self, PtrBase): + return self + + if bases == (object,): + ptr_bases = (self, PtrBase) + else: + ptr_bases = (self, POINTER(bases[0])) + p = p_meta(f"POINTER({self.__name__})", ptr_bases, {}) + ctypes._pointer_type_cache[self] = p + return self + + class p_meta(PyCSimpleType, ct_meta): + pass + + class PtrBase(c_void_p, metaclass=p_meta): + pass + + class CtBase(object, metaclass=ct_meta): + pass + + class Sub(CtBase): + pass + + class Sub2(Sub): + pass + + self.assertIsInstance(POINTER(Sub2), p_meta) + self.assertTrue(issubclass(POINTER(Sub2), Sub2)) + self.assertTrue(issubclass(POINTER(Sub2), POINTER(Sub))) + self.assertTrue(issubclass(POINTER(Sub), POINTER(CtBase))) + + def test_creating_pointer_in_dunder_new_2(self): + # A simpler variant of the above, used in `CoClass` of the `comtypes` + # project. + + class ct_meta(type): + def __new__(cls, name, bases, namespace): + self = super().__new__(cls, name, bases, namespace) + if isinstance(self, p_meta): + return self + p = p_meta(f"POINTER({self.__name__})", (self, c_void_p), {}) + ctypes._pointer_type_cache[self] = p + return self + + class p_meta(PyCSimpleType, ct_meta): + pass + + class Core(object): + pass + + class CtBase(Core, metaclass=ct_meta): + pass + + class Sub(CtBase): + pass + + self.assertIsInstance(POINTER(Sub), p_meta) + self.assertTrue(issubclass(POINTER(Sub), Sub)) + + def test_creating_pointer_in_dunder_init_1(self): + class ct_meta(type): + def __init__(self, name, bases, namespace): + super().__init__(name, bases, namespace) + + # Avoid recursion. + # (See test_creating_pointer_in_dunder_new_1) + if bases == (c_void_p,): + return + if issubclass(self, PtrBase): + return + if bases == (object,): + ptr_bases = (self, PtrBase) + else: + ptr_bases = (self, POINTER(bases[0])) + p = p_meta(f"POINTER({self.__name__})", ptr_bases, {}) + ctypes._pointer_type_cache[self] = p + + class p_meta(PyCSimpleType, ct_meta): + pass + + class PtrBase(c_void_p, metaclass=p_meta): + pass + + class CtBase(object, metaclass=ct_meta): + pass + + class Sub(CtBase): + pass + + class Sub2(Sub): + pass + + self.assertIsInstance(POINTER(Sub2), p_meta) + self.assertTrue(issubclass(POINTER(Sub2), Sub2)) + self.assertTrue(issubclass(POINTER(Sub2), POINTER(Sub))) + self.assertTrue(issubclass(POINTER(Sub), POINTER(CtBase))) + + def test_creating_pointer_in_dunder_init_2(self): + class ct_meta(type): + def __init__(self, name, bases, namespace): + super().__init__(name, bases, namespace) + + # Avoid recursion. + # (See test_creating_pointer_in_dunder_new_2) + if isinstance(self, p_meta): + return + p = p_meta(f"POINTER({self.__name__})", (self, c_void_p), {}) + ctypes._pointer_type_cache[self] = p + + class p_meta(PyCSimpleType, ct_meta): + pass + + class Core(object): + pass + + class CtBase(Core, metaclass=ct_meta): + pass + + class Sub(CtBase): + pass + + self.assertIsInstance(POINTER(Sub), p_meta) + self.assertTrue(issubclass(POINTER(Sub), Sub)) diff --git a/Lib/ctypes/test/test_callbacks.py b/Lib/test/test_ctypes/test_callbacks.py similarity index 82% rename from Lib/ctypes/test/test_callbacks.py rename to Lib/test/test_ctypes/test_callbacks.py index 8f95a24443..8f483dfe1d 100644 --- a/Lib/ctypes/test/test_callbacks.py +++ b/Lib/test/test_ctypes/test_callbacks.py @@ -1,19 +1,25 @@ +import ctypes import functools +import gc +import math +import sys import unittest +from _ctypes import CTYPES_MAX_ARGCOUNT +from ctypes import (CDLL, cdll, Structure, CFUNCTYPE, + ArgumentError, POINTER, sizeof, + c_byte, c_ubyte, c_char, + c_short, c_ushort, c_int, c_uint, + c_long, c_longlong, c_ulonglong, c_ulong, + c_float, c_double, c_longdouble, py_object) +from ctypes.util import find_library from test import support +from test.support import import_helper +_ctypes_test = import_helper.import_module("_ctypes_test") -from ctypes import * -from ctypes.test import need_symbol -from _ctypes import CTYPES_MAX_ARGCOUNT -import _ctypes_test class Callbacks(unittest.TestCase): functype = CFUNCTYPE -## def tearDown(self): -## import gc -## gc.collect() - def callback(self, *args): self.got_args = args return args[-1] @@ -35,8 +41,6 @@ def check_type(self, typ, arg): self.assertEqual(self.got_args, (-3, arg)) self.assertEqual(result, arg) - ################ - def test_byte(self): self.check_type(c_byte, 42) self.check_type(c_byte, -42) @@ -65,18 +69,15 @@ def test_long(self): def test_ulong(self): self.check_type(c_ulong, 42) - @need_symbol('c_longlong') def test_longlong(self): self.check_type(c_longlong, 42) self.check_type(c_longlong, -42) - @need_symbol('c_ulonglong') def test_ulonglong(self): self.check_type(c_ulonglong, 42) def test_float(self): # only almost equal: double -> float -> double - import math self.check_type(c_float, math.e) self.check_type(c_float, -math.e) @@ -84,7 +85,6 @@ def test_double(self): self.check_type(c_double, 3.14) self.check_type(c_double, -3.14) - @need_symbol('c_longdouble') def test_longdouble(self): self.check_type(c_longdouble, 3.14) self.check_type(c_longdouble, -3.14) @@ -93,30 +93,21 @@ def test_char(self): self.check_type(c_char, b"x") self.check_type(c_char, b"a") - # disabled: would now (correctly) raise a RuntimeWarning about - # a memory leak. A callback function cannot return a non-integral - # C type without causing a memory leak. - @unittest.skip('test disabled') - def test_char_p(self): - self.check_type(c_char_p, "abc") - self.check_type(c_char_p, "def") - def test_pyobject(self): o = () - from sys import getrefcount as grc for o in (), [], object(): - initial = grc(o) + initial = sys.getrefcount(o) # This call leaks a reference to 'o'... self.check_type(py_object, o) - before = grc(o) + before = sys.getrefcount(o) # ...but this call doesn't leak any more. Where is the refcount? self.check_type(py_object, o) - after = grc(o) + after = sys.getrefcount(o) self.assertEqual((after, o), (before, o)) def test_unsupported_restype_1(self): # Only "fundamental" result types are supported for callback - # functions, the type must have a non-NULL stgdict->setfunc. + # functions, the type must have a non-NULL stginfo->setfunc. # POINTER(c_double), for example, is not supported. prototype = self.functype.__func__(POINTER(c_double)) @@ -130,12 +121,11 @@ def test_unsupported_restype_2(self): def test_issue_7959(self): proto = self.functype.__func__(None) - class X(object): + class X: def func(self): pass def __init__(self): self.v = proto(self.func) - import gc for i in range(32): X() gc.collect() @@ -144,21 +134,30 @@ def __init__(self): self.assertEqual(len(live), 0) def test_issue12483(self): - import gc class Nasty: def __del__(self): gc.collect() CFUNCTYPE(None)(lambda x=Nasty(): None) + @unittest.skipUnless(hasattr(ctypes, 'WINFUNCTYPE'), + 'ctypes.WINFUNCTYPE is required') + def test_i38748_stackCorruption(self): + callback_funcType = ctypes.WINFUNCTYPE(c_long, c_long, c_longlong) + @callback_funcType + def callback(a, b): + c = a + b + print(f"a={a}, b={b}, c={c}") + return c + dll = cdll[_ctypes_test.__file__] + with support.captured_stdout() as out: + # With no fix for i38748, the next line will raise OSError and cause the test to fail. + self.assertEqual(dll._test_i38748_runCallback(callback, 5, 10), 15) + self.assertEqual(out.getvalue(), "a=5, b=10, c=15\n") + +if hasattr(ctypes, 'WINFUNCTYPE'): + class StdcallCallbacks(Callbacks): + functype = ctypes.WINFUNCTYPE -@need_symbol('WINFUNCTYPE') -class StdcallCallbacks(Callbacks): - try: - functype = WINFUNCTYPE - except NameError: - pass - -################################################################ class SampleCallbacksTestCase(unittest.TestCase): @@ -183,7 +182,6 @@ def func(x): self.assertLess(diff, 0.01, "%s not less than 0.01" % diff) def test_issue_8959_a(self): - from ctypes.util import find_library libc_path = find_library("c") if not libc_path: self.skipTest('could not find libc') @@ -198,19 +196,21 @@ def cmp_func(a, b): libc.qsort(array, len(array), sizeof(c_int), cmp_func) self.assertEqual(array[:], [1, 5, 7, 33, 99]) - @need_symbol('WINFUNCTYPE') + @unittest.skipUnless(hasattr(ctypes, 'WINFUNCTYPE'), + 'ctypes.WINFUNCTYPE is required') def test_issue_8959_b(self): from ctypes.wintypes import BOOL, HWND, LPARAM global windowCount windowCount = 0 - @WINFUNCTYPE(BOOL, HWND, LPARAM) + @ctypes.WINFUNCTYPE(BOOL, HWND, LPARAM) def EnumWindowsCallbackFunc(hwnd, lParam): global windowCount windowCount += 1 return True #Allow windows to keep enumerating - windll.user32.EnumWindows(EnumWindowsCallbackFunc, 0) + user32 = ctypes.windll.user32 + user32.EnumWindows(EnumWindowsCallbackFunc, 0) def test_callback_register_int(self): # Issue #8275: buggy handling of callback args under Win64 @@ -324,9 +324,9 @@ def func(): self.assertIsInstance(cm.unraisable.exc_value, TypeError) self.assertEqual(cm.unraisable.err_msg, - "Exception ignored on converting result " - "of ctypes callback function") - self.assertIs(cm.unraisable.object, func) + f"Exception ignored on converting result " + f"of ctypes callback function {func!r}") + self.assertIsNone(cm.unraisable.object) if __name__ == '__main__': diff --git a/Lib/ctypes/test/test_cast.py b/Lib/test/test_ctypes/test_cast.py similarity index 92% rename from Lib/ctypes/test/test_cast.py rename to Lib/test/test_ctypes/test_cast.py index 6878f97328..db6bdc75ef 100644 --- a/Lib/ctypes/test/test_cast.py +++ b/Lib/test/test_ctypes/test_cast.py @@ -1,10 +1,11 @@ -from ctypes import * -from ctypes.test import need_symbol -import unittest import sys +import unittest +from ctypes import (Structure, Union, POINTER, cast, sizeof, addressof, + c_void_p, c_char_p, c_wchar_p, + c_byte, c_short, c_int) -class Test(unittest.TestCase): +class Test(unittest.TestCase): def test_array2pointer(self): array = (c_int * 3)(42, 17, 2) @@ -12,7 +13,7 @@ def test_array2pointer(self): ptr = cast(array, POINTER(c_int)) self.assertEqual([ptr[i] for i in range(3)], [42, 17, 2]) - if 2*sizeof(c_short) == sizeof(c_int): + if 2 * sizeof(c_short) == sizeof(c_int): ptr = cast(array, POINTER(c_short)) if sys.byteorder == "little": self.assertEqual([ptr[i] for i in range(6)], @@ -31,6 +32,8 @@ def test_address2pointer(self): ptr = cast(address, POINTER(c_int)) self.assertEqual([ptr[i] for i in range(3)], [42, 17, 2]) + # TODO: RUSTPYTHON + @unittest.expectedFailure def test_p2a_objects(self): array = (c_char_p * 5)() self.assertEqual(array._objects, None) @@ -76,11 +79,10 @@ def test_char_p(self): self.assertEqual(cast(cast(s, c_void_p), c_char_p).value, b"hiho") - @need_symbol('c_wchar_p') def test_wchar_p(self): s = c_wchar_p("hiho") self.assertEqual(cast(cast(s, c_void_p), c_wchar_p).value, - "hiho") + "hiho") def test_bad_type_arg(self): # The type argument must be a ctypes pointer type. @@ -95,5 +97,6 @@ class MyUnion(Union): _fields_ = [("a", c_int)] self.assertRaises(TypeError, cast, array, MyUnion) + if __name__ == "__main__": unittest.main() diff --git a/Lib/ctypes/test/test_cfuncs.py b/Lib/test/test_ctypes/test_cfuncs.py similarity index 93% rename from Lib/ctypes/test/test_cfuncs.py rename to Lib/test/test_ctypes/test_cfuncs.py index 09b06840bf..48330c4b0a 100644 --- a/Lib/ctypes/test/test_cfuncs.py +++ b/Lib/test/test_ctypes/test_cfuncs.py @@ -1,11 +1,13 @@ -# A lot of failures in these tests on Mac OS X. -# Byte order related? - +import ctypes import unittest -from ctypes import * -from ctypes.test import need_symbol +from ctypes import (CDLL, + c_byte, c_ubyte, c_char, + c_short, c_ushort, c_int, c_uint, + c_long, c_ulong, c_longlong, c_ulonglong, + c_float, c_double, c_longdouble) +from test.support import import_helper +_ctypes_test = import_helper.import_module("_ctypes_test") -import _ctypes_test class CFunctions(unittest.TestCase): _dll = CDLL(_ctypes_test.__file__) @@ -111,28 +113,24 @@ def test_ulong_plus(self): self.assertEqual(self._dll.tf_bL(b' ', 4294967295), 1431655765) self.assertEqual(self.U(), 4294967295) - @need_symbol('c_longlong') def test_longlong(self): self._dll.tf_q.restype = c_longlong self._dll.tf_q.argtypes = (c_longlong, ) self.assertEqual(self._dll.tf_q(-9223372036854775806), -3074457345618258602) self.assertEqual(self.S(), -9223372036854775806) - @need_symbol('c_longlong') def test_longlong_plus(self): self._dll.tf_bq.restype = c_longlong self._dll.tf_bq.argtypes = (c_byte, c_longlong) self.assertEqual(self._dll.tf_bq(0, -9223372036854775806), -3074457345618258602) self.assertEqual(self.S(), -9223372036854775806) - @need_symbol('c_ulonglong') def test_ulonglong(self): self._dll.tf_Q.restype = c_ulonglong self._dll.tf_Q.argtypes = (c_ulonglong, ) self.assertEqual(self._dll.tf_Q(18446744073709551615), 6148914691236517205) self.assertEqual(self.U(), 18446744073709551615) - @need_symbol('c_ulonglong') def test_ulonglong_plus(self): self._dll.tf_bQ.restype = c_ulonglong self._dll.tf_bQ.argtypes = (c_byte, c_ulonglong) @@ -163,14 +161,12 @@ def test_double_plus(self): self.assertEqual(self._dll.tf_bd(0, 42.), 14.) self.assertEqual(self.S(), 42) - @need_symbol('c_longdouble') def test_longdouble(self): self._dll.tf_D.restype = c_longdouble self._dll.tf_D.argtypes = (c_longdouble,) self.assertEqual(self._dll.tf_D(42.), 14.) self.assertEqual(self.S(), 42) - @need_symbol('c_longdouble') def test_longdouble_plus(self): self._dll.tf_bD.restype = c_longdouble self._dll.tf_bD.argtypes = (c_byte, c_longdouble) @@ -195,14 +191,11 @@ def test_void(self): self.assertEqual(self._dll.tv_i(-42), None) self.assertEqual(self.S(), -42) + # The following repeats the above tests with stdcall functions (where # they are available) -try: - WinDLL -except NameError: - def stdcall_dll(*_): pass -else: - class stdcall_dll(WinDLL): +if hasattr(ctypes, 'WinDLL'): + class stdcall_dll(ctypes.WinDLL): def __getattr__(self, name): if name[:2] == '__' and name[-2:] == '__': raise AttributeError(name) @@ -210,9 +203,9 @@ def __getattr__(self, name): setattr(self, name, func) return func -@need_symbol('WinDLL') -class stdcallCFunctions(CFunctions): - _dll = stdcall_dll(_ctypes_test.__file__) + class stdcallCFunctions(CFunctions): + _dll = stdcall_dll(_ctypes_test.__file__) + if __name__ == '__main__': unittest.main() diff --git a/Lib/ctypes/test/test_checkretval.py b/Lib/test/test_ctypes/test_checkretval.py similarity index 66% rename from Lib/ctypes/test/test_checkretval.py rename to Lib/test/test_ctypes/test_checkretval.py index e9567dc391..9d6bfdb845 100644 --- a/Lib/ctypes/test/test_checkretval.py +++ b/Lib/test/test_ctypes/test_checkretval.py @@ -1,7 +1,9 @@ +import ctypes import unittest +from ctypes import CDLL, c_int +from test.support import import_helper +_ctypes_test = import_helper.import_module("_ctypes_test") -from ctypes import * -from ctypes.test import need_symbol class CHECKED(c_int): def _check_retval_(value): @@ -9,11 +11,9 @@ def _check_retval_(value): return str(value.value) _check_retval_ = staticmethod(_check_retval_) -class Test(unittest.TestCase): +class Test(unittest.TestCase): def test_checkretval(self): - - import _ctypes_test dll = CDLL(_ctypes_test.__file__) self.assertEqual(42, dll._testfunc_p_p(42)) @@ -26,11 +26,12 @@ def test_checkretval(self): del dll._testfunc_p_p.restype self.assertEqual(42, dll._testfunc_p_p(42)) - @need_symbol('oledll') + @unittest.skipUnless(hasattr(ctypes, 'oledll'), + 'ctypes.oledll is required') def test_oledll(self): - self.assertRaises(OSError, - oledll.oleaut32.CreateTypeLib2, - 0, None, None) + oleaut32 = ctypes.oledll.oleaut32 + self.assertRaises(OSError, oleaut32.CreateTypeLib2, 0, None, None) + if __name__ == "__main__": unittest.main() diff --git a/Lib/test/test_ctypes/test_delattr.py b/Lib/test/test_ctypes/test_delattr.py new file mode 100644 index 0000000000..e80b5fa6ef --- /dev/null +++ b/Lib/test/test_ctypes/test_delattr.py @@ -0,0 +1,26 @@ +import unittest +from ctypes import Structure, c_char, c_int + + +class X(Structure): + _fields_ = [("foo", c_int)] + + +class TestCase(unittest.TestCase): + def test_simple(self): + with self.assertRaises(TypeError): + del c_int(42).value + + def test_chararray(self): + chararray = (c_char * 5)() + with self.assertRaises(TypeError): + del chararray.value + + def test_struct(self): + struct = X() + with self.assertRaises(TypeError): + del struct.foo + + +if __name__ == "__main__": + unittest.main() diff --git a/Lib/test/test_ctypes/test_dlerror.py b/Lib/test/test_ctypes/test_dlerror.py new file mode 100644 index 0000000000..cd87bad382 --- /dev/null +++ b/Lib/test/test_ctypes/test_dlerror.py @@ -0,0 +1,180 @@ +import _ctypes +import os +import platform +import sys +import test.support +import unittest +from ctypes import CDLL, c_int +from ctypes.util import find_library + + +FOO_C = r""" +#include + +/* This is a 'GNU indirect function' (IFUNC) that will be called by + dlsym() to resolve the symbol "foo" to an address. Typically, such + a function would return the address of an actual function, but it + can also just return NULL. For some background on IFUNCs, see + https://willnewton.name/uncategorized/using-gnu-indirect-functions. + + Adapted from Michael Kerrisk's answer: https://stackoverflow.com/a/53590014. +*/ + +asm (".type foo STT_GNU_IFUNC"); + +void *foo(void) +{ + write($DESCRIPTOR, "OK", 2); + return NULL; +} +""" + + +@unittest.skipUnless(sys.platform.startswith('linux'), + 'test requires GNU IFUNC support') +class TestNullDlsym(unittest.TestCase): + """GH-126554: Ensure that we catch NULL dlsym return values + + In rare cases, such as when using GNU IFUNCs, dlsym(), + the C function that ctypes' CDLL uses to get the address + of symbols, can return NULL. + + The objective way of telling if an error during symbol + lookup happened is to call glibc's dlerror() and check + for a non-NULL return value. + + However, there can be cases where dlsym() returns NULL + and dlerror() is also NULL, meaning that glibc did not + encounter any error. + + In the case of ctypes, we subjectively treat that as + an error, and throw a relevant exception. + + This test case ensures that we correctly enforce + this 'dlsym returned NULL -> throw Error' rule. + """ + + @unittest.expectedFailure # TODO: RUSTPYTHON + def test_null_dlsym(self): + import subprocess + import tempfile + + try: + retcode = subprocess.call(["gcc", "--version"], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL) + except OSError: + self.skipTest("gcc is missing") + if retcode != 0: + self.skipTest("gcc --version failed") + + pipe_r, pipe_w = os.pipe() + self.addCleanup(os.close, pipe_r) + self.addCleanup(os.close, pipe_w) + + with tempfile.TemporaryDirectory() as d: + # Create a C file with a GNU Indirect Function (FOO_C) + # and compile it into a shared library. + srcname = os.path.join(d, 'foo.c') + dstname = os.path.join(d, 'libfoo.so') + with open(srcname, 'w') as f: + f.write(FOO_C.replace('$DESCRIPTOR', str(pipe_w))) + args = ['gcc', '-fPIC', '-shared', '-o', dstname, srcname] + p = subprocess.run(args, capture_output=True) + + if p.returncode != 0: + # IFUNC is not supported on all architectures. + if platform.machine() == 'x86_64': + # It should be supported here. Something else went wrong. + p.check_returncode() + else: + # IFUNC might not be supported on this machine. + self.skipTest(f"could not compile indirect function: {p}") + + # Case #1: Test 'PyCFuncPtr_FromDll' from Modules/_ctypes/_ctypes.c + L = CDLL(dstname) + with self.assertRaisesRegex(AttributeError, "function 'foo' not found"): + # Try accessing the 'foo' symbol. + # It should resolve via dlsym() to NULL, + # and since we subjectively treat NULL + # addresses as errors, we should get + # an error. + L.foo + + # Assert that the IFUNC was called + self.assertEqual(os.read(pipe_r, 2), b'OK') + + # Case #2: Test 'CDataType_in_dll_impl' from Modules/_ctypes/_ctypes.c + with self.assertRaisesRegex(ValueError, "symbol 'foo' not found"): + c_int.in_dll(L, "foo") + + # Assert that the IFUNC was called + self.assertEqual(os.read(pipe_r, 2), b'OK') + + # Case #3: Test 'py_dl_sym' from Modules/_ctypes/callproc.c + dlopen = test.support.get_attribute(_ctypes, 'dlopen') + dlsym = test.support.get_attribute(_ctypes, 'dlsym') + L = dlopen(dstname) + with self.assertRaisesRegex(OSError, "symbol 'foo' not found"): + dlsym(L, "foo") + + # Assert that the IFUNC was called + self.assertEqual(os.read(pipe_r, 2), b'OK') + + +@unittest.skipUnless(os.name != 'nt', 'test requires dlerror() calls') +class TestLocalization(unittest.TestCase): + + @staticmethod + def configure_locales(func): + return test.support.run_with_locale( + 'LC_ALL', + 'fr_FR.iso88591', 'ja_JP.sjis', 'zh_CN.gbk', + 'fr_FR.utf8', 'en_US.utf8', + '', + )(func) + + @classmethod + def setUpClass(cls): + cls.libc_filename = find_library("c") + if cls.libc_filename is None: + raise unittest.SkipTest('cannot find libc') + + @configure_locales + def test_localized_error_from_dll(self): + dll = CDLL(self.libc_filename) + with self.assertRaises(AttributeError): + dll.this_name_does_not_exist + + @configure_locales + def test_localized_error_in_dll(self): + dll = CDLL(self.libc_filename) + with self.assertRaises(ValueError): + c_int.in_dll(dll, 'this_name_does_not_exist') + + @unittest.skipUnless(hasattr(_ctypes, 'dlopen'), + 'test requires _ctypes.dlopen()') + @configure_locales + def test_localized_error_dlopen(self): + missing_filename = b'missing\xff.so' + # Depending whether the locale, we may encode '\xff' differently + # but we are only interested in avoiding a UnicodeDecodeError + # when reporting the dlerror() error message which contains + # the localized filename. + filename_pattern = r'missing.*?\.so' + with self.assertRaisesRegex(OSError, filename_pattern): + _ctypes.dlopen(missing_filename, 2) + + @unittest.skipUnless(hasattr(_ctypes, 'dlopen'), + 'test requires _ctypes.dlopen()') + @unittest.skipUnless(hasattr(_ctypes, 'dlsym'), + 'test requires _ctypes.dlsym()') + @configure_locales + def test_localized_error_dlsym(self): + dll = _ctypes.dlopen(self.libc_filename) + with self.assertRaises(OSError): + _ctypes.dlsym(dll, 'this_name_does_not_exist') + + +if __name__ == "__main__": + unittest.main() diff --git a/Lib/ctypes/test/test_errno.py b/Lib/test/test_ctypes/test_errno.py similarity index 71% rename from Lib/ctypes/test/test_errno.py rename to Lib/test/test_ctypes/test_errno.py index 3685164dde..65d99c1e49 100644 --- a/Lib/ctypes/test/test_errno.py +++ b/Lib/test/test_ctypes/test_errno.py @@ -1,14 +1,18 @@ -import unittest, os, errno +import ctypes +import errno +import os import threading - -from ctypes import * +import unittest +from ctypes import CDLL, c_int, c_char_p, c_wchar_p, get_errno, set_errno from ctypes.util import find_library + class Test(unittest.TestCase): def test_open(self): libc_name = find_library("c") if libc_name is None: - raise unittest.SkipTest("Unable to find C library") + self.skipTest("Unable to find C library") + libc = CDLL(libc_name, use_errno=True) if os.name == "nt": libc_open = libc._open @@ -44,33 +48,34 @@ def _worker(): @unittest.skipUnless(os.name == "nt", 'Test specific to Windows') def test_GetLastError(self): - dll = WinDLL("kernel32", use_last_error=True) + dll = ctypes.WinDLL("kernel32", use_last_error=True) GetModuleHandle = dll.GetModuleHandleA GetModuleHandle.argtypes = [c_wchar_p] self.assertEqual(0, GetModuleHandle("foo")) - self.assertEqual(get_last_error(), 126) + self.assertEqual(ctypes.get_last_error(), 126) - self.assertEqual(set_last_error(32), 126) - self.assertEqual(get_last_error(), 32) + self.assertEqual(ctypes.set_last_error(32), 126) + self.assertEqual(ctypes.get_last_error(), 32) def _worker(): - set_last_error(0) + ctypes.set_last_error(0) - dll = WinDLL("kernel32", use_last_error=False) + dll = ctypes.WinDLL("kernel32", use_last_error=False) GetModuleHandle = dll.GetModuleHandleW GetModuleHandle.argtypes = [c_wchar_p] GetModuleHandle("bar") - self.assertEqual(get_last_error(), 0) + self.assertEqual(ctypes.get_last_error(), 0) t = threading.Thread(target=_worker) t.start() t.join() - self.assertEqual(get_last_error(), 32) + self.assertEqual(ctypes.get_last_error(), 32) + + ctypes.set_last_error(0) - set_last_error(0) if __name__ == "__main__": unittest.main() diff --git a/Lib/ctypes/test/test_find.py b/Lib/test/test_ctypes/test_find.py similarity index 80% rename from Lib/ctypes/test/test_find.py rename to Lib/test/test_ctypes/test_find.py index 1ff9d019b1..85b28617d2 100644 --- a/Lib/ctypes/test/test_find.py +++ b/Lib/test/test_ctypes/test_find.py @@ -1,11 +1,12 @@ -import unittest -import unittest.mock import os.path import sys import test.support -from test.support import os_helper -from ctypes import * +import unittest +import unittest.mock +from ctypes import CDLL, RTLD_GLOBAL from ctypes.util import find_library +from test.support import os_helper + # On some systems, loading the OpenGL libraries needs the RTLD_GLOBAL mode. class Test_OpenGL_libs(unittest.TestCase): @@ -22,7 +23,7 @@ def setUpClass(cls): lib_glu = find_library("GLU") lib_gle = find_library("gle") - ## print, for debugging + # print, for debugging if test.support.verbose: print("OpenGL libraries:") for item in (("GL", lib_gl), @@ -36,11 +37,13 @@ def setUpClass(cls): cls.gl = CDLL(lib_gl, mode=RTLD_GLOBAL) except OSError: pass + if lib_glu: try: cls.glu = CDLL(lib_glu, RTLD_GLOBAL) except OSError: pass + if lib_gle: try: cls.gle = CDLL(lib_gle) @@ -122,6 +125,32 @@ def test_find_library_with_ld(self): unittest.mock.patch("ctypes.util._findLib_gcc", lambda *args: None): self.assertNotEqual(find_library('c'), None) + def test_gh114257(self): + self.assertIsNone(find_library("libc")) + + +@unittest.skipUnless(sys.platform == 'android', 'Test only valid for Android') +class FindLibraryAndroid(unittest.TestCase): + def test_find(self): + for name in [ + "c", "m", # POSIX + "z", # Non-POSIX, but present on Linux + "log", # Not present on Linux + ]: + with self.subTest(name=name): + path = find_library(name) + self.assertIsInstance(path, str) + self.assertEqual( + os.path.dirname(path), + "/system/lib64" if "64" in os.uname().machine + else "/system/lib") + self.assertEqual(os.path.basename(path), f"lib{name}.so") + self.assertTrue(os.path.isfile(path), path) + + for name in ["libc", "nonexistent"]: + with self.subTest(name=name): + self.assertIsNone(find_library(name)) + if __name__ == "__main__": unittest.main() diff --git a/Lib/ctypes/test/test_frombuffer.py b/Lib/test/test_ctypes/test_frombuffer.py similarity index 97% rename from Lib/ctypes/test/test_frombuffer.py rename to Lib/test/test_ctypes/test_frombuffer.py index 55c244356b..d4e161f864 100644 --- a/Lib/ctypes/test/test_frombuffer.py +++ b/Lib/test/test_ctypes/test_frombuffer.py @@ -1,7 +1,10 @@ -from ctypes import * import array import gc import unittest +from ctypes import (Structure, Union, Array, sizeof, + _Pointer, _SimpleCData, _CFuncPtr, + c_char, c_int) + class X(Structure): _fields_ = [("c_int", c_int)] @@ -9,6 +12,7 @@ class X(Structure): def __init__(self): self._init_called = True + class Test(unittest.TestCase): def test_from_buffer(self): a = array.array("i", range(16)) @@ -121,8 +125,6 @@ def test_from_buffer_copy_with_offset(self): (c_int * 1).from_buffer_copy(a, 16 * sizeof(c_int)) def test_abstract(self): - from ctypes import _Pointer, _SimpleCData, _CFuncPtr - self.assertRaises(TypeError, Array.from_buffer, bytearray(10)) self.assertRaises(TypeError, Structure.from_buffer, bytearray(10)) self.assertRaises(TypeError, Union.from_buffer, bytearray(10)) @@ -137,5 +139,6 @@ def test_abstract(self): self.assertRaises(TypeError, _Pointer.from_buffer_copy, b"123") self.assertRaises(TypeError, _SimpleCData.from_buffer_copy, b"123") + if __name__ == '__main__': unittest.main() diff --git a/Lib/ctypes/test/test_funcptr.py b/Lib/test/test_ctypes/test_funcptr.py similarity index 68% rename from Lib/ctypes/test/test_funcptr.py rename to Lib/test/test_ctypes/test_funcptr.py index e0b9b54e97..8362fb16d9 100644 --- a/Lib/ctypes/test/test_funcptr.py +++ b/Lib/test/test_ctypes/test_funcptr.py @@ -1,16 +1,41 @@ +import ctypes import unittest -from ctypes import * +from ctypes import (CDLL, Structure, CFUNCTYPE, sizeof, _CFuncPtr, + c_void_p, c_char_p, c_char, c_int, c_uint, c_long) +from test.support import import_helper +_ctypes_test = import_helper.import_module("_ctypes_test") +from ._support import (_CData, PyCFuncPtrType, Py_TPFLAGS_DISALLOW_INSTANTIATION, + Py_TPFLAGS_IMMUTABLETYPE) + try: - WINFUNCTYPE -except NameError: + WINFUNCTYPE = ctypes.WINFUNCTYPE +except AttributeError: # fake to enable this test on Linux WINFUNCTYPE = CFUNCTYPE -import _ctypes_test lib = CDLL(_ctypes_test.__file__) + class CFuncPtrTestCase(unittest.TestCase): + def test_inheritance_hierarchy(self): + self.assertEqual(_CFuncPtr.mro(), [_CFuncPtr, _CData, object]) + + self.assertEqual(PyCFuncPtrType.__name__, "PyCFuncPtrType") + self.assertEqual(type(PyCFuncPtrType), type) + + def test_type_flags(self): + for cls in _CFuncPtr, PyCFuncPtrType: + with self.subTest(cls=cls): + self.assertTrue(_CFuncPtr.__flags__ & Py_TPFLAGS_IMMUTABLETYPE) + self.assertFalse(_CFuncPtr.__flags__ & Py_TPFLAGS_DISALLOW_INSTANTIATION) + + def test_metaclass_details(self): + # Cannot call the metaclass __init__ more than once + CdeclCallback = CFUNCTYPE(c_int, c_int, c_int) + with self.assertRaisesRegex(SystemError, "already initialized"): + PyCFuncPtrType.__init__(CdeclCallback, 'ptr', (), {}) + def test_basic(self): X = WINFUNCTYPE(c_int, c_int, c_int) @@ -20,8 +45,8 @@ def func(*args): x = X(func) self.assertEqual(x.restype, c_int) self.assertEqual(x.argtypes, (c_int, c_int)) - self.assertEqual(sizeof(x), sizeof(c_voidp)) - self.assertEqual(sizeof(X), sizeof(c_voidp)) + self.assertEqual(sizeof(x), sizeof(c_void_p)) + self.assertEqual(sizeof(X), sizeof(c_void_p)) def test_first(self): StdCallback = WINFUNCTYPE(c_int, c_int, c_int) @@ -39,7 +64,7 @@ def func(a, b): # possible, as in C, to call cdecl functions with more parameters. #self.assertRaises(TypeError, c, 1, 2, 3) self.assertEqual(c(1, 2, 3, 4, 5, 6), 3) - if not WINFUNCTYPE is CFUNCTYPE: + if WINFUNCTYPE is not CFUNCTYPE: self.assertRaises(TypeError, s, 1, 2, 3) def test_structures(self): @@ -69,17 +94,9 @@ class WNDCLASS(Structure): WNDPROC_2 = WINFUNCTYPE(c_long, c_int, c_int, c_int, c_int) - # This is no longer true, now that WINFUNCTYPE caches created types internally. - ## # CFuncPtr subclasses are compared by identity, so this raises a TypeError: - ## self.assertRaises(TypeError, setattr, wndclass, - ## "lpfnWndProc", WNDPROC_2(wndproc)) - # instead: - self.assertIs(WNDPROC, WNDPROC_2) - # 'wndclass.lpfnWndProc' leaks 94 references. Why? self.assertEqual(wndclass.lpfnWndProc(1, 2, 3, 4), 10) - f = wndclass.lpfnWndProc del wndclass @@ -88,24 +105,14 @@ class WNDCLASS(Structure): self.assertEqual(f(10, 11, 12, 13), 46) def test_dllfunctions(self): - - def NoNullHandle(value): - if not value: - raise WinError() - return value - strchr = lib.my_strchr strchr.restype = c_char_p strchr.argtypes = (c_char_p, c_char) self.assertEqual(strchr(b"abcdefghi", b"b"), b"bcdefghi") self.assertEqual(strchr(b"abcdefghi", b"x"), None) - strtok = lib.my_strtok strtok.restype = c_char_p - # Neither of this does work: strtok changes the buffer it is passed -## strtok.argtypes = (c_char_p, c_char_p) -## strtok.argtypes = (c_string, c_char_p) def c_string(init): size = len(init) + 1 @@ -114,19 +121,14 @@ def c_string(init): s = b"a\nb\nc" b = c_string(s) -## b = (c_char * (len(s)+1))() -## b.value = s - -## b = c_string(s) self.assertEqual(strtok(b, b"\n"), b"a") self.assertEqual(strtok(None, b"\n"), b"b") self.assertEqual(strtok(None, b"\n"), b"c") self.assertEqual(strtok(None, b"\n"), None) def test_abstract(self): - from ctypes import _CFuncPtr - self.assertRaises(TypeError, _CFuncPtr, 13, "name", 42, "iid") + if __name__ == '__main__': unittest.main() diff --git a/Lib/ctypes/test/test_functions.py b/Lib/test/test_ctypes/test_functions.py similarity index 76% rename from Lib/ctypes/test/test_functions.py rename to Lib/test/test_ctypes/test_functions.py index fc571700ce..63e393f7b7 100644 --- a/Lib/ctypes/test/test_functions.py +++ b/Lib/test/test_ctypes/test_functions.py @@ -1,30 +1,36 @@ -""" -Here is probably the place to write the docs, since the test-cases -show how the type behave. +import ctypes +import sys +import unittest +from ctypes import (CDLL, Structure, Array, CFUNCTYPE, + byref, POINTER, pointer, ArgumentError, + c_char, c_wchar, c_byte, c_char_p, c_wchar_p, + c_short, c_int, c_long, c_longlong, c_void_p, + c_float, c_double, c_longdouble) +from test.support import import_helper +_ctypes_test = import_helper.import_module("_ctypes_test") +from _ctypes import _Pointer, _SimpleCData -Later... -""" - -from ctypes import * -from ctypes.test import need_symbol -import sys, unittest try: - WINFUNCTYPE -except NameError: + WINFUNCTYPE = ctypes.WINFUNCTYPE +except AttributeError: # fake to enable this test on Linux WINFUNCTYPE = CFUNCTYPE -import _ctypes_test dll = CDLL(_ctypes_test.__file__) if sys.platform == "win32": - windll = WinDLL(_ctypes_test.__file__) + windll = ctypes.WinDLL(_ctypes_test.__file__) + class POINT(Structure): _fields_ = [("x", c_int), ("y", c_int)] + + class RECT(Structure): _fields_ = [("left", c_int), ("top", c_int), ("right", c_int), ("bottom", c_int)] + + class FunctionTestCase(unittest.TestCase): def test_mro(self): @@ -40,21 +46,34 @@ class X(object, Array): _length_ = 5 _type_ = "i" - from _ctypes import _Pointer with self.assertRaises(TypeError): - class X(object, _Pointer): + class X2(object, _Pointer): pass - from _ctypes import _SimpleCData with self.assertRaises(TypeError): - class X(object, _SimpleCData): + class X3(object, _SimpleCData): _type_ = "i" with self.assertRaises(TypeError): - class X(object, Structure): + class X4(object, Structure): _fields_ = [] - @need_symbol('c_wchar') + def test_c_char_parm(self): + proto = CFUNCTYPE(c_int, c_char) + def callback(*args): + return 0 + + callback = proto(callback) + + self.assertEqual(callback(b"a"), 0) + + with self.assertRaises(ArgumentError) as cm: + callback(b"abc") + + self.assertEqual(str(cm.exception), + "argument 1: TypeError: one character bytes, " + "bytearray or integer expected") + def test_wchar_parm(self): f = dll._testfunc_i_bhilfd f.argtypes = [c_byte, c_wchar, c_int, c_long, c_float, c_double] @@ -62,7 +81,66 @@ def test_wchar_parm(self): self.assertEqual(result, 139) self.assertEqual(type(result), int) - @need_symbol('c_wchar') + with self.assertRaises(ArgumentError) as cm: + f(1, 2, 3, 4, 5.0, 6.0) + self.assertEqual(str(cm.exception), + "argument 2: TypeError: unicode string expected " + "instead of int instance") + + with self.assertRaises(ArgumentError) as cm: + f(1, "abc", 3, 4, 5.0, 6.0) + self.assertEqual(str(cm.exception), + "argument 2: TypeError: one character unicode string " + "expected") + + def test_c_char_p_parm(self): + """Test the error message when converting an incompatible type to c_char_p.""" + proto = CFUNCTYPE(c_int, c_char_p) + def callback(*args): + return 0 + + callback = proto(callback) + self.assertEqual(callback(b"abc"), 0) + + with self.assertRaises(ArgumentError) as cm: + callback(10) + + self.assertEqual(str(cm.exception), + "argument 1: TypeError: 'int' object cannot be " + "interpreted as ctypes.c_char_p") + + def test_c_wchar_p_parm(self): + """Test the error message when converting an incompatible type to c_wchar_p.""" + proto = CFUNCTYPE(c_int, c_wchar_p) + def callback(*args): + return 0 + + callback = proto(callback) + self.assertEqual(callback("abc"), 0) + + with self.assertRaises(ArgumentError) as cm: + callback(10) + + self.assertEqual(str(cm.exception), + "argument 1: TypeError: 'int' object cannot be " + "interpreted as ctypes.c_wchar_p") + + def test_c_void_p_parm(self): + """Test the error message when converting an incompatible type to c_void_p.""" + proto = CFUNCTYPE(c_int, c_void_p) + def callback(*args): + return 0 + + callback = proto(callback) + self.assertEqual(callback(5), 0) + + with self.assertRaises(ArgumentError) as cm: + callback(2.5) + + self.assertEqual(str(cm.exception), + "argument 1: TypeError: 'float' object cannot be " + "interpreted as ctypes.c_void_p") + def test_wchar_result(self): f = dll._testfunc_i_bhilfd f.argtypes = [c_byte, c_short, c_int, c_long, c_float, c_double] @@ -128,7 +206,6 @@ def test_doubleresult(self): self.assertEqual(result, -21) self.assertEqual(type(result), float) - @need_symbol('c_longdouble') def test_longdoubleresult(self): f = dll._testfunc_D_bhilfD f.argtypes = [c_byte, c_short, c_int, c_long, c_float, c_longdouble] @@ -141,7 +218,6 @@ def test_longdoubleresult(self): self.assertEqual(result, -21) self.assertEqual(type(result), float) - @need_symbol('c_longlong') def test_longlongresult(self): f = dll._testfunc_q_bhilfd f.restype = c_longlong @@ -200,7 +276,6 @@ def test_pointers(self): result = f(byref(c_int(99))) self.assertNotEqual(result.contents, 99) - ################################################################ def test_shorts(self): f = dll._testfunc_callback_i_if @@ -218,9 +293,6 @@ def callback(v): f(2**18, cb) self.assertEqual(args, expected) - ################################################################ - - def test_callbacks(self): f = dll._testfunc_callback_i_if f.restype = c_int @@ -229,7 +301,6 @@ def test_callbacks(self): MyCallback = CFUNCTYPE(c_int, c_int) def callback(value): - #print "called back with", value return value cb = MyCallback(callback) @@ -262,7 +333,6 @@ def test_callbacks_2(self): f.argtypes = [c_int, MyCallback] def callback(value): - #print "called back with", value self.assertEqual(type(value), int) return value @@ -270,7 +340,6 @@ def callback(value): result = f(-10, cb) self.assertEqual(result, -18) - @need_symbol('c_longlong') def test_longlong_callbacks(self): f = dll._testfunc_callback_q_qf @@ -371,7 +440,7 @@ class S8I(Structure): (9*2, 8*3, 7*4, 6*5, 5*6, 4*7, 3*8, 2*9)) def test_sf1651235(self): - # see https://www.python.org/sf/1651235 + # see https://bugs.python.org/issue1651235 proto = CFUNCTYPE(c_int, RECT, POINT) def callback(*args): @@ -380,5 +449,6 @@ def callback(*args): callback = proto(callback) self.assertRaises(ArgumentError, lambda: callback((1, 2, 3, 4), POINT())) + if __name__ == '__main__': unittest.main() diff --git a/Lib/ctypes/test/test_incomplete.py b/Lib/test/test_ctypes/test_incomplete.py similarity index 53% rename from Lib/ctypes/test/test_incomplete.py rename to Lib/test/test_ctypes/test_incomplete.py index 00c430ef53..9f859793d8 100644 --- a/Lib/ctypes/test/test_incomplete.py +++ b/Lib/test/test_ctypes/test_incomplete.py @@ -1,12 +1,14 @@ +import ctypes import unittest -from ctypes import * +import warnings +from ctypes import Structure, POINTER, pointer, c_char_p -################################################################ -# -# The incomplete pointer example from the tutorial -# -class MyTestCase(unittest.TestCase): +# The incomplete pointer example from the tutorial +class TestSetPointerType(unittest.TestCase): + def tearDown(self): + # to not leak references, we must clean _pointer_type_cache + ctypes._reset_cache() def test_incomplete_example(self): lpcell = POINTER("cell") @@ -14,7 +16,9 @@ class cell(Structure): _fields_ = [("name", c_char_p), ("next", lpcell)] - SetPointerType(lpcell, cell) + with warnings.catch_warnings(): + warnings.simplefilter('ignore', DeprecationWarning) + ctypes.SetPointerType(lpcell, cell) c1 = cell() c1.name = b"foo" @@ -32,11 +36,15 @@ class cell(Structure): p = p.next[0] self.assertEqual(result, [b"foo", b"bar"] * 4) - # to not leak references, we must clean _pointer_type_cache - from ctypes import _pointer_type_cache - del _pointer_type_cache[cell] + def test_deprecation(self): + lpcell = POINTER("cell") + class cell(Structure): + _fields_ = [("name", c_char_p), + ("next", lpcell)] + + with self.assertWarns(DeprecationWarning): + ctypes.SetPointerType(lpcell, cell) -################################################################ if __name__ == '__main__': unittest.main() diff --git a/Lib/ctypes/test/test_init.py b/Lib/test/test_ctypes/test_init.py similarity index 96% rename from Lib/ctypes/test/test_init.py rename to Lib/test/test_ctypes/test_init.py index 75fad112a0..113425e582 100644 --- a/Lib/ctypes/test/test_init.py +++ b/Lib/test/test_ctypes/test_init.py @@ -1,5 +1,6 @@ -from ctypes import * import unittest +from ctypes import Structure, c_int + class X(Structure): _fields_ = [("a", c_int), @@ -15,6 +16,7 @@ def __init__(self): self.a = 9 self.b = 12 + class Y(Structure): _fields_ = [("x", X)] @@ -36,5 +38,6 @@ def test_get(self): self.assertEqual((y.x.a, y.x.b), (9, 12)) self.assertEqual(y.x.new_was_called, False) + if __name__ == "__main__": unittest.main() diff --git a/Lib/ctypes/test/test_internals.py b/Lib/test/test_ctypes/test_internals.py similarity index 84% rename from Lib/ctypes/test/test_internals.py rename to Lib/test/test_ctypes/test_internals.py index 271e3f57f8..292633aaa4 100644 --- a/Lib/ctypes/test/test_internals.py +++ b/Lib/test/test_ctypes/test_internals.py @@ -1,7 +1,4 @@ # This tests the internal _objects attribute -import unittest -from ctypes import * -from sys import getrefcount as grc # XXX This test must be reviewed for correctness!!! @@ -14,22 +11,29 @@ # # What about pointers? +import sys +import unittest +from ctypes import Structure, POINTER, c_char_p, c_int + + class ObjectsTestCase(unittest.TestCase): def assertSame(self, a, b): self.assertEqual(id(a), id(b)) def test_ints(self): i = 42000123 - refcnt = grc(i) + refcnt = sys.getrefcount(i) ci = c_int(i) - self.assertEqual(refcnt, grc(i)) + self.assertEqual(refcnt, sys.getrefcount(i)) self.assertEqual(ci._objects, None) + # TODO: RUSTPYTHON + @unittest.expectedFailure def test_c_char_p(self): - s = b"Hello, World" - refcnt = grc(s) + s = "Hello, World".encode("ascii") + refcnt = sys.getrefcount(s) cs = c_char_p(s) - self.assertEqual(refcnt + 1, grc(s)) + self.assertEqual(refcnt + 1, sys.getrefcount(s)) self.assertSame(cs._objects, s) def test_simple_struct(self): @@ -60,6 +64,8 @@ class Y(Structure): x1.a, x2.b = 42, 93 self.assertEqual(y._objects, {"0": {}, "1": {}}) + # TODO: RUSTPYTHON + @unittest.expectedFailure def test_xxx(self): class X(Structure): _fields_ = [("a", c_char_p), ("b", c_char_p)] @@ -78,9 +84,6 @@ class Y(Structure): y = Y() y.x = x self.assertEqual(y._objects, {"0": {"0": s1, "1": s2}}) -## x = y.x -## del y -## print x._b_base_._objects def test_ptr_struct(self): class X(Structure): @@ -92,9 +95,7 @@ class X(Structure): x = X() x.data = a -##XXX print x._objects -##XXX print x.data[0] -##XXX print x.data._objects + if __name__ == '__main__': unittest.main() diff --git a/Lib/ctypes/test/test_keeprefs.py b/Lib/test/test_ctypes/test_keeprefs.py similarity index 76% rename from Lib/ctypes/test/test_keeprefs.py rename to Lib/test/test_ctypes/test_keeprefs.py index 94c02573fa..5aa5b86fa4 100644 --- a/Lib/ctypes/test/test_keeprefs.py +++ b/Lib/test/test_ctypes/test_keeprefs.py @@ -1,5 +1,7 @@ -from ctypes import * import unittest +from ctypes import (Structure, POINTER, pointer, _pointer_type_cache, + c_char_p, c_int) + class SimpleTestCase(unittest.TestCase): def test_cint(self): @@ -10,6 +12,8 @@ def test_cint(self): x = c_int(99) self.assertEqual(x._objects, None) + # TODO: RUSTPYTHON + @unittest.expectedFailure def test_ccharp(self): x = c_char_p() self.assertEqual(x._objects, None) @@ -18,6 +22,7 @@ def test_ccharp(self): x = c_char_p(b"spam") self.assertEqual(x._objects, b"spam") + class StructureTestCase(unittest.TestCase): def test_cint_struct(self): class X(Structure): @@ -30,6 +35,8 @@ class X(Structure): x.b = 99 self.assertEqual(x._objects, None) + # TODO: RUSTPYTHON + @unittest.expectedFailure def test_ccharp_struct(self): class X(Structure): _fields_ = [("a", c_char_p), @@ -64,6 +71,7 @@ class RECT(Structure): r.lr = POINT() self.assertEqual(r._objects, {'0': {}, '1': {}}) + class ArrayTestCase(unittest.TestCase): def test_cint_array(self): INTARR = c_int * 3 @@ -87,43 +95,13 @@ class X(Structure): x.a = ia self.assertEqual(x._objects, {'1': {}}) + class PointerTestCase(unittest.TestCase): def test_p_cint(self): i = c_int(42) x = pointer(i) self.assertEqual(x._objects, {'1': i}) -class DeletePointerTestCase(unittest.TestCase): - @unittest.skip('test disabled') - def test_X(self): - class X(Structure): - _fields_ = [("p", POINTER(c_char_p))] - x = X() - i = c_char_p("abc def") - from sys import getrefcount as grc - print("2?", grc(i)) - x.p = pointer(i) - print("3?", grc(i)) - for i in range(320): - c_int(99) - x.p[0] - print(x.p[0]) -## del x -## print "2?", grc(i) -## del i - import gc - gc.collect() - for i in range(320): - c_int(99) - x.p[0] - print(x.p[0]) - print(x.p.contents) -## print x._objects - - x.p[0] = "spam spam" -## print x.p[0] - print("+" * 42) - print(x._objects) class PointerToStructure(unittest.TestCase): def test(self): @@ -137,17 +115,14 @@ class RECT(Structure): r.a = pointer(p1) r.b = pointer(p1) -## from pprint import pprint as pp -## pp(p1._objects) -## pp(r._objects) r.a[0].x = 42 r.a[0].y = 99 # to avoid leaking when tests are run several times # clean up the types left in the cache. - from ctypes import _pointer_type_cache del _pointer_type_cache[POINT] + if __name__ == "__main__": unittest.main() diff --git a/Lib/ctypes/test/test_libc.py b/Lib/test/test_ctypes/test_libc.py similarity index 78% rename from Lib/ctypes/test/test_libc.py rename to Lib/test/test_ctypes/test_libc.py index 56285b5ff8..7716100b08 100644 --- a/Lib/ctypes/test/test_libc.py +++ b/Lib/test/test_ctypes/test_libc.py @@ -1,20 +1,24 @@ +import math import unittest +from ctypes import (CDLL, CFUNCTYPE, POINTER, create_string_buffer, sizeof, + c_void_p, c_char, c_int, c_double, c_size_t) +from test.support import import_helper +_ctypes_test = import_helper.import_module("_ctypes_test") -from ctypes import * -import _ctypes_test lib = CDLL(_ctypes_test.__file__) + def three_way_cmp(x, y): """Return -1 if x < y, 0 if x == y and 1 if x > y""" return (x > y) - (x < y) + class LibTest(unittest.TestCase): def test_sqrt(self): lib.my_sqrt.argtypes = c_double, lib.my_sqrt.restype = c_double self.assertEqual(lib.my_sqrt(4.0), 2.0) - import math self.assertEqual(lib.my_sqrt(2.0), math.sqrt(2.0)) def test_qsort(self): @@ -29,5 +33,6 @@ def sort(a, b): lib.my_qsort(chars, len(chars)-1, sizeof(c_char), comparefunc(sort)) self.assertEqual(chars.raw, b" ,,aaaadmmmnpppsss\x00") + if __name__ == "__main__": unittest.main() diff --git a/Lib/ctypes/test/test_loading.py b/Lib/test/test_ctypes/test_loading.py similarity index 73% rename from Lib/ctypes/test/test_loading.py rename to Lib/test/test_ctypes/test_loading.py index ea892277c4..a4d54f676a 100644 --- a/Lib/ctypes/test/test_loading.py +++ b/Lib/test/test_ctypes/test_loading.py @@ -1,16 +1,20 @@ -from ctypes import * +import _ctypes +import ctypes import os import shutil import subprocess import sys -import unittest import test.support -from test.support import import_helper -from test.support import os_helper +import unittest +from ctypes import CDLL, cdll, addressof, c_void_p, c_char_p from ctypes.util import find_library +from test.support import import_helper, os_helper +_ctypes_test = import_helper.import_module("_ctypes_test") + libc_name = None + def setUpModule(): global libc_name if os.name == "nt": @@ -23,15 +27,22 @@ def setUpModule(): if test.support.verbose: print("libc_name is", libc_name) + class LoaderTest(unittest.TestCase): unknowndll = "xxrandomnamexx" def test_load(self): - if libc_name is None: - self.skipTest('could not find libc') - CDLL(libc_name) - CDLL(os.path.basename(libc_name)) + if libc_name is not None: + test_lib = libc_name + else: + if os.name == "nt": + test_lib = _ctypes_test.__file__ + else: + self.skipTest('could not find library to load') + CDLL(test_lib) + CDLL(os.path.basename(test_lib)) + CDLL(os_helper.FakePath(test_lib)) self.assertRaises(OSError, CDLL, self.unknowndll) def test_load_version(self): @@ -45,11 +56,15 @@ def test_load_version(self): self.assertRaises(OSError, cdll.LoadLibrary, self.unknowndll) def test_find(self): + found = False for name in ("c", "m"): lib = find_library(name) if lib: + found = True cdll.LoadLibrary(lib) CDLL(lib) + if not found: + self.skipTest("Could not find c and m libraries") @unittest.skipUnless(os.name == "nt", 'test specific to Windows') @@ -62,18 +77,17 @@ def test_load_library(self): print(find_library("user32")) if os.name == "nt": - windll.kernel32.GetModuleHandleW - windll["kernel32"].GetModuleHandleW - windll.LoadLibrary("kernel32").GetModuleHandleW - WinDLL("kernel32").GetModuleHandleW + ctypes.windll.kernel32.GetModuleHandleW + ctypes.windll["kernel32"].GetModuleHandleW + ctypes.windll.LoadLibrary("kernel32").GetModuleHandleW + ctypes.WinDLL("kernel32").GetModuleHandleW # embedded null character - self.assertRaises(ValueError, windll.LoadLibrary, "kernel32\0") + self.assertRaises(ValueError, ctypes.windll.LoadLibrary, "kernel32\0") @unittest.skipUnless(os.name == "nt", 'test specific to Windows') def test_load_ordinal_functions(self): - import _ctypes_test - dll = WinDLL(_ctypes_test.__file__) + dll = ctypes.WinDLL(_ctypes_test.__file__) # We load the same function both via ordinal and name func_ord = dll[2] func_name = dll.GetString @@ -86,16 +100,21 @@ def test_load_ordinal_functions(self): self.assertRaises(AttributeError, dll.__getitem__, 1234) + @unittest.skipUnless(os.name == "nt", 'Windows-specific test') + def test_load_without_name_and_with_handle(self): + handle = ctypes.windll.kernel32._handle + lib = ctypes.WinDLL(name=None, handle=handle) + self.assertIs(handle, lib._handle) + @unittest.skipUnless(os.name == "nt", 'Windows-specific test') def test_1703286_A(self): - from _ctypes import LoadLibrary, FreeLibrary # On winXP 64-bit, advapi32 loads at an address that does # NOT fit into a 32-bit integer. FreeLibrary must be able # to accept this address. - # These are tests for https://www.python.org/sf/1703286 - handle = LoadLibrary("advapi32") - FreeLibrary(handle) + # These are tests for https://bugs.python.org/issue1703286 + handle = _ctypes.LoadLibrary("advapi32") + _ctypes.FreeLibrary(handle) @unittest.skipUnless(os.name == "nt", 'Windows-specific test') def test_1703286_B(self): @@ -103,25 +122,33 @@ def test_1703286_B(self): # above, the (arbitrarily selected) CloseEventLog function # also has a high address. 'call_function' should accept # addresses so large. - from _ctypes import call_function - advapi32 = windll.advapi32 + + advapi32 = ctypes.windll.advapi32 # Calling CloseEventLog with a NULL argument should fail, # but the call should not segfault or so. self.assertEqual(0, advapi32.CloseEventLog(None)) - windll.kernel32.GetProcAddress.argtypes = c_void_p, c_char_p - windll.kernel32.GetProcAddress.restype = c_void_p - proc = windll.kernel32.GetProcAddress(advapi32._handle, - b"CloseEventLog") + + kernel32 = ctypes.windll.kernel32 + kernel32.GetProcAddress.argtypes = c_void_p, c_char_p + kernel32.GetProcAddress.restype = c_void_p + proc = kernel32.GetProcAddress(advapi32._handle, b"CloseEventLog") self.assertTrue(proc) + # This is the real test: call the function via 'call_function' - self.assertEqual(0, call_function(proc, (None,))) + self.assertEqual(0, _ctypes.call_function(proc, (None,))) + + @unittest.skipUnless(os.name == "nt", + 'test specific to Windows') + def test_load_hasattr(self): + # bpo-34816: shouldn't raise OSError + self.assertFalse(hasattr(ctypes.windll, 'test')) @unittest.skipUnless(os.name == "nt", 'test specific to Windows') def test_load_dll_with_flags(self): _sqlite3 = import_helper.import_module("_sqlite3") src = _sqlite3.__file__ - if src.lower().endswith("_d.pyd"): + if os.path.basename(src).partition(".")[0].lower().endswith("_d"): ext = "_d.dll" else: ext = ".dll" @@ -177,6 +204,5 @@ def should_fail(command): "WinDLL('_sqlite3.dll'); p.close()") - if __name__ == "__main__": unittest.main() diff --git a/Lib/ctypes/test/test_macholib.py b/Lib/test/test_ctypes/test_macholib.py similarity index 99% rename from Lib/ctypes/test/test_macholib.py rename to Lib/test/test_ctypes/test_macholib.py index bc75f1a05a..9d90617995 100644 --- a/Lib/ctypes/test/test_macholib.py +++ b/Lib/test/test_ctypes/test_macholib.py @@ -1,7 +1,3 @@ -import os -import sys -import unittest - # Bob Ippolito: # # Ok.. the code to find the filename for __getattr__ should look @@ -31,10 +27,15 @@ # # -bob +import os +import sys +import unittest + from ctypes.macholib.dyld import dyld_find from ctypes.macholib.dylib import dylib_info from ctypes.macholib.framework import framework_info + def find_lib(name): possible = ['lib'+name+'.dylib', name+'.dylib', name+'.framework/'+name] for dylib in possible: @@ -106,5 +107,6 @@ def test_framework_info(self): self.assertEqual(framework_info('P/F.framework/Versions/A/F_debug'), d('P', 'F.framework/Versions/A/F_debug', 'F', 'A', 'debug')) + if __name__ == "__main__": unittest.main() diff --git a/Lib/ctypes/test/test_memfunctions.py b/Lib/test/test_ctypes/test_memfunctions.py similarity index 87% rename from Lib/ctypes/test/test_memfunctions.py rename to Lib/test/test_ctypes/test_memfunctions.py index e784b9a706..112b27ba48 100644 --- a/Lib/ctypes/test/test_memfunctions.py +++ b/Lib/test/test_ctypes/test_memfunctions.py @@ -1,20 +1,23 @@ import sys -from test import support import unittest -from ctypes import * -from ctypes.test import need_symbol +from test import support +from ctypes import (POINTER, sizeof, cast, + create_string_buffer, string_at, + create_unicode_buffer, wstring_at, + memmove, memset, + c_char_p, c_byte, c_ubyte, c_wchar) + class MemFunctionsTest(unittest.TestCase): - @unittest.skip('test disabled') def test_overflow(self): # string_at and wstring_at must use the Python calling # convention (which acquires the GIL and checks the Python # error flag). Provoke an error and catch it; see also issue - # #3554: + # gh-47804. self.assertRaises((OverflowError, MemoryError, SystemError), - lambda: wstring_at(u"foo", sys.maxint - 1)) + lambda: wstring_at(u"foo", sys.maxsize - 1)) self.assertRaises((OverflowError, MemoryError, SystemError), - lambda: string_at("foo", sys.maxint - 1)) + lambda: string_at("foo", sys.maxsize - 1)) def test_memmove(self): # large buffers apparently increase the chance that the memory @@ -63,7 +66,6 @@ def test_string_at(self): self.assertEqual(string_at(b"foo bar", 7), b"foo bar") self.assertEqual(string_at(b"foo bar", 3), b"foo") - @need_symbol('create_unicode_buffer') def test_wstring_at(self): p = create_unicode_buffer("Hello, World") a = create_unicode_buffer(1000000) @@ -75,5 +77,6 @@ def test_wstring_at(self): self.assertEqual(wstring_at(a, 16), "Hello, World\0\0\0\0") self.assertEqual(wstring_at(a, 0), "") + if __name__ == "__main__": unittest.main() diff --git a/Lib/ctypes/test/test_numbers.py b/Lib/test/test_ctypes/test_numbers.py similarity index 89% rename from Lib/ctypes/test/test_numbers.py rename to Lib/test/test_ctypes/test_numbers.py index a5c661b0e9..29108a28ec 100644 --- a/Lib/ctypes/test/test_numbers.py +++ b/Lib/test/test_ctypes/test_numbers.py @@ -1,6 +1,13 @@ -from ctypes import * -import unittest +import array import struct +import sys +import unittest +from operator import truth +from ctypes import (byref, sizeof, alignment, + c_char, c_byte, c_ubyte, c_short, c_ushort, c_int, c_uint, + c_long, c_ulong, c_longlong, c_ulonglong, + c_float, c_double, c_longdouble, c_bool) + def valid_ranges(*types): # given a sequence of numeric types, collect their _type_ @@ -19,36 +26,18 @@ def valid_ranges(*types): result.append((min(a, b, c, d), max(a, b, c, d))) return result + ArgType = type(byref(c_int(0))) -unsigned_types = [c_ubyte, c_ushort, c_uint, c_ulong] +unsigned_types = [c_ubyte, c_ushort, c_uint, c_ulong, c_ulonglong] signed_types = [c_byte, c_short, c_int, c_long, c_longlong] - -bool_types = [] - +bool_types = [c_bool] float_types = [c_double, c_float] -try: - c_ulonglong - c_longlong -except NameError: - pass -else: - unsigned_types.append(c_ulonglong) - signed_types.append(c_longlong) - -try: - c_bool -except NameError: - pass -else: - bool_types.append(c_bool) - unsigned_ranges = valid_ranges(*unsigned_types) signed_ranges = valid_ranges(*signed_types) bool_values = [True, False, 0, 1, -1, 5000, 'test', [], [1]] -################################################################ class NumberTestCase(unittest.TestCase): @@ -71,7 +60,6 @@ def test_signed_values(self): self.assertEqual(t(h).value, h) def test_bool_values(self): - from operator import truth for t, v in zip(bool_types, bool_values): self.assertEqual(t(v).value, truth(v)) @@ -147,21 +135,20 @@ def test_alignments(self): # alignment of the type... self.assertEqual((code, alignment(t)), - (code, align)) + (code, align)) # and alignment of an instance self.assertEqual((code, alignment(t())), - (code, align)) + (code, align)) def test_int_from_address(self): - from array import array for t in signed_types + unsigned_types: # the array module doesn't support all format codes # (no 'q' or 'Q') try: - array(t._type_) + array.array(t._type_) except ValueError: continue - a = array(t._type_, [100]) + a = array.array(t._type_, [100]) # v now is an integer at an 'external' memory location v = t.from_address(a.buffer_info()[0]) @@ -174,9 +161,8 @@ def test_int_from_address(self): def test_float_from_address(self): - from array import array for t in float_types: - a = array(t._type_, [3.14]) + a = array.array(t._type_, [3.14]) v = t.from_address(a.buffer_info()[0]) self.assertEqual(v.value, a[0]) self.assertIs(type(v), t) @@ -185,10 +171,7 @@ def test_float_from_address(self): self.assertIs(type(v), t) def test_char_from_address(self): - from ctypes import c_char - from array import array - - a = array('b', [0]) + a = array.array('b', [0]) a[0] = ord('x') v = c_char.from_address(a.buffer_info()[0]) self.assertEqual(v.value, b'x') @@ -204,7 +187,6 @@ def test_init(self): self.assertRaises(TypeError, c_int, c_long(42)) def test_float_overflow(self): - import sys big_int = int(sys.float_info.max) * 2 for t in float_types + [c_longdouble]: self.assertRaises(OverflowError, t, big_int) diff --git a/Lib/ctypes/test/test_objects.py b/Lib/test/test_ctypes/test_objects.py similarity index 78% rename from Lib/ctypes/test/test_objects.py rename to Lib/test/test_ctypes/test_objects.py index 19e3dc1f2d..8db1cd873f 100644 --- a/Lib/ctypes/test/test_objects.py +++ b/Lib/test/test_ctypes/test_objects.py @@ -11,7 +11,7 @@ Here is an array of string pointers: ->>> from ctypes import * +>>> from ctypes import Structure, c_int, c_char_p >>> array = (c_char_p * 5)() >>> print(array._objects) None @@ -42,7 +42,7 @@ of 'x' ('_b_base_' is either None, or the root object owning the memory block): >>> print(x.array._b_base_) # doctest: +ELLIPSIS - + >>> >>> x.array[0] = b'spam spam spam' @@ -51,17 +51,17 @@ >>> x.array._b_base_._objects {'0:2': b'spam spam spam'} >>> - ''' -import unittest, doctest +import doctest +import unittest + -import ctypes.test.test_objects +def load_tests(loader, tests, pattern): + # TODO: RUSTPYTHON - doctest disabled due to null terminator in _objects + # tests.addTest(doctest.DocTestSuite()) + return tests -class TestCase(unittest.TestCase): - def test(self): - failures, tests = doctest.testmod(ctypes.test.test_objects) - self.assertFalse(failures, 'doctests failed, see output above') if __name__ == '__main__': - doctest.testmod(ctypes.test.test_objects) + unittest.main() diff --git a/Lib/ctypes/test/test_parameters.py b/Lib/test/test_ctypes/test_parameters.py similarity index 72% rename from Lib/ctypes/test/test_parameters.py rename to Lib/test/test_ctypes/test_parameters.py index 38af7ac13d..1a6ddb91a7 100644 --- a/Lib/ctypes/test/test_parameters.py +++ b/Lib/test/test_ctypes/test_parameters.py @@ -1,11 +1,25 @@ +import sys import unittest -from ctypes.test import need_symbol import test.support +from ctypes import (CDLL, PyDLL, ArgumentError, + Structure, Array, Union, + _Pointer, _SimpleCData, _CFuncPtr, + POINTER, pointer, byref, + c_void_p, c_char_p, c_wchar_p, py_object, + c_bool, + c_char, c_wchar, + c_byte, c_ubyte, + c_short, c_ushort, + c_int, c_uint, + c_long, c_ulong, + c_longlong, c_ulonglong, + c_float, c_double, c_longdouble) +from test.support import import_helper +_ctypes_test = import_helper.import_module("_ctypes_test") -class SimpleTypesTestCase(unittest.TestCase): +class SimpleTypesTestCase(unittest.TestCase): def setUp(self): - import ctypes try: from _ctypes import set_conversion_mode except ImportError: @@ -22,7 +36,6 @@ def tearDown(self): set_conversion_mode(*self.prev_conv_mode) def test_subclasses(self): - from ctypes import c_void_p, c_char_p # ctypes 0.9.5 and before did overwrite from_param in SimpleType_new class CVOIDP(c_void_p): def from_param(cls, value): @@ -37,10 +50,7 @@ def from_param(cls, value): self.assertEqual(CVOIDP.from_param("abc"), "abcabc") self.assertEqual(CCHARP.from_param("abc"), "abcabcabcabc") - @need_symbol('c_wchar_p') def test_subclasses_c_wchar_p(self): - from ctypes import c_wchar_p - class CWCHARP(c_wchar_p): def from_param(cls, value): return value * 3 @@ -50,8 +60,6 @@ def from_param(cls, value): # XXX Replace by c_char_p tests def test_cstrings(self): - from ctypes import c_char_p - # c_char_p.from_param on a Python String packs the string # into a cparam object s = b"123" @@ -67,10 +75,7 @@ def test_cstrings(self): a = c_char_p(b"123") self.assertIs(c_char_p.from_param(a), a) - @need_symbol('c_wchar_p') def test_cw_strings(self): - from ctypes import c_wchar_p - c_wchar_p.from_param("123") self.assertRaises(TypeError, c_wchar_p.from_param, 42) @@ -79,12 +84,27 @@ def test_cw_strings(self): pa = c_wchar_p.from_param(c_wchar_p("123")) self.assertEqual(type(pa), c_wchar_p) + def test_c_char(self): + with self.assertRaises(TypeError) as cm: + c_char.from_param(b"abc") + self.assertEqual(str(cm.exception), + "one character bytes, bytearray or integer expected") + + def test_c_wchar(self): + with self.assertRaises(TypeError) as cm: + c_wchar.from_param("abc") + self.assertEqual(str(cm.exception), + "one character unicode string expected") + + + with self.assertRaises(TypeError) as cm: + c_wchar.from_param(123) + self.assertEqual(str(cm.exception), + "unicode string expected instead of int instance") + def test_int_pointers(self): - from ctypes import c_short, c_uint, c_int, c_long, POINTER, pointer LPINT = POINTER(c_int) -## p = pointer(c_int(42)) -## x = LPINT.from_param(p) x = LPINT.from_param(pointer(c_int(42))) self.assertEqual(x.contents.value, 42) self.assertEqual(LPINT(c_int(42)).contents.value, 42) @@ -99,7 +119,6 @@ def test_int_pointers(self): def test_byref_pointer(self): # The from_param class method of POINTER(typ) classes accepts what is # returned by byref(obj), it type(obj) == typ - from ctypes import c_short, c_uint, c_int, c_long, POINTER, byref LPINT = POINTER(c_int) LPINT.from_param(byref(c_int(42))) @@ -111,7 +130,6 @@ def test_byref_pointer(self): def test_byref_pointerpointer(self): # See above - from ctypes import c_short, c_uint, c_int, c_long, pointer, POINTER, byref LPLPINT = POINTER(POINTER(c_int)) LPLPINT.from_param(byref(pointer(c_int(42)))) @@ -122,7 +140,6 @@ def test_byref_pointerpointer(self): self.assertRaises(TypeError, LPLPINT.from_param, byref(pointer(c_uint(22)))) def test_array_pointers(self): - from ctypes import c_short, c_uint, c_int, c_long, POINTER INTARRAY = c_int * 3 ia = INTARRAY() self.assertEqual(len(ia), 3) @@ -137,15 +154,12 @@ def test_array_pointers(self): self.assertRaises(TypeError, LPINT.from_param, c_uint*3) def test_noctypes_argtype(self): - import _ctypes_test - from ctypes import CDLL, c_void_p, ArgumentError - func = CDLL(_ctypes_test.__file__)._testfunc_p_p func.restype = c_void_p # TypeError: has no from_param method self.assertRaises(TypeError, setattr, func, "argtypes", (object,)) - class Adapter(object): + class Adapter: def from_param(cls, obj): return None @@ -153,7 +167,7 @@ def from_param(cls, obj): self.assertEqual(func(None), None) self.assertEqual(func(object()), None) - class Adapter(object): + class Adapter: def from_param(cls, obj): return obj @@ -162,7 +176,7 @@ def from_param(cls, obj): self.assertRaises(ArgumentError, func, object()) self.assertEqual(func(c_void_p(42)), 42) - class Adapter(object): + class Adapter: def from_param(cls, obj): raise ValueError(obj) @@ -171,9 +185,6 @@ def from_param(cls, obj): self.assertRaises(ArgumentError, func, 99) def test_abstract(self): - from ctypes import (Array, Structure, Union, _Pointer, - _SimpleCData, _CFuncPtr) - self.assertRaises(TypeError, Array.from_param, 42) self.assertRaises(TypeError, Structure.from_param, 42) self.assertRaises(TypeError, Union.from_param, 42) @@ -185,7 +196,6 @@ def test_abstract(self): def test_issue31311(self): # __setstate__ should neither raise a SystemError nor crash in case # of a bad __dict__. - from ctypes import Structure class BadStruct(Structure): @property @@ -202,27 +212,6 @@ def __dict__(self): WorseStruct().__setstate__({}, b'foo') def test_parameter_repr(self): - from ctypes import ( - c_bool, - c_char, - c_wchar, - c_byte, - c_ubyte, - c_short, - c_ushort, - c_int, - c_uint, - c_long, - c_ulong, - c_longlong, - c_ulonglong, - c_float, - c_double, - c_longdouble, - c_char_p, - c_wchar_p, - c_void_p, - ) self.assertRegex(repr(c_bool.from_param(True)), r"^$") self.assertEqual(repr(c_char.from_param(97)), "") self.assertRegex(repr(c_wchar.from_param('a')), r"^$") @@ -238,13 +227,62 @@ def test_parameter_repr(self): self.assertRegex(repr(c_ulonglong.from_param(20000)), r"^$") self.assertEqual(repr(c_float.from_param(1.5)), "") self.assertEqual(repr(c_double.from_param(1.5)), "") - self.assertEqual(repr(c_double.from_param(1e300)), "") + if sys.float_repr_style == 'short': + self.assertEqual(repr(c_double.from_param(1e300)), "") self.assertRegex(repr(c_longdouble.from_param(1.5)), r"^$") 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"^$") -################################################################ + @test.support.cpython_only + def test_from_param_result_refcount(self): + # Issue #99952 + class X(Structure): + """This struct size is <= sizeof(void*).""" + _fields_ = [("a", c_void_p)] + + def __del__(self): + trace.append(4) + + @classmethod + def from_param(cls, value): + trace.append(2) + return cls() + + PyList_Append = PyDLL(_ctypes_test.__file__)._testfunc_pylist_append + PyList_Append.restype = c_int + PyList_Append.argtypes = [py_object, py_object, X] + + trace = [] + trace.append(1) + PyList_Append(trace, 3, "dummy") + trace.append(5) + + self.assertEqual(trace, [1, 2, 3, 4, 5]) + + class Y(Structure): + """This struct size is > sizeof(void*).""" + _fields_ = [("a", c_void_p), ("b", c_void_p)] + + def __del__(self): + trace.append(4) + + @classmethod + def from_param(cls, value): + trace.append(2) + return cls() + + PyList_Append = PyDLL(_ctypes_test.__file__)._testfunc_pylist_append + PyList_Append.restype = c_int + PyList_Append.argtypes = [py_object, py_object, Y] + + trace = [] + trace.append(1) + PyList_Append(trace, 3, "dummy") + trace.append(5) + + self.assertEqual(trace, [1, 2, 3, 4, 5]) + if __name__ == '__main__': unittest.main() diff --git a/Lib/ctypes/test/test_pep3118.py b/Lib/test/test_ctypes/test_pep3118.py similarity index 70% rename from Lib/ctypes/test/test_pep3118.py rename to Lib/test/test_ctypes/test_pep3118.py index efffc80a66..06b2ccecad 100644 --- a/Lib/ctypes/test/test_pep3118.py +++ b/Lib/test/test_ctypes/test_pep3118.py @@ -1,6 +1,13 @@ +import re +import sys import unittest -from ctypes import * -import re, sys +from ctypes import (CFUNCTYPE, POINTER, sizeof, Union, + Structure, LittleEndianStructure, BigEndianStructure, + c_char, c_byte, c_ubyte, + c_short, c_ushort, c_int, c_uint, + c_long, c_ulong, c_longlong, c_ulonglong, c_uint64, + c_bool, c_float, c_double, c_longdouble, py_object) + if sys.byteorder == "little": THIS_ENDIAN = "<" @@ -9,6 +16,7 @@ THIS_ENDIAN = ">" OTHER_ENDIAN = "<" + def normalize(format): # Remove current endian specifier and white space from a format # string @@ -17,65 +25,54 @@ def normalize(format): format = format.replace(OTHER_ENDIAN, THIS_ENDIAN) return re.sub(r"\s", "", format) -class Test(unittest.TestCase): +class Test(unittest.TestCase): def test_native_types(self): for tp, fmt, shape, itemtp in native_types: ob = tp() v = memoryview(ob) - try: - self.assertEqual(normalize(v.format), normalize(fmt)) - if shape: - self.assertEqual(len(v), shape[0]) - else: - self.assertEqual(len(v) * sizeof(itemtp), sizeof(ob)) - self.assertEqual(v.itemsize, sizeof(itemtp)) - self.assertEqual(v.shape, shape) - # XXX Issue #12851: PyCData_NewGetBuffer() must provide strides - # if requested. memoryview currently reconstructs missing - # stride information, so this assert will fail. - # self.assertEqual(v.strides, ()) - - # they are always read/write - self.assertFalse(v.readonly) - - if v.shape: - n = 1 - for dim in v.shape: - n = n * dim - self.assertEqual(n * v.itemsize, len(v.tobytes())) - except: - # so that we can see the failing type - print(tp) - raise + self.assertEqual(normalize(v.format), normalize(fmt)) + if shape: + self.assertEqual(len(v), shape[0]) + else: + self.assertRaises(TypeError, len, v) + self.assertEqual(v.itemsize, sizeof(itemtp)) + self.assertEqual(v.shape, shape) + # XXX Issue #12851: PyCData_NewGetBuffer() must provide strides + # if requested. memoryview currently reconstructs missing + # stride information, so this assert will fail. + # self.assertEqual(v.strides, ()) + + # they are always read/write + self.assertFalse(v.readonly) + + n = 1 + for dim in v.shape: + n = n * dim + self.assertEqual(n * v.itemsize, len(v.tobytes())) def test_endian_types(self): for tp, fmt, shape, itemtp in endian_types: ob = tp() v = memoryview(ob) - try: - self.assertEqual(v.format, fmt) - if shape: - self.assertEqual(len(v), shape[0]) - else: - self.assertEqual(len(v) * sizeof(itemtp), sizeof(ob)) - self.assertEqual(v.itemsize, sizeof(itemtp)) - self.assertEqual(v.shape, shape) - # XXX Issue #12851 - # self.assertEqual(v.strides, ()) - - # they are always read/write - self.assertFalse(v.readonly) - - if v.shape: - n = 1 - for dim in v.shape: - n = n * dim - self.assertEqual(n, len(v)) - except: - # so that we can see the failing type - print(tp) - raise + self.assertEqual(v.format, fmt) + if shape: + self.assertEqual(len(v), shape[0]) + else: + self.assertRaises(TypeError, len, v) + self.assertEqual(v.itemsize, sizeof(itemtp)) + self.assertEqual(v.shape, shape) + # XXX Issue #12851 + # self.assertEqual(v.strides, ()) + + # they are always read/write + self.assertFalse(v.readonly) + + n = 1 + for dim in v.shape: + n = n * dim + self.assertEqual(n * v.itemsize, len(v.tobytes())) + # define some structure classes @@ -86,6 +83,20 @@ class PackedPoint(Structure): _pack_ = 2 _fields_ = [("x", c_long), ("y", c_long)] +class PointMidPad(Structure): + _fields_ = [("x", c_byte), ("y", c_uint)] + +class PackedPointMidPad(Structure): + _pack_ = 2 + _fields_ = [("x", c_byte), ("y", c_uint64)] + +class PointEndPad(Structure): + _fields_ = [("x", c_uint), ("y", c_byte)] + +class PackedPointEndPad(Structure): + _pack_ = 2 + _fields_ = [("x", c_uint64), ("y", c_byte)] + class Point2(Structure): pass Point2._fields_ = [("x", c_long), ("y", c_long)] @@ -107,6 +118,7 @@ class Complete(Structure): PComplete = POINTER(Complete) Complete._fields_ = [("a", c_long)] + ################################################################ # # This table contains format strings as they look on little endian @@ -185,11 +197,14 @@ class Complete(Structure): ## structures and unions - (Point, "T{l:x:>l:y:}".replace('l', s_long), (), BEPoint), - (LEPoint, "T{l:x:>l:y:}".replace('l', s_long), (), POINTER(BEPoint)), (POINTER(LEPoint), "&T{= 0 return a + def c_wbuffer(init): n = len(init) + 1 return (c_wchar * n)(*init) -class CharPointersTestCase(unittest.TestCase): +class CharPointersTestCase(unittest.TestCase): def setUp(self): func = testdll._testfunc_p_p func.restype = c_long @@ -66,6 +72,32 @@ def test_paramflags(self): self.assertEqual(func(None), None) self.assertEqual(func(input=None), None) + def test_invalid_paramflags(self): + proto = CFUNCTYPE(c_int, c_char_p) + with self.assertRaises(ValueError): + func = proto(("myprintf", testdll), ((1, "fmt"), (1, "arg1"))) + + def test_invalid_setattr_argtypes(self): + proto = CFUNCTYPE(c_int, c_char_p) + func = proto(("myprintf", testdll), ((1, "fmt"),)) + + with self.assertRaisesRegex(TypeError, "_argtypes_ must be a sequence of types"): + func.argtypes = 123 + self.assertEqual(func.argtypes, (c_char_p,)) + + with self.assertRaisesRegex(ValueError, "paramflags must have the same length as argtypes"): + func.argtypes = (c_char_p, c_int) + self.assertEqual(func.argtypes, (c_char_p,)) + + def test_paramflags_outarg(self): + proto = CFUNCTYPE(c_int, c_char_p, c_int) + with self.assertRaisesRegex(TypeError, "must be a pointer type"): + func = proto(("myprintf", testdll), ((1, "fmt"), (2, "out"))) + + proto = CFUNCTYPE(c_int, c_char_p, c_void_p) + func = proto(("myprintf", testdll), ((1, "fmt"), (2, "out"))) + with self.assertRaisesRegex(TypeError, "must be a pointer type"): + func.argtypes = (c_char_p, c_int) def test_int_pointer_arg(self): func = testdll._testfunc_p_p @@ -100,7 +132,7 @@ def test_POINTER_c_char_arg(self): self.assertEqual(None, func(c_char_p(None))) self.assertEqual(b"123", func(c_char_p(b"123"))) - self.assertEqual(b"123", func(c_buffer(b"123"))) + self.assertEqual(b"123", func(create_string_buffer(b"123"))) ca = c_char(b"a") self.assertEqual(ord(b"a"), func(pointer(ca))[0]) self.assertEqual(ord(b"a"), func(byref(ca))[0]) @@ -115,7 +147,7 @@ def test_c_char_p_arg(self): self.assertEqual(None, func(c_char_p(None))) self.assertEqual(b"123", func(c_char_p(b"123"))) - self.assertEqual(b"123", func(c_buffer(b"123"))) + self.assertEqual(b"123", func(create_string_buffer(b"123"))) ca = c_char(b"a") self.assertEqual(ord(b"a"), func(pointer(ca))[0]) self.assertEqual(ord(b"a"), func(byref(ca))[0]) @@ -130,7 +162,7 @@ def test_c_void_p_arg(self): self.assertEqual(b"123", func(c_char_p(b"123"))) self.assertEqual(None, func(c_char_p(None))) - self.assertEqual(b"123", func(c_buffer(b"123"))) + self.assertEqual(b"123", func(create_string_buffer(b"123"))) ca = c_char(b"a") self.assertEqual(ord(b"a"), func(pointer(ca))[0]) self.assertEqual(ord(b"a"), func(byref(ca))[0]) @@ -139,7 +171,6 @@ def test_c_void_p_arg(self): func(pointer(c_int())) func((c_int * 3)()) - @need_symbol('c_wchar_p') def test_c_void_p_arg_with_c_wchar_p(self): func = testdll._testfunc_p_p func.restype = c_wchar_p @@ -161,9 +192,8 @@ class X: func.argtypes = None self.assertEqual(None, func(X())) -@need_symbol('c_wchar') -class WCharPointersTestCase(unittest.TestCase): +class WCharPointersTestCase(unittest.TestCase): def setUp(self): func = testdll._testfunc_p_p func.restype = c_int @@ -203,6 +233,7 @@ def test_c_wchar_p_arg(self): self.assertEqual("a", func(pointer(ca))[0]) self.assertEqual("a", func(byref(ca))[0]) + class ArrayTest(unittest.TestCase): def test(self): func = testdll._testfunc_ai8 @@ -216,7 +247,6 @@ def test(self): def func(): pass CFUNCTYPE(None, c_int * 3)(func) -################################################################ if __name__ == '__main__': unittest.main() diff --git a/Lib/ctypes/test/test_python_api.py b/Lib/test/test_ctypes/test_python_api.py similarity index 69% rename from Lib/ctypes/test/test_python_api.py rename to Lib/test/test_ctypes/test_python_api.py index 49571f97bb..2e68b35f8a 100644 --- a/Lib/ctypes/test/test_python_api.py +++ b/Lib/test/test_ctypes/test_python_api.py @@ -1,18 +1,14 @@ -from ctypes import * +import _ctypes +import sys import unittest from test import support +from ctypes import (pythonapi, POINTER, create_string_buffer, sizeof, + py_object, c_char_p, c_char, c_long, c_size_t) -################################################################ -# This section should be moved into ctypes\__init__.py, when it's ready. - -from _ctypes import PyObj_FromPtr - -################################################################ - -from sys import getrefcount as grc class PythonAPITestCase(unittest.TestCase): - + # TODO: RUSTPYTHON + @unittest.expectedFailure def test_PyBytes_FromStringAndSize(self): PyBytes_FromStringAndSize = pythonapi.PyBytes_FromStringAndSize @@ -27,46 +23,49 @@ def test_PyString_FromString(self): pythonapi.PyBytes_FromString.argtypes = (c_char_p,) s = b"abc" - refcnt = grc(s) + refcnt = sys.getrefcount(s) pyob = pythonapi.PyBytes_FromString(s) - self.assertEqual(grc(s), refcnt) + self.assertEqual(sys.getrefcount(s), refcnt) self.assertEqual(s, pyob) del pyob - self.assertEqual(grc(s), refcnt) + self.assertEqual(sys.getrefcount(s), refcnt) @support.refcount_test def test_PyLong_Long(self): - ref42 = grc(42) + ref42 = sys.getrefcount(42) pythonapi.PyLong_FromLong.restype = py_object self.assertEqual(pythonapi.PyLong_FromLong(42), 42) - self.assertEqual(grc(42), ref42) + self.assertEqual(sys.getrefcount(42), ref42) pythonapi.PyLong_AsLong.argtypes = (py_object,) pythonapi.PyLong_AsLong.restype = c_long res = pythonapi.PyLong_AsLong(42) - self.assertEqual(grc(res), ref42 + 1) + # Small int refcnts don't change + self.assertEqual(sys.getrefcount(res), ref42) del res - self.assertEqual(grc(42), ref42) + self.assertEqual(sys.getrefcount(42), ref42) @support.refcount_test def test_PyObj_FromPtr(self): - s = "abc def ghi jkl" - ref = grc(s) + s = object() + ref = sys.getrefcount(s) # id(python-object) is the address - pyobj = PyObj_FromPtr(id(s)) + pyobj = _ctypes.PyObj_FromPtr(id(s)) self.assertIs(s, pyobj) - self.assertEqual(grc(s), ref + 1) + self.assertEqual(sys.getrefcount(s), ref + 1) del pyobj - self.assertEqual(grc(s), ref) + self.assertEqual(sys.getrefcount(s), ref) + # TODO: RUSTPYTHON + @unittest.expectedFailure def test_PyOS_snprintf(self): PyOS_snprintf = pythonapi.PyOS_snprintf PyOS_snprintf.argtypes = POINTER(c_char), c_size_t, c_char_p - buf = c_buffer(256) + buf = create_string_buffer(256) PyOS_snprintf(buf, sizeof(buf), b"Hello from %s", b"ctypes") self.assertEqual(buf.value, b"Hello from ctypes") @@ -81,5 +80,6 @@ def test_pyobject_repr(self): self.assertEqual(repr(py_object(42)), "py_object(42)") self.assertEqual(repr(py_object(object)), "py_object(%r)" % object) + if __name__ == "__main__": unittest.main() diff --git a/Lib/ctypes/test/test_random_things.py b/Lib/test/test_ctypes/test_random_things.py similarity index 73% rename from Lib/ctypes/test/test_random_things.py rename to Lib/test/test_ctypes/test_random_things.py index 2988e275cf..3908eca092 100644 --- a/Lib/ctypes/test/test_random_things.py +++ b/Lib/test/test_ctypes/test_random_things.py @@ -1,30 +1,34 @@ -from ctypes import * +import _ctypes import contextlib -from test import support -import unittest +import ctypes import sys +import unittest +from test import support +from ctypes import CFUNCTYPE, c_void_p, c_char_p, c_int, c_double def callback_func(arg): 42 / arg raise ValueError(arg) + @unittest.skipUnless(sys.platform == "win32", 'Windows-specific test') class call_function_TestCase(unittest.TestCase): # _ctypes.call_function is deprecated and private, but used by # Gary Bishp's readline module. If we have it, we must test it as well. def test(self): - from _ctypes import call_function - windll.kernel32.LoadLibraryA.restype = c_void_p - windll.kernel32.GetProcAddress.argtypes = c_void_p, c_char_p - windll.kernel32.GetProcAddress.restype = c_void_p + kernel32 = ctypes.windll.kernel32 + kernel32.LoadLibraryA.restype = c_void_p + kernel32.GetProcAddress.argtypes = c_void_p, c_char_p + kernel32.GetProcAddress.restype = c_void_p + + hdll = kernel32.LoadLibraryA(b"kernel32") + funcaddr = kernel32.GetProcAddress(hdll, b"GetModuleHandleA") - hdll = windll.kernel32.LoadLibraryA(b"kernel32") - funcaddr = windll.kernel32.GetProcAddress(hdll, b"GetModuleHandleA") + self.assertEqual(_ctypes.call_function(funcaddr, (None,)), + kernel32.GetModuleHandleA(None)) - self.assertEqual(call_function(funcaddr, (None,)), - windll.kernel32.GetModuleHandleA(None)) class CallbackTracbackTestCase(unittest.TestCase): # When an exception is raised in a ctypes callback function, the C @@ -47,9 +51,9 @@ def expect_unraisable(self, exc_type, exc_msg=None): if exc_msg is not None: self.assertEqual(str(cm.unraisable.exc_value), exc_msg) self.assertEqual(cm.unraisable.err_msg, - "Exception ignored on calling ctypes " - "callback function") - self.assertIs(cm.unraisable.object, callback_func) + f"Exception ignored on calling ctypes " + f"callback function {callback_func!r}") + self.assertIsNone(cm.unraisable.object) def test_ValueError(self): cb = CFUNCTYPE(c_int, c_int)(callback_func) @@ -66,6 +70,8 @@ def test_FloatDivisionError(self): with self.expect_unraisable(ZeroDivisionError): cb(0.0) + # TODO: RUSTPYTHON + @unittest.expectedFailure def test_TypeErrorDivisionError(self): cb = CFUNCTYPE(c_int, c_char_p)(callback_func) err_msg = "unsupported operand type(s) for /: 'int' and 'bytes'" diff --git a/Lib/ctypes/test/test_refcounts.py b/Lib/test/test_ctypes/test_refcounts.py similarity index 57% rename from Lib/ctypes/test/test_refcounts.py rename to Lib/test/test_ctypes/test_refcounts.py index 48958cd2a6..9e87cfc661 100644 --- a/Lib/ctypes/test/test_refcounts.py +++ b/Lib/test/test_ctypes/test_refcounts.py @@ -1,60 +1,59 @@ -import unittest -from test import support import ctypes import gc +import sys +import unittest +from test import support +from test.support import import_helper +from test.support import script_helper +_ctypes_test = import_helper.import_module("_ctypes_test") + MyCallback = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_int) OtherCallback = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_int, ctypes.c_ulonglong) -import _ctypes_test dll = ctypes.CDLL(_ctypes_test.__file__) -class RefcountTestCase(unittest.TestCase): +class RefcountTestCase(unittest.TestCase): @support.refcount_test def test_1(self): - from sys import getrefcount as grc - f = dll._testfunc_callback_i_if f.restype = ctypes.c_int f.argtypes = [ctypes.c_int, MyCallback] def callback(value): - #print "called back with", value return value - self.assertEqual(grc(callback), 2) + self.assertEqual(sys.getrefcount(callback), 2) cb = MyCallback(callback) - self.assertGreater(grc(callback), 2) + self.assertGreater(sys.getrefcount(callback), 2) result = f(-10, cb) self.assertEqual(result, -18) cb = None gc.collect() - self.assertEqual(grc(callback), 2) - + self.assertEqual(sys.getrefcount(callback), 2) @support.refcount_test def test_refcount(self): - from sys import getrefcount as grc def func(*args): pass # this is the standard refcount for func - self.assertEqual(grc(func), 2) + self.assertEqual(sys.getrefcount(func), 2) # the CFuncPtr instance holds at least one refcount on func: f = OtherCallback(func) - self.assertGreater(grc(func), 2) + self.assertGreater(sys.getrefcount(func), 2) # and may release it again del f - self.assertGreaterEqual(grc(func), 2) + self.assertGreaterEqual(sys.getrefcount(func), 2) # but now it must be gone gc.collect() - self.assertEqual(grc(func), 2) + self.assertEqual(sys.getrefcount(func), 2) class X(ctypes.Structure): _fields_ = [("a", OtherCallback)] @@ -62,32 +61,31 @@ class X(ctypes.Structure): x.a = OtherCallback(func) # the CFuncPtr instance holds at least one refcount on func: - self.assertGreater(grc(func), 2) + self.assertGreater(sys.getrefcount(func), 2) # and may release it again del x - self.assertGreaterEqual(grc(func), 2) + self.assertGreaterEqual(sys.getrefcount(func), 2) # and now it must be gone again gc.collect() - self.assertEqual(grc(func), 2) + self.assertEqual(sys.getrefcount(func), 2) f = OtherCallback(func) # the CFuncPtr instance holds at least one refcount on func: - self.assertGreater(grc(func), 2) + self.assertGreater(sys.getrefcount(func), 2) # create a cycle f.cycle = f del f gc.collect() - self.assertEqual(grc(func), 2) + self.assertEqual(sys.getrefcount(func), 2) + class AnotherLeak(unittest.TestCase): def test_callback(self): - import sys - proto = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_int, ctypes.c_int) def func(a, b): return a * b * 2 @@ -112,5 +110,34 @@ def func(): for _ in range(10000): func() + +class ModuleIsolationTest(unittest.TestCase): + def test_finalize(self): + # check if gc_decref() succeeds + script = ( + "import ctypes;" + "import sys;" + "del sys.modules['_ctypes'];" + "import _ctypes;" + "exit()" + ) + script_helper.assert_python_ok("-c", script) + + +class PyObjectRestypeTest(unittest.TestCase): + def test_restype_py_object_with_null_return(self): + # Test that a function which returns a NULL PyObject * + # without setting an exception does not crash. + PyErr_Occurred = ctypes.pythonapi.PyErr_Occurred + PyErr_Occurred.argtypes = [] + PyErr_Occurred.restype = ctypes.py_object + + # At this point, there's no exception set, so PyErr_Occurred + # returns NULL. Given the restype is py_object, the + # ctypes machinery will raise a custom error. + with self.assertRaisesRegex(ValueError, "PyObject is NULL"): + PyErr_Occurred() + + if __name__ == '__main__': unittest.main() diff --git a/Lib/ctypes/test/test_repr.py b/Lib/test/test_ctypes/test_repr.py similarity index 80% rename from Lib/ctypes/test/test_repr.py rename to Lib/test/test_ctypes/test_repr.py index 60a2c80345..e7587984a9 100644 --- a/Lib/ctypes/test/test_repr.py +++ b/Lib/test/test_ctypes/test_repr.py @@ -1,5 +1,8 @@ -from ctypes import * import unittest +from ctypes import (c_byte, c_short, c_int, c_long, c_longlong, + c_ubyte, c_ushort, c_uint, c_ulong, c_ulonglong, + c_float, c_double, c_longdouble, c_bool, c_char) + subclasses = [] for base in [c_byte, c_short, c_int, c_long, c_longlong, @@ -9,11 +12,12 @@ class X(base): pass subclasses.append(X) + class X(c_char): pass -# This test checks if the __repr__ is correct for subclasses of simple types +# This test checks if the __repr__ is correct for subclasses of simple types class ReprTest(unittest.TestCase): def test_numbers(self): for typ in subclasses: @@ -25,5 +29,6 @@ def test_char(self): self.assertEqual("c_char(b'x')", repr(c_char(b'x'))) self.assertEqual(" +# for reference. +# +# Tests also work on POSIX -from ctypes import * +import unittest +from ctypes import POINTER, cast, c_int16 from ctypes import wintypes @@ -38,6 +40,22 @@ def test_variant_bool(self): vb.value = [] self.assertIs(vb.value, False) + def assertIsSigned(self, ctype): + self.assertLess(ctype(-1).value, 0) + + def assertIsUnsigned(self, ctype): + self.assertGreater(ctype(-1).value, 0) + + def test_signedness(self): + for ctype in (wintypes.BYTE, wintypes.WORD, wintypes.DWORD, + wintypes.BOOLEAN, wintypes.UINT, wintypes.ULONG): + with self.subTest(ctype=ctype): + self.assertIsUnsigned(ctype) + + for ctype in (wintypes.BOOL, wintypes.INT, wintypes.LONG): + with self.subTest(ctype=ctype): + self.assertIsSigned(ctype) + if __name__ == "__main__": unittest.main() diff --git a/Lib/test/test_exceptions.py b/Lib/test/test_exceptions.py index 61f4156dc6..3db9203602 100644 --- a/Lib/test/test_exceptions.py +++ b/Lib/test/test_exceptions.py @@ -430,6 +430,7 @@ def test_WindowsError(self): @unittest.skipUnless(sys.platform == 'win32', 'test specific to Windows') + @unittest.expectedFailure # TODO: RUSTPYTHON def test_windows_message(self): """Should fill in unknown error code in Windows error message""" ctypes = import_module('ctypes') diff --git a/Lib/test/test_os.py b/Lib/test/test_os.py index 939315379f..bb558524c2 100644 --- a/Lib/test/test_os.py +++ b/Lib/test/test_os.py @@ -939,7 +939,6 @@ def get_file_system(self, path): return buf.value # return None if the filesystem is unknown - @unittest.expectedFailureIfWindows("TODO: RUSTPYTHON; (ModuleNotFoundError: No module named '_ctypes')") def test_large_time(self): # Many filesystems are limited to the year 2038. At least, the test # pass with NTFS filesystem. @@ -2712,12 +2711,10 @@ def _kill(self, sig): os.kill(proc.pid, sig) self.assertEqual(proc.wait(), sig) - @unittest.expectedFailureIfWindows("TODO: RUSTPYTHON; (ModuleNotFoundError: No module named '_ctypes')") def test_kill_sigterm(self): # SIGTERM doesn't mean anything special, but make sure it works self._kill(signal.SIGTERM) - @unittest.expectedFailureIfWindows("TODO: RUSTPYTHON; (ModuleNotFoundError: No module named '_ctypes')") def test_kill_int(self): # os.kill on Windows can take an int which gets set as the exit code self._kill(100) diff --git a/Lib/test/test_sysconfig.py b/Lib/test/test_sysconfig.py index 965780668c..82c11bdf7e 100644 --- a/Lib/test/test_sysconfig.py +++ b/Lib/test/test_sysconfig.py @@ -598,7 +598,6 @@ def test_android_ext_suffix(self): self.assertTrue(suffix.endswith(f"-{expected_triplet}.so"), f"{machine=}, {suffix=}") - @unittest.expectedFailure # TODO: RUSTPYTHON @unittest.skipUnless(sys.platform == 'darwin', 'OS X-specific test') def test_osx_ext_suffix(self): suffix = sysconfig.get_config_var('EXT_SUFFIX') diff --git a/crates/vm/src/stdlib/ctypes.rs b/crates/vm/src/stdlib/ctypes.rs index 3fdb2df610..9b92223043 100644 --- a/crates/vm/src/stdlib/ctypes.rs +++ b/crates/vm/src/stdlib/ctypes.rs @@ -95,6 +95,7 @@ pub(crate) fn make_module(vm: &VirtualMachine) -> PyRef { pointer::PyCPointerType::make_class(ctx); structure::PyCStructType::make_class(ctx); union::PyCUnionType::make_class(ctx); + function::PyCFuncPtrType::make_class(ctx); extend_module!(vm, &module, { "_CData" => PyCData::make_class(ctx), "_SimpleCData" => PyCSimple::make_class(ctx), @@ -385,12 +386,8 @@ pub(crate) mod _ctypes { #[pyattr] const RTLD_GLOBAL: i32 = 0; - #[cfg(target_os = "windows")] - #[pyattr] - const SIZEOF_TIME_T: usize = 8; - #[cfg(not(target_os = "windows"))] #[pyattr] - const SIZEOF_TIME_T: usize = 4; + const SIZEOF_TIME_T: usize = std::mem::size_of::(); #[pyattr] const CTYPES_MAX_ARGCOUNT: usize = 1024; @@ -578,30 +575,42 @@ pub(crate) mod _ctypes { #[pyfunction(name = "dlopen")] fn load_library_unix( name: Option, - _load_flags: OptionalArg, + load_flags: OptionalArg, vm: &VirtualMachine, ) -> PyResult { - // TODO: audit functions first - // TODO: load_flags + // Default mode: RTLD_NOW | RTLD_LOCAL, always force RTLD_NOW + let mode = load_flags.unwrap_or(libc::RTLD_NOW | libc::RTLD_LOCAL) | libc::RTLD_NOW; + match name { Some(name) => { let cache = library::libcache(); let mut cache_write = cache.write(); let os_str = name.as_os_str(vm)?; - let (id, _) = cache_write.get_or_insert_lib(&*os_str, vm).map_err(|e| { - // Include filename in error message for better diagnostics - let name_str = os_str.to_string_lossy(); - vm.new_os_error(format!("{}: {}", name_str, e)) - })?; + let (id, _) = cache_write + .get_or_insert_lib_with_mode(&*os_str, mode, vm) + .map_err(|e| { + let name_str = os_str.to_string_lossy(); + vm.new_os_error(format!("{}: {}", name_str, e)) + })?; Ok(id) } None => { - // If None, call libc::dlopen(null, mode) to get the current process handle - let handle = unsafe { libc::dlopen(std::ptr::null(), libc::RTLD_NOW) }; + // dlopen(NULL, mode) to get the current process handle (for pythonapi) + let handle = unsafe { libc::dlopen(std::ptr::null(), mode) }; if handle.is_null() { - return Err(vm.new_os_error("dlopen() error")); + let err = unsafe { libc::dlerror() }; + let msg = if err.is_null() { + "dlopen() error".to_string() + } else { + unsafe { std::ffi::CStr::from_ptr(err).to_string_lossy().into_owned() } + }; + return Err(vm.new_os_error(msg)); } - Ok(handle as usize) + // Add to library cache so symbol lookup works + let cache = library::libcache(); + let mut cache_write = cache.write(); + let id = cache_write.insert_raw_handle(handle); + Ok(id) } } } @@ -614,6 +623,48 @@ pub(crate) mod _ctypes { Ok(()) } + #[cfg(not(windows))] + #[pyfunction] + fn dlclose(handle: usize, _vm: &VirtualMachine) -> PyResult<()> { + // Remove from cache, which triggers SharedLibrary drop. + // libloading::Library calls dlclose automatically on Drop. + let cache = library::libcache(); + let mut cache_write = cache.write(); + cache_write.drop_lib(handle); + Ok(()) + } + + #[cfg(not(windows))] + #[pyfunction] + fn dlsym( + handle: usize, + name: crate::builtins::PyStrRef, + vm: &VirtualMachine, + ) -> PyResult { + let symbol_name = std::ffi::CString::new(name.as_str()) + .map_err(|_| vm.new_value_error("symbol name contains null byte"))?; + + // Clear previous error + unsafe { libc::dlerror() }; + + let ptr = unsafe { libc::dlsym(handle as *mut libc::c_void, symbol_name.as_ptr()) }; + + // Check for error via dlerror first + let err = unsafe { libc::dlerror() }; + if !err.is_null() { + let msg = unsafe { std::ffi::CStr::from_ptr(err).to_string_lossy().into_owned() }; + return Err(vm.new_os_error(msg)); + } + + // Treat NULL symbol address as error + // This handles cases like GNU IFUNCs that resolve to NULL + if ptr.is_null() { + return Err(vm.new_os_error(format!("symbol '{}' not found", name.as_str()))); + } + + Ok(ptr as usize) + } + #[pyfunction(name = "POINTER")] fn create_pointer_type(cls: PyObjectRef, vm: &VirtualMachine) -> PyResult { use crate::builtins::PyStr; @@ -905,25 +956,24 @@ pub(crate) mod _ctypes { #[pyfunction] fn get_errno() -> i32 { - errno::errno().0 + super::function::get_errno_value() } #[pyfunction] - fn set_errno(value: i32) { - errno::set_errno(errno::Errno(value)); + fn set_errno(value: i32) -> i32 { + super::function::set_errno_value(value) } #[cfg(windows)] #[pyfunction] fn get_last_error() -> PyResult { - Ok(unsafe { windows_sys::Win32::Foundation::GetLastError() }) + Ok(super::function::get_last_error_value()) } #[cfg(windows)] #[pyfunction] - fn set_last_error(value: u32) -> PyResult<()> { - unsafe { windows_sys::Win32::Foundation::SetLastError(value) }; - Ok(()) + fn set_last_error(value: u32) -> u32 { + super::function::set_last_error_value(value) } #[pyattr] @@ -1084,9 +1134,9 @@ pub(crate) mod _ctypes { ffi_args.push(Arg::new(val)); } - let cif = Cif::new(arg_types, Type::isize()); + let cif = Cif::new(arg_types, Type::c_int()); let code_ptr = CodePtr::from_ptr(func_addr as *const _); - let result: isize = unsafe { cif.call(code_ptr, &ffi_args) }; + let result: libc::c_int = unsafe { cif.call(code_ptr, &ffi_args) }; Ok(vm.ctx.new_int(result).into()) } diff --git a/crates/vm/src/stdlib/ctypes/array.rs b/crates/vm/src/stdlib/ctypes/array.rs index 60e6516bfe..208b3e3f4d 100644 --- a/crates/vm/src/stdlib/ctypes/array.rs +++ b/crates/vm/src/stdlib/ctypes/array.rs @@ -1,9 +1,12 @@ use super::StgInfo; use super::base::{CDATA_BUFFER_METHODS, PyCData}; +use super::type_info; use crate::{ AsObject, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, TryFromObject, VirtualMachine, atomic_func, - builtins::{PyBytes, PyInt, PyList, PySlice, PyStr, PyType, PyTypeRef}, + builtins::{ + PyBytes, PyInt, PyList, PySlice, PyStr, PyType, PyTypeRef, genericalias::PyGenericAlias, + }, class::StaticType, function::{ArgBytesLike, FuncArgs, PySetterValue}, protocol::{BufferDescriptor, PyBuffer, PyNumberMethods, PySequenceMethods}, @@ -11,6 +14,19 @@ use crate::{ }; use num_traits::{Signed, ToPrimitive}; +/// Get itemsize from a PEP 3118 format string +/// Extracts the type code (last char after endianness prefix) and returns its size +fn get_size_from_format(fmt: &str) -> usize { + // Format is like "q", etc. - strip endianness prefix and get type code + let code = fmt + .trim_start_matches(['<', '>', '@', '=', '!', '&']) + .chars() + .next() + .map(|c| c.to_string()); + code.map(|c| type_info(&c).map(|t| t.size).unwrap_or(1)) + .unwrap_or(1) +} + /// Creates array type for (element_type, length) /// Uses _array_type_cache to ensure identical calls return the same type object pub(super) fn array_type_from_ctype( @@ -444,6 +460,11 @@ impl AsSequence for PyCArray { with(Constructor, AsSequence, AsBuffer) )] impl PyCArray { + #[pyclassmethod] + fn __class_getitem__(cls: PyTypeRef, args: PyObjectRef, vm: &VirtualMachine) -> PyGenericAlias { + PyGenericAlias::from_args(cls, args, vm) + } + fn int_to_bytes(i: &malachite_bigint::BigInt, size: usize) -> Vec { // Try unsigned first (handles values like 0xFFFFFFFF that overflow signed) // then fall back to signed (handles negative values) @@ -1056,19 +1077,30 @@ impl AsBuffer for PyCArray { .expect("PyCArray type must have StgInfo"); let format = stg_info.format.clone(); let shape = stg_info.shape.clone(); - let element_size = stg_info.element_size; let desc = if let Some(fmt) = format && !shape.is_empty() { + // itemsize is the size of the base element type (item_info->size) + // For empty arrays, we still need the element size, not 0 + let total_elements: usize = shape.iter().product(); + let has_zero_dim = shape.contains(&0); + let itemsize = if total_elements > 0 && buffer_len > 0 { + buffer_len / total_elements + } else { + // For empty arrays, get itemsize from format type code + get_size_from_format(&fmt) + }; + // Build dim_desc from shape (C-contiguous: row-major order) // stride[i] = product(shape[i+1:]) * itemsize + // For empty arrays (any dimension is 0), all strides are 0 let mut dim_desc = Vec::with_capacity(shape.len()); - let mut stride = element_size as isize; + let mut stride = itemsize as isize; - // Calculate strides from innermost to outermost dimension for &dim_size in shape.iter().rev() { - dim_desc.push((dim_size, stride, 0)); + let current_stride = if has_zero_dim { 0 } else { stride }; + dim_desc.push((dim_size, current_stride, 0)); stride *= dim_size as isize; } dim_desc.reverse(); @@ -1076,7 +1108,7 @@ impl AsBuffer for PyCArray { BufferDescriptor { len: buffer_len, readonly: false, - itemsize: element_size, + itemsize, format: std::borrow::Cow::Owned(fmt), dim_desc, } diff --git a/crates/vm/src/stdlib/ctypes/base.rs b/crates/vm/src/stdlib/ctypes/base.rs index 38c371346e..44793a2156 100644 --- a/crates/vm/src/stdlib/ctypes/base.rs +++ b/crates/vm/src/stdlib/ctypes/base.rs @@ -300,15 +300,27 @@ pub(super) fn get_field_format( big_endian: bool, vm: &VirtualMachine, ) -> String { + let endian_prefix = if big_endian { ">" } else { "<" }; + // 1. Check StgInfo for format if let Some(type_obj) = field_type.downcast_ref::() && let Some(stg_info) = type_obj.stg_info_opt() && let Some(fmt) = &stg_info.format { - // Handle endian prefix for simple types - if fmt.len() == 1 { - let endian_prefix = if big_endian { ">" } else { "<" }; - return format!("{}{}", endian_prefix, fmt); + // For structures (T{...}), arrays ((n)...), and pointers (&...), return as-is + // These complex types have their own endianness markers inside + if fmt.starts_with('T') + || fmt.starts_with('(') + || fmt.starts_with('&') + || fmt.starts_with("X{") + { + return fmt.clone(); + } + + // For simple types, replace existing endian prefix with the correct one + let base_fmt = fmt.trim_start_matches(['<', '>', '@', '=', '!']); + if !base_fmt.is_empty() { + return format!("{}{}", endian_prefix, base_fmt); } return fmt.clone(); } @@ -318,8 +330,7 @@ pub(super) fn get_field_format( && let Some(type_str) = type_attr.downcast_ref::() { let s = type_str.as_str(); - if s.len() == 1 { - let endian_prefix = if big_endian { ">" } else { "<" }; + if !s.is_empty() { return format!("{}{}", endian_prefix, s); } return s.to_string(); @@ -1168,29 +1179,30 @@ impl PyCData { .ok_or_else(|| vm.new_value_error("Invalid library handle"))? }; - // Get symbol address using platform-specific API - let symbol_name = std::ffi::CString::new(name.as_str()) - .map_err(|_| vm.new_value_error("Invalid symbol name"))?; - - #[cfg(windows)] - let ptr: *const u8 = unsafe { - match windows_sys::Win32::System::LibraryLoader::GetProcAddress( - handle as windows_sys::Win32::Foundation::HMODULE, - symbol_name.as_ptr() as *const u8, - ) { - Some(p) => p as *const u8, - None => std::ptr::null(), + // Look up the library in the cache and use lib.get() for symbol lookup + let library_cache = super::library::libcache().read(); + let library = library_cache + .get_lib(handle) + .ok_or_else(|| vm.new_value_error("Library not found"))?; + let inner_lib = library.lib.lock(); + + let symbol_name_with_nul = format!("{}\0", name.as_str()); + let ptr: *const u8 = if let Some(lib) = &*inner_lib { + unsafe { + lib.get::<*const u8>(symbol_name_with_nul.as_bytes()) + .map(|sym| *sym) + .map_err(|_| { + vm.new_value_error(format!("symbol '{}' not found", name.as_str())) + })? } + } else { + return Err(vm.new_value_error("Library closed")); }; - #[cfg(not(windows))] - let ptr: *const u8 = - unsafe { libc::dlsym(handle as *mut libc::c_void, symbol_name.as_ptr()) as *const u8 }; - + // dlsym can return NULL for symbols that resolve to NULL (e.g., GNU IFUNC) + // Treat NULL addresses as errors if ptr.is_null() { - return Err( - vm.new_value_error(format!("symbol '{}' not found in library", name.as_str())) - ); + return Err(vm.new_value_error(format!("symbol '{}' not found", name.as_str()))); } // PyCData_AtAddress @@ -1593,7 +1605,7 @@ impl PyCField { /// PyCField_set #[pyslot] fn descr_set( - zelf: &crate::PyObject, + zelf: &PyObject, obj: PyObjectRef, value: PySetterValue, vm: &VirtualMachine, @@ -1804,7 +1816,7 @@ pub enum FfiArgValue { F64(f64), Pointer(usize), /// Pointer with owned data. The PyObjectRef keeps the pointed data alive. - OwnedPointer(usize, #[allow(dead_code)] crate::PyObjectRef), + OwnedPointer(usize, #[allow(dead_code)] PyObjectRef), } impl FfiArgValue { @@ -2145,6 +2157,16 @@ pub(super) fn read_ptr_from_buffer(buffer: &[u8]) -> usize { } } +/// Check if a type is a "simple instance" (direct subclass of a simple type) +/// Returns TRUE for c_int, c_void_p, etc. (simple types with _type_ attribute) +/// Returns FALSE for Structure, Array, POINTER(T), etc. +pub(super) fn is_simple_instance(typ: &Py) -> bool { + // _ctypes_simple_instance + // Check if the type's metaclass is PyCSimpleType + let metaclass = typ.class(); + metaclass.fast_issubclass(super::simple::PyCSimpleType::static_type()) +} + /// Set or initialize StgInfo on a type pub(super) fn set_or_init_stginfo(type_ref: &PyType, stg_info: StgInfo) { if type_ref.init_type_data(stg_info.clone()).is_err() diff --git a/crates/vm/src/stdlib/ctypes/field.rs b/crates/vm/src/stdlib/ctypes/field.rs deleted file mode 100644 index ea57d68065..0000000000 --- a/crates/vm/src/stdlib/ctypes/field.rs +++ /dev/null @@ -1,306 +0,0 @@ -use crate::builtins::PyType; -use crate::function::PySetterValue; -use crate::types::{GetDescriptor, Representable}; -use crate::{AsObject, Py, PyObject, PyObjectRef, PyResult, VirtualMachine}; -use num_traits::ToPrimitive; - -use super::structure::PyCStructure; -use super::union::PyCUnion; - -#[pyclass(name = "PyCFieldType", base = PyType, module = "_ctypes")] -#[derive(Debug)] -pub struct PyCFieldType { - pub _base: PyType, - #[allow(dead_code)] - pub(super) inner: PyCField, -} - -#[pyclass] -impl PyCFieldType {} - -#[pyclass(name = "CField", module = "_ctypes")] -#[derive(Debug, PyPayload)] -pub struct PyCField { - pub(super) byte_offset: usize, - pub(super) byte_size: usize, - #[allow(unused)] - pub(super) index: usize, - /// The ctypes type for this field (can be any ctypes type including arrays) - pub(super) proto: PyObjectRef, - pub(super) anonymous: bool, - pub(super) bitfield_size: bool, - pub(super) bit_offset: u8, - pub(super) name: String, -} - -impl PyCField { - pub fn new( - name: String, - proto: PyObjectRef, - byte_offset: usize, - byte_size: usize, - index: usize, - ) -> Self { - Self { - name, - proto, - byte_offset, - byte_size, - index, - anonymous: false, - bitfield_size: false, - bit_offset: 0, - } - } -} - -impl Representable for PyCField { - fn repr_str(zelf: &Py, vm: &VirtualMachine) -> PyResult { - // Get type name from the proto object - let tp_name = if let Some(name_attr) = vm - .ctx - .interned_str("__name__") - .and_then(|s| zelf.proto.get_attr(s, vm).ok()) - { - name_attr.str(vm)?.to_string() - } else { - zelf.proto.class().name().to_string() - }; - - if zelf.bitfield_size { - Ok(format!( - "<{} type={}, ofs={byte_offset}, bit_size={bitfield_size}, bit_offset={bit_offset}", - zelf.name, - tp_name, - byte_offset = zelf.byte_offset, - bitfield_size = zelf.bitfield_size, - bit_offset = zelf.bit_offset - )) - } else { - Ok(format!( - "<{} type={tp_name}, ofs={}, size={}", - zelf.name, zelf.byte_offset, zelf.byte_size - )) - } - } -} - -impl GetDescriptor for PyCField { - fn descr_get( - zelf: PyObjectRef, - obj: Option, - _cls: Option, - vm: &VirtualMachine, - ) -> PyResult { - let zelf = zelf - .downcast::() - .map_err(|_| vm.new_type_error("expected CField".to_owned()))?; - - // If obj is None, return the descriptor itself (class attribute access) - let obj = match obj { - Some(obj) if !vm.is_none(&obj) => obj, - _ => return Ok(zelf.into()), - }; - - // Instance attribute access - read value from the structure/union's buffer - if let Some(structure) = obj.downcast_ref::() { - let cdata = structure.cdata.read(); - let offset = zelf.byte_offset; - let size = zelf.byte_size; - - if offset + size <= cdata.buffer.len() { - let bytes = &cdata.buffer[offset..offset + size]; - return PyCField::bytes_to_value(bytes, size, vm); - } - } else if let Some(union) = obj.downcast_ref::() { - let cdata = union.cdata.read(); - let offset = zelf.byte_offset; - let size = zelf.byte_size; - - if offset + size <= cdata.buffer.len() { - let bytes = &cdata.buffer[offset..offset + size]; - return PyCField::bytes_to_value(bytes, size, vm); - } - } - - // Fallback: return 0 for uninitialized or unsupported types - Ok(vm.ctx.new_int(0).into()) - } -} - -impl PyCField { - /// Convert bytes to a Python value based on size - fn bytes_to_value(bytes: &[u8], size: usize, vm: &VirtualMachine) -> PyResult { - match size { - 1 => Ok(vm.ctx.new_int(bytes[0] as i8).into()), - 2 => { - let val = i16::from_ne_bytes([bytes[0], bytes[1]]); - Ok(vm.ctx.new_int(val).into()) - } - 4 => { - let val = i32::from_ne_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]); - Ok(vm.ctx.new_int(val).into()) - } - 8 => { - let val = i64::from_ne_bytes([ - bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7], - ]); - Ok(vm.ctx.new_int(val).into()) - } - _ => Ok(vm.ctx.new_int(0).into()), - } - } - - /// Convert a Python value to bytes - fn value_to_bytes(value: &PyObject, size: usize, vm: &VirtualMachine) -> PyResult> { - if let Ok(int_val) = value.try_int(vm) { - let i = int_val.as_bigint(); - match size { - 1 => { - let val = i.to_i8().unwrap_or(0); - Ok(val.to_ne_bytes().to_vec()) - } - 2 => { - let val = i.to_i16().unwrap_or(0); - Ok(val.to_ne_bytes().to_vec()) - } - 4 => { - let val = i.to_i32().unwrap_or(0); - Ok(val.to_ne_bytes().to_vec()) - } - 8 => { - let val = i.to_i64().unwrap_or(0); - Ok(val.to_ne_bytes().to_vec()) - } - _ => Ok(vec![0u8; size]), - } - } else { - Ok(vec![0u8; size]) - } - } -} - -#[pyclass( - flags(DISALLOW_INSTANTIATION, IMMUTABLETYPE), - with(Representable, GetDescriptor) -)] -impl PyCField { - #[pyslot] - fn descr_set( - zelf: &crate::PyObject, - obj: PyObjectRef, - value: PySetterValue, - vm: &VirtualMachine, - ) -> PyResult<()> { - let zelf = zelf - .downcast_ref::() - .ok_or_else(|| vm.new_type_error("expected CField".to_owned()))?; - - // Get the structure/union instance - use downcast_ref() to access the struct data - if let Some(structure) = obj.downcast_ref::() { - match value { - PySetterValue::Assign(value) => { - let offset = zelf.byte_offset; - let size = zelf.byte_size; - let bytes = PyCField::value_to_bytes(&value, size, vm)?; - - let mut cdata = structure.cdata.write(); - if offset + size <= cdata.buffer.len() { - cdata.buffer[offset..offset + size].copy_from_slice(&bytes); - } - Ok(()) - } - PySetterValue::Delete => { - Err(vm.new_type_error("cannot delete structure field".to_owned())) - } - } - } else if let Some(union) = obj.downcast_ref::() { - match value { - PySetterValue::Assign(value) => { - let offset = zelf.byte_offset; - let size = zelf.byte_size; - let bytes = PyCField::value_to_bytes(&value, size, vm)?; - - let mut cdata = union.cdata.write(); - if offset + size <= cdata.buffer.len() { - cdata.buffer[offset..offset + size].copy_from_slice(&bytes); - } - Ok(()) - } - PySetterValue::Delete => { - Err(vm.new_type_error("cannot delete union field".to_owned())) - } - } - } else { - Err(vm.new_type_error(format!( - "descriptor works only on Structure or Union instances, got {}", - obj.class().name() - ))) - } - } - - #[pymethod] - fn __set__( - zelf: PyObjectRef, - obj: PyObjectRef, - value: PyObjectRef, - vm: &VirtualMachine, - ) -> PyResult<()> { - Self::descr_set(&zelf, obj, PySetterValue::Assign(value), vm) - } - - #[pymethod] - fn __delete__(zelf: PyObjectRef, obj: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> { - Self::descr_set(&zelf, obj, PySetterValue::Delete, vm) - } - - #[pygetset] - fn size(&self) -> usize { - self.byte_size - } - - #[pygetset] - fn bit_size(&self) -> bool { - self.bitfield_size - } - - #[pygetset] - fn is_bitfield(&self) -> bool { - self.bitfield_size - } - - #[pygetset] - fn is_anonymous(&self) -> bool { - self.anonymous - } - - #[pygetset] - fn name(&self) -> String { - self.name.clone() - } - - #[pygetset(name = "type")] - fn type_(&self) -> PyObjectRef { - self.proto.clone() - } - - #[pygetset] - fn offset(&self) -> usize { - self.byte_offset - } - - #[pygetset] - fn byte_offset(&self) -> usize { - self.byte_offset - } - - #[pygetset] - fn byte_size(&self) -> usize { - self.byte_size - } - - #[pygetset] - fn bit_offset(&self) -> u8 { - self.bit_offset - } -} diff --git a/crates/vm/src/stdlib/ctypes/function.rs b/crates/vm/src/stdlib/ctypes/function.rs index 9bddb0ef0e..04ff238ebc 100644 --- a/crates/vm/src/stdlib/ctypes/function.rs +++ b/crates/vm/src/stdlib/ctypes/function.rs @@ -1,16 +1,19 @@ // spell-checker:disable use super::{ - _ctypes::CArgObject, PyCArray, PyCData, PyCPointer, PyCStructure, base::FfiArgValue, - simple::PyCSimple, type_info, + _ctypes::CArgObject, + PyCArray, PyCData, PyCPointer, PyCStructure, StgInfo, + base::{CDATA_BUFFER_METHODS, FfiArgValue, ParamFunc, StgInfoFlags}, + simple::PyCSimple, + type_info, }; use crate::{ AsObject, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine, builtins::{PyBytes, PyDict, PyNone, PyStr, PyTuple, PyType, PyTypeRef}, class::StaticType, - convert::ToPyObject, function::FuncArgs, - types::{Callable, Constructor, Representable}, + protocol::{BufferDescriptor, PyBuffer}, + types::{AsBuffer, Callable, Constructor, Initializer, Representable}, vm::thread::with_current_vm, }; use libffi::{ @@ -18,9 +21,10 @@ use libffi::{ middle::{Arg, Cif, Closure, CodePtr, Type}, }; use libloading::Symbol; -use num_traits::ToPrimitive; +use num_traits::{Signed, ToPrimitive}; use rustpython_common::lock::PyRwLock; -use std::ffi::{self, c_void}; +use std::borrow::Cow; +use std::ffi::c_void; use std::fmt::Debug; // Internal function addresses for special ctypes functions @@ -28,6 +32,90 @@ pub(super) const INTERNAL_CAST_ADDR: usize = 1; pub(super) const INTERNAL_STRING_AT_ADDR: usize = 2; pub(super) const INTERNAL_WSTRING_AT_ADDR: usize = 3; +// Thread-local errno storage for ctypes +std::thread_local! { + /// Thread-local storage for ctypes errno + /// This is separate from the system errno - ctypes swaps them during FFI calls + /// when use_errno=True is specified. + static CTYPES_LOCAL_ERRNO: std::cell::Cell = const { std::cell::Cell::new(0) }; +} + +/// Get ctypes thread-local errno value +pub(super) fn get_errno_value() -> i32 { + CTYPES_LOCAL_ERRNO.with(|e| e.get()) +} + +/// Set ctypes thread-local errno value, returns old value +pub(super) fn set_errno_value(value: i32) -> i32 { + CTYPES_LOCAL_ERRNO.with(|e| { + let old = e.get(); + e.set(value); + old + }) +} + +/// Save and restore errno around FFI call (called when use_errno=True) +/// Before: restore thread-local errno to system +/// After: save system errno to thread-local +#[cfg(not(windows))] +fn swap_errno(f: F) -> R +where + F: FnOnce() -> R, +{ + // Before call: restore thread-local errno to system + let saved = CTYPES_LOCAL_ERRNO.with(|e| e.get()); + errno::set_errno(errno::Errno(saved)); + + // Call the function + let result = f(); + + // After call: save system errno to thread-local + let new_error = errno::errno().0; + CTYPES_LOCAL_ERRNO.with(|e| e.set(new_error)); + + result +} + +#[cfg(windows)] +std::thread_local! { + /// Thread-local storage for ctypes last_error (Windows only) + static CTYPES_LOCAL_LAST_ERROR: std::cell::Cell = const { std::cell::Cell::new(0) }; +} + +#[cfg(windows)] +pub(super) fn get_last_error_value() -> u32 { + CTYPES_LOCAL_LAST_ERROR.with(|e| e.get()) +} + +#[cfg(windows)] +pub(super) fn set_last_error_value(value: u32) -> u32 { + CTYPES_LOCAL_LAST_ERROR.with(|e| { + let old = e.get(); + e.set(value); + old + }) +} + +/// Save and restore last_error around FFI call (called when use_last_error=True) +#[cfg(windows)] +fn save_and_restore_last_error(f: F) -> R +where + F: FnOnce() -> R, +{ + // Before call: restore thread-local last_error to Windows + let saved = CTYPES_LOCAL_LAST_ERROR.with(|e| e.get()); + unsafe { windows_sys::Win32::Foundation::SetLastError(saved) }; + + // Call the function + let result = f(); + + // After call: save Windows last_error to thread-local + let new_error = unsafe { windows_sys::Win32::Foundation::GetLastError() }; + CTYPES_LOCAL_LAST_ERROR.with(|e| e.set(new_error)); + + result +} + type FP = unsafe extern "C" fn(); /// Get FFI type for a ctypes type code @@ -131,11 +219,19 @@ fn convert_to_pointer(value: &PyObject, vm: &VirtualMachine) -> PyResult direct value + // 7. Integer -> direct value (PyLong_AsVoidPtr behavior) if let Ok(int_val) = value.try_int(vm) { - return Ok(FfiArgValue::Pointer( - int_val.as_bigint().to_usize().unwrap_or(0), - )); + let bigint = int_val.as_bigint(); + // Negative values: use signed conversion (allows -1 as 0xFFFF...) + if bigint.is_negative() { + if let Some(signed_val) = bigint.to_isize() { + return Ok(FfiArgValue::Pointer(signed_val as usize)); + } + } else if let Some(unsigned_val) = bigint.to_usize() { + return Ok(FfiArgValue::Pointer(unsigned_val)); + } + // Value out of range - raise OverflowError + return Err(vm.new_overflow_error("int too large to convert to pointer".to_string())); } // 8. Check _as_parameter_ attribute ( recursive ConvParam) @@ -150,46 +246,86 @@ fn convert_to_pointer(value: &PyObject, vm: &VirtualMachine) -> PyResult PyResult<(Type, FfiArgValue)> { +/// Returns an Argument with FFI type, value, and optional keep object +fn conv_param(value: &PyObject, vm: &VirtualMachine) -> PyResult { // 1. CArgObject (from byref() or paramfunc) -> use stored type and value if let Some(carg) = value.downcast_ref::() { let ffi_type = ffi_type_from_tag(carg.tag); - return Ok((ffi_type, carg.value.clone())); + return Ok(Argument { + ffi_type, + keep: None, + value: carg.value.clone(), + }); } // 2. None -> NULL pointer if value.is(&vm.ctx.none) { - return Ok((Type::pointer(), FfiArgValue::Pointer(0))); + return Ok(Argument { + ffi_type: Type::pointer(), + keep: None, + value: FfiArgValue::Pointer(0), + }); } // 3. ctypes objects -> use paramfunc if let Ok(carg) = super::base::call_paramfunc(value, vm) { let ffi_type = ffi_type_from_tag(carg.tag); - return Ok((ffi_type, carg.value.clone())); + return Ok(Argument { + ffi_type, + keep: None, + value: carg.value.clone(), + }); } - // 4. Python str -> pointer (use internal UTF-8 buffer) + // 4. Python str -> wide string pointer (like PyUnicode_AsWideCharString) if let Some(s) = value.downcast_ref::() { - let addr = s.as_str().as_ptr() as usize; - return Ok((Type::pointer(), FfiArgValue::Pointer(addr))); + // Convert to null-terminated UTF-16 (wide string) + let wide: Vec = s + .as_str() + .encode_utf16() + .chain(std::iter::once(0)) + .collect(); + let wide_bytes: Vec = wide.iter().flat_map(|&x| x.to_ne_bytes()).collect(); + let keep = vm.ctx.new_bytes(wide_bytes); + let addr = keep.as_bytes().as_ptr() as usize; + return Ok(Argument { + ffi_type: Type::pointer(), + keep: Some(keep.into()), + value: FfiArgValue::Pointer(addr), + }); } - // 9. Python bytes -> pointer to buffer + // 9. Python bytes -> null-terminated buffer pointer + // Need to ensure null termination like c_char_p if let Some(bytes) = value.downcast_ref::() { - let addr = bytes.as_bytes().as_ptr() as usize; - return Ok((Type::pointer(), FfiArgValue::Pointer(addr))); + let mut buffer = bytes.as_bytes().to_vec(); + buffer.push(0); // Add null terminator + let keep = vm.ctx.new_bytes(buffer); + let addr = keep.as_bytes().as_ptr() as usize; + return Ok(Argument { + ffi_type: Type::pointer(), + keep: Some(keep.into()), + value: FfiArgValue::Pointer(addr), + }); } // 10. Python int -> i32 (default integer type) if let Ok(int_val) = value.try_int(vm) { let val = int_val.as_bigint().to_i32().unwrap_or(0); - return Ok((Type::i32(), FfiArgValue::I32(val))); + return Ok(Argument { + ffi_type: Type::i32(), + keep: None, + value: FfiArgValue::I32(val), + }); } // 11. Python float -> f64 if let Ok(float_val) = value.try_float(vm) { - return Ok((Type::f64(), FfiArgValue::F64(float_val.to_f64()))); + return Ok(Argument { + ffi_type: Type::f64(), + keep: None, + value: FfiArgValue::F64(float_val.to_f64()), + }); } // 12. Check _as_parameter_ attribute @@ -245,7 +381,7 @@ impl ArgumentType for PyTypeRef { } fn convert_object(&self, value: PyObjectRef, vm: &VirtualMachine) -> PyResult { - // Call from_param first to convert the value (like CPython's callproc.c:1235) + // Call from_param first to convert the value // converter = PyTuple_GET_ITEM(argtypes, i); // v = PyObject_CallOneArg(converter, arg); let from_param = self @@ -264,11 +400,10 @@ impl ArgumentType for PyTypeRef { return Ok(FfiArgValue::Pointer(0)); } - // For pointer types (POINTER(T)), we need to pass the ADDRESS of the value's buffer + // 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(cdata) = converted.downcast_ref::() { - let addr = cdata.buffer.read().as_ptr() as usize; - return Ok(FfiArgValue::Pointer(addr)); + if let Some(pointer) = converted.downcast_ref::() { + return Ok(FfiArgValue::Pointer(pointer.get_ptr_value())); } return convert_to_pointer(&converted, vm); } @@ -305,12 +440,6 @@ impl ArgumentType for PyTypeRef { trait ReturnType { fn to_ffi_type(&self, vm: &VirtualMachine) -> Option; - #[allow(clippy::wrong_self_convention)] - fn from_ffi_type( - &self, - value: *mut ffi::c_void, - vm: &VirtualMachine, - ) -> PyResult>; } impl ReturnType for PyTypeRef { @@ -343,130 +472,56 @@ impl ReturnType for PyTypeRef { // Fallback to class name get_ffi_type(self.name().to_string().as_str()) } - - fn from_ffi_type( - &self, - value: *mut ffi::c_void, - vm: &VirtualMachine, - ) -> PyResult> { - // Get the type code from _type_ attribute (use get_attr to traverse MRO) - let type_code = self - .as_object() - .get_attr(vm.ctx.intern_str("_type_"), vm) - .ok() - .and_then(|t| t.downcast_ref::().map(|s| s.to_string())); - - let result = match type_code.as_deref() { - Some("b") => vm - .ctx - .new_int(unsafe { *(value as *const i8) } as i32) - .into(), - Some("B") => vm - .ctx - .new_int(unsafe { *(value as *const u8) } as i32) - .into(), - Some("c") => vm - .ctx - .new_bytes(vec![unsafe { *(value as *const u8) }]) - .into(), - Some("h") => vm - .ctx - .new_int(unsafe { *(value as *const i16) } as i32) - .into(), - Some("H") => vm - .ctx - .new_int(unsafe { *(value as *const u16) } as i32) - .into(), - Some("i") => vm.ctx.new_int(unsafe { *(value as *const i32) }).into(), - Some("I") => vm.ctx.new_int(unsafe { *(value as *const u32) }).into(), - Some("l") => vm - .ctx - .new_int(unsafe { *(value as *const libc::c_long) }) - .into(), - Some("L") => vm - .ctx - .new_int(unsafe { *(value as *const libc::c_ulong) }) - .into(), - Some("q") => vm - .ctx - .new_int(unsafe { *(value as *const libc::c_longlong) }) - .into(), - Some("Q") => vm - .ctx - .new_int(unsafe { *(value as *const libc::c_ulonglong) }) - .into(), - Some("f") => vm - .ctx - .new_float(unsafe { *(value as *const f32) } as f64) - .into(), - Some("d") => vm.ctx.new_float(unsafe { *(value as *const f64) }).into(), - Some("P") | Some("z") | Some("Z") => { - vm.ctx.new_int(unsafe { *(value as *const usize) }).into() - } - Some("?") => vm - .ctx - .new_bool(unsafe { *(value as *const u8) } != 0) - .into(), - None => { - // No _type_ attribute - check for Structure/Array types - // GetResult: PyCData_FromBaseObj creates instance from memory - if let Some(stg_info) = self.stg_info_opt() { - let size = stg_info.size; - // Create instance of the ctypes type - let instance = self.as_object().call((), vm)?; - - // Copy return value memory into instance buffer - // Use a block to properly scope the borrow - { - let src = unsafe { std::slice::from_raw_parts(value as *const u8, size) }; - if let Some(cdata) = instance.downcast_ref::() { - let mut buffer = cdata.buffer.write(); - if buffer.len() >= size { - buffer.to_mut()[..size].copy_from_slice(src); - } - } else if let Some(structure) = instance.downcast_ref::() { - let mut buffer = structure.0.buffer.write(); - if buffer.len() >= size { - buffer.to_mut()[..size].copy_from_slice(src); - } - } else if let Some(array) = instance.downcast_ref::() { - let mut buffer = array.0.buffer.write(); - if buffer.len() >= size { - buffer.to_mut()[..size].copy_from_slice(src); - } - } - } - return Ok(Some(instance)); - } - // Not a ctypes type - call type with int result - return self - .as_object() - .call((unsafe { *(value as *const i32) },), vm) - .map(Some); - } - _ => return Err(vm.new_type_error("Unsupported return type")), - }; - Ok(Some(result)) - } } impl ReturnType for PyNone { fn to_ffi_type(&self, _vm: &VirtualMachine) -> Option { get_ffi_type("void") } +} + +// PyCFuncPtrType - Metaclass for function pointer types +// PyCFuncPtrType_init + +#[pyclass(name = "PyCFuncPtrType", base = PyType, module = "_ctypes")] +#[derive(Debug)] +#[repr(transparent)] +pub(super) struct PyCFuncPtrType(PyType); - fn from_ffi_type( - &self, - _value: *mut ffi::c_void, - _vm: &VirtualMachine, - ) -> PyResult> { - Ok(None) +impl Initializer for PyCFuncPtrType { + type Args = FuncArgs; + + fn init(zelf: PyRef, _args: Self::Args, vm: &VirtualMachine) -> PyResult<()> { + let obj: PyObjectRef = zelf.clone().into(); + let new_type: PyTypeRef = obj + .downcast() + .map_err(|_| vm.new_type_error("expected type"))?; + + new_type.check_not_initialized(vm)?; + + let ptr_size = std::mem::size_of::(); + let mut stg_info = StgInfo::new(ptr_size, ptr_size); + stg_info.format = Some("X{}".to_string()); + stg_info.length = 1; + stg_info.flags |= StgInfoFlags::TYPEFLAG_ISPOINTER; + stg_info.paramfunc = ParamFunc::Pointer; // CFuncPtr is passed as a pointer + + let _ = new_type.init_type_data(stg_info); + Ok(()) } } +#[pyclass(flags(IMMUTABLETYPE), with(Initializer))] +impl PyCFuncPtrType {} + /// PyCFuncPtr - Function pointer instance /// Saved in _base.buffer -#[pyclass(module = "_ctypes", name = "CFuncPtr", base = PyCData)] +#[pyclass( + module = "_ctypes", + name = "CFuncPtr", + base = PyCData, + metaclass = "PyCFuncPtrType" +)] #[repr(C)] pub(super) struct PyCFuncPtr { pub _base: PyCData, @@ -892,7 +947,13 @@ impl Constructor for PyCFuncPtr { .map_err(|err| err.to_string()) .map_err(|err| vm.new_attribute_error(err))? }; - *pointer as usize + let addr = *pointer as usize; + // dlsym can return NULL for symbols that resolve to NULL (e.g., GNU IFUNC) + // Treat NULL addresses as errors + if addr == 0 { + return Err(vm.new_attribute_error(format!("function '{}' not found", name))); + } + addr } else { 0 }; @@ -921,12 +982,17 @@ impl Constructor for PyCFuncPtr { // Get argument types and result type from the class let class_argtypes = cls.get_attr(vm.ctx.intern_str("_argtypes_")); let class_restype = cls.get_attr(vm.ctx.intern_str("_restype_")); + let class_flags = cls + .get_attr(vm.ctx.intern_str("_flags_")) + .and_then(|f| f.try_to_value::(vm).ok()) + .unwrap_or(0); // Create the thunk (C-callable wrapper for the Python function) let thunk = PyCThunk::new( first_arg.clone(), class_argtypes.clone(), class_restype.clone(), + class_flags, vm, )?; let code_ptr = thunk.code_ptr(); @@ -1060,13 +1126,16 @@ fn extract_call_info(zelf: &Py, vm: &VirtualMachine) -> PyResult().ok()) - .and_then(|t| t.as_object().get_attr(vm.ctx.intern_str("_type_"), vm).ok()) - .and_then(|t| t.downcast_ref::().map(|s| s.to_string())) - .is_some_and(|tc| matches!(tc.as_str(), "P" | "z" | "Z")); + .and_then(|t| { + t.stg_info_opt() + .map(|info| info.flags.contains(StgInfoFlags::TYPEFLAG_ISPOINTER)) + }) + .unwrap_or(false); Ok(CallInfo { explicit_arg_types, @@ -1178,13 +1247,18 @@ fn resolve_com_method( Ok((Some(CodePtr(fptr as *mut _)), true)) } -/// Prepared arguments for FFI call -struct PreparedArgs { - ffi_arg_types: Vec, - ffi_values: Vec, - out_buffers: Vec<(usize, PyObjectRef)>, +/// Single argument for FFI call +// struct argument +struct Argument { + ffi_type: Type, + value: FfiArgValue, + #[allow(dead_code)] + keep: Option, // Object to keep alive during call } +/// Out buffers for paramflags OUT parameters +type OutBuffers = Vec<(usize, PyObjectRef)>; + /// Get buffer address from a ctypes object fn get_buffer_addr(obj: &PyObjectRef) -> Option { obj.downcast_ref::() @@ -1213,18 +1287,16 @@ fn create_out_buffer(arg_type: &PyTypeRef, vm: &VirtualMachine) -> PyResult PyResult { - let results: Vec<(Type, FfiArgValue)> = args +fn build_callargs_no_argtypes( + args: &FuncArgs, + vm: &VirtualMachine, +) -> PyResult<(Vec, OutBuffers)> { + let arguments: Vec = args .args .iter() .map(|arg| conv_param(arg, vm)) .collect::>>()?; - let (ffi_arg_types, ffi_values) = results.into_iter().unzip(); - Ok(PreparedArgs { - ffi_arg_types, - ffi_values, - out_buffers: Vec::new(), - }) + Ok((arguments, Vec::new())) } /// Build callargs for regular function with argtypes (no paramflags) @@ -1232,12 +1304,8 @@ fn build_callargs_simple( args: &FuncArgs, arg_types: &[PyTypeRef], vm: &VirtualMachine, -) -> PyResult { - let ffi_arg_types = arg_types - .iter() - .map(|t| ArgumentType::to_ffi_type(t, vm)) - .collect::>>()?; - let ffi_values = args +) -> PyResult<(Vec, OutBuffers)> { + let arguments: Vec = args .args .iter() .enumerate() @@ -1245,14 +1313,16 @@ fn build_callargs_simple( let arg_type = arg_types .get(n) .ok_or_else(|| vm.new_type_error("argument amount mismatch"))?; - arg_type.convert_object(arg.clone(), vm) + let ffi_type = ArgumentType::to_ffi_type(arg_type, vm)?; + let value = arg_type.convert_object(arg.clone(), vm)?; + Ok(Argument { + ffi_type, + keep: None, + value, + }) }) - .collect::, _>>()?; - Ok(PreparedArgs { - ffi_arg_types, - ffi_values, - out_buffers: Vec::new(), - }) + .collect::>>()?; + Ok((arguments, Vec::new())) } /// Build callargs with paramflags (handles IN/OUT parameters) @@ -1262,27 +1332,21 @@ fn build_callargs_with_paramflags( paramflags: &ParsedParamFlags, skip_first_arg: bool, // true for COM methods vm: &VirtualMachine, -) -> PyResult { - let mut ffi_arg_types = Vec::new(); - let mut ffi_values = Vec::new(); +) -> PyResult<(Vec, OutBuffers)> { + let mut arguments = Vec::new(); let mut out_buffers = Vec::new(); // For COM methods, first arg is self (pointer) let mut caller_arg_idx = if skip_first_arg { - ffi_arg_types.push(Type::pointer()); if !args.args.is_empty() { - ffi_values.push(conv_param(&args.args[0], vm)?.1); + let arg = conv_param(&args.args[0], vm)?; + arguments.push(arg); } 1usize } else { 0usize }; - // Add FFI types for all argtypes - for arg_type in arg_types { - ffi_arg_types.push(ArgumentType::to_ffi_type(arg_type, vm)?); - } - // Process parameters based on paramflags for (param_idx, (direction, _name, default)) in paramflags.iter().enumerate() { let arg_type = arg_types @@ -1292,13 +1356,19 @@ fn build_callargs_with_paramflags( let is_out = (*direction & 2) != 0; // OUT flag let is_in = (*direction & 1) != 0 || *direction == 0; // IN flag or default + let ffi_type = ArgumentType::to_ffi_type(arg_type, vm)?; + if is_out && !is_in { // Pure OUT parameter: create buffer, don't consume caller arg let buffer = create_out_buffer(arg_type, vm)?; let addr = get_buffer_addr(&buffer).ok_or_else(|| { vm.new_type_error("Cannot create OUT buffer for this type".to_string()) })?; - ffi_values.push(FfiArgValue::Pointer(addr)); + arguments.push(Argument { + ffi_type, + keep: None, + value: FfiArgValue::Pointer(addr), + }); out_buffers.push((param_idx, buffer)); } else { // IN or IN|OUT: get from caller args or default @@ -1315,15 +1385,16 @@ fn build_callargs_with_paramflags( // IN|OUT: track for return out_buffers.push((param_idx, arg.clone())); } - ffi_values.push(arg_type.convert_object(arg, vm)?); + let value = arg_type.convert_object(arg, vm)?; + arguments.push(Argument { + ffi_type, + keep: None, + value, + }); } } - Ok(PreparedArgs { - ffi_arg_types, - ffi_values, - out_buffers, - }) + Ok((arguments, out_buffers)) } /// Build call arguments (main dispatcher) @@ -1333,7 +1404,7 @@ fn build_callargs( paramflags: Option<&ParsedParamFlags>, is_com_method: bool, vm: &VirtualMachine, -) -> PyResult { +) -> PyResult<(Vec, OutBuffers)> { let Some(ref arg_types) = call_info.explicit_arg_types else { // No argtypes: use ConvParam return build_callargs_no_argtypes(args, vm); @@ -1344,28 +1415,23 @@ fn build_callargs( build_callargs_with_paramflags(args, arg_types, pflags, is_com_method, vm) } else if is_com_method { // COM method without paramflags - let mut ffi_types = vec![Type::pointer()]; - ffi_types.extend( - arg_types - .iter() - .map(|t| ArgumentType::to_ffi_type(t, vm)) - .collect::>>()?, - ); - let mut ffi_vals = Vec::new(); + let mut arguments = Vec::new(); if !args.args.is_empty() { - ffi_vals.push(conv_param(&args.args[0], vm)?.1); + arguments.push(conv_param(&args.args[0], vm)?); } for (n, arg) in args.args.iter().skip(1).enumerate() { let arg_type = arg_types .get(n) .ok_or_else(|| vm.new_type_error("argument amount mismatch"))?; - ffi_vals.push(arg_type.convert_object(arg.clone(), vm)?); + let ffi_type = ArgumentType::to_ffi_type(arg_type, vm)?; + let value = arg_type.convert_object(arg.clone(), vm)?; + arguments.push(Argument { + ffi_type, + keep: None, + value, + }); } - Ok(PreparedArgs { - ffi_arg_types: ffi_types, - ffi_values: ffi_vals, - out_buffers: Vec::new(), - }) + Ok((arguments, Vec::new())) } else { // Regular function build_callargs_simple(args, arg_types, vm) @@ -1380,12 +1446,10 @@ enum RawResult { } /// Execute FFI call -fn ctypes_callproc(code_ptr: CodePtr, prepared: &PreparedArgs, call_info: &CallInfo) -> RawResult { - let cif = Cif::new( - prepared.ffi_arg_types.clone(), - call_info.ffi_return_type.clone(), - ); - let ffi_args: Vec = prepared.ffi_values.iter().map(|v| v.as_arg()).collect(); +fn ctypes_callproc(code_ptr: CodePtr, arguments: &[Argument], call_info: &CallInfo) -> RawResult { + let ffi_arg_types: Vec = arguments.iter().map(|a| a.ffi_type.clone()).collect(); + let cif = Cif::new(ffi_arg_types, call_info.ffi_return_type.clone()); + let ffi_args: Vec = arguments.iter().map(|a| a.value.as_arg()).collect(); if call_info.restype_is_none { unsafe { cif.call::<()>(code_ptr, &ffi_args) }; @@ -1438,73 +1502,118 @@ fn check_hresult(hresult: i32, zelf: &Py, vm: &VirtualMachine) -> Py } /// Convert raw FFI result to Python object +// = GetResult fn convert_raw_result( raw_result: &mut RawResult, call_info: &CallInfo, vm: &VirtualMachine, ) -> Option { - match raw_result { - RawResult::Void => None, + // Get result as bytes for type conversion + let (result_bytes, result_size) = match raw_result { + RawResult::Void => return None, RawResult::Pointer(ptr) => { - // Get type code from restype to determine conversion method - let type_code = call_info - .restype_obj - .as_ref() - .and_then(|t| t.clone().downcast::().ok()) - .and_then(|t| t.as_object().get_attr(vm.ctx.intern_str("_type_"), vm).ok()) - .and_then(|t| t.downcast_ref::().map(|s| s.to_string())); - - match type_code.as_deref() { - Some("z") => { - // c_char_p: NULL -> None, otherwise read C string -> bytes - if *ptr == 0 { - Some(vm.ctx.none()) - } else { - let cstr = unsafe { std::ffi::CStr::from_ptr(*ptr as _) }; - Some(vm.ctx.new_bytes(cstr.to_bytes().to_vec()).into()) - } - } - Some("Z") => { - // c_wchar_p: NULL -> None, otherwise read wide string -> str - if *ptr == 0 { - Some(vm.ctx.none()) - } else { - let wstr_ptr = *ptr as *const libc::wchar_t; - let mut len = 0; - unsafe { - while *wstr_ptr.add(len) != 0 { - len += 1; - } - } - let slice = unsafe { std::slice::from_raw_parts(wstr_ptr, len) }; - let s: String = slice - .iter() - .filter_map(|&c| char::from_u32(c as u32)) - .collect(); - Some(vm.ctx.new_str(s).into()) - } - } - _ => { - // c_void_p ("P") and other pointer types: NULL -> None, otherwise int - if *ptr == 0 { - Some(vm.ctx.none()) - } else { - Some(vm.ctx.new_int(*ptr).into()) - } - } - } + let bytes = ptr.to_ne_bytes(); + (bytes.to_vec(), std::mem::size_of::()) } - RawResult::Value(val) => call_info - .restype_obj - .as_ref() - .and_then(|f| f.clone().downcast::().ok()) - .map(|f| { - f.from_ffi_type(val as *mut _ as *mut c_void, vm) - .ok() - .flatten() - }) - .unwrap_or_else(|| Some(vm.ctx.new_int(*val as usize).as_object().to_pyobject(vm))), + RawResult::Value(val) => { + let bytes = val.to_ne_bytes(); + (bytes.to_vec(), std::mem::size_of::()) + } + }; + + // 1. No restype → return as int + let restype = match &call_info.restype_obj { + None => { + // Default: return as int + let val = match raw_result { + RawResult::Pointer(p) => *p as isize, + RawResult::Value(v) => *v as isize, + RawResult::Void => return None, + }; + return Some(vm.ctx.new_int(val).into()); + } + Some(r) => r, + }; + + // 2. restype is None → return None + if restype.is(&vm.ctx.none()) { + return None; + } + + // 3. Get restype as PyType + let restype_type = match restype.clone().downcast::() { + Ok(t) => t, + Err(_) => { + // Not a type, call it with int result + let val = match raw_result { + RawResult::Pointer(p) => *p as isize, + RawResult::Value(v) => *v as isize, + RawResult::Void => return None, + }; + return restype.call((val,), vm).ok(); + } + }; + + // 4. Get StgInfo + let stg_info = restype_type.stg_info_opt(); + + // No StgInfo → call restype with int + if stg_info.is_none() { + let val = match raw_result { + RawResult::Pointer(p) => *p as isize, + RawResult::Value(v) => *v as isize, + RawResult::Void => return None, + }; + return restype_type.as_object().call((val,), vm).ok(); + } + + let info = stg_info.unwrap(); + + // 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 super::base::bytes_to_pyobject(&restype_type, &result_bytes, vm).ok(); + } + + // 6. Complex type → create ctypes instance (PyCData_FromBaseObj) + // This handles POINTER(T), Structure, Array, etc. + + // Special handling for POINTER(T) types - set pointer value directly + if info.flags.contains(StgInfoFlags::TYPEFLAG_ISPOINTER) + && info.proto.is_some() + && let RawResult::Pointer(ptr) = raw_result + && let Ok(instance) = restype_type.as_object().call((), vm) + { + if let Some(pointer) = instance.downcast_ref::() { + pointer.set_ptr_value(*ptr); + } + return Some(instance); } + + // Create instance and copy result data + pycdata_from_ffi_result(&restype_type, &result_bytes, result_size, vm).ok() +} + +/// Create a ctypes instance from FFI result (PyCData_FromBaseObj equivalent) +fn pycdata_from_ffi_result( + typ: &PyTypeRef, + result_bytes: &[u8], + size: usize, + vm: &VirtualMachine, +) -> PyResult { + // Create instance + let instance = PyType::call(typ, ().into(), vm)?; + + // Copy result data into instance buffer + if let Some(cdata) = instance.downcast_ref::() { + let mut buffer = cdata.buffer.write(); + let copy_size = size.min(buffer.len()).min(result_bytes.len()); + if copy_size > 0 { + buffer.to_mut()[..copy_size].copy_from_slice(&result_bytes[..copy_size]); + } + } + + Ok(instance) } /// Extract values from OUT buffers @@ -1522,7 +1631,7 @@ fn extract_out_values( fn build_result( mut raw_result: RawResult, call_info: &CallInfo, - prepared: PreparedArgs, + out_buffers: OutBuffers, zelf: &Py, args: &FuncArgs, vm: &VirtualMachine, @@ -1552,11 +1661,11 @@ fn build_result( } // Handle OUT parameter return values - if prepared.out_buffers.is_empty() { + if out_buffers.is_empty() { return result.map(Ok).unwrap_or_else(|| Ok(vm.ctx.none())); } - let out_values = extract_out_values(prepared.out_buffers, vm); + let out_values = extract_out_values(out_buffers, vm); Ok(match <[PyObjectRef; 1]>::try_from(out_values) { Ok([single]) => single, Err(v) => PyTuple::new_ref(v, &vm.ctx).into(), @@ -1584,23 +1693,43 @@ impl Callable for PyCFuncPtr { let paramflags = parse_paramflags(zelf, vm)?; // 5. Build call arguments - let prepared = build_callargs(&args, &call_info, paramflags.as_ref(), is_com_method, vm)?; + let (arguments, out_buffers) = + build_callargs(&args, &call_info, paramflags.as_ref(), is_com_method, vm)?; // 6. Get code pointer let code_ptr = match func_ptr.or_else(|| zelf.get_code_ptr()) { Some(cp) => cp, None => { debug_assert!(false, "NULL function pointer"); - // In release mode, this will crash like CPython + // In release mode, this will crash CodePtr(std::ptr::null_mut()) } }; - // 7. Call the function - let raw_result = ctypes_callproc(code_ptr, &prepared, &call_info); + // 7. Get flags to check for use_last_error/use_errno + let flags = PyCFuncPtr::_flags_(zelf, vm); - // 8. Build result - build_result(raw_result, &call_info, prepared, zelf, &args, vm) + // 8. Call the function (with use_last_error/use_errno handling) + #[cfg(not(windows))] + let raw_result = { + if flags & super::base::StgInfoFlags::FUNCFLAG_USE_ERRNO.bits() != 0 { + swap_errno(|| ctypes_callproc(code_ptr, &arguments, &call_info)) + } else { + ctypes_callproc(code_ptr, &arguments, &call_info) + } + }; + + #[cfg(windows)] + let raw_result = { + if flags & super::base::StgInfoFlags::FUNCFLAG_USE_LASTERROR.bits() != 0 { + save_and_restore_last_error(|| ctypes_callproc(code_ptr, &arguments, &call_info)) + } else { + ctypes_callproc(code_ptr, &arguments, &call_info) + } + }; + + // 9. Build result + build_result(raw_result, &call_info, out_buffers, zelf, &args, vm) } } @@ -1614,7 +1743,36 @@ impl Representable for PyCFuncPtr { } } -#[pyclass(flags(BASETYPE), with(Callable, Constructor, Representable))] +// PyCData_NewGetBuffer +impl AsBuffer for PyCFuncPtr { + fn as_buffer(zelf: &Py, _vm: &VirtualMachine) -> PyResult { + // CFuncPtr types may not have StgInfo if PyCFuncPtrType metaclass is not used + // Use default values for function pointers: format="X{}", size=sizeof(pointer) + let (format, itemsize) = if let Some(stg_info) = zelf.class().stg_info_opt() { + ( + stg_info + .format + .clone() + .map(Cow::Owned) + .unwrap_or(Cow::Borrowed("X{}")), + stg_info.size, + ) + } else { + (Cow::Borrowed("X{}"), std::mem::size_of::()) + }; + let desc = BufferDescriptor { + len: itemsize, + readonly: false, + itemsize, + format, + dim_desc: vec![], + }; + let buf = PyBuffer::new(zelf.to_owned().into(), desc, &CDATA_BUFFER_METHODS); + Ok(buf) + } +} + +#[pyclass(flags(BASETYPE), with(Callable, Constructor, Representable, AsBuffer))] impl PyCFuncPtr { // restype getter/setter #[pygetset] @@ -1685,7 +1843,6 @@ impl PyCFuncPtr { } // Fallback to StgInfo for native types - use super::base::StgInfoFlags; zelf.class() .stg_info_opt() .map(|stg| stg.flags.bits()) @@ -1708,7 +1865,9 @@ struct ThunkUserData { /// Argument types for conversion arg_types: Vec, /// Result type for conversion (None means void) - res_type: Option, + pub res_type: Option, + /// Function flags (FUNCFLAG_USE_ERRNO, etc.) + pub flags: u32, } /// Check if ty is a subclass of a simple type (like MyInt(c_int)). @@ -1758,11 +1917,23 @@ fn ffi_to_python(ty: &Py, ptr: *const c_void, vm: &VirtualMachine) -> Py len += 1; } let slice = std::slice::from_raw_parts(wstr_ptr, len); - let s: String = slice - .iter() - .filter_map(|&c| char::from_u32(c as u32)) - .collect(); - vm.ctx.new_str(s).into() + // Windows: wchar_t = u16 (UTF-16) -> use Wtf8Buf::from_wide + // Unix: wchar_t = i32 (UTF-32) -> convert via char::from_u32 + #[cfg(windows)] + { + use rustpython_common::wtf8::Wtf8Buf; + let wide: Vec = slice.to_vec(); + let wtf8 = Wtf8Buf::from_wide(&wide); + vm.ctx.new_str(wtf8).into() + } + #[cfg(not(windows))] + { + let s: String = slice + .iter() + .filter_map(|&c| char::from_u32(c as u32)) + .collect(); + vm.ctx.new_str(s).into() + } } } Some("P") => vm.ctx.new_int(*(ptr as *const usize)).into(), @@ -1865,6 +2036,16 @@ unsafe extern "C" fn thunk_callback( userdata: &ThunkUserData, ) { with_current_vm(|vm| { + // Swap errno before call if FUNCFLAG_USE_ERRNO is set + let use_errno = userdata.flags & StgInfoFlags::FUNCFLAG_USE_ERRNO.bits() != 0; + let saved_errno = if use_errno { + let current = rustpython_common::os::get_errno(); + // TODO: swap with ctypes stored errno (thread-local) + Some(current) + } else { + None + }; + let py_args: Vec = userdata .arg_types .iter() @@ -1877,6 +2058,15 @@ unsafe extern "C" fn thunk_callback( let py_result = userdata.callable.call(py_args, vm); + // Swap errno back after call + if use_errno { + let _current = rustpython_common::os::get_errno(); + // TODO: store current errno to ctypes storage + if let Some(saved) = saved_errno { + rustpython_common::os::set_errno(saved); + } + } + // Call unraisable hook if exception occurred if let Err(exc) = &py_result { let repr = userdata @@ -1935,6 +2125,7 @@ impl PyCThunk { callable: PyObjectRef, arg_types: Option, res_type: Option, + flags: u32, vm: &VirtualMachine, ) -> PyResult { let arg_type_vec: Vec = match arg_types { @@ -1979,6 +2170,7 @@ impl PyCThunk { callable: callable.clone(), arg_types: arg_type_vec, res_type: res_type_ref, + flags, }); let userdata_ptr = Box::into_raw(userdata); let userdata_ref: &'static ThunkUserData = unsafe { &*userdata_ptr }; diff --git a/crates/vm/src/stdlib/ctypes/library.rs b/crates/vm/src/stdlib/ctypes/library.rs index ec8ca91af0..7512ce29d8 100644 --- a/crates/vm/src/stdlib/ctypes/library.rs +++ b/crates/vm/src/stdlib/ctypes/library.rs @@ -2,12 +2,14 @@ use crate::VirtualMachine; use libloading::Library; use rustpython_common::lock::{PyMutex, PyRwLock}; use std::collections::HashMap; -use std::ffi::{OsStr, c_void}; +use std::ffi::OsStr; use std::fmt; -use std::ptr::null; -pub(super) struct SharedLibrary { - pub(super) lib: PyMutex>, +#[cfg(unix)] +use libloading::os::unix::Library as UnixLibrary; + +pub struct SharedLibrary { + pub(crate) lib: PyMutex>, } impl fmt::Debug for SharedLibrary { @@ -17,18 +19,44 @@ impl fmt::Debug for SharedLibrary { } impl SharedLibrary { - fn new(name: impl AsRef) -> Result { + #[cfg(windows)] + pub fn new(name: impl AsRef) -> Result { Ok(SharedLibrary { lib: PyMutex::new(unsafe { Some(Library::new(name.as_ref())?) }), }) } - fn get_pointer(&self) -> usize { + #[cfg(unix)] + pub fn new_with_mode( + name: impl AsRef, + mode: i32, + ) -> Result { + Ok(SharedLibrary { + lib: PyMutex::new(Some(unsafe { + UnixLibrary::open(Some(name.as_ref()), mode)?.into() + })), + }) + } + + /// Create a SharedLibrary from a raw dlopen handle (for pythonapi / dlopen(NULL)) + #[cfg(unix)] + pub fn from_raw_handle(handle: *mut libc::c_void) -> SharedLibrary { + SharedLibrary { + lib: PyMutex::new(Some(unsafe { UnixLibrary::from_raw(handle).into() })), + } + } + + /// Get the underlying OS handle (HMODULE on Windows, dlopen handle on Unix) + pub fn get_pointer(&self) -> usize { let lib_lock = self.lib.lock(); if let Some(l) = &*lib_lock { - l as *const Library as usize + // libloading::Library internally stores the OS handle directly + // On Windows: HMODULE (*mut c_void) + // On Unix: *mut c_void from dlopen + // We use transmute_copy to read the handle without consuming the Library + unsafe { std::mem::transmute_copy::(l) } } else { - null::() as usize + 0 } } @@ -36,16 +64,6 @@ impl SharedLibrary { let lib_lock = self.lib.lock(); lib_lock.is_none() } - - fn close(&self) { - *self.lib.lock() = None; - } -} - -impl Drop for SharedLibrary { - fn drop(&mut self) { - self.close(); - } } pub(super) struct ExternalLibs { @@ -63,6 +81,7 @@ impl ExternalLibs { self.libraries.get(&key) } + #[cfg(windows)] pub fn get_or_insert_lib( &mut self, library_path: impl AsRef, @@ -71,20 +90,53 @@ impl ExternalLibs { let new_lib = SharedLibrary::new(library_path)?; let key = new_lib.get_pointer(); - match self.libraries.get(&key) { - Some(l) => { - if l.is_closed() { - self.libraries.insert(key, new_lib); - } - } - _ => { - self.libraries.insert(key, new_lib); - } - }; + // Check if library already exists and is not closed + let should_use_cached = self.libraries.get(&key).is_some_and(|l| !l.is_closed()); + + if should_use_cached { + // new_lib will be dropped, calling FreeLibrary (decrements refcount) + // But library stays loaded because cached version maintains refcount + drop(new_lib); + return Ok((key, self.libraries.get(&key).expect("just checked"))); + } + self.libraries.insert(key, new_lib); Ok((key, self.libraries.get(&key).expect("just inserted"))) } + #[cfg(unix)] + pub fn get_or_insert_lib_with_mode( + &mut self, + library_path: impl AsRef, + mode: i32, + _vm: &VirtualMachine, + ) -> Result<(usize, &SharedLibrary), libloading::Error> { + let new_lib = SharedLibrary::new_with_mode(library_path, mode)?; + let key = new_lib.get_pointer(); + + // Check if library already exists and is not closed + let should_use_cached = self.libraries.get(&key).is_some_and(|l| !l.is_closed()); + + if should_use_cached { + // new_lib will be dropped, calling dlclose (decrements refcount) + // But library stays loaded because cached version maintains refcount + drop(new_lib); + return Ok((key, self.libraries.get(&key).expect("just checked"))); + } + + self.libraries.insert(key, new_lib); + Ok((key, self.libraries.get(&key).expect("just inserted"))) + } + + /// Insert a raw dlopen handle into the cache (for pythonapi / dlopen(NULL)) + #[cfg(unix)] + pub fn insert_raw_handle(&mut self, handle: *mut libc::c_void) -> usize { + let shared_lib = SharedLibrary::from_raw_handle(handle); + let key = handle as usize; + self.libraries.insert(key, shared_lib); + key + } + pub fn drop_lib(&mut self, key: usize) { self.libraries.remove(&key); } diff --git a/crates/vm/src/stdlib/ctypes/pointer.rs b/crates/vm/src/stdlib/ctypes/pointer.rs index 3ee39af3a7..ae97b741b3 100644 --- a/crates/vm/src/stdlib/ctypes/pointer.rs +++ b/crates/vm/src/stdlib/ctypes/pointer.rs @@ -1,6 +1,7 @@ +use super::base::CDATA_BUFFER_METHODS; use super::{PyCArray, PyCData, PyCSimple, PyCStructure, StgInfo, StgInfoFlags}; -use crate::protocol::PyNumberMethods; -use crate::types::{AsNumber, Constructor, Initializer}; +use crate::protocol::{BufferDescriptor, PyBuffer, PyNumberMethods}; +use crate::types::{AsBuffer, AsNumber, Constructor, Initializer}; use crate::{ AsObject, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine, builtins::{PyBytes, PyInt, PyList, PySlice, PyStr, PyType, PyTypeRef}, @@ -8,6 +9,7 @@ use crate::{ function::{FuncArgs, OptionalArg}, }; use num_traits::ToPrimitive; +use std::borrow::Cow; #[pyclass(name = "PyCPointerType", base = PyType, module = "_ctypes")] #[derive(Debug)] @@ -42,11 +44,19 @@ impl Initializer for PyCPointerType { stg_info.length = 1; stg_info.flags |= StgInfoFlags::TYPEFLAG_ISPOINTER; - // Set format string: "&" - if let Some(ref proto) = stg_info.proto { - let item_info = proto.stg_info_opt().expect("proto has StgInfo"); + // Set format string: "&" or "&(shape)" for arrays + if let Some(ref proto) = stg_info.proto + && let Some(item_info) = proto.stg_info_opt() + { let current_format = item_info.format.as_deref().unwrap_or("B"); - stg_info.format = Some(format!("&{}", current_format)); + // Include shape for array types in the pointer format + let shape_str = if !item_info.shape.is_empty() { + let dims: Vec = item_info.shape.iter().map(|d| d.to_string()).collect(); + format!("({})", dims.join(",")) + } else { + String::new() + }; + stg_info.format = Some(format!("&{}{}", shape_str, current_format)); } let _ = new_type.init_type_data(stg_info); @@ -69,6 +79,15 @@ impl PyCPointerType { return Ok(value); } + // 1.5 CArgObject (from byref()) - check if underlying obj is instance of _type_ + if let Some(carg) = value.downcast_ref::() + && let Ok(type_attr) = cls.as_object().get_attr("_type_", vm) + && let Ok(type_ref) = type_attr.downcast::() + && carg.obj.is_instance(type_ref.as_object(), vm)? + { + return Ok(value); + } + // 2. If already an instance of the requested type, return it if value.is_instance(cls.as_object(), vm)? { return Ok(value); @@ -149,10 +168,17 @@ impl PyCPointerType { if let Some(mut stg_info) = zelf.get_type_data_mut::() { stg_info.proto = Some(typ_type.clone()); - // Update format string: "&" + // Update format string: "&" or "&(shape)" for arrays let item_info = typ_type.stg_info_opt().expect("proto has StgInfo"); let current_format = item_info.format.as_deref().unwrap_or("B"); - stg_info.format = Some(format!("&{}", current_format)); + // Include shape for array types in the pointer format + let shape_str = if !item_info.shape.is_empty() { + let dims: Vec = item_info.shape.iter().map(|d| d.to_string()).collect(); + format!("({})", dims.join(",")) + } else { + String::new() + }; + stg_info.format = Some(format!("&{}{}", shape_str, current_format)); } // 4. Set _type_ attribute on the pointer type @@ -233,7 +259,10 @@ impl Initializer for PyCPointer { } } -#[pyclass(flags(BASETYPE, IMMUTABLETYPE), with(Constructor, Initializer))] +#[pyclass( + flags(BASETYPE, IMMUTABLETYPE), + with(Constructor, Initializer, AsBuffer) +)] impl PyCPointer { /// Get the pointer value stored in buffer as usize pub fn get_ptr_value(&self) -> usize { @@ -605,20 +634,47 @@ impl PyCPointer { unsafe { let ptr = addr as *const u8; match type_code { + // Single-byte types don't need read_unaligned Some("c") => Ok(vm.ctx.new_bytes(vec![*ptr]).into()), - Some("b") => Ok(vm.ctx.new_int(*(ptr as *const i8) as i32).into()), + Some("b") => Ok(vm.ctx.new_int(*ptr as i8 as i32).into()), Some("B") => Ok(vm.ctx.new_int(*ptr as i32).into()), - Some("h") => Ok(vm.ctx.new_int(*(ptr as *const i16) as i32).into()), - Some("H") => Ok(vm.ctx.new_int(*(ptr as *const u16) as i32).into()), - Some("i") | Some("l") => Ok(vm.ctx.new_int(*(ptr as *const i32)).into()), - Some("I") | Some("L") => Ok(vm.ctx.new_int(*(ptr as *const u32)).into()), - Some("q") => Ok(vm.ctx.new_int(*(ptr as *const i64)).into()), - Some("Q") => Ok(vm.ctx.new_int(*(ptr as *const u64)).into()), - Some("f") => Ok(vm.ctx.new_float(*(ptr as *const f32) as f64).into()), - Some("d") | Some("g") => Ok(vm.ctx.new_float(*(ptr as *const f64)).into()), - Some("P") | Some("z") | Some("Z") => { - Ok(vm.ctx.new_int(*(ptr as *const usize)).into()) - } + // Multi-byte types need read_unaligned for safety on strict-alignment architectures + Some("h") => Ok(vm + .ctx + .new_int(std::ptr::read_unaligned(ptr as *const i16) as i32) + .into()), + Some("H") => Ok(vm + .ctx + .new_int(std::ptr::read_unaligned(ptr as *const u16) as i32) + .into()), + Some("i") | Some("l") => Ok(vm + .ctx + .new_int(std::ptr::read_unaligned(ptr as *const i32)) + .into()), + Some("I") | Some("L") => Ok(vm + .ctx + .new_int(std::ptr::read_unaligned(ptr as *const u32)) + .into()), + Some("q") => Ok(vm + .ctx + .new_int(std::ptr::read_unaligned(ptr as *const i64)) + .into()), + Some("Q") => Ok(vm + .ctx + .new_int(std::ptr::read_unaligned(ptr as *const u64)) + .into()), + Some("f") => Ok(vm + .ctx + .new_float(std::ptr::read_unaligned(ptr as *const f32) as f64) + .into()), + Some("d") | Some("g") => Ok(vm + .ctx + .new_float(std::ptr::read_unaligned(ptr as *const f64)) + .into()), + Some("P") | Some("z") | Some("Z") => Ok(vm + .ctx + .new_int(std::ptr::read_unaligned(ptr as *const usize)) + .into()), _ => { // Default: read as bytes let bytes = std::slice::from_raw_parts(ptr, size).to_vec(); @@ -652,27 +708,37 @@ impl PyCPointer { "bytes/string or integer address expected".to_owned(), )); }; - *(ptr as *mut usize) = ptr_val; + std::ptr::write_unaligned(ptr as *mut usize, ptr_val); return Ok(()); } _ => {} } // Try to get value as integer + // Use write_unaligned for safety on strict-alignment architectures if let Ok(int_val) = value.try_int(vm) { let i = int_val.as_bigint(); match size { 1 => { - *ptr = i.to_u8().unwrap_or(0); + *ptr = i.to_u8().expect("int too large"); } 2 => { - *(ptr as *mut i16) = i.to_i16().unwrap_or(0); + std::ptr::write_unaligned( + ptr as *mut i16, + i.to_i16().expect("int too large"), + ); } 4 => { - *(ptr as *mut i32) = i.to_i32().unwrap_or(0); + std::ptr::write_unaligned( + ptr as *mut i32, + i.to_i32().expect("int too large"), + ); } 8 => { - *(ptr as *mut i64) = i.to_i64().unwrap_or(0); + std::ptr::write_unaligned( + ptr as *mut i64, + i.to_i64().expect("int too large"), + ); } _ => { let bytes = i.to_signed_bytes_le(); @@ -688,10 +754,10 @@ impl PyCPointer { let f = float_val.to_f64(); match size { 4 => { - *(ptr as *mut f32) = f as f32; + std::ptr::write_unaligned(ptr as *mut f32, f as f32); } 8 => { - *(ptr as *mut f64) = f; + std::ptr::write_unaligned(ptr as *mut f64, f); } _ => {} } @@ -712,3 +778,28 @@ impl PyCPointer { } } } + +impl AsBuffer for PyCPointer { + fn as_buffer(zelf: &Py, _vm: &VirtualMachine) -> PyResult { + let stg_info = zelf + .class() + .stg_info_opt() + .expect("PyCPointer type must have StgInfo"); + let format = stg_info + .format + .clone() + .map(Cow::Owned) + .unwrap_or(Cow::Borrowed("&B")); + let itemsize = stg_info.size; + // Pointer types are scalars with ndim=0, shape=() + let desc = BufferDescriptor { + len: itemsize, + readonly: false, + itemsize, + format, + dim_desc: vec![], + }; + let buf = PyBuffer::new(zelf.to_owned().into(), desc, &CDATA_BUFFER_METHODS); + Ok(buf) + } +} diff --git a/crates/vm/src/stdlib/ctypes/simple.rs b/crates/vm/src/stdlib/ctypes/simple.rs index 1c0ec250d7..803b38d6e0 100644 --- a/crates/vm/src/stdlib/ctypes/simple.rs +++ b/crates/vm/src/stdlib/ctypes/simple.rs @@ -14,12 +14,47 @@ use crate::protocol::{BufferDescriptor, PyBuffer, PyNumberMethods}; use crate::types::{AsBuffer, AsNumber, Constructor, Initializer, Representable}; use crate::{AsObject, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine}; use num_traits::ToPrimitive; +use std::borrow::Cow; use std::fmt::Debug; /// Valid type codes for ctypes simple types // spell-checker: disable-next-line pub(super) const SIMPLE_TYPE_CHARS: &str = "cbBhHiIlLdfuzZqQPXOv?g"; +/// Convert ctypes type code to PEP 3118 format code. +/// Some ctypes codes need to be mapped to standard-size codes based on platform. +/// _ctypes_alloc_format_string_for_type +fn ctypes_code_to_pep3118(code: char) -> char { + match code { + // c_int: map based on sizeof(int) + 'i' if std::mem::size_of::() == 2 => 'h', + 'i' if std::mem::size_of::() == 4 => 'i', + 'i' if std::mem::size_of::() == 8 => 'q', + 'I' if std::mem::size_of::() == 2 => 'H', + 'I' if std::mem::size_of::() == 4 => 'I', + 'I' if std::mem::size_of::() == 8 => 'Q', + // c_long: map based on sizeof(long) + 'l' if std::mem::size_of::() == 4 => 'l', + 'l' if std::mem::size_of::() == 8 => 'q', + 'L' if std::mem::size_of::() == 4 => 'L', + 'L' if std::mem::size_of::() == 8 => 'Q', + // c_bool: map based on sizeof(bool) - typically 1 byte on all platforms + '?' if std::mem::size_of::() == 1 => '?', + '?' if std::mem::size_of::() == 2 => 'H', + '?' if std::mem::size_of::() == 4 => 'L', + '?' if std::mem::size_of::() == 8 => 'Q', + // Default: use the same code + _ => code, + } +} + +/// _ctypes_alloc_format_string_for_type +fn alloc_format_string_for_type(code: char, big_endian: bool) -> String { + let prefix = if big_endian { ">" } else { "<" }; + let pep_code = ctypes_code_to_pep3118(code); + format!("{}{}", prefix, pep_code) +} + /// Create a new simple type instance from a class fn new_simple_type( cls: Either<&PyObject, &Py>, @@ -165,6 +200,20 @@ fn set_primitive(_type_: &str, value: &PyObject, vm: &VirtualMachine) -> PyResul } // O_set: py_object accepts any Python object "O" => Ok(value.to_owned()), + // X_set: BSTR - same as Z (c_wchar_p), accepts None, int, or str + "X" => { + if value.is(&vm.ctx.none) + || value.downcast_ref_if_exact::(vm).is_some() + || value.downcast_ref_if_exact::(vm).is_some() + { + Ok(value.to_owned()) + } else { + Err(vm.new_type_error(format!( + "unicode string or integer address expected instead of {} instance", + value.class().name() + ))) + } + } _ => { // "P" if value.downcast_ref_if_exact::(vm).is_some() @@ -313,7 +362,7 @@ impl PyCSimpleType { .to_pyobject(vm)); } // 2. Array/Pointer with c_wchar element type - if is_cwchar_array_or_pointer(&value, vm) { + if is_cwchar_array_or_pointer(&value, vm)? { return Ok(value); } // 3. CArgObject (byref(c_wchar(...))) @@ -521,13 +570,10 @@ impl Initializer for PyCSimpleType { let mut stg_info = StgInfo::new(size, align); // Set format for PEP 3118 buffer protocol - // Format is endian prefix + type code (e.g., "" - }; - stg_info.format = Some(format!("{}{}", endian_prefix, type_str)); + stg_info.format = Some(alloc_format_string_for_type( + type_str.chars().next().unwrap_or('?'), + cfg!(target_endian = "big"), + )); stg_info.paramfunc = super::base::ParamFunc::Simple; // Set TYPEFLAG_ISPOINTER for pointer types: z (c_char_p), Z (c_wchar_p), @@ -619,13 +665,14 @@ fn create_swapped_types( swapped_type.set_attr("_swappedbytes_", vm.ctx.none(), vm)?; // Update swapped type's StgInfo format to use opposite endian prefix - // Native uses '<' on little-endian, '>' on big-endian - // Swapped uses the opposite if let Ok(swapped_type_ref) = swapped_type.clone().downcast::() && let Some(mut sw_stg) = swapped_type_ref.get_type_data_mut::() { - let swapped_prefix = if is_little_endian { ">" } else { "<" }; - sw_stg.format = Some(format!("{}{}", swapped_prefix, type_str)); + // Swapped: little-endian system uses big-endian prefix and vice versa + sw_stg.format = Some(alloc_format_string_for_type( + type_str.chars().next().unwrap_or('?'), + is_little_endian, + )); } // Set attributes based on system byte order @@ -734,63 +781,56 @@ fn value_to_bytes_endian( } "b" => { // c_byte - signed char (1 byte) - // PyLong_AsLongMask pattern: wrapping for overflow values if let Ok(int_val) = value.try_index(vm) { - let v = int_val.as_bigint().to_i128().unwrap_or(0) as i8; + let v = int_val.as_bigint().to_i128().expect("int too large") as i8; return vec![v as u8]; } vec![0] } "B" => { // c_ubyte - unsigned char (1 byte) - // PyLong_AsUnsignedLongMask: wrapping for negative values if let Ok(int_val) = value.try_index(vm) { - let v = int_val.as_bigint().to_i128().map(|n| n as u8).unwrap_or(0); + let v = int_val.as_bigint().to_i128().expect("int too large") as u8; return vec![v]; } vec![0] } "h" => { // c_short (2 bytes) - // PyLong_AsLongMask pattern: wrapping for overflow values if let Ok(int_val) = value.try_index(vm) { - let v = int_val.as_bigint().to_i128().unwrap_or(0) as i16; + let v = int_val.as_bigint().to_i128().expect("int too large") as i16; return to_bytes!(v); } vec![0; 2] } "H" => { // c_ushort (2 bytes) - // PyLong_AsUnsignedLongMask: wrapping for negative values if let Ok(int_val) = value.try_index(vm) { - let v = int_val.as_bigint().to_i128().map(|n| n as u16).unwrap_or(0); + let v = int_val.as_bigint().to_i128().expect("int too large") as u16; return to_bytes!(v); } vec![0; 2] } "i" => { // c_int (4 bytes) - // PyLong_AsLongMask pattern: wrapping for overflow values if let Ok(int_val) = value.try_index(vm) { - let v = int_val.as_bigint().to_i128().unwrap_or(0) as i32; + let v = int_val.as_bigint().to_i128().expect("int too large") as i32; return to_bytes!(v); } vec![0; 4] } "I" => { // c_uint (4 bytes) - // PyLong_AsUnsignedLongMask: wrapping for negative values if let Ok(int_val) = value.try_index(vm) { - let v = int_val.as_bigint().to_i128().map(|n| n as u32).unwrap_or(0); + let v = int_val.as_bigint().to_i128().expect("int too large") as u32; return to_bytes!(v); } vec![0; 4] } "l" => { // c_long (platform dependent) - // PyLong_AsLongMask pattern: wrapping for overflow values if let Ok(int_val) = value.try_index(vm) { - let v = int_val.as_bigint().to_i128().unwrap_or(0) as libc::c_long; + let v = int_val.as_bigint().to_i128().expect("int too large") as libc::c_long; return to_bytes!(v); } const SIZE: usize = std::mem::size_of::(); @@ -798,13 +838,8 @@ fn value_to_bytes_endian( } "L" => { // c_ulong (platform dependent) - // PyLong_AsUnsignedLongMask: wrapping for negative values if let Ok(int_val) = value.try_index(vm) { - let v = int_val - .as_bigint() - .to_i128() - .map(|n| n as libc::c_ulong) - .unwrap_or(0); + let v = int_val.as_bigint().to_i128().expect("int too large") as libc::c_ulong; return to_bytes!(v); } const SIZE: usize = std::mem::size_of::(); @@ -812,18 +847,16 @@ fn value_to_bytes_endian( } "q" => { // c_longlong (8 bytes) - // PyLong_AsLongMask pattern: wrapping for overflow values if let Ok(int_val) = value.try_index(vm) { - let v = int_val.as_bigint().to_i128().unwrap_or(0) as i64; + let v = int_val.as_bigint().to_i128().expect("int too large") as i64; return to_bytes!(v); } vec![0; 8] } "Q" => { // c_ulonglong (8 bytes) - // PyLong_AsUnsignedLongLongMask: wrapping for negative values if let Ok(int_val) = value.try_index(vm) { - let v = int_val.as_bigint().to_i128().map(|n| n as u64).unwrap_or(0); + let v = int_val.as_bigint().to_i128().expect("int too large") as u64; return to_bytes!(v); } vec![0; 8] @@ -899,7 +932,10 @@ fn value_to_bytes_endian( "P" => { // c_void_p - pointer type (platform pointer size) if let Ok(int_val) = value.try_index(vm) { - let v = int_val.as_bigint().to_usize().unwrap_or(0); + let v = int_val + .as_bigint() + .to_usize() + .expect("int too large for pointer"); return to_bytes!(v); } vec![0; std::mem::size_of::()] @@ -908,7 +944,10 @@ fn value_to_bytes_endian( // c_char_p - pointer to char (stores pointer value from int) // PyBytes case is handled in slot_new/set_value with make_z_buffer() if let Ok(int_val) = value.try_index(vm) { - let v = int_val.as_bigint().to_usize().unwrap_or(0); + let v = int_val + .as_bigint() + .to_usize() + .expect("int too large for pointer"); return to_bytes!(v); } vec![0; std::mem::size_of::()] @@ -917,7 +956,10 @@ fn value_to_bytes_endian( // c_wchar_p - pointer to wchar_t (stores pointer value from int) // PyStr case is handled in slot_new/set_value with make_wchar_buffer() if let Ok(int_val) = value.try_index(vm) { - let v = int_val.as_bigint().to_usize().unwrap_or(0); + let v = int_val + .as_bigint() + .to_usize() + .expect("int too large for pointer"); return to_bytes!(v); } vec![0; std::mem::size_of::()] @@ -939,7 +981,7 @@ fn is_cchar_array_or_pointer(value: &PyObject, vm: &VirtualMachine) -> bool { if let Some(arr) = value.downcast_ref::() && let Some(info) = arr.class().stg_info_opt() && let Some(ref elem_type) = info.element_type - && let Some(elem_code) = elem_type.class().type_code(vm) + && let Some(elem_code) = elem_type.type_code(vm) { return elem_code == "c"; } @@ -947,7 +989,7 @@ fn is_cchar_array_or_pointer(value: &PyObject, vm: &VirtualMachine) -> bool { if let Some(ptr) = value.downcast_ref::() && let Some(info) = ptr.class().stg_info_opt() && let Some(ref proto) = info.proto - && let Some(proto_code) = proto.class().type_code(vm) + && let Some(proto_code) = proto.type_code(vm) { return proto_code == "c"; } @@ -955,25 +997,25 @@ fn is_cchar_array_or_pointer(value: &PyObject, vm: &VirtualMachine) -> bool { } /// Check if value is a c_wchar array or pointer(c_wchar) -fn is_cwchar_array_or_pointer(value: &PyObject, vm: &VirtualMachine) -> bool { +fn is_cwchar_array_or_pointer(value: &PyObject, vm: &VirtualMachine) -> PyResult { // Check Array with c_wchar element type if let Some(arr) = value.downcast_ref::() { - let info = arr.class().stg_info_opt().expect("array has StgInfo"); + let info = arr.class().stg_info(vm)?; let elem_type = info.element_type.as_ref().expect("array has element_type"); - if let Some(elem_code) = elem_type.class().type_code(vm) { - return elem_code == "u"; + if let Some(elem_code) = elem_type.type_code(vm) { + return Ok(elem_code == "u"); } } // Check Pointer to c_wchar if let Some(ptr) = value.downcast_ref::() { - let info = ptr.class().stg_info_opt().expect("pointer has StgInfo"); + let info = ptr.class().stg_info(vm)?; if let Some(ref proto) = info.proto - && let Some(proto_code) = proto.class().type_code(vm) + && let Some(proto_code) = proto.type_code(vm) { - return proto_code == "u"; + return Ok(proto_code == "u"); } } - false + Ok(false) } impl Constructor for PyCSimple { @@ -1121,15 +1163,27 @@ impl PyCSimple { return Ok(vm.ctx.none()); } // Read null-terminated wide string at the address + // Windows: wchar_t = u16 (UTF-16) -> use Wtf8Buf::from_wide for surrogate pairs + // Unix: wchar_t = i32 (UTF-32) -> convert via char::from_u32 unsafe { let w_ptr = ptr as *const libc::wchar_t; let len = libc::wcslen(w_ptr); let wchars = std::slice::from_raw_parts(w_ptr, len); - let s: String = wchars - .iter() - .filter_map(|&c| char::from_u32(c as u32)) - .collect(); - return Ok(vm.ctx.new_str(s).into()); + #[cfg(windows)] + { + use rustpython_common::wtf8::Wtf8Buf; + let wide: Vec = wchars.to_vec(); + let wtf8 = Wtf8Buf::from_wide(&wide); + return Ok(vm.ctx.new_str(wtf8).into()); + } + #[cfg(not(windows))] + { + let s: String = wchars + .iter() + .filter_map(|&c| char::from_u32(c as u32)) + .collect(); + return Ok(vm.ctx.new_str(s).into()); + } } } @@ -1349,12 +1403,25 @@ impl PyCSimple { impl AsBuffer for PyCSimple { fn as_buffer(zelf: &Py, _vm: &VirtualMachine) -> PyResult { - let buffer_len = zelf.0.buffer.read().len(); - let buf = PyBuffer::new( - zelf.to_owned().into(), - BufferDescriptor::simple(buffer_len, false), // readonly=false for ctypes - &CDATA_BUFFER_METHODS, - ); + let stg_info = zelf + .class() + .stg_info_opt() + .expect("PyCSimple type must have StgInfo"); + let format = stg_info + .format + .clone() + .map(Cow::Owned) + .unwrap_or(Cow::Borrowed("B")); + let itemsize = stg_info.size; + // Simple types are scalars with ndim=0, shape=() + let desc = BufferDescriptor { + len: itemsize, + readonly: false, + itemsize, + format, + dim_desc: vec![], + }; + let buf = PyBuffer::new(zelf.to_owned().into(), desc, &CDATA_BUFFER_METHODS); Ok(buf) } } diff --git a/crates/vm/src/stdlib/ctypes/structure.rs b/crates/vm/src/stdlib/ctypes/structure.rs index 10b8812e42..1ca428669d 100644 --- a/crates/vm/src/stdlib/ctypes/structure.rs +++ b/crates/vm/src/stdlib/ctypes/structure.rs @@ -601,12 +601,6 @@ impl PyCStructure { fn _b0_(&self) -> Option { self.0.base.read().clone() } - - #[pygetset] - fn _fields_(&self, vm: &VirtualMachine) -> PyObjectRef { - // Return the _fields_ from the class, not instance - vm.ctx.none() - } } impl AsBuffer for PyCStructure { diff --git a/crates/vm/src/stdlib/sys.rs b/crates/vm/src/stdlib/sys.rs index cfe6f9f5e6..0e46ec18a0 100644 --- a/crates/vm/src/stdlib/sys.rs +++ b/crates/vm/src/stdlib/sys.rs @@ -1,6 +1,8 @@ use crate::{Py, PyResult, VirtualMachine, builtins::PyModule, convert::ToPyObject}; -pub(crate) use sys::{__module_def, DOC, MAXSIZE, MULTIARCH, UnraisableHookArgsData}; +pub(crate) use sys::{ + __module_def, DOC, MAXSIZE, RUST_MULTIARCH, UnraisableHookArgsData, multiarch, +}; #[pymodule] mod sys { @@ -37,10 +39,14 @@ mod sys { System::LibraryLoader::{GetModuleFileNameW, GetModuleHandleW}, }; - // not the same as CPython (e.g. rust's x86_x64-unknown-linux-gnu is just x86_64-linux-gnu) - // but hopefully that's just an implementation detail? TODO: copy CPython's multiarch exactly, - // https://github.com/python/cpython/blob/3.8/configure.ac#L725 - pub(crate) const MULTIARCH: &str = env!("RUSTPYTHON_TARGET_TRIPLE"); + // Rust target triple (e.g., "x86_64-unknown-linux-gnu") + pub(crate) const RUST_MULTIARCH: &str = env!("RUSTPYTHON_TARGET_TRIPLE"); + + /// Convert Rust target triple to CPython-style multiarch + /// e.g., "x86_64-unknown-linux-gnu" -> "x86_64-linux-gnu" + pub(crate) fn multiarch() -> String { + RUST_MULTIARCH.replace("-unknown", "") + } #[pyattr(name = "_rustpython_debugbuild")] const RUSTPYTHON_DEBUGBUILD: bool = cfg!(debug_assertions); @@ -189,7 +195,7 @@ mod sys { py_namespace!(vm, { "name" => ctx.new_str(NAME), "cache_tag" => ctx.new_str(cache_tag), - "_multiarch" => ctx.new_str(MULTIARCH.to_owned()), + "_multiarch" => ctx.new_str(multiarch()), "version" => version_info(vm), "hexversion" => ctx.new_int(version::VERSION_HEX), }) @@ -1249,6 +1255,6 @@ pub(crate) fn sysconfigdata_name() -> String { "_sysconfigdata_{}_{}_{}", sys::ABIFLAGS, sys::PLATFORM, - sys::MULTIARCH + sys::multiarch() ) } diff --git a/crates/vm/src/stdlib/sysconfigdata.rs b/crates/vm/src/stdlib/sysconfigdata.rs index ee40b693aa..5e954f7fe7 100644 --- a/crates/vm/src/stdlib/sysconfigdata.rs +++ b/crates/vm/src/stdlib/sysconfigdata.rs @@ -4,20 +4,23 @@ pub(crate) use _sysconfigdata::make_module; #[pymodule] pub(crate) mod _sysconfigdata { - use crate::{VirtualMachine, builtins::PyDictRef, convert::ToPyObject, stdlib::sys::MULTIARCH}; + use crate::stdlib::sys::{RUST_MULTIARCH, multiarch}; + use crate::{VirtualMachine, builtins::PyDictRef, convert::ToPyObject}; #[pyattr] fn build_time_vars(vm: &VirtualMachine) -> PyDictRef { let vars = vm.ctx.new_dict(); + let multiarch = multiarch(); macro_rules! sysvars { ($($key:literal => $value:expr),*$(,)?) => {{ $(vars.set_item($key, $value.to_pyobject(vm), vm).unwrap();)* }}; } sysvars! { - // fake shared module extension - "EXT_SUFFIX" => format!(".rustpython-{MULTIARCH}"), - "MULTIARCH" => MULTIARCH, + // Extension module suffix in CPython-compatible format + "EXT_SUFFIX" => format!(".rustpython313-{multiarch}.so"), + "MULTIARCH" => multiarch.clone(), + "RUST_MULTIARCH" => RUST_MULTIARCH, // enough for tests to stop expecting urandom() to fail after restricting file resources "HAVE_GETRANDOM" => 1, // Compiler configuration for native extension builds diff --git a/crates/vm/src/version.rs b/crates/vm/src/version.rs index 9d472e8be0..deb3dccd53 100644 --- a/crates/vm/src/version.rs +++ b/crates/vm/src/version.rs @@ -15,12 +15,29 @@ pub const VERSION_HEX: usize = (MAJOR << 24) | (MINOR << 16) | (MICRO << 8) | (RELEASELEVEL_N << 4) | SERIAL; pub fn get_version() -> String { + // Windows: include MSC v. for compatibility with ctypes.util.find_library + // MSC v.1929 = VS 2019, version 14+ makes find_msvcrt() return None + #[cfg(windows)] + let msc_info = { + let arch = if cfg!(target_pointer_width = "64") { + "64 bit (AMD64)" + } else { + "32 bit (Intel)" + }; + // Include both RustPython identifier and MSC v. for compatibility + format!(" MSC v.1929 {arch}",) + }; + + #[cfg(not(windows))] + let msc_info = String::new(); + format!( - "{:.80} ({:.80}) \n[RustPython {} with {:.80}]", // \n is PyPy convention + "{:.80} ({:.80}) \n[RustPython {} with {:.80}{}]", // \n is PyPy convention get_version_number(), get_build_info(), env!("CARGO_PKG_VERSION"), COMPILER, + msc_info, ) } diff --git a/whats_left.py b/whats_left.py index 91e46bef7e..c5b0be6ead 100755 --- a/whats_left.py +++ b/whats_left.py @@ -361,7 +361,9 @@ def method_incompatibility_reason(typ, method_name, real_method_value): if platform.python_implementation() == "CPython": if not_implementeds: - sys.exit("ERROR: CPython should have all the methods") + sys.exit( + f"ERROR: CPython should have all the methods but missing: {not_implementeds}" + ) mod_names = [ name.decode() @@ -455,6 +457,7 @@ def remove_one_indent(s): ) # The last line should be json output, the rest of the lines can contain noise # because importing certain modules can print stuff to stdout/stderr +print(result.stderr, file=sys.stderr) result = json.loads(result.stdout.splitlines()[-1]) if args.json: