Skip to content

feat(mypyc): proper weakref support #19056

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 23 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
1f4a3a0
feat: proper weakref support
BobTheBuidler May 8, 2025
afbefea
Update emitclass.py
BobTheBuidler May 8, 2025
1a166d4
Update class_ir.py
BobTheBuidler May 8, 2025
65fa594
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 8, 2025
e8d9394
Update emitclass.py
BobTheBuidler May 8, 2025
887bc39
Update emitclass.py
BobTheBuidler May 8, 2025
85b07d5
Update class_ir.py
BobTheBuidler May 9, 2025
211ea3c
feat: support_weakrefs=True in mypyc_attr
BobTheBuidler May 9, 2025
fa595d2
Update vtable.py
BobTheBuidler May 9, 2025
292e849
Update prepare.py
BobTheBuidler May 9, 2025
259d89a
fix: serialization segfault
BobTheBuidler May 13, 2025
2949df3
chore: add comment
BobTheBuidler May 13, 2025
a116ab1
fix: deserialization discrepancy
BobTheBuidler May 13, 2025
7da4e53
feat(test): test mypyc_attr(supports_weakref=True)
BobTheBuidler May 13, 2025
34e4862
fix: set Py_TPFLAGS_MANAGED_WEAKREF flag on python>=3.12
BobTheBuidler May 13, 2025
f43eaae
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 13, 2025
bc96da2
Update emitclass.py
BobTheBuidler May 13, 2025
14ad56a
fix: handle weakrefs in tp_dealloc
BobTheBuidler May 13, 2025
bc82b3e
Update registry.py
BobTheBuidler May 16, 2025
7f59edf
Create weakref_ops.py
BobTheBuidler May 16, 2025
ca22a58
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 16, 2025
408576b
Update weakref_ops.py
BobTheBuidler May 16, 2025
fd2b234
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 16, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions mypyc/codegen/emitclass.py
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,19 @@ def emit_line() -> None:
if emitter.capi_version < (3, 12):
fields["tp_dictoffset"] = base_size
fields["tp_weaklistoffset"] = weak_offset
elif cl.supports_weakref:
# __weakref__ lives right after the struct
# TODO: It should get a member in the struct instead of doing this nonsense.
emitter.emit_lines(
f"PyMemberDef {members_name}[] = {{",
f'{{"__weakref__", T_OBJECT_EX, {base_size}, 0, NULL}},',
"{0}",
"};",
)
if emitter.capi_version < (3, 12):
# versions >= 3.12 set Py_TPFLAGS_MANAGED_WEAKREF flag instead
# https://docs.python.org/3.12/extending/newtypes.html#weak-reference-support
fields["tp_weaklistoffset"] = base_size
else:
fields["tp_basicsize"] = base_size

Expand Down Expand Up @@ -343,6 +356,9 @@ def emit_line() -> None:
fields["tp_call"] = "PyVectorcall_Call"
if has_managed_dict(cl, emitter):
flags.append("Py_TPFLAGS_MANAGED_DICT")
if cl.supports_weakref and emitter.capi_version >= (3, 12):
flags.append("Py_TPFLAGS_MANAGED_WEAKREF")

fields["tp_flags"] = " | ".join(flags)

