Skip to content

Commit

Permalink
pythongh-103509: PEP 697 -- Limited C API for Extending Opaque Types
Browse files Browse the repository at this point in the history
  • Loading branch information
encukou committed Apr 13, 2023
1 parent 59e0de4 commit a9679fa
Show file tree
Hide file tree
Showing 23 changed files with 758 additions and 17 deletions.
39 changes: 39 additions & 0 deletions Doc/c-api/object.rst
Original file line number Diff line number Diff line change
Expand Up @@ -395,3 +395,42 @@ Object Protocol
returns ``NULL`` if the object cannot be iterated.
.. versionadded:: 3.10
.. c:function:: PyObject* PyObject_GetTypeData(PyObject *o, PyTypeObject *cls)
Get a pointer to subclass-specific data reserved for *cls*.
The object *o* **must** be an instance of *cls*, and *cls* must have been
created using negative :c:member:`PyType_Spec.basicsize`.
Python does not check this.
On error, set an exception and return ``NULL``.
.. versionadded:: 3.12
.. c:function:: Py_ssize_t PyType_GetTypeDataSize(PyTypeObject *cls)
Return the size of the memory reserved for *cls*, i.e. the size of the
memory :c:func:`PyObject_GetTypeData` returns.
This may be larger than requested using :c:member:`-PyType_Spec.basicsize <PyType_Spec.basicsize>`;
it is safe to use this larger size (e.g. with :c:func:`!memset`).
The type *cls* **must** have been created using
negative :c:member:`PyType_Spec.basicsize`.
Python does not check this.
On error, set an exception and return a negative value.
.. versionadded:: 3.12
.. c:function:: PyObject* PyObject_GetItemData(PyObject *o)
Get a pointer to per-item data for a class with
:c:macro:`Py_TPFLAGS_ITEMS_AT_END`.
On error, set an exception and return ``NULL``.
:py:exc:`TypeError` is raised if *o* does not have
:c:macro:`Py_TPFLAGS_ITEMS_AT_END` set.
.. versionadded:: 3.12
16 changes: 16 additions & 0 deletions Doc/c-api/structures.rst
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,22 @@ The following flags can be used with :c:member:`PyMemberDef.flags`:
Emit an ``object.__getattr__`` :ref:`audit event <audit-events>`
before reading.
.. c:macro:: Py_RELATIVE_OFFSET
Indicates that the :c:member:`~PyMemberDef.offset` of this ``PyMemberDef``
entry indicates an offset from the subclass-specific data, rather than
from ``PyObject``.
Can only be used as part of :c:member:`Py_tp_members <PyTypeObject.tp_members>`
:c:type:`slot <PyTypeSlot>` when creating a class using negative
:c:member:`~PyTypeDef.basicsize`.
It is mandatory in that case.
This flag is only used in :c:type:`PyTypeSlot`.
When setting :c:member:`~PyTypeObject.tp_members` during
class creation, Python clears it and sets
:c:member:`PyMemberDef.offset` to the offset from the ``PyObject`` struct.
.. index::
single: READ_RESTRICTED
single: WRITE_RESTRICTED
Expand Down
48 changes: 40 additions & 8 deletions Doc/c-api/type.rst
Original file line number Diff line number Diff line change
Expand Up @@ -322,25 +322,57 @@ The following functions and structs are used to create
Structure defining a type's behavior.
.. c:member:: const char* PyType_Spec.name
.. c:member:: const char* name
Name of the type, used to set :c:member:`PyTypeObject.tp_name`.
.. c:member:: int PyType_Spec.basicsize
.. c:member:: int PyType_Spec.itemsize
.. c:member:: int basicsize
Size of the instance in bytes, used to set
:c:member:`PyTypeObject.tp_basicsize` and
:c:member:`PyTypeObject.tp_itemsize`.
If positive, specifies the size of the instance in bytes.
It is used to set :c:member:`PyTypeObject.tp_basicsize`.
.. c:member:: int PyType_Spec.flags
If zero, specifies that :c:member:`~PyTypeObject.tp_basicsize`
should be inherited.
If negative, the absolute value specifies how much space instances of the
class need *in addition* to the superclass.
Use :c:func:`PyObject_GetTypeData` to get a pointer to subclass-specific
memory reserved this way.
.. versionchanged:: 3.12
Previously, this field could not be negative.
.. c:member:: int itemsize
Size of one element of a variable-size type, in bytes
Used to set :c:member:`PyTypeObject.tp_itemsize`.
See ``tp_itemsize`` documentation for caveats.
If zero, :c:member:`~PyTypeObject.tp_itemsize` is inherited.
Extending arbitrary variable-sized classes is dangerous,
since some types use a fixed offset for variable-sized memory,
which can then overlap fixed-sized memory used by a subclass.
To help prevent mistakes, inheriting ``itemsize`` is only possible
in the following situations:
- The base is not variable-sized (its
:c:member:`~PyTypeObject.tp_itemsize`).
- The requested :c:member:`PyType_Spec.basicsize` is positive,
suggesting that the memory layout of the base class is known.
- The requested :c:member:`PyType_Spec.basicsize` is zero,
suggesting that the subclass does not access the instance's memory
directly.
- With the :c:macro:`Py_TPFLAGS_ITEMS_AT_END` flag.
.. c:member:: int flags
Type flags, used to set :c:member:`PyTypeObject.tp_flags`.
If the ``Py_TPFLAGS_HEAPTYPE`` flag is not set,
:c:func:`PyType_FromSpecWithBases` sets it automatically.
.. c:member:: PyType_Slot *PyType_Spec.slots
.. c:member:: PyType_Slot *slots
Array of :c:type:`PyType_Slot` structures.
Terminated by the special slot value ``{0, NULL}``.
Expand Down
20 changes: 20 additions & 0 deletions Doc/c-api/typeobj.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1171,6 +1171,26 @@ and :c:type:`PyType_Type` effectively act as defaults.)
:c:member:`~PyTypeObject.tp_weaklistoffset` field is set in a superclass.


