(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