emitter.emit_line(f"static PyTypeObject {emitter.type_struct_name(cl)}_template_ = {{")
Expand Down Expand Up @@ -782,6 +798,13 @@ def generate_dealloc_for_class(
emitter.emit_line("static void")
emitter.emit_line(f"{dealloc_func_name}({cl.struct_name(emitter.names)} *self)")
emitter.emit_line("{")
if cl.supports_weakref:
if emitter.capi_version < (3, 12):
emitter.emit_line("if (self->weakreflist != NULL) {")
emitter.emit_line("PyObject_ClearWeakRefs((PyObject *) self);")
emitter.emit_line("}")
else:
emitter.emit_line("PyObject_ClearWeakRefs((PyObject *) self);")
if has_tp_finalize:
emitter.emit_line("if (!PyObject_GC_IsFinalized((PyObject *)self)) {")
emitter.emit_line("Py_TYPE(self)->tp_finalize((PyObject *)self);")
Expand Down
4 changes: 4 additions & 0 deletions mypyc/ir/class_ir.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,8 @@ def __init__(
self.inherits_python = False
# Do instances of this class have __dict__?
self.has_dict = False
# Do instances of this class have __weakref__?
self.supports_weakref = False
# Do we allow interpreted subclasses? Derived from a mypyc_attr.
self.allow_interpreted_subclasses = False
# Does this class need getseters to be generated for its attributes? (getseters are also
Expand Down Expand Up @@ -362,6 +364,7 @@ def serialize(self) -> JsonDict:
"is_final_class": self.is_final_class,
"inherits_python": self.inherits_python,
"has_dict": self.has_dict,
"supports_weakref": self.supports_weakref,
"allow_interpreted_subclasses": self.allow_interpreted_subclasses,
"needs_getseters": self.needs_getseters,
"_serializable": self._serializable,
Expand Down Expand Up @@ -419,6 +422,7 @@ def deserialize(cls, data: JsonDict, ctx: DeserMaps) -> ClassIR:
ir.is_final_class = data["is_final_class"]
ir.inherits_python = data["inherits_python"]
ir.has_dict = data["has_dict"]
ir.supports_weakref = data["supports_weakref"]
ir.allow_interpreted_subclasses = data["allow_interpreted_subclasses"]
ir.needs_getseters = data["needs_getseters"]
ir._serializable = data["_serializable"]
Expand Down
3 changes: 3 additions & 0 deletions mypyc/irbuild/prepare.py
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,9 @@ def prepare_class_def(
if attrs.get("serializable") is True:
# Supports copy.copy and pickle (including subclasses)
ir._serializable = True
if attrs.get("supports_weakref") is True:
# Has a tp_weakrefoffset slot allowing the creation of weak references (including subclasses)
ir.supports_weakref = True

# Check for subclassing from builtin types
for cls in info.mro:
Expand Down
2 changes: 2 additions & 0 deletions mypyc/irbuild/vtable.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ def compute_vtable(cls: ClassIR) -> None:

if not cls.is_generated:
cls.has_dict = any(x.inherits_python for x in cls.mro)
# TODO: define more weakref triggers
cls.supports_weakref = cls.supports_weakref or cls.has_dict

for t in cls.mro[1:]:
# Make sure all ancestors are processed first
Expand Down
8 changes: 0 additions & 8 deletions mypyc/primitives/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -364,11 +364,3 @@ def load_address_op(name: str, type: RType, src: str) -> LoadAddressDescription:


# Import various modules that set up global state.
import mypyc.primitives.bytes_ops
import mypyc.primitives.dict_ops
import mypyc.primitives.float_ops
import mypyc.primitives.int_ops
import mypyc.primitives.list_ops
import mypyc.primitives.misc_ops
import mypyc.primitives.str_ops
import mypyc.primitives.tuple_ops # noqa: F401
38 changes: 38 additions & 0 deletions mypyc/primitives/weakref_ops.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from mypyc.ir.rtypes import object_rprimitive
from mypyc.primitives.registry import function_op

# Weakref operations

"""
py_new_weak_ref_op = function_op(
name="weakref.weakref",
arg_types=[object_rprimitive],
# TODO: how do I pass NULL as the 2nd arg?
Copy link
Author

@BobTheBuidler BobTheBuidler May 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JukkaL I've also added 2 new primitives for weakref.ref and weakref.proxy, when called with 2 args. I'm now working on 2 more primitives when called with 1 arg. I just need to know how to pass a NULL as the 2nd arg to the c func. what would you advise here?

#extra_int_constants=[],
result_type=object_rprimitive,
c_function_name="PyWeakref_NewRef",
)
"""

py_new_weak_ref_with_callback_op = function_op(
name="weakref.weakref",
arg_types=[object_rprimitive, object_rprimitive],
result_type=object_rprimitive,
c_function_name="PyWeakref_NewRef",
)

"""
py_new_weak_proxy_op = function_op(
name="weakref.proxy",
arg_types=[object_rprimitive],
result_type=object_rprimitive,
c_function_name="PyWeakref_NewProxy",
)
"""

py_new_weak_proxy_with_callback_op = function_op(
name="weakref.proxy",
arg_types=[object_rprimitive, object_rprimitive],
result_type=object_rprimitive,
c_function_name="PyWeakref_NewProxy",
)
42 changes: 42 additions & 0 deletions mypyc/test-data/irbuild-classes.test
Original file line number Diff line number Diff line change
Expand Up @@ -1381,3 +1381,45 @@ class M(type): # E: Inheriting from most builtin types is unimplemented
@mypyc_attr(native_class=True)
class A(metaclass=M): # E: Class is marked as native_class=True but it can't be a native class. Classes with a metaclass other than ABCMeta, TypingMeta or GenericMeta can't be native classes.
pass

[case testMypycAttrSupportsWeakref]
import weakref
from mypy_extensions import mypyc_attr

@mypyc_attr(supports_weakref=True)
class WeakrefClass:
pass

obj = WeakrefClass()
ref = weakref.ref(obj)
assert ref() is obj

[case testMypycAttrSupportsWeakrefInheritance]
import weakref
from mypy_extensions import mypyc_attr

@mypyc_attr(supports_weakref=True)
class WeakrefClass:
pass

class WeakrefInheritor(WeakrefClass):
pass

obj = WeakrefInheritor()
ref = weakref.ref(obj)
assert ref() is obj

[case testMypycAttrSupportsWeakrefSubclass]
import weakref
from mypy_extensions import mypyc_attr

class NativeClass:
pass

@mypyc_attr(supports_weakref=True)
class WeakrefSubclass(NativeClass):
pass

obj = WeakrefSubclass()
ref = weakref.ref(obj)
assert ref() is obj
Loading