.. c:macro:: Py_TPFLAGS_ITEMS_AT_END
Only usable with variable-size types, i.e. ones with non-zero
:c:member:`~PyObject.tp_itemsize`.

Indicates that the variable-sized portion of an instance of this type is
at the end of the instance's memory area, at an offset of
:c:expr:`Py_TYPE(obj)->tp_basicsize` (which may be different in each
subclass).

When setting this flag, be sure that all superclasses either
use this memory layout, or are not variable-sized.
Python does not check this.

.. versionadded:: 3.12

**Inheritance:**

This flag is inherited.

.. XXX Document more flags here?
Expand Down
2 changes: 2 additions & 0 deletions Doc/data/stable_abi.dat

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

1 change: 1 addition & 0 deletions Include/cpython/object.h
Original file line number Diff line number Diff line change
Expand Up @@ -553,6 +553,7 @@ Py_DEPRECATED(3.11) typedef int UsingDeprecatedTrashcanMacro;
Py_TRASHCAN_END; \
} while(0);

PyAPI_FUNC(void *) PyObject_GetItemData(PyObject *obj);

PyAPI_FUNC(int) _PyObject_VisitManagedDict(PyObject *obj, visitproc visit, void *arg);
PyAPI_FUNC(void) _PyObject_ClearManagedDict(PyObject *obj);
Expand Down
1 change: 1 addition & 0 deletions Include/descrobject.h
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ struct PyMemberDef {
#define Py_READONLY 1
#define Py_AUDIT_READ 2 // Added in 3.10, harmless no-op before that
#define _Py_WRITE_RESTRICTED 4 // Deprecated, no-op. Do not reuse the value.
#define Py_RELATIVE_OFFSET 8

PyAPI_FUNC(PyObject *) PyMember_GetOne(const char *, PyMemberDef *);
PyAPI_FUNC(int) PyMember_SetOne(char *, PyMemberDef *, PyObject *);
Expand Down
5 changes: 0 additions & 5 deletions Include/internal/pycore_object.h
Original file line number Diff line number Diff line change
Expand Up @@ -376,11 +376,6 @@ extern int _PyObject_IsInstanceDictEmpty(PyObject *);
extern int _PyType_HasSubclasses(PyTypeObject *);
extern PyObject* _PyType_GetSubclasses(PyTypeObject *);

// Access macro to the members which are floating "behind" the object
static inline PyMemberDef* _PyHeapType_GET_MEMBERS(PyHeapTypeObject *etype) {
return (PyMemberDef*)((char*)etype + Py_TYPE(etype)->tp_basicsize);
}

PyAPI_FUNC(PyObject *) _PyObject_LookupSpecial(PyObject *, PyObject *);

/* C function call trampolines to mitigate bad function pointer casts.
Expand Down
5 changes: 5 additions & 0 deletions Include/object.h
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,8 @@ PyAPI_FUNC(PyObject *) PyType_GetQualName(PyTypeObject *);
#endif
#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= 0x030C0000
PyAPI_FUNC(PyObject *) PyType_FromMetaclass(PyTypeObject*, PyObject*, PyType_Spec*, PyObject*);
PyAPI_FUNC(void *) PyObject_GetTypeData(PyObject *obj, PyTypeObject *cls);
PyAPI_FUNC(Py_ssize_t) PyType_GetTypeDataSize(PyTypeObject *cls);
#endif

/* Generic type check */
Expand Down Expand Up @@ -436,6 +438,9 @@ given type object has a specified feature.
// subject itself (rather than a mapped attribute on it):
#define _Py_TPFLAGS_MATCH_SELF (1UL << 22)

/* Items (ob_size*tp_itemsize) are found at the end of an instance's memory */
#define Py_TPFLAGS_ITEMS_AT_END (1UL << 23)

/* These flags are used to determine if a type is a subclass. */
#define Py_TPFLAGS_LONG_SUBCLASS (1UL << 24)
#define Py_TPFLAGS_LIST_SUBCLASS (1UL << 25)
Expand Down
114 changes: 114 additions & 0 deletions Lib/test/test_capi/test_misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,13 @@
import unittest
import warnings
import weakref
import operator
from test import support
from test.support import MISSING_C_DOCSTRINGS
from test.support import import_helper
from test.support import threading_helper
from test.support import warnings_helper
from test.support import requires_limited_api
from test.support.script_helper import assert_python_failure, assert_python_ok, run_python_until_end
try:
import _posixsubprocess
Expand Down Expand Up @@ -756,6 +758,118 @@ def meth(self):
MutableBase.meth = lambda self: 'changed'
self.assertEqual(instance.meth(), 'changed')

@requires_limited_api
def test_heaptype_relative_sizes(self):
# Test subclassing using "relative" basicsize, see PEP 697
def check(extra_base_size, extra_size):
Base, Sub, instance, data_ptr, data_size = (
_testcapi.make_sized_heaptypes(
extra_base_size, -extra_size))

# no alignment shenanigans when inheriting directly
if extra_size == 0:
self.assertEqual(Base.__basicsize__, Sub.__basicsize__)
self.assertEqual(data_size, 0)

else:
# The following offsets should be in increasing order:
data_offset = data_ptr - id(instance)
offsets = [
(0, 'start of object'),
(Base.__basicsize__, 'end of base data'),
(data_offset, 'subclass data'),
(data_offset + extra_size, 'end of requested subcls data'),
(data_offset + data_size, 'end of reserved subcls data'),
(Sub.__basicsize__, 'end of object'),
]
ordered_offsets = sorted(offsets, key=operator.itemgetter(0))
self.assertEqual(
offsets, ordered_offsets,
msg=f'Offsets not in expected order, got: {ordered_offsets}')

# end of reserved subcls data == end of object
self.assertEqual(Sub.__basicsize__, data_offset + data_size)

# we don't reserve (requested + alignment) or more data
self.assertLess(data_size - extra_size,
_testcapi.alignof_max_align_t)

# Everything should be aligned
self.assertEqual(data_ptr % _testcapi.alignof_max_align_t, 0)
self.assertEqual(data_size % _testcapi.alignof_max_align_t, 0)

sizes = sorted({0, 1, 2, 3, 4, 7, 8, 123,
object.__basicsize__,
object.__basicsize__-1,
object.__basicsize__+1})
for extra_base_size in sizes:
for extra_size in sizes:
args = dict(extra_base_size=extra_base_size,
extra_size=extra_size)
with self.subTest(**args):
check(**args)

@requires_limited_api
def test_HeapCCollection(self):
"""Make sure HeapCCollection works properly by itself"""
collection = _testcapi.HeapCCollection(1, 2, 3)
self.assertEqual(list(collection), [1, 2, 3])

@requires_limited_api
def test_heaptype_inherit_itemsize(self):
"""Test HeapCCollection subclasses work properly"""
sizes = sorted({0, 1, 2, 3, 4, 7, 8, 123,
object.__basicsize__,
object.__basicsize__-1,
object.__basicsize__+1})
for extra_size in sizes:
with self.subTest(extra_size=extra_size):
Sub = _testcapi.subclass_var_heaptype(
_testcapi.HeapCCollection, -extra_size, 0, 0)
collection = Sub(1, 2, 3)
collection.set_data_to_3s()

self.assertEqual(list(collection), [1, 2, 3])
mem = collection.get_data()
self.assertGreaterEqual(len(mem), extra_size)
self.assertTrue(set(mem) <= {3}, f'got {mem!r}')

@requires_limited_api
def test_heaptype_relative_members(self):
"""Test HeapCCollection subclasses work properly"""
sizes = sorted({0, 1, 2, 3, 4, 7, 8, 123,
object.__basicsize__,
object.__basicsize__-1,
object.__basicsize__+1})
for extra_base_size in sizes:
for extra_size in sizes:
for offset in sizes:
with self.subTest(extra_base_size=extra_base_size, extra_size=extra_size, offset=offset):
if offset < extra_size:
Sub = _testcapi.make_heaptype_with_member(
extra_base_size, -extra_size, offset, True)
Base = Sub.mro()[1]
instance = Sub()
self.assertEqual(instance.memb, instance.get_memb())
instance.set_memb(13)
self.assertEqual(instance.memb, instance.get_memb())
self.assertEqual(instance.get_memb(), 13)
instance.memb = 14
self.assertEqual(instance.memb, instance.get_memb())
self.assertEqual(instance.get_memb(), 14)
self.assertGreaterEqual(instance.get_memb_offset(), Base.__basicsize__)
self.assertLess(instance.get_memb_offset(), Sub.__basicsize__)
else:
with self.assertRaises(SystemError):
Sub = _testcapi.make_heaptype_with_member(
extra_base_size, -extra_size, offset, True)
with self.assertRaises(SystemError):
Sub = _testcapi.make_heaptype_with_member(
extra_base_size, extra_size, offset, True)
with self.subTest(extra_base_size=extra_base_size, extra_size=extra_size):
with self.assertRaises(SystemError):
Sub = _testcapi.make_heaptype_with_member(
extra_base_size, -extra_size, -1, True)

def test_pynumber_tobase(self):
from _testcapi import pynumber_tobase
Expand Down
2 changes: 2 additions & 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.

9 changes: 9 additions & 0 deletions Misc/stable_abi.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2397,3 +2397,12 @@
added = '3.12' # Before 3.12, available in "structmember.h" w/o Py_ prefix
[const.Py_AUDIT_READ]
added = '3.12' # Before 3.12, available in "structmember.h"

[function.PyObject_GetTypeData]
added = '3.12'
[function.PyType_GetTypeDataSize]
added = '3.12'
[const.Py_RELATIVE_OFFSET]
added = '3.12'
[const.Py_TPFLAGS_ITEMS_AT_END]
added = '3.12'
2 changes: 1 addition & 1 deletion Modules/Setup.stdlib.in
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@
@MODULE__XXTESTFUZZ_TRUE@_xxtestfuzz _xxtestfuzz/_xxtestfuzz.c _xxtestfuzz/fuzzer.c
@MODULE__TESTBUFFER_TRUE@_testbuffer _testbuffer.c
@MODULE__TESTINTERNALCAPI_TRUE@_testinternalcapi _testinternalcapi.c
@MODULE__TESTCAPI_TRUE@_testcapi _testcapimodule.c _testcapi/vectorcall.c _testcapi/vectorcall_limited.c _testcapi/heaptype.c _testcapi/unicode.c _testcapi/getargs.c _testcapi/pytime.c _testcapi/datetime.c _testcapi/docstring.c _testcapi/mem.c _testcapi/watchers.c _testcapi/long.c _testcapi/float.c _testcapi/structmember.c _testcapi/exceptions.c _testcapi/code.c _testcapi/pyos.c
@MODULE__TESTCAPI_TRUE@_testcapi _testcapimodule.c _testcapi/vectorcall.c _testcapi/vectorcall_limited.c _testcapi/heaptype.c _testcapi/unicode.c _testcapi/getargs.c _testcapi/pytime.c _testcapi/datetime.c _testcapi/docstring.c _testcapi/mem.c _testcapi/watchers.c _testcapi/long.c _testcapi/float.c _testcapi/structmember.c _testcapi/exceptions.c _testcapi/code.c _testcapi/pyos.c _testcapi/heaptype_relative.c
@MODULE__TESTCLINIC_TRUE@_testclinic _testclinic.c

# Some testing modules MUST be built as shared libraries.
Expand Down
Loading

0 comments on commit a9679fa

Please sign in to comment.