diff --git a/Cargo.lock b/Cargo.lock index d885b52242..790987bfde 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1053,6 +1053,16 @@ dependencies = [ "make-cmd", ] +[[package]] +name = "libloading" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f84d96438c15fcd6c3f244c8fce01d1e2b9c6b5623e9c711dc9286d8fc92d6a" +dependencies = [ + "cfg-if 1.0.0", + "winapi", +] + [[package]] name = "libz-sys" version = "1.1.3" @@ -1904,6 +1914,8 @@ dependencies = [ "itertools", "lexical-core", "libc", + "libffi", + "libloading", "libz-sys", "log", "md-5", diff --git a/Lib/ctypes/__init__.py b/Lib/ctypes/__init__.py new file mode 100644 index 0000000000..c13121bf48 --- /dev/null +++ b/Lib/ctypes/__init__.py @@ -0,0 +1,349 @@ + +"""create and manipulate C data types in Python""" + +import os as _os, sys as _sys + +__version__ = "1.1.0" + +from _ctypes import Struct, Array +from _ctypes import _Pointer +from _ctypes import CFuncPtr as _CFuncPtr +from _ctypes import dlopen as _dlopen + +from struct import calcsize as _calcsize + +def create_string_buffer(init, size=None): + """create_string_buffer(aBytes) -> character array + create_string_buffer(anInteger) -> character array + create_string_buffer(aBytes, anInteger) -> character array + """ + if isinstance(init, bytes): + if size is None: + size = len(init)+1 + _sys.audit("ctypes.create_string_buffer", init, size) + buftype = c_char * size + buf = buftype() + buf.value = init + return buf + elif isinstance(init, int): + _sys.audit("ctypes.create_string_buffer", None, init) + buftype = c_char * init + buf = buftype() + return buf + raise TypeError(init) + +def c_buffer(init, size=None): + return create_string_buffer(init, size) + +_c_functype_cache = {} +def CFUNCTYPE(restype, *argtypes, **kw): + """CFUNCTYPE(restype, *argtypes, + use_errno=False, use_last_error=False) -> function prototype. + restype: the result type + argtypes: a sequence specifying the argument types + The function prototype can be called in different ways to create a + callable object: + prototype(integer address) -> foreign function + prototype(callable) -> create and return a C callable function from callable + prototype(integer index, method name[, paramflags]) -> foreign function calling a COM method + prototype((ordinal number, dll object)[, paramflags]) -> foreign function exported by ordinal + prototype((function name, dll object)[, paramflags]) -> foreign function exported by name + """ + if kw: + raise ValueError("unexpected keyword argument(s) %s" % kw.keys()) + try: + return _c_functype_cache[(restype, argtypes)] + except KeyError: + class CFunctionType(_CFuncPtr): + _argtypes_ = argtypes + _restype_ = restype + _c_functype_cache[(restype, argtypes)] = CFunctionType + return CFunctionType + +if _os.name == "nt": + _win_functype_cache = {} + def WINFUNCTYPE(restype, *argtypes, **kw): + # docstring set later (very similar to CFUNCTYPE.__doc__) + if kw: + raise ValueError("unexpected keyword argument(s) %s" % kw.keys()) + try: + return _win_functype_cache[(restype, argtypes)] + except KeyError: + class WinFunctionType(_CFuncPtr): + _argtypes_ = argtypes + _restype_ = restype + _win_functype_cache[(restype, argtypes)] = WinFunctionType + return WinFunctionType + if WINFUNCTYPE.__doc__: + WINFUNCTYPE.__doc__ = CFUNCTYPE.__doc__.replace("CFUNCTYPE", "WINFUNCTYPE") + + +from _ctypes import sizeof, byref, addressof, alignment +from _ctypes import _SimpleCData + + +def _check_size(typ, typecode=None): + # Check if sizeof(ctypes_type) against struct.calcsize. This + # should protect somewhat against a misconfigured libffi. + from struct import calcsize + if typecode is None: + # Most _type_ codes are the same as used in struct + typecode = typ._type_ + actual, required = sizeof(typ), calcsize(typecode) + if actual != required: + raise SystemError("sizeof(%s) wrong: %d instead of %d" % \ + (typ, actual, required)) + +class c_short(_SimpleCData): + _type_ = "h" +_check_size(c_short) + +class c_ushort(_SimpleCData): + _type_ = "H" + +class c_long(_SimpleCData): + _type_ = "l" +_check_size(c_long) + +class c_ulong(_SimpleCData): + _type_ = "L" +_check_size(c_ulong) + +if _calcsize("i") == _calcsize("l"): +# if int and long have the same size, make c_int an alias for c_long + c_int = c_long + c_uint = c_ulong +else: + class c_int(_SimpleCData): + _type_ = "i" + _check_size(c_int) + + class c_uint(_SimpleCData): + _type_ = "I" + _check_size(c_uint) + +class c_float(_SimpleCData): + _type_ = "f" +_check_size(c_float) + +class c_double(_SimpleCData): + _type_ = "d" +_check_size(c_double) + +class c_longdouble(_SimpleCData): + _type_ = "g" +if sizeof(c_longdouble) == sizeof(c_double): + c_longdouble = c_double + +if _calcsize("l") == _calcsize("q"): + # if long and long long have the same size, make c_longlong an alias for c_long + c_longlong = c_long + c_ulonglong = c_ulong +else: + class c_longlong(_SimpleCData): + _type_ = "q" + _check_size(c_longlong) + + class c_ulonglong(_SimpleCData): + _type_ = "Q" + _check_size(c_ulonglong) + +class c_ubyte(_SimpleCData): + _type_ = "B" +c_ubyte.__ctype_le__ = c_ubyte.__ctype_be__ = c_ubyte +_check_size(c_ubyte) + +class c_byte(_SimpleCData): + _type_ = "b" +c_byte.__ctype_le__ = c_byte.__ctype_be__ = c_byte +_check_size(c_byte) + +class c_char(_SimpleCData): + _type_ = "c" +c_char.__ctype_le__ = c_char.__ctype_be__ = c_char +_check_size(c_char) + +class c_char_p(_SimpleCData): + _type_ = "z" + def __repr__(self): + return "%s(%s)" % (self.__class__.__name__, c_void_p.from_buffer(self).value) +_check_size(c_char_p, "P") + + +class c_void_p(_SimpleCData): + _type_ = "P" +c_voidp = c_void_p # backwards compatibility (to a bug) +_check_size(c_void_p) + +class c_bool(_SimpleCData): + _type_ = "?" + +from _ctypes import POINTER, pointer, _pointer_type_cache + +class c_wchar_p(_SimpleCData): + _type_ = "Z" + def __repr__(self): + return "%s(%s)" % (self.__class__.__name__, c_void_p.from_buffer(self).value) + +class c_wchar(_SimpleCData): + _type_ = "u" + +def _reset_cache(): + _pointer_type_cache.clear() + _c_functype_cache.clear() + if _os.name == "nt": + _win_functype_cache.clear() + # _SimpleCData.c_wchar_p_from_param + POINTER(c_wchar).from_param = c_wchar_p.from_param + # _SimpleCData.c_char_p_from_param + POINTER(c_char).from_param = c_char_p.from_param + _pointer_type_cache[None] = c_void_p + +def create_unicode_buffer(init, size=None): + """create_unicode_buffer(aString) -> character array + create_unicode_buffer(anInteger) -> character array + create_unicode_buffer(aString, anInteger) -> character array + """ + if isinstance(init, str): + if size is None: + if sizeof(c_wchar) == 2: + # UTF-16 requires a surrogate pair (2 wchar_t) for non-BMP + # characters (outside [U+0000; U+FFFF] range). +1 for trailing + # NUL character. + size = sum(2 if ord(c) > 0xFFFF else 1 for c in init) + 1 + else: + # 32-bit wchar_t (1 wchar_t per Unicode character). +1 for + # trailing NUL character. + size = len(init) + 1 + _sys.audit("ctypes.create_unicode_buffer", init, size) + buftype = c_wchar * size + buf = buftype() + buf.value = init + return buf + elif isinstance(init, int): + _sys.audit("ctypes.create_unicode_buffer", None, init) + buftype = c_wchar * init + buf = buftype() + return buf + raise TypeError(init) + + +# XXX Deprecated +def SetPointerType(pointer, cls): + 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: + raise RuntimeError("What's this???") + pointer.set_type(cls) + _pointer_type_cache[cls] = pointer + del _pointer_type_cache[id(pointer)] + +# XXX Deprecated +def ARRAY(typ, len): + return typ * len + +################################################################ + +class CDLL(object): + """An instance of this class represents a loaded dll/shared + library, exporting functions using the standard C calling + convention (named 'cdecl' on Windows). + The exported functions can be accessed as attributes, or by + indexing with the function name. Examples: + .qsort -> callable object + ['qsort'] -> callable object + """ + _name = '' + _handle = 0 + + def __init__(self, name,handle=None): + self._name = name + + class _FuncPtr(_CFuncPtr): + pass + + self._FuncPtr = _FuncPtr + + if handle is None: + self._handle = _dlopen(self._name) + else: + self._handle = handle + + def __repr__(self): + return "<%s '%s', handle %x at %#x>" % \ + (self.__class__.__name__, self._name, + (self._handle & (_sys.maxsize*2 + 1)), + id(self) & (_sys.maxsize*2 + 1)) + + def __getattr__(self, name): + if name.startswith('__') and name.endswith('__'): + raise AttributeError(name) + func = self.__getitem__(name) + setattr(self, name, func) + return func + + def __getitem__(self, name_or_ordinal): + func = self._FuncPtr(name_or_ordinal, self) + if not isinstance(name_or_ordinal, int): + func.__name__ = name_or_ordinal + return func + +class LibraryLoader(object): + def __init__(self, dlltype): + self._dlltype = dlltype + + def __getattr__(self, name): + if name[0] == '_': + raise AttributeError(name) + + dll = self._dlltype(name) + setattr(self, name, dll) + + return dll + + def __getitem__(self, name): + return getattr(self, name) + + def LoadLibrary(self, name): + return self._dlltype(name) + +cdll = LibraryLoader(CDLL) + +if _os.name == "nt": + windll = LibraryLoader(CDLL) + oledll = LibraryLoader(CDLL) + + GetLastError = windll.kernel32.GetLastError + + def WinError(code=None, descr=None): + if code is None: + code = GetLastError() + if descr is None: + # descr = FormatError(code).strip() + descr = "Windows Error " + str(code) + return OSError(None, descr, None, code) + +if sizeof(c_uint) == sizeof(c_void_p): + c_size_t = c_uint + c_ssize_t = c_int +elif sizeof(c_ulong) == sizeof(c_void_p): + c_size_t = c_ulong + c_ssize_t = c_long +elif sizeof(c_ulonglong) == sizeof(c_void_p): + c_size_t = c_ulonglong + c_ssize_t = c_longlong + +# Fill in specifically-sized types +c_int8 = c_byte +c_uint8 = c_ubyte +for kind in [c_short, c_int, c_long, c_longlong]: + if sizeof(kind) == 2: c_int16 = kind + elif sizeof(kind) == 4: c_int32 = kind + elif sizeof(kind) == 8: c_int64 = kind +for kind in [c_ushort, c_uint, c_ulong, c_ulonglong]: + if sizeof(kind) == 2: c_uint16 = kind + elif sizeof(kind) == 4: c_uint32 = kind + elif sizeof(kind) == 8: c_uint64 = kind +del(kind) + +# _reset_cache() \ No newline at end of file diff --git a/Lib/ctypes/_aix.py b/Lib/ctypes/_aix.py new file mode 100644 index 0000000000..190cac6507 --- /dev/null +++ b/Lib/ctypes/_aix.py @@ -0,0 +1,331 @@ +""" +Lib/ctypes.util.find_library() support for AIX +Similar approach as done for Darwin support by using separate files +but unlike Darwin - no extension such as ctypes.macholib.* + +dlopen() is an interface to AIX initAndLoad() - primary documentation at: +https://www.ibm.com/support/knowledgecenter/en/ssw_aix_61/com.ibm.aix.basetrf1/dlopen.htm +https://www.ibm.com/support/knowledgecenter/en/ssw_aix_61/com.ibm.aix.basetrf1/load.htm + +AIX supports two styles for dlopen(): svr4 (System V Release 4) which is common on posix +platforms, but also a BSD style - aka SVR3. + +From AIX 5.3 Difference Addendum (December 2004) +2.9 SVR4 linking affinity +Nowadays, there are two major object file formats used by the operating systems: +XCOFF: The COFF enhanced by IBM and others. The original COFF (Common +Object File Format) was the base of SVR3 and BSD 4.2 systems. +ELF: Executable and Linking Format that was developed by AT&T and is a +base for SVR4 UNIX. + +While the shared library content is identical on AIX - one is located as a filepath name +(svr4 style) and the other is located as a member of an archive (and the archive +is located as a filepath name). + +The key difference arises when supporting multiple abi formats (i.e., 32 and 64 bit). +For svr4 either only one ABI is supported, or there are two directories, or there +are different file names. The most common solution for multiple ABI is multiple +directories. + +For the XCOFF (aka AIX) style - one directory (one archive file) is sufficient +as multiple shared libraries can be in the archive - even sharing the same name. +In documentation the archive is also referred to as the "base" and the shared +library object is referred to as the "member". + +For dlopen() on AIX (read initAndLoad()) the calls are similar. +Default activity occurs when no path information is provided. When path +information is provided dlopen() does not search any other directories. + +For SVR4 - the shared library name is the name of the file expected: libFOO.so +For AIX - the shared library is expressed as base(member). The search is for the +base (e.g., libFOO.a) and once the base is found the shared library - identified by +member (e.g., libFOO.so, or shr.o) is located and loaded. + +The mode bit RTLD_MEMBER tells initAndLoad() that it needs to use the AIX (SVR3) +naming style. +""" +__author__ = "Michael Felt " + +import re +from os import environ, path +from sys import executable +from ctypes import c_void_p, sizeof +from subprocess import Popen, PIPE, DEVNULL + +# Executable bit size - 32 or 64 +# Used to filter the search in an archive by size, e.g., -X64 +AIX_ABI = sizeof(c_void_p) * 8 + + +from sys import maxsize +def _last_version(libnames, sep): + def _num_version(libname): + # "libxyz.so.MAJOR.MINOR" => [MAJOR, MINOR] + parts = libname.split(sep) + nums = [] + try: + while parts: + nums.insert(0, int(parts.pop())) + except ValueError: + pass + return nums or [maxsize] + return max(reversed(libnames), key=_num_version) + +def get_ld_header(p): + # "nested-function, but placed at module level + ld_header = None + for line in p.stdout: + if line.startswith(('/', './', '../')): + ld_header = line + elif "INDEX" in line: + return ld_header.rstrip('\n') + return None + +def get_ld_header_info(p): + # "nested-function, but placed at module level + # as an ld_header was found, return known paths, archives and members + # these lines start with a digit + info = [] + for line in p.stdout: + if re.match("[0-9]", line): + info.append(line) + else: + # blank line (separator), consume line and end for loop + break + return info + +def get_ld_headers(file): + """ + Parse the header of the loader section of executable and archives + This function calls /usr/bin/dump -H as a subprocess + and returns a list of (ld_header, ld_header_info) tuples. + """ + # get_ld_headers parsing: + # 1. Find a line that starts with /, ./, or ../ - set as ld_header + # 2. If "INDEX" in occurs in a following line - return ld_header + # 3. get info (lines starting with [0-9]) + ldr_headers = [] + p = Popen(["/usr/bin/dump", f"-X{AIX_ABI}", "-H", file], + universal_newlines=True, stdout=PIPE, stderr=DEVNULL) + # be sure to read to the end-of-file - getting all entries + while True: + ld_header = get_ld_header(p) + if ld_header: + ldr_headers.append((ld_header, get_ld_header_info(p))) + else: + break + p.stdout.close() + p.wait() + return ldr_headers + +def get_shared(ld_headers): + """ + extract the shareable objects from ld_headers + character "[" is used to strip off the path information. + Note: the "[" and "]" characters that are part of dump -H output + are not removed here. + """ + shared = [] + for (line, _) in ld_headers: + # potential member lines contain "[" + # otherwise, no processing needed + if "[" in line: + # Strip off trailing colon (:) + shared.append(line[line.index("["):-1]) + return shared + +def get_one_match(expr, lines): + """ + Must be only one match, otherwise result is None. + When there is a match, strip leading "[" and trailing "]" + """ + # member names in the ld_headers output are between square brackets + expr = rf'\[({expr})\]' + matches = list(filter(None, (re.search(expr, line) for line in lines))) + if len(matches) == 1: + return matches[0].group(1) + else: + return None + +# additional processing to deal with AIX legacy names for 64-bit members +def get_legacy(members): + """ + This routine provides historical aka legacy naming schemes started + in AIX4 shared library support for library members names. + e.g., in /usr/lib/libc.a the member name shr.o for 32-bit binary and + shr_64.o for 64-bit binary. + """ + if AIX_ABI == 64: + # AIX 64-bit member is one of shr64.o, shr_64.o, or shr4_64.o + expr = r'shr4?_?64\.o' + member = get_one_match(expr, members) + if member: + return member + else: + # 32-bit legacy names - both shr.o and shr4.o exist. + # shr.o is the preffered name so we look for shr.o first + # i.e., shr4.o is returned only when shr.o does not exist + for name in ['shr.o', 'shr4.o']: + member = get_one_match(re.escape(name), members) + if member: + return member + return None + +def get_version(name, members): + """ + Sort list of members and return highest numbered version - if it exists. + This function is called when an unversioned libFOO.a(libFOO.so) has + not been found. + + Versioning for the member name is expected to follow + GNU LIBTOOL conventions: the highest version (x, then X.y, then X.Y.z) + * find [libFoo.so.X] + * find [libFoo.so.X.Y] + * find [libFoo.so.X.Y.Z] + + Before the GNU convention became the standard scheme regardless of + binary size AIX packagers used GNU convention "as-is" for 32-bit + archive members but used an "distinguishing" name for 64-bit members. + This scheme inserted either 64 or _64 between libFOO and .so + - generally libFOO_64.so, but occasionally libFOO64.so + """ + # the expression ending for versions must start as + # '.so.[0-9]', i.e., *.so.[at least one digit] + # while multiple, more specific expressions could be specified + # to search for .so.X, .so.X.Y and .so.X.Y.Z + # after the first required 'dot' digit + # any combination of additional 'dot' digits pairs are accepted + # anything more than libFOO.so.digits.digits.digits + # should be seen as a member name outside normal expectations + exprs = [rf'lib{name}\.so\.[0-9]+[0-9.]*', + rf'lib{name}_?64\.so\.[0-9]+[0-9.]*'] + for expr in exprs: + versions = [] + for line in members: + m = re.search(expr, line) + if m: + versions.append(m.group(0)) + if versions: + return _last_version(versions, '.') + return None + +def get_member(name, members): + """ + Return an archive member matching the request in name. + Name is the library name without any prefix like lib, suffix like .so, + or version number. + Given a list of members find and return the most appropriate result + Priority is given to generic libXXX.so, then a versioned libXXX.so.a.b.c + and finally, legacy AIX naming scheme. + """ + # look first for a generic match - prepend lib and append .so + expr = rf'lib{name}\.so' + member = get_one_match(expr, members) + if member: + return member + elif AIX_ABI == 64: + expr = rf'lib{name}64\.so' + member = get_one_match(expr, members) + if member: + return member + # since an exact match with .so as suffix was not found + # look for a versioned name + # If a versioned name is not found, look for AIX legacy member name + member = get_version(name, members) + if member: + return member + else: + return get_legacy(members) + +def get_libpaths(): + """ + On AIX, the buildtime searchpath is stored in the executable. + as "loader header information". + The command /usr/bin/dump -H extracts this info. + Prefix searched libraries with LD_LIBRARY_PATH (preferred), + or LIBPATH if defined. These paths are appended to the paths + to libraries the python executable is linked with. + This mimics AIX dlopen() behavior. + """ + libpaths = environ.get("LD_LIBRARY_PATH") + if libpaths is None: + libpaths = environ.get("LIBPATH") + if libpaths is None: + libpaths = [] + else: + libpaths = libpaths.split(":") + objects = get_ld_headers(executable) + for (_, lines) in objects: + for line in lines: + # the second (optional) argument is PATH if it includes a / + path = line.split()[1] + if "/" in path: + libpaths.extend(path.split(":")) + return libpaths + +def find_shared(paths, name): + """ + paths is a list of directories to search for an archive. + name is the abbreviated name given to find_library(). + Process: search "paths" for archive, and if an archive is found + return the result of get_member(). + If an archive is not found then return None + """ + for dir in paths: + # /lib is a symbolic link to /usr/lib, skip it + if dir == "/lib": + continue + # "lib" is prefixed to emulate compiler name resolution, + # e.g., -lc to libc + base = f'lib{name}.a' + archive = path.join(dir, base) + if path.exists(archive): + members = get_shared(get_ld_headers(archive)) + member = get_member(re.escape(name), members) + if member != None: + return (base, member) + else: + return (None, None) + return (None, None) + +def find_library(name): + """AIX implementation of ctypes.util.find_library() + Find an archive member that will dlopen(). If not available, + also search for a file (or link) with a .so suffix. + + AIX supports two types of schemes that can be used with dlopen(). + The so-called SystemV Release4 (svr4) format is commonly suffixed + with .so while the (default) AIX scheme has the library (archive) + ending with the suffix .a + As an archive has multiple members (e.g., 32-bit and 64-bit) in one file + the argument passed to dlopen must include both the library and + the member names in a single string. + + find_library() looks first for an archive (.a) with a suitable member. + If no archive+member pair is found, look for a .so file. + """ + + libpaths = get_libpaths() + (base, member) = find_shared(libpaths, name) + if base != None: + return f"{base}({member})" + + # To get here, a member in an archive has not been found + # In other words, either: + # a) a .a file was not found + # b) a .a file did not have a suitable member + # So, look for a .so file + # Check libpaths for .so file + # Note, the installation must prepare a link from a .so + # to a versioned file + # This is common practice by GNU libtool on other platforms + soname = f"lib{name}.so" + for dir in libpaths: + # /lib is a symbolic link to /usr/lib, skip it + if dir == "/lib": + continue + shlib = path.join(dir, soname) + if path.exists(shlib): + return soname + # if we are here, we have not found anything plausible + return None diff --git a/Lib/ctypes/_endian.py b/Lib/ctypes/_endian.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/Lib/ctypes/macholib/README.ctypes b/Lib/ctypes/macholib/README.ctypes new file mode 100644 index 0000000000..2866e9f349 --- /dev/null +++ b/Lib/ctypes/macholib/README.ctypes @@ -0,0 +1,7 @@ +Files in this directory come from Bob Ippolito's py2app. + +License: Any components of the py2app suite may be distributed under +the MIT or PSF open source licenses. + +This is version 1.0, SVN revision 789, from 2006/01/25. +The main repository is http://svn.red-bean.com/bob/macholib/trunk/macholib/ \ No newline at end of file diff --git a/Lib/ctypes/macholib/__init__.py b/Lib/ctypes/macholib/__init__.py new file mode 100644 index 0000000000..5621defccd --- /dev/null +++ b/Lib/ctypes/macholib/__init__.py @@ -0,0 +1,9 @@ +""" +Enough Mach-O to make your head spin. + +See the relevant header files in /usr/include/mach-o + +And also Apple's documentation. +""" + +__version__ = '1.0' diff --git a/Lib/ctypes/macholib/dyld.py b/Lib/ctypes/macholib/dyld.py new file mode 100644 index 0000000000..1c3f8fd38b --- /dev/null +++ b/Lib/ctypes/macholib/dyld.py @@ -0,0 +1,173 @@ +""" +dyld emulation +""" + +import os +from ctypes.macholib.framework import framework_info +from ctypes.macholib.dylib import dylib_info +from itertools import * +try: + from _ctypes import _dyld_shared_cache_contains_path +except ImportError: + def _dyld_shared_cache_contains_path(*args): + raise NotImplementedError + +__all__ = [ + 'dyld_find', 'framework_find', + 'framework_info', 'dylib_info', +] + +# These are the defaults as per man dyld(1) +# +DEFAULT_FRAMEWORK_FALLBACK = [ + os.path.expanduser("~/Library/Frameworks"), + "/Library/Frameworks", + "/Network/Library/Frameworks", + "/System/Library/Frameworks", +] + +DEFAULT_LIBRARY_FALLBACK = [ + os.path.expanduser("~/lib"), + "/usr/local/lib", + "/lib", + "/usr/lib", +] + +def dyld_env(env, var): + if env is None: + env = os.environ + rval = env.get(var) + if rval is None: + return [] + return rval.split(':') + +def dyld_image_suffix(env=None): + if env is None: + env = os.environ + return env.get('DYLD_IMAGE_SUFFIX') + +def dyld_framework_path(env=None): + return dyld_env(env, 'DYLD_FRAMEWORK_PATH') + +def dyld_library_path(env=None): + return dyld_env(env, 'DYLD_LIBRARY_PATH') + +def dyld_fallback_framework_path(env=None): + return dyld_env(env, 'DYLD_FALLBACK_FRAMEWORK_PATH') + +def dyld_fallback_library_path(env=None): + return dyld_env(env, 'DYLD_FALLBACK_LIBRARY_PATH') + +def dyld_image_suffix_search(iterator, env=None): + """For a potential path iterator, add DYLD_IMAGE_SUFFIX semantics""" + suffix = dyld_image_suffix(env) + if suffix is None: + return iterator + def _inject(iterator=iterator, suffix=suffix): + for path in iterator: + if path.endswith('.dylib'): + yield path[:-len('.dylib')] + suffix + '.dylib' + else: + yield path + suffix + yield path + return _inject() + +def dyld_override_search(name, env=None): + # If DYLD_FRAMEWORK_PATH is set and this dylib_name is a + # framework name, use the first file that exists in the framework + # path if any. If there is none go on to search the DYLD_LIBRARY_PATH + # if any. + + framework = framework_info(name) + + if framework is not None: + for path in dyld_framework_path(env): + yield os.path.join(path, framework['name']) + + # If DYLD_LIBRARY_PATH is set then use the first file that exists + # in the path. If none use the original name. + for path in dyld_library_path(env): + yield os.path.join(path, os.path.basename(name)) + +def dyld_executable_path_search(name, executable_path=None): + # If we haven't done any searching and found a library and the + # dylib_name starts with "@executable_path/" then construct the + # library name. + if name.startswith('@executable_path/') and executable_path is not None: + yield os.path.join(executable_path, name[len('@executable_path/'):]) + +def dyld_default_search(name, env=None): + yield name + + framework = framework_info(name) + + if framework is not None: + fallback_framework_path = dyld_fallback_framework_path(env) + for path in fallback_framework_path: + yield os.path.join(path, framework['name']) + + fallback_library_path = dyld_fallback_library_path(env) + for path in fallback_library_path: + yield os.path.join(path, os.path.basename(name)) + + if framework is not None and not fallback_framework_path: + for path in DEFAULT_FRAMEWORK_FALLBACK: + yield os.path.join(path, framework['name']) + + if not fallback_library_path: + for path in DEFAULT_LIBRARY_FALLBACK: + yield os.path.join(path, os.path.basename(name)) + +def dyld_find(name, executable_path=None, env=None): + """ + Find a library or framework using dyld semantics + """ + for path in dyld_image_suffix_search(chain( + dyld_override_search(name, env), + dyld_executable_path_search(name, executable_path), + dyld_default_search(name, env), + ), env): + + if os.path.isfile(path): + return path + try: + if _dyld_shared_cache_contains_path(path): + return path + except NotImplementedError: + pass + + raise ValueError("dylib %s could not be found" % (name,)) + +def framework_find(fn, executable_path=None, env=None): + """ + Find a framework using dyld semantics in a very loose manner. + + Will take input such as: + Python + Python.framework + Python.framework/Versions/Current + """ + error = None + try: + return dyld_find(fn, executable_path=executable_path, env=env) + except ValueError as e: + error = e + fmwk_index = fn.rfind('.framework') + if fmwk_index == -1: + fmwk_index = len(fn) + fn += '.framework' + fn = os.path.join(fn, os.path.basename(fn[:fmwk_index])) + try: + return dyld_find(fn, executable_path=executable_path, env=env) + except ValueError: + raise error + finally: + error = None + +def test_dyld_find(): + env = {} + assert dyld_find('libSystem.dylib') == '/usr/lib/libSystem.dylib' + assert dyld_find('System.framework/System') == '/System/Library/Frameworks/System.framework/System' + +if __name__ == '__main__': + test_dyld_find() diff --git a/Lib/ctypes/macholib/dylib.py b/Lib/ctypes/macholib/dylib.py new file mode 100644 index 0000000000..aa107507bd --- /dev/null +++ b/Lib/ctypes/macholib/dylib.py @@ -0,0 +1,63 @@ +""" +Generic dylib path manipulation +""" + +import re + +__all__ = ['dylib_info'] + +DYLIB_RE = re.compile(r"""(?x) +(?P^.*)(?:^|/) +(?P + (?P\w+?) + (?:\.(?P[^._]+))? + (?:_(?P[^._]+))? + \.dylib$ +) +""") + +def dylib_info(filename): + """ + A dylib name can take one of the following four forms: + Location/Name.SomeVersion_Suffix.dylib + Location/Name.SomeVersion.dylib + Location/Name_Suffix.dylib + Location/Name.dylib + + returns None if not found or a mapping equivalent to: + dict( + location='Location', + name='Name.SomeVersion_Suffix.dylib', + shortname='Name', + version='SomeVersion', + suffix='Suffix', + ) + + Note that SomeVersion and Suffix are optional and may be None + if not present. + """ + is_dylib = DYLIB_RE.match(filename) + if not is_dylib: + return None + return is_dylib.groupdict() + + +def test_dylib_info(): + def d(location=None, name=None, shortname=None, version=None, suffix=None): + return dict( + location=location, + name=name, + shortname=shortname, + version=version, + suffix=suffix + ) + assert dylib_info('completely/invalid') is None + assert dylib_info('completely/invalide_debug') is None + assert dylib_info('P/Foo.dylib') == d('P', 'Foo.dylib', 'Foo') + assert dylib_info('P/Foo_debug.dylib') == d('P', 'Foo_debug.dylib', 'Foo', suffix='debug') + assert dylib_info('P/Foo.A.dylib') == d('P', 'Foo.A.dylib', 'Foo', 'A') + assert dylib_info('P/Foo_debug.A.dylib') == d('P', 'Foo_debug.A.dylib', 'Foo_debug', 'A') + assert dylib_info('P/Foo.A_debug.dylib') == d('P', 'Foo.A_debug.dylib', 'Foo', 'A', 'debug') + +if __name__ == '__main__': + test_dylib_info() diff --git a/Lib/ctypes/macholib/fetch_macholib b/Lib/ctypes/macholib/fetch_macholib new file mode 100755 index 0000000000..e6d6a22659 --- /dev/null +++ b/Lib/ctypes/macholib/fetch_macholib @@ -0,0 +1,2 @@ +#!/bin/sh +svn export --force http://svn.red-bean.com/bob/macholib/trunk/macholib/ . diff --git a/Lib/ctypes/macholib/fetch_macholib.bat b/Lib/ctypes/macholib/fetch_macholib.bat new file mode 100644 index 0000000000..f9e1c0dc96 --- /dev/null +++ b/Lib/ctypes/macholib/fetch_macholib.bat @@ -0,0 +1 @@ +svn export --force http://svn.red-bean.com/bob/macholib/trunk/macholib/ . diff --git a/Lib/ctypes/macholib/framework.py b/Lib/ctypes/macholib/framework.py new file mode 100644 index 0000000000..ad6ed554ba --- /dev/null +++ b/Lib/ctypes/macholib/framework.py @@ -0,0 +1,65 @@ +""" +Generic framework path manipulation +""" + +import re + +__all__ = ['framework_info'] + +STRICT_FRAMEWORK_RE = re.compile(r"""(?x) +(?P^.*)(?:^|/) +(?P + (?P\w+).framework/ + (?:Versions/(?P[^/]+)/)? + (?P=shortname) + (?:_(?P[^_]+))? +)$ +""") + +def framework_info(filename): + """ + A framework name can take one of the following four forms: + Location/Name.framework/Versions/SomeVersion/Name_Suffix + Location/Name.framework/Versions/SomeVersion/Name + Location/Name.framework/Name_Suffix + Location/Name.framework/Name + + returns None if not found, or a mapping equivalent to: + dict( + location='Location', + name='Name.framework/Versions/SomeVersion/Name_Suffix', + shortname='Name', + version='SomeVersion', + suffix='Suffix', + ) + + Note that SomeVersion and Suffix are optional and may be None + if not present + """ + is_framework = STRICT_FRAMEWORK_RE.match(filename) + if not is_framework: + return None + return is_framework.groupdict() + +def test_framework_info(): + def d(location=None, name=None, shortname=None, version=None, suffix=None): + return dict( + location=location, + name=name, + shortname=shortname, + version=version, + suffix=suffix + ) + assert framework_info('completely/invalid') is None + assert framework_info('completely/invalid/_debug') is None + assert framework_info('P/F.framework') is None + assert framework_info('P/F.framework/_debug') is None + assert framework_info('P/F.framework/F') == d('P', 'F.framework/F', 'F') + assert framework_info('P/F.framework/F_debug') == d('P', 'F.framework/F_debug', 'F', suffix='debug') + assert framework_info('P/F.framework/Versions') is None + assert framework_info('P/F.framework/Versions/A') is None + assert framework_info('P/F.framework/Versions/A/F') == d('P', 'F.framework/Versions/A/F', 'F', 'A') + assert framework_info('P/F.framework/Versions/A/F_debug') == d('P', 'F.framework/Versions/A/F_debug', 'F', 'A', 'debug') + +if __name__ == '__main__': + test_framework_info() diff --git a/Lib/ctypes/test/__init__.py b/Lib/ctypes/test/__init__.py new file mode 100644 index 0000000000..659a392b7e --- /dev/null +++ b/Lib/ctypes/test/__init__.py @@ -0,0 +1,16 @@ +import os +import unittest +from test import support +from test.support import import_module + + +# skip tests if _ctypes was not built +ctypes = 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) \ No newline at end of file diff --git a/Lib/ctypes/test/__main__.py b/Lib/ctypes/test/__main__.py new file mode 100644 index 0000000000..b59e271e2a --- /dev/null +++ b/Lib/ctypes/test/__main__.py @@ -0,0 +1,5 @@ + +from ctypes.test import load_tests +import unittest + +unittest.main() \ No newline at end of file diff --git a/Lib/ctypes/test/test_arrays.py b/Lib/ctypes/test/test_arrays.py new file mode 100644 index 0000000000..14603b7049 --- /dev/null +++ b/Lib/ctypes/test/test_arrays.py @@ -0,0 +1,238 @@ +import unittest +from test.support import bigmemtest, _2G +import sys +from ctypes import * + +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_simple(self): + # create classes holding simple numeric types, and check + # various properties. + + init = list(range(15, 25)) + + for fmt in formats: + alen = len(init) + int_array = ARRAY(fmt, alen) + + ia = int_array(*init) + # length of instance ok? + self.assertEqual(len(ia), alen) + + # slot values ok? + values = [ia[i] for i in range(alen)] + self.assertEqual(values, init) + + # out-of-bounds accesses should be caught + with self.assertRaises(IndexError): ia[alen] + 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)] + values = [ia[i] for i in range(alen)] + self.assertEqual(values, new_values) + + # are the items initialized to 0? + ia = int_array() + values = [ia[i] for i in range(alen)] + self.assertEqual(values, [0] * alen) + + # Too many initializers should be caught + self.assertRaises(IndexError, int_array, *range(alen*2)) + + CharArray = ARRAY(c_char, 3) + + ca = CharArray(b"a", b"b", b"c") + + # Should this work? It doesn't: + # CharArray("abc") + self.assertRaises(TypeError, CharArray, "abc") + + self.assertEqual(ca[0], b"a") + self.assertEqual(ca[1], b"b") + self.assertEqual(ca[2], b"c") + self.assertEqual(ca[-3], b"a") + self.assertEqual(ca[-2], b"b") + self.assertEqual(ca[-1], b"c") + + self.assertEqual(len(ca), 3) + + # cannot delete items + from operator import delitem + self.assertRaises(TypeError, delitem, ca, 0) + + def test_step_overflow(self): + a = (c_int * 5)() + a[3::sys.maxsize] = (1,) + self.assertListEqual(a[3::sys.maxsize], [1]) + a = (c_char * 5)() + a[3::sys.maxsize] = b"A" + self.assertEqual(a[3::sys.maxsize], b"A") + a = (c_wchar * 5)() + a[3::sys.maxsize] = u"X" + self.assertEqual(a[3::sys.maxsize], u"X") + + def test_numeric_arrays(self): + + alen = 5 + + numarray = ARRAY(c_int, alen) + + na = numarray() + values = [na[i] for i in range(alen)] + self.assertEqual(values, [0] * alen) + + na = numarray(*[c_int()] * alen) + values = [na[i] for i in range(alen)] + self.assertEqual(values, [0]*alen) + + na = numarray(1, 2, 3, 4, 5) + values = [i for i in na] + self.assertEqual(values, [1, 2, 3, 4, 5]) + + na = numarray(*map(c_int, (1, 2, 3, 4, 5))) + values = [i for i in na] + self.assertEqual(values, [1, 2, 3, 4, 5]) + + def test_classcache(self): + self.assertIsNot(ARRAY(c_int, 3), ARRAY(c_int, 4)) + self.assertIs(ARRAY(c_int, 3), ARRAY(c_int, 3)) + + def test_from_address(self): + # Failed with 0.9.8, reported by JUrner + p = create_string_buffer(b"foo") + sz = (c_char * 3).from_address(addressof(p)) + self.assertEqual(sz[:], b"foo") + self.assertEqual(sz[::], b"foo") + self.assertEqual(sz[::-1], b"oof") + self.assertEqual(sz[::3], b"f") + 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)) + self.assertEqual(sz[:], "foo") + self.assertEqual(sz[::], "foo") + self.assertEqual(sz[::-1], "oof") + self.assertEqual(sz[::3], "f") + self.assertEqual(sz[1:4:2], "o") + self.assertEqual(sz.value, "foo") + + def test_cache(self): + # Array types are cached internally in the _ctypes extension, + # in a WeakValueDictionary. Make sure the array type is + # removed from the cache when the itemtype goes away. This + # test will not fail, but will show a leak in the testsuite. + + # Create a new type: + class my_int(c_int): + pass + # Create a new array type based on it: + t1 = my_int * 1 + t2 = my_int * 1 + self.assertIs(t1, t2) + + def test_subclass(self): + class T(Array): + _type_ = c_int + _length_ = 13 + class U(T): + pass + class V(U): + pass + class W(V): + pass + class X(T): + _type_ = c_short + class Y(T): + _length_ = 187 + + for c in [T, U, V, W]: + self.assertEqual(c._type_, c_int) + self.assertEqual(c._length_, 13) + self.assertEqual(c()._type_, c_int) + self.assertEqual(c()._length_, 13) + + self.assertEqual(X._type_, c_short) + self.assertEqual(X._length_, 13) + self.assertEqual(X()._type_, c_short) + self.assertEqual(X()._length_, 13) + + self.assertEqual(Y._type_, c_int) + self.assertEqual(Y._length_, 187) + self.assertEqual(Y()._type_, c_int) + self.assertEqual(Y()._length_, 187) + + def test_bad_subclass(self): + with self.assertRaises(AttributeError): + class T(Array): + pass + with self.assertRaises(AttributeError): + class T(Array): + _type_ = c_int + with self.assertRaises(AttributeError): + class T(Array): + _length_ = 13 + + def test_bad_length(self): + with self.assertRaises(ValueError): + class T(Array): + _type_ = c_int + _length_ = - sys.maxsize * 2 + with self.assertRaises(ValueError): + class T(Array): + _type_ = c_int + _length_ = -1 + with self.assertRaises(TypeError): + class T(Array): + _type_ = c_int + _length_ = 1.87 + with self.assertRaises(OverflowError): + class T(Array): + _type_ = c_int + _length_ = sys.maxsize * 2 + + def test_zero_length(self): + # _length_ can be zero. + class T(Array): + _type_ = c_int + _length_ = 0 + + def test_empty_element_struct(self): + class EmptyStruct(Structure): + _fields_ = [] + + obj = (EmptyStruct * 2)() # bpo37188: Floating point exception + self.assertEqual(sizeof(obj), 0) + + def test_empty_element_array(self): + class EmptyArray(Array): + _type_ = c_int + _length_ = 0 + + obj = (EmptyArray * 2)() # bpo37188: Floating point exception + self.assertEqual(sizeof(obj), 0) + + def test_bpo36504_signed_int_overflow(self): + # The overflow check in PyCArrayType_new() could cause signed integer + # overflow. + with self.assertRaises(OverflowError): + c_char * sys.maxsize * 2 + + @unittest.skipUnless(sys.maxsize > 2**32, 'requires 64bit platform') + @bigmemtest(size=_2G, memuse=1, dry_run=False) + def test_large_array(self, size): + c_char * size + +if __name__ == '__main__': + unittest.main() diff --git a/Lib/ctypes/test/test_buffers.py b/Lib/ctypes/test/test_buffers.py new file mode 100644 index 0000000000..15782be757 --- /dev/null +++ b/Lib/ctypes/test/test_buffers.py @@ -0,0 +1,73 @@ +from ctypes import * +from ctypes.test import need_symbol +import unittest + +class StringBufferTestCase(unittest.TestCase): + + def test_buffer(self): + b = create_string_buffer(32) + self.assertEqual(len(b), 32) + self.assertEqual(sizeof(b), 32 * sizeof(c_char)) + self.assertIs(type(b[0]), bytes) + + b = create_string_buffer(b"abc") + self.assertEqual(len(b), 4) # trailing nul char + self.assertEqual(sizeof(b), 4 * sizeof(c_char)) + self.assertIs(type(b[0]), bytes) + self.assertEqual(b[0], b"a") + self.assertEqual(b[:], b"abc\0") + self.assertEqual(b[::], b"abc\0") + self.assertEqual(b[::-1], b"\0cba") + self.assertEqual(b[::2], b"ac") + self.assertEqual(b[::5], b"a") + + self.assertRaises(TypeError, create_string_buffer, "abc") + + 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) + self.assertEqual(sizeof(b), 32 * sizeof(c_wchar)) + self.assertIs(type(b[0]), str) + + b = create_unicode_buffer("abc") + self.assertEqual(len(b), 4) # trailing nul char + self.assertEqual(sizeof(b), 4 * sizeof(c_wchar)) + self.assertIs(type(b[0]), str) + self.assertEqual(b[0], "a") + self.assertEqual(b[:], "abc\0") + self.assertEqual(b[::], "abc\0") + self.assertEqual(b[::-1], "\0cba") + self.assertEqual(b[::2], "ac") + self.assertEqual(b[::5], "a") + + 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 + self.assertEqual(sizeof(b), 4 * sizeof(c_wchar)) + self.assertIs(type(b[0]), str) + self.assertEqual(b[0], "a") + self.assertEqual(b[:], "abc\0") + self.assertEqual(b[::], "abc\0") + self.assertEqual(b[::-1], "\0cba") + 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': + b = create_unicode_buffer(s) + self.assertEqual(len(b), expected) + self.assertEqual(b[-1], '\0') + + +if __name__ == "__main__": + unittest.main() diff --git a/Lib/ctypes/test/test_bytes.py b/Lib/ctypes/test/test_bytes.py new file mode 100644 index 0000000000..092ec5af05 --- /dev/null +++ b/Lib/ctypes/test/test_bytes.py @@ -0,0 +1,66 @@ +"""Test where byte objects are accepted""" +import unittest +import sys +from ctypes import * + +class BytesTest(unittest.TestCase): + def test_c_char(self): + x = c_char(b"x") + self.assertRaises(TypeError, c_char, "x") + x.value = b"y" + with self.assertRaises(TypeError): + x.value = "y" + c_char.from_param(b"x") + self.assertRaises(TypeError, c_char.from_param, "x") + self.assertIn('xbd', repr(c_char.from_param(b"\xbd"))) + (c_char * 3)(b"a", b"b", b"c") + self.assertRaises(TypeError, c_char * 3, "a", "b", "c") + + def test_c_wchar(self): + x = c_wchar("x") + self.assertRaises(TypeError, c_wchar, b"x") + x.value = "y" + with self.assertRaises(TypeError): + x.value = b"y" + c_wchar.from_param("x") + self.assertRaises(TypeError, c_wchar.from_param, b"x") + (c_wchar * 3)("a", "b", "c") + self.assertRaises(TypeError, c_wchar * 3, b"a", b"b", b"c") + + def test_c_char_p(self): + c_char_p(b"foo bar") + self.assertRaises(TypeError, c_char_p, "foo bar") + + def test_c_wchar_p(self): + c_wchar_p("foo bar") + self.assertRaises(TypeError, c_wchar_p, b"foo bar") + + def test_struct(self): + class X(Structure): + _fields_ = [("a", c_char * 3)] + + x = X(b"abc") + self.assertRaises(TypeError, X, "abc") + self.assertEqual(x.a, b"abc") + self.assertEqual(type(x.a), bytes) + + def test_struct_W(self): + class X(Structure): + _fields_ = [("a", c_wchar * 3)] + + x = X("abc") + self.assertRaises(TypeError, X, b"abc") + self.assertEqual(x.a, "abc") + self.assertEqual(type(x.a), str) + + @unittest.skipUnless(sys.platform == "win32", 'Windows-specific test') + def test_BSTR(self): + from _ctypes import _SimpleCData + class BSTR(_SimpleCData): + _type_ = "X" + + BSTR("abc") + + +if __name__ == '__main__': + unittest.main() diff --git a/Lib/ctypes/test/test_funcptr.py b/Lib/ctypes/test/test_funcptr.py new file mode 100644 index 0000000000..e0b9b54e97 --- /dev/null +++ b/Lib/ctypes/test/test_funcptr.py @@ -0,0 +1,132 @@ +import unittest +from ctypes import * + +try: + WINFUNCTYPE +except NameError: + # fake to enable this test on Linux + WINFUNCTYPE = CFUNCTYPE + +import _ctypes_test +lib = CDLL(_ctypes_test.__file__) + +class CFuncPtrTestCase(unittest.TestCase): + def test_basic(self): + X = WINFUNCTYPE(c_int, c_int, c_int) + + def func(*args): + return len(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)) + + def test_first(self): + StdCallback = WINFUNCTYPE(c_int, c_int, c_int) + CdeclCallback = CFUNCTYPE(c_int, c_int, c_int) + + def func(a, b): + return a + b + + s = StdCallback(func) + c = CdeclCallback(func) + + self.assertEqual(s(1, 2), 3) + self.assertEqual(c(1, 2), 3) + # The following no longer raises a TypeError - it is now + # 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: + self.assertRaises(TypeError, s, 1, 2, 3) + + def test_structures(self): + WNDPROC = WINFUNCTYPE(c_long, c_int, c_int, c_int, c_int) + + def wndproc(hwnd, msg, wParam, lParam): + return hwnd + msg + wParam + lParam + + HINSTANCE = c_int + HICON = c_int + HCURSOR = c_int + LPCTSTR = c_char_p + + class WNDCLASS(Structure): + _fields_ = [("style", c_uint), + ("lpfnWndProc", WNDPROC), + ("cbClsExtra", c_int), + ("cbWndExtra", c_int), + ("hInstance", HINSTANCE), + ("hIcon", HICON), + ("hCursor", HCURSOR), + ("lpszMenuName", LPCTSTR), + ("lpszClassName", LPCTSTR)] + + wndclass = WNDCLASS() + wndclass.lpfnWndProc = WNDPROC(wndproc) + + 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 + del wndproc + + 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 + return (c_char*size)(*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/ctypes/test/test_functions.py new file mode 100644 index 0000000000..d3c6536f27 --- /dev/null +++ b/Lib/ctypes/test/test_functions.py @@ -0,0 +1,402 @@ +""" +Here is probably the place to write the docs, since the test-cases +show how the type behave. + +Later... +""" + +from ctypes import * +from ctypes.test import need_symbol +import sys, unittest + +try: + WINFUNCTYPE +except NameError: + # 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__) + +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): + # in Python 2.3, this raises TypeError: MRO conflict among bases classes, + # in Python 2.2 it works. + # + # But in early versions of _ctypes.c, the result of tp_new + # wasn't checked, and it even crashed Python. + # Found by Greg Chapman. + + try: + class X(object, Array): + _length_ = 5 + _type_ = "i" + except TypeError: + pass + + + from _ctypes import _Pointer + try: + class X(object, _Pointer): + pass + except TypeError: + pass + + from _ctypes import _SimpleCData + try: + class X(object, _SimpleCData): + _type_ = "i" + except TypeError: + pass + + try: + class X(object, Structure): + _fields_ = [] + except TypeError: + pass + + + @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] + result = f(1, "x", 3, 4, 5.0, 6.0) + self.assertEqual(result, 139) + self.assertEqual(type(result), int) + + @need_symbol('c_wchar') + def test_wchar_result(self): + f = dll._testfunc_i_bhilfd + f.argtypes = [c_byte, c_short, c_int, c_long, c_float, c_double] + f.restype = c_wchar + result = f(0, 0, 0, 0, 0, 0) + self.assertEqual(result, '\x00') + + def test_voidresult(self): + f = dll._testfunc_v + f.restype = None + f.argtypes = [c_int, c_int, POINTER(c_int)] + result = c_int() + self.assertEqual(None, f(1, 2, byref(result))) + self.assertEqual(result.value, 3) + + def test_intresult(self): + f = dll._testfunc_i_bhilfd + f.argtypes = [c_byte, c_short, c_int, c_long, c_float, c_double] + f.restype = c_int + result = f(1, 2, 3, 4, 5.0, 6.0) + self.assertEqual(result, 21) + self.assertEqual(type(result), int) + + result = f(-1, -2, -3, -4, -5.0, -6.0) + self.assertEqual(result, -21) + self.assertEqual(type(result), int) + + # If we declare the function to return a short, + # is the high part split off? + f.restype = c_short + result = f(1, 2, 3, 4, 5.0, 6.0) + self.assertEqual(result, 21) + self.assertEqual(type(result), int) + + result = f(1, 2, 3, 0x10004, 5.0, 6.0) + self.assertEqual(result, 21) + self.assertEqual(type(result), int) + + # You cannot assign character format codes as restype any longer + self.assertRaises(TypeError, setattr, f, "restype", "i") + + def test_floatresult(self): + f = dll._testfunc_f_bhilfd + f.argtypes = [c_byte, c_short, c_int, c_long, c_float, c_double] + f.restype = c_float + result = f(1, 2, 3, 4, 5.0, 6.0) + self.assertEqual(result, 21) + self.assertEqual(type(result), float) + + result = f(-1, -2, -3, -4, -5.0, -6.0) + self.assertEqual(result, -21) + self.assertEqual(type(result), float) + + def test_doubleresult(self): + f = dll._testfunc_d_bhilfd + f.argtypes = [c_byte, c_short, c_int, c_long, c_float, c_double] + f.restype = c_double + result = f(1, 2, 3, 4, 5.0, 6.0) + self.assertEqual(result, 21) + self.assertEqual(type(result), float) + + result = f(-1, -2, -3, -4, -5.0, -6.0) + self.assertEqual(result, -21) + self.assertEqual(type(result), float) + + def test_longdoubleresult(self): + f = dll._testfunc_D_bhilfD + f.argtypes = [c_byte, c_short, c_int, c_long, c_float, c_longdouble] + f.restype = c_longdouble + result = f(1, 2, 3, 4, 5.0, 6.0) + self.assertEqual(result, 21) + self.assertEqual(type(result), float) + + result = f(-1, -2, -3, -4, -5.0, -6.0) + 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 + f.argtypes = [c_byte, c_short, c_int, c_long, c_float, c_double] + result = f(1, 2, 3, 4, 5.0, 6.0) + self.assertEqual(result, 21) + + f = dll._testfunc_q_bhilfdq + f.restype = c_longlong + f.argtypes = [c_byte, c_short, c_int, c_long, c_float, c_double, c_longlong] + result = f(1, 2, 3, 4, 5.0, 6.0, 21) + self.assertEqual(result, 42) + + def test_stringresult(self): + f = dll._testfunc_p_p + f.argtypes = None + f.restype = c_char_p + result = f(b"123") + self.assertEqual(result, b"123") + + result = f(None) + self.assertEqual(result, None) + + def test_pointers(self): + f = dll._testfunc_p_p + f.restype = POINTER(c_int) + f.argtypes = [POINTER(c_int)] + + # This only works if the value c_int(42) passed to the + # function is still alive while the pointer (the result) is + # used. + + v = c_int(42) + + self.assertEqual(pointer(v).contents.value, 42) + result = f(pointer(v)) + self.assertEqual(type(result), POINTER(c_int)) + self.assertEqual(result.contents.value, 42) + + # This on works... + result = f(pointer(v)) + self.assertEqual(result.contents.value, v.value) + + p = pointer(c_int(99)) + result = f(p) + self.assertEqual(result.contents.value, 99) + + arg = byref(v) + result = f(arg) + self.assertNotEqual(result.contents, v.value) + + self.assertRaises(ArgumentError, f, byref(c_short(22))) + + # It is dangerous, however, because you don't control the lifetime + # of the pointer: + result = f(byref(c_int(99))) + self.assertNotEqual(result.contents, 99) + + def test_errors(self): + f = dll._testfunc_p_p + f.restype = c_int + + class X(Structure): + _fields_ = [("y", c_int)] + + self.assertRaises(TypeError, f, X()) #cannot convert parameter + + ################################################################ + def test_shorts(self): + f = dll._testfunc_callback_i_if + + args = [] + expected = [262144, 131072, 65536, 32768, 16384, 8192, 4096, 2048, + 1024, 512, 256, 128, 64, 32, 16, 8, 4, 2, 1] + + def callback(v): + args.append(v) + return v + + CallBack = CFUNCTYPE(c_int, c_int) + + cb = CallBack(callback) + f(2**18, cb) + self.assertEqual(args, expected) + + ################################################################ + + + def test_callbacks(self): + f = dll._testfunc_callback_i_if + f.restype = c_int + f.argtypes = None + + MyCallback = CFUNCTYPE(c_int, c_int) + + def callback(value): + #print "called back with", value + return value + + cb = MyCallback(callback) + result = f(-10, cb) + self.assertEqual(result, -18) + + # test with prototype + f.argtypes = [c_int, MyCallback] + cb = MyCallback(callback) + result = f(-10, cb) + self.assertEqual(result, -18) + + AnotherCallback = WINFUNCTYPE(c_int, c_int, c_int, c_int, c_int) + + # check that the prototype works: we call f with wrong + # argument types + cb = AnotherCallback(callback) + self.assertRaises(ArgumentError, f, -10, cb) + + + def test_callbacks_2(self): + # Can also use simple datatypes as argument type specifiers + # for the callback function. + # In this case the call receives an instance of that type + f = dll._testfunc_callback_i_if + f.restype = c_int + + MyCallback = CFUNCTYPE(c_int, c_int) + + f.argtypes = [c_int, MyCallback] + + def callback(value): + #print "called back with", value + self.assertEqual(type(value), int) + return value + + cb = MyCallback(callback) + result = f(-10, cb) + self.assertEqual(result, -18) + + @need_symbol('c_longlong') + def test_longlong_callbacks(self): + + f = dll._testfunc_callback_q_qf + f.restype = c_longlong + + MyCallback = CFUNCTYPE(c_longlong, c_longlong) + + f.argtypes = [c_longlong, MyCallback] + + def callback(value): + self.assertIsInstance(value, int) + return value & 0x7FFFFFFF + + cb = MyCallback(callback) + + self.assertEqual(13577625587, f(1000000000000, cb)) + + def test_errors(self): + self.assertRaises(AttributeError, getattr, dll, "_xxx_yyy") + self.assertRaises(ValueError, c_int.in_dll, dll, "_xxx_yyy") + + def test_byval(self): + + # without prototype + ptin = POINT(1, 2) + ptout = POINT() + # EXPORT int _testfunc_byval(point in, point *pout) + result = dll._testfunc_byval(ptin, byref(ptout)) + got = result, ptout.x, ptout.y + expected = 3, 1, 2 + self.assertEqual(got, expected) + + # with prototype + ptin = POINT(101, 102) + ptout = POINT() + dll._testfunc_byval.argtypes = (POINT, POINTER(POINT)) + dll._testfunc_byval.restype = c_int + result = dll._testfunc_byval(ptin, byref(ptout)) + got = result, ptout.x, ptout.y + expected = 203, 101, 102 + self.assertEqual(got, expected) + + def test_struct_return_2H(self): + class S2H(Structure): + _fields_ = [("x", c_short), + ("y", c_short)] + dll.ret_2h_func.restype = S2H + dll.ret_2h_func.argtypes = [S2H] + inp = S2H(99, 88) + s2h = dll.ret_2h_func(inp) + self.assertEqual((s2h.x, s2h.y), (99*2, 88*3)) + + @unittest.skipUnless(sys.platform == "win32", 'Windows-specific test') + def test_struct_return_2H_stdcall(self): + class S2H(Structure): + _fields_ = [("x", c_short), + ("y", c_short)] + + windll.s_ret_2h_func.restype = S2H + windll.s_ret_2h_func.argtypes = [S2H] + s2h = windll.s_ret_2h_func(S2H(99, 88)) + self.assertEqual((s2h.x, s2h.y), (99*2, 88*3)) + + def test_struct_return_8H(self): + class S8I(Structure): + _fields_ = [("a", c_int), + ("b", c_int), + ("c", c_int), + ("d", c_int), + ("e", c_int), + ("f", c_int), + ("g", c_int), + ("h", c_int)] + dll.ret_8i_func.restype = S8I + dll.ret_8i_func.argtypes = [S8I] + inp = S8I(9, 8, 7, 6, 5, 4, 3, 2) + s8i = dll.ret_8i_func(inp) + self.assertEqual((s8i.a, s8i.b, s8i.c, s8i.d, s8i.e, s8i.f, s8i.g, s8i.h), + (9*2, 8*3, 7*4, 6*5, 5*6, 4*7, 3*8, 2*9)) + + @unittest.skipUnless(sys.platform == "win32", 'Windows-specific test') + def test_struct_return_8H_stdcall(self): + class S8I(Structure): + _fields_ = [("a", c_int), + ("b", c_int), + ("c", c_int), + ("d", c_int), + ("e", c_int), + ("f", c_int), + ("g", c_int), + ("h", c_int)] + windll.s_ret_8i_func.restype = S8I + windll.s_ret_8i_func.argtypes = [S8I] + inp = S8I(9, 8, 7, 6, 5, 4, 3, 2) + s8i = windll.s_ret_8i_func(inp) + self.assertEqual( + (s8i.a, s8i.b, s8i.c, s8i.d, s8i.e, s8i.f, s8i.g, s8i.h), + (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 + + proto = CFUNCTYPE(c_int, RECT, POINT) + def callback(*args): + return 0 + + 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_loading.py b/Lib/ctypes/test/test_loading.py new file mode 100644 index 0000000000..ea892277c4 --- /dev/null +++ b/Lib/ctypes/test/test_loading.py @@ -0,0 +1,182 @@ +from ctypes import * +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 +from ctypes.util import find_library + +libc_name = None + +def setUpModule(): + global libc_name + if os.name == "nt": + libc_name = find_library("c") + elif sys.platform == "cygwin": + libc_name = "cygwin1.dll" + else: + libc_name = find_library("c") + + 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)) + self.assertRaises(OSError, CDLL, self.unknowndll) + + def test_load_version(self): + if libc_name is None: + self.skipTest('could not find libc') + if os.path.basename(libc_name) != 'libc.so.6': + self.skipTest('wrong libc path for test') + cdll.LoadLibrary("libc.so.6") + # linux uses version, libc 9 should not exist + self.assertRaises(OSError, cdll.LoadLibrary, "libc.so.9") + self.assertRaises(OSError, cdll.LoadLibrary, self.unknowndll) + + def test_find(self): + for name in ("c", "m"): + lib = find_library(name) + if lib: + cdll.LoadLibrary(lib) + CDLL(lib) + + @unittest.skipUnless(os.name == "nt", + 'test specific to Windows') + def test_load_library(self): + # CRT is no longer directly loadable. See issue23606 for the + # discussion about alternative approaches. + #self.assertIsNotNone(libc_name) + if test.support.verbose: + print(find_library("kernel32")) + print(find_library("user32")) + + if os.name == "nt": + windll.kernel32.GetModuleHandleW + windll["kernel32"].GetModuleHandleW + windll.LoadLibrary("kernel32").GetModuleHandleW + WinDLL("kernel32").GetModuleHandleW + # embedded null character + self.assertRaises(ValueError, 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__) + # We load the same function both via ordinal and name + func_ord = dll[2] + func_name = dll.GetString + # addressof gets the address where the function pointer is stored + a_ord = addressof(func_ord) + a_name = addressof(func_name) + f_ord_addr = c_void_p.from_address(a_ord).value + f_name_addr = c_void_p.from_address(a_name).value + self.assertEqual(hex(f_ord_addr), hex(f_name_addr)) + + self.assertRaises(AttributeError, dll.__getitem__, 1234) + + @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) + + @unittest.skipUnless(os.name == "nt", 'Windows-specific test') + def test_1703286_B(self): + # Since on winXP 64-bit advapi32 loads like described + # 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 + # 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") + self.assertTrue(proc) + # This is the real test: call the function via 'call_function' + self.assertEqual(0, call_function(proc, (None,))) + + @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"): + ext = "_d.dll" + else: + ext = ".dll" + + with os_helper.temp_dir() as tmp: + # We copy two files and load _sqlite3.dll (formerly .pyd), + # which has a dependency on sqlite3.dll. Then we test + # loading it in subprocesses to avoid it starting in memory + # for each test. + target = os.path.join(tmp, "_sqlite3.dll") + shutil.copy(src, target) + shutil.copy(os.path.join(os.path.dirname(src), "sqlite3" + ext), + os.path.join(tmp, "sqlite3" + ext)) + + def should_pass(command): + with self.subTest(command): + subprocess.check_output( + [sys.executable, "-c", + "from ctypes import *; import nt;" + command], + cwd=tmp + ) + + def should_fail(command): + with self.subTest(command): + with self.assertRaises(subprocess.CalledProcessError): + subprocess.check_output( + [sys.executable, "-c", + "from ctypes import *; import nt;" + command], + cwd=tmp, stderr=subprocess.STDOUT, + ) + + # Default load should not find this in CWD + should_fail("WinDLL('_sqlite3.dll')") + + # Relative path (but not just filename) should succeed + should_pass("WinDLL('./_sqlite3.dll')") + + # Insecure load flags should succeed + # Clear the DLL directory to avoid safe search settings propagating + should_pass("windll.kernel32.SetDllDirectoryW(None); WinDLL('_sqlite3.dll', winmode=0)") + + # Full path load without DLL_LOAD_DIR shouldn't find dependency + should_fail("WinDLL(nt._getfullpathname('_sqlite3.dll'), " + + "winmode=nt._LOAD_LIBRARY_SEARCH_SYSTEM32)") + + # Full path load with DLL_LOAD_DIR should succeed + should_pass("WinDLL(nt._getfullpathname('_sqlite3.dll'), " + + "winmode=nt._LOAD_LIBRARY_SEARCH_SYSTEM32|" + + "nt._LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR)") + + # User-specified directory should succeed + should_pass("import os; p = os.add_dll_directory(os.getcwd());" + + "WinDLL('_sqlite3.dll'); p.close()") + + + +if __name__ == "__main__": + unittest.main() diff --git a/Lib/ctypes/test/test_strings.py b/Lib/ctypes/test/test_strings.py new file mode 100644 index 0000000000..5434efda10 --- /dev/null +++ b/Lib/ctypes/test/test_strings.py @@ -0,0 +1,232 @@ +import unittest +from ctypes import * +from ctypes.test import need_symbol + +class StringArrayTestCase(unittest.TestCase): + def test(self): + BUF = c_char * 4 + + buf = BUF(b"a", b"b", b"c") + self.assertEqual(buf.value, b"abc") + self.assertEqual(buf.raw, b"abc\000") + + buf.value = b"ABCD" + self.assertEqual(buf.value, b"ABCD") + self.assertEqual(buf.raw, b"ABCD") + + buf.value = b"x" + self.assertEqual(buf.value, b"x") + self.assertEqual(buf.raw, b"x\000CD") + + buf[1] = b"Z" + self.assertEqual(buf.value, b"xZCD") + self.assertEqual(buf.raw, b"xZCD") + + self.assertRaises(ValueError, setattr, buf, "value", b"aaaaaaaa") + self.assertRaises(TypeError, setattr, buf, "value", 42) + + def test_c_buffer_value(self): + buf = c_buffer(32) + + buf.value = b"Hello, World" + self.assertEqual(buf.value, b"Hello, World") + + self.assertRaises(TypeError, setattr, buf, "value", memoryview(b"Hello, World")) + self.assertRaises(TypeError, setattr, buf, "value", memoryview(b"abc")) + self.assertRaises(ValueError, setattr, buf, "raw", memoryview(b"x" * 100)) + + def test_c_buffer_raw(self): + buf = c_buffer(32) + + buf.raw = memoryview(b"Hello, World") + self.assertEqual(buf.value, b"Hello, World") + self.assertRaises(TypeError, setattr, buf, "value", memoryview(b"abc")) + self.assertRaises(ValueError, setattr, buf, "raw", memoryview(b"x" * 100)) + + def test_param_1(self): + BUF = c_char * 4 + buf = BUF() +## print c_char_p.from_param(buf) + + def test_param_2(self): + BUF = c_char * 4 + buf = BUF() +## print BUF.from_param(c_char_p("python")) +## print BUF.from_param(BUF(*"pyth")) + + def test_del_segfault(self): + BUF = c_char * 4 + buf = BUF() + with self.assertRaises(AttributeError): + del buf.raw + + +@need_symbol('c_wchar') +class WStringArrayTestCase(unittest.TestCase): + def test(self): + BUF = c_wchar * 4 + + buf = BUF("a", "b", "c") + self.assertEqual(buf.value, "abc") + + buf.value = "ABCD" + self.assertEqual(buf.value, "ABCD") + + buf.value = "x" + self.assertEqual(buf.value, "x") + + buf[1] = "Z" + self.assertEqual(buf.value, "xZCD") + + @unittest.skipIf(sizeof(c_wchar) < 4, + "sizeof(wchar_t) is smaller than 4 bytes") + def test_nonbmp(self): + u = chr(0x10ffff) + w = c_wchar(u) + self.assertEqual(w.value, u) + +class StringTestCase(unittest.TestCase): + @unittest.skip('test disabled') + def test_basic_strings(self): + cs = c_string("abcdef") + + # Cannot call len on a c_string any longer + self.assertRaises(TypeError, len, cs) + self.assertEqual(sizeof(cs), 7) + + # The value property is the string up to the first terminating NUL. + self.assertEqual(cs.value, "abcdef") + self.assertEqual(c_string("abc\000def").value, "abc") + + # The raw property is the total buffer contents: + self.assertEqual(cs.raw, "abcdef\000") + self.assertEqual(c_string("abc\000def").raw, "abc\000def\000") + + # We can change the value: + cs.value = "ab" + self.assertEqual(cs.value, "ab") + self.assertEqual(cs.raw, "ab\000\000\000\000\000") + + cs.raw = "XY" + self.assertEqual(cs.value, "XY") + self.assertEqual(cs.raw, "XY\000\000\000\000\000") + + self.assertRaises(TypeError, c_string, "123") + + @unittest.skip('test disabled') + def test_sized_strings(self): + + # New in releases later than 0.4.0: + self.assertRaises(TypeError, c_string, None) + + # New in releases later than 0.4.0: + # c_string(number) returns an empty string of size number + self.assertEqual(len(c_string(32).raw), 32) + self.assertRaises(ValueError, c_string, -1) + self.assertRaises(ValueError, c_string, 0) + + # These tests fail, because it is no longer initialized +## self.assertEqual(c_string(2).value, "") +## self.assertEqual(c_string(2).raw, "\000\000") + self.assertEqual(c_string(2).raw[-1], "\000") + self.assertEqual(len(c_string(2).raw), 2) + + @unittest.skip('test disabled') + def test_initialized_strings(self): + + self.assertEqual(c_string("ab", 4).raw[:2], "ab") + self.assertEqual(c_string("ab", 4).raw[:2:], "ab") + self.assertEqual(c_string("ab", 4).raw[:2:-1], "ba") + self.assertEqual(c_string("ab", 4).raw[:2:2], "a") + self.assertEqual(c_string("ab", 4).raw[-1], "\000") + self.assertEqual(c_string("ab", 2).raw, "a\000") + + @unittest.skip('test disabled') + def test_toolong(self): + cs = c_string("abcdef") + # Much too long string: + self.assertRaises(ValueError, setattr, cs, "value", "123456789012345") + + # One char too long values: + self.assertRaises(ValueError, setattr, cs, "value", "1234567") + + @unittest.skip('test disabled') + def test_perf(self): + check_perf() + +@need_symbol('c_wchar') +class WStringTestCase(unittest.TestCase): + def test_wchar(self): + c_wchar("x") + repr(byref(c_wchar("x"))) + c_wchar("x") + + + @unittest.skip('test disabled') + def test_basic_wstrings(self): + cs = c_wstring("abcdef") + + # XXX This behaviour is about to change: + # len returns the size of the internal buffer in bytes. + # This includes the terminating NUL character. + self.assertEqual(sizeof(cs), 14) + + # The value property is the string up to the first terminating NUL. + self.assertEqual(cs.value, "abcdef") + self.assertEqual(c_wstring("abc\000def").value, "abc") + + self.assertEqual(c_wstring("abc\000def").value, "abc") + + # The raw property is the total buffer contents: + self.assertEqual(cs.raw, "abcdef\000") + self.assertEqual(c_wstring("abc\000def").raw, "abc\000def\000") + + # We can change the value: + cs.value = "ab" + self.assertEqual(cs.value, "ab") + self.assertEqual(cs.raw, "ab\000\000\000\000\000") + + self.assertRaises(TypeError, c_wstring, "123") + self.assertRaises(ValueError, c_wstring, 0) + + @unittest.skip('test disabled') + def test_toolong(self): + cs = c_wstring("abcdef") + # Much too long string: + self.assertRaises(ValueError, setattr, cs, "value", "123456789012345") + + # One char too long values: + self.assertRaises(ValueError, setattr, cs, "value", "1234567") + + +def run_test(rep, msg, func, arg): + items = range(rep) + from time import perf_counter as clock + start = clock() + for i in items: + func(arg); func(arg); func(arg); func(arg); func(arg) + stop = clock() + print("%20s: %.2f us" % (msg, ((stop-start)*1e6/5/rep))) + +def check_perf(): + # Construct 5 objects + + REP = 200000 + + run_test(REP, "c_string(None)", c_string, None) + run_test(REP, "c_string('abc')", c_string, 'abc') + +# Python 2.3 -OO, win2k, P4 700 MHz: +# +# c_string(None): 1.75 us +# c_string('abc'): 2.74 us + +# Python 2.2 -OO, win2k, P4 700 MHz: +# +# c_string(None): 2.95 us +# c_string('abc'): 3.67 us + + +if __name__ == '__main__': +## check_perf() + unittest.main() diff --git a/Lib/ctypes/test/test_unicode.py b/Lib/ctypes/test/test_unicode.py new file mode 100644 index 0000000000..60c75424b7 --- /dev/null +++ b/Lib/ctypes/test/test_unicode.py @@ -0,0 +1,64 @@ +import unittest +import ctypes +from ctypes.test import need_symbol + +import _ctypes_test + +@need_symbol('c_wchar') +class UnicodeTestCase(unittest.TestCase): + def test_wcslen(self): + dll = ctypes.CDLL(_ctypes_test.__file__) + wcslen = dll.my_wcslen + wcslen.argtypes = [ctypes.c_wchar_p] + + self.assertEqual(wcslen("abc"), 3) + self.assertEqual(wcslen("ab\u2070"), 3) + self.assertRaises(ctypes.ArgumentError, wcslen, b"ab\xe4") + + def test_buffers(self): + buf = ctypes.create_unicode_buffer("abc") + self.assertEqual(len(buf), 3+1) + + buf = ctypes.create_unicode_buffer("ab\xe4\xf6\xfc") + self.assertEqual(buf[:], "ab\xe4\xf6\xfc\0") + self.assertEqual(buf[::], "ab\xe4\xf6\xfc\0") + self.assertEqual(buf[::-1], '\x00\xfc\xf6\xe4ba') + self.assertEqual(buf[::2], 'a\xe4\xfc') + self.assertEqual(buf[6:5:-1], "") + + def test_embedded_null(self): + class TestStruct(ctypes.Structure): + _fields_ = [("unicode", ctypes.c_wchar_p)] + t = TestStruct() + # This would raise a ValueError: + t.unicode = "foo\0bar\0\0" + + +func = ctypes.CDLL(_ctypes_test.__file__)._testfunc_p_p + +class StringTestCase(UnicodeTestCase): + def setUp(self): + func.argtypes = [ctypes.c_char_p] + func.restype = ctypes.c_char_p + + def tearDown(self): + func.argtypes = None + func.restype = ctypes.c_int + + def test_func(self): + self.assertEqual(func(b"abc\xe4"), b"abc\xe4") + + def test_buffers(self): + buf = ctypes.create_string_buffer(b"abc") + self.assertEqual(len(buf), 3+1) + + buf = ctypes.create_string_buffer(b"ab\xe4\xf6\xfc") + self.assertEqual(buf[:], b"ab\xe4\xf6\xfc\0") + self.assertEqual(buf[::], b"ab\xe4\xf6\xfc\0") + self.assertEqual(buf[::-1], b'\x00\xfc\xf6\xe4ba') + self.assertEqual(buf[::2], b'a\xe4\xfc') + self.assertEqual(buf[6:5:-1], b"") + + +if __name__ == '__main__': + unittest.main() diff --git a/Lib/ctypes/util.py b/Lib/ctypes/util.py new file mode 100644 index 0000000000..0c2510e161 --- /dev/null +++ b/Lib/ctypes/util.py @@ -0,0 +1,376 @@ +import os +import shutil +import subprocess +import sys + +# find_library(name) returns the pathname of a library, or None. +if os.name == "nt": + + def _get_build_version(): + """Return the version of MSVC that was used to build Python. + + For Python 2.3 and up, the version number is included in + sys.version. For earlier versions, assume the compiler is MSVC 6. + """ + # This function was copied from Lib/distutils/msvccompiler.py + prefix = "MSC v." + i = sys.version.find(prefix) + if i == -1: + return 6 + i = i + len(prefix) + s, rest = sys.version[i:].split(" ", 1) + majorVersion = int(s[:-2]) - 6 + if majorVersion >= 13: + majorVersion += 1 + minorVersion = int(s[2:3]) / 10.0 + # I don't think paths are affected by minor version in version 6 + if majorVersion == 6: + minorVersion = 0 + if majorVersion >= 6: + return majorVersion + minorVersion + # else we don't know what version of the compiler this is + return None + + def find_msvcrt(): + """Return the name of the VC runtime dll""" + version = _get_build_version() + if version is None: + # better be safe than sorry + return None + if version <= 6: + clibname = 'msvcrt' + elif version <= 13: + clibname = 'msvcr%d' % (version * 10) + else: + # CRT is no longer directly loadable. See issue23606 for the + # discussion about alternative approaches. + return None + + # If python was built with in debug mode + import importlib.machinery + if '_d.pyd' in importlib.machinery.EXTENSION_SUFFIXES: + clibname += 'd' + return clibname+'.dll' + + def find_library(name): + if name in ('c', 'm'): + return find_msvcrt() + # See MSDN for the REAL search order. + for directory in os.environ['PATH'].split(os.pathsep): + fname = os.path.join(directory, name) + if os.path.isfile(fname): + return fname + if fname.lower().endswith(".dll"): + continue + fname = fname + ".dll" + if os.path.isfile(fname): + return fname + return None + +elif os.name == "posix" and sys.platform == "darwin": + from ctypes.macholib.dyld import dyld_find as _dyld_find + def find_library(name): + possible = ['lib%s.dylib' % name, + '%s.dylib' % name, + '%s.framework/%s' % (name, name)] + for name in possible: + try: + return _dyld_find(name) + except ValueError: + continue + return None + +elif sys.platform.startswith("aix"): + # AIX has two styles of storing shared libraries + # GNU auto_tools refer to these as svr4 and aix + # svr4 (System V Release 4) is a regular file, often with .so as suffix + # AIX style uses an archive (suffix .a) with members (e.g., shr.o, libssl.so) + # see issue#26439 and _aix.py for more details + + from ctypes._aix import find_library + +elif os.name == "posix": + # Andreas Degert's find functions, using gcc, /sbin/ldconfig, objdump + import re, tempfile + + 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 + + def _findLib_gcc(name): + # Run GCC's linker with the -t (aka --trace) option and examine the + # library name it prints out. The GCC command will fail because we + # haven't supplied a proper program with main(), but that does not + # matter. + expr = os.fsencode(r'[^\(\)\s]*lib%s\.[^\(\)\s]*' % re.escape(name)) + + c_compiler = shutil.which('gcc') + if not c_compiler: + c_compiler = shutil.which('cc') + if not c_compiler: + # No C compiler available, give up + return None + + temp = tempfile.NamedTemporaryFile() + try: + args = [c_compiler, '-Wl,-t', '-o', temp.name, '-l' + name] + + env = dict(os.environ) + env['LC_ALL'] = 'C' + env['LANG'] = 'C' + try: + proc = subprocess.Popen(args, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + env=env) + except OSError: # E.g. bad executable + return None + with proc: + trace = proc.stdout.read() + finally: + try: + temp.close() + except FileNotFoundError: + # Raised if the file was already removed, which is the normal + # behaviour of GCC if linking fails + pass + res = re.findall(expr, trace) + if not res: + return None + + for file in res: + # Check if the given file is an elf file: gcc can report + # some files that are linker scripts and not actual + # shared objects. See bpo-41976 for more details + if not _is_elf(file): + continue + return os.fsdecode(file) + + + if sys.platform == "sunos5": + # use /usr/ccs/bin/dump on solaris + def _get_soname(f): + if not f: + return None + + try: + proc = subprocess.Popen(("/usr/ccs/bin/dump", "-Lpv", f), + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL) + except OSError: # E.g. command not found + return None + with proc: + data = proc.stdout.read() + res = re.search(br'\[.*\]\sSONAME\s+([^\s]+)', data) + if not res: + return None + return os.fsdecode(res.group(1)) + else: + def _get_soname(f): + # assuming GNU binutils / ELF + if not f: + return None + objdump = shutil.which('objdump') + if not objdump: + # objdump is not available, give up + return None + + try: + proc = subprocess.Popen((objdump, '-p', '-j', '.dynamic', f), + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL) + except OSError: # E.g. bad executable + return None + with proc: + dump = proc.stdout.read() + res = re.search(br'\sSONAME\s+([^\s]+)', dump) + if not res: + return None + return os.fsdecode(res.group(1)) + + if sys.platform.startswith(("freebsd", "openbsd", "dragonfly")): + + def _num_version(libname): + # "libxyz.so.MAJOR.MINOR" => [ MAJOR, MINOR ] + parts = libname.split(b".") + nums = [] + try: + while parts: + nums.insert(0, int(parts.pop())) + except ValueError: + pass + return nums or [sys.maxsize] + + def find_library(name): + ename = re.escape(name) + expr = r':-l%s\.\S+ => \S*/(lib%s\.\S+)' % (ename, ename) + expr = os.fsencode(expr) + + try: + proc = subprocess.Popen(('/sbin/ldconfig', '-r'), + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL) + except OSError: # E.g. command not found + data = b'' + else: + with proc: + data = proc.stdout.read() + + res = re.findall(expr, data) + if not res: + return _get_soname(_findLib_gcc(name)) + res.sort(key=_num_version) + return os.fsdecode(res[-1]) + + elif sys.platform == "sunos5": + + def _findLib_crle(name, is64): + if not os.path.exists('/usr/bin/crle'): + return None + + env = dict(os.environ) + env['LC_ALL'] = 'C' + + if is64: + args = ('/usr/bin/crle', '-64') + else: + args = ('/usr/bin/crle',) + + paths = None + try: + proc = subprocess.Popen(args, + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, + env=env) + except OSError: # E.g. bad executable + return None + with proc: + for line in proc.stdout: + line = line.strip() + if line.startswith(b'Default Library Path (ELF):'): + paths = os.fsdecode(line).split()[4] + + if not paths: + return None + + for dir in paths.split(":"): + libfile = os.path.join(dir, "lib%s.so" % name) + if os.path.exists(libfile): + return libfile + + return None + + def find_library(name, is64 = False): + return _get_soname(_findLib_crle(name, is64) or _findLib_gcc(name)) + + else: + + def _findSoname_ldconfig(name): + import struct + if struct.calcsize('l') == 4: + machine = os.uname().machine + '-32' + else: + machine = os.uname().machine + '-64' + mach_map = { + 'x86_64-64': 'libc6,x86-64', + 'ppc64-64': 'libc6,64bit', + 'sparc64-64': 'libc6,64bit', + 's390x-64': 'libc6,64bit', + 'ia64-64': 'libc6,IA-64', + } + abi_type = mach_map.get(machine, 'libc6') + + # XXX assuming GLIBC's ldconfig (with option -p) + regex = r'\s+(lib%s\.[^\s]+)\s+\(%s' + regex = os.fsencode(regex % (re.escape(name), abi_type)) + try: + with subprocess.Popen(['/sbin/ldconfig', '-p'], + stdin=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + stdout=subprocess.PIPE, + env={'LC_ALL': 'C', 'LANG': 'C'}) as p: + res = re.search(regex, p.stdout.read()) + if res: + return os.fsdecode(res.group(1)) + except OSError: + pass + + def _findLib_ld(name): + # See issue #9998 for why this is needed + expr = r'[^\(\)\s]*lib%s\.[^\(\)\s]*' % re.escape(name) + cmd = ['ld', '-t'] + libpath = os.environ.get('LD_LIBRARY_PATH') + if libpath: + for d in libpath.split(':'): + cmd.extend(['-L', d]) + cmd.extend(['-o', os.devnull, '-l%s' % name]) + result = None + try: + p = subprocess.Popen(cmd, stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True) + out, _ = p.communicate() + res = re.findall(expr, os.fsdecode(out)) + for file in res: + # Check if the given file is an elf file: gcc can report + # some files that are linker scripts and not actual + # shared objects. See bpo-41976 for more details + if not _is_elf(file): + continue + return os.fsdecode(file) + except Exception: + pass # result will be None + return result + + def find_library(name): + # See issue #9998 + return _findSoname_ldconfig(name) or \ + _get_soname(_findLib_gcc(name)) or _get_soname(_findLib_ld(name)) + +################################################################ +# test code + +def test(): + from ctypes import cdll + if os.name == "nt": + print(cdll.msvcrt) + print(cdll.load("msvcrt")) + print(find_library("msvcrt")) + + if os.name == "posix": + # find and load_version + print(find_library("m")) + print(find_library("c")) + print(find_library("bz2")) + + # load + if sys.platform == "darwin": + print(cdll.LoadLibrary("libm.dylib")) + print(cdll.LoadLibrary("libcrypto.dylib")) + print(cdll.LoadLibrary("libSystem.dylib")) + print(cdll.LoadLibrary("System.framework/System")) + # issue-26439 - fix broken test call for AIX + elif sys.platform.startswith("aix"): + from ctypes import CDLL + if sys.maxsize < 2**32: + print(f"Using CDLL(name, os.RTLD_MEMBER): {CDLL('libc.a(shr.o)', os.RTLD_MEMBER)}") + print(f"Using cdll.LoadLibrary(): {cdll.LoadLibrary('libc.a(shr.o)')}") + # librpm.so is only available as 32-bit shared library + print(find_library("rpm")) + print(cdll.LoadLibrary("librpm.so")) + else: + print(f"Using CDLL(name, os.RTLD_MEMBER): {CDLL('libc.a(shr_64.o)', os.RTLD_MEMBER)}") + print(f"Using cdll.LoadLibrary(): {cdll.LoadLibrary('libc.a(shr_64.o)')}") + print(f"crypt\t:: {find_library('crypt')}") + print(f"crypt\t:: {cdll.LoadLibrary(find_library('crypt'))}") + print(f"crypto\t:: {find_library('crypto')}") + print(f"crypto\t:: {cdll.LoadLibrary(find_library('crypto'))}") + else: + print(cdll.LoadLibrary("libm.so")) + print(cdll.LoadLibrary("libcrypt.so")) + print(find_library("crypt")) + +if __name__ == "__main__": + test() diff --git a/Lib/ctypes/wintypes.py b/Lib/ctypes/wintypes.py new file mode 100644 index 0000000000..c619d27596 --- /dev/null +++ b/Lib/ctypes/wintypes.py @@ -0,0 +1,202 @@ +# The most useful windows datatypes +import ctypes + +BYTE = ctypes.c_byte +WORD = ctypes.c_ushort +DWORD = ctypes.c_ulong + +#UCHAR = ctypes.c_uchar +CHAR = ctypes.c_char +WCHAR = ctypes.c_wchar +UINT = ctypes.c_uint +INT = ctypes.c_int + +DOUBLE = ctypes.c_double +FLOAT = ctypes.c_float + +BOOLEAN = BYTE +BOOL = ctypes.c_long + +class VARIANT_BOOL(ctypes._SimpleCData): + _type_ = "v" + def __repr__(self): + return "%s(%r)" % (self.__class__.__name__, self.value) + +ULONG = ctypes.c_ulong +LONG = ctypes.c_long + +USHORT = ctypes.c_ushort +SHORT = ctypes.c_short + +# in the windows header files, these are structures. +_LARGE_INTEGER = LARGE_INTEGER = ctypes.c_longlong +_ULARGE_INTEGER = ULARGE_INTEGER = ctypes.c_ulonglong + +LPCOLESTR = LPOLESTR = OLESTR = ctypes.c_wchar_p +LPCWSTR = LPWSTR = ctypes.c_wchar_p +LPCSTR = LPSTR = ctypes.c_char_p +LPCVOID = LPVOID = ctypes.c_void_p + +# WPARAM is defined as UINT_PTR (unsigned type) +# LPARAM is defined as LONG_PTR (signed type) +if ctypes.sizeof(ctypes.c_long) == ctypes.sizeof(ctypes.c_void_p): + WPARAM = ctypes.c_ulong + LPARAM = ctypes.c_long +elif ctypes.sizeof(ctypes.c_longlong) == ctypes.sizeof(ctypes.c_void_p): + WPARAM = ctypes.c_ulonglong + LPARAM = ctypes.c_longlong + +ATOM = WORD +LANGID = WORD + +COLORREF = DWORD +LGRPID = DWORD +LCTYPE = DWORD + +LCID = DWORD + +################################################################ +# HANDLE types +HANDLE = ctypes.c_void_p # in the header files: void * + +HACCEL = HANDLE +HBITMAP = HANDLE +HBRUSH = HANDLE +HCOLORSPACE = HANDLE +HDC = HANDLE +HDESK = HANDLE +HDWP = HANDLE +HENHMETAFILE = HANDLE +HFONT = HANDLE +HGDIOBJ = HANDLE +HGLOBAL = HANDLE +HHOOK = HANDLE +HICON = HANDLE +HINSTANCE = HANDLE +HKEY = HANDLE +HKL = HANDLE +HLOCAL = HANDLE +HMENU = HANDLE +HMETAFILE = HANDLE +HMODULE = HANDLE +HMONITOR = HANDLE +HPALETTE = HANDLE +HPEN = HANDLE +HRGN = HANDLE +HRSRC = HANDLE +HSTR = HANDLE +HTASK = HANDLE +HWINSTA = HANDLE +HWND = HANDLE +SC_HANDLE = HANDLE +SERVICE_STATUS_HANDLE = HANDLE + +################################################################ +# Some important structure definitions + +class RECT(ctypes.Structure): + _fields_ = [("left", LONG), + ("top", LONG), + ("right", LONG), + ("bottom", LONG)] +tagRECT = _RECTL = RECTL = RECT + +class _SMALL_RECT(ctypes.Structure): + _fields_ = [('Left', SHORT), + ('Top', SHORT), + ('Right', SHORT), + ('Bottom', SHORT)] +SMALL_RECT = _SMALL_RECT + +class _COORD(ctypes.Structure): + _fields_ = [('X', SHORT), + ('Y', SHORT)] + +class POINT(ctypes.Structure): + _fields_ = [("x", LONG), + ("y", LONG)] +tagPOINT = _POINTL = POINTL = POINT + +class SIZE(ctypes.Structure): + _fields_ = [("cx", LONG), + ("cy", LONG)] +tagSIZE = SIZEL = SIZE + +def RGB(red, green, blue): + return red + (green << 8) + (blue << 16) + +class FILETIME(ctypes.Structure): + _fields_ = [("dwLowDateTime", DWORD), + ("dwHighDateTime", DWORD)] +_FILETIME = FILETIME + +class MSG(ctypes.Structure): + _fields_ = [("hWnd", HWND), + ("message", UINT), + ("wParam", WPARAM), + ("lParam", LPARAM), + ("time", DWORD), + ("pt", POINT)] +tagMSG = MSG +MAX_PATH = 260 + +class WIN32_FIND_DATAA(ctypes.Structure): + _fields_ = [("dwFileAttributes", DWORD), + ("ftCreationTime", FILETIME), + ("ftLastAccessTime", FILETIME), + ("ftLastWriteTime", FILETIME), + ("nFileSizeHigh", DWORD), + ("nFileSizeLow", DWORD), + ("dwReserved0", DWORD), + ("dwReserved1", DWORD), + ("cFileName", CHAR * MAX_PATH), + ("cAlternateFileName", CHAR * 14)] + +class WIN32_FIND_DATAW(ctypes.Structure): + _fields_ = [("dwFileAttributes", DWORD), + ("ftCreationTime", FILETIME), + ("ftLastAccessTime", FILETIME), + ("ftLastWriteTime", FILETIME), + ("nFileSizeHigh", DWORD), + ("nFileSizeLow", DWORD), + ("dwReserved0", DWORD), + ("dwReserved1", DWORD), + ("cFileName", WCHAR * MAX_PATH), + ("cAlternateFileName", WCHAR * 14)] + +################################################################ +# Pointer types + +LPBOOL = PBOOL = ctypes.POINTER(BOOL) +PBOOLEAN = ctypes.POINTER(BOOLEAN) +LPBYTE = PBYTE = ctypes.POINTER(BYTE) +PCHAR = ctypes.POINTER(CHAR) +LPCOLORREF = ctypes.POINTER(COLORREF) +LPDWORD = PDWORD = ctypes.POINTER(DWORD) +LPFILETIME = PFILETIME = ctypes.POINTER(FILETIME) +PFLOAT = ctypes.POINTER(FLOAT) +LPHANDLE = PHANDLE = ctypes.POINTER(HANDLE) +PHKEY = ctypes.POINTER(HKEY) +LPHKL = ctypes.POINTER(HKL) +LPINT = PINT = ctypes.POINTER(INT) +PLARGE_INTEGER = ctypes.POINTER(LARGE_INTEGER) +PLCID = ctypes.POINTER(LCID) +LPLONG = PLONG = ctypes.POINTER(LONG) +LPMSG = PMSG = ctypes.POINTER(MSG) +LPPOINT = PPOINT = ctypes.POINTER(POINT) +PPOINTL = ctypes.POINTER(POINTL) +LPRECT = PRECT = ctypes.POINTER(RECT) +LPRECTL = PRECTL = ctypes.POINTER(RECTL) +LPSC_HANDLE = ctypes.POINTER(SC_HANDLE) +PSHORT = ctypes.POINTER(SHORT) +LPSIZE = PSIZE = ctypes.POINTER(SIZE) +LPSIZEL = PSIZEL = ctypes.POINTER(SIZEL) +PSMALL_RECT = ctypes.POINTER(SMALL_RECT) +LPUINT = PUINT = ctypes.POINTER(UINT) +PULARGE_INTEGER = ctypes.POINTER(ULARGE_INTEGER) +PULONG = ctypes.POINTER(ULONG) +PUSHORT = ctypes.POINTER(USHORT) +PWCHAR = ctypes.POINTER(WCHAR) +LPWIN32_FIND_DATAA = PWIN32_FIND_DATAA = ctypes.POINTER(WIN32_FIND_DATAA) +LPWIN32_FIND_DATAW = PWIN32_FIND_DATAW = ctypes.POINTER(WIN32_FIND_DATAW) +LPWORD = PWORD = ctypes.POINTER(WORD) diff --git a/Lib/test/libregrtest/refleak.py b/Lib/test/libregrtest/refleak.py index 822c905238..1866c816b1 100644 --- a/Lib/test/libregrtest/refleak.py +++ b/Lib/test/libregrtest/refleak.py @@ -264,7 +264,8 @@ def clear_caches(): except KeyError: pass else: - ctypes._reset_cache() + pass # FIXME: RustPython + # ctypes._reset_cache() try: typing = sys.modules['typing'] diff --git a/derive/src/pyclass.rs b/derive/src/pyclass.rs index cd5c759fbc..838b677f79 100644 --- a/derive/src/pyclass.rs +++ b/derive/src/pyclass.rs @@ -142,6 +142,7 @@ fn generate_class_def( name: &str, module_name: Option<&str>, base: Option, + metaclass: Option, attrs: &[Attribute], ) -> std::result::Result { let doc = if let Some(doc) = attrs.doc() { @@ -175,8 +176,6 @@ fn generate_class_def( ) .into()); } - let base = base.map(|name| Ident::new(&name, ident.span())); - let base_class = if is_pystruct { quote! { fn static_baseclass() -> &'static ::rustpython_vm::builtins::PyTypeRef { @@ -184,11 +183,24 @@ fn generate_class_def( rustpython_vm::builtins::PyTuple::static_type() } } - } else if let Some(base) = base { + } else if let Some(typ) = base { + let typ = Ident::new(&typ, ident.span()); quote! { fn static_baseclass() -> &'static ::rustpython_vm::builtins::PyTypeRef { use rustpython_vm::StaticType; - #base::static_type() + #typ::static_type() + } + } + } else { + quote!() + }; + + let meta_class = if let Some(typ) = metaclass { + let typ = Ident::new(&typ, ident.span()); + quote! { + fn static_metaclass() -> &'static ::rustpython_vm::builtins::PyTypeRef { + use rustpython_vm::StaticType; + #typ::static_type() } } } else { @@ -211,6 +223,8 @@ fn generate_class_def( &CELL } + #meta_class + #base_class } }; @@ -227,7 +241,15 @@ pub(crate) fn impl_pyclass( let class_name = class_meta.class_name()?; let module_name = class_meta.module()?; let base = class_meta.base()?; - let class_def = generate_class_def(ident, &class_name, module_name.as_deref(), base, attrs)?; + let metaclass = class_meta.metaclass()?; + let class_def = generate_class_def( + ident, + &class_name, + module_name.as_deref(), + base, + metaclass, + attrs, + )?; let ret = quote! { #item diff --git a/derive/src/util.rs b/derive/src/util.rs index 756478f201..90cc16e708 100644 --- a/derive/src/util.rs +++ b/derive/src/util.rs @@ -233,7 +233,7 @@ impl ItemMeta for SimpleItemMeta { pub(crate) struct ClassItemMeta(ItemMetaInner); impl ItemMeta for ClassItemMeta { - const ALLOWED_NAMES: &'static [&'static str] = &["module", "name", "base"]; + const ALLOWED_NAMES: &'static [&'static str] = &["module", "name", "base", "metaclass"]; fn from_inner(inner: ItemMetaInner) -> Self { Self(inner) @@ -272,6 +272,10 @@ impl ClassItemMeta { self.inner()._optional_str("base") } + pub fn metaclass(&self) -> Result> { + self.inner()._optional_str("metaclass") + } + pub fn module(&self) -> Result> { const KEY: &str = "module"; let inner = self.inner(); diff --git a/extra_tests/snippets/ctypes_tests/__init__.py b/extra_tests/snippets/ctypes_tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/extra_tests/snippets/ctypes_tests/support.py b/extra_tests/snippets/ctypes_tests/support.py new file mode 100644 index 0000000000..7092507d77 --- /dev/null +++ b/extra_tests/snippets/ctypes_tests/support.py @@ -0,0 +1,57 @@ +import py +import sys +import ctypes + +try: + import _rawffi +except ImportError: + _rawffi = None + +class WhiteBoxTests: + + def setup_class(cls): + if _rawffi: + py.test.skip("white-box tests for pypy _rawffi based ctypes impl") + +def del_funcptr_refs_maybe(obj, attrname): + dll = getattr(obj, attrname, None) + if not dll: + return + _FuncPtr = dll._FuncPtr + for name in dir(dll): + obj = getattr(dll, name, None) + if isinstance(obj, _FuncPtr): + delattr(dll, name) + +class BaseCTypesTestChecker: + def setup_class(cls): + if _rawffi: + import gc + for _ in range(4): + gc.collect() + try: + cls.old_num = _rawffi._num_of_allocated_objects() + except RuntimeError: + pass + + def teardown_class(cls): + if not hasattr(sys, 'pypy_translation_info'): + return + if sys.pypy_translation_info['translation.gc'] == 'boehm': + return # it seems that boehm has problems with __del__, so not + # everything is freed + # + mod = sys.modules[cls.__module__] + del_funcptr_refs_maybe(mod, 'dll') + del_funcptr_refs_maybe(mod, 'dll2') + del_funcptr_refs_maybe(mod, 'lib') + del_funcptr_refs_maybe(mod, 'testdll') + del_funcptr_refs_maybe(mod, 'ctdll') + del_funcptr_refs_maybe(cls, '_dll') + # + if hasattr(cls, 'old_num'): + import gc + for _ in range(4): + gc.collect() + # there is one reference coming from the byref() above + assert _rawffi._num_of_allocated_objects() == cls.old_num diff --git a/extra_tests/snippets/ctypes_tests/test_anon.py b/extra_tests/snippets/ctypes_tests/test_anon.py new file mode 100644 index 0000000000..0c42b696f8 --- /dev/null +++ b/extra_tests/snippets/ctypes_tests/test_anon.py @@ -0,0 +1,55 @@ +import pytest +from ctypes import * + +@pytest.mark.pypy_only +def test_nested(): + class ANON_S(Structure): + _fields_ = [("a", c_int)] + + class ANON_U(Union): + _fields_ = [("_", ANON_S), + ("b", c_int)] + _anonymous_ = ["_"] + + class Y(Structure): + _fields_ = [("x", c_int), + ("_", ANON_U), + ("y", c_int)] + _anonymous_ = ["_"] + + assert Y.x.offset == 0 + assert Y.a.offset == sizeof(c_int) + assert Y.b.offset == sizeof(c_int) + assert Y._.offset == sizeof(c_int) + assert Y.y.offset == sizeof(c_int) * 2 + + assert Y._names_ == ['x', 'a', 'b', 'y'] + +def test_anonymous_fields_on_instance(): + # this is about the *instance-level* access of anonymous fields, + # which you'd guess is the most common, but used not to work + # (issue #2230) + + class B(Structure): + _fields_ = [("x", c_int), ("y", c_int), ("z", c_int)] + class A(Structure): + _anonymous_ = ["b"] + _fields_ = [("b", B)] + + a = A() + a.x = 5 + assert a.x == 5 + assert a.b.x == 5 + a.b.x += 1 + assert a.x == 6 + + class C(Structure): + _anonymous_ = ["a"] + _fields_ = [("v", c_int), ("a", A)] + + c = C() + c.v = 3 + c.y = -8 + assert c.v == 3 + assert c.y == c.a.y == c.a.b.y == -8 + assert not hasattr(c, 'b') diff --git a/extra_tests/snippets/ctypes_tests/test_array.py b/extra_tests/snippets/ctypes_tests/test_array.py new file mode 100644 index 0000000000..bf4de49fd5 --- /dev/null +++ b/extra_tests/snippets/ctypes_tests/test_array.py @@ -0,0 +1,64 @@ +import pytest +from ctypes import * + +def test_slice(): + values = list(range(5)) + numarray = c_int * 5 + + na = numarray(*(c_int(x) for x in values)) + + assert list(na[0:0]) == [] + assert list(na[:]) == values + assert list(na[:10]) == values + +def test_init_again(): + sz = (c_char * 3)() + addr1 = addressof(sz) + sz.__init__(*b"foo") + addr2 = addressof(sz) + assert addr1 == addr2 + +def test_array_of_structures(): + class X(Structure): + _fields_ = [('x', c_int), ('y', c_int)] + + Y = X * 2 + y = Y() + x = X() + x.y = 3 + y[1] = x + assert y[1].y == 3 + +def test_output_simple(): + A = c_char * 10 + TP = POINTER(A) + x = TP(A()) + assert x[0] != b'' + + A = c_wchar * 10 + TP = POINTER(A) + x = TP(A()) + assert x[0] != b'' + +def test_output_simple_array(): + A = c_char * 10 + AA = A * 10 + aa = AA() + assert aa[0] != b'' + +def test_output_complex_test(): + class Car(Structure): + _fields_ = [("brand", c_char * 10), + ("speed", c_float), + ("owner", c_char * 10)] + + assert isinstance(Car(b"abcdefghi", 42.0, b"12345").brand, bytes) + assert Car(b"abcdefghi", 42.0, b"12345").brand == b"abcdefghi" + assert Car(b"abcdefghio", 42.0, b"12345").brand == b"abcdefghio" + with pytest.raises(ValueError): + Car(b"abcdefghiop", 42.0, b"12345") + + A = Car._fields_[2][1] + TP = POINTER(A) + x = TP(A()) + assert x[0] != b'' diff --git a/extra_tests/snippets/ctypes_tests/test_base.py b/extra_tests/snippets/ctypes_tests/test_base.py new file mode 100644 index 0000000000..e3f9f21f16 --- /dev/null +++ b/extra_tests/snippets/ctypes_tests/test_base.py @@ -0,0 +1,24 @@ +import pytest +from ctypes import * + +pytestmark = pytest.mark.pypy_only + +def test_pointer(): + p = pointer(pointer(c_int(2))) + x = p[0] + assert x._base is p + +def test_structure(): + class X(Structure): + _fields_ = [('x', POINTER(c_int)), + ('y', POINTER(c_int))] + + x = X() + assert x.y._base is x + assert x.y._index == 1 + +def test_array(): + X = POINTER(c_int) * 24 + x = X() + assert x[16]._base is x + assert x[16]._index == 16 diff --git a/extra_tests/snippets/ctypes_tests/test_bitfields.py b/extra_tests/snippets/ctypes_tests/test_bitfields.py new file mode 100644 index 0000000000..2d9a139052 --- /dev/null +++ b/extra_tests/snippets/ctypes_tests/test_bitfields.py @@ -0,0 +1,19 @@ +import pytest +from ctypes import * + + +def test_set_fields_attr(): + class A(Structure): + pass + A._fields_ = [("a", c_byte), ("b", c_ubyte)] + +def test_set_fields_attr_bitfields(): + class A(Structure): + pass + A._fields_ = [("a", POINTER(A)), ("b", c_ubyte, 4)] + +def test_set_fields_cycle_fails(): + class A(Structure): + pass + with pytest.raises(AttributeError): + A._fields_ = [("a", A)] diff --git a/extra_tests/snippets/ctypes_tests/test_buffers.py b/extra_tests/snippets/ctypes_tests/test_buffers.py new file mode 100644 index 0000000000..adbf664645 --- /dev/null +++ b/extra_tests/snippets/ctypes_tests/test_buffers.py @@ -0,0 +1,70 @@ +import pytest +import sys +from ctypes import * + +def test_buffer(): + b = create_string_buffer(32) + assert len(b) == 32 + assert sizeof(b) == 32 * sizeof(c_char) + assert type(b[0]) is bytes + + b = create_string_buffer(b"abc") + assert len(b) == 4 # trailing nul char + assert sizeof(b) == 4 * sizeof(c_char) + assert type(b[0]) is bytes + assert b[0] == b"a" + assert b[:] == b"abc\0" + +def test_from_buffer(): + b1 = bytearray(b"abcde") + b = (c_char * 5).from_buffer(b1) + assert b[2] == b"c" + # + b1 = bytearray(b"abcd") + b = c_int.from_buffer(b1) + assert b.value in (1684234849, # little endian + 1633837924) # big endian + +def test_from_buffer_keepalive(): + # Issue #2878 + b1 = bytearray(b"ab") + array = (c_uint16 * 32)() + array[6] = c_uint16.from_buffer(b1) + # this is also what we get on CPython. I don't think it makes + # sense because the array contains just a copy of the number. + assert array._objects == {'6': b1} + +def normalize(fmt): + if sys.byteorder == "big": + return fmt.replace('<', '>') + else: + return fmt + +s_long = {4: 'l', 8: 'q'}[sizeof(c_long)] +s_ulong = {4: 'L', 8: 'Q'}[sizeof(c_long)] + +@pytest.mark.parametrize("tp, fmt", [ + ## simple types + (c_char, ">>" % (value,)) + monkeypatch.setattr(_rawffi, 'exit', custom_exit) + cb = CFUNCTYPE(c_int, c_int)(callback_func) + cb2 = cast(cast(cb, c_void_p), CFUNCTYPE(c_int, c_int)) + out, err = capsys.readouterr() + assert not err + cb2(0) + out, err = capsys.readouterr() + assert err.splitlines()[-1] == "Exception: <<>>" + # + cb = CFUNCTYPE(c_int, c_int)(callback_func) + cb(0) + out, err = capsys.readouterr() + assert err.splitlines()[-1] == "Exception: <<>>" diff --git a/extra_tests/snippets/ctypes_tests/test_callbacks.py b/extra_tests/snippets/ctypes_tests/test_callbacks.py new file mode 100644 index 0000000000..98b553cd30 --- /dev/null +++ b/extra_tests/snippets/ctypes_tests/test_callbacks.py @@ -0,0 +1,199 @@ +import pytest + +import math +from ctypes import * +from .support import BaseCTypesTestChecker + +functypes = [CFUNCTYPE] +try: + functypes.append(WINFUNCTYPE) +except NameError: + pass + + +def callback(*args): + callback.got_args = args + return args[-1] + +unwrapped_types = { + c_float: (float,), + c_double: (float,), + c_char: (bytes,), + c_char_p: (bytes,), + c_uint: (int,), + c_ulong: (int,), + } + +@pytest.mark.parametrize("typ, arg", [ + (c_byte, 42), + (c_byte, -42), + (c_ubyte, 42), + (c_short, 42), + (c_short, -42), + (c_ushort, 42), + (c_int, 42), + (c_int, -42), + (c_uint, 42), + (c_long, 42), + (c_long, -42), + (c_ulong, 42), + (c_longlong, 42), + (c_longlong, -42), + (c_ulonglong, 42), + (c_float, math.e), # only almost equal: double -> float -> double + (c_float, -math.e), + (c_double, 3.14), + (c_double, -3.14), + (c_char, b"x"), + (c_char, b"a"), +]) +@pytest.mark.parametrize('functype', functypes) +def test_types(typ, arg, functype): + PROTO = functype(typ, typ) + cfunc = PROTO(callback) + result = cfunc(arg) + if typ == c_float: + assert abs(result - arg) < 0.000001 + else: + assert callback.got_args == (arg,) + assert result == arg + + result2 = cfunc(typ(arg)) + assert type(result2) in unwrapped_types.get(typ, (int,)) + + PROTO = functype(typ, c_byte, typ) + result = PROTO(callback)(-3, arg) + if typ == c_float: + assert abs(result - arg) < 0.000001 + else: + assert callback.got_args == (-3, arg) + assert result == arg + +@pytest.mark.parametrize('functype', functypes) +def test_unsupported_restype_1(functype): + # Only "fundamental" result types are supported for callback + # functions, the type must have a non-NULL stgdict->setfunc. + # POINTER(c_double), for example, is not supported. + + prototype = functype(POINTER(c_double)) + # The type is checked when the prototype is called + with pytest.raises(TypeError): + prototype(lambda: None) + + +def test_callback_with_struct_argument(): + class RECT(Structure): + _fields_ = [("left", c_int), ("top", c_int), + ("right", c_int), ("bottom", c_int)] + + proto = CFUNCTYPE(c_int, RECT) + + def callback(point): + point.left *= -1 + return point.left + point.top + point.right + point.bottom + + cbp = proto(callback) + rect = RECT(-1000, 100, 10, 1) + res = cbp(rect) + assert res == 1111 + assert rect.left == -1000 # must not have been changed! + +def test_callback_from_c_with_struct_argument(dll): + class RECT(Structure): + _fields_ = [("left", c_long), ("top", c_long), + ("right", c_long), ("bottom", c_long)] + + proto = CFUNCTYPE(c_int, RECT) + + def callback(point): + return point.left + point.top + point.right + point.bottom + + cbp = proto(callback) + rect = RECT(1000, 100, 10, 1) + + call_callback_with_rect = dll.call_callback_with_rect + call_callback_with_rect.restype = c_int + call_callback_with_rect.argtypes = [proto, RECT] + res = call_callback_with_rect(cbp, rect) + assert res == 1111 + +def test_callback_unsupported_return_struct(): + class RECT(Structure): + _fields_ = [("left", c_int), ("top", c_int), + ("right", c_int), ("bottom", c_int)] + + proto = CFUNCTYPE(RECT, c_int) + with pytest.raises(TypeError): + proto(lambda r: 0) + + +def test_qsort(dll): + PI = POINTER(c_int) + A = c_int*5 + a = A() + for i in range(5): + a[i] = 5-i + + assert a[0] == 5 # sanity + + def comp(a, b): + a = a.contents.value + b = b.contents.value + if a < b: + return -1 + elif a > b: + return 1 + else: + return 0 + qs = dll.my_qsort + qs.restype = None + CMP = CFUNCTYPE(c_int, PI, PI) + qs.argtypes = (PI, c_size_t, c_size_t, CMP) + + qs(cast(a, PI), 5, sizeof(c_int), CMP(comp)) + + res = list(a) + + assert res == [1,2,3,4,5] + +def test_pyobject_as_opaque(dll): + def callback(arg): + return arg() + + CTP = CFUNCTYPE(c_int, py_object) + cfunc = dll._testfunc_callback_opaque + cfunc.argtypes = [CTP, py_object] + cfunc.restype = c_int + res = cfunc(CTP(callback), lambda : 3) + assert res == 3 + +def test_callback_void(capsys, dll): + def callback(): + pass + + CTP = CFUNCTYPE(None) + cfunc = dll._testfunc_callback_void + cfunc.argtypes = [CTP] + cfunc.restype = int + cfunc(CTP(callback)) + out, err = capsys.readouterr() + assert (out, err) == ("", "") + + +def test_callback_pyobject(): + def callback(obj): + return obj + + FUNC = CFUNCTYPE(py_object, py_object) + cfunc = FUNC(callback) + param = c_int(42) + assert cfunc(param) is param + +def test_raise_argumenterror(): + def callback(x): + pass + FUNC = CFUNCTYPE(None, c_void_p) + cfunc = FUNC(callback) + param = c_uint(42) + with pytest.raises(ArgumentError): + cfunc(param) diff --git a/extra_tests/snippets/ctypes_tests/test_cast.py b/extra_tests/snippets/ctypes_tests/test_cast.py new file mode 100644 index 0000000000..024978d497 --- /dev/null +++ b/extra_tests/snippets/ctypes_tests/test_cast.py @@ -0,0 +1,40 @@ +import pytest + +from ctypes import * + +def test_cast_functype(dll): + # make sure we can cast function type + my_sqrt = dll.my_sqrt + saved_objects = my_sqrt._objects.copy() + sqrt = cast(cast(my_sqrt, c_void_p), CFUNCTYPE(c_double, c_double)) + assert sqrt(4.0) == 2.0 + assert not cast(0, CFUNCTYPE(c_int)) + # + assert sqrt._objects is my_sqrt._objects # on CPython too + my_sqrt._objects.clear() + my_sqrt._objects.update(saved_objects) + +def test_cast_argumenterror(): + param = c_uint(42) + with pytest.raises(ArgumentError): + cast(param, c_void_p) + +def test_c_bool(): + x = c_bool(42) + assert x.value is True + x = c_bool(0.0) + assert x.value is False + x = c_bool("") + assert x.value is False + x = c_bool(['yadda']) + assert x.value is True + +def test_cast_array(): + import sys + data = b'data' + ubyte = c_ubyte * len(data) + byteslike = ubyte.from_buffer_copy(data) + m = memoryview(byteslike) + if sys.version_info > (3, 3): + b = m.cast('B') + assert bytes(b) == data diff --git a/extra_tests/snippets/ctypes_tests/test_commethods.py b/extra_tests/snippets/ctypes_tests/test_commethods.py new file mode 100644 index 0000000000..1912aaa80b --- /dev/null +++ b/extra_tests/snippets/ctypes_tests/test_commethods.py @@ -0,0 +1,86 @@ +# unittest for SOME ctypes com function calls. +# Can't resist from implementing some kind of mini-comtypes +# theller ;-) + +import pytest +import sys +if sys.platform != "win32": + # this doesn't work, it still tries to do module-level imports + # pytestmark = pytest.mark.skip("skip_the_whole_module") + pytest.importorskip('skip_the_whole_module') # hack! + + +import ctypes, types, unittest +from ctypes import HRESULT +from _ctypes import COMError + +oleaut32 = ctypes.OleDLL("oleaut32") + +class UnboundMethod(object): + def __init__(self, func, index, name): + self.func = func + self.index = index + self.name = name + self.__doc__ = func.__doc__ + + def __repr__(self): + return "" % (self.index, self.name, id(self)) + + def __get__(self, instance, owner): + if instance is None: + return self + return types.MethodType(self.func, instance, owner) + +def commethod(index, restype, *argtypes): + """A decorator that generates COM methods. The decorated function + itself is not used except for it's name.""" + def make_commethod(func): + comfunc = ctypes.WINFUNCTYPE(restype, *argtypes)(index, func.__name__) + comfunc.__name__ = func.__name__ + comfunc.__doc__ = func.__doc__ + return UnboundMethod(comfunc, index, func.__name__) + return make_commethod + +class ICreateTypeLib2(ctypes.c_void_p): + + @commethod(1, ctypes.c_long) + def AddRef(self): + pass + + @commethod(2, ctypes.c_long) + def Release(self): + pass + + @commethod(4, HRESULT, ctypes.c_wchar_p) + def SetName(self): + """Set the name of the library.""" + + @commethod(12, HRESULT) + def SaveAllChanges(self): + pass + + +CreateTypeLib2 = oleaut32.CreateTypeLib2 +CreateTypeLib2.argtypes = (ctypes.c_int, ctypes.c_wchar_p, ctypes.POINTER(ICreateTypeLib2)) + +################################################################ + +def test_basic_comtypes(): + punk = ICreateTypeLib2() + hr = CreateTypeLib2(0, "foobar.tlb", punk) + assert hr == 0 + + assert 2 == punk.AddRef() + assert 3 == punk.AddRef() + assert 4 == punk.AddRef() + + punk.SetName("TypeLib_ByPYPY") + with pytest.raises(COMError): + punk.SetName(None) + + # This would save the typelib to disk. + ## punk.SaveAllChanges() + + assert 3 == punk.Release() + assert 2 == punk.Release() + assert 1 == punk.Release() diff --git a/extra_tests/snippets/ctypes_tests/test_errno.py b/extra_tests/snippets/ctypes_tests/test_errno.py new file mode 100644 index 0000000000..3b4613c3fb --- /dev/null +++ b/extra_tests/snippets/ctypes_tests/test_errno.py @@ -0,0 +1,19 @@ +import pytest + +import ctypes +_rawffi = pytest.importorskip('_rawffi') # PyPy-only + +def test_errno_saved_and_restored(): + def check(): + assert _rawffi.get_errno() == 42 + assert ctypes.get_errno() == old + check.free_temp_buffers = lambda *args: None + f = ctypes._CFuncPtr() + old = _rawffi.get_errno() + f._flags_ = _rawffi.FUNCFLAG_USE_ERRNO + ctypes.set_errno(42) + f._call_funcptr(check) + assert _rawffi.get_errno() == old + ctypes.set_errno(0) + +# see also test_functions.test_errno diff --git a/extra_tests/snippets/ctypes_tests/test_extra.py b/extra_tests/snippets/ctypes_tests/test_extra.py new file mode 100644 index 0000000000..dea9dbb2da --- /dev/null +++ b/extra_tests/snippets/ctypes_tests/test_extra.py @@ -0,0 +1,248 @@ +""" +The purpose of this test file is to check how ctypes really work, +down to what aliases what and what exact types operations return. +""" + +import pytest +from ctypes import * + +def test_primitive_pointer(): + x = c_int(5) + assert x.value == 5 + x.value = 6 + assert x.value == 6 + + p = pointer(x) # p ---> x = 6 + assert isinstance(p.contents, c_int) + p.contents.value += 1 + assert x.value == 7 # p ---> x = 7 + + y = c_int(12) + p.contents = y # p ---> y = 12 + p.contents.value += 2 # p ---> y = 14 + assert y.value == 14 + assert x.value == 7 + + pp = pointer(p) # pp ---> p ---> y = 14 + pp.contents.contents = x # pp ---> p ---> x = 7 + p.contents.value += 2 # pp ---> p ---> x = 9 + assert x.value == 9 + + assert isinstance(p[0], int) + p[0] += 1 # pp ---> p ---> x = 10 + assert x.value == 10 + z = c_int(86) + p[0] = z # pp ---> p ---> x = 86 (not z!) + assert x.value == 86 + z.value = 84 + assert x.value == 86 + + assert isinstance(pp[0], POINTER(c_int)) + assert pp[0].contents.value == x.value == 86 + pp[0].contents = z # pp ---> p ---> z = 84 + assert p.contents.value == z.value == 84 + +## *** the rest is commented out because it should work but occasionally +## *** trigger a ctypes bug (SourceForge bug #1467852). *** +## q = pointer(y) +## pp[0] = q # pp ---> p ---> y = 14 +## assert y.value == 14 # (^^^ not q! ) +## assert p.contents.value == 14 +## assert pp.contents.contents.value == 14 +## q.contents = x +## assert pp.contents.contents.value == 14 + + +def test_char_p(): + x = c_char_p(b"hello\x00world") + assert x.value == b"hello" + x.value = b"world" + assert x.value == b"world" + + p = pointer(x) + assert p[0] == x.value == b"world" + p[0] = b"other" + assert x.value == p.contents.value == p[0] == b"other" + + myarray = (c_char_p * 10)() + myarray[7] = b"hello" + assert isinstance(myarray[7], bytes) + assert myarray[7] == b"hello" + +def test_struct(): + class tagpoint(Structure): + _fields_ = [('x', c_int), + ('p', POINTER(c_short))] + + y = c_short(123) + z = c_short(-33) + s = tagpoint() + s.p.contents = z + assert s.p.contents.value == -33 + s.p = pointer(y) + assert s.p.contents.value == 123 + s.p.contents.value = 124 + assert y.value == 124 + + s = tagpoint(x=12) + assert s.x == 12 + s = tagpoint(17, p=pointer(z)) + assert s.x == 17 + assert s.p.contents.value == -33 + +def test_ptr_array(): + a = (POINTER(c_ushort) * 5)() + x = c_ushort(52) + y = c_ushort(1000) + + a[2] = pointer(x) + assert a[2].contents.value == 52 + a[2].contents.value += 1 + assert x.value == 53 + + a[3].contents = y + assert a[3].contents.value == 1000 + a[3].contents.value += 1 + assert y.value == 1001 + +def test_void_p(): + x = c_int(12) + p1 = cast(pointer(x), c_void_p) + p2 = cast(p1, POINTER(c_int)) + assert p2.contents.value == 12 + +def test_char_array(): + a = (c_char * 3)() + a[0] = b'x' + a[1] = b'y' + assert a.value == b'xy' + a[2] = b'z' + assert a.value == b'xyz' + + b = create_string_buffer(3) + assert type(b) is type(a) + assert len(b) == 3 + + b.value = b"nxw" + assert b[0] == b'n' + assert b[1] == b'x' + assert b[2] == b'w' + + b.value = b"?" + assert b[0] == b'?' + assert b[1] == b'\x00' + assert b[2] == b'w' + + class S(Structure): + _fields_ = [('p', POINTER(c_char))] + + s = S() + s.p = b + s.p.contents.value = b'!' + assert b.value == b'!' + + assert len(create_string_buffer(0)) == 0 + +def test_array(): + a = (c_int * 10)() + + class S(Structure): + _fields_ = [('p', POINTER(c_int))] + + s = S() + s.p = a + s.p.contents.value = 42 + assert a[0] == 42 + + a = (c_int * 5)(5, 6, 7) + assert list(a) == [5, 6, 7, 0, 0] + +def test_truth_value(): + p = POINTER(c_int)() + assert not p + p.contents = c_int(12) + assert p + # I can't figure out how to reset p to NULL... + + assert c_int(12) + assert not c_int(0) # a bit strange, if you ask me + assert c_int(-1) + assert not c_byte(0) + assert not c_char(b'\x00') # hum + assert not c_float(0.0) + assert not c_double(0.0) + assert not c_ulonglong(0) + assert c_ulonglong(2**42) + + assert c_char_p(b"hello") + assert c_char_p(b"") + assert not c_char_p(None) + + assert not c_void_p() + +def test_sizeof(): + x = create_string_buffer(117) + assert sizeof(x) == 117 # assumes that chars are one byte each + x = (c_int * 42)() + assert sizeof(x) == 42 * sizeof(c_int) + +def test_convert_pointers(dll): + func = dll._testfunc_p_p + func.restype = c_char_p + + # automatic conversions to c_char_p + func.argtypes = [c_char_p] + assert func(b"hello") == b"hello" + assert func(c_char_p(b"hello")) == b"hello" + assert func((c_char * 6)(*b"hello")) == b"hello" + assert func(create_string_buffer(b"hello")) == b"hello" + + # automatic conversions to c_void_p + func.argtypes = [c_void_p] + assert func(b"hello") == b"hello" + assert func(c_char_p(b"hello")) == b"hello" + assert func((c_char * 6)(*b"hello")) == b"hello" + assert func((c_byte * 6)(104,101,108,108,111)) ==b"hello" + assert func(create_string_buffer(b"hello")) == b"hello" + +def test_varsize_cast(): + import struct + N = struct.calcsize("l") + x = c_long() + p = cast(pointer(x), POINTER(c_ubyte*N)) + for i, c in enumerate(struct.pack("l", 12345678)): + p.contents[i] = c + assert x.value == 12345678 + +def test_cfunctype_inspection(): + T = CFUNCTYPE(c_int, c_ubyte) + # T.argtypes and T.restype don't work, must use a dummy instance + assert list(T().argtypes) == [c_ubyte] + assert T().restype == c_int + +def test_from_param(): + # other working cases of from_param + assert isinstance(c_void_p.from_param((c_int * 4)()), c_int * 4) + +def test_array_mul(): + assert c_int * 10 == 10 * c_int + with pytest.raises(TypeError): + c_int * c_int + with pytest.raises(TypeError): + c_int * (-1.5) + with pytest.raises(TypeError): + c_int * "foo" + with pytest.raises(TypeError): + (-1.5) * c_int + with pytest.raises(TypeError): + "foo" * c_int + +def test_cast_none(): + def check(P): + x = cast(None, P) + assert isinstance(x, P) + assert not x + check(c_void_p) + check(c_char_p) + check(POINTER(c_int)) + check(POINTER(c_int * 10)) diff --git a/extra_tests/snippets/ctypes_tests/test_funcptr.py b/extra_tests/snippets/ctypes_tests/test_funcptr.py new file mode 100644 index 0000000000..2e3193abc2 --- /dev/null +++ b/extra_tests/snippets/ctypes_tests/test_funcptr.py @@ -0,0 +1,5 @@ +from ctypes import * + +def test_restype(dll): + foo = dll.my_unused_function + assert foo.restype is c_int # by default diff --git a/extra_tests/snippets/ctypes_tests/test_functions.py b/extra_tests/snippets/ctypes_tests/test_functions.py new file mode 100644 index 0000000000..56b50420a9 --- /dev/null +++ b/extra_tests/snippets/ctypes_tests/test_functions.py @@ -0,0 +1,243 @@ +from ctypes import * +import sys +import pytest + +@pytest.fixture +def dll(sofile): + return CDLL(str(sofile), use_errno=True) + + +def test_char_result(dll): + f = dll._testfunc_i_bhilfd + f.argtypes = [c_byte, c_short, c_int, c_long, c_float, c_double] + f.restype = c_char + result = f(0, 0, 0, 0, 0, 0) + assert result == b'\x00' + +def test_boolresult(dll): + f = dll._testfunc_i_bhilfd + f.argtypes = [c_byte, c_short, c_int, c_long, c_float, c_double] + f.restype = c_bool + false_result = f(0, 0, 0, 0, 0, 0) + assert false_result is False + true_result = f(1, 0, 0, 0, 0, 0) + assert true_result is True + +def test_unicode_function_name(dll): + f = dll[u'_testfunc_i_bhilfd'] + f.argtypes = [c_byte, c_short, c_int, c_long, c_float, c_double] + f.restype = c_int + result = f(1, 2, 3, 4, 5.0, 6.0) + assert result == 21 + +def test_truncate_python_longs(dll): + f = dll._testfunc_i_bhilfd + f.argtypes = [c_byte, c_short, c_int, c_long, c_float, c_double] + f.restype = c_int + x = sys.maxsize * 2 + result = f(x, x, x, x, 0, 0) + assert result == -8 + +def test_convert_pointers(dll): + f = dll.deref_LP_c_char_p + f.restype = c_char + f.argtypes = [POINTER(c_char_p)] + # + s = c_char_p(b'hello world') + ps = pointer(s) + assert f(ps) == b'h' + assert f(s) == b'h' # automatic conversion from char** to char* + +################################################################ + +def test_call_some_args(dll): + f = dll.my_strchr + f.argtypes = [c_char_p] + f.restype = c_char_p + result = f(b"abcd", ord("b")) + assert result == b"bcd" + +@pytest.mark.pypy_only +def test_keepalive_buffers(monkeypatch, dll): + import gc + f = dll.my_strchr + f.argtypes = [c_char_p] + f.restype = c_char_p + # + orig__call_funcptr = f._call_funcptr + def _call_funcptr(funcptr, *newargs): + gc.collect() + gc.collect() + gc.collect() + return orig__call_funcptr(funcptr, *newargs) + monkeypatch.setattr(f, '_call_funcptr', _call_funcptr) + # + result = f(b"abcd", ord("b")) + assert result == b"bcd" + +def test_caching_bug_1(dll): + # the same test as test_call_some_args, with two extra lines + # in the middle that trigger caching in f._ptr, which then + # makes the last two lines fail + f = dll.my_strchr + f.argtypes = [c_char_p, c_int] + f.restype = c_char_p + result = f(b"abcd", ord("b")) + assert result == b"bcd" + result = f(b"abcd", ord("b"), 42) + assert result == b"bcd" + +def test_argument_conversion_and_checks(dll): + #This test is designed to check for segfaults if the wrong type of argument is passed as parameter + strlen = dll.my_strchr + strlen.argtypes = [c_char_p, c_int] + strlen.restype = c_char_p + assert strlen(b"eggs", ord("g")) == b"ggs" + + # Should raise ArgumentError, not segfault + with pytest.raises(ArgumentError): + strlen(0, 0) + with pytest.raises(ArgumentError): + strlen(False, 0) + +def test_union_as_passed_value(dll): + class UN(Union): + _fields_ = [("x", c_short), + ("y", c_long)] + dll.ret_un_func.restype = UN + dll.ret_un_func.argtypes = [UN] + A = UN * 2 + a = A() + a[1].x = 33 + u = dll.ret_un_func(a[1]) + assert u.y == 33 * 10000 + +@pytest.mark.pypy_only +def test_cache_funcptr(dll): + tf_b = dll.tf_b + tf_b.restype = c_byte + tf_b.argtypes = (c_byte,) + assert tf_b(-126) == -42 + ptr = tf_b._ptr + assert ptr is not None + assert tf_b(-126) == -42 + assert tf_b._ptr is ptr + +def test_custom_from_param(dll): + class A(c_byte): + @classmethod + def from_param(cls, obj): + seen.append(obj) + return -126 + tf_b = dll.tf_b + tf_b.restype = c_byte + tf_b.argtypes = (c_byte,) + tf_b.argtypes = [A] + seen = [] + assert tf_b("yadda") == -42 + assert seen == ["yadda"] + +@pytest.mark.xfail(reason="warnings are disabled") +def test_warnings(dll): + import warnings + warnings.simplefilter("always") + with warnings.catch_warnings(record=True) as w: + dll.get_an_integer() + assert len(w) == 1 + assert issubclass(w[0].category, RuntimeWarning) + assert "C function without declared arguments called" in str(w[0].message) + +@pytest.mark.xfail +def test_errcheck(dll): + import warnings + def errcheck(result, func, args): + assert result == -42 + assert type(result) is int + arg, = args + assert arg == -126 + assert type(arg) is int + return result + # + tf_b = dll.tf_b + tf_b.restype = c_byte + tf_b.argtypes = (c_byte,) + tf_b.errcheck = errcheck + assert tf_b(-126) == -42 + del tf_b.errcheck + with warnings.catch_warnings(record=True) as w: + dll.get_an_integer.argtypes = [] + dll.get_an_integer() + assert len(w) == 1 + assert issubclass(w[0].category, RuntimeWarning) + assert "C function without declared return type called" in str(w[0].message) + + with warnings.catch_warnings(record=True) as w: + dll.get_an_integer.restype = None + dll.get_an_integer() + assert len(w) == 0 + + warnings.resetwarnings() + +def test_errno(dll): + test_errno = dll.test_errno + test_errno.restype = c_int + set_errno(42) + res = test_errno() + n = get_errno() + assert (res, n) == (42, 43) + set_errno(0) + assert get_errno() == 0 + +def test_issue1655(dll): + def ret_list_p(icount): + def sz_array_p(obj, func, args): + assert ('.LP_c_int object' in repr(obj) or + '.LP_c_long object' in repr(obj)) + assert repr(args) =="(b'testing!', c_int(4))" + assert args[icount].value == 4 + return [obj[i] for i in range(args[icount].value)] + return sz_array_p + + get_data_prototype = CFUNCTYPE(POINTER(c_int), + c_char_p, POINTER(c_int)) + get_data_paramflag = ((1,), (2,)) + get_data_signature = ('test_issue1655', dll) + + get_data = get_data_prototype(get_data_signature, get_data_paramflag) + assert get_data(b'testing!') == 4 + + get_data.errcheck = ret_list_p(1) + assert get_data(b'testing!') == [-1, -2, -3, -4] + +def test_issue2533(tmpdir): + import cffi + ffi = cffi.FFI() + ffi.cdef("int **fetchme(void);") + ffi.set_source("_x_cffi", """ + int **fetchme(void) + { + static int a = 42; + static int *pa = &a; + return &pa; + } + """) + ffi.compile(verbose=True, tmpdir=str(tmpdir)) + + import sys + sys.path.insert(0, str(tmpdir)) + try: + from _x_cffi import ffi, lib + finally: + sys.path.pop(0) + fetchme = ffi.addressof(lib, 'fetchme') + fetchme = int(ffi.cast("intptr_t", fetchme)) + + FN = CFUNCTYPE(POINTER(POINTER(c_int))) + ff = cast(fetchme, FN) + + g = ff() + assert g.contents.contents.value == 42 + + h = c_int(43) + g[0] = pointer(h) # used to crash here + assert g.contents.contents.value == 43 diff --git a/extra_tests/snippets/ctypes_tests/test_guess_argtypes.py b/extra_tests/snippets/ctypes_tests/test_guess_argtypes.py new file mode 100644 index 0000000000..e3e79f48b5 --- /dev/null +++ b/extra_tests/snippets/ctypes_tests/test_guess_argtypes.py @@ -0,0 +1,38 @@ + +""" This test checks whether args wrapping behavior is correct +""" +import pytest +import sys + +from ctypes import * + +@pytest.mark.pypy_only +def test_wrap_args(): + from _ctypes import CFuncPtr + + def guess(value): + _, cobj, ctype = CFuncPtr._conv_param(None, value) + return ctype + ## cobj = CFuncPtr._conv_param(None, value) + ## return type(cobj) + + assert guess(13) == c_int + assert guess(0) == c_int + assert guess(b'xca') == c_char_p + assert guess(None) == c_void_p + assert guess(c_int(3)) == c_int + assert guess(u'xca') == c_wchar_p + + class Stuff: + pass + s = Stuff() + s._as_parameter_ = None + + assert guess(s) == c_void_p + +def test_guess_unicode(dll): + if not hasattr(sys, 'pypy_translation_info') and sys.platform != 'win32': + pytest.skip("CPython segfaults: see http://bugs.python.org/issue5203") + wcslen = dll.my_wcslen + text = u"Some long unicode string" + assert wcslen(text) == len(text) diff --git a/extra_tests/snippets/ctypes_tests/test_keepalive.py b/extra_tests/snippets/ctypes_tests/test_keepalive.py new file mode 100644 index 0000000000..ae390aef4b --- /dev/null +++ b/extra_tests/snippets/ctypes_tests/test_keepalive.py @@ -0,0 +1,263 @@ +""" Tests whether various objects land in _objects +or not +""" +import pytest + +from ctypes import * +import sys + +def test_array_of_pointers(): + # tests array item assignements & pointer.contents = ... + A = POINTER(c_long) * 24 + a = A() + l = c_long(2) + p = pointer(l) + a[3] = p + assert l._objects is None + assert p._objects == {'1':l} + assert a._objects == {'3':{'1':l}} + +def test_simple_structure_and_pointer(): + class X(Structure): + _fields_ = [('x', POINTER(c_int))] + + x = X() + p = POINTER(c_int)() + assert x._objects is None + assert p._objects is None + x.x = p + assert p._objects == {} + assert len(x._objects) == 1 + assert x._objects['0'] is p._objects + +def test_simple_structure_and_pointer_with_array(): + class X(Structure): + _fields_ = [('array', POINTER(c_int))] + + x = X() + a = (c_int * 3)(1, 2, 3) + assert x._objects is None + x.array = a + assert x._objects['0'] is a + +def test_structure_with_pointers(): + class X(Structure): + _fields_ = [('x', POINTER(c_int)), + ('y', POINTER(c_int))] + + x = X() + u = c_int(3) + p = pointer(u) + x.x = p + assert x.x._objects is None + assert p._objects == {'1': u} + assert x._objects == {'0': p._objects} + + w = c_int(4) + q = pointer(w) + x.y = q + assert p._objects == {'1': u} + assert q._objects == {'1': w} + assert x._objects == {'0': p._objects, '1': q._objects} + + n = POINTER(c_int)() + x.x = n + x.y = n + assert x._objects == {'0': n._objects, '1': n._objects} + assert x._objects['0'] is n._objects + assert n._objects is not None + +def test_union_with_pointers(): + class X(Union): + _fields_ = [('x', POINTER(c_int)), + ('y', POINTER(c_int))] + + x = X() + u = c_int(3) + p = pointer(u) + x.x = p + assert x.x._objects is None + assert p._objects == {'1': u} + assert x._objects == {'0': p._objects} + + # unions works just like structures it seems + w = c_int(4) + q = pointer(w) + x.y = q + assert p._objects == {'1': u} + assert q._objects == {'1': w} + assert x._objects == {'0': p._objects, '1': q._objects} + + n = POINTER(c_int)() + x.x = n + x.y = n + assert x._objects == {'0': n._objects, '1': n._objects} + assert x._objects['0'] is n._objects + assert n._objects is not None + +def test_pointer_setitem(): + x = c_int(2) + y = c_int(3) + p = pointer(x) + assert p._objects == {'1':x} + p[0] = y + assert list(p._objects.keys()) == ['1'] + assert p._objects['1'].value == 3 + +@pytest.mark.pypy_only +def test_primitive(): + assert c_char_p(b"abc")._objects._buffer[0] == b"a" + assert c_int(3)._objects is None + +def test_pointer_to_pointer(): + l = c_long(2) + assert l._objects is None + + p1 = pointer(l) + assert p1._objects == {'1':l} + + p2 = pointer(p1) + assert p2._objects == {'1':p1, '0':{'1':l}} + +def test_cfunc(): + def f(): + pass + cf = CFUNCTYPE(c_int, c_int)(f) + assert cf._objects == {'0':cf} + +def test_cfunc_cast(): + def f(): + pass + cf = CFUNCTYPE(c_int, c_int)(f) + p1 = cast(cf, c_void_p) + assert p1._objects == {id(cf): cf, '0': cf} + +def test_array_of_struct_with_pointer(): + class S(Structure): + _fields_ = [('x', c_int)] + PS = POINTER(S) + + class Q(Structure): + _fields_ = [('p', PS)] + + A = Q*10 + a=A() + s=S() + s.x=3 + a[3].p = pointer(s) + + assert a._objects['0:3']['1'] is s + +def test_array_of_union_with_pointer(): + class S(Structure): + _fields_ = [('x', c_int)] + PS = POINTER(S) + + class Q(Union): + _fields_ = [('p', PS), ('x', c_int)] + + A = Q*10 + a=A() + s=S() + s.x=3 + a[3].p = pointer(s) + + assert a._objects['0:3']['1'] is s + +def test_struct_with_inlined_array(): + class S(Structure): + _fields_ = [('b', c_int), + ('a', POINTER(c_int) * 2)] + + s = S() + stuff = c_int(2) + s.a[1] = pointer(stuff) + assert s._objects == {'1:1': {'1': stuff}} + +def test_union_with_inlined_array(): + class S(Union): + _fields_ = [('b', c_int), + ('a', POINTER(c_int) * 2)] + + s = S() + stuff = c_int(2) + s.a[1] = pointer(stuff) + assert s._objects == {'1:1': {'1': stuff}} + +def test_struct_within_struct(): + class R(Structure): + _fields_ = [('p', POINTER(c_int))] + + class S(Structure): + _fields_ = [('b', c_int), + ('r', R)] + + s = S() + stuff = c_int(2) + s.r.p = pointer(stuff) + assert s._objects == {'0:1': {'1': stuff}} + + r = R() + s.r = r + # obscure + assert s._objects == {'1': {}, '0:1': {'1': stuff}} + +def test_union_within_union(): + class R(Union): + _fields_ = [('p', POINTER(c_int))] + + class S(Union): + _fields_ = [('b', c_int), + ('r', R)] + + s = S() + stuff = c_int(2) + s.r.p = pointer(stuff) + assert s._objects == {'0:1': {'1': stuff}} + + r = R() + s.r = r + # obscure + assert s._objects == {'1': {}, '0:1': {'1': stuff}} + +def test_c_char_p(): + n = 2 + xs = b"hello" * n + x = c_char_p(xs) + del xs + import gc; gc.collect() + assert x.value == b'hellohello' + assert x._objects == b'hellohello' + # + class datum(Structure): + _fields_ = [ + ('dptr', c_char_p), + ('dsize', c_int), + ] + class union(Union): + _fields_ = [ + ('dptr', c_char_p), + ('dsize', c_int), + ] + for wrap in [False, True]: + n = 2 + xs = b"hello" * n + if wrap: + xs = c_char_p(xs) + dat = datum() + dat.dptr = xs + dat.dsize = 15 + del xs + import gc; gc.collect() + assert dat.dptr == b"hellohello" + assert list(dat._objects.keys()) == ['0'] + + xs = b"hello" * n + if wrap: + xs = c_char_p(xs) + dat = union() + dat.dptr = xs + del xs + import gc; gc.collect() + assert dat.dptr == b"hellohello" + assert list(dat._objects.keys()) == ['0'] diff --git a/extra_tests/snippets/ctypes_tests/test_loading.py b/extra_tests/snippets/ctypes_tests/test_loading.py new file mode 100644 index 0000000000..65b9b63710 --- /dev/null +++ b/extra_tests/snippets/ctypes_tests/test_loading.py @@ -0,0 +1,8 @@ +from ctypes import CDLL +from ctypes.util import find_library + +def test__handle(): + lib = find_library("c") + if lib: + cdll = CDLL(lib) + assert type(cdll._handle) is int diff --git a/extra_tests/snippets/ctypes_tests/test_numbers.py b/extra_tests/snippets/ctypes_tests/test_numbers.py new file mode 100644 index 0000000000..f73bf4f126 --- /dev/null +++ b/extra_tests/snippets/ctypes_tests/test_numbers.py @@ -0,0 +1,56 @@ +import pytest +from ctypes import * + +unsigned_types = [c_ubyte, c_ushort, c_uint, c_ulong] +signed_types = [c_byte, c_short, c_int, c_long, c_longlong] + +float_types = [c_double, c_float, c_longdouble] + +try: + c_ulonglong + c_longlong +except NameError: + pass +else: + unsigned_types.append(c_ulonglong) + signed_types.append(c_longlong) + +################################################################ + +@pytest.mark.parametrize('t', signed_types + unsigned_types + float_types) +def test_init_again(t): + parm = t() + addr1 = addressof(parm) + parm.__init__(0) + addr2 = addressof(parm) + assert addr1 == addr2 + +def test_subclass(): + class enum(c_int): + def __new__(cls, value): + dont_call_me + class S(Structure): + _fields_ = [('t', enum)] + assert isinstance(S().t, enum) + +#@pytest.mark.xfail("'__pypy__' not in sys.builtin_module_names") +@pytest.mark.xfail +def test_no_missing_shape_to_ffi_type(): + # whitebox test + "re-enable after adding 'g' to _shape_to_ffi_type.typemap, " + "which I think needs fighting all the way up from " + "rpython.rlib.libffi" + from _ctypes.basics import _shape_to_ffi_type + from _rawffi import Array + for i in range(1, 256): + try: + Array(chr(i)) + except ValueError: + pass + else: + assert chr(i) in _shape_to_ffi_type.typemap + +@pytest.mark.xfail +def test_pointer_to_long_double(): + import ctypes + ctypes.POINTER(ctypes.c_longdouble) diff --git a/extra_tests/snippets/ctypes_tests/test_parameters.py b/extra_tests/snippets/ctypes_tests/test_parameters.py new file mode 100644 index 0000000000..2f39e046b0 --- /dev/null +++ b/extra_tests/snippets/ctypes_tests/test_parameters.py @@ -0,0 +1,24 @@ +from ctypes import POINTER, c_void_p + +def test_pointer_subclasses(): + Void_pp = POINTER(c_void_p) + class My_void_p(c_void_p): + pass + + My_void_pp = POINTER(My_void_p) + o = My_void_pp() + + assert Void_pp.from_param(o) is o + + +def test_multiple_signature(dll): + # when .argtypes is not set, calling a function with a certain + # set of parameters should not prevent another call with + # another set. + func = dll._testfunc_p_p + + # This is call has too many arguments + assert func(None, 1) == 0 + + # This one is normal + assert func(None) == 0 diff --git a/extra_tests/snippets/ctypes_tests/test_pointers.py b/extra_tests/snippets/ctypes_tests/test_pointers.py new file mode 100644 index 0000000000..2dcf18dd2c --- /dev/null +++ b/extra_tests/snippets/ctypes_tests/test_pointers.py @@ -0,0 +1,92 @@ +import pytest +from ctypes import * +import struct + +@pytest.mark.pypy_only +def test_get_ffi_argtype(): + P = POINTER(c_int) + ffitype = P.get_ffi_argtype() + assert P.get_ffi_argtype() is ffitype + assert ffitype.deref_pointer() is c_int.get_ffi_argtype() + +@pytest.mark.parametrize("c_type, py_type", [ + (c_byte, int), + (c_ubyte, int), + (c_short, int), + (c_ushort, int), + (c_int, int), + (c_uint, int), + (c_long, int), + (c_ulong, int), + (c_longlong, int), + (c_ulonglong, int), + (c_double, float), + (c_float, float), +]) +def test_byref(c_type, py_type): + i = c_type(42) + p = byref(i) + assert type(p._obj) is c_type + assert p._obj.value == 42 + +def test_pointer_to_pointer(): + x = c_int(32) + y = c_int(42) + p1 = pointer(x) + p2 = pointer(p1) + assert p2.contents.contents.value == 32 + p2.contents.contents = y + assert p2.contents.contents.value == 42 + assert p1.contents.value == 42 + +def test_c_char_p_byref(dll): + TwoOutArgs = dll.TwoOutArgs + TwoOutArgs.restype = None + TwoOutArgs.argtypes = [c_int, c_void_p, c_int, c_void_p] + a = c_int(3) + b = c_int(4) + c = c_int(5) + d = c_int(6) + TwoOutArgs(a, byref(b), c, byref(d)) + assert b.value == 7 + assert d.value == 11 + +def test_byref_cannot_be_bound(): + class A(object): + _byref = byref + A._byref(c_int(5)) + +def test_byref_with_offset(): + c = c_int() + d = byref(c) + base = cast(d, c_void_p).value + for i in [0, 1, 4, 1444, -10293]: + assert cast(byref(c, i), c_void_p).value == base + i + +@pytest.mark.pypy_only +def test_issue2813_fix(): + class C(Structure): + pass + POINTER(C) + C._fields_ = [('x', c_int)] + ffitype = C.get_ffi_argtype() + assert C.get_ffi_argtype() is ffitype + assert ffitype.sizeof() == sizeof(c_int) + +@pytest.mark.pypy_only +def test_issue2813_cant_change_fields_after_get_ffi_argtype(): + class C(Structure): + pass + ffitype = C.get_ffi_argtype() + with pytest.raises(NotImplementedError): + C._fields_ = [('x', c_int)] + +def test_memoryview(): + x = c_int(32) + p1 = pointer(x) + p2 = pointer(p1) + + m1 = memoryview(p1) + assert struct.unpack('P', m1)[0] == addressof(x) + m2 = memoryview(p2) + assert struct.unpack('P', m2)[0] == addressof(p1) diff --git a/extra_tests/snippets/ctypes_tests/test_prototypes.py b/extra_tests/snippets/ctypes_tests/test_prototypes.py new file mode 100644 index 0000000000..236ce74499 --- /dev/null +++ b/extra_tests/snippets/ctypes_tests/test_prototypes.py @@ -0,0 +1,65 @@ +import pytest +from ctypes import * + + +def test_restype_setattr(dll): + func = dll._testfunc_p_p + with pytest.raises(TypeError): + setattr(func, 'restype', 20) + +def test_argtypes_setattr(dll): + func = dll._testfunc_p_p + with pytest.raises(TypeError): + setattr(func, 'argtypes', 20) + with pytest.raises(TypeError): + setattr(func, 'argtypes', [20]) + + func = CFUNCTYPE(c_long, c_void_p, c_long)(lambda: None) + assert func.argtypes == (c_void_p, c_long) + +def test_paramflags_setattr(): + func = CFUNCTYPE(c_long, c_void_p, c_long)(lambda: None) + with pytest.raises(TypeError): + setattr(func, 'paramflags', 'spam') + with pytest.raises(ValueError): + setattr(func, 'paramflags', (1, 2, 3, 4)) + with pytest.raises(TypeError): + setattr(func, 'paramflags', ((1,), ('a',))) + func.paramflags = (1,), (1|4,) + +def test_kwargs(dll): + proto = CFUNCTYPE(c_char_p, c_char_p, c_int) + paramflags = (1, 'text', b"tavino"), (1, 'letter', ord('v')) + func = proto(('my_strchr', dll), paramflags) + assert func.argtypes == (c_char_p, c_int) + assert func.restype == c_char_p + + result = func(b"abcd", ord('b')) + assert result == b"bcd" + + result = func() + assert result == b"vino" + + result = func(b"grapevine") + assert result == b"vine" + + result = func(text=b"grapevine") + assert result == b"vine" + + result = func(letter=ord('i')) + assert result == b"ino" + + result = func(letter=ord('p'), text=b"impossible") + assert result == b"possible" + + result = func(text=b"impossible", letter=ord('p')) + assert result == b"possible" + +def test_array_to_ptr_wrongtype(dll): + ARRAY = c_byte * 8 + func = dll._testfunc_ai8 + func.restype = POINTER(c_int) + func.argtypes = [c_int * 8] + array = ARRAY(1, 2, 3, 4, 5, 6, 7, 8) + with pytest.raises(ArgumentError): + func(array) diff --git a/extra_tests/snippets/ctypes_tests/test_structures.py b/extra_tests/snippets/ctypes_tests/test_structures.py new file mode 100644 index 0000000000..96531ddf3c --- /dev/null +++ b/extra_tests/snippets/ctypes_tests/test_structures.py @@ -0,0 +1,223 @@ +from ctypes import * + +import pytest +import sys + + +def test_subclass_initializer(): + class POINT(Structure): + _fields_ = [("x", c_int), ("y", c_int)] + + class POSITION(POINT): + # A subclass without _fields_ + pass + pos = POSITION(1, 2) + assert (pos.x, pos.y) == (1, 2) + # Try a second time, result may be different (cf. issue1498) + pos = POSITION(1, 2) + assert (pos.x, pos.y) == (1, 2) + +def test_fields_is_a_tuple(): + class Person(Structure): + _fields_ = (("name", c_char*6), + ("age", c_int)) + + # short enough + p = Person(b"123456", 6) + assert p.name == b"123456" + assert p.age == 6 + +def test___init__(): + class Person(Structure): + _fields_ = (("name", c_char*10), + ("age", c_int)) + + def __init__(self, name, surname, age): + self.name = name + b' ' + surname + self.age = age + + p = Person(b"John", b"Doe", 25) + assert p.name == b"John Doe" + assert p.age == 25 + +def test_setattr(): + class X(Structure): + _fields_ = [("a", c_int)] + + x = X() + x.other = 42 + assert x.other == 42 + +def test_withslots(): + class X(Structure): + _fields_ = [("a", c_int * 2)] + __slots__ = ['a'] + + x = X() + x.a = (42, 43) + assert tuple(x.a) == (42, 43) + +def test_getattr_recursion(): + # Structure.__getattr__ used to call itself recursively + # and hit the recursion limit. + import sys + events = [] + + def tracefunc(frame, event, arg): + funcname = frame.f_code.co_name + if 'getattr' in funcname: + events.append(funcname) + + oldtrace = sys.settrace(tracefunc) + try: + class X(Structure): + _fields_ = [("a", c_int)] + + assert len(events) < 20 + finally: + sys.settrace(oldtrace) + events = None + +def test_large_fields(): + # make sure that large fields are not "confused" with bitfields + # (because the bitfields use the higher bits of the "size" attribute) + Array = c_long * 8192 + class X(Structure): + _fields_ = [('items', Array)] + obj = X() + assert isinstance(obj.items, Array) + +def test_b_base(): + # _b_base_ used to be None here in PyPy + class X(Structure): + _fields_ = [('x', c_int)] + obj = X() + p = pointer(obj) + assert p.contents._b_base_ is p + +def test_swapped_bytes(): + import sys + + for i in [c_short, c_int, c_long, c_longlong, + c_float, c_double, c_ushort, c_uint, + c_ulong, c_ulonglong]: + FIELDS = [ + ('n', i) + ] + + class Native(Structure): + _fields_ = FIELDS + + class Big(BigEndianStructure): + _fields_ = FIELDS + + class Little(LittleEndianStructure): + _fields_ = FIELDS + + def dostruct(c): + ba = create_string_buffer(sizeof(c)) + ms = c.from_buffer(ba) + ms.n = 0xff00 + return repr(ba[:]) + + nstruct = dostruct(Native) + if sys.byteorder == 'little': + assert nstruct == dostruct(Little) + assert nstruct != dostruct(Big) + assert Big._fields_[0][1] is not i + else: + assert nstruct == dostruct(Big) + assert nstruct != dostruct(Little) + assert Little._fields_[0][1] is not i + +def test_from_buffer_copy(): + from array import array + + class S(Structure): + _fields_ = [('i', c_int)] + def __init__(self, some, unused, arguments): + pass + a = array('i', [1234567]) + s1 = S.from_buffer(a) + s2 = S.from_buffer_copy(a) + assert s1.i == 1234567 + assert s2.i == 1234567 + a[0] = -7654321 + assert s1.i == -7654321 + assert s2.i == 1234567 + + +def test_nonfinal_struct(): + class X(Structure): + pass + assert sizeof(X) == 0 + X._fields_ = [("a", c_int),] + with pytest.raises(AttributeError): + X._fields_ = [] + + class X(Structure): + pass + X() + with pytest.raises(AttributeError): + X._fields_ = [] + + class X(Structure): + pass + class Y(X): + pass + with pytest.raises(AttributeError): + X._fields_ = [] + Y.__fields__ = [] + + +def test_structure_overloading_getattr(): + class X(Structure): + _fields_ = [('x', c_int)] + + def __getattr__(self, name): + raise AttributeError(name) + + x = X() + assert x.x == 0 + +def test_duplicate_names(): + class S(Structure): + _fields_ = [('a', c_int), + ('b', c_int), + ('a', c_byte)] + s = S(260, -123) + assert sizeof(s) == 3 * sizeof(c_int) + assert s.a == 4 # 256 + 4 + assert s.b == -123 + +def test_memoryview(): + class S(Structure): + _fields_ = [('a', c_int16), + ('b', c_int16), + ] + + S3 = S * 3 + c_array = (2 * S3)( + S3(S(a=0, b=1), S(a=2, b=3), S(a=4, b=5)), + S3(S(a=6, b=7), S(a=8, b=9), S(a=10, b=11)), + ) + + mv = memoryview(c_array) + if sys.byteorder == 'little': + assert mv.format == 'T{h:a:>h:b:}' + assert mv.shape == (2, 3) + assert mv.itemsize == 4 + +def test_memoryview_endian(): + class LES(LittleEndianStructure): + _pack_ = 1 + _fields_ = [ + ('a', c_ubyte * 16), + ('i', c_uint64) + ] + c_les = LES() + mv = memoryview(c_les) + assert mv.format == 'B' + diff --git a/extra_tests/snippets/ctypes_tests/test_unions.py b/extra_tests/snippets/ctypes_tests/test_unions.py new file mode 100644 index 0000000000..f033f0846a --- /dev/null +++ b/extra_tests/snippets/ctypes_tests/test_unions.py @@ -0,0 +1,28 @@ +import sys +from ctypes import * + +def test_getattr(): + class Stuff(Union): + _fields_ = [('x', c_char), ('y', c_int)] + + stuff = Stuff() + stuff.y = ord('x') | (ord('z') << 24) + if sys.byteorder == 'little': + assert stuff.x == b'x' + else: + assert stuff.x == b'z' + +def test_union_of_structures(): + class Stuff(Structure): + _fields_ = [('x', c_int)] + + class Stuff2(Structure): + _fields_ = [('x', c_int)] + + class UnionofStuff(Union): + _fields_ = [('one', Stuff), + ('two', Stuff2)] + + u = UnionofStuff() + u.one.x = 3 + assert u.two.x == 3 diff --git a/extra_tests/snippets/ctypes_tests/test_values.py b/extra_tests/snippets/ctypes_tests/test_values.py new file mode 100644 index 0000000000..2c3aa8d8f7 --- /dev/null +++ b/extra_tests/snippets/ctypes_tests/test_values.py @@ -0,0 +1,10 @@ +from ctypes import * + +def test_a_string(dll): + """ + A testcase which accesses *values* in a dll. + """ + a_string = (c_char * 16).in_dll(dll, "a_string") + assert a_string.raw == b"0123456789abcdef" + a_string[15:16] = b'$' + assert dll.get_a_string_char(15) == ord('$') diff --git a/extra_tests/snippets/ctypes_tests/test_win32.py b/extra_tests/snippets/ctypes_tests/test_win32.py new file mode 100644 index 0000000000..8ee68e6580 --- /dev/null +++ b/extra_tests/snippets/ctypes_tests/test_win32.py @@ -0,0 +1,13 @@ +# Windows specific tests + +from ctypes import * + +import pytest + +@pytest.mark.skipif("sys.platform != 'win32'") +def test_VARIANT(): + from ctypes import wintypes + a = wintypes.VARIANT_BOOL() + assert a.value is False + b = wintypes.VARIANT_BOOL(3) + assert b.value is True diff --git a/jit/Cargo.toml b/jit/Cargo.toml index 5e354273e3..be6f31de3a 100644 --- a/jit/Cargo.toml +++ b/jit/Cargo.toml @@ -14,7 +14,7 @@ cranelift = "0.72.0" cranelift-module = "0.72.0" cranelift-jit = "0.72.0" num-traits = "0.2" -libffi = "1.0" +libffi = "1.0.1" rustpython-bytecode = { path = "../bytecode", version = "0.1.2" } thiserror = "1.0" diff --git a/vm/Cargo.toml b/vm/Cargo.toml index a452bcdbbb..dba11e905d 100644 --- a/vm/Cargo.toml +++ b/vm/Cargo.toml @@ -130,6 +130,10 @@ openssl-sys = { version = "0.9", optional = true } openssl-probe = { version = "0.1", optional = true } which = "4.0" foreign-types-shared = { version = "0.1", optional = true } +# ctypes +libloading = "0.7.0" +libffi = "1.0.1" +widestring = "0.4.3" [target.'cfg(any(not(target_arch = "wasm32"), target_os = "wasi"))'.dependencies] num_cpus = "1" diff --git a/vm/src/stdlib/ctypes/_ctypes_test.c b/vm/src/stdlib/ctypes/_ctypes_test.c new file mode 100644 index 0000000000..17e10c64f4 --- /dev/null +++ b/vm/src/stdlib/ctypes/_ctypes_test.c @@ -0,0 +1,860 @@ +#if defined(_MSC_VER) || defined(__CYGWIN__) +#include +#define MS_WIN32 +#endif + +#ifdef _WIN32 +#define EXPORT(x) __declspec(dllexport) x +#else +#define EXPORT(x) extern x +#endif + +#include +#include +#include +#include +#include +#include + +#define HAVE_LONG_LONG +#define LONG_LONG long long +#define HAVE_WCHAR_H + +/* some functions handy for testing */ + +EXPORT(int) +_testfunc_cbk_reg_int(int a, int b, int c, int d, int e, + int (*func)(int, int, int, int, int)) +{ + return func(a*a, b*b, c*c, d*d, e*e); +} + +EXPORT(double) +_testfunc_cbk_reg_double(double a, double b, double c, double d, double e, + double (*func)(double, double, double, double, double)) +{ + return func(a*a, b*b, c*c, d*d, e*e); +} + +/* + * This structure should be the same as in test_callbacks.py and the + * method test_callback_large_struct. See issues 17310 and 20160: the + * structure must be larger than 8 bytes long. + */ + +typedef struct { + unsigned long first; + unsigned long second; + unsigned long third; +} Test; + +EXPORT(void) +_testfunc_cbk_large_struct(Test in, void (*func)(Test)) +{ + func(in); +} + +/* + * See issue 29565. Update a structure passed by value; + * the caller should not see any change. + */ + +EXPORT(void) +_testfunc_large_struct_update_value(Test in) +{ + ((volatile Test *)&in)->first = 0x0badf00d; + ((volatile Test *)&in)->second = 0x0badf00d; + ((volatile Test *)&in)->third = 0x0badf00d; +} + +typedef struct { + unsigned int first; + unsigned int second; +} TestReg; + + +EXPORT(TestReg) last_tfrsuv_arg = {0}; + + +EXPORT(void) +_testfunc_reg_struct_update_value(TestReg in) +{ + last_tfrsuv_arg = in; + ((volatile TestReg *)&in)->first = 0x0badf00d; + ((volatile TestReg *)&in)->second = 0x0badf00d; +} + +/* + * See bpo-22273. Structs containing arrays should work on Linux 64-bit. + */ + +typedef struct { + unsigned char data[16]; +} Test2; + +EXPORT(int) +_testfunc_array_in_struct1(Test2 in) +{ + int result = 0; + + for (unsigned i = 0; i < 16; i++) + result += in.data[i]; + /* As the structure is passed by value, changes to it shouldn't be + * reflected in the caller. + */ + memset(in.data, 0, sizeof(in.data)); + return result; +} + +typedef struct { + double data[2]; +} Test3; + +typedef struct { + float data[2]; + float more_data[2]; +} Test3B; + +EXPORT(double) +_testfunc_array_in_struct2(Test3 in) +{ + double result = 0; + + for (unsigned i = 0; i < 2; i++) + result += in.data[i]; + /* As the structure is passed by value, changes to it shouldn't be + * reflected in the caller. + */ + memset(in.data, 0, sizeof(in.data)); + return result; +} + +EXPORT(double) +_testfunc_array_in_struct2a(Test3B in) +{ + double result = 0; + + for (unsigned i = 0; i < 2; i++) + result += in.data[i]; + for (unsigned i = 0; i < 2; i++) + result += in.more_data[i]; + /* As the structure is passed by value, changes to it shouldn't be + * reflected in the caller. + */ + memset(in.data, 0, sizeof(in.data)); + return result; +} + +typedef union { + long a_long; + struct { + int an_int; + int another_int; + } a_struct; +} Test4; + +typedef struct { + int an_int; + struct { + int an_int; + Test4 a_union; + } nested; + int another_int; +} Test5; + +EXPORT(long) +_testfunc_union_by_value1(Test4 in) { + long result = in.a_long + in.a_struct.an_int + in.a_struct.another_int; + + /* As the union/struct are passed by value, changes to them shouldn't be + * reflected in the caller. + */ + memset(&in, 0, sizeof(in)); + return result; +} + +EXPORT(long) +_testfunc_union_by_value2(Test5 in) { + long result = in.an_int + in.nested.an_int; + + /* As the union/struct are passed by value, changes to them shouldn't be + * reflected in the caller. + */ + memset(&in, 0, sizeof(in)); + return result; +} + +EXPORT(long) +_testfunc_union_by_reference1(Test4 *in) { + long result = in->a_long; + + memset(in, 0, sizeof(Test4)); + return result; +} + +EXPORT(long) +_testfunc_union_by_reference2(Test4 *in) { + long result = in->a_struct.an_int + in->a_struct.another_int; + + memset(in, 0, sizeof(Test4)); + return result; +} + +EXPORT(long) +_testfunc_union_by_reference3(Test5 *in) { + long result = in->an_int + in->nested.an_int + in->another_int; + + memset(in, 0, sizeof(Test5)); + return result; +} + +typedef struct { + signed int A: 1, B:2, C:3, D:2; +} Test6; + +EXPORT(long) +_testfunc_bitfield_by_value1(Test6 in) { + long result = in.A + in.B + in.C + in.D; + + /* As the struct is passed by value, changes to it shouldn't be + * reflected in the caller. + */ + memset(&in, 0, sizeof(in)); + return result; +} + +EXPORT(long) +_testfunc_bitfield_by_reference1(Test6 *in) { + long result = in->A + in->B + in->C + in->D; + + memset(in, 0, sizeof(Test6)); + return result; +} + +typedef struct { + unsigned int A: 1, B:2, C:3, D:2; +} Test7; + +EXPORT(long) +_testfunc_bitfield_by_reference2(Test7 *in) { + long result = in->A + in->B + in->C + in->D; + + memset(in, 0, sizeof(Test7)); + return result; +} + +typedef union { + signed int A: 1, B:2, C:3, D:2; +} Test8; + +EXPORT(long) +_testfunc_bitfield_by_value2(Test8 in) { + long result = in.A + in.B + in.C + in.D; + + /* As the struct is passed by value, changes to it shouldn't be + * reflected in the caller. + */ + memset(&in, 0, sizeof(in)); + return result; +} + +EXPORT(void)testfunc_array(int values[4]) +{ + printf("testfunc_array %d %d %d %d\n", + values[0], + values[1], + values[2], + values[3]); +} + +EXPORT(long double)testfunc_Ddd(double a, double b) +{ + long double result = (long double)(a * b); + printf("testfunc_Ddd(%p, %p)\n", (void *)&a, (void *)&b); + printf("testfunc_Ddd(%g, %g)\n", a, b); + return result; +} + +EXPORT(long double)testfunc_DDD(long double a, long double b) +{ + long double result = a * b; + printf("testfunc_DDD(%p, %p)\n", (void *)&a, (void *)&b); + printf("testfunc_DDD(%Lg, %Lg)\n", a, b); + return result; +} + +EXPORT(int)testfunc_iii(int a, int b) +{ + int result = a * b; + printf("testfunc_iii(%p, %p)\n", (void *)&a, (void *)&b); + return result; +} + +EXPORT(int)myprintf(char *fmt, ...) +{ + int result; + va_list argptr; + va_start(argptr, fmt); + result = vprintf(fmt, argptr); + va_end(argptr); + return result; +} + +EXPORT(char *)my_strtok(char *token, const char *delim) +{ + return strtok(token, delim); +} + +EXPORT(char *)my_strchr(const char *s, int c) +{ + return strchr(s, c); +} + + +EXPORT(double) my_sqrt(double a) +{ + return sqrt(a); +} + +EXPORT(void) my_qsort(void *base, size_t num, size_t width, int(*compare)(const void*, const void*)) +{ + qsort(base, num, width, compare); +} + +EXPORT(char) deref_LP_c_char_p(char** argv) +{ + char* s = *argv; + return s[0]; +} + +EXPORT(int *) _testfunc_ai8(int a[8]) +{ + return a; +} + +EXPORT(void) _testfunc_v(int a, int b, int *presult) +{ + *presult = a + b; +} + +EXPORT(int) _testfunc_i_bhilfd(signed char b, short h, int i, long l, float f, double d) +{ +/* printf("_testfunc_i_bhilfd got %d %d %d %ld %f %f\n", + b, h, i, l, f, d); +*/ + return (int)(b + h + i + l + f + d); +} + +EXPORT(float) _testfunc_f_bhilfd(signed char b, short h, int i, long l, float f, double d) +{ +/* printf("_testfunc_f_bhilfd got %d %d %d %ld %f %f\n", + b, h, i, l, f, d); +*/ + return (float)(b + h + i + l + f + d); +} + +EXPORT(double) _testfunc_d_bhilfd(signed char b, short h, int i, long l, float f, double d) +{ +/* printf("_testfunc_d_bhilfd got %d %d %d %ld %f %f\n", + b, h, i, l, f, d); +*/ + return (double)(b + h + i + l + f + d); +} + +EXPORT(char *) _testfunc_p_p(void *s) +{ + return (char *)s; +} + +EXPORT(void *) _testfunc_c_p_p(int *argcp, char **argv) +{ + return argv[(*argcp)-1]; +} + +EXPORT(void *) get_strchr(void) +{ + return (void *)strchr; +} + +EXPORT(char *) my_strdup(char *src) +{ + char *dst = (char *)malloc(strlen(src)+1); + if (!dst) + return NULL; + strcpy(dst, src); + return dst; +} + +EXPORT(void)my_free(void *ptr) +{ + free(ptr); +} + +#ifdef HAVE_WCHAR_H +EXPORT(wchar_t *) my_wcsdup(wchar_t *src) +{ + size_t len = wcslen(src); + wchar_t *ptr = (wchar_t *)malloc((len + 1) * sizeof(wchar_t)); + if (ptr == NULL) + return NULL; + memcpy(ptr, src, (len+1) * sizeof(wchar_t)); + return ptr; +} + +EXPORT(size_t) my_wcslen(wchar_t *src) +{ + return wcslen(src); +} +#endif + +#ifndef MS_WIN32 +# ifndef __stdcall +# define __stdcall /* */ +# endif +#endif + +typedef struct { + int (*c)(int, int); + int (__stdcall *s)(int, int); +} FUNCS; + +EXPORT(int) _testfunc_callfuncp(FUNCS *fp) +{ + fp->c(1, 2); + fp->s(3, 4); + return 0; +} + +EXPORT(int) _testfunc_deref_pointer(int *pi) +{ + return *pi; +} + +#ifdef MS_WIN32 +EXPORT(int) _testfunc_piunk(IUnknown FAR *piunk) +{ + piunk->lpVtbl->AddRef(piunk); + return piunk->lpVtbl->Release(piunk); +} +#endif + +EXPORT(int) _testfunc_callback_with_pointer(int (*func)(int *)) +{ + int table[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; + + return (*func)(table); +} + +EXPORT(int) _testfunc_callback_opaque(int (*func)(void*), void* arg) +{ + return (*func)(arg); +} + +EXPORT(int) _testfunc_callback_void(void (*func)(void)) +{ + func(); + return 0; +} + +#ifdef HAVE_LONG_LONG +EXPORT(LONG_LONG) _testfunc_q_bhilfdq(signed char b, short h, int i, long l, float f, + double d, LONG_LONG q) +{ + return (LONG_LONG)(b + h + i + l + f + d + q); +} + +EXPORT(LONG_LONG) _testfunc_q_bhilfd(signed char b, short h, int i, long l, float f, double d) +{ + return (LONG_LONG)(b + h + i + l + f + d); +} + +EXPORT(int) _testfunc_callback_i_if(int value, int (*func)(int)) +{ + int sum = 0; + while (value != 0) { + sum += func(value); + value /= 2; + } + return sum; +} + +EXPORT(LONG_LONG) _testfunc_callback_q_qf(LONG_LONG value, + LONG_LONG (*func)(LONG_LONG)) +{ + LONG_LONG sum = 0; + + while (value != 0) { + sum += func(value); + value /= 2; + } + return sum; +} + +#endif + +typedef struct { + char *name; + char *value; +} SPAM; + +typedef struct { + char *name; + int num_spams; + SPAM *spams; +} EGG; + +SPAM my_spams[2] = { + { "name1", "value1" }, + { "name2", "value2" }, +}; + +EGG my_eggs[1] = { + { "first egg", 1, my_spams } +}; + +EXPORT(int) getSPAMANDEGGS(EGG **eggs) +{ + *eggs = my_eggs; + return 1; +} + +typedef struct tagpoint { + int x; + int y; +} point; + +EXPORT(int) _testfunc_byval(point in, point *pout) +{ + if (pout) { + pout->x = in.x; + pout->y = in.y; + } + return in.x + in.y; +} + +EXPORT (int) an_integer = 42; + +EXPORT(int) get_an_integer(void) +{ + return an_integer; +} + +EXPORT(char) a_string[16] = "0123456789abcdef"; + +EXPORT(int) get_a_string_char(int index) +{ + return a_string[index]; +} + +EXPORT(double) +integrate(double a, double b, double (*f)(double), long nstep) +{ + double x, sum=0.0, dx=(b-a)/(double)nstep; + for(x=a+0.5*dx; (b-x)*(x-a)>0.0; x+=dx) + { + double y = f(x); + printf("f(x)=%.1f\n", y); + sum += f(x); + } + return sum/(double)nstep; +} + +typedef struct { + void (*initialize)(void *(*)(int), void(*)(void *)); +} xxx_library; + +static void _xxx_init(void *(*Xalloc)(int), void (*Xfree)(void *)) +{ + void *ptr; + + printf("_xxx_init got %p %p\n", Xalloc, Xfree); + printf("calling\n"); + ptr = Xalloc(32); + Xfree(ptr); + printf("calls done, ptr was %p\n", ptr); +} + +xxx_library _xxx_lib = { + _xxx_init +}; + +EXPORT(xxx_library) *library_get(void) +{ + return &_xxx_lib; +} + +#ifdef MS_WIN32 +/* See Don Box (german), pp 79ff. */ +EXPORT(void) GetString(BSTR *pbstr) +{ + *pbstr = SysAllocString(L"Goodbye!"); +} +#endif + +EXPORT(void) _py_func_si(char *s, int i) +{ +} + +EXPORT(void) _py_func(void) +{ +} + +EXPORT(LONG_LONG) last_tf_arg_s = 0; +EXPORT(unsigned LONG_LONG) last_tf_arg_u = 0; + +struct BITS { + int A: 1, B:2, C:3, D:4, E: 5, F: 6, G: 7, H: 8, I: 9; + short M: 1, N: 2, O: 3, P: 4, Q: 5, R: 6, S: 7; +}; + +EXPORT(void) set_bitfields(struct BITS *bits, char name, int value) +{ + switch (name) { + case 'A': bits->A = value; break; + case 'B': bits->B = value; break; + case 'C': bits->C = value; break; + case 'D': bits->D = value; break; + case 'E': bits->E = value; break; + case 'F': bits->F = value; break; + case 'G': bits->G = value; break; + case 'H': bits->H = value; break; + case 'I': bits->I = value; break; + + case 'M': bits->M = value; break; + case 'N': bits->N = value; break; + case 'O': bits->O = value; break; + case 'P': bits->P = value; break; + case 'Q': bits->Q = value; break; + case 'R': bits->R = value; break; + case 'S': bits->S = value; break; + } +} + +EXPORT(int) unpack_bitfields(struct BITS *bits, char name) +{ + switch (name) { + case 'A': return bits->A; + case 'B': return bits->B; + case 'C': return bits->C; + case 'D': return bits->D; + case 'E': return bits->E; + case 'F': return bits->F; + case 'G': return bits->G; + case 'H': return bits->H; + case 'I': return bits->I; + + case 'M': return bits->M; + case 'N': return bits->N; + case 'O': return bits->O; + case 'P': return bits->P; + case 'Q': return bits->Q; + case 'R': return bits->R; + case 'S': return bits->S; + } + return 0; +} + +#define S last_tf_arg_s = (LONG_LONG)c +#define U last_tf_arg_u = (unsigned LONG_LONG)c + +EXPORT(signed char) tf_b(signed char c) { S; return c/3; } +EXPORT(unsigned char) tf_B(unsigned char c) { U; return c/3; } +EXPORT(short) tf_h(short c) { S; return c/3; } +EXPORT(unsigned short) tf_H(unsigned short c) { U; return c/3; } +EXPORT(int) tf_i(int c) { S; return c/3; } +EXPORT(unsigned int) tf_I(unsigned int c) { U; return c/3; } +EXPORT(long) tf_l(long c) { S; return c/3; } +EXPORT(unsigned long) tf_L(unsigned long c) { U; return c/3; } +EXPORT(LONG_LONG) tf_q(LONG_LONG c) { S; return c/3; } +EXPORT(unsigned LONG_LONG) tf_Q(unsigned LONG_LONG c) { U; return c/3; } +EXPORT(float) tf_f(float c) { S; return c/3; } +EXPORT(double) tf_d(double c) { S; return c/3; } + +#ifdef MS_WIN32 +EXPORT(signed char) __stdcall s_tf_b(signed char c) { S; return c/3; } +EXPORT(unsigned char) __stdcall s_tf_B(unsigned char c) { U; return c/3; } +EXPORT(short) __stdcall s_tf_h(short c) { S; return c/3; } +EXPORT(unsigned short) __stdcall s_tf_H(unsigned short c) { U; return c/3; } +EXPORT(int) __stdcall s_tf_i(int c) { S; return c/3; } +EXPORT(unsigned int) __stdcall s_tf_I(unsigned int c) { U; return c/3; } +EXPORT(long) __stdcall s_tf_l(long c) { S; return c/3; } +EXPORT(unsigned long) __stdcall s_tf_L(unsigned long c) { U; return c/3; } +EXPORT(LONG_LONG) __stdcall s_tf_q(LONG_LONG c) { S; return c/3; } +EXPORT(unsigned LONG_LONG) __stdcall s_tf_Q(unsigned LONG_LONG c) { U; return c/3; } +EXPORT(float) __stdcall s_tf_f(float c) { S; return c/3; } +EXPORT(double) __stdcall s_tf_d(double c) { S; return c/3; } +#endif +/*******/ + +EXPORT(signed char) tf_bb(signed char x, signed char c) { S; return c/3; } +EXPORT(unsigned char) tf_bB(signed char x, unsigned char c) { U; return c/3; } +EXPORT(short) tf_bh(signed char x, short c) { S; return c/3; } +EXPORT(unsigned short) tf_bH(signed char x, unsigned short c) { U; return c/3; } +EXPORT(int) tf_bi(signed char x, int c) { S; return c/3; } +EXPORT(unsigned int) tf_bI(signed char x, unsigned int c) { U; return c/3; } +EXPORT(long) tf_bl(signed char x, long c) { S; return c/3; } +EXPORT(unsigned long) tf_bL(signed char x, unsigned long c) { U; return c/3; } +EXPORT(LONG_LONG) tf_bq(signed char x, LONG_LONG c) { S; return c/3; } +EXPORT(unsigned LONG_LONG) tf_bQ(signed char x, unsigned LONG_LONG c) { U; return c/3; } +EXPORT(float) tf_bf(signed char x, float c) { S; return c/3; } +EXPORT(double) tf_bd(signed char x, double c) { S; return c/3; } +EXPORT(void) tv_i(int c) { S; return; } + +#ifdef MS_WIN32 +EXPORT(signed char) __stdcall s_tf_bb(signed char x, signed char c) { S; return c/3; } +EXPORT(unsigned char) __stdcall s_tf_bB(signed char x, unsigned char c) { U; return c/3; } +EXPORT(short) __stdcall s_tf_bh(signed char x, short c) { S; return c/3; } +EXPORT(unsigned short) __stdcall s_tf_bH(signed char x, unsigned short c) { U; return c/3; } +EXPORT(int) __stdcall s_tf_bi(signed char x, int c) { S; return c/3; } +EXPORT(unsigned int) __stdcall s_tf_bI(signed char x, unsigned int c) { U; return c/3; } +EXPORT(long) __stdcall s_tf_bl(signed char x, long c) { S; return c/3; } +EXPORT(unsigned long) __stdcall s_tf_bL(signed char x, unsigned long c) { U; return c/3; } +EXPORT(LONG_LONG) __stdcall s_tf_bq(signed char x, LONG_LONG c) { S; return c/3; } +EXPORT(unsigned LONG_LONG) __stdcall s_tf_bQ(signed char x, unsigned LONG_LONG c) { U; return c/3; } +EXPORT(float) __stdcall s_tf_bf(signed char x, float c) { S; return c/3; } +EXPORT(double) __stdcall s_tf_bd(signed char x, double c) { S; return c/3; } +EXPORT(void) __stdcall s_tv_i(int c) { S; return; } +#endif + +/********/ + +#ifndef MS_WIN32 + +typedef struct { + long x; + long y; +} POINT; + +typedef struct { + long left; + long top; + long right; + long bottom; +} RECT; + +#endif + +EXPORT(int) PointInRect(RECT *prc, POINT pt) +{ + if (pt.x < prc->left) + return 0; + if (pt.x > prc->right) + return 0; + if (pt.y < prc->top) + return 0; + if (pt.y > prc->bottom) + return 0; + return 1; +} + +typedef struct { + short x; + short y; +} S2H; + +EXPORT(S2H) ret_2h_func(S2H inp) +{ + inp.x *= 2; + inp.y *= 3; + return inp; +} + +typedef struct { + int a, b, c, d, e, f, g, h; +} S8I; + + + +typedef int (*CALLBACK_RECT)(RECT rect); + +EXPORT(int) call_callback_with_rect(CALLBACK_RECT cb, RECT rect) +{ + return cb(rect); +} + + +EXPORT(S8I) ret_8i_func(S8I inp) +{ + inp.a *= 2; + inp.b *= 3; + inp.c *= 4; + inp.d *= 5; + inp.e *= 6; + inp.f *= 7; + inp.g *= 8; + inp.h *= 9; + return inp; +} + +EXPORT(int) GetRectangle(int flag, RECT *prect) +{ + if (flag == 0) + return 0; + prect->left = (int)flag; + prect->top = (int)flag + 1; + prect->right = (int)flag + 2; + prect->bottom = (int)flag + 3; + return 1; +} + +EXPORT(void) TwoOutArgs(int a, int *pi, int b, int *pj) +{ + *pi += a; + *pj += b; +} + +#ifdef MS_WIN32 +EXPORT(S2H) __stdcall s_ret_2h_func(S2H inp) { return ret_2h_func(inp); } +EXPORT(S8I) __stdcall s_ret_8i_func(S8I inp) { return ret_8i_func(inp); } +#endif + +#ifdef MS_WIN32 +/* Should port this */ +#include +#include + +EXPORT (HRESULT) KeepObject(IUnknown *punk) +{ + static IUnknown *pobj; + if (punk) + punk->lpVtbl->AddRef(punk); + if (pobj) + pobj->lpVtbl->Release(pobj); + pobj = punk; + return S_OK; +} + +#endif + +typedef union { + short x; + long y; +} UN; + +EXPORT(UN) ret_un_func(UN inp) +{ + inp.y = inp.x * 10000; + return inp; +} + +EXPORT(int) my_unused_function(void) +{ + return 42; +} + +EXPORT(int) test_errno(void) +{ + int result = errno; + errno = result + 1; + return result; +} + +EXPORT(int *) test_issue1655(char const *tag, int *len) +{ + static int data[] = { -1, -2, -3, -4 }; + *len = -42; + if (strcmp(tag, "testing!") != 0) + return NULL; + *len = sizeof(data) / sizeof(data[0]); + return data; +} diff --git a/vm/src/stdlib/ctypes/_ctypes_test.h b/vm/src/stdlib/ctypes/_ctypes_test.h new file mode 100644 index 0000000000..9ecba887ef --- /dev/null +++ b/vm/src/stdlib/ctypes/_ctypes_test.h @@ -0,0 +1 @@ +extern int _testfunc_i_bhilfd(char b, short h, int i, long l, float f, double d); \ No newline at end of file diff --git a/vm/src/stdlib/ctypes/array.rs b/vm/src/stdlib/ctypes/array.rs new file mode 100644 index 0000000000..c37f981cfc --- /dev/null +++ b/vm/src/stdlib/ctypes/array.rs @@ -0,0 +1,707 @@ +use super::{ + pointer::PyCPointer, + primitive::{new_simple_type, PyCSimple}, +}; +use crate::builtins::{ + self, + memory::{try_buffer_from_object, Buffer}, + slice::PySlice, + PyBytes, PyInt, PyList, PyRange, PyStr, PyType, PyTypeRef, +}; +use crate::common::lock::{PyRwLock, PyRwLockReadGuard, PyRwLockWriteGuard}; +use crate::function::OptionalArg; +use crate::pyobject::{ + IdProtocol, ItemProtocol, PyIterable, PyObjectRef, PyRef, PyResult, PyValue, StaticType, TryFromObject, + TypeProtocol, +}; +use crate::sliceable::SequenceIndex; +use crate::slots::BufferProtocol; +use crate::stdlib::ctypes::basics::{ + default_from_param, generic_get_buffer, get_size, BorrowValue as BorrowValueCData, + BorrowValueMut, PyCData, PyCDataFunctions, PyCDataMethods, PyCDataSequenceMethods, RawBuffer, +}; +use crate::utils::Either; +use crate::VirtualMachine; +use num_traits::Signed; +use std::convert::TryInto; +use std::fmt; +use widestring::WideCString; + +// TODO: make sure that this is correct wrt windows and unix wstr +fn slice_to_obj(ty: &str, b: &[u8], vm: &VirtualMachine) -> PyResult { + if ty == "u" { + Ok(vm.new_pyobj(if cfg!(windows) { + u16::from_ne_bytes( + b.try_into().map_err(|_| { + vm.new_value_error("buffer does not fit widestring".to_string()) + })?, + ) as u32 + } else { + u32::from_ne_bytes( + b.try_into().map_err(|_| { + vm.new_value_error("buffer does not fit widestring".to_string()) + })?, + ) + })) + } else { + macro_rules! byte_match_type { + ( + $( + $($type: literal)|+ => $body: ident + )+ + ) => { + match ty { + $( + $( + t if t == $type => { + Ok(vm.new_pyobj($body::from_ne_bytes(b.try_into().map_err(|_| vm.new_value_error(format!("buffer does not fit type '{}'",ty)))?))) + } + )+ + )+ + _ => unreachable!() + } + } + } + byte_match_type!( + "c" | "b" => i8 + "h" => i16 + "H" => u16 + "i" => i32 + "I" => u32 + "l" | "q" => i64 + "L" | "Q" => u64 + "f" => f32 + "d" | "g" => f64 + "?" | "B" => u8 + "P" | "z" | "Z" => usize + ) + } +} + +pub fn make_array_with_length( + cls: PyTypeRef, + length: usize, + vm: &VirtualMachine, +) -> PyResult> { + let outer_type = cls.get_attr("_type_").ok_or_else(|| { + vm.new_attribute_error("class must define a '_type_' attribute".to_string()) + })?; + let length = length as usize; + let _type_ = vm + .get_attribute(outer_type.clone(), "_type_") + .map_err(|_| vm.new_type_error("_type_ must have storage info".to_string()))?; + let itemsize = get_size(_type_.downcast::().unwrap().to_string().as_str()); + let capacity = length + .checked_mul(itemsize) + .ok_or_else(|| vm.new_overflow_error("array too large".to_string()))?; + // FIXME change this initialization + Ok(PyCArray { + _type_: new_simple_type(Either::A(&outer_type), vm)?.into_ref(vm), + _length_: length, + _buffer: PyRwLock::new(RawBuffer { + inner: Vec::with_capacity(capacity).as_mut_ptr(), + size: capacity, + }), + } + .into_ref_with_type(vm, cls)?) +} +// TODO: finish implementation +fn set_array_value( + zelf: &PyObjectRef, + dst_buffer: &mut [u8], + idx: usize, + size: usize, + obj: PyObjectRef, + vm: &VirtualMachine, +) -> PyResult<()> { + let self_cls = zelf.clone_class(); + + if !self_cls.issubclass(PyCData::static_type()) { + return Err(vm.new_type_error("not a ctype instance".to_string())); + } + + let obj_cls = obj.clone_class(); + + if !obj_cls.issubclass(PyCData::static_type()) { + // TODO: Fill here + } + + if vm.isinstance(&obj, &self_cls)? { + let o_buffer = try_buffer_from_object(vm, &obj)?; + let src_buffer = o_buffer.obj_bytes(); + + assert!(dst_buffer.len() == size && src_buffer.len() >= size); + + dst_buffer.copy_from_slice(&src_buffer[..size]); + } + + if self_cls.is(PyCPointer::static_type()) && obj_cls.is(PyCArray::static_type()) { + //TODO: Fill here + } else { + return Err(vm.new_type_error(format!( + "incompatible types, {} instance instead of {} instance", + obj_cls.name, self_cls.name + ))); + } + + Ok(()) +} + +fn array_get_slice_params( + slice: PyRef, + length: &Option, + vm: &VirtualMachine, +) -> PyResult<(isize, isize, isize)> { + if let Some(ref len) = length { + let indices = vm.get_method(slice.as_object().clone(), "indices").unwrap().unwrap(); + let tuple = vm.invoke(&indices, (len.clone(),))?; + + let (start, stop, step) = ( + tuple.get_item(0, vm)?, + tuple.get_item(1, vm)?, + tuple.get_item(2, vm)?, + ); + + Ok(( + isize::try_from_object(vm, step)?, + isize::try_from_object(vm, start)?, + isize::try_from_object(vm, stop)?, + )) + } else { + let step = slice.step.as_ref() + .map_or(Ok(1), |o| isize::try_from_object(vm, o.clone()))?; + + let start = slice.start.as_ref() + .map_or_else(|| { + if step > 0 { + Ok(0) + } else { + Err(vm.new_value_error("slice start is required for step < 0".to_string())) + } + }, + |o| isize::try_from_object(vm, o.clone()), + )?; + + let stop = isize::try_from_object(vm, slice.stop.clone())?; + + Ok((step, start, stop)) + } +} + +fn array_slice_getitem<'a>( + zelf: PyObjectRef, + buffer_bytes: &'a [u8], + slice: PyRef, + size: usize, + vm: &'a VirtualMachine, +) -> PyResult { + let length = vm + .get_attribute(zelf.clone(), "_length_") + .map(|c_l| usize::try_from_object(vm, c_l))??; + + let tp = vm.get_attribute(zelf, "_type_")?.downcast::().unwrap().to_string(); + let _type_ = tp.as_str(); + let (step, start, stop) = array_get_slice_params(slice, &Some(vm.ctx.new_int(length)), vm)?; + + let _range = PyRange { + start: PyInt::from(start).into_ref(vm), + stop: PyInt::from(stop).into_ref(vm), + step: PyInt::from(step).into_ref(vm), + }; + + let mut obj_vec = Vec::new(); + let mut offset; + + for curr in PyIterable::try_from_object(vm,_range.into_object(vm))?.iter(vm)? { + let idx = fix_index(isize::try_from_object(vm, curr?)?, length, vm)? as usize; + offset = idx * size; + + obj_vec.push(slice_to_obj( + _type_, + buffer_bytes[offset..offset + size].as_ref(), + vm, + )?); + } + + Ok(vm.new_pyobj(PyList::from(obj_vec))) +} + +fn array_slice_setitem( + zelf: PyObjectRef, + slice: PyRef, + buffer_bytes: &mut [u8], + obj: PyObjectRef, + length: Option, + vm: &VirtualMachine, +) -> PyResult<()> { + let (step, start, stop) = array_get_slice_params(slice, &length, vm)?; + + let slice_length = if (step < 0 && stop >= start) || (step > 0 && start >= stop) { + 0 + } else if step < 0 { + (stop - start + 1) / step + 1 + } else { + (stop - start - 1) / step + 1 + }; + + if slice_length != vm.obj_len(&obj)? as isize { + return Err(vm.new_value_error("Can only assign sequence of same size".to_string())); + } + + let _range = PyRange { + start: PyInt::from(start).into_ref(vm), + stop: PyInt::from(stop).into_ref(vm), + step: PyInt::from(step).into_ref(vm), + }; + + //FIXME: this function should be called for pointer too (length should be None), + //thus, needs to make sure the size. + //Right now I'm setting one + let size = length.map_or(Ok(1), |v| usize::try_from_object(vm, v))?; + + for (i, curr) in PyIterable::try_from_object(vm,_range.into_object(vm))?.iter(vm)?.enumerate() { + let idx = fix_index(isize::try_from_object(vm, curr?)?, size, vm)? as usize; + let offset = idx * size; + let item = obj.get_item(i, vm)?; + let buffer_slice = &mut buffer_bytes[offset..offset + size]; + + set_array_value(&zelf, buffer_slice, idx, size, item, vm)?; + } + + Ok(()) +} + +#[inline(always)] +fn fix_index(index: isize, length: usize, vm: &VirtualMachine) -> PyResult { + let index = if index < 0 { + index + length as isize + } else { + index + }; + + if 0 <= index && index <= length as isize { + Ok(index) + } else { + Err(vm.new_index_error("invalid index".to_string())) + } +} + +#[pyclass(module = "_ctypes", name = "PyCArrayType", base = "PyType")] +pub struct PyCArrayMeta {} + +#[pyclass( + module = "_ctypes", + name = "Array", + base = "PyCData", + metaclass = "PyCArrayMeta" +)] +pub struct PyCArray { + _type_: PyRef, + _length_: usize, + _buffer: PyRwLock, +} + +impl fmt::Debug for PyCArrayMeta { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "PyCArrayMeta",) + } +} + +impl fmt::Debug for PyCArray { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "PyCArray {{ {} {} }}", + self._type_._type_.as_str(), + self._length_ + ) + } +} + +impl PyValue for PyCArrayMeta { + fn class(_vm: &VirtualMachine) -> &PyTypeRef { + Self::static_type() + } +} + +impl PyValue for PyCArray { + fn class(_vm: &VirtualMachine) -> &PyTypeRef { + Self::static_type() + } +} + +impl<'a> BorrowValueCData<'a> for PyCArray { + fn borrow_value(&'a self) -> PyRwLockReadGuard<'a, RawBuffer> { + self._buffer.read() + } +} + +impl<'a> BorrowValueMut<'a> for PyCArray { + fn borrow_value_mut(&'a self) -> PyRwLockWriteGuard<'a, RawBuffer> { + self._buffer.write() + } +} + +impl BufferProtocol for PyCArray { + fn get_buffer(zelf: &PyRef, vm: &VirtualMachine) -> PyResult> { + generic_get_buffer::(zelf, vm) + } +} + +impl PyCDataMethods for PyCArrayMeta { + fn from_param( + zelf: PyRef, + value: PyObjectRef, + vm: &VirtualMachine, + ) -> PyResult { + let mut value = value; + let cls = zelf.clone_class(); + + if vm.isinstance(&value, &cls)? { + return Ok(value); + } + + let length = vm + .get_attribute(zelf.as_object().clone(), "_length_") + .map(|c_l| usize::try_from_object(vm, c_l))??; + + let value_len = vm.obj_len(&value)?; + + if let Ok(tp) = vm.get_attribute(zelf.as_object().clone(), "_type_") { + let _type = tp.downcast::().unwrap(); + + if _type._type_.as_str() == "c" { + if vm.isinstance(&value, &vm.ctx.types.bytes_type).is_ok() { + if value_len > length { + return Err(vm.new_value_error("Invalid length".to_string())); + } + value = make_array_with_length(cls.clone(), length, vm)?.as_object().clone(); + } else if vm.isinstance(&value, &cls).is_err() { + return Err( + vm.new_type_error(format!("expected bytes, {} found", value.class().name)) + ); + } + } else if _type._type_.as_str() == "u" { + if vm.isinstance(&value, &vm.ctx.types.str_type).is_ok() { + if value_len > length { + return Err(vm.new_value_error("Invalid length".to_string())); + } + value = make_array_with_length(cls.clone(), length, vm)?.as_object().clone(); + } else if vm.isinstance(&value, &cls).is_err() { + return Err(vm.new_type_error(format!( + "expected unicode string, {} found", + value.class().name + ))); + } + } + } + + if vm.isinstance(&value, &vm.ctx.types.tuple_type).is_ok() { + if value_len > length { + return Err(vm.new_runtime_error("Invalid length".to_string())); + } + value = make_array_with_length(cls, length, vm)?.as_object().clone(); + } + + default_from_param(zelf, value, vm) + } +} + +#[pyimpl(with(PyCDataMethods), flags(BASETYPE))] +impl PyCArrayMeta { + #[pyslot] + fn tp_new(cls: PyTypeRef, vm: &VirtualMachine) -> PyResult { + let length_obj = vm + .get_attribute(cls.as_object().to_owned(), "_length_") + .map_err(|_| { + vm.new_attribute_error("class must define a '_length_' attribute".to_string()) + })?; + let length_int = length_obj.downcast_exact::(vm).map_err(|_| { + vm.new_type_error("The '_length_' attribute must be an integer".to_string()) + })?; + let length: usize = if length_int.as_bigint().is_negative() { + Err(vm.new_value_error("The '_length_' attribute must not be negative".to_string())) + } else { + Ok( + builtins::int::try_to_primitive(length_int.as_bigint(), vm).map_err(|_| { + vm.new_overflow_error("The '_length_' attribute is too large".to_owned()) + })?, + ) + }?; + + Ok(make_array_with_length(cls, length, vm)?.as_object().clone()) + } +} + +#[pyimpl(flags(BASETYPE), with(BufferProtocol, PyCDataFunctions))] +impl PyCArray { + #[pymethod(magic)] + pub fn init(zelf: PyRef, value: OptionalArg, vm: &VirtualMachine) -> PyResult<()> { + value.map_or(Ok(()), |value| { + let value_length = vm.obj_len(&value)?; + + if value_length < zelf._length_ { + let value_vec: Vec = vm.extract_elements(&value)?; + for (i, v) in value_vec.iter().enumerate() { + Self::setitem(zelf.clone(), SequenceIndex::Int(i as isize), v.clone(), vm)? + } + Ok(()) + } else if value_length == zelf._length_ { + let py_slice = SequenceIndex::Slice( + PySlice { + start: Some(vm.new_pyobj(0)), + stop: vm.new_pyobj(zelf._length_), + step: None, + } + .into_ref(vm), + ); + + Self::setitem(zelf, py_slice, value, vm) + } else { + Err(vm.new_value_error("value has size greater than the array".to_string())) + } + }) + } + + #[pyproperty] + pub fn value(zelf: PyRef, vm: &VirtualMachine) -> PyResult { + // TODO: make sure that this is correct + let obj = zelf.as_object(); + let buffer = try_buffer_from_object(vm, obj)?; + + let res = if zelf._type_._type_ == "u" { + vm.new_pyobj( + unsafe { + if cfg!(windows) { + WideCString::from_vec_with_nul_unchecked( + buffer + .obj_bytes() + .chunks_exact(2) + .map(|c| { + let chunk: [u8; 2] = c.try_into().unwrap(); + u16::from_ne_bytes(chunk) as u32 + }) + .collect::>(), + ) + } else { + WideCString::from_vec_with_nul_unchecked( + buffer + .obj_bytes() + .chunks(4) + .map(|c| { + let chunk: [u8; 4] = c.try_into().unwrap(); + u32::from_ne_bytes(chunk) + }) + .collect::>(), + ) + } + } + .to_string() + .map_err(|e| vm.new_runtime_error(e.to_string()))?, + ) + } else { + // self._type_ == "c" + let bytes = buffer.obj_bytes(); + + let bytes_inner = if let Some((last, elements)) = bytes.split_last() { + if *last == 0 { + elements.to_vec() + } else { + bytes.to_vec() + } + } else { + Vec::new() + }; + + PyBytes::from(bytes_inner).into_object(vm) + }; + + Ok(res) + } + + #[pyproperty(setter)] + fn set_value(zelf: PyRef, value: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> { + let obj = zelf.as_object(); + let buffer = try_buffer_from_object(vm, obj)?; + let my_size = buffer.get_options().len; + let mut bytes = buffer.obj_bytes_mut(); + + if zelf._type_._type_ == "c" { + // bytes + let value = value.downcast_exact::(vm).map_err(|value| { + vm.new_value_error(format!( + "bytes expected instead of {} instance", + value.class().name + )) + })?; + let wide_bytes = value.to_vec(); + + if wide_bytes.len() > my_size { + return Err(vm.new_value_error("byte string too long".to_string())); + } + + bytes[..wide_bytes.len()].copy_from_slice(wide_bytes.as_slice()); + if wide_bytes.len() < my_size { + bytes[my_size] = 0; + } + } else { + // TODO: make sure that this is correct + // unicode string zelf._type_ == "u" + let value = value.downcast_exact::(vm).map_err(|value| { + vm.new_value_error(format!( + "unicode string expected instead of {} instance", + value.class().name + )) + })?; + let wide_str = unsafe { WideCString::from_str_with_nul_unchecked(value.to_string()) }; + + let wide_str_len = wide_str.len(); + + if wide_str.len() > my_size { + return Err(vm.new_value_error("string too long".to_string())); + } + let res = if cfg!(windows) { + wide_str + .into_vec() + .iter_mut() + .map(|i| u16::to_ne_bytes(*i as u16).to_vec()) + .flatten() + .collect::>() + } else { + wide_str + .into_vec() + .iter_mut() + .map(|i| u32::to_ne_bytes(*i).to_vec()) + .flatten() + .collect::>() + }; + + bytes[..wide_str_len].copy_from_slice(res.as_slice()); + } + + Ok(()) + } + + #[pyproperty] + fn raw(zelf: PyRef, vm: &VirtualMachine) -> PyResult { + // zelf._type_ == "c" + + let obj = zelf.as_object(); + let buffer = try_buffer_from_object(vm, obj)?; + let buffer_vec = buffer.obj_bytes().to_vec(); + + Ok(PyBytes::from(buffer_vec)) + } + + #[pyproperty(setter)] + fn set_raw(zelf: PyRef, value: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> { + let obj = zelf.as_object(); + let my_buffer = try_buffer_from_object(vm, obj)?; + let my_size = my_buffer.get_options().len; + + let new_value = try_buffer_from_object(vm, &value)?; + let new_size = new_value.get_options().len; + + // byte string zelf._type_ == "c" + if new_size > my_size { + Err(vm.new_value_error("byte string too long".to_string())) + } else { + let mut borrowed_buffer = my_buffer.obj_bytes_mut(); + let src = new_value.obj_bytes(); + borrowed_buffer[..new_size].copy_from_slice(&src); + Ok(()) + } + } + + #[pymethod(magic)] + fn len(&self) -> usize { + self._length_ + } + + #[pymethod(magic)] + fn getitem(zelf: PyRef, k_or_idx: SequenceIndex, vm: &VirtualMachine) -> PyResult { + let buffer = try_buffer_from_object(vm, zelf.as_object())?; + let buffer_size = buffer.get_options().len; + let buffer_bytes = buffer.obj_bytes(); + let size = buffer_size / zelf._length_; + + match k_or_idx { + SequenceIndex::Int(idx) => { + let idx = fix_index(idx, zelf._length_, vm)? as usize; + let offset = idx * size; + let buffer_slice = buffer_bytes[offset..offset + size].as_ref(); + slice_to_obj(zelf._type_._type_.as_str(), buffer_slice, vm) + } + SequenceIndex::Slice(slice) => array_slice_getitem( + zelf.as_object().clone(), + &buffer_bytes[..], + slice, + size, + vm, + ), + } + } + + #[pymethod(magic)] + fn setitem( + zelf: PyRef, + k_or_idx: SequenceIndex, + obj: PyObjectRef, + vm: &VirtualMachine, + ) -> PyResult<()> { + let buffer = try_buffer_from_object(vm, zelf.as_object())?; + let buffer_size = buffer.get_options().len; + let mut buffer_bytes = buffer.obj_bytes_mut(); + + let size = buffer_size / zelf._length_; + + match k_or_idx { + SequenceIndex::Int(idx) => { + let idx = fix_index(idx, zelf._length_, vm)? as usize; + let offset = idx * size; + let buffer_slice = &mut buffer_bytes[offset..offset + size]; + set_array_value(&zelf.as_object().clone(), buffer_slice, idx, size, obj, vm) + } + SequenceIndex::Slice(slice) => array_slice_setitem( + zelf.as_object().clone(), + slice, + &mut buffer_bytes[..], + obj, + Some(vm.ctx.new_int(zelf._length_)), + vm, + ), + } + } +} + +impl PyCDataFunctions for PyCArray { + fn size_of_instances(zelf: PyRef, vm: &VirtualMachine) -> PyResult { + Ok(zelf._length_ * PyCDataFunctions::size_of_instances(zelf._type_.clone(), vm)?) + } + + fn alignment_of_instances(zelf: PyRef, vm: &VirtualMachine) -> PyResult { + PyCDataFunctions::alignment_of_instances(zelf._type_.clone(), vm) + } + + fn ref_to(zelf: PyRef, offset: OptionalArg, vm: &VirtualMachine) -> PyResult { + let offset = offset + .into_option() + .map_or(Ok(0), |o| usize::try_from_object(vm, o))?; + + if offset > zelf._length_ * get_size(zelf._type_._type_.as_str()) { + Err(vm.new_index_error("offset out of bounds".to_string())) + } else { + let guard = zelf.borrow_value(); + let ref_at: *mut u8 = unsafe { guard.inner.add(offset) }; + + Ok(vm.new_pyobj(ref_at as *mut _ as *mut usize as usize)) + } + } + + fn address_of(zelf: PyRef, vm: &VirtualMachine) -> PyResult { + Ok(vm + .new_pyobj(unsafe { &*zelf.borrow_value().inner } as *const _ as *const usize as usize)) + } +} + +impl PyCDataSequenceMethods for PyCArrayMeta {} diff --git a/vm/src/stdlib/ctypes/basics.rs b/vm/src/stdlib/ctypes/basics.rs new file mode 100644 index 0000000000..8759ce23e0 --- /dev/null +++ b/vm/src/stdlib/ctypes/basics.rs @@ -0,0 +1,488 @@ +use std::{fmt, mem, os::raw::*, ptr, slice}; + +use widestring::WideChar; + +use crate::builtins::int::PyInt; +use crate::builtins::memory::{try_buffer_from_object, Buffer, BufferOptions}; +use crate::builtins::pystr::PyStrRef; +use crate::builtins::pytype::PyTypeRef; +use crate::common::borrow::{BorrowedValue, BorrowedValueMut}; +use crate::common::lock::{PyRwLock, PyRwLockReadGuard, PyRwLockWriteGuard}; +use crate::function::OptionalArg; +use crate::pyobject::{ + PyObjectRef, PyRef, PyResult, PyValue, StaticType, TryFromObject, TypeProtocol, +}; +use crate::slots::BufferProtocol; +use crate::utils::Either; +use crate::VirtualMachine; + +use crate::stdlib::ctypes::array::make_array_with_length; +use crate::stdlib::ctypes::dll::dlsym; +use crate::stdlib::ctypes::primitive::{new_simple_type, PyCSimple}; + +use crossbeam_utils::atomic::AtomicCell; + +pub fn get_size(ty: &str) -> usize { + match ty { + "u" => mem::size_of::(), + "c" | "b" => mem::size_of::(), + "h" => mem::size_of::(), + "H" => mem::size_of::(), + "i" => mem::size_of::(), + "I" => mem::size_of::(), + "l" => mem::size_of::(), + "q" => mem::size_of::(), + "L" => mem::size_of::(), + "Q" => mem::size_of::(), + "f" => mem::size_of::(), + "d" | "g" => mem::size_of::(), + "?" | "B" => mem::size_of::(), + "P" | "z" | "Z" => mem::size_of::(), + _ => unreachable!(), + } +} + +fn at_address(cls: &PyTypeRef, buf: usize, vm: &VirtualMachine) -> PyResult { + match vm.get_attribute(cls.as_object().to_owned(), "__abstract__") { + Ok(attr) => match bool::try_from_object(vm, attr) { + Ok(false) => { + let len = vm + .get_attribute(cls.as_object().to_owned(), "_length_") + .map_or(Ok(1), |o: PyObjectRef| match i64::try_from_object(vm, o) { + Ok(v_int) => { + if v_int < 0 { + Err(vm.new_type_error("'_length_' must positive".to_string())) + } else { + Ok(v_int as usize) + } + } + _ => Err(vm.new_type_error("'_length_' must be an integer".to_string())), + })?; + + Ok(RawBuffer { + inner: buf as *const u8 as *mut _, + size: len, + }) + } + Ok(_) => Err(vm.new_type_error("abstract class".to_string())), + // FIXME: A sanity check + Err(_) => Err(vm.new_type_error("attribute '__abstract__' must be bool".to_string())), + }, + // FIXME: I think it's unreachable + Err(_) => Err(vm.new_attribute_error("abstract class".to_string())), + } +} + +// FIXME: rework this function +fn buffer_copy( + cls: PyTypeRef, + obj: PyObjectRef, + offset: OptionalArg, + vm: &VirtualMachine, + copy: bool, +) -> PyResult { + match vm.get_attribute(cls.as_object().to_owned(), "__abstract__") { + Ok(attr) => { + match bool::try_from_object(vm, attr) { + Ok(b) if !b => { + let buffer = try_buffer_from_object(vm, &obj)?; + let opts = buffer.get_options().clone(); + + // TODO: Fix the way the size of stored + // Would this be the a proper replacement? + // vm.call_method(cls.as_object().to_owned(), "size", ())?. + let cls_size = vm + .get_attribute(cls.as_object().to_owned(), "_size") + .map(|c_s| usize::try_from_object(vm, c_s))??; + + let offset_int = offset + .into_option() + .map_or(Ok(0), |off| i64::try_from_object(vm, off))?; + + if opts.readonly { + Err(vm.new_type_error("underlying buffer is not writable".to_string())) + } else if !opts.contiguous { + Err(vm.new_type_error("underlying buffer is not C contiguous".to_string())) + } else if offset_int < 0 { + Err(vm.new_value_error("offset cannot be negative".to_string())) + } else if cls_size > opts.len - (offset_int as usize) { + Err(vm.new_value_error(format!( + "Buffer size too small ({} instead of at least {} bytes)", + cls_size, + opts.len + (offset_int as usize) + ))) + } else if let Some(mut buffer) = buffer.as_contiguous_mut() { + // FIXME: Perform copy + let buffered = if copy { + unsafe { slice::from_raw_parts_mut(buffer.as_mut_ptr(), buffer.len()) } + .as_mut_ptr() + } else { + buffer.as_mut_ptr() + }; + + Ok(PyCData::new( + None, + Some(RawBuffer { + inner: buffered, + size: buffer.len(), + }), + )) + } else { + Err(vm.new_buffer_error("empty buffer".to_string())) + } + } + Ok(_) => Err(vm.new_type_error("abstract class".to_string())), + Err(_) => { + // TODO: A sanity check + Err(vm.new_type_error("attribute '__abstract__' must be bool".to_string())) + } + } + } + // TODO: I think this is unreachable... + Err(_) => Err(vm.new_type_error("abstract class".to_string())), + } +} + +pub fn default_from_param(zelf: PyRef, value: PyObjectRef, vm: &VirtualMachine) -> PyResult +where + T: PyCDataMethods + PyValue, +{ + //TODO: check if this behaves like it should + let cls = zelf.as_object().clone_class(); + if vm.isinstance(&value, &cls)? { + Ok(value) + } else if let Ok(parameter) = vm.get_attribute(value.clone(), "_as_parameter_") { + T::from_param(zelf, parameter, vm) + } else { + Err(vm.new_attribute_error(format!( + "expected {} instance instead of {}", + cls.name, + value.class().name + ))) + } +} +#[pyimpl] +pub trait PyCDataFunctions: PyValue { + #[pymethod] + fn size_of_instances(zelf: PyRef, vm: &VirtualMachine) -> PyResult; + + #[pymethod] + fn alignment_of_instances(zelf: PyRef, vm: &VirtualMachine) -> PyResult; + + #[pymethod] + fn ref_to(zelf: PyRef, offset: OptionalArg, vm: &VirtualMachine) -> PyResult; + + #[pymethod] + fn address_of(zelf: PyRef, vm: &VirtualMachine) -> PyResult; +} +#[pyimpl] +pub trait PyCDataMethods: PyValue { + // A lot of the logic goes in this trait + // There's also other traits that should have different implementations for some functions + // present here + + // The default methods (representing CDataType_methods) here are for: + // StructType_Type + // UnionType_Type + // PyCArrayType_Type + // PyCFuncPtrType_Type + + #[pymethod] + fn from_param(zelf: PyRef, value: PyObjectRef, vm: &VirtualMachine) -> PyResult; + + #[pyclassmethod] + fn from_address( + cls: PyTypeRef, + address: PyObjectRef, + vm: &VirtualMachine, + ) -> PyResult { + if let Ok(obj) = address.downcast_exact::(vm) { + if let Ok(v) = usize::try_from_object(vm, obj.into_object()) { + let buffer = at_address(&cls, v, vm)?; + Ok(PyCData::new(None, Some(buffer))) + } else { + Err(vm.new_runtime_error("casting pointer failed".to_string())) + } + } else { + Err(vm.new_type_error("integer expected".to_string())) + } + } + + #[pyclassmethod] + fn from_buffer( + cls: PyTypeRef, + obj: PyObjectRef, + offset: OptionalArg, + vm: &VirtualMachine, + ) -> PyResult { + buffer_copy(cls, obj, offset, vm, false) + } + + #[pyclassmethod] + fn from_buffer_copy( + cls: PyTypeRef, + obj: PyObjectRef, + offset: OptionalArg, + vm: &VirtualMachine, + ) -> PyResult { + buffer_copy(cls, obj, offset, vm, true) + } + + #[pyclassmethod] + fn in_dll( + cls: PyTypeRef, + dll: PyObjectRef, + name: PyStrRef, + vm: &VirtualMachine, + ) -> PyResult { + if let Ok(h) = vm.get_attribute(cls.as_object().to_owned(), "_handle") { + // This is something to be "CPython" like + let raw_ptr = if let Ok(h_int) = h.downcast_exact::(vm) { + dlsym(h_int, name, vm) + } else { + Err(vm.new_type_error(format!("_handle must be an int not {}", dll.class().name))) + }?; + + let sym_ptr = usize::try_from_object(vm, raw_ptr)?; + + let buffer = at_address(&cls, sym_ptr, vm)?; + Ok(PyCData::new(None, Some(buffer))) + } else { + Err(vm.new_attribute_error("atribute '_handle' not found".to_string())) + } + } +} + +#[pyimpl] +pub trait PyCDataSequenceMethods: PyValue { + // CDataType_as_sequence methods are default for all *Type_Type + // Basically the sq_repeat slot is CDataType_repeat + // which transforms into a Array + + #[pymethod(name = "__mul__")] + #[pymethod(name = "__rmul__")] + fn mul(zelf: PyRef, length: isize, vm: &VirtualMachine) -> PyResult { + if length < 0 { + Err(vm.new_value_error(format!("Array length must be >= 0, not {} length", length))) + } else { + Ok( + make_array_with_length(zelf.clone_class(), length as usize, vm)? + .as_object() + .clone(), + ) + } + } +} + +pub fn generic_get_buffer(zelf: &PyRef, vm: &VirtualMachine) -> PyResult> +where + for<'a> T: PyValue + fmt::Debug + BorrowValue<'a> + BorrowValueMut<'a>, +{ + if let Ok(buffer) = vm.get_attribute(zelf.as_object().clone(), "_buffer") { + if let Ok(_buffer) = buffer.downcast_exact::(vm) { + Ok(Box::new(PyCBuffer:: { + data: zelf.clone(), + options: BufferOptions { + readonly: false, + len: _buffer.size, + ..Default::default() + }, + })) + } else { + Err(vm.new_attribute_error("_buffer attribute should be RawBuffer".to_string())) + } + } else { + Err(vm.new_attribute_error("_buffer not found".to_string())) + } +} + +pub trait BorrowValueMut<'a> { + fn borrow_value_mut(&'a self) -> PyRwLockWriteGuard<'a, RawBuffer>; +} + +pub trait BorrowValue<'a> { + fn borrow_value(&'a self) -> PyRwLockReadGuard<'a, RawBuffer>; +} + +impl<'a> BorrowValue<'a> for PyCData { + fn borrow_value(&'a self) -> PyRwLockReadGuard<'a, RawBuffer> { + self._buffer.read() + } +} + +impl<'a> BorrowValueMut<'a> for PyCData { + fn borrow_value_mut(&'a self) -> PyRwLockWriteGuard<'a, RawBuffer> { + self._buffer.write() + } +} + +impl BufferProtocol for PyCData { + fn get_buffer(zelf: &PyRef, vm: &VirtualMachine) -> PyResult> { + generic_get_buffer::(zelf, vm) + } +} + +// This trait will be used by all types +impl Buffer for PyCBuffer +where + for<'a> T: PyValue + fmt::Debug + BorrowValue<'a> + BorrowValueMut<'a>, +{ + fn obj_bytes(&self) -> BorrowedValue<[u8]> { + PyRwLockReadGuard::map(self.data.borrow_value(), |x| unsafe { + slice::from_raw_parts(x.inner, x.size) + }) + .into() + } + + fn obj_bytes_mut(&self) -> BorrowedValueMut<[u8]> { + PyRwLockWriteGuard::map(self.data.borrow_value_mut(), |x| unsafe { + slice::from_raw_parts_mut(x.inner, x.size) + }) + .into() + } + + fn release(&self) {} + + fn get_options(&self) -> &BufferOptions { + &self.options + } +} + +#[derive(Debug)] +pub struct PyCBuffer +where + for<'a> T: PyValue + fmt::Debug + BorrowValue<'a> + BorrowValueMut<'a>, +{ + pub data: PyRef, + pub options: BufferOptions, +} + +// FIXME: Change this implementation +pub struct RawBuffer { + pub inner: *mut u8, + pub size: usize, +} + +impl fmt::Debug for RawBuffer { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "RawBuffer {{ size: {} }}", self.size) + } +} + +impl PyValue for RawBuffer { + fn class(vm: &VirtualMachine) -> &PyTypeRef { + &vm.ctx.types.object_type + } +} + +unsafe impl Send for RawBuffer {} +unsafe impl Sync for RawBuffer {} + +// This Trait is the equivalent of PyCData_Type on tp_base for +// Struct_Type, Union_Type, PyCPointer_Type +// PyCArray_Type, PyCSimple_Type, PyCFuncPtr_Type +#[pyclass(module = "_ctypes", name = "_CData")] +pub struct PyCData { + _objects: AtomicCell>, + _buffer: PyRwLock, +} + +impl fmt::Debug for PyCData { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "PyCData {{ _objects: {{}}, _buffer: {{}}}}",) + } +} + +impl PyValue for PyCData { + fn class(_vm: &VirtualMachine) -> &PyTypeRef { + Self::static_type() + } +} + +impl PyCData { + pub fn new(objs: Option>, buffer: Option) -> Self { + PyCData { + _objects: AtomicCell::new(objs.unwrap_or_default()), + _buffer: PyRwLock::new(buffer.unwrap_or(RawBuffer { + inner: ptr::null::() as *mut _, + size: 0, + })), + } + } +} + +#[pyimpl(flags(BASETYPE), with(BufferProtocol))] +impl PyCData { + // PyCData_methods + #[pymethod(magic)] + pub fn ctypes_from_outparam(zelf: PyRef) {} + + #[pymethod(magic)] + pub fn reduce(zelf: PyRef) {} + + #[pymethod(magic)] + pub fn setstate(zelf: PyRef) {} +} + +// FIXME: this function is too hacky, work a better way of doing it +pub fn sizeof_func(tp: Either, vm: &VirtualMachine) -> PyResult { + match tp { + Either::A(type_) if type_.issubclass(PyCSimple::static_type()) => { + let zelf = new_simple_type(Either::B(&type_), vm)?; + PyCDataFunctions::size_of_instances(zelf.into_ref(vm), vm) + } + Either::B(obj) if obj.has_class_attr("size_of_instances") => { + let size_of_method = vm.get_attribute(obj, "size_of_instances").unwrap(); + let size_of_return = vm.invoke(&size_of_method, ())?; + Ok(usize::try_from_object(vm, size_of_return)?) + } + _ => Err(vm.new_type_error("this type has no size".to_string())), + } +} + +// FIXME: this function is too hacky, work a better way of doing it +pub fn alignment(tp: Either, vm: &VirtualMachine) -> PyResult { + match tp { + Either::A(type_) if type_.issubclass(PyCSimple::static_type()) => { + let zelf = new_simple_type(Either::B(&type_), vm)?; + PyCDataFunctions::alignment_of_instances(zelf.into_ref(vm), vm) + } + Either::B(obj) if obj.has_class_attr("alignment_of_instances") => { + let alignment_of_m = vm.get_attribute(obj, "alignment_of_instances")?; + let alignment_of_r = vm.invoke(&alignment_of_m, ())?; + usize::try_from_object(vm, alignment_of_m) + } + _ => Err(vm.new_type_error("no alignment info".to_string())), + } +} + +pub fn byref(tp: PyObjectRef, vm: &VirtualMachine) -> PyResult { + //TODO: Return a Pointer when Pointer implementation is ready + let class = tp.clone_class(); + + if class.issubclass(PyCData::static_type()) { + if let Some(ref_to) = vm.get_method(tp, "ref_to") { + return vm.invoke(&ref_to?, ()); + } + }; + + Err(vm.new_type_error(format!( + "byref() argument must be a ctypes instance, not '{}'", + class.name + ))) +} + +pub fn addressof(tp: PyObjectRef, vm: &VirtualMachine) -> PyResult { + let class = tp.clone_class(); + + if class.issubclass(PyCData::static_type()) { + if let Some(address_of) = vm.get_method(tp, "address_of") { + return vm.invoke(&address_of?, ()); + } + }; + + Err(vm.new_type_error(format!( + "addressof() argument must be a ctypes instance, not '{}'", + class.name + ))) +} diff --git a/vm/src/stdlib/ctypes/dll.rs b/vm/src/stdlib/ctypes/dll.rs new file mode 100644 index 0000000000..bd32d2fa6a --- /dev/null +++ b/vm/src/stdlib/ctypes/dll.rs @@ -0,0 +1,53 @@ +pub(crate) use _ctypes::*; + +#[pymodule] +pub(crate) mod _ctypes { + use crate::builtins::pystr::PyStrRef; + use crate::builtins::PyIntRef; + use crate::pyobject::{PyResult, TryFromObject}; + use crate::VirtualMachine; + + use super::super::shared_lib::libcache; + + #[pyfunction] + pub fn dlopen(lib_path: PyStrRef, vm: &VirtualMachine) -> PyResult { + let mut data_cache = libcache().write(); + + let result = data_cache.get_or_insert_lib(lib_path.as_ref(), vm); + + match result { + Ok(lib) => Ok(vm.new_pyobj(lib.get_pointer())), + Err(_) => Err(vm.new_os_error(format!( + "{} : cannot open shared object file: No such file or directory", + lib_path.to_string() + ))), + } + } + + #[pyfunction] + pub fn dlsym(slib: PyIntRef, str_ref: PyStrRef, vm: &VirtualMachine) -> PyResult { + let func_name = str_ref.as_ref(); + let data_cache = libcache().read(); + + match data_cache.get_lib(usize::try_from_object(vm, slib.as_object().clone())?) { + Some(l) => match l.get_sym(func_name) { + Ok(ptr) => Ok(vm.new_pyobj(ptr as *const _ as usize)), + Err(e) => Err(vm.new_runtime_error(e)), + }, + _ => Err(vm.new_runtime_error("not a valid pointer to a shared library".to_string())), + } + } + + #[pyfunction] + pub fn dlclose(slib: PyIntRef, vm: &VirtualMachine) -> PyResult<()> { + let data_cache = libcache().read(); + + match data_cache.get_lib(usize::try_from_object(vm, slib.as_object().clone())?) { + Some(l) => { + l.close(); + Ok(()) + } + _ => Err(vm.new_runtime_error("not a valid pointer to a shared library".to_string())), + } + } +} diff --git a/vm/src/stdlib/ctypes/function.rs b/vm/src/stdlib/ctypes/function.rs new file mode 100644 index 0000000000..d833413c55 --- /dev/null +++ b/vm/src/stdlib/ctypes/function.rs @@ -0,0 +1,440 @@ +use std::{fmt, os::raw::*}; + +use crossbeam_utils::atomic::AtomicCell; + +use libffi::middle::{arg, Arg, Cif, CodePtr, Type}; + +use crate::builtins::pystr::PyStrRef; +use crate::builtins::{PyInt, PyTypeRef}; +use crate::common::lock::PyRwLock; + +use crate::function::FuncArgs; +use crate::pyobject::{ + PyObjectRef, PyRef, PyResult, PyValue, StaticType, TryFromObject, TypeProtocol, +}; +use crate::VirtualMachine; + +use crate::stdlib::ctypes::basics::PyCData; +use crate::stdlib::ctypes::primitive::PyCSimple; + +use crate::slots::Callable; +use crate::stdlib::ctypes::dll::dlsym; + +macro_rules! ffi_type { + ($name: ident) => { + Type::$name() + }; +} + +macro_rules! match_ffi_type { + ( + $pointer: expr, + + $( + $($type: ident)|+ => $body: expr + )+ + ) => { + match $pointer.as_raw_ptr() { + $( + $( + t if t == ffi_type!($type).as_raw_ptr() => { $body } + )+ + )+ + _ => unreachable!() + } + }; + ( + $kind: expr, + + $( + $($type: literal)|+ => $body: ident + )+ + ) => { + match $kind { + $( + $( + t if t == $type => { ffi_type!($body) } + )? + )+ + _ => unreachable!() + } + } +} + +fn str_to_type(ty: &str) -> Type { + if ty == "u" { + if cfg!(windows) { + ffi_type!(c_ushort) + } else { + ffi_type!(c_uint) + } + } else { + match_ffi_type!( + ty, + "c" => c_schar + "b" => i8 + "h" => c_short + "H" => c_ushort + "i" => c_int + "I" => c_uint + "l" => c_long + "q" => c_longlong + "L" => c_ulong + "Q" => c_ulonglong + "f" => f32 + "d" => f64 + "g" => longdouble + "?" | "B" => c_uchar + "P" | "z" | "Z" => pointer + ) + } +} + +fn py_to_ffi(ty: &Type, obj: PyObjectRef, vm: &VirtualMachine) -> PyResult { + let res = match_ffi_type!( + ty , + c_schar => { + arg(&i8::try_from_object(vm, obj)?) + } + c_int => { + arg(&i32::try_from_object(vm, obj)?) + } + c_short => { + arg(&i16::try_from_object(vm, obj)?) + } + c_ushort => { + arg(&u16::try_from_object(vm, obj)?) + } + c_uint => { + arg(&u32::try_from_object(vm, obj)?) + } + c_long | c_longlong => { + arg(&i64::try_from_object(vm, obj)?) + } + c_ulong | c_ulonglong => { + arg(&u64::try_from_object(vm, obj)?) + } + f32 => { + arg(&f32::try_from_object(vm, obj)?) + } + f64 | longdouble=> { + arg(&f64::try_from_object(vm, obj)?) + } + c_uchar => { + arg(&u8::try_from_object(vm, obj)?) + } + pointer => { + arg(&(usize::try_from_object(vm, obj)? as *mut usize as *mut c_void)) + } + // void should not be here, once an argument cannot be pure void + ); + + Ok(res) +} + +#[derive(Debug)] +struct Function { + pointer: *mut c_void, + arguments: Vec, + return_type: Box, +} + +impl Function { + pub fn new(fn_ptr: usize, arguments: Vec, return_type: &str) -> Function { + Function { + pointer: fn_ptr as *mut _, + arguments: arguments.iter().map(|s| str_to_type(s.as_str())).collect(), + + return_type: Box::new(if return_type == "P" { + Type::void() + } else { + str_to_type(return_type) + }), + } + } + pub fn set_args(&mut self, args: Vec) { + self.arguments.clear(); + self.arguments + .extend(args.iter().map(|s| str_to_type(s.as_str()))); + } + + pub fn set_ret(&mut self, ret: &str) { + (*self.return_type.as_mut()) = if ret == "P" { + Type::void() + } else { + str_to_type(ret) + }; + } + + pub fn call(&mut self, arg_ptrs: Vec, vm: &VirtualMachine) -> PyResult { + let args_vec: Vec = arg_ptrs + .iter() + .zip(self.arguments.iter_mut()) + .map(|(o, t)| py_to_ffi(t, o.clone(), vm)) + .collect::>>()?; + + let args = args_vec.as_slice(); + + let cif = Cif::new( + self.arguments.clone().into_iter(), + self.return_type.as_ref().to_owned(), + ); + + let fun_ptr = CodePtr(self.pointer); + + let res = unsafe { + match_ffi_type!( + self.return_type.as_ref(), + c_schar => { + let r: c_schar = cif.call(fun_ptr, args); + vm.new_pyobj(r as i8) + } + c_int => { + let r: c_int = cif.call(fun_ptr, args); + vm.new_pyobj(r as i32) + } + c_short => { + let r: c_short = cif.call(fun_ptr, args); + vm.new_pyobj(r as i16) + } + c_ushort => { + let r: c_ushort = cif.call(fun_ptr, args); + vm.new_pyobj(r as u16) + } + c_uint => { + let r: c_uint = cif.call(fun_ptr, args); + vm.new_pyobj(r as u32) + } + c_long | c_longlong => { + let r: c_long = cif.call(fun_ptr, args); + vm.new_pyobj(r as i64) + } + c_ulong | c_ulonglong => { + let r: c_ulong = cif.call(fun_ptr, args); + vm.new_pyobj(r as u64) + } + f32 => { + let r: c_float = cif.call(fun_ptr, args); + vm.new_pyobj(r as f32) + } + f64 | longdouble=> { + let r: c_double = cif.call(fun_ptr, args); + vm.new_pyobj(r as f64) + } + c_uchar => { + let r: c_uchar = cif.call(fun_ptr, args); + vm.new_pyobj(r as u8) + } + pointer => { + let r: *mut c_void = cif.call(fun_ptr, args); + vm.new_pyobj(r as *const _ as usize) + } + void => { + vm.ctx.none() + } + ) + }; + + Ok(res) + } +} + +unsafe impl Send for Function {} +unsafe impl Sync for Function {} + +#[pyclass(module = "_ctypes", name = "CFuncPtr", base = "PyCData")] +pub struct PyCFuncPtr { + pub _name_: String, + pub _argtypes_: AtomicCell>, + pub _restype_: AtomicCell, + _handle: PyObjectRef, + _f: PyRwLock, +} + +impl fmt::Debug for PyCFuncPtr { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "PyCFuncPtr {{ _name_, _argtypes_, _restype_}}") + } +} + +impl PyValue for PyCFuncPtr { + fn class(_vm: &VirtualMachine) -> &PyTypeRef { + Self::static_type() + } +} + +#[pyimpl(with(Callable), flags(BASETYPE))] +impl PyCFuncPtr { + #[pyproperty(name = "_argtypes_")] + fn argtypes(&self, vm: &VirtualMachine) -> PyObjectRef { + vm.ctx + .new_list(unsafe { &*self._argtypes_.as_ptr() }.clone()) + } + + #[pyproperty(name = "_restype_")] + fn restype(&self, _vm: &VirtualMachine) -> PyObjectRef { + unsafe { &*self._restype_.as_ptr() }.clone() + } + + #[pyproperty(name = "_argtypes_", setter)] + fn set_argtypes(&self, argtypes: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> { + if vm + .isinstance(&argtypes, &vm.ctx.types.list_type) + .and_then(|_| vm.isinstance(&argtypes, &vm.ctx.types.tuple_type)) + .map_err(|e| { + vm.new_type_error(format!( + "_argtypes_ must be a sequence of types, {} found.", + argtypes.to_string() + )) + })? + { + let args = vm.extract_elements(&argtypes)?; + let c_args_res: PyResult> = args + .iter() + .enumerate() + .map(|(idx, inner_obj)| { + match vm.isinstance(inner_obj, PyCSimple::static_type()) { + // FIXME: checks related to _type_ are temporary + // it needs to check for from_param method, instead + Ok(_) => vm.get_attribute(inner_obj.clone(), "_type_"), + _ => Err(vm.new_type_error(format!( + "item {} in _argtypes_ must be subclass of _SimpleType, but type {} found", + idx, + inner_obj.class().name + ))), + } + }) + .collect(); + + let c_args = c_args_res?; + + self._argtypes_.store(c_args.clone()); + + let str_types: Vec = c_args + .iter() + .map(|obj| vm.to_str(&obj).unwrap().to_string()) + .collect(); + + let mut fn_ptr = self._f.write(); + fn_ptr.set_args(str_types); + } + + Ok(()) + } + + #[pyproperty(name = "_restype_", setter)] + fn set_restype(&self, restype: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> { + match vm.isinstance(&restype, PyCSimple::static_type()) { + // TODO: checks related to _type_ are temporary + Ok(_) => match vm.get_attribute(restype.clone(), "_type_") { + Ok(_type_) => { + // TODO: restype must be a type, a callable, or None + self._restype_.store(restype.clone()); + let mut fn_ptr = self._f.write(); + fn_ptr.set_ret(vm.to_str(&_type_)?.as_ref()); + + Ok(()) + } + Err(_) => Err(vm.new_attribute_error("atribute _type_ not found".to_string())), + }, + + Err(_) => Err(vm.new_type_error(format!( + "value is not an instance of _CData, type {} found", + restype.class().name + ))), + } + } + + // TODO: Needs to check and implement other forms of new + #[pyslot] + fn tp_new( + cls: PyTypeRef, + func_name: PyStrRef, + arg: PyObjectRef, + vm: &VirtualMachine, + ) -> PyResult> { + match vm.get_attribute(cls.as_object().to_owned(), "_argtypes_") { + Ok(_) => Self::from_dll(cls, func_name, arg, vm), + Err(_) => Err(vm.new_type_error( + "cannot construct instance of this class: no argtypes slot".to_string(), + )), + } + } + + /// Returns a PyCFuncPtr from a Python DLL object + /// # Arguments + /// + /// * `func_name` - A string that names the function symbol + /// * `arg` - A Python object with _handle attribute of type int + /// + fn from_dll( + cls: PyTypeRef, + func_name: PyStrRef, + arg: PyObjectRef, + vm: &VirtualMachine, + ) -> PyResult> { + if let Ok(h) = vm.get_attribute(arg.clone(), "_handle") { + if let Ok(handle) = h.downcast::() { + let handle_obj = handle.clone().into_object(); + let ptr_fn = dlsym(handle, func_name.clone(), vm)?; + let fn_ptr = usize::try_from_object(vm, ptr_fn)?; + + PyCFuncPtr { + _name_: func_name.to_string(), + _argtypes_: AtomicCell::default(), + _restype_: AtomicCell::new(vm.ctx.none()), + _handle: handle_obj, + _f: PyRwLock::new(Function::new( + fn_ptr, + Vec::new(), + "i", // put a default here + )), + } + .into_ref_with_type(vm, cls) + } else { + Err(vm.new_type_error(format!("_handle must be an int not {}", arg.class().name))) + } + } else { + Err(vm.new_attribute_error( + "positional argument 2 must have _handle attribute".to_string(), + )) + } + } +} + +impl Callable for PyCFuncPtr { + fn call(zelf: &PyRef, args: FuncArgs, vm: &VirtualMachine) -> PyResult { + let inner_args = unsafe { &*zelf._argtypes_.as_ptr() }; + + if args.args.len() != inner_args.len() { + return Err(vm.new_runtime_error(format!( + "this function takes at least {} argument{} ({} given)", + inner_args.len(), + if !inner_args.is_empty() { "s" } else { "" }, + args.args.len() + ))); + } + + let arg_res: Result, _> = args + .args + .iter() + .enumerate() + .map(|(idx, obj)| { + if vm + .issubclass(&obj.clone_class(), PyCSimple::static_type()) + .is_ok() + { + Ok(vm.get_attribute(obj.clone(), "value")?) + } else { + Err(vm.new_type_error(format!( + "positional argument {} must be subclass of _SimpleType, but type {} found", + idx, + obj.class().name + ))) + } + }) + .collect(); + + (*zelf._f.write()).call(arg_res?, vm) + } +} diff --git a/vm/src/stdlib/ctypes/mod.rs b/vm/src/stdlib/ctypes/mod.rs new file mode 100644 index 0000000000..51a8179fce --- /dev/null +++ b/vm/src/stdlib/ctypes/mod.rs @@ -0,0 +1,48 @@ +use crate::pyobject::{PyClassImpl, PyObjectRef}; +use crate::VirtualMachine; + +mod array; +mod basics; +mod dll; +mod function; +mod pointer; +mod primitive; +mod shared_lib; +mod structure; + +use array::{PyCArray, PyCArrayMeta}; +use basics::{addressof, alignment, byref, sizeof_func, PyCData}; +use function::PyCFuncPtr; +use pointer::{pointer_fn, PyCPointer, POINTER}; +use primitive::{PyCSimple, PySimpleMeta}; +use structure::PyCStructure; + +pub(crate) fn make_module(vm: &VirtualMachine) -> PyObjectRef { + let ctx = &vm.ctx; + PyCData::make_class(ctx); + PySimpleMeta::make_class(ctx); + PyCArrayMeta::make_class(ctx); + + let module = py_module!(vm, "_ctypes", { + "__version__" => ctx.new_str("1.1.0"), + + "alignment" => ctx.new_function("alignment", alignment), + "sizeof" => ctx.new_function("sizeof", sizeof_func), + "byref" => ctx.new_function("byref", byref), + "addressof" => ctx.new_function("addressof", addressof), + + "POINTER" => ctx.new_function("POINTER", POINTER), + "pointer" => ctx.new_function("pointer", pointer_fn), + "_pointer_type_cache" => ctx.new_dict(), + + "CFuncPtr" => PyCFuncPtr::make_class(ctx), + "_SimpleCData" => PyCSimple::make_class(ctx), + "_Pointer" => PyCPointer::make_class(ctx), + "Array" => PyCArray::make_class(ctx), + "Struct" => PyCStructure::make_class(ctx) + }); + + dll::extend_module(vm, &module); + + module +} diff --git a/vm/src/stdlib/ctypes/pointer.rs b/vm/src/stdlib/ctypes/pointer.rs new file mode 100644 index 0000000000..a5fc734a32 --- /dev/null +++ b/vm/src/stdlib/ctypes/pointer.rs @@ -0,0 +1,35 @@ +use std::fmt; + +use crate::builtins::PyTypeRef; +use crate::pyobject::{PyObjectRef, PyValue, StaticType}; +use crate::VirtualMachine; + +use crate::stdlib::ctypes::basics::PyCData; + +#[pyclass(module = "_ctypes", name = "_Pointer", base = "PyCData")] +pub struct PyCPointer {} + +impl fmt::Debug for PyCPointer { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "_Pointer {{}}") + } +} + +impl PyValue for PyCPointer { + fn class(_vm: &VirtualMachine) -> &PyTypeRef { + Self::static_type() + } +} + +// impl PyCDataMethods for PyCPointer { +// fn from_param(zelf: PyRef, value: PyObjectRef, vm: &VirtualMachine) -> PyResult { + +// } +// } + +#[pyimpl(flags(BASETYPE))] +impl PyCPointer {} + +pub fn POINTER(cls: PyTypeRef) {} + +pub fn pointer_fn(inst: PyObjectRef) {} diff --git a/vm/src/stdlib/ctypes/primitive.rs b/vm/src/stdlib/ctypes/primitive.rs new file mode 100644 index 0000000000..1c3615317c --- /dev/null +++ b/vm/src/stdlib/ctypes/primitive.rs @@ -0,0 +1,448 @@ +use crossbeam_utils::atomic::AtomicCell; +use std::fmt; + +use crate::builtins::memory::try_buffer_from_object; +use crate::builtins::PyTypeRef; +use crate::builtins::{ + int::try_to_primitive, pybool::boolval, PyByteArray, PyBytes, PyFloat, PyInt, PyNone, PyStr, + PyType, +}; +use crate::function::OptionalArg; +use crate::pyobject::{ + IdProtocol, PyObjectRef, PyRef, PyResult, PyValue, StaticType, TryFromObject, TypeProtocol, +}; +use crate::stdlib::ctypes::array::PyCArray; +use crate::stdlib::ctypes::basics::{ + default_from_param, get_size, BorrowValueMut, PyCData, PyCDataFunctions, PyCDataMethods, + PyCDataSequenceMethods, +}; +use crate::stdlib::ctypes::function::PyCFuncPtr; +use crate::stdlib::ctypes::pointer::PyCPointer; +use crate::utils::Either; +use crate::VirtualMachine; + +const SIMPLE_TYPE_CHARS: &str = "cbBhHiIlLdfguzZPqQ?"; + +fn set_primitive(_type_: &str, value: &PyObjectRef, vm: &VirtualMachine) -> PyResult { + match _type_ { + "c" => { + if value + .clone() + .downcast_exact::(vm) + .map_or(false, |v| v.len() == 1) + || value + .clone() + .downcast_exact::(vm) + .map_or(false, |v| v.borrow_buf().len() == 1) + || value + .clone() + .downcast_exact::(vm) + .map_or(Ok(false), |v| { + let n: i64 = try_to_primitive(v.as_bigint(), vm)?; + Ok(0 <= n && n <= 255) + })? + { + Ok(value.clone()) + } else { + Err(vm.new_type_error( + "one character bytes, bytearray or integer expected".to_string(), + )) + } + } + "u" => { + if let Ok(b) = value + .clone() + .downcast_exact::(vm) + .map(|v| v.as_ref().chars().count() == 1) + { + if b { + Ok(value.clone()) + } else { + Err(vm.new_type_error("one character unicode string expected".to_string())) + } + } else { + Err(vm.new_type_error(format!( + "unicode string expected instead of {} instance", + value.class().name + ))) + } + } + "b" | "h" | "H" | "i" | "I" | "l" | "q" | "L" | "Q" => { + if value.clone().downcast_exact::(vm).is_ok() { + Ok(value.clone()) + } else { + Err(vm.new_type_error(format!( + "an integer is required (got type {})", + value.class().name + ))) + } + } + "f" | "d" | "g" => { + if value.clone().downcast_exact::(vm).is_ok() { + Ok(value.clone()) + } else { + Err(vm.new_type_error(format!("must be real number, not {}", value.class().name))) + } + } + "?" => Ok(vm.ctx.new_bool(boolval(vm, value.clone())?)), + "B" => { + if value.clone().downcast_exact::(vm).is_ok() { + Ok(vm.new_pyobj(u8::try_from_object(vm, value.clone())?)) + } else { + Err(vm.new_type_error(format!("int expected instead of {}", value.class().name))) + } + } + "z" => { + if value.clone().downcast_exact::(vm).is_ok() + || value.clone().downcast_exact::(vm).is_ok() + { + Ok(value.clone()) + } else { + Err(vm.new_type_error(format!( + "bytes or integer address expected instead of {} instance", + value.class().name + ))) + } + } + "Z" => { + if value.clone().downcast_exact::(vm).is_ok() { + Ok(value.clone()) + } else { + Err(vm.new_type_error(format!( + "unicode string or integer address expected instead of {} instance", + value.class().name + ))) + } + } + _ => { + // "P" + if value.clone().downcast_exact::(vm).is_ok() + || value.clone().downcast_exact::(vm).is_ok() + { + Ok(value.clone()) + } else { + Err(vm.new_type_error("cannot be converted to pointer".to_string())) + } + } + } +} + +fn generic_xxx_p_from_param( + cls: &PyTypeRef, + value: &PyObjectRef, + type_str: &str, + vm: &VirtualMachine, +) -> PyResult { + if vm.is_none(value) { + return Ok(vm.ctx.none()); + } + + if vm.isinstance(value, &vm.ctx.types.str_type)? + || vm.isinstance(value, &vm.ctx.types.bytes_type)? + { + Ok(PyCSimple { + _type_: type_str.to_string(), + value: AtomicCell::new(value.clone()), + } + .into_object(vm)) + } else if vm.isinstance(value, PyCSimple::static_type())? + && (type_str == "z" || type_str == "Z" || type_str == "P") + { + Ok(value.clone()) + } else { + // TODO: better message + Err(vm.new_type_error("wrong type".to_string())) + } +} + +fn from_param_char_p( + cls: &PyTypeRef, + value: &PyObjectRef, + vm: &VirtualMachine, +) -> PyResult { + let _type_ = vm + .get_attribute(value.clone(), "_type_")? + .downcast_exact::(vm) + .unwrap(); + let type_str = _type_.as_ref(); + + let res = generic_xxx_p_from_param(cls, value, type_str, vm)?; + + if !vm.is_none(&res) { + Ok(res) + } else if (vm.isinstance(value, PyCArray::static_type())? + || vm.isinstance(value, PyCPointer::static_type())?) + && (type_str == "z" || type_str == "Z" || type_str == "P") + { + Ok(value.clone()) + } else { + // TODO: Make sure of what goes here + Err(vm.new_type_error("some error".to_string())) + } +} + +fn from_param_void_p( + cls: &PyTypeRef, + value: &PyObjectRef, + vm: &VirtualMachine, +) -> PyResult { + let _type_ = vm + .get_attribute(value.clone(), "_type_")? + .downcast_exact::(vm) + .unwrap(); + let type_str = _type_.as_ref(); + + let res = generic_xxx_p_from_param(cls, value, type_str, vm)?; + + if !vm.is_none(&res) { + Ok(res) + } else if vm.isinstance(value, PyCArray::static_type())? { + Ok(value.clone()) + } else if vm.isinstance(value, PyCFuncPtr::static_type())? + || vm.isinstance(value, PyCPointer::static_type())? + { + // TODO: Is there a better way of doing this? + if let Some(from_address) = vm.get_method(cls.as_object().clone(), "from_address") { + if let Ok(cdata) = value.clone().downcast::() { + let buffer_guard = cdata.borrow_value_mut(); + let addr = buffer_guard.inner as usize; + + Ok(vm.invoke(&from_address?, (cls.clone_class(), addr))?) + } else { + // TODO: Make sure of what goes here + Err(vm.new_type_error("value should be an instance of _CData".to_string())) + } + } else { + // TODO: Make sure of what goes here + Err(vm.new_attribute_error("class has no from_address method".to_string())) + } + } else if vm.isinstance(value, &vm.ctx.types.int_type)? { + Ok(PyCSimple { + _type_: type_str.to_string(), + value: AtomicCell::new(value.clone()), + } + .into_object(vm)) + } else { + // TODO: Make sure of what goes here + Err(vm.new_type_error("some error".to_string())) + } +} + +pub fn new_simple_type( + cls: Either<&PyObjectRef, &PyTypeRef>, + vm: &VirtualMachine, +) -> PyResult { + let cls = match cls { + Either::A(obj) => obj, + Either::B(typ) => typ.as_object(), + }; + + if let Ok(_type_) = vm.get_attribute(cls.clone(), "_type_") { + if vm.isinstance(&_type_, &vm.ctx.types.str_type)? { + let tp_str = _type_.downcast_exact::(vm).unwrap().to_string(); + + if tp_str.len() != 1 { + Err(vm.new_value_error( + "class must define a '_type_' attribute which must be a string of length 1" + .to_string(), + )) + } else if !SIMPLE_TYPE_CHARS.contains(tp_str.as_str()) { + Err(vm.new_attribute_error(format!("class must define a '_type_' attribute which must be\n a single character string containing one of {}.",SIMPLE_TYPE_CHARS))) + } else { + Ok(PyCSimple { + _type_: tp_str, + value: AtomicCell::new(vm.ctx.none()), + }) + } + } else { + Err(vm.new_type_error("class must define a '_type_' string attribute".to_string())) + } + } else { + Err(vm.new_attribute_error("class must define a '_type_' attribute".to_string())) + } +} + +#[pyclass(module = "_ctypes", name = "PyCSimpleType", base = "PyType")] +pub struct PySimpleMeta {} + +#[pyimpl(with(PyCDataMethods), flags(BASETYPE))] +impl PySimpleMeta { + #[pyslot] + fn tp_new(cls: PyTypeRef, _: OptionalArg, vm: &VirtualMachine) -> PyResult { + Ok(new_simple_type(Either::B(&cls), vm)? + .into_ref_with_type(vm, cls)? + .as_object() + .clone()) + } +} + +#[pyclass( + module = "_ctypes", + name = "_SimpleCData", + base = "PyCData", + metaclass = "PySimpleMeta" +)] +pub struct PyCSimple { + pub _type_: String, + value: AtomicCell, +} + +impl fmt::Debug for PyCSimple { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let value = unsafe { (*self.value.as_ptr()).to_string() }; + + write!( + f, + "PyCSimple {{ + _type_: {}, + value: {}, + }}", + self._type_.as_str(), + value + ) + } +} + +impl fmt::Debug for PySimpleMeta { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "PySimpleMeta") + } +} + +impl PyValue for PyCSimple { + fn class(_vm: &VirtualMachine) -> &PyTypeRef { + Self::static_type() + } +} + +impl PyValue for PySimpleMeta { + fn class(_vm: &VirtualMachine) -> &PyTypeRef { + Self::static_type() + } +} + +impl PyCDataMethods for PySimpleMeta { + // From PyCSimpleType_Type PyCSimpleType_methods + fn from_param( + zelf: PyRef, + value: PyObjectRef, + vm: &VirtualMachine, + ) -> PyResult { + let cls = zelf.clone_class(); + if cls.is(PyCSimple::static_type()) { + Err(vm.new_type_error("abstract class".to_string())) + } else if vm.isinstance(&value, &cls)? { + Ok(value) + } else { + let tp = vm.get_attribute(zelf.as_object().clone(), "_type_")?.downcast::().unwrap().to_string(); + let _type_ = tp.as_str(); + + match _type_ { + "z" | "Z" => from_param_char_p(&cls, &value, vm), + "P" => from_param_void_p(&cls, &value, vm), + _ => match new_simple_type(Either::B(&cls), vm) { + Ok(obj) => Ok(obj.into_object(vm)), + Err(e) => { + if vm.isinstance(&e.clone().into_object(), &vm.ctx.exceptions.type_error)? + || vm.isinstance( + &e.clone().into_object(), + &vm.ctx.exceptions.value_error, + )? + { + default_from_param(zelf, value.clone(), vm) + } else { + Err(e) + } + } + }, + } + } + } +} + +#[pyimpl(with(PyCDataFunctions), flags(BASETYPE))] +impl PyCSimple { + #[pymethod(magic)] + pub fn init(&self, value: OptionalArg, vm: &VirtualMachine) -> PyResult<()> { + if let Some(ref v) = value.into_option() { + let content = set_primitive(self._type_.as_str(), v, vm)?; + self.value.store(content); + } else { + self.value.store(match self._type_.as_str() { + "c" | "u" => vm.ctx.new_bytes(vec![0]), + "b" | "B" | "h" | "H" | "i" | "I" | "l" | "q" | "L" | "Q" => vm.ctx.new_int(0), + "f" | "d" | "g" => vm.ctx.new_float(0.0), + "?" => vm.ctx.new_bool(false), + _ => vm.ctx.none(), // "z" | "Z" | "P" + }); + } + Ok(()) + } + + #[pyproperty(name = "value")] + pub fn value(&self) -> PyObjectRef { + unsafe { (*self.value.as_ptr()).clone() } + } + + #[pyproperty(name = "value", setter)] + fn set_value(&self, value: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> { + let content = set_primitive(self._type_.as_str(), &value, vm)?; + self.value.store(content); + Ok(()) + } + + // From Simple_Type Simple_methods + #[pymethod(magic)] + pub fn ctypes_from_outparam(zelf: PyRef, vm: &VirtualMachine) -> PyResult { + if let Some(base) = zelf.class().base.clone() { + if vm.bool_eq(&base.as_object(), PyCSimple::static_type().as_object())? { + return Ok(zelf.as_object().clone()); + } + } + Ok(zelf.value()) + } + + // Simple_repr + #[pymethod(magic)] + fn repr(zelf: PyRef, vm: &VirtualMachine) -> PyResult { + Ok(format!( + "{}({})", + zelf.class().name, + vm.to_repr(&zelf.value())?.to_string() + )) + } + + // Simple_as_number + #[pymethod(magic)] + fn bool(zelf: PyRef, vm: &VirtualMachine) -> PyResult { + let buffer = try_buffer_from_object(vm, zelf.as_object())? + .obj_bytes() + .to_vec(); + + Ok(vm.new_pyobj(buffer != vec![0])) + } +} + +impl PyCDataFunctions for PyCSimple { + fn size_of_instances(zelf: PyRef, vm: &VirtualMachine) -> PyResult { + Ok(get_size(zelf._type_.as_str())) + } + + fn alignment_of_instances(zelf: PyRef, vm: &VirtualMachine) -> PyResult { + Self::size_of_instances(zelf, vm) + } + + fn ref_to( + zelf: PyRef, + offset: OptionalArg, + vm: &VirtualMachine, + ) -> PyResult { + Ok(vm.new_pyobj(zelf.value.as_ptr() as *mut _ as *mut usize as usize)) + } + + fn address_of(zelf: PyRef, vm: &VirtualMachine) -> PyResult { + Ok(vm.new_pyobj(unsafe { &*zelf.value.as_ptr() } as *const _ as *const usize as usize)) + } +} + +impl PyCDataSequenceMethods for PySimpleMeta {} diff --git a/vm/src/stdlib/ctypes/shared_lib.rs b/vm/src/stdlib/ctypes/shared_lib.rs new file mode 100644 index 0000000000..65d908d1b5 --- /dev/null +++ b/vm/src/stdlib/ctypes/shared_lib.rs @@ -0,0 +1,115 @@ +use std::{collections::HashMap, fmt, os::raw::c_void, ptr::null}; + +use crossbeam_utils::atomic::AtomicCell; +use libloading::Library; + +use crate::builtins::PyTypeRef; +use crate::common::lock::PyRwLock; +use crate::pyobject::{PyRef, PyValue}; +use crate::VirtualMachine; +pub struct SharedLibrary { + lib: AtomicCell>, +} + +impl fmt::Debug for SharedLibrary { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "SharedLibrary {{ + lib: {}, + }}", + self.get_pointer() + ) + } +} + +impl PyValue for SharedLibrary { + fn class(vm: &VirtualMachine) -> &PyTypeRef { + &vm.ctx.types.object_type + } +} + +impl SharedLibrary { + pub fn new(name: &str) -> Result { + Ok(SharedLibrary { + lib: AtomicCell::new(Some(unsafe { Library::new(name.to_string())? })), + }) + } + + pub fn get_sym(&self, name: &str) -> Result<*mut c_void, String> { + if let Some(inner) = unsafe { &*self.lib.as_ptr() } { + unsafe { + inner + .get(name.as_bytes()) + .map(|f: libloading::Symbol<*mut c_void>| *f) + .map_err(|err| err.to_string()) + } + } else { + Err("The library has been closed".to_string()) + } + } + + pub fn get_pointer(&self) -> usize { + if let Some(l) = unsafe { &*self.lib.as_ptr() } { + l as *const Library as usize + } else { + null::() as usize + } + } + + pub fn is_closed(&self) -> bool { + unsafe { &*self.lib.as_ptr() }.is_none() + } + + pub fn close(&self) { + let old = self.lib.take(); + self.lib.store(None); + drop(old); + } +} + +pub struct ExternalLibs { + libraries: HashMap>, +} + +impl ExternalLibs { + pub fn new() -> Self { + Self { + libraries: HashMap::new(), + } + } + + pub fn get_lib(&self, key: usize) -> Option<&PyRef> { + self.libraries.get(&key) + } + + pub fn get_or_insert_lib( + &mut self, + library_path: &str, + vm: &VirtualMachine, + ) -> Result<&PyRef, libloading::Error> { + let nlib = SharedLibrary::new(library_path)?.into_ref(vm); + let key = nlib.get_pointer(); + + match self.libraries.get(&key) { + Some(l) => { + if l.is_closed() { + self.libraries.insert(key, nlib); + } + } + _ => { + self.libraries.insert(key, nlib); + } + }; + + Ok(self.libraries.get(&key).unwrap()) + } +} + +rustpython_common::static_cell! { + static LIBCACHE: PyRwLock; +} + +pub fn libcache() -> &'static PyRwLock { + LIBCACHE.get_or_init(|| PyRwLock::new(ExternalLibs::new())) +} diff --git a/vm/src/stdlib/ctypes/structure.rs b/vm/src/stdlib/ctypes/structure.rs new file mode 100644 index 0000000000..c2c5ac7640 --- /dev/null +++ b/vm/src/stdlib/ctypes/structure.rs @@ -0,0 +1,25 @@ +use std::fmt; + +use crate::builtins::PyTypeRef; +use crate::pyobject::{PyValue, StaticType}; +use crate::VirtualMachine; + +use crate::stdlib::ctypes::basics::PyCData; + +#[pyclass(module = "_ctypes", name = "Structure", base = "PyCData")] +pub struct PyCStructure {} + +impl fmt::Debug for PyCStructure { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "PyCStructure {{}}") + } +} + +impl PyValue for PyCStructure { + fn class(_vm: &VirtualMachine) -> &PyTypeRef { + Self::static_type() + } +} + +#[pyimpl(flags(BASETYPE))] +impl PyCStructure {} diff --git a/vm/src/stdlib/ctypes/union.rs b/vm/src/stdlib/ctypes/union.rs new file mode 100644 index 0000000000..e69de29bb2 diff --git a/vm/src/stdlib/mod.rs b/vm/src/stdlib/mod.rs index 8f4cb7d92c..52d29298ce 100644 --- a/vm/src/stdlib/mod.rs +++ b/vm/src/stdlib/mod.rs @@ -12,6 +12,7 @@ mod bisect; mod codecs; mod collections; mod csv; +mod ctypes; mod dis; mod errno; mod functools; @@ -152,6 +153,10 @@ pub fn get_module_inits() -> StdlibMap { { os::MODULE_NAME => os::make_module, } + #[cfg(any(unix, windows, target_os = "wasi"))] + { + "_ctypes" => ctypes::make_module, + } #[cfg(any(unix, target_os = "wasi"))] { "fcntl" => fcntl::make_module,