Skip to content

gh-124153: Introduce PyType_GetBaseByToken function (PoC) #121079

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

Closed
wants to merge 103 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
103 commits
Select commit Hold shift + click to select a range
eb0d23d
add document
neonene Jun 27, 2024
273bac1
introduce Py_tp_token and ht_token
neonene Jun 27, 2024
d041167
fix PyType_GetSlot()
neonene Jun 27, 2024
d41cead
add PyType_GetBaseByToken() PyType_GetToken()
neonene Jun 27, 2024
1456baa
add test
neonene Jun 27, 2024
14b4103
add ctypes example
neonene Jun 27, 2024
7af3378
use tp_mro directly (like PyType_IsSubtype) for free-threading err
neonene Jun 27, 2024
0b8bd07
silence warning
neonene Jun 27, 2024
f917ad9
edit testcase
neonene Jun 27, 2024
e08d58f
remove an assertion
neonene Jun 27, 2024
22c20d9
add missing cast
neonene Jun 27, 2024
7fa0d81
cast again
neonene Jun 27, 2024
5621704
edit testcase
neonene Jun 27, 2024
492a7d5
remove redundant testcase
neonene Jun 27, 2024
f71fa78
edit testcase
neonene Jun 28, 2024
83760d7
add test for no-result mode 1/2
neonene Jun 28, 2024
245d9d4
add test for no-result mode 2/2
neonene Jun 28, 2024
a0858a7
edit test
neonene Jun 28, 2024
4fb9cf8
add functions to check perf
neonene Jun 28, 2024
eb29cf8
remove previous experiment commit
neonene Jun 28, 2024
ccd5ede
abandon the proposal of PyType_GetToken()
neonene Jul 3, 2024
7d3f8b6
optimize GetBaseByToken like GetModuleByDef
neonene Jul 3, 2024
100972d
add a repeat test (temporary use)
neonene Jul 3, 2024
c58ef1c
Merge branch 'main' into bytoken
neonene Jul 3, 2024
7e89a98
fix a build error
neonene Jul 3, 2024
1de4cba
remove the repeat test
neonene Jul 3, 2024
c0d4210
add Py_TP_USE_SPEC to the test module
neonene Jul 3, 2024
68c4b8a
remove a duplicate GetSlot test
neonene Jul 5, 2024
b0cb58a
tweak GetBaseBytoken()
neonene Jul 5, 2024
2e59344
add repeat tests
neonene Jul 5, 2024
8f74f7f
Merge branch 'main' into bytoken
neonene Jul 5, 2024
7fcf53e
Use Py_TP_USE_SPEC in PyType_FromMetaclass()
neonene Jul 8, 2024
a1ac466
Merge branch 'main' into bytoken
neonene Jul 8, 2024
2312cac
reword
neonene Jul 8, 2024
958d845
typo
neonene Jul 8, 2024
a1736b0
Doc: GetSlot() returns &spec with Py_TP_USE_SPEC
neonene Jul 9, 2024
295f498
fix comment in test
neonene Jul 11, 2024
f45e4d0
remove repeat tests
neonene Jul 11, 2024
9e1857e
Merge branch 'main' into bytoken
neonene Jul 11, 2024
f5c082b
Reword/rearrange documentation
encukou Jul 22, 2024
1f5e289
Add to stable ABI
encukou Jul 22, 2024
0e14a59
Nitpick: Add new field at the end
encukou Jul 22, 2024
ab9dc41
PyType_GetBaseByToken: Raise exceptions for user errors
encukou Jul 22, 2024
00c0b7b
Add internal comments
encukou Jul 22, 2024
99362b2
Merge pull request #1 from encukou/bytoken
neonene Jul 23, 2024
df696ed
Merge branch 'main' into bytoken
neonene Jul 23, 2024
c75e355
consider the suggestion in create_type_with_token()
neonene Jul 23, 2024
fbdec08
remove a cast
neonene Jul 23, 2024
5341b1d
move a doc right under PyType_GetModuleByDef()
neonene Jul 23, 2024
79ef825
cleanup create_type_with_token()
neonene Jul 23, 2024
1bb40b8
fix a typo in the doc
neonene Jul 29, 2024
249c8fa
clarify an example code in the doc
neonene Jul 29, 2024
bfc9321
Merge branch 'main' into bytoken
neonene Jul 31, 2024
5967fd0
apply proposal to ctypes completely
neonene Jul 31, 2024
82f511e
What's New
neonene Jul 31, 2024
4507079
doc: fix the slot example usage
neonene Aug 1, 2024
92b08eb
ditto
neonene Aug 2, 2024
eb4c2f8
doc: use a pronoun
neonene Aug 2, 2024
7cd33c9
Merge branch 'main' into bytoken
neonene Aug 7, 2024
910c596
Merge branch 'main' into bytoken
neonene Aug 8, 2024
a1444bd
ctypes: fix an error handling
neonene Aug 9, 2024
d9fce25
ctypes: do not print metaclass on error
neonene Aug 9, 2024
3faf73a
ctypes: reword a comment
neonene Aug 15, 2024
9f953c2
Merge branch 'main' into bytoken
neonene Aug 15, 2024
fc39abc
static type check first
neonene Aug 18, 2024
90edfef
revert for correctness
neonene Aug 19, 2024
f21b166
update test
neonene Aug 19, 2024
6f6ae41
apply proposal to defdict_or() in _collectionsmodule.c
neonene Aug 21, 2024
a73f956
apply proposal to _decimal.c (replace _PyType_GetModuleByDef2)
neonene Aug 22, 2024
04d74bb
Merge branch 'main' into bytoken
neonene Aug 24, 2024
1d17bec
_decimal: make the code PGO-friendly with MSVC
neonene Aug 26, 2024
7324e2b
move an assert and a comment
neonene Aug 26, 2024
a2fd0ff
nits
neonene Aug 26, 2024
ec77d16
edit whats new
neonene Aug 27, 2024
5c5daa9
use _PyType_CAST in recursive function
neonene Aug 27, 2024
7735ef1
setup fixed repeat tests
neonene Aug 27, 2024
eaa3007
revert repeat tests
neonene Aug 27, 2024
15e0582
_decimal: simplify dec_richcompare()
neonene Aug 27, 2024
52616f1
_decimal: restore PyType_GetModuleByDef() in bin-ops
neonene Aug 29, 2024
5e094d0
Merge branch 'main' into bytoken
neonene Aug 29, 2024
ac03429
fix build error
neonene Aug 29, 2024
ac82d36
optimize PyType_GetBaseByToken()
neonene Aug 31, 2024
2a24934
_decimal: improve performance
neonene Aug 31, 2024
ce68195
Merge branch 'main' into bytoken
neonene Aug 31, 2024
fa936d7
rename
neonene Aug 31, 2024
a9121fc
clarify the optimizations
neonene Sep 1, 2024
7f50633
typo
neonene Sep 1, 2024
ea6e9d3
Add a private function and use it
neonene Sep 3, 2024
60779f8
Merge branch 'main' into bytoken
neonene Sep 3, 2024
588d5ee
revert private function (borrowed ref ver.)
neonene Sep 8, 2024
de19b5a
Merge branch 'main' into bytoken
neonene Sep 8, 2024
b730abb
edit header file
neonene Sep 8, 2024
77f143a
bad PyType_GetBaseByToken() example
neonene Sep 10, 2024
b114bc8
recover performance when a reference is needed
neonene Sep 13, 2024
08bfb87
Merge branch 'main' into bytoken
neonene Sep 13, 2024
f08da25
edit PyType_GetBaseByToken()
neonene Sep 13, 2024
e3c1182
recover performance take2
neonene Sep 15, 2024
d95e422
Merge branch 'main' into bytoken
neonene Sep 15, 2024
974fce3
do not exercise in ctypes
neonene Sep 17, 2024
61bb346
edit this version of PyType_GetBaseByToken()
neonene Sep 17, 2024
cd750ca
ditto
neonene Sep 17, 2024
ca3043f
📜🤖 Added by blurb_it.
blurb-it[bot] Sep 17, 2024
5ccf0b8
typo
neonene Sep 17, 2024
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.

