-
Notifications
You must be signed in to change notification settings - Fork 0
Description
One of the issues we've seen in the existing API is that we grow more specific functions over time, for performance and other reasons.
For example, PyObject_GetItem is theoretically sufficient for all get-item scenarios, and yet we offer a variety of public APIs that streamline certain operations.12
So I want to propose an API that lets us extend these into the future, allowing even more special/optimised cases without causing undue burden on maintainers. Names are obviously all going to be bikeshedded, and I suspect this will fit nicely into one of the other proposals going around, but this is the aspect I personally care most about so happy to see it merged into another one. But until then, I'm going to call it PyObject_GetSlots.
The basic idea is that each object may return a struct containing function pointers that are specific to its current instance, based on the argument given by the caller. Thinking about how you might perform __getitem__, here's a more fully spelled out example (again, using current naming/objects, but could be adapted to fit a new API design):
// These definitions would be #included from our own headers
// I don't particularly care what type the slot names are, provided they're statically linked into the caller
typedef (...) PySlotName;
#define PY_SLOT_GETITEM (...)
#define PY_SLOT_GETITEM_STR (...)
#define PY_SLOT_GETITEM_INT (...)
struct PySlots_Base {
int (*release)(struct PySlots *self);
};
struct PySlots {
void *handle;
struct PySlots_Base *slots;
} PySlots;
PyAPI_FUNC(int) PyObject_GetSlots(PyObject *o, PySlotName name, PySlots *slots);
static inline int PySlots_Release(PySlots *slots) {
return (*slots->slots->release)(slots);
}
struct PySlot_GetItem {
struct PySlots_Base base;
PyObject * (*getitem)(PySlots *self, PyObject *key);
};
struct PySlot_GetItemString {
struct PySlots_Base base;
PyObject * (*getitem_utf8)(struct PySlot_GetItemString *self, const char *key);
PyObject * (*getitem_utf16)(struct PySlot_GetItemString *self, const wchar_t *key);
};
struct PySlot_GetItemInt {
struct PySlots_Base base;
PyObject * (*getitem_int)(struct PySlot_GetItemInt *self, int key);
PyObject * (*getitem_ssize_t)(struct PySlot_GetItemInt *self, Py_ssize_t key);
};
// This would be in user's code (or maybe even a header or static library)
void f(PyObject *dict, ssize_t k) {
PyObject *value;
PySlots dict_getitem;
if (PyObject_GetSlots(dict, PY_SLOT_GETITEM_INT, &dict_getitem))) {
// Obtained the fast path
value = (*((struct PySlots_GetItemInt *)dict_getitem.slots)->getitem_ssize_t)(&dict_getitem, k);
PySlots_Release(&dict_getitem);
} else if (PyObject_GetSlots(dict, PY_SLOT_GETITEM, &dict_getitem)) {
// Fast path not available on this version, use the slow path
PyObject *key = PyLong_FromSSizeT(k);
value = (*((struct PySlots_GetItem *)dict_getitem.slots)->getitem)(&dict_getitem, key);
PySlots_Release(&dict_getitem);
Py_DECREF(key);
} else {
// should be unreachable unless we deprecate/remove PY_SLOT_GETITEM
}
// handle value==NULL or else go on and use it
}Key points:
- compiling with a newer Python release gives you access to more slot types/names, and they are embedded in your build
- explicit checks for whether the object supports the requested slot, so callers know they need fallbacks
- the check can be based on values, which means a dict can refuse
PySlot_GetItemStrif it has non-str keys - slots must be released, which means objects can do allocations on demand
- you pass the slots back into its functions, not the original object, so it doesn't strictly have to return itself
- the object provides the slot struct memory (allocated or static, doesn't matter)
The biggest downside is that it can get to be really messy C code when used directly (I'm not 100% sure I typed it right in my example), but headers/macros and/or a static library could really help. As long as it's statically compiled into the caller, and not part of the Python runtime itself, or it will break on earlier versions.