Skip to content

Forward-compatibility for special case APIs #4

@zooba

Description

@zooba

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_GetItemStr if 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.

Footnotes

  1. Particularly for dict objects, but those are largely due to back-compat with design flaws we made in the past.

  2. I recognise many of these are implementations of type-specific handlers for GetItem, but even if we wanted to make them internal now, we can't and wouldn't.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions