Skip to content

gh-124153: Implement PyType_GetBaseByToken() and Py_tp_token slot #124163

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

Merged
merged 10 commits into from
Sep 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
68 changes: 67 additions & 1 deletion Doc/c-api/type.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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`
Expand Down Expand Up @@ -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
1 change: 1 addition & 0 deletions Doc/data/stable_abi.dat

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions Doc/whatsnew/3.14.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
----------------------
Expand Down
1 change: 1 addition & 0 deletions Include/cpython/object.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions Include/object.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 *);
Expand Down
4 changes: 4 additions & 0 deletions Include/typeslots.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
71 changes: 71 additions & 0 deletions Lib/test/test_capi/test_misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -1144,6 +1144,77 @@ class MyType:
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
Expand Down
1 change: 1 addition & 0 deletions Lib/test/test_stable_abi_ctypes.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Lib/test/test_sys.py
Original file line number Diff line number Diff line change
Expand Up @@ -1718,7 +1718,7 @@ def delx(self): del self.__x
'3P' # PyMappingMethods
'10P' # PySequenceMethods
'2P' # PyBufferProcs
'6P'
'7P'
'1PIP' # Specializer cache
+ typeid # heap type id (free-threaded only)
)
Expand Down
Original file line number Diff line number Diff line change
@@ -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`.
8 changes: 7 additions & 1 deletion Misc/stable_abi.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2527,4 +2527,10 @@
[function.PyLong_AsUInt64]
added = '3.14'
[const.Py_tp_vectorcall]
added = '3.14'
added = '3.14'
[function.PyType_GetBaseByToken]
added = '3.14'
[const.Py_tp_token]
added = '3.14'
[const.Py_TP_USE_SPEC]
added = '3.14'
5 changes: 3 additions & 2 deletions Modules/_ctypes/_ctypes.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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},
Expand All @@ -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 |
Expand Down
20 changes: 14 additions & 6 deletions Modules/_ctypes/ctypes.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
Loading
Loading