from Cython.Compiler.Visitor import VisitorTransform, temp_name_handle, CythonTransform from Cython.Compiler.ModuleNode import ModuleNode from Cython.Compiler.Nodes import * from Cython.Compiler.ExprNodes import * from Cython.Compiler.TreeFragment import TreeFragment from Cython.Utils import EncodedString from Cython.Compiler.Errors import CompileError import PyrexTypes try: set except NameError: from sets import Set as set import textwrap def dedent(text, reindent=0): text = textwrap.dedent(text) if reindent > 0: indent = " " * reindent text = '\n'.join([indent + x for x in text.split('\n')]) return text class IntroduceBufferAuxiliaryVars(CythonTransform): # # Entry point # buffers_exists = False def __call__(self, node): assert isinstance(node, ModuleNode) self.max_ndim = 0 result = super(IntroduceBufferAuxiliaryVars, self).__call__(node) if self.buffers_exists: use_py2_buffer_functions(node.scope) use_empty_bufstruct_code(node.scope, self.max_ndim) node.scope.use_utility_code(access_utility_code) return result # # Basic operations for transforms # def handle_scope(self, node, scope): # For all buffers, insert extra variables in the scope. # The variables are also accessible from the buffer_info # on the buffer entry bufvars = [entry for name, entry in scope.entries.iteritems() if entry.type.is_buffer] if len(bufvars) > 0: self.buffers_exists = True if isinstance(node, ModuleNode) and len(bufvars) > 0: # for now...note that pos is wrong raise CompileError(node.pos, "Buffer vars not allowed in module scope") for entry in bufvars: name = entry.name buftype = entry.type if buftype.ndim > self.max_ndim: self.max_ndim = buftype.ndim # Get or make a type string checker tschecker = buffer_type_checker(buftype.dtype, scope) # Declare auxiliary vars cname = scope.mangle(Naming.bufstruct_prefix, name) bufinfo = scope.declare_var(name="$%s" % cname, cname=cname, type=PyrexTypes.c_py_buffer_type, pos=node.pos) bufinfo.used = True def var(prefix, idx, initval): cname = scope.mangle(prefix, "%d_%s" % (idx, name)) result = scope.declare_var("$%s" % cname, PyrexTypes.c_py_ssize_t_type, node.pos, cname=cname, is_cdef=True) result.init = initval if entry.is_arg: result.used = True return result stridevars = [var(Naming.bufstride_prefix, i, "0") for i in range(entry.type.ndim)] shapevars = [var(Naming.bufshape_prefix, i, "0") for i in range(entry.type.ndim)] entry.buffer_aux = Symtab.BufferAux(bufinfo, stridevars, shapevars, tschecker) mode = entry.type.mode if mode == 'full': suboffsetvars = [var(Naming.bufsuboffset_prefix, i, "-1") for i in range(entry.type.ndim)] entry.buffer_aux.lookup = get_buf_lookup_full(scope, entry.type.ndim) elif mode == 'strided': suboffsetvars = None entry.buffer_aux.lookup = get_buf_lookup_strided(scope, entry.type.ndim) entry.buffer_aux.suboffsetvars = suboffsetvars entry.buffer_aux.get_buffer_cname = tschecker scope.buffer_entries = bufvars self.scope = scope def visit_ModuleNode(self, node): self.handle_scope(node, node.scope) self.visitchildren(node) return node def visit_FuncDefNode(self, node): self.handle_scope(node, node.local_scope) self.visitchildren(node) return node def get_flags(buffer_aux, buffer_type): flags = 'PyBUF_FORMAT' if buffer_type.mode == 'full': flags += '| PyBUF_INDIRECT' elif buffer_type.mode == 'strided': flags += '| PyBUF_STRIDES' else: assert False if buffer_aux.writable_needed: flags += "| PyBUF_WRITABLE" return flags def used_buffer_aux_vars(entry): buffer_aux = entry.buffer_aux buffer_aux.buffer_info_var.used = True for s in buffer_aux.shapevars: s.used = True for s in buffer_aux.stridevars: s.used = True for s in buffer_aux.suboffsetvars: s.used = True def put_unpack_buffer_aux_into_scope(buffer_aux, mode, code): # Generate code to copy the needed struct info into local # variables. bufstruct = buffer_aux.buffer_info_var.cname varspec = [("strides", buffer_aux.stridevars), ("shape", buffer_aux.shapevars)] if mode == 'full': varspec.append(("suboffsets", buffer_aux.suboffsetvars)) for field, vars in varspec: code.putln(" ".join(["%s = %s.%s[%d];" % (s.cname, bufstruct, field, idx) for idx, s in enumerate(vars)])) def getbuffer_cond_code(obj_cname, buffer_aux, flags, ndim): bufstruct = buffer_aux.buffer_info_var.cname return "%s(%s, &%s, %s, %d) == -1" % ( buffer_aux.get_buffer_cname, obj_cname, bufstruct, flags, ndim) def put_acquire_arg_buffer(entry, code, pos): buffer_aux = entry.buffer_aux cname = entry.cname bufstruct = buffer_aux.buffer_info_var.cname flags = get_flags(buffer_aux, entry.type) # Acquire any new buffer code.putln(code.error_goto_if(getbuffer_cond_code(cname, buffer_aux, flags, entry.type.ndim), pos)) # An exception raised in arg parsing cannot be catched, so no # need to do care about the buffer then. put_unpack_buffer_aux_into_scope(buffer_aux, entry.type.mode, code) #def put_release_buffer_normal(entry, code): # code.putln("if (%s != Py_None) PyObject_ReleaseBuffer(%s, &%s);" % ( # entry.cname, # entry.cname, # entry.buffer_aux.buffer_info_var.cname)) def get_release_buffer_code(entry): return "if (%s != Py_None) PyObject_ReleaseBuffer(%s, &%s)" % ( entry.cname, entry.cname, entry.buffer_aux.buffer_info_var.cname) def put_assign_to_buffer(lhs_cname, rhs_cname, buffer_aux, buffer_type, is_initialized, pos, code): """ Generate code for reassigning a buffer variables. This only deals with getting the buffer auxiliary structure and variables set up correctly, the assignment itself and refcounting is the responsibility of the caller. However, the assignment operation may throw an exception so that the reassignment never happens. Depending on the circumstances there are two possible outcomes: - Old buffer released, new acquired, rhs assigned to lhs - Old buffer released, new acquired which fails, reaqcuire old lhs buffer (which may or may not succeed). """ bufstruct = buffer_aux.buffer_info_var.cname flags = get_flags(buffer_aux, buffer_type) getbuffer = "%s(%%s, &%s, %s, %d)" % (buffer_aux.get_buffer_cname, # note: object is filled in later bufstruct, flags, buffer_type.ndim) if is_initialized: # Release any existing buffer code.put('if (%s != Py_None) ' % lhs_cname) code.begin_block(); code.putln('PyObject_ReleaseBuffer(%s, &%s);' % ( lhs_cname, bufstruct)) code.end_block() # Acquire retcode_cname = code.func.allocate_temp(PyrexTypes.c_int_type) code.putln("%s = %s;" % (retcode_cname, getbuffer % rhs_cname)) code.putln('if (%s) ' % (code.unlikely("%s < 0" % retcode_cname))) # If acquisition failed, attempt to reacquire the old buffer # before raising the exception. A failure of reacquisition # will cause the reacquisition exception to be reported, one # can consider working around this later. code.begin_block() type, value, tb = [code.func.allocate_temp(PyrexTypes.py_object_type) for i in range(3)] code.putln('PyErr_Fetch(&%s, &%s, &%s);' % (type, value, tb)) code.put('if (%s) ' % code.unlikely("%s == -1" % (getbuffer % lhs_cname))) code.begin_block() code.putln('Py_XDECREF(%s); Py_XDECREF(%s); Py_XDECREF(%s);' % (type, value, tb)) code.putln('__Pyx_RaiseBufferFallbackError();') code.putln('} else {') code.putln('PyErr_Restore(%s, %s, %s);' % (type, value, tb)) for t in (type, value, tb): code.func.release_temp(t) code.end_block() # Unpack indices code.end_block() put_unpack_buffer_aux_into_scope(buffer_aux, buffer_type.mode, code) code.putln(code.error_goto_if_neg(retcode_cname, pos)) code.func.release_temp(retcode_cname) else: # Our entry had no previous value, so set to None when acquisition fails. # In this case, auxiliary vars should be set up right in initialization to a zero-buffer, # so it suffices to set the buf field to NULL. code.putln('if (%s) {' % code.unlikely("%s == -1" % (getbuffer % rhs_cname))) code.putln('%s = Py_None; Py_INCREF(Py_None); %s.buf = NULL;' % (lhs_cname, bufstruct)) code.putln(code.error_goto(pos)) code.put('} else {') # Unpack indices put_unpack_buffer_aux_into_scope(buffer_aux, buffer_type.mode, code) code.putln('}') def put_access(entry, index_signeds, index_cnames, pos, code): """Returns a c string which can be used to access the buffer for reading or writing. As the bounds checking can have any number of combinations of unsigned arguments, smart optimizations etc. we insert it directly in the function body. The lookup however is delegated to a inline function that is instantiated once per ndim (lookup with suboffsets tend to get quite complicated). """ bufaux = entry.buffer_aux bufstruct = bufaux.buffer_info_var.cname # Check bounds and fix negative indices boundscheck = True nonegs = True tmp_cname = code.func.allocate_temp(PyrexTypes.c_int_type) if boundscheck: code.putln("%s = -1;" % tmp_cname) for idx, (signed, cname, shape) in enumerate(zip(index_signeds, index_cnames, bufaux.shapevars)): if signed != 0: nonegs = False # not unsigned, deal with negative index code.putln("if (%s < 0) {" % cname) code.putln("%s += %s;" % (cname, shape.cname)) if boundscheck: code.putln("if (%s) %s = %d;" % ( code.unlikely("%s < 0" % cname), tmp_cname, idx)) code.put("} else ") else: if idx > 0: code.put("else ") if boundscheck: # check bounds in positive direction code.putln("if (%s) %s = %d;" % ( code.unlikely("%s >= %s" % (cname, shape.cname)), tmp_cname, idx)) if boundscheck: code.put("if (%s) " % code.unlikely("%s != -1" % tmp_cname)) code.begin_block() code.putln('__Pyx_BufferIndexError(%s);' % tmp_cname) code.putln(code.error_goto(pos)) code.end_block() code.func.release_temp(tmp_cname) # Create buffer lookup and return it params = [] if entry.type.mode == 'full': for i, s, o in zip(index_cnames, bufaux.stridevars, bufaux.suboffsetvars): params.append(i) params.append(s.cname) params.append(o.cname) else: for i, s in zip(index_cnames, bufaux.stridevars): params.append(i) params.append(s.cname) ptrcode = "%s(%s.buf, %s)" % (bufaux.lookup, bufstruct, ", ".join(params)) valuecode = "*%s" % entry.type.buffer_ptr_type.cast_code(ptrcode) return valuecode def use_empty_bufstruct_code(env, max_ndim): code = dedent(""" Py_ssize_t __Pyx_zeros[] = {%s}; Py_ssize_t __Pyx_minusones[] = {%s}; """) % (", ".join(["0"] * max_ndim), ", ".join(["-1"] * max_ndim)) env.use_utility_code([code, ""]) def get_buf_lookup_strided(env, nd): """ Generates and registers as utility a buffer lookup function for the right number of dimensions. The function gives back a void* at the right location. """ name = "__Pyx_BufPtrStrided_%dd" % nd if not env.has_utility_code(name): # _i_ndex, _s_tride args = ", ".join(["i%d, s%d" % (i, i) for i in range(nd)]) offset = " + ".join(["i%d * s%d" % (i, i) for i in range(nd)]) proto = dedent("""\ #define %s(buf, %s) ((char*)buf + %s) """) % (name, args, offset) env.use_utility_code([proto, ""], name=name) return name def get_buf_lookup_full(env, nd): """ Generates and registers as utility a buffer lookup function for the right number of dimensions. The function gives back a void* at the right location. """ name = "__Pyx_BufPtrFull_%dd" % nd if not env.has_utility_code(name): # _i_ndex, _s_tride, sub_o_ffset args = ", ".join(["Py_ssize_t i%d, Py_ssize_t s%d, Py_ssize_t o%d" % (i, i, i) for i in range(nd)]) proto = dedent("""\ static INLINE void* %s(void* buf, %s); """) % (name, args) func = dedent(""" static INLINE void* %s(void* buf, %s) { char* ptr = (char*)buf; """) % (name, args) + "".join([dedent("""\ ptr += s%d * i%d; if (o%d >= 0) ptr = *((char**)ptr) + o%d; """) % (i, i, i, i) for i in range(nd)] ) + "\nreturn ptr;\n}" env.use_utility_code([proto, func], name=name) return name # # Utils for creating type string checkers # def mangle_dtype_name(dtype): # Use prefixes to seperate user defined types from builtins # (consider "typedef float unsigned_int") if dtype.typestring is None: prefix = "nn_" else: prefix = "" return prefix + dtype.declaration_code("").replace(" ", "_") def get_ts_check_item(dtype, env): # See if we can consume one (unnamed) dtype as next item # Put native types and structs in seperate namespaces (as one could create a struct named unsigned_int...) name = "__Pyx_BufferTypestringCheck_item_%s" % mangle_dtype_name(dtype) if not env.has_utility_code(name): char = dtype.typestring if char is not None: # Can use direct comparison code = dedent("""\ if (*ts != '%s') { PyErr_Format(PyExc_ValueError, "Buffer datatype mismatch (rejecting on '%%s')", ts); return NULL; } else return ts + 1; """, 2) % char else: # Cannot trust declared size; but rely on int vs float and # signed/unsigned to be correctly declared ctype = dtype.declaration_code("") code = dedent("""\ int ok; switch (*ts) {""", 2) if dtype.is_int: types = [ ('b', 'char'), ('h', 'short'), ('i', 'int'), ('l', 'long'), ('q', 'long long') ] if dtype.signed == 0: code += "".join(["\n case '%s': ok = (sizeof(%s) == sizeof(%s) && (%s)-1 > 0); break;" % (char.upper(), ctype, against, ctype) for char, against in types]) else: code += "".join(["\n case '%s': ok = (sizeof(%s) == sizeof(%s) && (%s)-1 < 0); break;" % (char, ctype, against, ctype) for char, against in types]) code += dedent("""\ default: ok = 0; } if (!ok) { PyErr_Format(PyExc_ValueError, "Buffer datatype mismatch (rejecting on '%s')", ts); return NULL; } else return ts + 1; """, 2) env.use_utility_code([dedent("""\ static const char* %s(const char* ts); /*proto*/ """) % name, dedent(""" static const char* %s(const char* ts) { %s } """) % (name, code)], name=name) return name def get_getbuffer_code(dtype, env): """ Generate a utility function for getting a buffer for the given dtype. The function will: - Call PyObject_GetBuffer - Check that ndim matched the expected value - Check that the format string is right - Set suboffsets to all -1 if it is returned as NULL. """ name = "__Pyx_GetBuffer_%s" % mangle_dtype_name(dtype) if not env.has_utility_code(name): env.use_utility_code(acquire_utility_code) itemchecker = get_ts_check_item(dtype, env) utilcode = [dedent(""" static int %s(PyObject* obj, Py_buffer* buf, int flags, int nd); /*proto*/ """) % name, dedent(""" static int %(name)s(PyObject* obj, Py_buffer* buf, int flags, int nd) { const char* ts; if (obj == Py_None) { __Pyx_ZeroBuffer(buf); return 0; } buf->buf = NULL; if (PyObject_GetBuffer(obj, buf, flags) == -1) goto fail; if (buf->ndim != nd) { __Pyx_BufferNdimError(buf, nd); goto fail; } ts = buf->format; ts = __Pyx_ConsumeWhitespace(ts); ts = __Pyx_BufferTypestringCheckEndian(ts); if (!ts) goto fail; ts = __Pyx_ConsumeWhitespace(ts); ts = %(itemchecker)s(ts); if (!ts) goto fail; ts = __Pyx_ConsumeWhitespace(ts); if (*ts != 0) { PyErr_Format(PyExc_ValueError, "Expected non-struct buffer data type (rejecting on '%%s')", ts); goto fail; } if (buf->suboffsets == NULL) buf->suboffsets = __Pyx_minusones; return 0; fail:; __Pyx_ZeroBuffer(buf); return -1; }""") % locals()] env.use_utility_code(utilcode, name) return name def buffer_type_checker(dtype, env): # Creates a type checker function for the given type. if dtype.is_struct_or_union: assert False elif dtype.is_int or dtype.is_float: # This includes simple typedef-ed types funcname = get_getbuffer_code(dtype, env) else: assert False return funcname def use_py2_buffer_functions(env): # will be refactored try: env.entries[u'numpy'] env.use_utility_code(["",""" static int numpy_getbuffer(PyObject *obj, Py_buffer *view, int flags) { /* This function is always called after a type-check; safe to cast */ PyArrayObject *arr = (PyArrayObject*)obj; PyArray_Descr *type = (PyArray_Descr*)arr->descr; int typenum = PyArray_TYPE(obj); if (!PyTypeNum_ISNUMBER(typenum)) { PyErr_Format(PyExc_TypeError, "Only numeric NumPy types currently supported."); return -1; } /* NumPy format codes doesn't completely match buffer codes; seems safest to retranslate. 01234567890123456789012345*/ const char* base_codes = "?bBhHiIlLqQfdgfdgO"; char* format = (char*)malloc(4); char* fp = format; *fp++ = type->byteorder; if (PyTypeNum_ISCOMPLEX(typenum)) *fp++ = 'Z'; *fp++ = base_codes[typenum]; *fp = 0; view->buf = arr->data; view->readonly = !PyArray_ISWRITEABLE(obj); view->ndim = PyArray_NDIM(arr); view->strides = PyArray_STRIDES(arr); view->shape = PyArray_DIMS(arr); view->suboffsets = NULL; view->format = format; view->itemsize = type->elsize; view->internal = 0; return 0; } static void numpy_releasebuffer(PyObject *obj, Py_buffer *view) { free((char*)view->format); view->format = NULL; } """]) except KeyError: pass codename = "PyObject_GetBuffer" # just a representative unique key # Search all types for __getbuffer__ overloads types = [] def find_buffer_types(scope): for m in scope.cimported_modules: find_buffer_types(m) for e in scope.type_entries: t = e.type if t.is_extension_type: release = get = None for x in t.scope.pyfunc_entries: if x.name == u"__getbuffer__": get = x.func_cname elif x.name == u"__releasebuffer__": release = x.func_cname if get: types.append((t.typeptr_cname, get, release)) find_buffer_types(env) # For now, hard-code numpy imported as "numpy" try: ndarrtype = env.entries[u'numpy'].as_module.entries['ndarray'].type types.append((ndarrtype.typeptr_cname, "numpy_getbuffer", "numpy_releasebuffer")) except KeyError: pass code = dedent(""" #if PY_VERSION_HEX < 0x02060000 static int PyObject_GetBuffer(PyObject *obj, Py_buffer *view, int flags) { """) if len(types) > 0: clause = "if" for t, get, release in types: code += " %s (PyObject_TypeCheck(obj, %s)) return %s(obj, view, flags);\n" % (clause, t, get) clause = "else if" code += " else {\n" code += dedent("""\ PyErr_Format(PyExc_TypeError, "'%100s' does not have the buffer interface", Py_TYPE(obj)->tp_name); return -1; """, 2) if len(types) > 0: code += " }" code += dedent(""" } static void PyObject_ReleaseBuffer(PyObject *obj, Py_buffer *view) { """) if len(types) > 0: clause = "if" for t, get, release in types: if release: code += "%s (PyObject_TypeCheck(obj, %s)) %s(obj, view);" % (clause, t, release) clause = "else if" code += dedent(""" } #endif """) env.use_utility_code([dedent("""\ #if PY_VERSION_HEX < 0x02060000 static int PyObject_GetBuffer(PyObject *obj, Py_buffer *view, int flags); static void PyObject_ReleaseBuffer(PyObject *obj, Py_buffer *view); #endif """) ,code], codename) # # Static utility code # # Utility function to set the right exception # The caller should immediately goto_error access_utility_code = [ """\ static void __Pyx_BufferIndexError(int axis); /*proto*/ ""","""\ static void __Pyx_BufferIndexError(int axis) { PyErr_Format(PyExc_IndexError, "Out of bounds on buffer access (axis %d)", axis); } """] # # Buffer type checking. Utility code for checking that acquired # buffers match our assumptions. We only need to check ndim and # the format string; the access mode/flags is checked by the # exporter. # acquire_utility_code = ["""\ static INLINE void __Pyx_ZeroBuffer(Py_buffer* buf); /*proto*/ static INLINE const char* __Pyx_ConsumeWhitespace(const char* ts); /*proto*/ static INLINE const char* __Pyx_BufferTypestringCheckEndian(const char* ts); /*proto*/ static void __Pyx_BufferNdimError(Py_buffer* buffer, int expected_ndim); /*proto*/ static void __Pyx_RaiseBufferFallbackError(void); /*proto*/ """, """ static INLINE void __Pyx_ZeroBuffer(Py_buffer* buf) { buf->buf = NULL; buf->strides = __Pyx_zeros; buf->shape = __Pyx_zeros; buf->suboffsets = __Pyx_minusones; } static INLINE const char* __Pyx_ConsumeWhitespace(const char* ts) { while (1) { switch (*ts) { case 10: case 13: case ' ': ++ts; default: return ts; } } } static INLINE const char* __Pyx_BufferTypestringCheckEndian(const char* ts) { int num = 1; int little_endian = ((char*)&num)[0]; int ok = 1; switch (*ts) { case '@': case '=': ++ts; break; case '<': if (little_endian) ++ts; else ok = 0; break; case '>': case '!': if (!little_endian) ++ts; else ok = 0; break; } if (!ok) { PyErr_Format(PyExc_ValueError, "Buffer has wrong endianness (rejecting on '%s')", ts); return NULL; } return ts; } static void __Pyx_BufferNdimError(Py_buffer* buffer, int expected_ndim) { PyErr_Format(PyExc_ValueError, "Buffer has wrong number of dimensions (expected %d, got %d)", expected_ndim, buffer->ndim); } static void __Pyx_RaiseBufferFallbackError(void) { PyErr_Format(PyExc_ValueError, "Buffer acquisition failed on assignment; and then reacquiring the old buffer failed too!"); } """]