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-103509: PEP 697 -- Limited C API for Extending Opaque Types #103511

Merged
merged 29 commits into from
May 4, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
a9679fa
gh-103509: PEP 697 -- Limited C API for Extending Opaque Types
encukou Oct 10, 2022
b8dc072
Add a blurb
encukou Apr 13, 2023
1fc5c52
Fix PyMember_SetOne error return (thanks MSVC!)
encukou Apr 17, 2023
075ca51
Add What's New entry
encukou Apr 17, 2023
439de5d
Fix warning in tests
encukou Apr 19, 2023
291731b
Work around lack of alignof & max_align_t
encukou Apr 19, 2023
2ba8084
Use the same Sphinx role for Py_TPFLAGS_ITEMS_AT_END as for other typ…
encukou Apr 19, 2023
b03d431
Add ALIGNOF_MAX_ALIGN_T to configure & PC/pyconfig.h
encukou Apr 19, 2023
ec9d5a8
Use ALIGNOF_MAX_ALIGN_T
encukou Apr 19, 2023
5cab814
Fix typo
encukou Apr 20, 2023
3ade585
Don't include <stdalign.h>, it's not always available on Windows
encukou Apr 20, 2023
986bf26
Define ALIGNOF_MAX_ALIGN_T with long double if it's not available
encukou Apr 20, 2023
e7838ee
tests: Compute data_offset in C to avoid overflow issues
encukou Apr 20, 2023
93d86d1
Merge branch 'main' into extend-opaque-sq
arhadthedev Apr 21, 2023
266834d
Cast to `char*` for pointer arithmetic
encukou Apr 24, 2023
f968206
Fix ALIGNOF_MAX_ALIGN_T value for 32-bit Windows
encukou Apr 25, 2023
37158af
Fix C++ behaviour and comments for ALIGNOF_MAX_ALIGN_T
encukou Apr 25, 2023
402ecc6
Merge branch 'main' into extend-opaque-sq
encukou Apr 27, 2023
1701688
Don't rely on PyObject* being aligned to ALIGNOF_MAX_ALIGN_T
encukou Apr 28, 2023
db5d49b
Merge in main branch
encukou Apr 28, 2023
9d9911d
Apply suggestions from code review
encukou May 2, 2023
06bcf5b
Raise TypeError on missing flag
encukou May 2, 2023
0b69748
Wrap new tests in a class
encukou May 2, 2023
33c5258
Use PyModule_AddIntMacro in tests
encukou May 2, 2023
88dade7
Test failure of extending non-ITEMS_AT_END variable-sized types
encukou May 2, 2023
8720b25
Test error cases around members
encukou May 2, 2023
0474e69
Merge in the main branch
encukou May 2, 2023
a975de9
Apply suggestions from code review
encukou May 3, 2023
b9ddf21
Merge in the main branch
encukou May 3, 2023
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
Next Next commit
gh-103509: PEP 697 -- Limited C API for Extending Opaque Types
  • Loading branch information
encukou committed Apr 13, 2023
commit a9679fa3165e5bce5a44c83fd57226850b327fc7
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)
encukou marked this conversation as resolved.
Show resolved Hide resolved

Get a pointer to subclass-specific data reserved for *cls*.
encukou marked this conversation as resolved.
Show resolved Hide resolved

The object *o* **must** be an instance of *cls*, and *cls* must have been
encukou marked this conversation as resolved.
Show resolved Hide resolved
created using negative :c:member:`PyType_Spec.basicsize`.
Python does not check this.

On error, set an exception and return ``NULL``.
encukou marked this conversation as resolved.
Show resolved Hide resolved

.. 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
encukou marked this conversation as resolved.
Show resolved Hide resolved
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.
erlend-aasland marked this conversation as resolved.
Show resolved Hide resolved

.. versionadded:: 3.12

.. c:function:: PyObject* PyObject_GetItemData(PyObject *o)
encukou marked this conversation as resolved.
Show resolved Hide resolved

Get a pointer to per-item data for a class with
arhadthedev marked this conversation as resolved.
Show resolved Hide resolved
: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
encukou marked this conversation as resolved.
Show resolved Hide resolved
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
encukou marked this conversation as resolved.
Show resolved Hide resolved

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
erlend-aasland marked this conversation as resolved.
Show resolved Hide resolved
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'
encukou marked this conversation as resolved.
Show resolved Hide resolved
[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