From e70c3378c039cae30cd9ae559c4bdeb923254c43 Mon Sep 17 00:00:00 2001 From: Thomas Heller Date: Wed, 4 Jun 2008 18:59:03 +0000 Subject: [PATCH] Issue #1798: Add ctypes calling convention that allows safe access to errno (and LastError, on Windows). ctypes maintains a module-global, but thread-local, variable that contains an error number; called 'ctypes_errno' for this discussion. This variable is a private copy of the systems 'errno' value; the copy is swapped with the 'errno' variable on several occasions. Foreign functions created with CDLL(..., use_errno=True), when called, swap the values 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. 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). Two new ctypes functions are provided to access the 'ctypes_errno' value from Python: - ctypes.set_errno(value) sets ctypes_errno to 'value', the previous ctypes_errno value is returned. - ctypes.get_errno() returns the current ctypes_errno value. --- On Windows, the same scheme is implemented for the error value which is managed by the GetLastError() and SetLastError() windows api calls. The ctypes functions are 'ctypes.set_last_error(value)' and 'ctypes.get_last_error()', the CDLL and WinDLL optional parameter is named 'use_last_error', defaults to False. --- On Windows, TlsSetValue and TlsGetValue calls are used to provide thread local storage for the variables; ctypes compiled with __GNUC__ uses __thread variables. --- Lib/ctypes/__init__.py | 72 ++++++++++++------ Lib/ctypes/test/test_errno.py | 76 +++++++++++++++++++ Misc/NEWS | 3 + Modules/_ctypes/_ctypes.c | 15 +++- Modules/_ctypes/callbacks.c | 21 +++++- Modules/_ctypes/callproc.c | 137 ++++++++++++++++++++++++++++++++++ Modules/_ctypes/ctypes.h | 13 +++- 7 files changed, 310 insertions(+), 27 deletions(-) create mode 100644 Lib/ctypes/test/test_errno.py 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..b80656b2405 --- /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 bec09804c37..830f6681e82 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. + - Patch #2125: Add GetInteger and GetString methods for msilib.Record objects. diff --git a/Modules/_ctypes/_ctypes.c b/Modules/_ctypes/_ctypes.c index 740b7f6e15f..7deb3f37a54 100644 --- a/Modules/_ctypes/_ctypes.c +++ b/Modules/_ctypes/_ctypes.c @@ -3271,7 +3271,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; @@ -5273,6 +5273,17 @@ init_ctypes(void) if (!m) return; +#ifdef MS_WIN32 + dwTlsIndex_LastError = TlsAlloc(); + dwTlsIndex_errno = TlsAlloc(); + if (dwTlsIndex_LastError == TLS_OUT_OF_INDEXES + || dwTlsIndex_errno == TLS_OUT_OF_INDEXES) { + PyErr_SetString(PyExc_MemoryError, + "Could not allocate TLSIndex for LastError value"); + return; + } +#endif + _pointer_type_cache = PyDict_New(); if (_pointer_type_cache == NULL) return; @@ -5394,6 +5405,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..78c7419ce44 100644 --- a/Modules/_ctypes/callbacks.c +++ b/Modules/_ctypes/callbacks.c @@ -189,6 +189,7 @@ static void _CallPythonObject(void *mem, SETFUNC setfunc, PyObject *callable, PyObject *converters, + int flags, void **pArgs) { Py_ssize_t i; @@ -271,8 +272,22 @@ 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) + _swap_errno(); +#ifdef MS_WIN32 + if (flags & FUNCFLAG_USE_LASTERROR) + _swap_last_error(); +#endif + result = PyObject_CallObject(callable, arglist); CHECK("'calling callback function'", result); + +#ifdef MS_WIN32 + if (flags & FUNCFLAG_USE_LASTERROR) + _swap_last_error(); +#endif + if (flags & FUNCFLAG_USE_ERRNO) + _swap_errno(); if ((restype != &ffi_type_void) && result) { PyObject *keep; assert(setfunc); @@ -322,6 +337,7 @@ static void closure_fcn(ffi_cif *cif, p->setfunc, p->callable, p->converters, + p->flags, args); } @@ -351,7 +367,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 +387,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 +415,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 95b2709e798..70ca73f10a0 100644 --- a/Modules/_ctypes/callproc.c +++ b/Modules/_ctypes/callproc.c @@ -83,6 +83,131 @@ #define DONT_USE_SEH #endif +/* + ctypes maintains a module-global, but thread-local, variable that contains + an error number; called 'ctypes_errno' for this discussion. This variable + is a private copy of the systems 'errno' value; the copy is swapped with the + 'errno' variable on several occasions. + + Foreign functions created with CDLL(..., use_errno=True), when called, swap + the values 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. + + 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). + + Two new ctypes functions are provided to access the 'ctypes_errno' value + from Python: + + - ctypes.set_errno(value) sets ctypes_errno to 'value', the previous + ctypes_errno value is returned. + + - ctypes.get_errno() returns the current ctypes_errno value. + + --- + + On Windows, the same scheme is implemented for the error value which is + managed by the GetLastError() and SetLastError() windows api calls. + + The ctypes functions are 'ctypes.set_last_error(value)' and + 'ctypes.get_last_error()', the CDLL and WinDLL optional parameter is named + 'use_last_error', defaults to False. + + --- + + On Windows, TlsSetValue and TlsGetValue calls are used to provide thread + local storage for the variables; ctypes compiled with __GNUC__ uses __thread + variables. +*/ + +#if defined(MS_WIN32) +DWORD dwTlsIndex_LastError; +DWORD dwTlsIndex_errno; + +void +_swap_last_error(void) +{ + DWORD temp = GetLastError(); + SetLastError((DWORD)TlsGetValue(dwTlsIndex_LastError)); + TlsSetValue(dwTlsIndex_LastError, (void *)temp); +} + +static PyObject * +get_last_error(PyObject *self, PyObject *args) +{ + return PyInt_FromLong((DWORD)TlsGetValue(dwTlsIndex_LastError)); +} + +static PyObject * +set_last_error(PyObject *self, PyObject *args) +{ + DWORD new_value, prev_value; + if (!PyArg_ParseTuple(args, "i", &new_value)) + return NULL; + prev_value = (DWORD)TlsGetValue(dwTlsIndex_LastError); + TlsSetValue(dwTlsIndex_LastError, (void *)new_value); + return PyInt_FromLong(prev_value); +} + +void +_swap_errno(void) +{ + int temp = errno; + errno = (int)TlsGetValue(dwTlsIndex_errno); + TlsSetValue(dwTlsIndex_errno, (void *)temp); +} + +static PyObject * +get_errno(PyObject *self, PyObject *args) +{ + return PyInt_FromLong((int)TlsGetValue(dwTlsIndex_errno)); +} + +static PyObject * +set_errno(PyObject *self, PyObject *args) +{ + int new_value, prev_value; + if (!PyArg_ParseTuple(args, "i", &new_value)) + return NULL; + prev_value = (int)TlsGetValue(dwTlsIndex_errno); + TlsSetValue(dwTlsIndex_errno, (void *)new_value); + return PyInt_FromLong(prev_value); +} + +#elif defined(__GNUC__) +static __thread int ctypes_errno; + +void +_swap_errno(void) +{ + int temp = errno; + errno = ctypes_errno; + ctypes_errno = temp; +} + +static PyObject * +get_errno(PyObject *self, PyObject *args) +{ + return PyInt_FromLong(ctypes_errno); +} + +static PyObject * +set_errno(PyObject *self, PyObject *args) +{ + int new_errno; + if (!PyArg_ParseTuple(args, "i", &new_errno)) + return NULL; + return PyInt_FromLong(_save_errno(new_errno)); +} +#else + +#error "TLS not implemented in this configuration" + +#endif + #ifdef MS_WIN32 PyObject *ComError; @@ -660,7 +785,11 @@ static int _call_function_pointer(int flags, if ((flags & FUNCFLAG_PYTHONAPI) == 0) Py_UNBLOCK_THREADS #endif + if (flags & FUNCFLAG_USE_ERRNO) + _swap_errno(); #ifdef MS_WIN32 + if (flags & FUNCFLAG_USE_LASTERROR) + _swap_last_error(); #ifndef DONT_USE_SEH __try { #endif @@ -675,7 +804,11 @@ static int _call_function_pointer(int flags, ; } #endif + if (flags & FUNCFLAG_USE_LASTERROR) + _swap_last_error(); #endif + if (flags & FUNCFLAG_USE_ERRNO) + _swap_errno(); #ifdef WITH_THREAD if ((flags & FUNCFLAG_PYTHONAPI) == 0) Py_BLOCK_THREADS @@ -1667,6 +1800,8 @@ pointer(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 }, @@ -1675,6 +1810,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 1a104cfb2c2..a7c35628e1b 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; @@ -303,6 +304,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 @@ -421,8 +424,16 @@ extern int IsSimpleSubType(PyObject *obj); extern PyObject *_pointer_type_cache; +extern void _swap_errno(void); + #ifdef MS_WIN32 + +extern void _swap_last_error(void); + extern PyObject *ComError; + +extern DWORD dwTlsIndex_LastError; +extern DWORD dwTlsIndex_errno; #endif /*