diff --git a/Lib/ctypes/__init__.py b/Lib/ctypes/__init__.py index 41d39dc93f1..d3e11dc663a 100644 --- a/Lib/ctypes/__init__.py +++ b/Lib/ctypes/__init__.py @@ -33,7 +33,9 @@ if _os.name == "posix" and _sys.platform == "darwin": DEFAULT_MODE = RTLD_GLOBAL from _ctypes import FUNCFLAG_CDECL as _FUNCFLAG_CDECL, \ - FUNCFLAG_PYTHONAPI as _FUNCFLAG_PYTHONAPI + FUNCFLAG_PYTHONAPI as _FUNCFLAG_PYTHONAPI, \ + FUNCFLAG_USE_ERRNO as _FUNCFLAG_USE_ERRNO, \ + FUNCFLAG_USE_LASTERROR as _FUNCFLAG_USE_LASTERROR """ WINOLEAPI -> HRESULT @@ -73,8 +75,9 @@ def c_buffer(init, size=None): return create_string_buffer(init, size) _c_functype_cache = {} -def CFUNCTYPE(restype, *argtypes): - """CFUNCTYPE(restype, *argtypes) -> function prototype. +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 @@ -88,14 +91,21 @@ def CFUNCTYPE(restype, *argtypes): prototype((ordinal number, dll object)[, paramflags]) -> foreign function exported by ordinal prototype((function name, dll object)[, paramflags]) -> foreign function exported by name """ + flags = _FUNCFLAG_CDECL + if kw.pop("use_errno", False): + flags |= _FUNCFLAG_USE_ERRNO + if kw.pop("use_last_error", False): + flags |= _FUNCFLAG_USE_LASTERROR + if kw: + raise ValueError("unexpected keyword argument(s) %s" % kw.keys()) try: - return _c_functype_cache[(restype, argtypes)] + return _c_functype_cache[(restype, argtypes, flags)] except KeyError: class CFunctionType(_CFuncPtr): _argtypes_ = argtypes _restype_ = restype - _flags_ = _FUNCFLAG_CDECL - _c_functype_cache[(restype, argtypes)] = CFunctionType + _flags_ = flags + _c_functype_cache[(restype, argtypes, flags)] = CFunctionType return CFunctionType if _os.name in ("nt", "ce"): @@ -106,16 +116,23 @@ if _os.name in ("nt", "ce"): _FUNCFLAG_STDCALL = _FUNCFLAG_CDECL _win_functype_cache = {} - def WINFUNCTYPE(restype, *argtypes): + def WINFUNCTYPE(restype, *argtypes, **kw): # docstring set later (very similar to CFUNCTYPE.__doc__) + flags = _FUNCFLAG_STDCALL + if kw.pop("use_errno", False): + flags |= _FUNCFLAG_USE_ERRNO + if kw.pop("use_last_error", False): + flags |= _FUNCFLAG_USE_LASTERROR + if kw: + raise ValueError("unexpected keyword argument(s) %s" % kw.keys()) try: - return _win_functype_cache[(restype, argtypes)] + return _win_functype_cache[(restype, argtypes, flags)] except KeyError: class WinFunctionType(_CFuncPtr): _argtypes_ = argtypes _restype_ = restype - _flags_ = _FUNCFLAG_STDCALL - _win_functype_cache[(restype, argtypes)] = WinFunctionType + _flags_ = flags + _win_functype_cache[(restype, argtypes, flags)] = WinFunctionType return WinFunctionType if WINFUNCTYPE.__doc__: WINFUNCTYPE.__doc__ = CFUNCTYPE.__doc__.replace("CFUNCTYPE", "WINFUNCTYPE") @@ -124,6 +141,7 @@ elif _os.name == "posix": from _ctypes import dlopen as _dlopen from _ctypes import sizeof, byref, addressof, alignment, resize +from _ctypes import get_errno, set_errno from _ctypes import _SimpleCData def _check_size(typ, typecode=None): @@ -313,12 +331,24 @@ class CDLL(object): Calling the functions releases the Python GIL during the call and reacquires it afterwards. """ - class _FuncPtr(_CFuncPtr): - _flags_ = _FUNCFLAG_CDECL - _restype_ = c_int # default, can be overridden in instances + _func_flags_ = _FUNCFLAG_CDECL + _func_restype_ = c_int - def __init__(self, name, mode=DEFAULT_MODE, handle=None): + def __init__(self, name, mode=DEFAULT_MODE, handle=None, + use_errno=False, + use_last_error=False): self._name = name + flags = self._func_flags_ + if use_errno: + flags |= _FUNCFLAG_USE_ERRNO + if use_last_error: + flags |= _FUNCFLAG_USE_LASTERROR + + class _FuncPtr(_CFuncPtr): + _flags_ = flags + _restype_ = self._func_restype_ + self._FuncPtr = _FuncPtr + if handle is None: self._handle = _dlopen(self._name, mode) else: @@ -348,9 +378,7 @@ class PyDLL(CDLL): access Python API functions. The GIL is not released, and Python exceptions are handled correctly. """ - class _FuncPtr(_CFuncPtr): - _flags_ = _FUNCFLAG_CDECL | _FUNCFLAG_PYTHONAPI - _restype_ = c_int # default, can be overridden in instances + _func_flags_ = _FUNCFLAG_CDECL | _FUNCFLAG_PYTHONAPI if _os.name in ("nt", "ce"): @@ -358,9 +386,7 @@ if _os.name in ("nt", "ce"): """This class represents a dll exporting functions using the Windows stdcall calling convention. """ - class _FuncPtr(_CFuncPtr): - _flags_ = _FUNCFLAG_STDCALL - _restype_ = c_int # default, can be overridden in instances + _func_flags_ = _FUNCFLAG_STDCALL # XXX Hm, what about HRESULT as normal parameter? # Mustn't it derive from c_long then? @@ -384,9 +410,8 @@ if _os.name in ("nt", "ce"): HRESULT error values are automatically raised as WindowsError exceptions. """ - class _FuncPtr(_CFuncPtr): - _flags_ = _FUNCFLAG_STDCALL - _restype_ = HRESULT + _func_flags_ = _FUNCFLAG_STDCALL + _func_restype_ = HRESULT class LibraryLoader(object): def __init__(self, dlltype): @@ -424,6 +449,7 @@ if _os.name in ("nt", "ce"): GetLastError = windll.kernel32.GetLastError else: GetLastError = windll.coredll.GetLastError + from _ctypes import get_last_error, set_last_error def WinError(code=None, descr=None): if code is None: diff --git a/Lib/ctypes/test/test_errno.py b/Lib/ctypes/test/test_errno.py new file mode 100644 index 00000000000..ce1cfad430f --- /dev/null +++ b/Lib/ctypes/test/test_errno.py @@ -0,0 +1,76 @@ +import unittest, os, errno +from ctypes import * +from ctypes.util import find_library +import threading + +class Test(unittest.TestCase): + def test_open(self): + libc_name = find_library("c") + if libc_name is not None: + libc = CDLL(libc_name, use_errno=True) + if os.name == "nt": + libc_open = libc._open + else: + libc_open = libc.open + + libc_open.argtypes = c_char_p, c_int + + self.failUnlessEqual(libc_open("", 0), -1) + self.failUnlessEqual(get_errno(), errno.ENOENT) + + self.failUnlessEqual(set_errno(32), errno.ENOENT) + self.failUnlessEqual(get_errno(), 32) + + + def _worker(): + set_errno(0) + + libc = CDLL(libc_name, use_errno=False) + if os.name == "nt": + libc_open = libc._open + else: + libc_open = libc.open + libc_open.argtypes = c_char_p, c_int + self.failUnlessEqual(libc_open("", 0), -1) + self.failUnlessEqual(get_errno(), 0) + + t = threading.Thread(target=_worker) + t.start() + t.join() + + self.failUnlessEqual(get_errno(), 32) + set_errno(0) + + if os.name == "nt": + + def test_GetLastError(self): + dll = WinDLL("kernel32", use_last_error=True) + GetModuleHandle = dll.GetModuleHandleA + GetModuleHandle.argtypes = [c_wchar_p] + + self.failUnlessEqual(0, GetModuleHandle("foo")) + self.failUnlessEqual(get_last_error(), 126) + + self.failUnlessEqual(set_last_error(32), 126) + self.failUnlessEqual(get_last_error(), 32) + + def _worker(): + set_last_error(0) + + dll = WinDLL("kernel32", use_last_error=False) + GetModuleHandle = dll.GetModuleHandleW + GetModuleHandle.argtypes = [c_wchar_p] + GetModuleHandle("bar") + + self.failUnlessEqual(get_last_error(), 0) + + t = threading.Thread(target=_worker) + t.start() + t.join() + + self.failUnlessEqual(get_last_error(), 32) + + set_last_error(0) + +if __name__ == "__main__": + unittest.main() diff --git a/Misc/NEWS b/Misc/NEWS index 9083bbcabe6..c0f25cc81bc 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -72,6 +72,9 @@ Extension Modules Library ------- +- Issue #1798: Add ctypes calling convention that allows safe access + to errno. + - Issue #2404: ctypes objects support the new pep3118 buffer interface - Patch #2125: Add GetInteger and GetString methods for diff --git a/Modules/_ctypes/_ctypes.c b/Modules/_ctypes/_ctypes.c index 30e981a7f94..34ec9f2580c 100644 --- a/Modules/_ctypes/_ctypes.c +++ b/Modules/_ctypes/_ctypes.c @@ -3412,7 +3412,7 @@ CFuncPtr_new(PyTypeObject *type, PyObject *args, PyObject *kwds) thunk = AllocFunctionCallback(callable, dict->argtypes, dict->restype, - dict->flags & FUNCFLAG_CDECL); + dict->flags); if (!thunk) return NULL; @@ -5535,6 +5535,8 @@ init_ctypes(void) PyModule_AddObject(m, "FUNCFLAG_STDCALL", PyInt_FromLong(FUNCFLAG_STDCALL)); #endif PyModule_AddObject(m, "FUNCFLAG_CDECL", PyInt_FromLong(FUNCFLAG_CDECL)); + PyModule_AddObject(m, "FUNCFLAG_USE_ERRNO", PyInt_FromLong(FUNCFLAG_USE_ERRNO)); + PyModule_AddObject(m, "FUNCFLAG_USE_LASTERROR", PyInt_FromLong(FUNCFLAG_USE_LASTERROR)); PyModule_AddObject(m, "FUNCFLAG_PYTHONAPI", PyInt_FromLong(FUNCFLAG_PYTHONAPI)); PyModule_AddStringConstant(m, "__version__", "1.1.0"); diff --git a/Modules/_ctypes/callbacks.c b/Modules/_ctypes/callbacks.c index b78c5282c88..aa021545f88 100644 --- a/Modules/_ctypes/callbacks.c +++ b/Modules/_ctypes/callbacks.c @@ -189,12 +189,15 @@ static void _CallPythonObject(void *mem, SETFUNC setfunc, PyObject *callable, PyObject *converters, + int flags, void **pArgs) { Py_ssize_t i; PyObject *result; PyObject *arglist = NULL; Py_ssize_t nArgs; + PyObject *error_object = NULL; + int *space; #ifdef WITH_THREAD PyGILState_STATE state = PyGILState_Ensure(); #endif @@ -271,8 +274,41 @@ static void _CallPythonObject(void *mem, #define CHECK(what, x) \ if (x == NULL) _AddTraceback(what, "_ctypes/callbacks.c", __LINE__ - 1), PyErr_Print() + if (flags & (FUNCFLAG_USE_ERRNO | FUNCFLAG_USE_LASTERROR)) { + error_object = get_error_object(&space); + if (error_object == NULL) + goto Done; + if (flags & FUNCFLAG_USE_ERRNO) { + int temp = space[0]; + space[0] = errno; + errno = temp; + } +#ifdef MS_WIN32 + if (flags & FUNCFLAG_USE_LASTERROR) { + int temp = space[1]; + space[1] = GetLastError(); + SetLastError(temp); + } +#endif + } + result = PyObject_CallObject(callable, arglist); CHECK("'calling callback function'", result); + +#ifdef MS_WIN32 + if (flags & FUNCFLAG_USE_LASTERROR) { + int temp = space[1]; + space[1] = GetLastError(); + SetLastError(temp); + } +#endif + if (flags & FUNCFLAG_USE_ERRNO) { + int temp = space[0]; + space[0] = errno; + errno = temp; + } + Py_XDECREF(error_object); + if ((restype != &ffi_type_void) && result) { PyObject *keep; assert(setfunc); @@ -322,6 +358,7 @@ static void closure_fcn(ffi_cif *cif, p->setfunc, p->callable, p->converters, + p->flags, args); } @@ -351,7 +388,7 @@ static CThunkObject* CThunkObject_new(Py_ssize_t nArgs) CThunkObject *AllocFunctionCallback(PyObject *callable, PyObject *converters, PyObject *restype, - int is_cdecl) + int flags) { int result; CThunkObject *p; @@ -371,6 +408,7 @@ CThunkObject *AllocFunctionCallback(PyObject *callable, goto error; } + p->flags = flags; for (i = 0; i < nArgs; ++i) { PyObject *cnv = PySequence_GetItem(converters, i); if (cnv == NULL) @@ -398,7 +436,7 @@ CThunkObject *AllocFunctionCallback(PyObject *callable, cc = FFI_DEFAULT_ABI; #if defined(MS_WIN32) && !defined(_WIN32_WCE) && !defined(MS_WIN64) - if (is_cdecl == 0) + if ((flags & FUNCFLAG_CDECL) == 0) cc = FFI_STDCALL; #endif result = ffi_prep_cif(&p->cif, cc, diff --git a/Modules/_ctypes/callproc.c b/Modules/_ctypes/callproc.c index 052017652f9..89a4d682502 100644 --- a/Modules/_ctypes/callproc.c +++ b/Modules/_ctypes/callproc.c @@ -83,7 +83,129 @@ #define DONT_USE_SEH #endif +/* + ctypes maintains thread-local storage that has space for two error numbers: + private copies of the system 'errno' value and, on Windows, the system error code + accessed by the GetLastError() and SetLastError() api functions. + + Foreign functions created with CDLL(..., use_errno=True), when called, swap + the system 'errno' value with the private copy just before the actual + function call, and swapped again immediately afterwards. The 'use_errno' + parameter defaults to False, in this case 'ctypes_errno' is not touched. + + On Windows, foreign functions created with CDLL(..., use_last_error=True) or + WinDLL(..., use_last_error=True) swap the system LastError value with the + ctypes private copy. + + The values are also swapped immeditately before and after ctypes callback + functions are called, if the callbacks are constructed using the new + optional use_errno parameter set to True: CFUNCTYPE(..., use_errno=TRUE) or + WINFUNCTYPE(..., use_errno=True). + + New ctypes functions are provided to access the ctypes private copies from + Python: + + - ctypes.set_errno(value) and ctypes.set_last_error(value) store 'value' in + the private copy and returns the previous value. + + - ctypes.get_errno() and ctypes.get_last_error() returns the current ctypes + private copies value. +*/ + +/* + This function creates and returns a thread-local Python object that has + space to store two integer error numbers; once created the Python object is + kept alive in the thread state dictionary as long as the thread itself. +*/ +PyObject * +get_error_object(int **pspace) +{ + PyObject *dict = PyThreadState_GetDict(); + PyObject *errobj; + if (dict == 0) { + PyErr_SetString(PyExc_RuntimeError, + "cannot get thread state"); + return NULL; + } + errobj = PyDict_GetItemString(dict, "ctypes.error_object"); + if (errobj) + Py_INCREF(errobj); + else { + void *space = PyMem_Malloc(sizeof(int) * 2); + if (space == NULL) + return NULL; + memset(space, 0, sizeof(int) * 2); + errobj = PyCObject_FromVoidPtr(space, PyMem_Free); + if (errobj == NULL) + return NULL; + if (-1 == PyDict_SetItemString(dict, "ctypes.error_object", + errobj)) { + Py_DECREF(errobj); + return NULL; + } + } + *pspace = (int *)PyCObject_AsVoidPtr(errobj); + return errobj; +} + +static PyObject * +get_error_internal(PyObject *self, PyObject *args, int index) +{ + int *space; + PyObject *errobj = get_error_object(&space); + PyObject *result; + + if (errobj == NULL) + return NULL; + result = PyInt_FromLong(space[index]); + Py_DECREF(errobj); + return result; +} + +static PyObject * +set_error_internal(PyObject *self, PyObject *args, int index) +{ + int new_errno, old_errno; + PyObject *errobj; + int *space; + + if (!PyArg_ParseTuple(args, "i", &new_errno)) + return NULL; + errobj = get_error_object(&space); + if (errobj == NULL) + return NULL; + old_errno = space[index]; + space[index] = new_errno; + Py_DECREF(errobj); + return PyInt_FromLong(old_errno); +} + +static PyObject * +get_errno(PyObject *self, PyObject *args) +{ + return get_error_internal(self, args, 0); +} + +static PyObject * +set_errno(PyObject *self, PyObject *args) +{ + return set_error_internal(self, args, 0); +} + #ifdef MS_WIN32 + +static PyObject * +get_last_error(PyObject *self, PyObject *args) +{ + return get_error_internal(self, args, 1); +} + +static PyObject * +set_last_error(PyObject *self, PyObject *args) +{ + return set_error_internal(self, args, 1); +} + PyObject *ComError; static TCHAR *FormatError(DWORD code) @@ -625,6 +747,8 @@ static int _call_function_pointer(int flags, #ifdef WITH_THREAD PyThreadState *_save = NULL; /* For Py_BLOCK_THREADS and Py_UNBLOCK_THREADS */ #endif + PyObject *error_object = NULL; + int *space; ffi_cif cif; int cc; #ifdef MS_WIN32 @@ -656,11 +780,26 @@ static int _call_function_pointer(int flags, return -1; } + if (flags & (FUNCFLAG_USE_ERRNO | FUNCFLAG_USE_LASTERROR)) { + error_object = get_error_object(&space); + if (error_object == NULL) + return -1; + } #ifdef WITH_THREAD if ((flags & FUNCFLAG_PYTHONAPI) == 0) Py_UNBLOCK_THREADS #endif + if (flags & FUNCFLAG_USE_ERRNO) { + int temp = space[0]; + space[0] = errno; + errno = temp; + } #ifdef MS_WIN32 + if (flags & FUNCFLAG_USE_LASTERROR) { + int temp = space[1]; + space[1] = GetLastError(); + SetLastError(temp); + } #ifndef DONT_USE_SEH __try { #endif @@ -675,7 +814,18 @@ static int _call_function_pointer(int flags, ; } #endif + if (flags & FUNCFLAG_USE_LASTERROR) { + int temp = space[1]; + space[1] = GetLastError(); + SetLastError(temp); + } #endif + if (flags & FUNCFLAG_USE_ERRNO) { + int temp = space[0]; + space[0] = errno; + errno = temp; + } + Py_XDECREF(error_object); #ifdef WITH_THREAD if ((flags & FUNCFLAG_PYTHONAPI) == 0) Py_BLOCK_THREADS @@ -1692,6 +1842,8 @@ buffer_info(PyObject *self, PyObject *arg) } PyMethodDef module_methods[] = { + {"get_errno", get_errno, METH_NOARGS}, + {"set_errno", set_errno, METH_VARARGS}, {"POINTER", POINTER, METH_O }, {"pointer", pointer, METH_O }, {"_unpickle", unpickle, METH_VARARGS }, @@ -1702,6 +1854,8 @@ PyMethodDef module_methods[] = { {"set_conversion_mode", set_conversion_mode, METH_VARARGS, set_conversion_mode_doc}, #endif #ifdef MS_WIN32 + {"get_last_error", get_last_error, METH_NOARGS}, + {"set_last_error", set_last_error, METH_VARARGS}, {"CopyComPointer", copy_com_pointer, METH_VARARGS, copy_com_pointer_doc}, {"FormatError", format_error, METH_VARARGS, format_error_doc}, {"LoadLibrary", load_library, METH_VARARGS, load_library_doc}, diff --git a/Modules/_ctypes/ctypes.h b/Modules/_ctypes/ctypes.h index d068ea5eecd..96db12f2559 100644 --- a/Modules/_ctypes/ctypes.h +++ b/Modules/_ctypes/ctypes.h @@ -87,6 +87,7 @@ typedef struct { PyObject_VAR_HEAD ffi_closure *pcl; /* the C callable */ ffi_cif cif; + int flags; PyObject *converters; PyObject *callable; PyObject *restype; @@ -185,7 +186,7 @@ extern PyMethodDef module_methods[]; extern CThunkObject *AllocFunctionCallback(PyObject *callable, PyObject *converters, PyObject *restype, - int stdcall); + int flags); /* a table entry describing a predefined ctypes type */ struct fielddesc { char code; @@ -311,6 +312,8 @@ PyObject *_CallProc(PPROC pProc, #define FUNCFLAG_CDECL 0x1 #define FUNCFLAG_HRESULT 0x2 #define FUNCFLAG_PYTHONAPI 0x4 +#define FUNCFLAG_USE_ERRNO 0x8 +#define FUNCFLAG_USE_LASTERROR 0x10 #define TYPEFLAG_ISPOINTER 0x100 #define TYPEFLAG_HASPOINTER 0x200 @@ -429,6 +432,7 @@ extern char *alloc_format_string(const char *prefix, const char *suffix); extern int IsSimpleSubType(PyObject *obj); extern PyObject *_pointer_type_cache; +PyObject *get_error_object(int **pspace); #ifdef MS_WIN32 extern PyObject *ComError;