diff --git a/docs/api.rst b/docs/api.rst index 3f30e53..2b61a8d 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -70,6 +70,11 @@ Python 3.15 On PyPy, always returns ``-1``. +.. c:function:: int PyUnstable_SetImmortal(PyObject *op) + + See `PyUnstable_SetImmortal() documentation `__. + + Availability: Python 3.13 and newer, not available on PyPy. Python 3.14 ----------- diff --git a/docs/changelog.rst b/docs/changelog.rst index 007a665..c68d88b 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,10 @@ Changelog ========= +* 2026-02-12: Add functions: + + * ``PyUnstable_SetImmortal()`` + * 2025-10-14: Add functions: * ``PyTuple_FromArray()`` diff --git a/pythoncapi_compat.h b/pythoncapi_compat.h index cdfdafa..e26185c 100644 --- a/pythoncapi_compat.h +++ b/pythoncapi_compat.h @@ -2659,6 +2659,37 @@ PyUnstable_Unicode_GET_CACHED_HASH(PyObject *op) } #endif +#if 0x030D0000 <= PY_VERSION_HEX && PY_VERSION_HEX < 0x030F00A7 && !defined(PYPY_VERSION) +// Immortal objects were implemented in Python 3.12, however there is no easy API +// to make objects immortal until 3.14 which has _Py_SetImmortal(). Since +// immortal objects are primarily needed for free-threading, this API is implemented +// for 3.14 using _Py_SetImmortal() and uses private macros on 3.13. +static inline int +PyUnstable_SetImmortal(PyObject *op) +{ + assert(op != NULL); + if (!PyUnstable_Object_IsUniquelyReferenced(op) || PyUnicode_Check(op)) { + return 0; + } +#if 0x030E0000 <= PY_VERSION_HEX + PyAPI_FUNC(void) _Py_SetImmortal(PyObject *op); + _Py_SetImmortal(op); +#else + // Python 3.13 doesn't export _Py_SetImmortal() function + if (PyObject_GC_IsTracked(op)) { + PyObject_GC_UnTrack(op); + } +#ifdef Py_GIL_DISABLED + op->ob_tid = _Py_UNOWNED_TID; + op->ob_ref_local = _Py_IMMORTAL_REFCNT_LOCAL; + op->ob_ref_shared = 0; +#else + op->ob_refcnt = _Py_IMMORTAL_REFCNT; +#endif +#endif + return 1; +} +#endif #ifdef __cplusplus } diff --git a/tests/test_pythoncapi_compat_cext.c b/tests/test_pythoncapi_compat_cext.c index e8ada23..6fd60b2 100644 --- a/tests/test_pythoncapi_compat_cext.c +++ b/tests/test_pythoncapi_compat_cext.c @@ -2489,6 +2489,39 @@ test_try_incref(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args)) Py_RETURN_NONE; } +#if 0x030D0000 <= PY_VERSION_HEX && !defined(PYPY_VERSION) +static PyObject * +test_set_immortal(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args)) +{ + PyObject object; + memset(&object, 0, sizeof(PyObject)); +#ifdef Py_GIL_DISABLED + object.ob_tid = _Py_ThreadId(); + object.ob_gc_bits = 0; + object.ob_ref_local = 1; + object.ob_ref_shared = 0; +#else + object.ob_refcnt = 1; +#endif + object.ob_type = &PyBaseObject_Type; + + int rc = PyUnstable_SetImmortal(&object); + assert(rc == 1); + Py_DECREF(&object); // should not dealloc + + // Check already immortal object + rc = PyUnstable_SetImmortal(&object); + assert(rc == 0); + + // Check unicode objects + PyObject *unicode = PyUnicode_FromString("test"); + rc = PyUnstable_SetImmortal(unicode); + assert(rc == 0); + Py_DECREF(unicode); + Py_RETURN_NONE; +} +#endif + static struct PyMethodDef methods[] = { {"test_object", test_object, METH_NOARGS, _Py_NULL}, @@ -2546,6 +2579,9 @@ static struct PyMethodDef methods[] = { {"test_byteswriter", test_byteswriter, METH_NOARGS, _Py_NULL}, {"test_tuple", test_tuple, METH_NOARGS, _Py_NULL}, {"test_try_incref", test_try_incref, METH_NOARGS, _Py_NULL}, +#if 0x030D0000 <= PY_VERSION_HEX && !defined(PYPY_VERSION) + {"test_set_immortal", test_set_immortal, METH_NOARGS, _Py_NULL}, +#endif {_Py_NULL, _Py_NULL, 0, _Py_NULL} };