Skip to content

Commit 81eaa5d

Browse files
authored
[mypyc] Wrap async functions with function-like type (#20260)
Currently, introspection functions from the `inspect` module like `iscoroutinefunction(fn)` don't return the expected result for functions compiled with mypyc. mypyc defines the functions using `PyMethodDef` in C for which cpython makes objects with type `PyCFunction_Type` and not `PyFunction_Type`. The latter corresponds to `types.FunctionType` in python. So the `isfunction(fn)` [check](https://github.com/python/cpython/blob/3.14/Lib/inspect.py#L267) fails. `iscoroutinefunction(fn)` and some others accept [function-like objects](https://github.com/python/cpython/blob/3.14/Lib/inspect.py#L2070) as long as they are callable and have some required attributes to support compiled functions. The most important attribute is `__code__` which is a code object that stores flags for the function. For example for `iscoroutinefunction(fn)`, the `CO_COROUTINE` flag is [checked](https://github.com/python/cpython/blob/3.14/Lib/inspect.py#L329). In this PR, mypyc adds a wrapper `CPyFunction` whose `__call__` calls the same function that would be defined with `PyMethodDef`. The wrapper is added to `__dict__` of either the module (for functions) or the containing class (for methods) with the original name of the function so the wrapper is returned in place of the `PyCFunction_Type` objects. For nested functions, mypyc already creates a callable class so the function-like attributes are added as properties. A static `CPyFunction` object is created separately for each class that is used in the getter and setter methods of the callable class. For now `CPyFunction`s are created only for async functions to support `iscoroutinefunction(fn)` but the implementation could be extended to support other introspection functions in future PRs. For non-async functions the wrapper does not seem necessary for now because the default behavior makes the function always return false. So the behavior is as-expected for non-async functions. This implementation was inspired by [Cython](https://github.com/cython/cython/blob/master/Cython/Utility/CythonFunction.c) which generates much richer function wrappers.
1 parent 7936e7e commit 81eaa5d

File tree

16 files changed

+837
-126
lines changed

16 files changed

+837
-126
lines changed

mypyc/codegen/emit.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,22 @@
77
import textwrap
88
from typing import Callable, Final
99

10+
from mypyc.codegen.cstring import c_string_initializer
1011
from mypyc.codegen.literals import Literals
1112
from mypyc.common import (
1213
ATTR_PREFIX,
1314
BITMAP_BITS,
1415
FAST_ISINSTANCE_MAX_SUBCLASSES,
1516
HAVE_IMMORTAL,
17+
MODULE_PREFIX,
1618
NATIVE_PREFIX,
19+
PREFIX,
1720
REG_PREFIX,
1821
STATIC_PREFIX,
1922
TYPE_PREFIX,
2023
)
2124
from mypyc.ir.class_ir import ClassIR, all_concrete_classes
22-
from mypyc.ir.func_ir import FuncDecl
25+
from mypyc.ir.func_ir import FuncDecl, FuncIR, get_text_signature
2326
from mypyc.ir.ops import BasicBlock, Value
2427
from mypyc.ir.rtypes import (
2528
RInstance,
@@ -169,13 +172,15 @@ def __init__(
169172
context: EmitterContext,
170173
value_names: dict[Value, str] | None = None,
171174
capi_version: tuple[int, int] | None = None,
175+
filepath: str | None = None,
172176
) -> None:
173177
self.context = context
174178
self.capi_version = capi_version or sys.version_info[:2]
175179
self.names = context.names
176180
self.value_names = value_names or {}
177181
self.fragments: list[str] = []
178182
self._indent = 0
183+
self.filepath = filepath
179184

180185
# Low-level operations
181186

@@ -1207,6 +1212,24 @@ def emit_unbox_failure_with_overlapping_error_value(
12071212
self.emit_line(failure)
12081213
self.emit_line("}")
12091214

1215+
def emit_cpyfunction_instance(
1216+
self, fn: FuncIR, name: str, filepath: str, error_stmt: str
1217+
) -> str:
1218+
module = self.static_name(fn.decl.module_name, None, prefix=MODULE_PREFIX)
1219+
cname = f"{PREFIX}{fn.cname(self.names)}"
1220+
wrapper_name = f"{cname}_wrapper"
1221+
cfunc = f"(PyCFunction){cname}"
1222+
func_flags = "METH_FASTCALL | METH_KEYWORDS"
1223+
doc = f"PyDoc_STR({native_function_doc_initializer(fn)})"
1224+
1225+
code_flags = "CO_COROUTINE"
1226+
self.emit_line(
1227+
f'PyObject* {wrapper_name} = CPyFunction_New({module}, "{filepath}", "{name}", {cfunc}, {func_flags}, {doc}, {fn.line}, {code_flags});'
1228+
)
1229+
self.emit_line(f"if (unlikely(!{wrapper_name}))")
1230+
self.emit_line(error_stmt)
1231+
return wrapper_name
1232+
12101233

12111234
def c_array_initializer(components: list[str], *, indented: bool = False) -> str:
12121235
"""Construct an initializer for a C array variable.
@@ -1238,3 +1261,11 @@ def c_array_initializer(components: list[str], *, indented: bool = False) -> str
12381261
# Multi-line result
12391262
res.append(indent + ", ".join(current))
12401263
return "{\n " + ",\n ".join(res) + "\n" + indent + "}"
1264+
1265+
1266+
def native_function_doc_initializer(func: FuncIR) -> str:
1267+
text_sig = get_text_signature(func)
1268+
if text_sig is None:
1269+
return "NULL"
1270+
docstring = f"{text_sig}\n--\n\n"
1271+
return c_string_initializer(docstring.encode("ascii", errors="backslashreplace"))

mypyc/codegen/emitclass.py

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,13 @@
77

88
from mypy.nodes import ARG_STAR, ARG_STAR2
99
from mypyc.codegen.cstring import c_string_initializer
10-
from mypyc.codegen.emit import Emitter, HeaderDeclaration, ReturnHandler
11-
from mypyc.codegen.emitfunc import native_function_doc_initializer, native_function_header
10+
from mypyc.codegen.emit import (
11+
Emitter,
12+
HeaderDeclaration,
13+
ReturnHandler,
14+
native_function_doc_initializer,
15+
)
16+
from mypyc.codegen.emitfunc import native_function_header
1217
from mypyc.codegen.emitwrapper import (
1318
generate_bin_op_wrapper,
1419
generate_bool_wrapper,
@@ -21,7 +26,14 @@
2126
generate_richcompare_wrapper,
2227
generate_set_del_item_wrapper,
2328
)
24-
from mypyc.common import BITMAP_BITS, BITMAP_TYPE, NATIVE_PREFIX, PREFIX, REG_PREFIX
29+
from mypyc.common import (
30+
BITMAP_BITS,
31+
BITMAP_TYPE,
32+
NATIVE_PREFIX,
33+
PREFIX,
34+
REG_PREFIX,
35+
short_id_from_name,
36+
)
2537
from mypyc.ir.class_ir import ClassIR, VTableEntries
2638
from mypyc.ir.func_ir import (
2739
FUNC_CLASSMETHOD,
@@ -240,6 +252,7 @@ def generate_class(cl: ClassIR, module: str, emitter: Emitter) -> None:
240252
dealloc_name = f"{name_prefix}_dealloc"
241253
methods_name = f"{name_prefix}_methods"
242254
vtable_setup_name = f"{name_prefix}_trait_vtable_setup"
255+
coroutine_setup_name = f"{name_prefix}_coroutine_setup"
243256

244257
fields: dict[str, str] = {"tp_name": f'"{name}"'}
245258

@@ -347,6 +360,8 @@ def emit_line() -> None:
347360
shadow_vtable_name = None
348361
vtable_name = generate_vtables(cl, vtable_setup_name, vtable_name, emitter, shadow=False)
349362
emit_line()
363+
generate_coroutine_setup(cl, coroutine_setup_name, module, emitter)
364+
emit_line()
350365
if del_method:
351366
generate_finalize_for_class(del_method, finalize_name, emitter)
352367
emit_line()
@@ -391,6 +406,10 @@ def emit_line() -> None:
391406
)
392407
)
393408

409+
if cl.coroutine_name:
410+
cpyfunction = emitter.static_name(cl.name + "_cpyfunction", module)
411+
emitter.emit_line(f"static PyObject *{cpyfunction} = NULL;")
412+
394413
emitter.emit_line()
395414
if generate_full:
396415
generate_setup_for_class(cl, defaults_fn, vtable_name, shadow_vtable_name, emitter)
@@ -1254,3 +1273,36 @@ def native_class_doc_initializer(cl: ClassIR) -> str:
12541273
text_sig = f"{cl.name}()"
12551274
docstring = f"{text_sig}\n--\n\n"
12561275
return c_string_initializer(docstring.encode("ascii", errors="backslashreplace"))
1276+
1277+
1278+
def generate_coroutine_setup(
1279+
cl: ClassIR, coroutine_setup_name: str, module_name: str, emitter: Emitter
1280+
) -> None:
1281+
emitter.emit_line("static bool")
1282+
emitter.emit_line(f"{NATIVE_PREFIX}{coroutine_setup_name}(PyObject *type)")
1283+
emitter.emit_line("{")
1284+
1285+
if not any(fn.decl.is_coroutine for fn in cl.methods.values()):
1286+
emitter.emit_line("return 1;")
1287+
emitter.emit_line("}")
1288+
return
1289+
1290+
emitter.emit_line("PyTypeObject *tp = (PyTypeObject *)type;")
1291+
1292+
for fn in cl.methods.values():
1293+
if not fn.decl.is_coroutine:
1294+
continue
1295+
1296+
filepath = emitter.filepath or ""
1297+
error_stmt = " return 2;"
1298+
name = short_id_from_name(fn.name, fn.decl.shortname, fn.line)
1299+
wrapper_name = emitter.emit_cpyfunction_instance(fn, name, filepath, error_stmt)
1300+
name_obj = f"{wrapper_name}_name"
1301+
emitter.emit_line(f'PyObject *{name_obj} = PyUnicode_FromString("{fn.name}");')
1302+
emitter.emit_line(f"if (unlikely(!{name_obj}))")
1303+
emitter.emit_line(error_stmt)
1304+
emitter.emit_line(f"if (PyDict_SetItem(tp->tp_dict, {name_obj}, {wrapper_name}) < 0)")
1305+
emitter.emit_line(error_stmt)
1306+
1307+
emitter.emit_line("return 1;")
1308+
emitter.emit_line("}")

mypyc/codegen/emitfunc.py

Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
from typing import Final
66

77
from mypyc.analysis.blockfreq import frequently_executed_blocks
8-
from mypyc.codegen.cstring import c_string_initializer
98
from mypyc.codegen.emit import DEBUG_ERRORS, Emitter, TracebackAndGotoHandler, c_array_initializer
109
from mypyc.common import (
1110
GENERATOR_ATTRIBUTE_PREFIX,
@@ -18,14 +17,7 @@
1817
TYPE_VAR_PREFIX,
1918
)
2019
from mypyc.ir.class_ir import ClassIR
21-
from mypyc.ir.func_ir import (
22-
FUNC_CLASSMETHOD,
23-
FUNC_STATICMETHOD,
24-
FuncDecl,
25-
FuncIR,
26-
all_values,
27-
get_text_signature,
28-
)
20+
from mypyc.ir.func_ir import FUNC_CLASSMETHOD, FUNC_STATICMETHOD, FuncDecl, FuncIR, all_values
2921
from mypyc.ir.ops import (
3022
ERR_FALSE,
3123
NAMESPACE_MODULE,
@@ -117,14 +109,6 @@ def native_function_header(fn: FuncDecl, emitter: Emitter) -> str:
117109
)
118110

119111

120-
def native_function_doc_initializer(func: FuncIR) -> str:
121-
text_sig = get_text_signature(func)
122-
if text_sig is None:
123-
return "NULL"
124-
docstring = f"{text_sig}\n--\n\n"
125-
return c_string_initializer(docstring.encode("ascii", errors="backslashreplace"))
126-
127-
128112
def generate_native_function(
129113
fn: FuncIR, emitter: Emitter, source_path: str, module_name: str
130114
) -> None:

mypyc/codegen/emitmodule.py

Lines changed: 49 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,15 @@
2929
from mypy.util import hash_digest, json_dumps
3030
from mypyc.analysis.capsule_deps import find_implicit_capsule_dependencies
3131
from mypyc.codegen.cstring import c_string_initializer
32-
from mypyc.codegen.emit import Emitter, EmitterContext, HeaderDeclaration, c_array_initializer
33-
from mypyc.codegen.emitclass import generate_class, generate_class_reuse, generate_class_type_decl
34-
from mypyc.codegen.emitfunc import (
35-
generate_native_function,
32+
from mypyc.codegen.emit import (
33+
Emitter,
34+
EmitterContext,
35+
HeaderDeclaration,
36+
c_array_initializer,
3637
native_function_doc_initializer,
37-
native_function_header,
3838
)
39+
from mypyc.codegen.emitclass import generate_class, generate_class_reuse, generate_class_type_decl
40+
from mypyc.codegen.emitfunc import generate_native_function, native_function_header
3941
from mypyc.codegen.emitwrapper import (
4042
generate_legacy_wrapper_function,
4143
generate_wrapper_function,
@@ -566,7 +568,7 @@ def generate_c_for_modules(self) -> list[tuple[str, str]]:
566568

567569
for module_name, module in self.modules.items():
568570
if multi_file:
569-
emitter = Emitter(self.context)
571+
emitter = Emitter(self.context, filepath=self.source_paths[module_name])
570572
emitter.emit_line(f'#include "__native{self.short_group_suffix}.h"')
571573
emitter.emit_line(f'#include "__native_internal{self.short_group_suffix}.h"')
572574

@@ -986,6 +988,9 @@ def emit_module_methods(
986988
for fn in module.functions:
987989
if fn.class_name is not None or fn.name == TOP_LEVEL_NAME:
988990
continue
991+
# Coroutines are added to the module dict when the module is initialized.
992+
if fn.decl.is_coroutine:
993+
continue
989994
name = short_id_from_name(fn.name, fn.decl.shortname, fn.line)
990995
if is_fastcall_supported(fn, emitter.capi_version):
991996
flag = "METH_FASTCALL"
@@ -1024,6 +1029,30 @@ def emit_module_def_struct(
10241029
emitter.emit_line("};")
10251030
emitter.emit_line()
10261031

1032+
def emit_coroutine_wrappers(self, emitter: Emitter, module: ModuleIR, globals: str) -> None:
1033+
"""Emit insertion of coroutines into the module dict when the module is initialized.
1034+
Coroutines are wrapped in CPyFunction objects to enable introspection by functions like
1035+
inspect.iscoroutinefunction(fn).
1036+
"""
1037+
for fn in module.functions:
1038+
if fn.class_name is not None or fn.name == TOP_LEVEL_NAME:
1039+
continue
1040+
if not fn.decl.is_coroutine:
1041+
continue
1042+
1043+
filepath = self.source_paths[module.fullname]
1044+
error_stmt = " goto fail;"
1045+
name = short_id_from_name(fn.name, fn.decl.shortname, fn.line)
1046+
wrapper_name = emitter.emit_cpyfunction_instance(fn, name, filepath, error_stmt)
1047+
name_obj = f"{wrapper_name}_name"
1048+
emitter.emit_line(f'PyObject *{name_obj} = PyUnicode_FromString("{fn.name}");')
1049+
emitter.emit_line(f"if (unlikely(!{name_obj}))")
1050+
emitter.emit_line(error_stmt)
1051+
emitter.emit_line(
1052+
f"if (PyDict_SetItem({globals}, {name_obj}, (PyObject *){wrapper_name}) < 0)"
1053+
)
1054+
emitter.emit_line(error_stmt)
1055+
10271056
def emit_module_exec_func(
10281057
self, emitter: Emitter, module_name: str, module_prefix: str, module: ModuleIR
10291058
) -> None:
@@ -1071,20 +1100,33 @@ def emit_module_exec_func(
10711100
" goto fail;",
10721101
)
10731102

1103+
self.emit_coroutine_wrappers(emitter, module, module_globals)
1104+
10741105
# HACK: Manually instantiate generated classes here
10751106
type_structs: list[str] = []
10761107
for cl in module.classes:
10771108
type_struct = emitter.type_struct_name(cl)
10781109
type_structs.append(type_struct)
10791110
if cl.is_generated:
1111+
error_stmt = " goto fail;"
10801112
emitter.emit_lines(
10811113
"{t} = (PyTypeObject *)CPyType_FromTemplate("
10821114
"(PyObject *){t}_template, NULL, modname);".format(t=type_struct)
10831115
)
1084-
emitter.emit_lines(f"if (unlikely(!{type_struct}))", " goto fail;")
1116+
emitter.emit_lines(f"if (unlikely(!{type_struct}))", error_stmt)
10851117
name_prefix = cl.name_prefix(emitter.names)
10861118
emitter.emit_line(f"CPyDef_{name_prefix}_trait_vtable_setup();")
10871119

1120+
if cl.coroutine_name:
1121+
fn = cl.methods["__call__"]
1122+
filepath = self.source_paths[module.fullname]
1123+
name = cl.coroutine_name
1124+
wrapper_name = emitter.emit_cpyfunction_instance(
1125+
fn, name, filepath, error_stmt
1126+
)
1127+
static_name = emitter.static_name(cl.name + "_cpyfunction", module.fullname)
1128+
emitter.emit_line(f"{static_name} = {wrapper_name};")
1129+
10881130
emitter.emit_lines("if (CPyGlobalsInit() < 0)", " goto fail;")
10891131

10901132
self.generate_top_level_call(module, emitter)

mypyc/common.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@
8282
"misc_ops.c",
8383
"generic_ops.c",
8484
"pythonsupport.c",
85+
"function_wrapper.c",
8586
]
8687

8788
# Python 3.12 introduced immortal objects, specified via a special reference count

mypyc/ir/class_ir.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,9 @@ def __init__(
223223
# Is this a class inheriting from enum.Enum? Such classes can be special-cased.
224224
self.is_enum = False
225225

226+
# Name of the function if this a callable class representing a coroutine.
227+
self.coroutine_name: str | None = None
228+
226229
def __repr__(self) -> str:
227230
return (
228231
"ClassIR("
@@ -424,6 +427,7 @@ def serialize(self) -> JsonDict:
424427
"env_user_function": self.env_user_function.id if self.env_user_function else None,
425428
"reuse_freed_instance": self.reuse_freed_instance,
426429
"is_enum": self.is_enum,
430+
"is_coroutine": self.coroutine_name,
427431
}
428432

429433
@classmethod
@@ -481,6 +485,7 @@ def deserialize(cls, data: JsonDict, ctx: DeserMaps) -> ClassIR:
481485
)
482486
ir.reuse_freed_instance = data["reuse_freed_instance"]
483487
ir.is_enum = data["is_enum"]
488+
ir.coroutine_name = data["is_coroutine"]
484489

485490
return ir
486491

0 commit comments

Comments
 (0)