Skip to content

Commit b11d314

Browse files
authored
[mypyc] Move setting of __cpyfunction__ outside of constructor (#20630)
When the `__cpyfunction__` attribute of callable class objects is initialized in the constructor, it might end up becoming `NULL` after a freed instance is reused because the attribute gets cleared when deallocating the object and the constructor would only initialize it when creating a new instance. To fix this, move the initialization outside of the constructor and do it right after. This also makes it consistent with the `__mypyc_env__` attribute.
1 parent 8615d82 commit b11d314

5 files changed

Lines changed: 84 additions & 25 deletions

File tree

mypyc/codegen/emitclass.py

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -412,9 +412,7 @@ def emit_line() -> None:
412412

413413
emitter.emit_line()
414414
if generate_full:
415-
generate_setup_for_class(
416-
cl, defaults_fn, vtable_name, shadow_vtable_name, coroutine_setup_name, emitter
417-
)
415+
generate_setup_for_class(cl, defaults_fn, vtable_name, shadow_vtable_name, emitter)
418416
emitter.emit_line()
419417
generate_constructor_for_class(cl, cl.ctor, init_fn, setup_name, vtable_name, emitter)
420418
emitter.emit_line()
@@ -606,7 +604,6 @@ def generate_setup_for_class(
606604
defaults_fn: FuncIR | None,
607605
vtable_name: str,
608606
shadow_vtable_name: str | None,
609-
coroutine_setup_name: str,
610607
emitter: Emitter,
611608
) -> None:
612609
"""Generate a native function that allocates an instance of a class."""
@@ -662,13 +659,6 @@ def generate_setup_for_class(
662659
if defaults_fn is not None:
663660
emit_attr_defaults_func_call(defaults_fn, "self", emitter)
664661

665-
# Initialize function wrapper for callable classes. As opposed to regular functions,
666-
# each instance of a callable class needs its own wrapper because they might be instantiated
667-
# inside other functions.
668-
if cl.coroutine_name:
669-
emitter.emit_line(f"if ({NATIVE_PREFIX}{coroutine_setup_name}((PyObject *)self) != 1)")
670-
emitter.emit_line(" return NULL;")
671-
672662
emitter.emit_line("return (PyObject *)self;")
673663
emitter.emit_line("}")
674664

mypyc/irbuild/builder.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
Assign,
7070
BasicBlock,
7171
Branch,
72+
Call,
7273
ComparisonOp,
7374
GetAttr,
7475
InitStatic,
@@ -91,6 +92,7 @@
9192
RType,
9293
RUnion,
9394
bitmap_rprimitive,
95+
bool_rprimitive,
9496
bytes_rprimitive,
9597
c_pyssize_t_rprimitive,
9698
dict_rprimitive,
@@ -1461,6 +1463,20 @@ def get_current_class_ir(self) -> ClassIR | None:
14611463
type_info = self.fn_info.fitem.info
14621464
return self.mapper.type_to_ir.get(type_info)
14631465

1466+
def add_coroutine_setup_call(self, class_name: str, obj: Value) -> Value:
1467+
return self.add(
1468+
Call(
1469+
FuncDecl(
1470+
class_name + "_coroutine_setup",
1471+
None,
1472+
self.module_name,
1473+
FuncSignature([RuntimeArg("type", object_rprimitive)], bool_rprimitive),
1474+
),
1475+
[obj],
1476+
-1,
1477+
)
1478+
)
1479+
14641480

14651481
def gen_arg_defaults(builder: IRBuilder) -> None:
14661482
"""Generate blocks for arguments that have default values.

mypyc/irbuild/callable_class.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,4 +232,9 @@ def instantiate_callable_class(builder: IRBuilder, fn_info: FuncInfo) -> Value:
232232
curr_env_reg = builder.fn_info.curr_env_reg
233233
if curr_env_reg:
234234
builder.add(SetAttr(func_reg, ENV_ATTR_NAME, curr_env_reg, fitem.line))
235+
# Initialize function wrapper for callable classes. As opposed to regular functions,
236+
# each instance of a callable class needs its own wrapper because they might be instantiated
237+
# inside other functions.
238+
if not fn_info.in_non_ext and fn_info.is_coroutine:
239+
builder.add_coroutine_setup_call(fn_info.callable_class.ir.name, func_reg)
235240
return func_reg

mypyc/irbuild/classdef.py

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
from mypy.types import Instance, UnboundType, get_proper_type
3232
from mypyc.common import PROPSET_PREFIX
3333
from mypyc.ir.class_ir import ClassIR, NonExtClassInfo
34-
from mypyc.ir.func_ir import FuncDecl, FuncSignature, RuntimeArg
34+
from mypyc.ir.func_ir import FuncDecl, FuncSignature
3535
from mypyc.ir.ops import (
3636
NAMESPACE_TYPE,
3737
BasicBlock,
@@ -473,19 +473,7 @@ def allocate_class(builder: IRBuilder, cdef: ClassDef) -> Value:
473473
-1,
474474
)
475475
)
476-
477-
builder.add(
478-
Call(
479-
FuncDecl(
480-
cdef.name + "_coroutine_setup",
481-
None,
482-
builder.module_name,
483-
FuncSignature([RuntimeArg("type", object_rprimitive)], bool_rprimitive),
484-
),
485-
[tp],
486-
-1,
487-
)
488-
)
476+
builder.add_coroutine_setup_call(cdef.name, tp)
489477

490478
# Populate a '__mypyc_attrs__' field containing the list of attrs
491479
builder.primitive_op(

mypyc/test-data/run-async.test

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1739,3 +1739,63 @@ async def m_future_with_reraised_exception(first_exc: Exception, second_exc: Exc
17391739
return await make_future(first_exc)
17401740
except type(first_exc):
17411741
raise second_exc
1742+
1743+
[case testCPyFunctionWithFreedInstance]
1744+
import asyncio
1745+
1746+
from functools import wraps
1747+
from typing import Any
1748+
1749+
class ctx_man:
1750+
async def __aenter__(self) -> None:
1751+
pass
1752+
1753+
async def __aexit__(self, *args: Any) -> None:
1754+
pass
1755+
1756+
def with_ctx_man():
1757+
def decorator(f):
1758+
@wraps(f)
1759+
async def inner():
1760+
async with ctx_man():
1761+
return await f()
1762+
1763+
return inner
1764+
1765+
return decorator
1766+
1767+
async def func() -> int:
1768+
return 33
1769+
1770+
async def run_wrapped():
1771+
wrapped = with_ctx_man()(func)
1772+
return await wrapped()
1773+
1774+
def test_native():
1775+
assert asyncio.run(run_wrapped()) == 33
1776+
1777+
[file driver.py]
1778+
import asyncio
1779+
1780+
from native import test_native, with_ctx_man
1781+
1782+
async def func() -> int:
1783+
return 42
1784+
1785+
async def run_wrapped():
1786+
wrapped = with_ctx_man()(func)
1787+
return await wrapped()
1788+
1789+
def test_interpreted():
1790+
assert asyncio.run(run_wrapped()) == 42
1791+
1792+
# Run multiple times to test that the CPyFunction attribute is still
1793+
# set correctly after reusing a freed instance of the inner callable class object.
1794+
for i in range(10):
1795+
test_interpreted()
1796+
test_native()
1797+
1798+
[file asyncio/__init__.pyi]
1799+
from typing import Any, Generator
1800+
1801+
def run(x: object) -> object: ...

0 commit comments

Comments
 (0)