]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-124153: Implement `PyType_GetBaseByToken()` and `Py_tp_token` slot (GH-124163)
authorneonene <53406459+neonene@users.noreply.github.com>
Wed, 18 Sep 2024 07:18:19 +0000 (16:18 +0900)
committerGitHub <noreply@github.com>
Wed, 18 Sep 2024 07:18:19 +0000 (09:18 +0200)
18 files changed:
Doc/c-api/type.rst
Doc/data/stable_abi.dat
Doc/whatsnew/3.14.rst
Include/cpython/object.h
Include/object.h
Include/typeslots.h
Lib/test/test_capi/test_misc.py
Lib/test/test_stable_abi_ctypes.py
Lib/test/test_sys.py
Misc/NEWS.d/next/C_API/2024-09-17-05-23-35.gh-issue-124153.L8TWmx.rst [new file with mode: 0644]
Misc/stable_abi.toml
Modules/_ctypes/_ctypes.c
Modules/_ctypes/ctypes.h
Modules/_testcapi/heaptype.c
Objects/typeobject.c
Objects/typeslots.inc
Objects/typeslots.py
PC/python3dll.c

index b25a407964be8d8948656d9f4d48468c7b4ba13d..b56da6954f41d4fd34f02cd5cd7712d144bc8863 100644 (file)
@@ -264,6 +264,24 @@ Type Objects
 
    .. versionadded:: 3.11
 
+.. c:function:: int PyType_GetBaseByToken(PyTypeObject *type, void *token, PyTypeObject **result)
+
+   Find the first superclass in *type*'s :term:`method resolution order` whose
+   :c:macro:`Py_tp_token` token is equal to the given one.
+
+   * If found, set *\*result* to a new :term:`strong reference`
+     to it and return ``1``.
+   * If not found, set *\*result* to ``NULL`` and return ``0``.
+   * On error, set *\*result* to ``NULL`` and return ``-1`` with an
+     exception set.
+
+   The *result* argument may be ``NULL``, in which case *\*result* is not set.
+   Use this if you need only the return value.
+
+   The *token* argument may not be ``NULL``.
+
+   .. versionadded:: 3.14
+
 .. c:function:: int PyUnstable_Type_AssignVersionTag(PyTypeObject *type)
 
    Attempt to assign a version tag to the given type.
@@ -488,6 +506,11 @@ The following functions and structs are used to create
       * ``Py_nb_add`` to set :c:member:`PyNumberMethods.nb_add`
       * ``Py_sq_length`` to set :c:member:`PySequenceMethods.sq_length`
 
+      An additional slot is supported that does not correspond to a
+      :c:type:`!PyTypeObject` struct field:
+
+      * :c:data:`Py_tp_token`
+
       The following “offset” fields cannot be set using :c:type:`PyType_Slot`:
 
          * :c:member:`~PyTypeObject.tp_weaklistoffset`
@@ -538,4 +561,47 @@ The following functions and structs are used to create
       The desired value of the slot. In most cases, this is a pointer
       to a function.
 
