From b09013757e71295ffeed5250e4607b2c1056c38a Mon Sep 17 00:00:00 2001 From: Sam Gross Date: Tue, 4 Nov 2025 11:02:15 -0500 Subject: [PATCH 1/4] Add PyUnstable_TryIncref and PyUnstable_EnableTryIncRef. --- pythoncapi_compat.h | 70 +++++++++++++++++++++++++++++ tests/test_pythoncapi_compat_cext.c | 48 ++++++++++++++++++++ 2 files changed, 118 insertions(+) diff --git a/pythoncapi_compat.h b/pythoncapi_compat.h index b16075f..8f0f9c1 100644 --- a/pythoncapi_compat.h +++ b/pythoncapi_compat.h @@ -2231,6 +2231,76 @@ static inline int PyUnstable_Object_IsUniquelyReferenced(PyObject *obj) } #endif +// gh-128926 added PyUnstable_TryIncRef() and PyUnstable_EnableTryIncRef() to +// Python 3.14.0a5. Adapted from _Py_TryIncref and _PyObject_SetMaybeWeakref. +#if PY_VERSION_HEX < 0x030E00A5 +static inline int PyUnstable_TryIncRef(PyObject *op) +{ +#ifndef Py_GIL_DISABLED + if (Py_REFCNT(op) > 0) { + Py_INCREF(op); + return 1; + } + return 0; +#else + uint32_t local = _Py_atomic_load_uint32_relaxed(&op->ob_ref_local); + local += 1; + if (local == 0) { + return 1; + } + if (_Py_IsOwnedByCurrentThread(op)) { + _Py_INCREF_STAT_INC(); + _Py_atomic_store_uint32_relaxed(&op->ob_ref_local, local); +#ifdef Py_REF_DEBUG + _Py_INCREF_IncRefTotal(); +#endif + return 1; + } + Py_ssize_t shared = _Py_atomic_load_ssize_relaxed(&op->ob_ref_shared); + for (;;) { + // If the shared refcount is zero and the object is either merged + // or may not have weak references, then we cannot incref it. + if (shared == 0 || shared == _Py_REF_MERGED) { + return 0; + } + + if (_Py_atomic_compare_exchange_ssize( + &op->ob_ref_shared, + &shared, + shared + (1 << _Py_REF_SHARED_SHIFT))) { +#ifdef Py_REF_DEBUG + _Py_INCREF_IncRefTotal(); +#endif + _Py_INCREF_STAT_INC(); + return 1; + } + } +#endif +} + +static inline void PyUnstable_EnableTryIncRef(PyObject *op) +{ +#ifdef Py_GIL_DISABLED + if (_Py_IsImmortal(op)) { + return; + } + for (;;) { + Py_ssize_t shared = _Py_atomic_load_ssize_relaxed(&op->ob_ref_shared); + if ((shared & _Py_REF_SHARED_FLAG_MASK) != 0) { + // Nothing to do if it's in WEAKREFS, QUEUED, or MERGED states. + return; + } + if (_Py_atomic_compare_exchange_ssize( + &op->ob_ref_shared, &shared, shared | _Py_REF_MAYBE_WEAKREF)) { + return; + } + } +#else + (void)op; // unused argument +#endif +} +#endif + #if PY_VERSION_HEX < 0x030F0000 static inline PyObject* diff --git a/tests/test_pythoncapi_compat_cext.c b/tests/test_pythoncapi_compat_cext.c index fed0822..4a4d76e 100644 --- a/tests/test_pythoncapi_compat_cext.c +++ b/tests/test_pythoncapi_compat_cext.c @@ -2448,6 +2448,46 @@ test_tuple(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args)) return test_tuple_fromarray(); } +// Test adapted from CPython's _testcapi/object.c +static int MyObject_dealloc_called = 0; + +static void +MyObject_dealloc(PyObject *op) +{ + // PyUnstable_TryIncRef should return 0 if object is being deallocated + assert(Py_REFCNT(op) == 0); + assert(!PyUnstable_TryIncRef(op)); + assert(Py_REFCNT(op) == 0); + + MyObject_dealloc_called++; + Py_TYPE(op)->tp_free(op); +} + +static PyTypeObject MyType; + +static PyObject* +test_try_incref(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args)) +{ + MyObject_dealloc_called = 0; + + PyObject *obj = PyObject_New(PyObject, &MyType); + if (obj == _Py_NULL) { + return _Py_NULL; + } + + PyUnstable_EnableTryIncRef(obj); + + Py_ssize_t refcount = Py_REFCNT(obj); + assert(PyUnstable_TryIncRef(obj)); + assert(Py_REFCNT(obj) == refcount + 1); + + Py_DECREF(obj); + Py_DECREF(obj); + + assert(MyObject_dealloc_called == 1); + Py_RETURN_NONE; +} + static struct PyMethodDef methods[] = { {"test_object", test_object, METH_NOARGS, _Py_NULL}, @@ -2504,6 +2544,7 @@ static struct PyMethodDef methods[] = { {"test_uniquely_referenced", test_uniquely_referenced, METH_NOARGS, _Py_NULL}, {"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}, {_Py_NULL, _Py_NULL, 0, _Py_NULL} }; @@ -2532,6 +2573,13 @@ module_exec(PyObject *module) return -1; } #endif + MyType.tp_name = "MyType"; + MyType.tp_basicsize = sizeof(PyObject); + MyType.tp_dealloc = MyObject_dealloc; + MyType.tp_free = PyObject_Del; + if (PyType_Ready(&MyType) < 0) { + return -1; + } return 0; } From 6240ab3b9ad4e34eed1cfda0fe3a35441364bfc1 Mon Sep 17 00:00:00 2001 From: Sam Gross Date: Wed, 5 Nov 2025 16:44:35 -0500 Subject: [PATCH 2/4] Apply suggestions from code review Co-authored-by: Victor Stinner --- pythoncapi_compat.h | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pythoncapi_compat.h b/pythoncapi_compat.h index 8f0f9c1..04378bd 100644 --- a/pythoncapi_compat.h +++ b/pythoncapi_compat.h @@ -2232,7 +2232,7 @@ static inline int PyUnstable_Object_IsUniquelyReferenced(PyObject *obj) #endif // gh-128926 added PyUnstable_TryIncRef() and PyUnstable_EnableTryIncRef() to -// Python 3.14.0a5. Adapted from _Py_TryIncref and _PyObject_SetMaybeWeakref. +// Python 3.14.0a5. Adapted from _Py_TryIncref() and _PyObject_SetMaybeWeakref(). #if PY_VERSION_HEX < 0x030E00A5 static inline int PyUnstable_TryIncRef(PyObject *op) { @@ -2243,9 +2243,11 @@ static inline int PyUnstable_TryIncRef(PyObject *op) } return 0; #else + // _Py_TryIncrefFast() uint32_t local = _Py_atomic_load_uint32_relaxed(&op->ob_ref_local); local += 1; if (local == 0) { + // immortal return 1; } if (_Py_IsOwnedByCurrentThread(op)) { @@ -2256,6 +2258,8 @@ static inline int PyUnstable_TryIncRef(PyObject *op) #endif return 1; } + + // _Py_TryIncRefShared() Py_ssize_t shared = _Py_atomic_load_ssize_relaxed(&op->ob_ref_shared); for (;;) { // If the shared refcount is zero and the object is either merged @@ -2281,6 +2285,7 @@ static inline int PyUnstable_TryIncRef(PyObject *op) static inline void PyUnstable_EnableTryIncRef(PyObject *op) { #ifdef Py_GIL_DISABLED + // _PyObject_SetMaybeWeakref() if (_Py_IsImmortal(op)) { return; } From ac639f69d9750164acf42d7562a29b2c9f6a3664 Mon Sep 17 00:00:00 2001 From: Sam Gross Date: Wed, 5 Nov 2025 16:45:14 -0500 Subject: [PATCH 3/4] Rename MyType to TryIncrefType --- tests/test_pythoncapi_compat_cext.c | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/test_pythoncapi_compat_cext.c b/tests/test_pythoncapi_compat_cext.c index 4a4d76e..28fb428 100644 --- a/tests/test_pythoncapi_compat_cext.c +++ b/tests/test_pythoncapi_compat_cext.c @@ -2463,14 +2463,14 @@ MyObject_dealloc(PyObject *op) Py_TYPE(op)->tp_free(op); } -static PyTypeObject MyType; +static PyTypeObject TryIncrefType; static PyObject* test_try_incref(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args)) { MyObject_dealloc_called = 0; - PyObject *obj = PyObject_New(PyObject, &MyType); + PyObject *obj = PyObject_New(PyObject, &TryIncrefType); if (obj == _Py_NULL) { return _Py_NULL; } @@ -2573,11 +2573,11 @@ module_exec(PyObject *module) return -1; } #endif - MyType.tp_name = "MyType"; - MyType.tp_basicsize = sizeof(PyObject); - MyType.tp_dealloc = MyObject_dealloc; - MyType.tp_free = PyObject_Del; - if (PyType_Ready(&MyType) < 0) { + TryIncrefType.tp_name = "TryIncrefType"; + TryIncrefType.tp_basicsize = sizeof(PyObject); + TryIncrefType.tp_dealloc = MyObject_dealloc; + TryIncrefType.tp_free = PyObject_Del; + if (PyType_Ready(&TryIncrefType) < 0) { return -1; } return 0; From 6d697809fc9f80e49d7fde93109a78a8e6848afc Mon Sep 17 00:00:00 2001 From: Sam Gross Date: Wed, 5 Nov 2025 16:56:29 -0500 Subject: [PATCH 4/4] Rename MyObject to TryIncref --- tests/test_pythoncapi_compat_cext.c | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/test_pythoncapi_compat_cext.c b/tests/test_pythoncapi_compat_cext.c index 28fb428..7d9fea4 100644 --- a/tests/test_pythoncapi_compat_cext.c +++ b/tests/test_pythoncapi_compat_cext.c @@ -2449,17 +2449,17 @@ test_tuple(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args)) } // Test adapted from CPython's _testcapi/object.c -static int MyObject_dealloc_called = 0; +static int TryIncref_dealloc_called = 0; static void -MyObject_dealloc(PyObject *op) +TryIncref_dealloc(PyObject *op) { // PyUnstable_TryIncRef should return 0 if object is being deallocated assert(Py_REFCNT(op) == 0); assert(!PyUnstable_TryIncRef(op)); assert(Py_REFCNT(op) == 0); - MyObject_dealloc_called++; + TryIncref_dealloc_called++; Py_TYPE(op)->tp_free(op); } @@ -2468,7 +2468,7 @@ static PyTypeObject TryIncrefType; static PyObject* test_try_incref(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args)) { - MyObject_dealloc_called = 0; + TryIncref_dealloc_called = 0; PyObject *obj = PyObject_New(PyObject, &TryIncrefType); if (obj == _Py_NULL) { @@ -2484,7 +2484,7 @@ test_try_incref(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args)) Py_DECREF(obj); Py_DECREF(obj); - assert(MyObject_dealloc_called == 1); + assert(TryIncref_dealloc_called == 1); Py_RETURN_NONE; } @@ -2575,7 +2575,7 @@ module_exec(PyObject *module) #endif TryIncrefType.tp_name = "TryIncrefType"; TryIncrefType.tp_basicsize = sizeof(PyObject); - TryIncrefType.tp_dealloc = MyObject_dealloc; + TryIncrefType.tp_dealloc = TryIncref_dealloc; TryIncrefType.tp_free = PyObject_Del; if (PyType_Ready(&TryIncrefType) < 0) { return -1;