| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979 |
- """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 __future__ import annotations
- from typing import Sequence
- from mypy.nodes import ARG_NAMED, ARG_NAMED_OPT, ARG_OPT, ARG_POS, ARG_STAR, ARG_STAR2, ArgKind
- from mypy.operators import op_methods_to_symbols, reverse_op_method_names, reverse_op_methods
- from mypyc.codegen.emit import AssignHandler, Emitter, ErrorHandler, GotoHandler, ReturnHandler
- from mypyc.common import (
- BITMAP_BITS,
- BITMAP_TYPE,
- DUNDER_PREFIX,
- NATIVE_PREFIX,
- PREFIX,
- bitmap_name,
- use_vectorcall,
- )
- from mypyc.ir.class_ir import ClassIR
- from mypyc.ir.func_ir import FUNC_STATICMETHOD, FuncIR, RuntimeArg
- from mypyc.ir.rtypes import (
- RInstance,
- RType,
- is_bool_rprimitive,
- is_int_rprimitive,
- is_object_rprimitive,
- object_rprimitive,
- )
- 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(f'"{arg.name}", ' for arg in args)
- return f"static const char * const kwlist[] = {{{arg_names}0}};"
- def make_format_string(func_name: str | None, 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 += f":{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(f"{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.sig.num_bitmap_args:
- real_args = real_args[: -fn.sig.num_bitmap_args]
- if fn.class_name and not fn.decl.kind == FUNC_STATICMETHOD:
- arg = real_args.pop(0)
- emitter.emit_line(f"PyObject *obj_{arg.name} = self;")
- # 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(f'static CPyArg_Parser parser = {{"{fmt}", kwlist, 0}};')
- for arg in real_args:
- emitter.emit_line(
- "PyObject *obj_{}{};".format(arg.name, " = NULL" if arg.optional else "")
- )
- cleanups = [f"CPy_DECREF(obj_{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 += [f"&obj_{groups[ARG_STAR][0].name}" if groups[ARG_STAR] else "NULL"]
- arg_ptrs += [f"&obj_{groups[ARG_STAR2][0].name}" if groups[ARG_STAR2] else "NULL"]
- arg_ptrs += [f"&obj_{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 not real_args:
- # 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;",
- "}",
- )
- for i in range(fn.sig.num_bitmap_args):
- name = bitmap_name(i)
- emitter.emit_line(f"{BITMAP_TYPE} {name} = 0;")
- 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(f"{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.sig.num_bitmap_args:
- real_args = real_args[: -fn.sig.num_bitmap_args]
- if fn.class_name and not fn.decl.kind == FUNC_STATICMETHOD:
- arg = real_args.pop(0)
- emitter.emit_line(f"PyObject *obj_{arg.name} = self;")
- # 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 = [f"CPy_DECREF(obj_{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 += [f"&obj_{groups[ARG_STAR][0].name}" if groups[ARG_STAR] else "NULL"]
- arg_ptrs += [f"&obj_{groups[ARG_STAR2][0].name}" if groups[ARG_STAR2] else "NULL"]
- arg_ptrs += [f"&obj_{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;",
- "}",
- )
- for i in range(fn.sig.num_bitmap_args):
- name = bitmap_name(i)
- emitter.emit_line(f"{BITMAP_TYPE} {name} = 0;")
- 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_ipow_wrapper(cl: ClassIR, fn: FuncIR, emitter: Emitter) -> str:
- """Generate a wrapper for native __ipow__.
- Since __ipow__ fills a ternary slot, but almost no one defines __ipow__ to take three
- arguments, the wrapper needs to tweaked to force it to accept three arguments.
- """
- gen = WrapperGenerator(cl, emitter)
- gen.set_target(fn)
- assert len(fn.args) in (2, 3), "__ipow__ should only take 2 or 3 arguments"
- gen.arg_names = ["self", "exp", "mod"]
- gen.emit_header()
- gen.emit_arg_processing()
- handle_third_pow_argument(
- fn,
- emitter,
- gen,
- if_unsupported=[
- 'PyErr_SetString(PyExc_TypeError, "__ipow__ takes 2 positional arguments but 3 were given");',
- "return NULL;",
- ],
- )
- 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)
- if fn.name in ("__pow__", "__rpow__"):
- gen.arg_names = ["left", "right", "mod"]
- else:
- 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(fn, 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)
- handle_third_pow_argument(fn, emitter, gen, if_unsupported=["goto typefail;"])
- 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
- # ...
- generate_bin_op_reverse_dunder_call(fn, emitter, reverse_op_methods[fn.name])
- gen.finish()
- def generate_bin_op_reverse_only_wrapper(
- fn: FuncIR, emitter: Emitter, gen: WrapperGenerator
- ) -> None:
- gen.arg_names = ["right", "left"]
- gen.emit_arg_processing(error=GotoHandler("typefail"), raise_exception=False)
- handle_third_pow_argument(fn, emitter, gen, if_unsupported=["goto typefail;"])
- 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)
- handle_third_pow_argument(fn, emitter, gen, if_unsupported=["goto typefail2;"])
- # Ternary __rpow__ calls aren't a thing so immediately bail
- # if ternary __pow__ returns NotImplemented.
- if fn.name == "__pow__" and len(fn.args) == 3:
- fwd_not_implemented_handler = "goto typefail2;"
- else:
- fwd_not_implemented_handler = "goto typefail;"
- gen.emit_call(not_implemented_handler=fwd_not_implemented_handler)
- 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)
- handle_third_pow_argument(fn_rev, emitter, gen, if_unsupported=["goto typefail2;"])
- gen.emit_call()
- gen.emit_error_handling()
- emitter.emit_line("} else {")
- generate_bin_op_reverse_dunder_call(fn, emitter, 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()
- def generate_bin_op_reverse_dunder_call(fn: FuncIR, emitter: Emitter, rmethod: str) -> None:
- if fn.name in ("__pow__", "__rpow__"):
- # Ternary pow() will never call the reverse dunder.
- emitter.emit_line("if (obj_mod == Py_None) {")
- emitter.emit_line(f"_Py_IDENTIFIER({rmethod});")
- emitter.emit_line(
- 'return CPy_CallReverseOpMethod(obj_left, obj_right, "{}", &PyId_{});'.format(
- op_methods_to_symbols[fn.name], rmethod
- )
- )
- if fn.name in ("__pow__", "__rpow__"):
- emitter.emit_line("} else {")
- emitter.emit_line("Py_INCREF(Py_NotImplemented);")
- emitter.emit_line("return Py_NotImplemented;")
- emitter.emit_line("}")
- def handle_third_pow_argument(
- fn: FuncIR, emitter: Emitter, gen: WrapperGenerator, *, if_unsupported: list[str]
- ) -> None:
- if fn.name not in ("__pow__", "__rpow__", "__ipow__"):
- return
- if (fn.name in ("__pow__", "__ipow__") and len(fn.args) == 2) or fn.name == "__rpow__":
- # If the power dunder only supports two arguments and the third
- # argument (AKA mod) is set to a non-default value, simply bail.
- #
- # Importantly, this prevents any ternary __rpow__ calls from
- # happening (as per the language specification).
- emitter.emit_line("if (obj_mod != Py_None) {")
- for line in if_unsupported:
- emitter.emit_line(line)
- emitter.emit_line("}")
- # The slot wrapper will receive three arguments, but the call only
- # supports two so make sure that the third argument isn't passed
- # along. This is needed as two-argument __(i)pow__ is allowed and
- # rather common.
- if len(gen.arg_names) == 3:
- gen.arg_names.pop()
- 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) -> str | None:
- """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 = f"{DUNDER_PREFIX}_RichCompare_{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(f"case {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 = f"{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(f"return {NATIVE_PREFIX}{fn.cname(emitter.names)}(self, instance, owner);")
- emitter.emit_line("}")
- return name
- def generate_hash_wrapper(cl: ClassIR, fn: FuncIR, emitter: Emitter) -> str:
- """Generates a wrapper for native __hash__ methods."""
- name = f"{DUNDER_PREFIX}{fn.name}{cl.name_prefix(emitter.names)}"
- emitter.emit_line(f"static Py_ssize_t {name}(PyObject *self) {{")
- 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 = f"{DUNDER_PREFIX}{fn.name}{cl.name_prefix(emitter.names)}"
- emitter.emit_line(f"static Py_ssize_t {name}(PyObject *self) {{")
- 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 = f"{DUNDER_PREFIX}{fn.name}{cl.name_prefix(emitter.names)}"
- emitter.emit_line(f"static int {name}(PyObject *self) {{")
- 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(f"PyObject *obj_{arg.name}" for arg in fn.args)
- emitter.emit_line(f"static int {name}({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(f"PyObject *obj_{arg.name}" for arg in args)
- emitter.emit_line(f"static int {name}({input_args}) {{")
- # First check if this is __delitem__
- emitter.emit_line(f"if (obj_{args[2].name} == NULL) {{")
- if del_name is not None:
- # We have a native implementation, so call it
- emitter.emit_line(f"return {del_name}(obj_{args[0].name}, obj_{args[1].name});")
- else:
- # Try to call superclass method instead
- emitter.emit_line(f"PyObject *super = CPy_Super(CPyModule_builtins, obj_{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(f"PyObject *super = CPy_Super(CPyModule_builtins, obj_{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 = f"'{cl.name}' object does not support item assignment"
- emitter.emit_line(f'PyErr_SetString(PyExc_TypeError, "{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(f"arg_{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 = f"{DUNDER_PREFIX}{fn.name}{cl.name_prefix(emitter.names)}"
- emitter.emit_line(f"static int {name}(PyObject *self, PyObject *obj_item) {{")
- 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: list[RuntimeArg] | None = None,
- arg_names: list[str] | None = None,
- cleanups: list[str] | None = None,
- traceback_code: str | None = 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)
- if arg_names:
- gen.arg_names = arg_names
- 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: ErrorHandler | None = None,
- *,
- optional: bool = False,
- raise_exception: bool = True,
- bitmap_arg_index: int = 0,
- ) -> 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:
- if typ.error_overlap and optional:
- # Update bitmap is value is provided.
- init = emitter.c_undefined_value(typ)
- emitter.emit_line(f"{emitter.ctype(typ)} arg_{name} = {init};")
- emitter.emit_line(f"if (obj_{name} != NULL) {{")
- bitmap = bitmap_name(bitmap_arg_index // BITMAP_BITS)
- emitter.emit_line(f"{bitmap} |= 1 << {bitmap_arg_index & (BITMAP_BITS - 1)};")
- emitter.emit_unbox(
- f"obj_{name}",
- f"arg_{name}",
- typ,
- declare_dest=False,
- raise_exception=raise_exception,
- error=error,
- borrow=True,
- )
- emitter.emit_line("}")
- else:
- # Borrow when unboxing to avoid reference count manipulation.
- emitter.emit_unbox(
- f"obj_{name}",
- f"arg_{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(f"PyObject *arg_{name};")
- emitter.emit_line(f"if (obj_{name} == NULL) {{")
- emitter.emit_line(f"arg_{name} = {emitter.c_error_value(typ)};")
- emitter.emit_lines("} else {", f"arg_{name} = obj_{name}; ", "}")
- else:
- emitter.emit_line(f"PyObject *arg_{name} = obj_{name};")
- else:
- emitter.emit_cast(
- f"obj_{name}",
- f"arg_{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: ClassIR | None, 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.num_bitmap_args = fn.sig.num_bitmap_args
- if self.num_bitmap_args:
- self.args = fn.args[: -self.num_bitmap_args]
- else:
- self.args = fn.args
- self.arg_names = [arg.name for arg in self.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(f"PyObject *obj_{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: ErrorHandler | None = None, raise_exception: bool = True
- ) -> None:
- """Emit validation and unboxing of arguments."""
- error = error or self.error()
- bitmap_arg_index = 0
- 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
- optional = arg in self.optional_args
- generate_arg_check(
- arg_name,
- typ,
- self.emitter,
- error,
- raise_exception=raise_exception,
- optional=optional,
- bitmap_arg_index=bitmap_arg_index,
- )
- if optional and typ.error_overlap:
- bitmap_arg_index += 1
- 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(f"arg_{arg}" for arg in self.arg_names)
- if self.num_bitmap_args:
- bitmap_args = ", ".join(
- [bitmap_name(i) for i in reversed(range(self.num_bitmap_args))]
- )
- native_args = f"{native_args}, {bitmap_args}"
- 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(f"return {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("}")
|