-      Slots other than ``Py_tp_doc`` may not be ``NULL``.
+      *pfunc* values may not be ``NULL``, except for the following slots:
+
+      * ``Py_tp_doc``
+      * :c:data:`Py_tp_token` (for clarity, prefer :c:data:`Py_TP_USE_SPEC`
+        rather than ``NULL``)
+
+.. c:macro:: Py_tp_token
+
+   A :c:member:`~PyType_Slot.slot` that records a static memory layout ID
+   for a class.
+
+   If the :c:type:`PyType_Spec` of the class is statically
+   allocated, the token can be set to the spec using the special value
+   :c:data:`Py_TP_USE_SPEC`:
+
+   .. code-block:: c
+
+      static PyType_Slot foo_slots[] = {
+         {Py_tp_token, Py_TP_USE_SPEC},
+
+   It can also be set to an arbitrary pointer, but you must ensure that:
+
+   * The pointer outlives the class, so it's not reused for something else
+     while the class exists.
+   * It "belongs" to the extension module where the class lives, so it will not
+     clash with other extensions.
+
+   Use :c:func:`PyType_GetBaseByToken` to check if a class's superclass has
+   a given token -- that is, check whether the memory layout is compatible.
+
+   To get the token for a given class (without considering superclasses),
+   use :c:func:`PyType_GetSlot` with ``Py_tp_token``.
+
+   .. versionadded:: 3.14
+
+   .. c:namespace:: NULL
+
+   .. c:macro:: Py_TP_USE_SPEC
+
+      Used as a value with :c:data:`Py_tp_token` to set the token to the
+      class's :c:type:`PyType_Spec`.
+      Expands to ``NULL``.
+
+      .. versionadded:: 3.14
index 7eeee270bb7f322461e6f8ac524f3a980784cda4..7c4b56d1dd652f42b9ecb36502230cb4cababebb 100644 (file)
@@ -690,6 +690,7 @@ func,PyType_FromSpec,3.2,,
 func,PyType_FromSpecWithBases,3.3,,
 func,PyType_GenericAlloc,3.2,,
 func,PyType_GenericNew,3.2,,
+func,PyType_GetBaseByToken,3.14,,
 func,PyType_GetFlags,3.2,,
 func,PyType_GetFullyQualifiedName,3.13,,
 func,PyType_GetModule,3.10,,
index 5e762336e547f6f2494b5006fb90b11b06d15b24..53399aa4e50fa6e24fc4f20590de4b103045c0b6 100644 (file)
@@ -554,6 +554,11 @@ New Features
 
   (Contributed by Victor Stinner in :gh:`107954`.)
 
+* Add :c:func:`PyType_GetBaseByToken` and :c:data:`Py_tp_token` slot for easier
+  superclass identification, which attempts to resolve the `type checking issue
+  <https://peps.python.org/pep-0630/#type-checking>`__ mentioned in :pep:`630`
+  (:gh:`124153`).
+
 
 Porting to Python 3.14
 ----------------------
index e1024ddbdf60625fbad2374eb3033a2b9d98e34d..9d092749b90096f6bff43c4fb2693c2a90dbc35a 100644 (file)
@@ -269,6 +269,7 @@ typedef struct _heaptypeobject {
     struct _dictkeysobject *ht_cached_keys;
     PyObject *ht_module;
     char *_ht_tpname;  // Storage for "tp_name"; see PyType_FromModuleAndSpec
+    void *ht_token;  // Storage for the "Py_tp_token" slot
     struct _specialization_cache _spec_cache; // For use by the specializer.
 #ifdef Py_GIL_DISABLED
     Py_ssize_t unique_id;  // ID used for thread-local refcounting
index abfdb6ce24df21ea51b969a95512a6dd27a00b1c..7124f58f6bdb375b8300e366be46eca634eef2ad 100644 (file)
@@ -391,6 +391,10 @@ PyAPI_FUNC(PyObject *) PyType_FromMetaclass(PyTypeObject*, PyObject*, PyType_Spe
 PyAPI_FUNC(void *) PyObject_GetTypeData(PyObject *obj, PyTypeObject *cls);
 PyAPI_FUNC(Py_ssize_t) PyType_GetTypeDataSize(PyTypeObject *cls);
 #endif
+#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= 0x030E0000
+PyAPI_FUNC(int) PyType_GetBaseByToken(PyTypeObject *, void *, PyTypeObject **);
+#define Py_TP_USE_SPEC NULL
+#endif
 
 /* Generic type check */
 PyAPI_FUNC(int) PyType_IsSubtype(PyTypeObject *, PyTypeObject *);
index e91caa1509c34b80cb32e13f545c24e5e55fda54..a7f3017ec02e92f69f8194b11f51d2ceee172a6c 100644 (file)
@@ -90,3 +90,7 @@
 /* New in 3.14 */
 #define Py_tp_vectorcall 82
 #endif
+#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= 0x030E0000
+/* New in 3.14 */
+#define Py_tp_token 83
+#endif
index ebc0a8a3d4efb5f6553562a3cf055d7434247651..5c6faa1626d380c71d8ebb1b118396b4186fee44 100644 (file)
@@ -1144,6 +1144,77 @@ class CAPITest(unittest.TestCase):
         MyType.__module__ = 123
         self.assertEqual(get_type_fullyqualname(MyType), 'my_qualname')
 
+    def test_get_base_by_token(self):
+        def get_base_by_token(src, key, comparable=True):
+            def run(use_mro):
+                find_first = _testcapi.pytype_getbasebytoken
+                ret1, result = find_first(src, key, use_mro, True)
+                ret2, no_result = find_first(src, key, use_mro, False)
+                self.assertIn(ret1, (0, 1))
+                self.assertEqual(ret1, result is not None)
+                self.assertEqual(ret1, ret2)
+                self.assertIsNone(no_result)
+                return result
+
+            found_in_mro = run(True)
+            found_in_bases = run(False)
+            if comparable:
+                self.assertIs(found_in_mro, found_in_bases)
+                return found_in_mro
+            return found_in_mro, found_in_bases
+
+        create_type = _testcapi.create_type_with_token
+        get_token = _testcapi.get_tp_token
+
+        Py_TP_USE_SPEC = _testcapi.Py_TP_USE_SPEC
+        self.assertEqual(Py_TP_USE_SPEC, 0)
+
+        A1 = create_type('_testcapi.A1', Py_TP_USE_SPEC)
+        self.assertTrue(get_token(A1) != Py_TP_USE_SPEC)
+
+        B1 = create_type('_testcapi.B1', id(self))
+        self.assertTrue(get_token(B1) == id(self))
+
+        tokenA1 = get_token(A1)
+        # find A1 from A1
+        found = get_base_by_token(A1, tokenA1)
+        self.assertIs(found, A1)
+
+        # no token in static types
+        STATIC = type(1)
+        self.assertEqual(get_token(STATIC), 0)
+        found = get_base_by_token(STATIC, tokenA1)
+        self.assertIs(found, None)
+
+        # no token in pure subtypes
+        class A2(A1): pass
+        self.assertEqual(get_token(A2), 0)
+        # find A1
+        class Z(STATIC, B1, A2): pass
+        found = get_base_by_token(Z, tokenA1)
+        self.assertIs(found, A1)
+
+        # searching for NULL token is an error
+        with self.assertRaises(SystemError):
+            get_base_by_token(Z, 0)
+        with self.assertRaises(SystemError):
+            get_base_by_token(STATIC, 0)
+
+        # share the token with A1
+        C1 = create_type('_testcapi.C1', tokenA1)
+        self.assertTrue(get_token(C1) == tokenA1)
+
+        # find C1 first by shared token
+        class Z(C1, A2): pass
+        found = get_base_by_token(Z, tokenA1)
+        self.assertIs(found, C1)
+        # B1 not found
+        found = get_base_by_token(Z, get_token(B1))
+        self.assertIs(found, None)
+
+        with self.assertRaises(TypeError):
+            _testcapi.pytype_getbasebytoken(
+                'not a type', id(self), True, False)
 
     def test_gen_get_code(self):
         def genf(): yield
index 4bca33b7451f80bc9fffc0dec0e830c975c248bb..4e509e30bee1227ba022269813d1cfe4b875e75c 100644 (file)
@@ -719,6 +719,7 @@ SYMBOL_NAMES = (
     "PyType_FromSpecWithBases",
     "PyType_GenericAlloc",
     "PyType_GenericNew",
+    "PyType_GetBaseByToken",
     "PyType_GetFlags",
     "PyType_GetFullyQualifiedName",
     "PyType_GetModule",
index 42b5a7c94e7700783fd634e96b8aa9fffe6854b6..77009648bd26ed3cb71d76b22f00b921fdee4eb3 100644 (file)
@@ -1718,7 +1718,7 @@ class SizeofTest(unittest.TestCase):
                   '3P'                  # PyMappingMethods
                   '10P'                 # PySequenceMethods
                   '2P'                  # PyBufferProcs
-                  '6P'
+                  '7P'
                   '1PIP'                # Specializer cache
                   + typeid              # heap type id (free-threaded only)
                   )
diff --git a/Misc/NEWS.d/next/C_API/2024-09-17-05-23-35.gh-issue-124153.L8TWmx.rst b/Misc/NEWS.d/next/C_API/2024-09-17-05-23-35.gh-issue-124153.L8TWmx.rst
new file mode 100644 (file)
index 0000000..b8c0b46
--- /dev/null
@@ -0,0 +1,2 @@
+Add :c:func:`PyType_GetBaseByToken` and :c:data:`Py_tp_token` slot for easier
+type checking, related to :pep:`489` and :pep:`630`.
index 6036fc96fdd99552e35d5ecf7a7257ecf94ca1af..d8a9d1f3335583c3cf892b1e1777b85bfa5b6366 100644 (file)
 [function.PyLong_AsUInt64]
     added = '3.14'
 [const.Py_tp_vectorcall]
-    added = '3.14'
\ No newline at end of file
+    added = '3.14'
+[function.PyType_GetBaseByToken]
+    added = '3.14'
+[const.Py_tp_token]
+    added = '3.14'
+[const.Py_TP_USE_SPEC]
+    added = '3.14'
index 2b23be7b753e34a8383c9224e890c4d77f06ac09..1d9e1699022d3e6e1a9ba62149697a81c7d07811 100644 (file)
@@ -500,7 +500,7 @@ CType_Type_dealloc(PyObject *self)
 {
     StgInfo *info = _PyStgInfo_FromType_NoState(self);
     if (!info) {
-        PyErr_WriteUnraisable(self);
+        PyErr_WriteUnraisable(NULL);  // NULL avoids segfault here
     }
     if (info) {
         PyMem_Free(info->ffi_type_pointer.elements);
@@ -560,6 +560,7 @@ static PyMethodDef ctype_methods[] = {
 };
 
 static PyType_Slot ctype_type_slots[] = {
+    {Py_tp_token, Py_TP_USE_SPEC},
     {Py_tp_traverse, CType_Type_traverse},
     {Py_tp_clear, CType_Type_clear},
     {Py_tp_dealloc, CType_Type_dealloc},
@@ -569,7 +570,7 @@ static PyType_Slot ctype_type_slots[] = {
     {0, NULL},
 };
 
-static PyType_Spec pyctype_type_spec = {
+PyType_Spec pyctype_type_spec = {
     .name = "_ctypes.CType_Type",
     .basicsize = -(Py_ssize_t)sizeof(StgInfo),
     .flags = (Py_TPFLAGS_DEFAULT | Py_TPFLAGS_IMMUTABLETYPE |
index 2eb1b6cae4d81bbdf0d5f2e0be3477148b51f8d7..738dcd1aaf8a01975a5761507086abe32b38fa70 100644 (file)
@@ -108,6 +108,7 @@ get_module_state_by_def(PyTypeObject *cls)
 }
 
 
+extern PyType_Spec pyctype_type_spec;
 extern PyType_Spec carg_spec;
 extern PyType_Spec cfield_spec;
 extern PyType_Spec cthunk_spec;
@@ -490,16 +491,23 @@ PyStgInfo_FromAny(ctypes_state *state, PyObject *obj, StgInfo **result)
 
 /* A variant of PyStgInfo_FromType that doesn't need the state,
  * so it can be called from finalization functions when the module
- * state is torn down. Does no checks; cannot fail.
- * This inlines the current implementation PyObject_GetTypeData,
- * so it might break in the future.
+ * state is torn down.
  */
 static inline StgInfo *
 _PyStgInfo_FromType_NoState(PyObject *type)
 {
-    size_t type_basicsize =_Py_SIZE_ROUND_UP(PyType_Type.tp_basicsize,
-                                             ALIGNOF_MAX_ALIGN_T);
-    return (StgInfo *)((char *)type + type_basicsize);
+    PyTypeObject *PyCType_Type;
+    if (PyType_GetBaseByToken(Py_TYPE(type), &pyctype_type_spec, &PyCType_Type) < 0) {
+        return NULL;
+    }
+    if (PyCType_Type == NULL) {
+        PyErr_Format(PyExc_TypeError, "expected a ctypes type, got '%N'", type);
+        return NULL;
+    }
+
+    StgInfo *info = PyObject_GetTypeData(type, PyCType_Type);
+    Py_DECREF(PyCType_Type);
+    return info;
 }
 
 // Initialize StgInfo on a newly created type
index b3fb9ec056c6c29752b9a1d1fdb4927f2a4d9824..cc88147dfcd7fb322260c55996c574dba2b04849 100644 (file)
@@ -410,6 +410,118 @@ pyobject_getitemdata(PyObject *self, PyObject *o)
 }
 
 
+static PyObject *
+create_type_with_token(PyObject *module, PyObject *args)
+{
+    const char *name;
+    PyObject *py_token;
+    if (!PyArg_ParseTuple(args, "sO", &name, &py_token)) {
+        return NULL;
+    }
+    void *token = PyLong_AsVoidPtr(py_token);
+    if (token == Py_TP_USE_SPEC) {
+        // Py_TP_USE_SPEC requires the spec that at least outlives the class
+        static PyType_Slot slots[] = {
+            {Py_tp_token, Py_TP_USE_SPEC},
+            {0},
+        };
+        static PyType_Spec spec = {
+            .name = "_testcapi.DefaultTokenTest",
+            .flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
+            .slots = slots,
+        };
+        PyObject *type = PyType_FromMetaclass(NULL, NULL, &spec, NULL);
+        if (!type) {
+            return NULL;
+        }
+        token = PyType_GetSlot((PyTypeObject *)type, Py_tp_token);
+        assert(!PyErr_Occurred());
+        Py_DECREF(type);
+        if (token != &spec) {
+            PyErr_SetString(PyExc_AssertionError,
+                            "failed to convert token from Py_TP_USE_SPEC");
+            return NULL;
+        }
+    }
+    // Test non-NULL token that must also outlive the class
+    PyType_Slot slots[] = {
+        {Py_tp_token, token},
+        {0},
+    };
+    PyType_Spec spec = {
+        .name = name,
+        .flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
+        .slots = slots,
+    };
+    return PyType_FromMetaclass(NULL, module, &spec, NULL);
+}
+
+static PyObject *
+get_tp_token(PyObject *self, PyObject *type)
+{
+    void *token = PyType_GetSlot((PyTypeObject *)type, Py_tp_token);
+    if (PyErr_Occurred()) {
+        return NULL;
+    }
+    return PyLong_FromVoidPtr(token);
+}
+
+static PyObject *
+pytype_getbasebytoken(PyObject *self, PyObject *args)
+{
+    PyTypeObject *type;
+    PyObject *py_token, *use_mro, *need_result;
+    if (!PyArg_ParseTuple(args, "OOOO",
+                          &type, &py_token, &use_mro, &need_result)) {
+        return NULL;
+    }
+
+    PyObject *mro_save = NULL;
+    if (use_mro != Py_True) {
+        // Test internal detail: PyType_GetBaseByToken works even with
+        // types that are only partially initialized (or torn down):
+        // if tp_mro=NULL we fall back to tp_bases.
+        assert(PyType_Check(type));
+        mro_save = type->tp_mro;
+        type->tp_mro = NULL;
+    }
+
+    void *token = PyLong_AsVoidPtr(py_token);
+    PyObject *result;
+    int ret;
+    if (need_result == Py_True) {
+        ret = PyType_GetBaseByToken(type, token, (PyTypeObject **)&result);
+    }
+    else {
+        result = NULL;
+        ret = PyType_GetBaseByToken(type, token, NULL);
+    }
+
+    if (use_mro != Py_True) {
+        type->tp_mro = mro_save;
+    }
+    if (ret < 0) {
+        assert(result == NULL);
+        return NULL;
+    }
+    PyObject *py_ret = PyLong_FromLong(ret);
+    if (py_ret == NULL) {
+        goto error;
+    }
+    PyObject *tuple = PyTuple_New(2);
+    if (tuple == NULL) {
+        goto error;
+    }
+    PyTuple_SET_ITEM(tuple, 0, py_ret);
+    PyTuple_SET_ITEM(tuple, 1, result ? result : Py_None);
+    return tuple;
+error:
+    Py_XDECREF(py_ret);
+    Py_XDECREF(result);
+    return NULL;
+}
+
+
 static PyMethodDef TestMethods[] = {
     {"pytype_fromspec_meta",    pytype_fromspec_meta,            METH_O},
     {"test_type_from_ephemeral_spec", test_type_from_ephemeral_spec, METH_NOARGS},
@@ -423,6 +535,9 @@ static PyMethodDef TestMethods[] = {
     {"make_immutable_type_with_base", make_immutable_type_with_base, METH_O},
     {"make_type_with_base", make_type_with_base, METH_O},
     {"pyobject_getitemdata", pyobject_getitemdata, METH_O},
+    {"create_type_with_token", create_type_with_token, METH_VARARGS},
+    {"get_tp_token", get_tp_token, METH_O},
+    {"pytype_getbasebytoken", pytype_getbasebytoken, METH_VARARGS},
     {NULL},
 };
 
@@ -1287,6 +1402,8 @@ _PyTestCapi_Init_Heaptype(PyObject *m) {
         &PyType_Type, m, &HeapCTypeMetaclassNullNew_spec, (PyObject *) &PyType_Type);
     ADD("HeapCTypeMetaclassNullNew", HeapCTypeMetaclassNullNew);
 
+    ADD("Py_TP_USE_SPEC", PyLong_FromVoidPtr(Py_TP_USE_SPEC));
+
     PyObject *HeapCCollection = PyType_FromMetaclass(
         NULL, m, &HeapCCollection_spec, NULL);
     if (HeapCCollection == NULL) {
index 28edd801284b814228219f42f9b5e0aae1567f65..68e481f8e5163b32859603cefbd4bf41744d175c 100644 (file)
@@ -3926,6 +3926,7 @@ type_new_alloc(type_new_ctx *ctx)
     et->ht_name = Py_NewRef(ctx->name);
     et->ht_module = NULL;
     et->_ht_tpname = NULL;
+    et->ht_token = NULL;
 
 #ifdef Py_GIL_DISABLED
     _PyType_AssignId(et);
@@ -4984,6 +4985,11 @@ PyType_FromMetaclass(
                 }
             }
             break;
+        case Py_tp_token:
+            {
+                res->ht_token = slot->pfunc == Py_TP_USE_SPEC ? spec : slot->pfunc;
+            }
+            break;
         default:
             {
                 /* Copy other slots directly */
@@ -5144,8 +5150,15 @@ PyType_GetSlot(PyTypeObject *type, int slot)
         PyErr_BadInternalCall();
         return NULL;
     }
+    int slot_offset = pyslot_offsets[slot].slot_offset;
 
-    parent_slot = *(void**)((char*)type + pyslot_offsets[slot].slot_offset);
+    if (slot_offset >= (int)sizeof(PyTypeObject)) {
+        if (!_PyType_HasFeature(type, Py_TPFLAGS_HEAPTYPE)) {
+            return NULL;
+        }
+    }
+
+    parent_slot = *(void**)((char*)type + slot_offset);
     if (parent_slot == NULL) {
         return NULL;
     }
@@ -5274,6 +5287,129 @@ _PyType_GetModuleByDef2(PyTypeObject *left, PyTypeObject *right,
     return module;
 }
 
+
+static PyTypeObject *
+get_base_by_token_recursive(PyTypeObject *type, void *token)
+{
+    assert(PyType_GetSlot(type, Py_tp_token) != token);
+    PyObject *bases = lookup_tp_bases(type);
+    assert(bases != NULL);
+    Py_ssize_t n = PyTuple_GET_SIZE(bases);
+    for (Py_ssize_t i = 0; i < n; i++) {
+        PyTypeObject *base = _PyType_CAST(PyTuple_GET_ITEM(bases, i));
+        if (!_PyType_HasFeature(base, Py_TPFLAGS_HEAPTYPE)) {
+            continue;
+        }
+        if (((PyHeapTypeObject*)base)->ht_token == token) {
+            return base;
+        }
+        base = get_base_by_token_recursive(base, token);
+        if (base != NULL) {
+            return base;
+        }
+    }
+    return NULL;
+}
+
+static inline PyTypeObject *
+get_base_by_token_from_mro(PyTypeObject *type, void *token)
+{
+    // Bypass lookup_tp_mro() as PyType_IsSubtype() does
+    PyObject *mro = type->tp_mro;
+    assert(mro != NULL);
+    assert(PyTuple_Check(mro));
+    // mro_invoke() ensures that the type MRO cannot be empty.
+    assert(PyTuple_GET_SIZE(mro) >= 1);
+    // Also, the first item in the MRO is the type itself, which is supposed
+    // to be already checked by the caller. We skip it in the loop.
+    assert(PyTuple_GET_ITEM(mro, 0) == (PyObject *)type);
+    assert(PyType_GetSlot(type, Py_tp_token) != token);
+
+    Py_ssize_t n = PyTuple_GET_SIZE(mro);
+    for (Py_ssize_t i = 1; i < n; i++) {
+        PyTypeObject *base = _PyType_CAST(PyTuple_GET_ITEM(mro, i));
+        if (!_PyType_HasFeature(base, Py_TPFLAGS_HEAPTYPE)) {
+            continue;
+        }
+        if (((PyHeapTypeObject*)base)->ht_token == token) {
+            return base;
+        }
+    }
+    return NULL;
+}
+
+static int
+check_base_by_token(PyTypeObject *type, void *token) {
+    // Chain the branches, which will be optimized exclusive here
+    if (token == NULL) {
+        PyErr_Format(PyExc_SystemError,
+                     "PyType_GetBaseByToken called with token=NULL");
+        return -1;
+    }
+    else if (!PyType_Check(type)) {
+        PyErr_Format(PyExc_TypeError,
+                     "expected a type, got a '%T' object", type);
+        return -1;
+    }
+    else if (!_PyType_HasFeature(type, Py_TPFLAGS_HEAPTYPE)) {
+        return 0;
+    }
+    else if (((PyHeapTypeObject*)type)->ht_token == token) {
+        return 1;
+    }
+    else if (type->tp_mro != NULL) {
+        // This will not be inlined
+        return get_base_by_token_from_mro(type, token) ? 1 : 0;
+    }
+    else {
+        return get_base_by_token_recursive(type, token)  ? 1 : 0;
+    }
+}
+
+int
+PyType_GetBaseByToken(PyTypeObject *type, void *token, PyTypeObject **result)
+{
+    if (result == NULL) {
+        // If the `result` is checked only once here, the subsequent
+        // branches will become trivial to optimize.
+        return check_base_by_token(type, token);
+    }
+    if (token == NULL || !PyType_Check(type)) {
+        *result = NULL;
+        return check_base_by_token(type, token);
+    }
+
+    // Chain the branches, which will be optimized exclusive here
+    PyTypeObject *base;
+    if (!_PyType_HasFeature(type, Py_TPFLAGS_HEAPTYPE)) {
+        // No static type has a heaptype superclass,
+        // which is ensured by type_ready_mro().
+        *result = NULL;
+        return 0;
+    }
+    else if (((PyHeapTypeObject*)type)->ht_token == token) {
+        *result = (PyTypeObject *)Py_NewRef(type);
+        return 1;
+    }
+    else if (type->tp_mro != NULL) {
+        // Expect this to be inlined
+        base = get_base_by_token_from_mro(type, token);
+    }
+    else {
+        base = get_base_by_token_recursive(type, token);
+    }
+
+    if (base != NULL) {
+        *result = (PyTypeObject *)Py_NewRef(base);
+        return 1;
+    }
+    else {
+        *result = NULL;
+        return 0;
+    }
+}
+
+
 void *
 PyObject_GetTypeData(PyObject *obj, PyTypeObject *cls)
 {
@@ -5966,6 +6102,7 @@ type_dealloc(PyObject *self)
 #ifdef Py_GIL_DISABLED
     _PyType_ReleaseId(et);
 #endif
+    et->ht_token = NULL;
     Py_TYPE(type)->tp_free((PyObject *)type);
 }
 
index ffb85ff56adff12a03c6b84d2921581b66022725..642160fe0bd8bcc7a0b0edc14613186f2d760ff0 100644 (file)
@@ -81,3 +81,4 @@
 {-1, offsetof(PyTypeObject, tp_finalize)},
 {offsetof(PyAsyncMethods, am_send), offsetof(PyTypeObject, tp_as_async)},
 {-1, offsetof(PyTypeObject, tp_vectorcall)},
+{-1, offsetof(PyHeapTypeObject, ht_token)},
index 8ab05f91be12b0db4249347c1264680a4d15e1f1..c7f8a33bb1e74ea44e529ad21889660871caf771 100755 (executable)
@@ -13,7 +13,11 @@ def generate_typeslots(out=sys.stdout):
             continue
 
         member = m.group(1)
-        if member.startswith("tp_"):
+        if member == "tp_token":
+            # The heap type structure (ht_*) is an implementation detail;
+            # the public slot for it has a familiar `tp_` prefix
+            member = '{-1, offsetof(PyHeapTypeObject, ht_token)}'
+        elif member.startswith("tp_"):
             member = f'{{-1, offsetof(PyTypeObject, {member})}}'
         elif member.startswith("am_"):
             member = (f'{{offsetof(PyAsyncMethods, {member}),'+
index 1845334b244d8c9d2bf0764e4821be967944965d..7bd04cb483b547b253c57feab1700b25155d172b 100755 (executable)
@@ -651,6 +651,7 @@ EXPORT_FUNC(PyType_FromSpec)
 EXPORT_FUNC(PyType_FromSpecWithBases)
 EXPORT_FUNC(PyType_GenericAlloc)
 EXPORT_FUNC(PyType_GenericNew)
+EXPORT_FUNC(PyType_GetBaseByToken)
 EXPORT_FUNC(PyType_GetFlags)
 EXPORT_FUNC(PyType_GetFullyQualifiedName)
 EXPORT_FUNC(PyType_GetModule)