Introduce Py_TPFLAGS_CUSTOMCHSLOT

Now if I create a metaclass in c level, and create the heap type, I want to control some slots on this type. However, when a type which is the type instance’s tp_base was modified by change attribute (such as __getattribute__), python will change this type instance tp_getattro too without the agreement from the metaclass, which may cause memory leak or even other serious problem. In 3.12 python add the capi PyType_Watch, but it depends on PyType_AddWatcher which is not safe in free-threading (Type Objects — Python 3.13.12 documentation), and there is an upper limit to the total number of watchers

Here is the proposal:

  • #define Py_TPFLAGS_CUSTOMCHSLOT /*an int value*/

This flags can only be added in metaclass (the tp_base must be the subclass of type). It will be inherted in submetaclass. If this flags was set, it must define tp_chslot in metaclass. The defination of this tp slots is:

  • struct PySlotApply {
    getattrofunc tp_getattro;
    setattrofunc tp_setattro;
    // the other slots that may changed in type.__setattr__
    };
  • typedef int (*chslotfunc)(PyTypeObject *cls, Py_ssize_t slot, PySlotApply apply);
  • chslotfunc tp_chslot
  • #define Py_CHSLOT_GETATTRO /*an int value*/
  • #define Py_CHSLOT_SETATTRO /*an int value*/
  • // the other slots that may changed
  • int PyType_GenericChSlot(PyTypeObject* cls, Py_ssize_t slot, PySlotApply apply)

How to use

Whenever change type slots, visit Py_TYPE(cls):

static int
change_getattro_slot(PyTypeObject* cls, getattrofunc slot)
{
    PyTypeObject* mcls = Py_TYPE(cls);
    if (mcls->tp_flags & Py_TPFLAGS_CUSTOMCHSLOT) {
        PySlotApply apply;
        apply.tp_getattro = slot;
        if (mcls->tp_chslot) {
            return mcls->tp_chslot(cls, Py_CHSLOT_GETATTRO, apply);
        }
    }
    cls->tp_getattro = slot;
}

In metaclass, you can define the tp_chslot like:

static int
MyMetaClass_tp_chslot(PyTypeObject* cls, Py_ssize_t flag, PySlotApply apply)
{
    switch (flag) {
    case Py_CHSLOT_GETATTRO:
        // do what you want to do with apply.tp_getattro and your cls instance
        return 0; // 0 means success, 1 means failed
    case Py_CHSLOT_SETATTRO:
        // change to do with apply.tp_setattro
        return 0;
    default:
        return PyType_GenericChSlot(cls, flag, apply);
    }
}

The PyType_GenericChSlot is the generic function to change the type slots.

Change to use PyType_Slot:

#define Py_TPFLAGS_CUSTOMCHSLOT /*an int value*/
typedef int (*chslotfunc)(PyTypeObject* cls, PyType_Slot apply);
int PyType_GenericChSlot(PyTypeObject* cls, PyType_Slot apply);

The usage is similar. By this way the number of new c-apis can be smaller.