Skip to content
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

gh-92678: Fix MRO calculation for C extension types with manual __dict__ #95242

Closed
wants to merge 1 commit into from

Conversation

Fidget-Spinner
Copy link
Member

@Fidget-Spinner Fidget-Spinner commented Jul 25, 2022

We need to account for C extension types with don't set Py_TPFLAGS_MANAGED_DICT and manually manage their own dict instead.

This may be unsightly, but this special case is special enough to break the rules. Unless we get a proper class creation and dict management API in 3.12, we will break a lot of code.

@Fidget-Spinner Fidget-Spinner requested a review from pablogsal July 25, 2022 12:10
@Fidget-Spinner Fidget-Spinner changed the title Fix MRO calculation for C extension types with manual __dict__ gh-92678: Fix MRO calculation for C extension types with manual __dict__ Jul 25, 2022
@pablogsal
Copy link
Member

What issue does this corresponds to?😅 I need a bit of context here to review it 😉

Comment on lines +2244 to +2249
if (!(type->tp_flags & Py_TPFLAGS_MANAGED_DICT) &&
type->tp_dictoffset && base->tp_dictoffset == 0 &&
type->tp_dictoffset + sizeof(PyObject *) == t_size &&
type->tp_flags & Py_TPFLAGS_HEAPTYPE) {
t_size -= sizeof(PyObject *);
}
Copy link
Member Author

Choose a reason for hiding this comment

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

This reintroduces the old code pre-3.11, but guards it with a check for not Py_TPFLAGS_MANAGED_DICT.

8319114#diff-1decebeef15f4e0b0ce106c665751ec55068d4d1d1825847925ad4f528b5b872L2254

Copy link
Member

Choose a reason for hiding this comment

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

The condition is a bit arcane. Could you capture it in a variable or use a macro or inline function so we can name what we are checking for? Currently is a bit difficult to decipher what happens when all of these are true or false

Copy link
Member Author

Choose a reason for hiding this comment

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

So funny thing is I also found the old code arcane and couldn't really understand it well. From my rough guesstimation

    // The __dict__ isn't automatically managed by CPython.
    if (!(type->tp_flags & Py_TPFLAGS_MANAGED_DICT) &&
        // That a dictoffset is set. Meaning it's a manual __dict__.
        // And the base class doesn't have a __dict__
        type->tp_dictoffset && base->tp_dictoffset == 0 &&
        // Huh?
        type->tp_dictoffset + sizeof(PyObject *) == t_size &&
        // Not a static type.
        type->tp_flags & Py_TPFLAGS_HEAPTYPE) {
        t_size -= sizeof(PyObject *);
    }

Copy link
Member Author

@Fidget-Spinner Fidget-Spinner Jul 25, 2022

Choose a reason for hiding this comment

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

I think this is a really cursed example of Hyrum's law.

The extra_ivars code assumes that __weakreflist__ comes first then __dict__. In mypyc it has to follow this order to work pre-3.11. Now in 3.11 when we remove that assumption, all the C extensions using this with complicated MROs break 🤦 .

@Fidget-Spinner
Copy link
Member Author

What issue does this corresponds to?😅 I need a bit of context here to review it 😉

Sorry. gh-92678

@@ -2241,6 +2241,12 @@ extra_ivars(PyTypeObject *type, PyTypeObject *base)
type->tp_weaklistoffset + sizeof(PyObject *) == t_size &&
type->tp_flags & Py_TPFLAGS_HEAPTYPE)
t_size -= sizeof(PyObject *);
if (!(type->tp_flags & Py_TPFLAGS_MANAGED_DICT) &&
Copy link
Member

@markshannon markshannon Jul 25, 2022

Choose a reason for hiding this comment

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

Is is correct to check Py_TPFLAGS_MANAGED_DICT during class creation?
It is set during class creation: https://github.com/python/cpython/blob/main/Objects/typeobject.c#L2989

Copy link
Member Author

Choose a reason for hiding this comment

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

I think setting the flag happens before the MRO check. https://github.com/python/cpython/blob/main/Objects/typeobject.c#L3174-L3186
(type_new_descriptors then PyType_Ready ).
(PyType_Ready -> type_ready -> type_ready_mro)

Copy link
Member

Choose a reason for hiding this comment

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

If you factor out the check into a macro, the code would be a bit clearer.

#define FIELD_AT_OFFSET(name, offset) type->tp_ ## name && base->tp_## name == 0 ...

@Fidget-Spinner Fidget-Spinner deleted the fix_mro_conflicts branch July 29, 2022 08:30
@Fidget-Spinner Fidget-Spinner restored the fix_mro_conflicts branch August 3, 2022 09:52
@Fidget-Spinner Fidget-Spinner reopened this Aug 3, 2022
@Fidget-Spinner
Copy link
Member Author

Reopened because this is an alternative fix.

Copy link
Member

@markshannon markshannon left a comment

Choose a reason for hiding this comment

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

This looks like it should fix the regression in #95589

Could you add some tests please?
You'll need to two classes each for tp_dictoffset and tp_weaklistoffset to the `_testcapi module then test for multiple inheritance of all pairs in test_capi.py

@@ -2241,6 +2241,12 @@ extra_ivars(PyTypeObject *type, PyTypeObject *base)
type->tp_weaklistoffset + sizeof(PyObject *) == t_size &&
type->tp_flags & Py_TPFLAGS_HEAPTYPE)
t_size -= sizeof(PyObject *);
if (!(type->tp_flags & Py_TPFLAGS_MANAGED_DICT) &&
Copy link
Member

Choose a reason for hiding this comment

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

If you factor out the check into a macro, the code would be a bit clearer.

#define FIELD_AT_OFFSET(name, offset) type->tp_ ## name && base->tp_## name == 0 ...

@@ -2241,6 +2241,12 @@ extra_ivars(PyTypeObject *type, PyTypeObject *base)
type->tp_weaklistoffset + sizeof(PyObject *) == t_size &&
type->tp_flags & Py_TPFLAGS_HEAPTYPE)
t_size -= sizeof(PyObject *);
if (!(type->tp_flags & Py_TPFLAGS_MANAGED_DICT) &&
Copy link
Member

Choose a reason for hiding this comment

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

We need to check for tp_dictoffset and tp_weaklistoffset, and shouldn't presume the order.
So we need to check for weakrefs, then dict, then weakrefs again (or vice versa).

if (FIELD_AT_OFFSET(dictoffset, t_size)) {
   t_size -= sizeof(PyObject *);
}
if (FIELD_AT_OFFSET(weaklistoffset, t_size)) {
   t_size -= sizeof(PyObject *);
}
if (FIELD_AT_OFFSET(dictoffset, t_size)) {
   t_size -= sizeof(PyObject *);
}

@markshannon
Copy link
Member

@Fidget-Spinner
As the RC is on Friday, I've implemented the suggested changes as #95596
Apologies for treading on your toes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants