Author: Armin Rigo <ar...@tunes.org> Branch: static-callback-embedding Changeset: r2499:2075cb3f448f Date: 2015-12-29 14:33 +0100 http://bitbucket.org/cffi/cffi/changeset/2075cb3f448f/
Log: hg merge default diff --git a/c/_cffi_backend.c b/c/_cffi_backend.c --- a/c/_cffi_backend.c +++ b/c/_cffi_backend.c @@ -266,18 +266,7 @@ /* whenever running Python code, the errno is saved in this thread-local variable */ #ifndef MS_WIN32 -# ifdef USE__THREAD -/* This macro ^^^ is defined by setup.py if it finds that it is - syntactically valid to use "__thread" with this C compiler. */ -static __thread int cffi_saved_errno = 0; -static void save_errno(void) { cffi_saved_errno = errno; } -static void restore_errno(void) { errno = cffi_saved_errno; } -static void init_errno(void) { } -# else -# include "misc_thread.h" -# endif -# define save_errno_only save_errno -# define restore_errno_only restore_errno +# include "misc_thread_posix.h" #endif #include "minibuffer.h" @@ -290,8 +279,11 @@ # include "wchar_helper.h" #endif -typedef PyObject *const cffi_allocator_t[3]; -static cffi_allocator_t default_allocator = { NULL, NULL, NULL }; +typedef struct _cffi_allocator_s { + PyObject *ca_alloc, *ca_free; + int ca_dont_clear; +} cffi_allocator_t; +static const cffi_allocator_t default_allocator = { NULL, NULL, 0 }; static PyObject *FFIError; static PyObject *unique_cache; @@ -3030,21 +3022,18 @@ static CDataObject *allocate_with_allocator(Py_ssize_t basesize, Py_ssize_t datasize, CTypeDescrObject *ct, - cffi_allocator_t allocator) + const cffi_allocator_t *allocator) { CDataObject *cd; - PyObject *my_alloc = allocator[0]; - PyObject *my_free = allocator[1]; - PyObject *dont_clear_after_alloc = allocator[2]; - - if (my_alloc == NULL) { /* alloc */ + + if (allocator->ca_alloc == NULL) { cd = allocate_owning_object(basesize + datasize, ct); if (cd == NULL) return NULL; cd->c_data = ((char *)cd) + basesize; } else { - PyObject *res = PyObject_CallFunction(my_alloc, "n", datasize); + PyObject *res = PyObject_CallFunction(allocator->ca_alloc, "n", datasize); if (res == NULL) return NULL; @@ -3069,16 +3058,16 @@ return NULL; } - cd = allocate_gcp_object(cd, ct, my_free); + cd = allocate_gcp_object(cd, ct, allocator->ca_free); Py_DECREF(res); } - if (dont_clear_after_alloc == NULL) + if (!allocator->ca_dont_clear) memset(cd->c_data, 0, datasize); return cd; } static PyObject *direct_newp(CTypeDescrObject *ct, PyObject *init, - cffi_allocator_t allocator) + const cffi_allocator_t *allocator) { CTypeDescrObject *ctitem; CDataObject *cd; @@ -3183,7 +3172,7 @@ PyObject *init = Py_None; if (!PyArg_ParseTuple(args, "O!|O:newp", &CTypeDescr_Type, &ct, &init)) return NULL; - return direct_newp(ct, init, default_allocator); + return direct_newp(ct, init, &default_allocator); } static int @@ -4659,7 +4648,9 @@ if (cif_descr != NULL) { /* exchange data size */ - cif_descr->exchange_size = exchange_offset; + /* we also align it to the next multiple of 8, in an attempt to + work around bugs(?) of libffi like #241 */ + cif_descr->exchange_size = ALIGN_ARG(exchange_offset); } return 0; } @@ -5101,15 +5092,9 @@ { save_errno(); { -#ifdef WITH_THREAD - PyGILState_STATE state = PyGILState_Ensure(); -#endif - + PyGILState_STATE state = gil_ensure(); general_invoke_callback(1, result, (char *)args, userdata); - -#ifdef WITH_THREAD - PyGILState_Release(state); -#endif + gil_release(state); } restore_errno(); } @@ -6515,7 +6500,7 @@ if (v == NULL || PyModule_AddObject(m, "_C_API", v) < 0) INITERROR; - v = PyText_FromString("1.3.1"); + v = PyText_FromString("1.4.2"); if (v == NULL || PyModule_AddObject(m, "__version__", v) < 0) INITERROR; @@ -6542,7 +6527,7 @@ INITERROR; } - init_errno(); + init_cffi_tls(); if (PyErr_Occurred()) INITERROR; diff --git a/c/call_python.c b/c/call_python.c --- a/c/call_python.c +++ b/c/call_python.c @@ -24,9 +24,6 @@ static PyObject *_ffi_def_extern_decorator(PyObject *outer_args, PyObject *fn) { -#if PY_MAJOR_VERSION >= 3 -# error review! -#endif char *s; PyObject *error, *onerror, *infotuple, *old1; int index, err; @@ -43,10 +40,10 @@ return NULL; if (s == NULL) { - PyObject *name = PyObject_GetAttrString(fn, "__name__"); + name = PyObject_GetAttrString(fn, "__name__"); if (name == NULL) return NULL; - s = PyString_AsString(name); + s = PyText_AsUTF8(name); if (s == NULL) { Py_DECREF(name); return NULL; @@ -203,9 +200,7 @@ err = 1; } else { -#ifdef WITH_THREAD - PyGILState_STATE state = PyGILState_Ensure(); -#endif + PyGILState_STATE state = gil_ensure(); if (externpy->reserved1 != PyThreadState_GET()->interp->modules) { /* Update the (reserved1, reserved2) cache. This will fail if we didn't call @ffi.def_extern() in this particular @@ -215,9 +210,7 @@ if (!err) { general_invoke_callback(0, args, args, externpy->reserved2); } -#ifdef WITH_THREAD - PyGILState_Release(state); -#endif + gil_release(state); } if (err) { static const char *msg[2] = { diff --git a/c/cffi1_module.c b/c/cffi1_module.c --- a/c/cffi1_module.c +++ b/c/cffi1_module.c @@ -22,7 +22,7 @@ static int init_ffi_lib(PyObject *m) { PyObject *x; - int i; + int i, res; static char init_done = 0; if (PyType_Ready(&FFI_Type) < 0) @@ -48,11 +48,13 @@ for (i = 0; all_dlopen_flags[i].name != NULL; i++) { x = PyInt_FromLong(all_dlopen_flags[i].value); - if (x == NULL || PyDict_SetItemString(FFI_Type.tp_dict, - all_dlopen_flags[i].name, - x) < 0) + if (x == NULL) return -1; + res = PyDict_SetItemString(FFI_Type.tp_dict, + all_dlopen_flags[i].name, x); Py_DECREF(x); + if (res < 0) + return -1; } init_done = 1; } diff --git a/c/commontypes.c b/c/commontypes.c --- a/c/commontypes.c +++ b/c/commontypes.c @@ -199,7 +199,8 @@ static PyObject *b__get_common_types(PyObject *self, PyObject *arg) { - int i, err; + int err; + size_t i; for (i = 0; i < num_common_simple_types; i++) { const char *s = common_simple_types[i]; PyObject *o = PyText_FromString(s + strlen(s) + 1); diff --git a/c/ffi_obj.c b/c/ffi_obj.c --- a/c/ffi_obj.c +++ b/c/ffi_obj.c @@ -335,7 +335,7 @@ "pointer to the memory somewhere else, e.g. into another structure."); static PyObject *_ffi_new(FFIObject *self, PyObject *args, PyObject *kwds, - cffi_allocator_t allocator) + const cffi_allocator_t *allocator) { CTypeDescrObject *ct; PyObject *arg, *init = Py_None; @@ -353,15 +353,22 @@ static PyObject *ffi_new(FFIObject *self, PyObject *args, PyObject *kwds) { - return _ffi_new(self, args, kwds, default_allocator); + return _ffi_new(self, args, kwds, &default_allocator); } static PyObject *_ffi_new_with_allocator(PyObject *allocator, PyObject *args, PyObject *kwds) { + cffi_allocator_t alloc1; + PyObject *my_alloc, *my_free; + my_alloc = PyTuple_GET_ITEM(allocator, 1); + my_free = PyTuple_GET_ITEM(allocator, 2); + alloc1.ca_alloc = (my_alloc == Py_None ? NULL : my_alloc); + alloc1.ca_free = (my_free == Py_None ? NULL : my_free); + alloc1.ca_dont_clear = (PyTuple_GET_ITEM(allocator, 3) == Py_False); + return _ffi_new((FFIObject *)PyTuple_GET_ITEM(allocator, 0), - args, kwds, - &PyTuple_GET_ITEM(allocator, 1)); + args, kwds, &alloc1); } PyDoc_STRVAR(ffi_new_allocator_doc, @@ -396,27 +403,14 @@ return NULL; } - allocator = PyTuple_New(4); + allocator = PyTuple_Pack(4, + (PyObject *)self, + my_alloc, + my_free, + PyBool_FromLong(should_clear_after_alloc)); if (allocator == NULL) return NULL; - Py_INCREF(self); - PyTuple_SET_ITEM(allocator, 0, (PyObject *)self); - - if (my_alloc != Py_None) { - Py_INCREF(my_alloc); - PyTuple_SET_ITEM(allocator, 1, my_alloc); - } - if (my_free != Py_None) { - Py_INCREF(my_free); - PyTuple_SET_ITEM(allocator, 2, my_free); - } - if (!should_clear_after_alloc) { - PyObject *my_true = Py_True; - Py_INCREF(my_true); - PyTuple_SET_ITEM(allocator, 3, my_true); /* dont_clear_after_alloc */ - } - { static PyMethodDef md = {"allocator", (PyCFunction)_ffi_new_with_allocator, @@ -896,7 +890,14 @@ #endif PyDoc_STRVAR(ffi_init_once_doc, - "XXX document me"); +"init_once(function, tag): run function() once. More precisely,\n" +"'function()' is called the first time we see a given 'tag'.\n" +"\n" +"The return value of function() is remembered and returned by the current\n" +"and all future init_once() with the same tag. If init_once() is called\n" +"from multiple threads in parallel, all calls block until the execution\n" +"of function() is done. If function() raises an exception, it is\n" +"propagated and nothing is cached."); #if PY_MAJOR_VERSION < 3 /* PyCapsule_New is redefined to be PyCObject_FromVoidPtr in _cffi_backend, diff --git a/c/lib_obj.c b/c/lib_obj.c --- a/c/lib_obj.c +++ b/c/lib_obj.c @@ -459,6 +459,7 @@ static PyObject *lib_getattr(LibObject *lib, PyObject *name) { + char *p; PyObject *x; LIB_GET_OR_CACHE_ADDR(x, lib, name, goto missing); @@ -469,16 +470,25 @@ return x; missing: - if (strcmp(PyText_AsUTF8(name), "__all__") == 0) { + p = PyText_AsUTF8(name); + if (p == NULL) + return NULL; + if (strcmp(p, "__all__") == 0) { PyErr_Clear(); return _lib_dir1(lib, 1); } - if (strcmp(PyText_AsUTF8(name), "__dict__") == 0) { + if (strcmp(p, "__dict__") == 0) { PyErr_Clear(); return _lib_dict(lib); } + if (strcmp(p, "__class__") == 0) { + PyErr_Clear(); + x = (PyObject *)Py_TYPE(lib); + Py_INCREF(x); + return x; + } /* this hack is for Python 3.5 */ - if (strcmp(PyText_AsUTF8(name), "__name__") == 0) { + if (strcmp(p, "__name__") == 0) { PyErr_Clear(); return lib_repr(lib); } diff --git a/c/misc_thread.h b/c/misc_thread.h deleted file mode 100644 --- a/c/misc_thread.h +++ /dev/null @@ -1,23 +0,0 @@ -#include <pthread.h> - -/* This is only included if GCC doesn't support "__thread" global variables. - * See USE__THREAD in _ffi_backend.c. - */ - -static pthread_key_t cffi_tls_key; - -static void init_errno(void) -{ - (void) pthread_key_create(&cffi_tls_key, NULL); -} - -static void save_errno(void) -{ - intptr_t value = errno; - (void) pthread_setspecific(cffi_tls_key, (void *)value); -} - -static void restore_errno(void) { - intptr_t value = (intptr_t)pthread_getspecific(cffi_tls_key); - errno = value; -} diff --git a/c/misc_thread_posix.h b/c/misc_thread_posix.h new file mode 100644 --- /dev/null +++ b/c/misc_thread_posix.h @@ -0,0 +1,186 @@ +/* + Logic for a better replacement of PyGILState_Ensure(). + + This version is ready to handle the case of a non-Python-started + thread in which we do a large number of calls to CFFI callbacks. If + we were to rely on PyGILState_Ensure() for that, we would constantly + be creating and destroying PyThreadStates---it is slow, and + PyThreadState_Delete() will actually walk the list of all thread + states, making it O(n). :-( + + This version only creates one PyThreadState object the first time we + see a given thread, and keep it alive until the thread is really + shut down, using a destructor on the tls key. +*/ + +#ifdef WITH_THREAD +#include <pthread.h> + + +static pthread_key_t cffi_tls_key; + +struct cffi_tls_s { + /* The locally-made thread state. This is only non-null in case + we build the thread state here. It remains null if this thread + had already a thread state provided by CPython. */ + PyThreadState *local_thread_state; + + /* The saved errno. If the C compiler supports '__thread', then + we use that instead; this value is not used at all in this case. */ + int saved_errno; +}; + +static void _tls_destructor(void *p) +{ + struct cffi_tls_s *tls = (struct cffi_tls_s *)p; + + if (tls->local_thread_state != NULL) { + /* We need to re-acquire the GIL temporarily to free the + thread state. I hope it is not a problem to do it in + a thread-local destructor. + */ + PyEval_RestoreThread(tls->local_thread_state); + PyThreadState_DeleteCurrent(); + } + free(tls); +} + +static void init_cffi_tls(void) +{ + if (pthread_key_create(&cffi_tls_key, _tls_destructor) != 0) + PyErr_SetString(PyExc_OSError, "pthread_key_create() failed"); +} + +static struct cffi_tls_s *_make_cffi_tls(void) +{ + void *p = calloc(1, sizeof(struct cffi_tls_s)); + if (p == NULL) + return NULL; + if (pthread_setspecific(cffi_tls_key, p) != 0) { + free(p); + return NULL; + } + return p; +} + +static struct cffi_tls_s *get_cffi_tls(void) +{ + void *p = pthread_getspecific(cffi_tls_key); + if (p == NULL) + p = _make_cffi_tls(); + return (struct cffi_tls_s *)p; +} + + +/* USE__THREAD is defined by setup.py if it finds that it is + syntactically valid to use "__thread" with this C compiler. */ +#ifdef USE__THREAD + +static __thread int cffi_saved_errno = 0; +static void save_errno(void) { cffi_saved_errno = errno; } +static void restore_errno(void) { errno = cffi_saved_errno; } + +#else + +static void save_errno(void) +{ + int saved = errno; + struct cffi_tls_s *tls = get_cffi_tls(); + if (tls != NULL) + tls->saved_errno = saved; +} + +static void restore_errno(void) +{ + struct cffi_tls_s *tls = get_cffi_tls(); + if (tls != NULL) + errno = tls->saved_errno; +} + +#endif + + +/* Seems that CPython 3.5.1 made our job harder. Did not find out how + to do that without these hacks. We can't use PyThreadState_GET(), + because that calls PyThreadState_Get() which fails an assert if the + result is NULL. */ +#if PY_MAJOR_VERSION >= 3 && !defined(_Py_atomic_load_relaxed) + /* this was abruptly un-defined in 3.5.1 */ +void *volatile _PyThreadState_Current; + /* XXX simple volatile access is assumed atomic */ +# define _Py_atomic_load_relaxed(pp) (*(pp)) +#endif + + +static PyThreadState *get_current_ts(void) +{ +#if PY_MAJOR_VERSION >= 3 + return (PyThreadState*)_Py_atomic_load_relaxed(&_PyThreadState_Current); +#else + return _PyThreadState_Current; +#endif +} + +static PyGILState_STATE gil_ensure(void) +{ + /* Called at the start of a callback. Replacement for + PyGILState_Ensure(). + */ + PyGILState_STATE result; + struct cffi_tls_s *tls; + PyThreadState *ts = PyGILState_GetThisThreadState(); + + if (ts != NULL) { + ts->gilstate_counter++; + if (ts != get_current_ts()) { + /* common case: 'ts' is our non-current thread state and + we have to make it current and acquire the GIL */ + PyEval_RestoreThread(ts); + return PyGILState_UNLOCKED; + } + else { + return PyGILState_LOCKED; + } + } + else { + /* no thread state here so far. */ + result = PyGILState_Ensure(); + assert(result == PyGILState_UNLOCKED); + + ts = PyGILState_GetThisThreadState(); + assert(ts != NULL); + assert(ts == get_current_ts()); + assert(ts->gilstate_counter >= 1); + + /* Save the now-current thread state inside our 'local_thread_state' + field, to be removed at thread shutdown */ + tls = get_cffi_tls(); + if (tls != NULL) { + tls->local_thread_state = ts; + ts->gilstate_counter++; + } + + return result; + } +} + +static void gil_release(PyGILState_STATE oldstate) +{ + PyGILState_Release(oldstate); +} + + +#else /* !WITH_THREAD */ + +static int cffi_saved_errno = 0; +static void save_errno(void) { cffi_saved_errno = errno; } +static void restore_errno(void) { errno = cffi_saved_errno; } + +static PyGILState_STATE gil_ensure(void) { return -1; } +static void gil_release(PyGILState_STATE oldstate) { } + +#endif /* !WITH_THREAD */ + + +#define save_errno_only save_errno +#define restore_errno_only restore_errno diff --git a/c/misc_win32.h b/c/misc_win32.h --- a/c/misc_win32.h +++ b/c/misc_win32.h @@ -10,7 +10,7 @@ static DWORD cffi_tls_index = TLS_OUT_OF_INDEXES; -static void init_errno(void) +static void init_cffi_tls(void) { if (cffi_tls_index == TLS_OUT_OF_INDEXES) { cffi_tls_index = TlsAlloc(); @@ -182,6 +182,17 @@ } #endif + +#ifdef WITH_THREAD +/* XXX should port the code from misc_thread_posix.h */ +static PyGILState_STATE gil_ensure(void) { return PyGILState_Ensure(); } +static void gil_release(PyGILState_STATE oldst) { PyGILState_Release(oldst); } +#else +static PyGILState_STATE gil_ensure(void) { return -1; } +static void gil_release(PyGILState_STATE oldstate) { } +#endif + + /************************************************************/ /* Emulate dlopen()&co. from the Windows API */ diff --git a/c/test_c.py b/c/test_c.py --- a/c/test_c.py +++ b/c/test_c.py @@ -12,7 +12,7 @@ # ____________________________________________________________ import sys -assert __version__ == "1.3.1", ("This test_c.py file is for testing a version" +assert __version__ == "1.4.2", ("This test_c.py file is for testing a version" " of cffi that differs from the one that we" " get from 'import _cffi_backend'") if sys.version_info < (3,): diff --git a/cffi/__init__.py b/cffi/__init__.py --- a/cffi/__init__.py +++ b/cffi/__init__.py @@ -4,8 +4,8 @@ from .api import FFI, CDefError, FFIError from .ffiplatform import VerificationError, VerificationMissing -__version__ = "1.3.1" -__version_info__ = (1, 3, 1) +__version__ = "1.4.2" +__version_info__ = (1, 4, 2) # The verifier module file names are based on the CRC32 of a string that # contains the following version number. It may be older than __version__ diff --git a/cffi/api.py b/cffi/api.py --- a/cffi/api.py +++ b/cffi/api.py @@ -73,6 +73,7 @@ self._included_ffis = [] self._windows_unicode = None self._init_once_cache = {} + self._cdef_version = None self._embedding_init_code = None if hasattr(backend, 'set_ffi'): backend.set_ffi(self) @@ -106,6 +107,7 @@ raise TypeError("cdef() argument must be a string") csource = csource.encode('ascii') with self._lock: + self._cdef_version = object() self._parser.parse(csource, override=override, packed=packed, dllexport=dllexport) self._cdefsources.append(csource) @@ -592,14 +594,15 @@ recompile(self, module_name, source, c_file=filename, call_c_compiler=False, **kwds) - def compile(self, tmpdir='.'): + def compile(self, tmpdir='.', verbose=0): from .recompiler import recompile # if not hasattr(self, '_assigned_source'): raise ValueError("set_source() must be called before compile()") module_name, source, source_extension, kwds = self._assigned_source return recompile(self, module_name, source, tmpdir=tmpdir, - source_extension=source_extension, **kwds) + source_extension=source_extension, + compiler_verbose=verbose, **kwds) def init_once(self, func, tag): # Read _init_once_cache[tag], which is either (False, lock) if @@ -660,70 +663,70 @@ import os backend = ffi._backend backendlib = _load_backend_lib(backend, libname, flags) - copied_enums = [] # - def make_accessor_locked(name): + def accessor_function(name): key = 'function ' + name - if key in ffi._parser._declarations: - tp, _ = ffi._parser._declarations[key] - BType = ffi._get_cached_btype(tp) - try: - value = backendlib.load_function(BType, name) - except KeyError as e: - raise AttributeError('%s: %s' % (name, e)) - library.__dict__[name] = value + tp, _ = ffi._parser._declarations[key] + BType = ffi._get_cached_btype(tp) + try: + value = backendlib.load_function(BType, name) + except KeyError as e: + raise AttributeError('%s: %s' % (name, e)) + library.__dict__[name] = value + # + def accessor_variable(name): + key = 'variable ' + name + tp, _ = ffi._parser._declarations[key] + BType = ffi._get_cached_btype(tp) + read_variable = backendlib.read_variable + write_variable = backendlib.write_variable + setattr(FFILibrary, name, property( + lambda self: read_variable(BType, name), + lambda self, value: write_variable(BType, name, value))) + # + def accessor_constant(name): + raise NotImplementedError("non-integer constant '%s' cannot be " + "accessed from a dlopen() library" % (name,)) + # + def accessor_int_constant(name): + library.__dict__[name] = ffi._parser._int_constants[name] + # + accessors = {} + accessors_version = [False] + # + def update_accessors(): + if accessors_version[0] is ffi._cdef_version: return # - key = 'variable ' + name - if key in ffi._parser._declarations: - tp, _ = ffi._parser._declarations[key] - BType = ffi._get_cached_btype(tp) - read_variable = backendlib.read_variable - write_variable = backendlib.write_variable - setattr(FFILibrary, name, property( - lambda self: read_variable(BType, name), - lambda self, value: write_variable(BType, name, value))) - return - # - if not copied_enums: - from . import model - error = None - for key, (tp, _) in ffi._parser._declarations.items(): - if not isinstance(tp, model.EnumType): - continue - try: - tp.check_not_partial() - except Exception as e: - error = e - continue - for enumname, enumval in zip(tp.enumerators, tp.enumvalues): - if enumname not in library.__dict__: - library.__dict__[enumname] = enumval - if error is not None: - if name in library.__dict__: - return # ignore error, about a different enum - raise error - - for key, val in ffi._parser._int_constants.items(): - if key not in library.__dict__: - library.__dict__[key] = val - - copied_enums.append(True) - if name in library.__dict__: - return - # - key = 'constant ' + name - if key in ffi._parser._declarations: - raise NotImplementedError("fetching a non-integer constant " - "after dlopen()") - # - raise AttributeError(name) + from . import model + for key, (tp, _) in ffi._parser._declarations.items(): + if not isinstance(tp, model.EnumType): + tag, name = key.split(' ', 1) + if tag == 'function': + accessors[name] = accessor_function + elif tag == 'variable': + accessors[name] = accessor_variable + elif tag == 'constant': + accessors[name] = accessor_constant + else: + for i, enumname in enumerate(tp.enumerators): + def accessor_enum(name, tp=tp, i=i): + tp.check_not_partial() + library.__dict__[name] = tp.enumvalues[i] + accessors[enumname] = accessor_enum + for name in ffi._parser._int_constants: + accessors.setdefault(name, accessor_int_constant) + accessors_version[0] = ffi._cdef_version # def make_accessor(name): with ffi._lock: if name in library.__dict__ or name in FFILibrary.__dict__: return # added by another thread while waiting for the lock - make_accessor_locked(name) + if name not in accessors: + update_accessors() + if name not in accessors: + raise AttributeError(name) + accessors[name](name) # class FFILibrary(object): def __getattr__(self, name): @@ -737,6 +740,10 @@ setattr(self, name, value) else: property.__set__(self, value) + def __dir__(self): + with ffi._lock: + update_accessors() + return accessors.keys() # if libname is not None: try: diff --git a/cffi/ffiplatform.py b/cffi/ffiplatform.py --- a/cffi/ffiplatform.py +++ b/cffi/ffiplatform.py @@ -17,15 +17,16 @@ def get_extension(srcfilename, modname, sources=(), **kwds): from distutils.core import Extension allsources = [srcfilename] - allsources.extend(sources) + for src in sources: + allsources.append(os.path.normpath(src)) return Extension(name=modname, sources=allsources, **kwds) -def compile(tmpdir, ext): +def compile(tmpdir, ext, compiler_verbose=0): """Compile a C extension module using distutils.""" saved_environ = os.environ.copy() try: - outputfilename = _build(tmpdir, ext) + outputfilename = _build(tmpdir, ext, compiler_verbose) outputfilename = os.path.abspath(outputfilename) finally: # workaround for a distutils bugs where some env vars can @@ -35,10 +36,10 @@ os.environ[key] = value return outputfilename -def _build(tmpdir, ext): +def _build(tmpdir, ext, compiler_verbose=0): # XXX compact but horrible :-( from distutils.core import Distribution - import distutils.errors + import distutils.errors, distutils.log # dist = Distribution({'ext_modules': [ext]}) dist.parse_config_files() @@ -48,7 +49,12 @@ options['build_temp'] = ('ffiplatform', tmpdir) # try: - dist.run_command('build_ext') + old_level = distutils.log.set_threshold(0) or 0 + try: + distutils.log.set_verbosity(compiler_verbose) + dist.run_command('build_ext') + finally: + distutils.log.set_threshold(old_level) except (distutils.errors.CompileError, distutils.errors.LinkError) as e: raise VerificationError('%s: %s' % (e.__class__.__name__, e)) diff --git a/cffi/recompiler.py b/cffi/recompiler.py --- a/cffi/recompiler.py +++ b/cffi/recompiler.py @@ -1354,7 +1354,8 @@ return os.path.join(outputdir, *parts), parts def recompile(ffi, module_name, preamble, tmpdir='.', call_c_compiler=True, - c_file=None, source_extension='.c', extradir=None, **kwds): + c_file=None, source_extension='.c', extradir=None, + compiler_verbose=1, **kwds): if not isinstance(module_name, str): module_name = module_name.encode('ascii') if ffi._windows_unicode: @@ -1374,7 +1375,7 @@ cwd = os.getcwd() try: os.chdir(tmpdir) - outputfilename = ffiplatform.compile('.', ext) + outputfilename = ffiplatform.compile('.', ext, compiler_verbose) finally: os.chdir(cwd) return outputfilename diff --git a/doc/source/cdef.rst b/doc/source/cdef.rst --- a/doc/source/cdef.rst +++ b/doc/source/cdef.rst @@ -62,7 +62,7 @@ ffi.cdef("C-like declarations with '...'") if __name__ == "__main__": - ffi.compile() + ffi.compile(verbose=True) Running ``python foo_build.py`` produces a file ``_foo.c`` and invokes the C compiler to turn it into a file ``_foo.so`` (or @@ -134,7 +134,8 @@ Similarly, the ``lib`` objects returned by the C version are read-only, apart from writes to global variables. Also, ``lib.__dict__`` does not work before version 1.2 or if ``lib`` happens to declare a name -called ``__dict__`` (use instead ``dir(lib)``). +called ``__dict__`` (use instead ``dir(lib)``). The same is true +for ``lib.__class__`` before version 1.4. ffi.cdef(): declaring types and functions @@ -504,7 +505,8 @@ the mtime to be updated anyway, delete the file before calling the functions. -**ffi.compile(tmpdir='.'):** explicitly generate the .py or .c file, +**ffi.compile(tmpdir='.', verbose=False):** +explicitly generate the .py or .c file, and (if .c) compile it. The output file is (or are) put in the directory given by ``tmpdir``. In the examples given here, we use ``if __name__ == "__main__": ffi.compile()`` in the build scripts---if @@ -513,6 +515,11 @@ to ``set_source()``, then a corresponding subdirectory of the ``tmpdir`` is used.) +*New in version 1.4:* ``verbose`` argument. If True, it prints the +usual distutils output, including the command lines that call the +compiler. (This parameter might be changed to True by default in a +future release.) + **ffi.emit_python_code(filename):** generate the given .py file (same as ``ffi.compile()`` for ABI mode, with an explicitly-named file to write). If you choose, you can include this .py file pre-packaged in diff --git a/doc/source/conf.py b/doc/source/conf.py --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -45,9 +45,9 @@ # built documents. # # The short X.Y version. -version = '1.3' +version = '1.4' # The full version, including alpha/beta/rc tags. -release = '1.3.1' +release = '1.4.2' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/doc/source/installation.rst b/doc/source/installation.rst --- a/doc/source/installation.rst +++ b/doc/source/installation.rst @@ -51,11 +51,11 @@ Download and Installation: -* http://pypi.python.org/packages/source/c/cffi/cffi-1.3.1.tar.gz +* http://pypi.python.org/packages/source/c/cffi/cffi-1.4.2.tar.gz - - MD5: ... + - MD5: 81357fe5042d00650b85b728cc181df2 - - SHA: ... + - SHA: 76cff6f1ff5bfb2b9c6c8e2cfa8bf90b5c944394 * Or grab the most current version from the `Bitbucket page`_: ``hg clone https://bitbucket.org/cffi/cffi`` diff --git a/doc/source/using.rst b/doc/source/using.rst --- a/doc/source/using.rst +++ b/doc/source/using.rst @@ -433,11 +433,11 @@ out-of-line API mode. The next section about Callbacks_ describes the ABI-mode solution. -This is *new in version 1.4.* Use Callbacks_ if backward compatibility -is an issue. (The original callbacks are slower to invoke and have -the same issue as libffi's callbacks; notably, see the warning__. -The new style described in the present section does not use libffi's -callbacks at all.) +This is *new in version 1.4.* Use old-style Callbacks_ if backward +compatibility is an issue. (The original callbacks are slower to +invoke and have the same issue as libffi's callbacks; notably, see the +warning__. The new style described in the present section does not +use libffi's callbacks at all.) .. __: Callbacks_ @@ -459,33 +459,35 @@ from _my_example import ffi, lib @ffi.def_extern() - def my_callback(fooptr, value): + def my_callback(x, y): return 42 -You can get a ``<cdata>`` pointer-to-function object from +You obtain a ``<cdata>`` pointer-to-function object by getting ``lib.my_callback``. This ``<cdata>`` can be passed to C code and then works like a callback: when the C code calls this function pointer, the Python function ``my_callback`` is called. (You need to pass ``lib.my_callback`` to C code, and not ``my_callback``: the -latter is just a plain Python function that cannot be passed to C.) +latter is just the Python function above, which cannot be passed to C.) CFFI implements this by defining ``my_callback`` as a static C function, written after the ``set_source()`` code. The ``<cdata>`` then points to this function. What this function does is invoke the -Python function object that was dynamically attached by +Python function object that is, at runtime, attached with ``@ffi.def_extern()``. -Each function from the cdef with ``extern "Python"`` turns into only -one C function. To support some corner cases, it is possible to -redefine the attached Python function by calling ``@ffi.def_extern()`` -again---but this is not recommended! Better write the Python function -more flexibly in the first place. Calling ``@ffi.def_extern()`` again -changes the C logic to call the new Python function; the old Python -function is not callable any more and the C function pointer you get -from ``lib.my_function`` is always the same. +The ``@ffi.def_extern()`` decorator should be applied to a global +function, once. This is because each function from the cdef with +``extern "Python"`` turns into only one C function. To support some +corner cases, it is possible to redefine the attached Python function +by calling ``@ffi.def_extern()`` again---but this is not recommended! +Better write the single global Python function more flexibly in the +first place. Calling ``@ffi.def_extern()`` again changes the C logic +to call the new Python function; the old Python function is not +callable any more and the C function pointer you get from +``lib.my_function`` is always the same. -Extern "Python" and "void *" arguments -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Extern "Python" and ``void *`` arguments +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ As described just before, you cannot use ``extern "Python"`` to make a variable number of C function pointers. However, achieving that @@ -567,11 +569,13 @@ In case you want to access some ``extern "Python"`` function directly from the C code written in ``set_source()``, you need to write a forward static declaration. The real implementation of this function -is added by CFFI *after* the C code---this is needed because even the +is added by CFFI *after* the C code---this is needed because the declaration might use types defined by ``set_source()`` (e.g. ``event_t`` above, from the ``#include``), so it cannot be generated before. +:: + ffi.set_source("_demo_cffi", """ #include <the_event_library.h> @@ -589,11 +593,12 @@ int my_algo(int); """) ffi.set_source("_example_cffi", """ - static int f(int); + static int f(int); /* the forward declaration */ + static int my_algo(int n) { int i, sum = 0; for (i = 0; i < n; i++) - sum += f(i); + sum += f(i); /* call f() here */ return sum; } """) @@ -705,7 +710,28 @@ keep this object alive for as long as the callback may be invoked. The easiest way to do that is to always use ``@ffi.callback()`` at module-level only, and to pass "context" information around with -`ffi.new_handle()`_, if possible. +`ffi.new_handle()`_, if possible. Example: + +.. code-block:: python + + # a good way to use this decorator is once at global level + @ffi.callback("int(int, void *)") + def my_global_callback(x, handle): + return ffi.from_handle(handle).some_method(x) + + + class Foo(object): + + def __init__(self): + handle = ffi.new_handle(self) + self._handle = handle # must be kept alive + lib.register_stuff_with_callback_and_voidp_arg(my_global_callback, handle) + + def some_method(self, x): + ... + +(See also the section about `extern "Python"`_ above, where the same +general style is used.) Note that callbacks of a variadic function type are not supported. A workaround is to add custom C code. In the following example, a @@ -797,7 +823,7 @@ hurt) to say ``WINAPI`` or ``__stdcall`` when declaring a plain function in the ``cdef()``. (The difference can still be seen if you take explicitly a pointer to this function with ``ffi.addressof()``, -or if the function is ``CFFI_CALL_PYTHON``.) +or if the function is ``extern "Python"``.) These calling convention specifiers are accepted but ignored on any platform other than 32-bit Windows. @@ -1139,6 +1165,58 @@ returned by ``alloc()`` is assumed to be already cleared (or you are fine with garbage); otherwise CFFI will clear it. +.. _initonce: + +**ffi.init_once(function, tag)**: run ``function()`` once. The +``tag`` should be a primitive object, like a string, that identifies +the function: ``function()`` is only called the first time we see the +``tag``. The return value of ``function()`` is remembered and +returned by the current and all future ``init_once()`` with the same +tag. If ``init_once()`` is called from multiple threads in parallel, +all calls block until the execution of ``function()`` is done. If +``function()`` raises an exception, it is propagated and nothing is +cached (i.e. ``function()`` will be called again, in case we catch the +exception and try ``init_once()`` again). *New in version 1.4.* + +Example:: + + from _xyz_cffi import ffi, lib + + def initlib(): + lib.init_my_library() + + def make_new_foo(): + ffi.init_once(initlib, "init") + return lib.make_foo() + +``init_once()`` is optimized to run very quickly if ``function()`` has +already been called. (On PyPy, the cost is zero---the JIT usually +removes everything in the machine code it produces.) + +*Note:* one motivation__ for ``init_once()`` is the CPython notion of +"subinterpreters" in the embedded case. If you are using the +out-of-line API mode, ``function()`` is called only once even in the +presence of multiple subinterpreters, and its return value is shared +among all subinterpreters. The goal is to mimic the way traditional +CPython C extension modules have their init code executed only once in +total even if there are subinterpreters. In the example above, the C +function ``init_my_library()`` is called once in total, not once per +subinterpreter. For this reason, avoid Python-level side-effects in +``function()`` (as they will only be applied in the first +subinterpreter to run); instead, return a value, as in the following +example:: + + def init_get_max(): + return lib.initialize_once_and_get_some_maximum_number() + + def process(i): + if i > ffi.init_once(init_get_max, "max"): + raise IndexError("index too large!") + ... + +.. __: https://bitbucket.org/cffi/cffi/issues/233/ + + .. _`Preparing and Distributing modules`: cdef.html#loading-libraries @@ -1233,8 +1311,8 @@ function with a ``char *`` argument to which you pass a Python string will not actually modify the array of characters passed in, and so passes directly a pointer inside the Python string object. - (PyPy might in the future do the same, but it is harder because a - string object can move in memory when the GC runs.) + (PyPy might in the future do the same, but it is harder because + strings are not naturally zero-terminated in PyPy.) `(**)` C function calls are done with the GIL released. diff --git a/doc/source/whatsnew.rst b/doc/source/whatsnew.rst --- a/doc/source/whatsnew.rst +++ b/doc/source/whatsnew.rst @@ -3,18 +3,61 @@ ====================== +v1.4.2 +====== + +Nothing changed from v1.4.1. + + +v1.4.1 +====== + +* Fix the compilation failure of cffi on CPython 3.5.0. (3.5.1 works; + some detail changed that makes some underscore-starting macros + disappear from view of extension modules, and I worked around it, + thinking it changed in all 3.5 versions---but no: it was only in + 3.5.1.) + + v1.4.0 ====== +* A `better way to do callbacks`__ has been added (faster and more + portable, and usually cleaner). It is a mechanism for the + out-of-line API mode that replaces the dynamic creation of callback + objects (i.e. C functions that invoke Python) with the static + declaration in ``cdef()`` of which callbacks are needed. This is + more C-like, in that you have to structure your code around the idea + that you get a fixed number of function pointers, instead of + creating them on-the-fly. + +* ``ffi.compile()`` now takes an optional ``verbose`` argument. When + ``True``, distutils prints the calls to the compiler. + +* ``ffi.compile()`` used to fail if given ``sources`` with a path that + includes ``".."``. Fixed. + +* ``ffi.init_once()`` added. See docs__. + +* ``dir(lib)`` now works on libs returned by ``ffi.dlopen()`` too. + +* Cleaned up and modernized the content of the ``demo`` subdirectory + in the sources (thanks matti!). + * ``ffi.new_handle()`` is now guaranteed to return unique ``void *`` values, even if called twice on the same object. Previously, in - that case, CPython (but not PyPy) would return two ``cdata`` objects - with the same ``void *`` value. This change is useful to add and - remove handles from a global dict or set without worrying about - duplicates. + that case, CPython would return two ``cdata`` objects with the same + ``void *`` value. This change is useful to add and remove handles + from a global dict (or set) without worrying about duplicates. + It already used to work like that on PyPy. + *This change can break code that used to work on CPython by relying + on the object to be kept alive by other means than keeping the + result of ffi.new_handle() alive.* (The corresponding `warning in + the docs`__ of ``ffi.new_handle()`` has been here since v0.8!) -* ``ffi.init_once()`` XXX - https://bitbucket.org/cffi/cffi/issues/233/ +.. __: using.html#extern-python +.. __: using.html#initonce +.. __: using.html#ffi-new-handle v1.3.1 diff --git a/setup.py b/setup.py --- a/setup.py +++ b/setup.py @@ -144,7 +144,7 @@ `Mailing list <https://groups.google.com/forum/#!forum/python-cffi>`_ """, - version='1.3.1', + version='1.4.2', packages=['cffi'] if cpython else [], package_data={'cffi': ['_cffi_include.h', 'parse_c_type.h']} if cpython else {}, diff --git a/testing/cffi0/backend_tests.py b/testing/cffi0/backend_tests.py --- a/testing/cffi0/backend_tests.py +++ b/testing/cffi0/backend_tests.py @@ -1352,8 +1352,8 @@ ffi = FFI(backend=self.Backend()) ffi.cdef("enum foo;") from cffi import __version_info__ - if __version_info__ < (1, 4): - py.test.skip("re-enable me in version 1.4") + if __version_info__ < (1, 5): + py.test.skip("re-enable me in version 1.5") e = py.test.raises(CDefError, ffi.cast, "enum foo", -1) assert str(e.value) == ( "'enum foo' has no values explicitly defined: refusing to guess " @@ -1826,7 +1826,12 @@ assert seen == [1, 1] def test_init_once_multithread(self): - import thread, time + import sys, time + if sys.version_info < (3,): + import thread + else: + import _thread as thread + # def do_init(): seen.append('init!') time.sleep(1) diff --git a/testing/cffi0/test_function.py b/testing/cffi0/test_function.py --- a/testing/cffi0/test_function.py +++ b/testing/cffi0/test_function.py @@ -464,10 +464,22 @@ ffi = FFI(backend=self.Backend()) ffi.cdef("double __stdcall sin(double x);") # stdcall ignored m = ffi.dlopen(lib_m) - if (sys.platform == 'win32' and sys.maxsize < 2**32 and + if (sys.platform == 'win32' and sys.maxsize < 2**32 and self.Backend is not CTypesBackend): assert "double(__stdcall *)(double)" in str(ffi.typeof(m.sin)) else: assert "double(*)(double)" in str(ffi.typeof(m.sin)) x = m.sin(1.23) assert x == math.sin(1.23) + + def test_dir_on_dlopen_lib(self): + ffi = FFI(backend=self.Backend()) + ffi.cdef(""" + typedef enum { MYE1, MYE2 } myenum_t; + double myfunc(double); + double myvar; + const double myconst; + #define MYFOO 42 + """) + m = ffi.dlopen(lib_m) + assert dir(m) == ['MYE1', 'MYE2', 'MYFOO', 'myconst', 'myfunc', 'myvar'] diff --git a/testing/cffi1/test_ffi_obj.py b/testing/cffi1/test_ffi_obj.py --- a/testing/cffi1/test_ffi_obj.py +++ b/testing/cffi1/test_ffi_obj.py @@ -7,6 +7,7 @@ p = ffi.new("int *") p[0] = -42 assert p[0] == -42 + assert type(ffi) is ffi.__class__ is _cffi1_backend.FFI def test_ffi_subclass(): class FOO(_cffi1_backend.FFI): @@ -16,6 +17,7 @@ assert foo.x == 42 p = foo.new("int *") assert p[0] == 0 + assert type(foo) is foo.__class__ is FOO def test_ffi_no_argument(): py.test.raises(TypeError, _cffi1_backend.FFI, 42) @@ -437,13 +439,18 @@ assert seen == [1, 1] def test_init_once_multithread(): - import thread, time + if sys.version_info < (3,): + import thread + else: + import _thread as thread + import time + # def do_init(): - print 'init!' + print('init!') seen.append('init!') time.sleep(1) seen.append('init done') - print 'init done' + print('init done') return 7 ffi = _cffi1_backend.FFI() seen = [] @@ -454,3 +461,37 @@ thread.start_new_thread(f, ()) time.sleep(1.5) assert seen == ['init!', 'init done'] + 6 * [7] + +def test_init_once_failure(): + def do_init(): + seen.append(1) + raise ValueError + ffi = _cffi1_backend.FFI() + seen = [] + for i in range(5): + py.test.raises(ValueError, ffi.init_once, do_init, "tag") + assert seen == [1] * (i + 1) + +def test_init_once_multithread_failure(): + if sys.version_info < (3,): + import thread + else: + import _thread as thread + import time + def do_init(): + seen.append('init!') + time.sleep(1) + seen.append('oops') + raise ValueError + ffi = _cffi1_backend.FFI() + seen = [] + for i in range(3): + def f(): + py.test.raises(ValueError, ffi.init_once, do_init, "tag") + thread.start_new_thread(f, ()) + i = 0 + while len(seen) < 6: + i += 1 + assert i < 20 + time.sleep(0.51) + assert seen == ['init!', 'oops'] * 3 diff --git a/testing/cffi1/test_recompiler.py b/testing/cffi1/test_recompiler.py --- a/testing/cffi1/test_recompiler.py +++ b/testing/cffi1/test_recompiler.py @@ -3,7 +3,7 @@ from cffi import FFI, VerificationError, FFIError from cffi import recompiler from testing.udir import udir -from testing.support import u +from testing.support import u, long from testing.support import FdWriteCapture, StdErrCapture @@ -948,6 +948,19 @@ """, sources=[str(extra_c_source)]) assert lib.external_foo == 42 +def test_dotdot_in_source_file_names(): + extra_c_source = udir.join( + 'extra_test_dotdot_in_source_file_names.c') + extra_c_source.write('const int external_foo = 42;\n') + ffi = FFI() + ffi.cdef("const int external_foo;") + lib = verify(ffi, 'test_dotdot_in_source_file_names', """ + extern const int external_foo; + """, sources=[os.path.join(os.path.dirname(str(extra_c_source)), + 'foobar', '..', + os.path.basename(str(extra_c_source)))]) + assert lib.external_foo == 42 + def test_call_with_incomplete_structs(): ffi = FFI() ffi.cdef("typedef struct {...;} foo_t; " @@ -1143,6 +1156,7 @@ assert hasattr(lib, '__dict__') assert lib.__all__ == ['MYFOO', 'mybar'] # but not 'myvar' assert lib.__name__ == repr(lib) + assert lib.__class__ is type(lib) def test_macro_var_callback(): ffi = FFI() @@ -1502,8 +1516,8 @@ res = lib.bar(4, 5) assert res == 0 assert f.getvalue() == ( - "extern \"Python\": function bar() called, but no code was attached " - "to it yet with @ffi.def_extern(). Returning 0.\n") + b"extern \"Python\": function bar() called, but no code was attached " + b"to it yet with @ffi.def_extern(). Returning 0.\n") @ffi.def_extern("bar") def my_bar(x, y): @@ -1520,10 +1534,10 @@ baz1 = ffi.def_extern()(baz) assert baz1 is baz seen = [] - baz(40L, 4L) - res = lib.baz(50L, 8L) + baz(long(40), long(4)) + res = lib.baz(long(50), long(8)) assert res is None - assert seen == [("Baz", 40L, 4L), ("Baz", 50, 8)] + assert seen == [("Baz", 40, 4), ("Baz", 50, 8)] assert type(seen[0][1]) is type(seen[0][2]) is long assert type(seen[1][1]) is type(seen[1][2]) is int diff --git a/testing/support.py b/testing/support.py --- a/testing/support.py +++ b/testing/support.py @@ -8,6 +8,7 @@ return eval('u'+repr(other).replace(r'\\u', r'\u') .replace(r'\\U', r'\U')) u = U() + long = long # for further "from testing.support import long" assert u+'a\x00b' == eval(r"u'a\x00b'") assert u+'a\u1234b' == eval(r"u'a\u1234b'") assert u+'a\U00012345b' == eval(r"u'a\U00012345b'") @@ -22,9 +23,12 @@ class StdErrCapture(object): """Capture writes to sys.stderr (not to the underlying file descriptor).""" def __enter__(self): - import StringIO + try: + from StringIO import StringIO + except ImportError: + from io import StringIO self.old_stderr = sys.stderr - sys.stderr = f = StringIO.StringIO() + sys.stderr = f = StringIO() return f def __exit__(self, *args): sys.stderr = self.old_stderr @@ -35,6 +39,9 @@ to the Posix manual.""" def __init__(self, capture_fd=2): # stderr by default + if sys.platform == 'win32': + import py + py.test.skip("seems not to work, too bad") self.capture_fd = capture_fd def __enter__(self): _______________________________________________ pypy-commit mailing list pypy-commit@python.org https://mail.python.org/mailman/listinfo/pypy-commit