Skip to content

Commit e7446a1

Browse files
authored
PEP 697 (new): C-API for Extending Opaque Types (#2772)
1 parent 5432a30 commit e7446a1

File tree

2 files changed

+358
-0
lines changed

2 files changed

+358
-0
lines changed

.github/CODEOWNERS

+1
Original file line numberDiff line numberDiff line change
@@ -577,6 +577,7 @@ pep-0693.rst @Yhg1s
577577
pep-0694.rst @dstufft
578578
pep-0695.rst @gvanrossum
579579
pep-0696.rst @jellezijlstra
580+
pep-0697.rst @encukou
580581
# ...
581582
# pep-0754.txt
582583
# ...

pep-0697.rst

+357
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,357 @@
1+
PEP: 697
2+
Title: C API for Extending Opaque Types
3+
Author: Petr Viktorin <[email protected]>
4+
Status: Draft
5+
Type: Standards Track
6+
Content-Type: text/x-rst
7+
Created: 23-Aug-2022
8+
Python-Version: 3.12
9+
10+
11+
Abstract
12+
========
13+
14+
Add limited C API for extending types whose ``struct`` is opaque,
15+
by allowing code to only deal with data specific to a particular (sub)class.
16+
17+
Make the mechanism usable with ``PyHeapType``.
18+
19+
20+
Motivation
21+
==========
22+
23+
Extending opaque types
24+
----------------------
25+
26+
In order to allow changing/optimizing CPython, and allow freedom for alternate
27+
implementations of the C API, best practice is to not expose memory layout
28+
(C structs) in public API, and instead rely on accessor functions.
29+
(When this hurts performance, direct struct access can be allowed in a
30+
less stable API tier, at the expense of compatibility with diferent
31+
versions/implementations of the interpreter.)
32+
33+
However, when a particular type's instance struct is hidden, it becomes
34+
difficult to subclass it.
35+
The usual subclassing pattern, explained `in the tutorial <https://docs.python.org/3.10/extending/newtypes_tutorial.html#subclassing-other-types>`_,
36+
is to put the base class ``struct`` as the first member of the subclass ``struct``.
37+
The tutorial shows this on a ``list`` subtype with extra state; adapted to
38+
a heap type (``PyType_Spec``) the example reads:
39+
40+
.. code-block:: c
41+
42+
typedef struct {
43+
PyListObject list;
44+
int state;
45+
} SubListObject;
46+
47+
static PyType_Spec Sublist_spec = {
48+
.name = "sublist.SubList",
49+
.basicsize = sizeof(SubListObject),
50+
.itemsize = 0,
51+
.flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
52+
.slots = SubList_slots
53+
};
54+
55+
Since the superclass struct (``PyListObject``) is part of the subclass struct
56+
(``SubListObject``):
57+
58+
- ``PyListObject`` size must be known at compile time, and
59+
- the size must be the same across all interpreters/versions the compiled
60+
extension is ABI-compatible with.
61+
62+
But in limited API/stable ABI, we do not expose the size of ``PyListObject``,
63+
so that it can vary between CPython versions (and even between possible
64+
alternate ABI-compatible C API implementations).
65+
66+
With the size not available, limited API users must resort to workarounds such
67+
as querying ``__basicsize__`` and plugging it into ``PyType_Spec`` at runtime,
68+
and divining the correct offset for their extra data.
69+
This requires making assumptions about memory layout, which the limited API
70+
is supposed to hide.
71+
72+
73+
Extending variable-size objects
74+
-------------------------------
75+
76+
Another scenario where the traditional way to extend an object does not work
77+
is variable-sized objects, i.e. ones with non-zero ``tp_itemsize``.
78+
If the instance struct ends with a variable-length array (such as
79+
in ``tuple`` or ``int``), subclasses cannot add their own extra data without
80+
detailed knowledge about how the superclass allocates and uses its memory.
81+
82+
Some types, such as CPython's ``PyHeapType``, handle this by storing
83+
variable-sized data after the fixed-size struct.
84+
This means that any subclass can add its own fixed-size data.
85+
(Only one class in the inheritance hierarchy can use variable-sized data, though.)
86+
This PEP proposes API that makes this practice easier, and ensures the
87+
variable-sized data is properly aligned.
88+
89+
Note that many variable-size types, like ``int`` or ``tuple``, do not use
90+
this mechanism.
91+
This PEP does not propose any changes to existing variable-size types (like
92+
``int`` or ``tuple``) except ``PyHeapType``.
93+
94+
95+
Extending ``PyHeapType`` specifically
96+
-------------------------------------
97+
98+
The motivating problem this PEP solves is creating metaclasses, that is,
99+
subclasses of ``type``.
100+
The underlying ``PyHeapTypeObject`` struct is both variable-sized and
101+
opaque in the limited API.
102+
103+
Projects such as language bindings and frameworks that need to attach custom
104+
data to metaclasses currently resort to questionable workarounds.
105+
The situation is worse in projects that target the Limited API.
106+
107+
For an example of the currently necessary workarounds, see:
108+
`nb_type_data_static <https://github.com/wjakob/nanobind/blob/f3044cf44763e105428e4e0cf8f42d951b9cc997/src/nb_type.cpp#L1085>`_
109+
in the not-yet-released limited-API branch of ``nanobind``
110+
(a spiritual successor of the popular C++ binding generator ``pybind11``).
111+
112+
113+
Rationale
114+
=========
115+
116+
This PEP proposes a different model: instead of the superclass data being
117+
part of the subclass data, the extra space a subclass needs is specified
118+
and accessed separately.
119+
(How base class data is accessed is left to whomever implements the base class:
120+
they can for example provide accessor functions, expose a part of its
121+
``struct`` for better performance, or do both.)
122+
123+
The proposed mechanism allows using static, read-only ``PyType_Spec``
124+
even if the superclass struct is opaque, like ``PyTypeObject`` in
125+
the Limited API.
126+
127+
Combined with a way to create class from ``PyType_Spec`` and a custom metaclass,
128+
this will allow libraries like nanobind or JPype to create metaclasses
129+
without making assumptions about ``PyTypeObject``'s memory layout.
130+
The approach generalizes to non-metaclass types as well.
131+
132+
133+
Specification
134+
=============
135+
136+
In the code blocks below, only function headers are part of the specification.
137+
Other code (the size/offset calculations) are details of the initial CPython
138+
implementation, and subject to change.
139+
140+
Relative ``basicsize``
141+
----------------------
142+
143+
The ``basicsize`` member of ``PyType_Spec`` will be allowed to be zero or
144+
negative.
145+
In that case, its absolute value will specify the amount of *extra* storage space instances of
146+
the new class require, in addition to the basicsize of the base class.
147+
That is, the basicsize of the resulting class will be:
148+
149+
.. code-block:: c
150+
151+
type->tp_basicsize = _align(base->tp_basicsize) + _align(-spec->basicsize);
152+
153+
where ``_align`` rounds up to a multiple of ``alignof(max_align_t)``.
154+
When ``spec->basicsize`` is zero, ``base->tp_basicsize`` will be inherited
155+
directly instead (i.e. set to ``base->tp_basicsize`` without aligning).
156+
157+
On an instance, the memory area specific to a subclass -- that is, the
158+
“extra space” that subclass reserves in addition its base -- will be available
159+
using a new function, ``PyObject_GetTypeData``.
160+
In CPython, this function will be defined as:
161+
162+
.. code-block:: c
163+
164+
void *
165+
PyObject_GetTypeData(PyObject *obj, PyTypeObject *cls) {
166+
return (char *)obj + _align(cls->tp_base->tp_basicsize);
167+
}
168+
169+
Another function will be added to retreive the size of this memory area:
170+
171+
.. code-block:: c
172+
173+
Py_ssize_t
174+
PyObject_GetTypeDataSize(PyTypeObject *cls) {
175+
return cls->tp_basicsize - _align(cls->tp_base->tp_basicsize);
176+
}
177+
178+
The functionality comes with two important caveats, which will be pointed out
179+
in documentation:
180+
181+
- The new functions may only be used for classes created using negative
182+
``PyType_Spec.basicsize``. For other classes, the behavior is undefined.
183+
(Note that this allows the above code to assume ``cls->tp_base`` is not
184+
``NULL``.)
185+
186+
- Classes of variable-length objects (those with non-zero ``tp_itemsize``)
187+
can only be meaningfully extended using negative ``basicsize`` if all
188+
superclasses cooperate (see below).
189+
Of types defined by Python, initially only ``PyTypeObject`` will do so,
190+
others (including ``int`` or ``tuple``) will not.
191+
192+
193+
Inheriting ``itemsize``
194+
-----------------------
195+
196+
If the ``itemsize`` member of ``PyType_Spec`` is set to zero,
197+
the itemsize will be inherited from the base class .
198+
199+
.. note::
200+
201+
This PEP does not propose specifying “relative” ``itemsize``
202+
(using a negative number).
203+
There is a lack of motivating use cases, and there's no obvious
204+
best memory layout for sharing item storage across classes in the
205+
inheritance hierarchy.
206+
207+
A new function, ``PyObject_GetItemData``, will be added to safely access the
208+
memory reserved for items, taking subclasses that extend ``tp_basicsize``
209+
into account.
210+
In CPython it will be defined as:
211+
212+
.. code-block:: c
213+
214+
void *
215+
PyObject_GetItemData(PyObject *obj) {
216+
return (char *)obj + Py_TYPE(obj)->tp_basicsize;
217+
}
218+
219+
This function will *not* be added to the Limited API.
220+
221+
Note that it **is not safe** to use **any** of the functions added in this PEP
222+
unless **all classes in the inheritance hierarchy** only use
223+
``PyObject_GetItemData`` (or an equivalent) for per-item memory, or don't
224+
use per-item memory at all.
225+
(This issue already exists for most current classes that use variable-length
226+
arrays in the instance struct, but it's much less obvious if the base struct
227+
layout is unknown.)
228+
229+
The documentation for all API added in this PEP will mention
230+
the caveat.
231+
232+
233+
Relative member offsets
234+
-----------------------
235+
236+
In types defined using negative ``PyType_Spec.basicsize``, the offsets of
237+
members defined via ``Py_tp_members`` must be “relative” -- to the
238+
extra subclass data, rather than the full ``PyObject`` struct.
239+
This will be indicated by a new flag, ``PY_RELATIVE_OFFSET``.
240+
241+
In the initial implementation, the new flag will be redundant -- it only serves
242+
to make the offset's changed meaning clear.
243+
It is an error to *not* use ``PY_RELATIVE_OFFSET`` with negative ``basicsize``,
244+
and it is an error to use it in any other context (i.e. direct or indirect
245+
calls to ``PyDescr_NewMember``, ``PyMember_GetOne``, ``PyMember_SetOne``).
246+
247+
CPython will adjust the offset and clear the ``PY_RELATIVE_OFFSET`` flag when
248+
intitializing a type.
249+
This means that the created type's ``tp_members`` will not match the input
250+
definition's ``Py_tp_members`` slot, and that any code that reads
251+
``tp_members`` does not need to handle the flag.
252+
253+
254+
Changes to ``PyTypeObject``
255+
---------------------------
256+
257+
Internally in CPython, access to ``PyTypeObject`` “items”
258+
(``_PyHeapType_GET_MEMBERS``) will be changed to use ``PyObject_GetItemData``.
259+
Note that the current implementation is equivalent except it lacks the
260+
alignment adjustment.
261+
The macro is used a few times in type creation, so no measurable
262+
performance impact is expected.
263+
Public API for this data, ``tp_members``, will not be affected.
264+
265+
266+
List of new API
267+
===============
268+
269+
The following new functions are proposed.
270+
These will be added to the Limited API/Stable ABI:
271+
272+
* ``void * PyObject_GetTypeData(PyObject *obj, PyTypeObject *cls)``
273+
* ``Py_ssize_t PyObject_GetTypeDataSize(PyTypeObject *cls)``
274+
275+
These will be added to the public C API only:
276+
277+
* ``void *PyObject_GetItemData(PyObject *obj)``
278+
279+
280+
Backwards Compatibility
281+
=======================
282+
283+
There are no known backwards compatibility concerns.
284+
285+
286+
Security Implications
287+
=====================
288+
289+
None known.
290+
291+
292+
Endorsements
293+
============
294+
295+
XXX: The PEP mentions nanobind -- make sure they agree!
296+
297+
XXX: HPy, JPype, PySide might also want to chime in.
298+
299+
300+
How to Teach This
301+
=================
302+
303+
The initial implementation will include reference documentation
304+
and a What's New entry, which should be enough for the target audience
305+
-- authors of C extension libraries.
306+
307+
308+
Reference Implementation
309+
========================
310+
311+
XXX: Not quite ready yet
312+
313+
314+
Possible Future Enhancements
315+
============================
316+
317+
Alignment
318+
---------
319+
320+
The proposed implementation may waste some space if instance structs
321+
need smaller alignment than ``alignof(max_align_t)``.
322+
Also, dealing with alignment makes the calculation slower than it could be
323+
if we could rely on ``base->tp_basicsize`` being properly aligned for the
324+
subtype.
325+
326+
In other words, the proposed implementation focuses on safety and ease of use,
327+
and trades space and time for it.
328+
If it turns out that this is a problem, the implementation can be adjusted
329+
without breaking the API:
330+
331+
- The offset to the type-specific buffer can be stored, so
332+
``PyObject_GetTypeData`` effectively becomes
333+
``(char *)obj + cls->ht_typedataoffset``, possibly speeding things up at
334+
the cost of an extra pointer in the class.
335+
- Then, a new ``PyType_Slot`` can specify the desired alignment, to
336+
reduce space requirements for instances.
337+
- Alternatively, it might be possible to align ``tp_basicsize`` up at class
338+
creation/readying time.
339+
340+
341+
Rejected Ideas
342+
==============
343+
344+
None yet.
345+
346+
347+
Open Issues
348+
===========
349+
350+
Is negative basicsize the way to go? Should this be enabled by a flag instead?
351+
352+
353+
Copyright
354+
=========
355+
356+
This document is placed in the public domain or under the
357+
CC0-1.0-Universal license, whichever is more permissive.

0 commit comments

Comments
 (0)