(These are rough personal notes; don't be fooled by PEP-like wording) ## Accessing the fields [add some background info here] [Sam's plan](https://github.com/python/cpython/issues/111506) was to use dynamic symbol lookup or weak symbols. I propose to not go with a platform-specific solution. Some of the functions have Python API, e.g. `Py_TYPE(o)` is `type(o)` in Python. I'm not proposing that: users can redefine `type` from Python, breaking type safety. Instead, I propose we add a [capsule](https://docs.python.org/3/c-api/capsule.html), `sys._abi_compat`. The capsule will contain a bit of version info and the addresses of new functions. (Note that users can break this from Python, most easily by `del sys._abi_compat`. I'd say a `Py_FatalError` is fair in that case, though. To get *wrong behaviour* rather than an aborted process, you'd need to create a fake capsule.) ```c typedef { uint32_t version; // set to Py_Version PyObject * (*func_Py_TYPE)(PyObject *o) Py_ssize_t (*func_Py_REFCNT)(PyObject *o) Py_ssize_t (*func_Py_SET_REFCNT)(PyObject *o) Py_ssize_t (*Py_SIZE)(PyObject *o) void (*Py_SET_SIZE)(PyObject *o, Py_ssize_t size) } struct _Py_abi_compat_capsule; ``` - Get the capsule using `PySys_GetObject` & `PyCapsule_GetPointer`. Failure is a fatal error. - If the capsule exists, get the result from it. (None of the fields may be NULL.) Since none of these macros are expected to report exceptions, any failure means `Py_FatalError`. ```c static inline int _Py_get_abi_compat_capsule(_Py_abi_compat_capsule **p_result) { int result = -1; static _Py_abi_compat_capsule *result = NULL; static PyMutex mutex; // (*if* this gets added to stable ABI) PyMutex_Lock(mutex); if (result) { result = 1; p_result = result; goto finally; } // Note that `sys` is special; we don't use `PyCapsule_Import` PyObject *capsule = PySys_GetObject("_abicompat") if (capsule) { result = (_Py_abi_compat_capsule*) PyCapsule_GetPointer(capsule, "sys._abicompat"); if (!result) { Py_FatalError("sys._abicompat unavailable"); } goto finally; } old_impl: PyObject *hexversion_obj = PySys_GetObject("hexversion"); if (!hexversion_obj) { PyMutex_Unlock(mutex); Py_FatalError("sys.hexversion unavailable"); goto finally; } long version = PyLong_AsLong(hexversion_obj); if (version < Py_LIMITED_API) { PyMutex_Unlock(mutex); if (!PyErr_Occurred) { Py_FatalError("sys._abicompat version mismatch"); } goto finally; } if (version > Py_PACK_VERSION(3, 14)) { PyMutex_Unlock(mutex); Py_FatalError("sys._abicompat version mismatch"); goto finally; } // result left NULL; result = 0; finally: PyMutex_Unlock(mutex); return result; } ``` ## Field-specific notes Best way to get/set a PyObject field (assuming the capsule is added), by lowest limited API one needs to support: * `ob_refcnt` * Increment/Decrement * 3.6+: - `Py_IncRef`/`Py_DecRef` have been functions since before 3.0 - Refcounting macros (`Py_[X]{INC|DEC|New}REF`) will call `Py_IncRef`/`Py_DecRef` * 3.10+: - `Py_[X]NewRef` are exported functions; called directly * 3.12+: - Macros that don't take NULL (`Py_{INC|DEC|New}REF`) call `_Py_IncRef`/`_Py_DecRef` instead * `Py_CLEAR` and `Py_[X]SETREF` remain macros (C-only). (Add their expansion to the documentation, for benefit of non-C languages.) * Get (`Py_REFCNT`): * 3.6+: Use capsule * 3.14+: exported function; called directly * Set (`Py_SET_REFCNT`): * 3.6+: Use capsule * 3.14+: exported function; called directly. * `ob_type` * Get (`Py_TYPE`) * 3.6+: Use capsule * 3.14+: exported function; called directly. * Set (`Py_SET_TYPE`): * Users should setattr `__class__` instead; this includes checks. * 3.6+: Use capsule * new: exported function; called directly. * `ob_base` - cast to `PyObject*` * `ob_size` * Get (`Py_SIZE`) * 3.6+: Use capsule * new: exported function; called directly. * *Note that many types make this available via `PyObject_Size` (`len(o)` in Python), or specialized functions (`PyTuple_Size`). These should be preferred; how an object uses `ob_size` for size information should generally be treated as its implementation detail.* * Set (`Py_SET_SIZE`) * 3.6+: Use capsule * new: exported function; called directly. Existing API: * `ob_refcnt` * Increment/decrement: * 3.6+: `Py_IncRef`/`Py_DecRef` * 3.10+: `Py_NewRef` & `Py_XNewRef` * 3.12+: `Py_INCREF`/`Py_DECREF` macros (→ `_Py_IncRef`/`_Py_DecRef` in 3.10+; `Py_IncRef`/`Py_DecRef`) * macro: `Py_XINCREF`/`Py_XDECREF` (→ `Py_IncRef`/`Py_DecRef`) * Get: * 3.6+: `sys.getrefcount(o)` in Python * 3.14+: `Py_REFCNT` * Set: * NEW: `Py_SetRefcnt` * 3.13: `Py_SET_REFCNT` macro (→ `_Py_SetRefcnt`) * `ob_type` * Get * `type(o)` in Python * 3.14+: `Py_TYPE` * Set * setattr `__class__` * 3.14+: Py_SET_TYPE` * `ob_base` - cast to `PyObject*` * `ob_size` * Get * NEW: `Py_GetSize` * Many types make this available via `len(o)` * `Py_SIZE` to call `Py_GetSize`, on failure clear the exception & return -1 * Set * NEW: `Py_SetSize` * `Py_GET_SIZE` to call `Py_SetSize`, on failure clear the exception