| """Generate CPython API wrapper functions for native functions. |
| |
| The wrapper functions are used by the CPython runtime when calling |
| native functions from interpreted code, and when the called function |
| can't be determined statically in compiled code. They validate, match, |
| unbox and type check function arguments, and box return values as |
| needed. All wrappers accept and return 'PyObject *' (boxed) values. |
| |
| The wrappers aren't used for most calls between two native functions |
| or methods in a single compilation unit. |
| """ |
| |
| from typing import List, Dict, Optional, Sequence |
| |
| from mypy.nodes import ArgKind, ARG_POS, ARG_OPT, ARG_NAMED_OPT, ARG_NAMED, ARG_STAR, ARG_STAR2 |
| from mypy.operators import op_methods_to_symbols, reverse_op_methods, reverse_op_method_names |
| |
| from mypyc.common import PREFIX, NATIVE_PREFIX, DUNDER_PREFIX, use_vectorcall |
| from mypyc.codegen.emit import Emitter, ErrorHandler, GotoHandler, AssignHandler, ReturnHandler |
| from mypyc.ir.rtypes import ( |
| RType, RInstance, is_object_rprimitive, is_int_rprimitive, is_bool_rprimitive, |
| object_rprimitive |
| ) |
| from mypyc.ir.func_ir import FuncIR, RuntimeArg, FUNC_STATICMETHOD |
| from mypyc.ir.class_ir import ClassIR |
| from mypyc.namegen import NameGenerator |
| |
| |
| # Generic vectorcall wrapper functions (Python 3.7+) |
| # |
| # A wrapper function has a signature like this: |
| # |
| # PyObject *fn(PyObject *self, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames) |
| # |
| # The function takes a self object, pointer to an array of arguments, |
| # the number of positional arguments, and a tuple of keyword argument |
| # names (that are stored starting in args[nargs]). |
| # |
| # It returns the returned object, or NULL on an exception. |
| # |
| # These are more efficient than legacy wrapper functions, since |
| # usually no tuple or dict objects need to be created for the |
| # arguments. Vectorcalls also use pre-constructed str objects for |
| # keyword argument names and other pre-computed information, instead |
| # of processing the argument format string on each call. |
| |
| |
| def wrapper_function_header(fn: FuncIR, names: NameGenerator) -> str: |
| """Return header of a vectorcall wrapper function. |
| |
| See comment above for a summary of the arguments. |
| """ |
| return ( |
| 'PyObject *{prefix}{name}(' |
| 'PyObject *self, PyObject *const *args, size_t nargs, PyObject *kwnames)').format( |
| prefix=PREFIX, |
| name=fn.cname(names)) |
| |
| |
| def generate_traceback_code(fn: FuncIR, |
| emitter: Emitter, |
| source_path: str, |
| module_name: str) -> str: |
| # If we hit an error while processing arguments, then we emit a |
| # traceback frame to make it possible to debug where it happened. |
| # Unlike traceback frames added for exceptions seen in IR, we do this |
| # even if there is no `traceback_name`. This is because the error will |
| # have originated here and so we need it in the traceback. |
| globals_static = emitter.static_name('globals', module_name) |
| traceback_code = 'CPy_AddTraceback("%s", "%s", %d, %s);' % ( |
| source_path.replace("\\", "\\\\"), |
| fn.traceback_name or fn.name, |
| fn.line, |
| globals_static) |
| return traceback_code |
| |
| |
| def make_arg_groups(args: List[RuntimeArg]) -> Dict[ArgKind, List[RuntimeArg]]: |
| """Group arguments by kind.""" |
| return {k: [arg for arg in args if arg.kind == k] for k in ArgKind} |
| |
| |
| def reorder_arg_groups(groups: Dict[ArgKind, List[RuntimeArg]]) -> List[RuntimeArg]: |
| """Reorder argument groups to match their order in a format string.""" |
| return groups[ARG_POS] + groups[ARG_OPT] + groups[ARG_NAMED_OPT] + groups[ARG_NAMED] |
| |
| |
| def make_static_kwlist(args: List[RuntimeArg]) -> str: |
| arg_names = ''.join('"{}", '.format(arg.name) for arg in args) |
| return 'static const char * const kwlist[] = {{{}0}};'.format(arg_names) |
| |
| |
| def make_format_string(func_name: Optional[str], groups: Dict[ArgKind, List[RuntimeArg]]) -> str: |
| """Return a format string that specifies the accepted arguments. |
| |
| The format string is an extended subset of what is supported by |
| PyArg_ParseTupleAndKeywords(). Only the type 'O' is used, and we |
| also support some extensions: |
| |
| - Required keyword-only arguments are introduced after '@' |
| - If the function receives *args or **kwargs, we add a '%' prefix |
| |
| Each group requires the previous groups' delimiters to be present |
| first. |
| |
| These are used by both vectorcall and legacy wrapper functions. |
| """ |
| format = '' |
| if groups[ARG_STAR] or groups[ARG_STAR2]: |
| format += '%' |
| format += 'O' * len(groups[ARG_POS]) |
| if groups[ARG_OPT] or groups[ARG_NAMED_OPT] or groups[ARG_NAMED]: |
| format += '|' + 'O' * len(groups[ARG_OPT]) |
| if groups[ARG_NAMED_OPT] or groups[ARG_NAMED]: |
| format += '$' + 'O' * len(groups[ARG_NAMED_OPT]) |
| if groups[ARG_NAMED]: |
| format += '@' + 'O' * len(groups[ARG_NAMED]) |
| if func_name is not None: |
| format += ':{}'.format(func_name) |
| return format |
| |
| |
| def generate_wrapper_function(fn: FuncIR, |
| emitter: Emitter, |
| source_path: str, |
| module_name: str) -> None: |
| """Generate a CPython-compatible vectorcall wrapper for a native function. |
| |
| In particular, this handles unboxing the arguments, calling the native function, and |
| then boxing the return value. |
| """ |
| emitter.emit_line('{} {{'.format(wrapper_function_header(fn, emitter.names))) |
| |
| # If fn is a method, then the first argument is a self param |
| real_args = list(fn.args) |
| if fn.class_name and not fn.decl.kind == FUNC_STATICMETHOD: |
| arg = real_args.pop(0) |
| emitter.emit_line('PyObject *obj_{} = self;'.format(arg.name)) |
| |
| # Need to order args as: required, optional, kwonly optional, kwonly required |
| # This is because CPyArg_ParseStackAndKeywords format string requires |
| # them grouped in that way. |
| groups = make_arg_groups(real_args) |
| reordered_args = reorder_arg_groups(groups) |
| |
| emitter.emit_line(make_static_kwlist(reordered_args)) |
| fmt = make_format_string(fn.name, groups) |
| # Define the arguments the function accepts (but no types yet) |
| emitter.emit_line('static CPyArg_Parser parser = {{"{}", kwlist, 0}};'.format(fmt)) |
| |
| for arg in real_args: |
| emitter.emit_line('PyObject *obj_{}{};'.format( |
| arg.name, ' = NULL' if arg.optional else '')) |
| |
| cleanups = ['CPy_DECREF(obj_{});'.format(arg.name) |
| for arg in groups[ARG_STAR] + groups[ARG_STAR2]] |
| |
| arg_ptrs: List[str] = [] |
| if groups[ARG_STAR] or groups[ARG_STAR2]: |
| arg_ptrs += ['&obj_{}'.format(groups[ARG_STAR][0].name) if groups[ARG_STAR] else 'NULL'] |
| arg_ptrs += ['&obj_{}'.format(groups[ARG_STAR2][0].name) if groups[ARG_STAR2] else 'NULL'] |
| arg_ptrs += ['&obj_{}'.format(arg.name) for arg in reordered_args] |
| |
| if fn.name == '__call__' and use_vectorcall(emitter.capi_version): |
| nargs = 'PyVectorcall_NARGS(nargs)' |
| else: |
| nargs = 'nargs' |
| parse_fn = 'CPyArg_ParseStackAndKeywords' |
| # Special case some common signatures |
| if len(real_args) == 0: |
| # No args |
| parse_fn = 'CPyArg_ParseStackAndKeywordsNoArgs' |
| elif len(real_args) == 1 and len(groups[ARG_POS]) == 1: |
| # Single positional arg |
| parse_fn = 'CPyArg_ParseStackAndKeywordsOneArg' |
| elif len(real_args) == len(groups[ARG_POS]) + len(groups[ARG_OPT]): |
| # No keyword-only args, *args or **kwargs |
| parse_fn = 'CPyArg_ParseStackAndKeywordsSimple' |
| emitter.emit_lines( |
| 'if (!{}(args, {}, kwnames, &parser{})) {{'.format( |
| parse_fn, nargs, ''.join(', ' + n for n in arg_ptrs)), |
| 'return NULL;', |
| '}') |
| traceback_code = generate_traceback_code(fn, emitter, source_path, module_name) |
| generate_wrapper_core(fn, emitter, groups[ARG_OPT] + groups[ARG_NAMED_OPT], |
| cleanups=cleanups, |
| traceback_code=traceback_code) |
| |
| emitter.emit_line('}') |
| |
| |
| # Legacy generic wrapper functions |
| # |
| # These take a self object, a Python tuple of positional arguments, |
| # and a dict of keyword arguments. These are a lot slower than |
| # vectorcall wrappers, especially in calls involving keyword |
| # arguments. |
| |
| |
| def legacy_wrapper_function_header(fn: FuncIR, names: NameGenerator) -> str: |
| return 'PyObject *{prefix}{name}(PyObject *self, PyObject *args, PyObject *kw)'.format( |
| prefix=PREFIX, |
| name=fn.cname(names)) |
| |
| |
| def generate_legacy_wrapper_function(fn: FuncIR, |
| emitter: Emitter, |
| source_path: str, |
| module_name: str) -> None: |
| """Generates a CPython-compatible legacy wrapper for a native function. |
| |
| In particular, this handles unboxing the arguments, calling the native function, and |
| then boxing the return value. |
| """ |
| emitter.emit_line('{} {{'.format(legacy_wrapper_function_header(fn, emitter.names))) |
| |
| # If fn is a method, then the first argument is a self param |
| real_args = list(fn.args) |
| if fn.class_name and not fn.decl.kind == FUNC_STATICMETHOD: |
| arg = real_args.pop(0) |
| emitter.emit_line('PyObject *obj_{} = self;'.format(arg.name)) |
| |
| # Need to order args as: required, optional, kwonly optional, kwonly required |
| # This is because CPyArg_ParseTupleAndKeywords format string requires |
| # them grouped in that way. |
| groups = make_arg_groups(real_args) |
| reordered_args = reorder_arg_groups(groups) |
| |
| emitter.emit_line(make_static_kwlist(reordered_args)) |
| for arg in real_args: |
| emitter.emit_line('PyObject *obj_{}{};'.format( |
| arg.name, ' = NULL' if arg.optional else '')) |
| |
| cleanups = ['CPy_DECREF(obj_{});'.format(arg.name) |
| for arg in groups[ARG_STAR] + groups[ARG_STAR2]] |
| |
| arg_ptrs: List[str] = [] |
| if groups[ARG_STAR] or groups[ARG_STAR2]: |
| arg_ptrs += ['&obj_{}'.format(groups[ARG_STAR][0].name) if groups[ARG_STAR] else 'NULL'] |
| arg_ptrs += ['&obj_{}'.format(groups[ARG_STAR2][0].name) if groups[ARG_STAR2] else 'NULL'] |
| arg_ptrs += ['&obj_{}'.format(arg.name) for arg in reordered_args] |
| |
| emitter.emit_lines( |
| 'if (!CPyArg_ParseTupleAndKeywords(args, kw, "{}", "{}", kwlist{})) {{'.format( |
| make_format_string(None, groups), fn.name, ''.join(', ' + n for n in arg_ptrs)), |
| 'return NULL;', |
| '}') |
| traceback_code = generate_traceback_code(fn, emitter, source_path, module_name) |
| generate_wrapper_core(fn, emitter, groups[ARG_OPT] + groups[ARG_NAMED_OPT], |
| cleanups=cleanups, |
| traceback_code=traceback_code) |
| |
| emitter.emit_line('}') |
| |
| |
| # Specialized wrapper functions |
| |
| |
| def generate_dunder_wrapper(cl: ClassIR, fn: FuncIR, emitter: Emitter) -> str: |
| """Generates a wrapper for native __dunder__ methods to be able to fit into the mapping |
| protocol slot. This specifically means that the arguments are taken as *PyObjects and returned |
| as *PyObjects. |
| """ |
| gen = WrapperGenerator(cl, emitter) |
| gen.set_target(fn) |
| gen.emit_header() |
| gen.emit_arg_processing() |
| gen.emit_call() |
| gen.finish() |
| return gen.wrapper_name() |
| |
| |
| def generate_bin_op_wrapper(cl: ClassIR, fn: FuncIR, emitter: Emitter) -> str: |
| """Generates a wrapper for a native binary dunder method. |
| |
| The same wrapper that handles the forward method (e.g. __add__) also handles |
| the corresponding reverse method (e.g. __radd__), if defined. |
| |
| Both arguments and the return value are PyObject *. |
| """ |
| gen = WrapperGenerator(cl, emitter) |
| gen.set_target(fn) |
| gen.arg_names = ['left', 'right'] |
| wrapper_name = gen.wrapper_name() |
| |
| gen.emit_header() |
| if fn.name not in reverse_op_methods and fn.name in reverse_op_method_names: |
| # There's only a reverse operator method. |
| generate_bin_op_reverse_only_wrapper(emitter, gen) |
| else: |
| rmethod = reverse_op_methods[fn.name] |
| fn_rev = cl.get_method(rmethod) |
| if fn_rev is None: |
| # There's only a forward operator method. |
| generate_bin_op_forward_only_wrapper(fn, emitter, gen) |
| else: |
| # There's both a forward and a reverse operator method. |
| generate_bin_op_both_wrappers(cl, fn, fn_rev, emitter, gen) |
| return wrapper_name |
| |
| |
| def generate_bin_op_forward_only_wrapper(fn: FuncIR, |
| emitter: Emitter, |
| gen: 'WrapperGenerator') -> None: |
| gen.emit_arg_processing(error=GotoHandler('typefail'), raise_exception=False) |
| gen.emit_call(not_implemented_handler='goto typefail;') |
| gen.emit_error_handling() |
| emitter.emit_label('typefail') |
| # If some argument has an incompatible type, treat this the same as |
| # returning NotImplemented, and try to call the reverse operator method. |
| # |
| # Note that in normal Python you'd instead of an explicit |
| # return of NotImplemented, but it doesn't generally work here |
| # the body won't be executed at all if there is an argument |
| # type check failure. |
| # |
| # The recommended way is to still use a type check in the |
| # body. This will only be used in interpreted mode: |
| # |
| # def __add__(self, other: int) -> Foo: |
| # if not isinstance(other, int): |
| # return NotImplemented |
| # ... |
| rmethod = reverse_op_methods[fn.name] |
| emitter.emit_line('_Py_IDENTIFIER({});'.format(rmethod)) |
| emitter.emit_line( |
| 'return CPy_CallReverseOpMethod(obj_left, obj_right, "{}", &PyId_{});'.format( |
| op_methods_to_symbols[fn.name], |
| rmethod)) |
| gen.finish() |
| |
| |
| def generate_bin_op_reverse_only_wrapper(emitter: Emitter, |
| gen: 'WrapperGenerator') -> None: |
| gen.arg_names = ['right', 'left'] |
| gen.emit_arg_processing(error=GotoHandler('typefail'), raise_exception=False) |
| gen.emit_call() |
| gen.emit_error_handling() |
| emitter.emit_label('typefail') |
| emitter.emit_line('Py_INCREF(Py_NotImplemented);') |
| emitter.emit_line('return Py_NotImplemented;') |
| gen.finish() |
| |
| |
| def generate_bin_op_both_wrappers(cl: ClassIR, |
| fn: FuncIR, |
| fn_rev: FuncIR, |
| emitter: Emitter, |
| gen: 'WrapperGenerator') -> None: |
| # There's both a forward and a reverse operator method. First |
| # check if we should try calling the forward one. If the |
| # argument type check fails, fall back to the reverse method. |
| # |
| # Similar to above, we can't perfectly match Python semantics. |
| # In regular Python code you'd return NotImplemented if the |
| # operand has the wrong type, but in compiled code we'll never |
| # get to execute the type check. |
| emitter.emit_line('if (PyObject_IsInstance(obj_left, (PyObject *){})) {{'.format( |
| emitter.type_struct_name(cl))) |
| gen.emit_arg_processing(error=GotoHandler('typefail'), raise_exception=False) |
| gen.emit_call(not_implemented_handler='goto typefail;') |
| gen.emit_error_handling() |
| emitter.emit_line('}') |
| emitter.emit_label('typefail') |
| emitter.emit_line('if (PyObject_IsInstance(obj_right, (PyObject *){})) {{'.format( |
| emitter.type_struct_name(cl))) |
| gen.set_target(fn_rev) |
| gen.arg_names = ['right', 'left'] |
| gen.emit_arg_processing(error=GotoHandler('typefail2'), raise_exception=False) |
| gen.emit_call() |
| gen.emit_error_handling() |
| emitter.emit_line('} else {') |
| emitter.emit_line('_Py_IDENTIFIER({});'.format(fn_rev.name)) |
| emitter.emit_line( |
| 'return CPy_CallReverseOpMethod(obj_left, obj_right, "{}", &PyId_{});'.format( |
| op_methods_to_symbols[fn.name], |
| fn_rev.name)) |
| emitter.emit_line('}') |
| emitter.emit_label('typefail2') |
| emitter.emit_line('Py_INCREF(Py_NotImplemented);') |
| emitter.emit_line('return Py_NotImplemented;') |
| gen.finish() |
| |
| |
| RICHCOMPARE_OPS = { |
| '__lt__': 'Py_LT', |
| '__gt__': 'Py_GT', |
| '__le__': 'Py_LE', |
| '__ge__': 'Py_GE', |
| '__eq__': 'Py_EQ', |
| '__ne__': 'Py_NE', |
| } |
| |
| |
| def generate_richcompare_wrapper(cl: ClassIR, emitter: Emitter) -> Optional[str]: |
| """Generates a wrapper for richcompare dunder methods.""" |
| # Sort for determinism on Python 3.5 |
| matches = sorted([name for name in RICHCOMPARE_OPS if cl.has_method(name)]) |
| if not matches: |
| return None |
| |
| name = '{}_RichCompare_{}'.format(DUNDER_PREFIX, cl.name_prefix(emitter.names)) |
| emitter.emit_line( |
| 'static PyObject *{name}(PyObject *obj_lhs, PyObject *obj_rhs, int op) {{'.format( |
| name=name) |
| ) |
| emitter.emit_line('switch (op) {') |
| for func in matches: |
| emitter.emit_line('case {}: {{'.format(RICHCOMPARE_OPS[func])) |
| method = cl.get_method(func) |
| assert method is not None |
| generate_wrapper_core(method, emitter, arg_names=['lhs', 'rhs']) |
| emitter.emit_line('}') |
| emitter.emit_line('}') |
| |
| emitter.emit_line('Py_INCREF(Py_NotImplemented);') |
| emitter.emit_line('return Py_NotImplemented;') |
| |
| emitter.emit_line('}') |
| |
| return name |
| |
| |
| def generate_get_wrapper(cl: ClassIR, fn: FuncIR, emitter: Emitter) -> str: |
| """Generates a wrapper for native __get__ methods.""" |
| name = '{}{}{}'.format(DUNDER_PREFIX, fn.name, cl.name_prefix(emitter.names)) |
| emitter.emit_line( |
| 'static PyObject *{name}(PyObject *self, PyObject *instance, PyObject *owner) {{'. |
| format(name=name)) |
| emitter.emit_line('instance = instance ? instance : Py_None;') |
| emitter.emit_line('return {}{}(self, instance, owner);'.format( |
| NATIVE_PREFIX, |
| fn.cname(emitter.names))) |
| emitter.emit_line('}') |
| |
| return name |
| |
| |
| def generate_hash_wrapper(cl: ClassIR, fn: FuncIR, emitter: Emitter) -> str: |
| """Generates a wrapper for native __hash__ methods.""" |
| name = '{}{}{}'.format(DUNDER_PREFIX, fn.name, cl.name_prefix(emitter.names)) |
| emitter.emit_line('static Py_ssize_t {name}(PyObject *self) {{'.format( |
| name=name |
| )) |
| emitter.emit_line('{}retval = {}{}{}(self);'.format(emitter.ctype_spaced(fn.ret_type), |
| emitter.get_group_prefix(fn.decl), |
| NATIVE_PREFIX, |
| fn.cname(emitter.names))) |
| emitter.emit_error_check('retval', fn.ret_type, 'return -1;') |
| if is_int_rprimitive(fn.ret_type): |
| emitter.emit_line('Py_ssize_t val = CPyTagged_AsSsize_t(retval);') |
| else: |
| emitter.emit_line('Py_ssize_t val = PyLong_AsSsize_t(retval);') |
| emitter.emit_dec_ref('retval', fn.ret_type) |
| emitter.emit_line('if (PyErr_Occurred()) return -1;') |
| # We can't return -1 from a hash function.. |
| emitter.emit_line('if (val == -1) return -2;') |
| emitter.emit_line('return val;') |
| emitter.emit_line('}') |
| |
| return name |
| |
| |
| def generate_len_wrapper(cl: ClassIR, fn: FuncIR, emitter: Emitter) -> str: |
| """Generates a wrapper for native __len__ methods.""" |
| name = '{}{}{}'.format(DUNDER_PREFIX, fn.name, cl.name_prefix(emitter.names)) |
| emitter.emit_line('static Py_ssize_t {name}(PyObject *self) {{'.format( |
| name=name |
| )) |
| emitter.emit_line('{}retval = {}{}{}(self);'.format(emitter.ctype_spaced(fn.ret_type), |
| emitter.get_group_prefix(fn.decl), |
| NATIVE_PREFIX, |
| fn.cname(emitter.names))) |
| emitter.emit_error_check('retval', fn.ret_type, 'return -1;') |
| if is_int_rprimitive(fn.ret_type): |
| emitter.emit_line('Py_ssize_t val = CPyTagged_AsSsize_t(retval);') |
| else: |
| emitter.emit_line('Py_ssize_t val = PyLong_AsSsize_t(retval);') |
| emitter.emit_dec_ref('retval', fn.ret_type) |
| emitter.emit_line('if (PyErr_Occurred()) return -1;') |
| emitter.emit_line('return val;') |
| emitter.emit_line('}') |
| |
| return name |
| |
| |
| def generate_bool_wrapper(cl: ClassIR, fn: FuncIR, emitter: Emitter) -> str: |
| """Generates a wrapper for native __bool__ methods.""" |
| name = '{}{}{}'.format(DUNDER_PREFIX, fn.name, cl.name_prefix(emitter.names)) |
| emitter.emit_line('static int {name}(PyObject *self) {{'.format( |
| name=name |
| )) |
| emitter.emit_line('{}val = {}{}(self);'.format(emitter.ctype_spaced(fn.ret_type), |
| NATIVE_PREFIX, |
| fn.cname(emitter.names))) |
| emitter.emit_error_check('val', fn.ret_type, 'return -1;') |
| # This wouldn't be that hard to fix but it seems unimportant and |
| # getting error handling and unboxing right would be fiddly. (And |
| # way easier to do in IR!) |
| assert is_bool_rprimitive(fn.ret_type), "Only bool return supported for __bool__" |
| emitter.emit_line('return val;') |
| emitter.emit_line('}') |
| |
| return name |
| |
| |
| def generate_del_item_wrapper(cl: ClassIR, fn: FuncIR, emitter: Emitter) -> str: |
| """Generates a wrapper for native __delitem__. |
| |
| This is only called from a combined __delitem__/__setitem__ wrapper. |
| """ |
| name = '{}{}{}'.format(DUNDER_PREFIX, '__delitem__', cl.name_prefix(emitter.names)) |
| input_args = ', '.join('PyObject *obj_{}'.format(arg.name) for arg in fn.args) |
| emitter.emit_line('static int {name}({input_args}) {{'.format( |
| name=name, |
| input_args=input_args, |
| )) |
| generate_set_del_item_wrapper_inner(fn, emitter, fn.args) |
| return name |
| |
| |
| def generate_set_del_item_wrapper(cl: ClassIR, fn: FuncIR, emitter: Emitter) -> str: |
| """Generates a wrapper for native __setitem__ method (also works for __delitem__). |
| |
| This is used with the mapping protocol slot. Arguments are taken as *PyObjects and we |
| return a negative C int on error. |
| |
| Create a separate wrapper function for __delitem__ as needed and have the |
| __setitem__ wrapper call it if the value is NULL. Return the name |
| of the outer (__setitem__) wrapper. |
| """ |
| method_cls = cl.get_method_and_class('__delitem__') |
| del_name = None |
| if method_cls and method_cls[1] == cl: |
| # Generate a separate wrapper for __delitem__ |
| del_name = generate_del_item_wrapper(cl, method_cls[0], emitter) |
| |
| args = fn.args |
| if fn.name == '__delitem__': |
| # Add an extra argument for value that we expect to be NULL. |
| args = list(args) + [RuntimeArg('___value', object_rprimitive, ARG_POS)] |
| |
| name = '{}{}{}'.format(DUNDER_PREFIX, '__setitem__', cl.name_prefix(emitter.names)) |
| input_args = ', '.join('PyObject *obj_{}'.format(arg.name) for arg in args) |
| emitter.emit_line('static int {name}({input_args}) {{'.format( |
| name=name, |
| input_args=input_args, |
| )) |
| |
| # First check if this is __delitem__ |
| emitter.emit_line('if (obj_{} == NULL) {{'.format(args[2].name)) |
| if del_name is not None: |
| # We have a native implementation, so call it |
| emitter.emit_line('return {}(obj_{}, obj_{});'.format(del_name, |
| args[0].name, |
| args[1].name)) |
| else: |
| # Try to call superclass method instead |
| emitter.emit_line( |
| 'PyObject *super = CPy_Super(CPyModule_builtins, obj_{});'.format(args[0].name)) |
| emitter.emit_line('if (super == NULL) return -1;') |
| emitter.emit_line( |
| 'PyObject *result = PyObject_CallMethod(super, "__delitem__", "O", obj_{});'.format( |
| args[1].name)) |
| emitter.emit_line('Py_DECREF(super);') |
| emitter.emit_line('Py_XDECREF(result);') |
| emitter.emit_line('return result == NULL ? -1 : 0;') |
| emitter.emit_line('}') |
| |
| method_cls = cl.get_method_and_class('__setitem__') |
| if method_cls and method_cls[1] == cl: |
| generate_set_del_item_wrapper_inner(fn, emitter, args) |
| else: |
| emitter.emit_line( |
| 'PyObject *super = CPy_Super(CPyModule_builtins, obj_{});'.format(args[0].name)) |
| emitter.emit_line('if (super == NULL) return -1;') |
| emitter.emit_line('PyObject *result;') |
| |
| if method_cls is None and cl.builtin_base is None: |
| msg = "'{}' object does not support item assignment".format(cl.name) |
| emitter.emit_line( |
| 'PyErr_SetString(PyExc_TypeError, "{}");'.format(msg)) |
| emitter.emit_line('result = NULL;') |
| else: |
| # A base class may have __setitem__ |
| emitter.emit_line( |
| 'result = PyObject_CallMethod(super, "__setitem__", "OO", obj_{}, obj_{});'.format( |
| args[1].name, args[2].name)) |
| emitter.emit_line('Py_DECREF(super);') |
| emitter.emit_line('Py_XDECREF(result);') |
| emitter.emit_line('return result == NULL ? -1 : 0;') |
| emitter.emit_line('}') |
| return name |
| |
| |
| def generate_set_del_item_wrapper_inner(fn: FuncIR, emitter: Emitter, |
| args: Sequence[RuntimeArg]) -> None: |
| for arg in args: |
| generate_arg_check(arg.name, arg.type, emitter, GotoHandler('fail')) |
| native_args = ', '.join('arg_{}'.format(arg.name) for arg in args) |
| emitter.emit_line('{}val = {}{}({});'.format(emitter.ctype_spaced(fn.ret_type), |
| NATIVE_PREFIX, |
| fn.cname(emitter.names), |
| native_args)) |
| emitter.emit_error_check('val', fn.ret_type, 'goto fail;') |
| emitter.emit_dec_ref('val', fn.ret_type) |
| emitter.emit_line('return 0;') |
| emitter.emit_label('fail') |
| emitter.emit_line('return -1;') |
| emitter.emit_line('}') |
| |
| |
| def generate_contains_wrapper(cl: ClassIR, fn: FuncIR, emitter: Emitter) -> str: |
| """Generates a wrapper for a native __contains__ method.""" |
| name = '{}{}{}'.format(DUNDER_PREFIX, fn.name, cl.name_prefix(emitter.names)) |
| emitter.emit_line( |
| 'static int {name}(PyObject *self, PyObject *obj_item) {{'. |
| format(name=name)) |
| generate_arg_check('item', fn.args[1].type, emitter, ReturnHandler('-1')) |
| emitter.emit_line('{}val = {}{}(self, arg_item);'.format(emitter.ctype_spaced(fn.ret_type), |
| NATIVE_PREFIX, |
| fn.cname(emitter.names))) |
| emitter.emit_error_check('val', fn.ret_type, 'return -1;') |
| if is_bool_rprimitive(fn.ret_type): |
| emitter.emit_line('return val;') |
| else: |
| emitter.emit_line('int boolval = PyObject_IsTrue(val);') |
| emitter.emit_dec_ref('val', fn.ret_type) |
| emitter.emit_line('return boolval;') |
| emitter.emit_line('}') |
| |
| return name |
| |
| |
| # Helpers |
| |
| |
| def generate_wrapper_core(fn: FuncIR, |
| emitter: Emitter, |
| optional_args: Optional[List[RuntimeArg]] = None, |
| arg_names: Optional[List[str]] = None, |
| cleanups: Optional[List[str]] = None, |
| traceback_code: Optional[str] = None) -> None: |
| """Generates the core part of a wrapper function for a native function. |
| |
| This expects each argument as a PyObject * named obj_{arg} as a precondition. |
| It converts the PyObject *s to the necessary types, checking and unboxing if necessary, |
| makes the call, then boxes the result if necessary and returns it. |
| """ |
| gen = WrapperGenerator(None, emitter) |
| gen.set_target(fn) |
| gen.arg_names = arg_names or [arg.name for arg in fn.args] |
| gen.cleanups = cleanups or [] |
| gen.optional_args = optional_args or [] |
| gen.traceback_code = traceback_code or '' |
| |
| error = ReturnHandler('NULL') if not gen.use_goto() else GotoHandler('fail') |
| gen.emit_arg_processing(error=error) |
| gen.emit_call() |
| gen.emit_error_handling() |
| |
| |
| def generate_arg_check(name: str, |
| typ: RType, |
| emitter: Emitter, |
| error: Optional[ErrorHandler] = None, |
| *, |
| optional: bool = False, |
| raise_exception: bool = True) -> None: |
| """Insert a runtime check for argument and unbox if necessary. |
| |
| The object is named PyObject *obj_{}. This is expected to generate |
| a value of name arg_{} (unboxed if necessary). For each primitive a runtime |
| check ensures the correct type. |
| """ |
| error = error or AssignHandler() |
| if typ.is_unboxed: |
| # Borrow when unboxing to avoid reference count manipulation. |
| emitter.emit_unbox('obj_{}'.format(name), |
| 'arg_{}'.format(name), |
| typ, |
| declare_dest=True, |
| raise_exception=raise_exception, |
| error=error, |
| borrow=True, |
| optional=optional) |
| elif is_object_rprimitive(typ): |
| # Object is trivial since any object is valid |
| if optional: |
| emitter.emit_line('PyObject *arg_{};'.format(name)) |
| emitter.emit_line('if (obj_{} == NULL) {{'.format(name)) |
| emitter.emit_line('arg_{} = {};'.format(name, emitter.c_error_value(typ))) |
| emitter.emit_lines('} else {', 'arg_{} = obj_{}; '.format(name, name), '}') |
| else: |
| emitter.emit_line('PyObject *arg_{} = obj_{};'.format(name, name)) |
| else: |
| emitter.emit_cast('obj_{}'.format(name), |
| 'arg_{}'.format(name), |
| typ, |
| declare_dest=True, |
| raise_exception=raise_exception, |
| error=error, |
| optional=optional) |
| |
| |
| class WrapperGenerator: |
| """Helper that simplifies the generation of wrapper functions.""" |
| |
| # TODO: Use this for more wrappers |
| |
| def __init__(self, cl: Optional[ClassIR], emitter: Emitter) -> None: |
| self.cl = cl |
| self.emitter = emitter |
| self.cleanups: List[str] = [] |
| self.optional_args: List[RuntimeArg] = [] |
| self.traceback_code = '' |
| |
| def set_target(self, fn: FuncIR) -> None: |
| """Set the wrapped function. |
| |
| It's fine to modify the attributes initialized here later to customize |
| the wrapper function. |
| """ |
| self.target_name = fn.name |
| self.target_cname = fn.cname(self.emitter.names) |
| self.arg_names = [arg.name for arg in fn.args] |
| self.args = fn.args[:] |
| self.ret_type = fn.ret_type |
| |
| def wrapper_name(self) -> str: |
| """Return the name of the wrapper function.""" |
| return '{}{}{}'.format(DUNDER_PREFIX, |
| self.target_name, |
| self.cl.name_prefix(self.emitter.names) if self.cl else '') |
| |
| def use_goto(self) -> bool: |
| """Do we use a goto for error handling (instead of straight return)?""" |
| return bool(self.cleanups or self.traceback_code) |
| |
| def emit_header(self) -> None: |
| """Emit the function header of the wrapper implementation.""" |
| input_args = ', '.join('PyObject *obj_{}'.format(arg) for arg in self.arg_names) |
| self.emitter.emit_line('static PyObject *{name}({input_args}) {{'.format( |
| name=self.wrapper_name(), |
| input_args=input_args, |
| )) |
| |
| def emit_arg_processing(self, |
| error: Optional[ErrorHandler] = None, |
| raise_exception: bool = True) -> None: |
| """Emit validation and unboxing of arguments.""" |
| error = error or self.error() |
| for arg_name, arg in zip(self.arg_names, self.args): |
| # Suppress the argument check for *args/**kwargs, since we know it must be right. |
| typ = arg.type if arg.kind not in (ARG_STAR, ARG_STAR2) else object_rprimitive |
| generate_arg_check(arg_name, |
| typ, |
| self.emitter, |
| error, |
| raise_exception=raise_exception, |
| optional=arg in self.optional_args) |
| |
| def emit_call(self, not_implemented_handler: str = '') -> None: |
| """Emit call to the wrapper function. |
| |
| If not_implemented_handler is non-empty, use this C code to handle |
| a NotImplemented return value (if it's possible based on the return type). |
| """ |
| native_args = ', '.join('arg_{}'.format(arg) for arg in self.arg_names) |
| ret_type = self.ret_type |
| emitter = self.emitter |
| if ret_type.is_unboxed or self.use_goto(): |
| # TODO: The Py_RETURN macros return the correct PyObject * with reference count |
| # handling. Are they relevant? |
| emitter.emit_line('{}retval = {}{}({});'.format(emitter.ctype_spaced(ret_type), |
| NATIVE_PREFIX, |
| self.target_cname, |
| native_args)) |
| emitter.emit_lines(*self.cleanups) |
| if ret_type.is_unboxed: |
| emitter.emit_error_check('retval', ret_type, 'return NULL;') |
| emitter.emit_box('retval', 'retbox', ret_type, declare_dest=True) |
| |
| emitter.emit_line( |
| 'return {};'.format('retbox' if ret_type.is_unboxed else 'retval')) |
| else: |
| if not_implemented_handler and not isinstance(ret_type, RInstance): |
| # The return value type may overlap with NotImplemented. |
| emitter.emit_line('PyObject *retbox = {}{}({});'.format(NATIVE_PREFIX, |
| self.target_cname, |
| native_args)) |
| emitter.emit_lines('if (retbox == Py_NotImplemented) {', |
| not_implemented_handler, |
| '}', |
| 'return retbox;') |
| else: |
| emitter.emit_line('return {}{}({});'.format(NATIVE_PREFIX, |
| self.target_cname, |
| native_args)) |
| # TODO: Tracebacks? |
| |
| def error(self) -> ErrorHandler: |
| """Figure out how to deal with errors in the wrapper.""" |
| if self.cleanups or self.traceback_code: |
| # We'll have a label at the end with error handling code. |
| return GotoHandler('fail') |
| else: |
| # Nothing special needs to done to handle errors, so just return. |
| return ReturnHandler('NULL') |
| |
| def emit_error_handling(self) -> None: |
| """Emit error handling block at the end of the wrapper, if needed.""" |
| emitter = self.emitter |
| if self.use_goto(): |
| emitter.emit_label('fail') |
| emitter.emit_lines(*self.cleanups) |
| if self.traceback_code: |
| emitter.emit_line(self.traceback_code) |
| emitter.emit_line('return NULL;') |
| |
| def finish(self) -> None: |
| self.emitter.emit_line('}') |