3 changes: 3 additions & 0 deletions Doc/whatsnew/3.14.rst
Original file line number Diff line number Diff line change
Expand Up @@ -554,6 +554,9 @@ 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`.

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'
20 changes: 8 additions & 12 deletions Modules/_collectionsmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -2179,6 +2179,8 @@ typedef struct {
PyObject *default_factory;
} defdictobject;

static PyType_Spec defdict_spec;

PyDoc_STRVAR(defdict_missing_doc,
"__missing__(key) # Called by __getitem__ for missing key; pseudo-code:\n\
if self.default_factory is None: raise KeyError((key,))\n\
Expand Down Expand Up @@ -2358,23 +2360,16 @@ defdict_or(PyObject* left, PyObject* right)
{
PyObject *self, *other;

// Find module state
PyTypeObject *tp = Py_TYPE(left);
PyObject *mod = PyType_GetModuleByDef(tp, &_collectionsmodule);
if (mod == NULL) {
PyErr_Clear();
tp = Py_TYPE(right);
mod = PyType_GetModuleByDef(tp, &_collectionsmodule);
int ret = PyType_GetBaseByToken(Py_TYPE(left), &defdict_spec, NULL);
if (ret < 0) {
return NULL;
}
assert(mod != NULL);
collections_state *state = get_module_state(mod);

if (PyObject_TypeCheck(left, state->defdict_type)) {
if (ret) {
self = left;
other = right;
}
else {
assert(PyObject_TypeCheck(right, state->defdict_type));
assert(PyType_GetBaseByToken(Py_TYPE(right), &defdict_spec, NULL) == 1);
self = right;
other = left;
}
Expand Down Expand Up @@ -2454,6 +2449,7 @@ passed to the dict constructor, including keyword arguments.\n\
#define DEFERRED_ADDRESS(ADDR) 0

static PyType_Slot defdict_slots[] = {
{Py_tp_token, Py_TP_USE_SPEC},
{Py_tp_dealloc, defdict_dealloc},
{Py_tp_repr, defdict_repr},
{Py_nb_or, defdict_or},
Expand Down
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