Description
This is likely to need to end up as PEP, but I want to sketch out the idea here first.
General idea
All Python objects should share a common layout that makes the layout visible to the VM, and provides the performance needed by C extensions. In order to ensure that it is fast enough we should use it for internal classes, like tuples, lists and dicts. If it is fast enough for those classes, it will be good enough for NumPy and Cython. Persuading them to use a new API will be another issue, but we need to provide the capability first.
Requirements
Performance
Important classes ("important" must be user defined, or it won't get used) should be able to get at least the speed of manually defined C code. Because the VM will have visibility into the classes, we should be able to get better performance in some cases.
Composability
Most classes should be freely composable, including through multiple inheritance.
Flexibility
The design shouldn't put unnecessary constraints on either the VM implementation or the C extension code.
Conceptual interface
One layout per class
The layout of a Python object depends on its class. All instances of a class have the same layout, from the point of view of C extensions (the VM may use "object shapes" or some other optimization resulting in differing physical layout in a way that is not visible to C extensions.
C extensions define a C struct and requirements, not layout
When defining a C extension type, the C struct will be described to the VM in terms of size and alignment, and the details of accessible fields in it, much like PEP 697, but without the control over layout that PEP 697 gives. Layout is up to the VM.
Layouts aren't inherited
If class C inherits from class B, its layout is not inherited. In other words if class B
defines a struct S
, the offset of S
within an object C()
may differ from the offset of S
within an object B()
.
Extension class properties
Each extension defined class can have the following properties, which are not inherited.
- HasLength: The objects have length, e.g.
list
,tuple
- VariableSized: The struct is variable sized, e.g.
tuple
(but notdict
orlist
). ImpliesHasLength
- Primary: The struct is "important", performance matters more than the ability to support multiple inheritance well.
No class can have more than one class in its MRO (including itself) which each of the above property.
The C-API.
To get the offset of a struct:
intptr_t PyObject_GetStructOffset(PyTypeObject *self_class, PyTypeObject *declaring_class);
will return the offset, in bytes, from the owning PyObject *
to the requires struct.
The offset may be negative, a return value of -1 indicates an error.
PyObject_GetStructOffset
is a pure function of the triple (sys.implementation, self_class, declaring_class)
meaning that the offset may be cached globally during execution but should not be cached in source code or to disk.
The offset of the start of primary structs is an unstable API constant
const intptr_t PyUnstable_PRIMARY_STRUCT_OFFSET
is a compile-time constant (which I expected to always be positive, but lets leave that open for now)
This allows "primary" classes to get performance on a par with the current implementation of, for example, PyTupleObject
.
Declaring fields.
If we want to expose a C field to the VM, we need to declare its type, and offset.
In addition we want to declare various attributes, much as are already declared in PyMemberDef
One additional capability we would like to add, that PyMemberDef
doesn't have, is to have fields that can be directly read, but have a function setter, which suggests the following struct:
struct PyAttributeDef {
const char *name;
const char *doc;
uint32_t offset;
int16_t flags;
uint8_t type;
getter get;
setter set;
};
If get
is NULL, it implies the field can be read directly, unless the WRITE_ONLY
flag is set.
If set
is NULL, it implies the field can be written directly, unless the READ_ONLY
flag is set.
A possible implementation.
Note: this is not part of the spec. I include this only to demonstrate that this can be implemented. The actual implementation may well differ.
Objects are laid as follows:
- 0 or more non-variable-sized, non-primary structs
- The core object (class, refcount,
__dict__
/values,__weakrefs__
, etc.) - The primary struct (if there is one)
- The variable-sized struct (if there is one) (May be combined with the primary struct, if class is both primary and variable-sized.
The object pointer (PyObject *) points into the middle of the the core object, as it has done since the cycle GC was introduced.
The offset from the object pointer to the end of the core object will be a multiple of the maximum C alignment and will be PyUnstable_PRIMARY_STRUCT_OFFSET
.
Example.
We stated above that if the "Grand Unified Python Object Layout" could not support classes like tuple
or dict
, it would not be reasonable to expect C extensions to use it.
We would define tuple with the following struct:
struct tuple_data {
uintptr_t size;
PyObject *items[1];
};
And declare that tuple
is a primary, variable-sized type.
We can then define the accessor functions efficiently as:
static inline PyObject *PyTupleStruct_GetItem(struct tuple_data *tuple, intptr_t index) ...
PyObject *PyTuple_GetItem(PyObject *op, intptr_t index)
{
assert(PyTuple_Check(op));
struct tuple_data *tuple = (struct tuple_data *)(((unsigned char *)op) + PyUnstable_PRIMARY_STRUCT_OFFSET);
return PyTupleStruct_GetItem(tuple, index);
}
The length property could be defined as follows (we wouldn't do this in practice as ().length
isn't a thing)
struct PyAttributeDef tuple_length = {
.name = "length",
.doc = "the length of the tuple",
.offset = offsetof(struct tuple_data, size),
.flags = READ_ONLY,
.type = Py_T_UINTPTR_T
.get = NULL,
.set = NULL
};