Skip to content

Commit d23f570

Browse files
colesburyencukou
andauthored
gh-128844: Make _Py_TryIncref public as an unstable API. (#128926)
This exposes `_Py_TryIncref` as `PyUnstable_TryIncref()` and the helper function `_PyObject_SetMaybeWeakref` as `PyUnstable_EnableTryIncRef`. These are helpers for dealing with unowned references in a safe way, particularly in the free threading build. Co-authored-by: Petr Viktorin <[email protected]>
1 parent 7dd0a7e commit d23f570

File tree

6 files changed

+160
-0
lines changed

6 files changed

+160
-0
lines changed

Doc/c-api/object.rst

+81
Original file line numberDiff line numberDiff line change
@@ -624,3 +624,84 @@ Object Protocol
624624
be immortal in another.
625625
626626
.. versionadded:: next
627+
628+
.. c:function:: int PyUnstable_TryIncRef(PyObject *obj)
629+
630+
Increments the reference count of *obj* if it is not zero. Returns ``1``
631+
if the object's reference count was successfully incremented. Otherwise,
632+
this function returns ``0``.
633+
634+
:c:func:`PyUnstable_EnableTryIncRef` must have been called
635+
earlier on *obj* or this function may spuriously return ``0`` in the
636+
:term:`free threading` build.
637+
638+
This function is logically equivalent to the following C code, except that
639+
it behaves atomically in the :term:`free threading` build::
640+
641+
if (Py_REFCNT(op) > 0) {
642+
Py_INCREF(op);
643+
return 1;
644+
}
645+
return 0;
646+
647+
This is intended as a building block for managing weak references
648+
without the overhead of a Python :ref:`weak reference object <weakrefobjects>`.
649+
650+
Typically, correct use of this function requires support from *obj*'s
651+
deallocator (:c:member:`~PyTypeObject.tp_dealloc`).
652+
For example, the following sketch could be adapted to implement a
653+
"weakmap" that works like a :py:class:`~weakref.WeakValueDictionary`
654+
for a specific type:
655+
656+
.. code-block:: c
657+
658+
PyMutex mutex;
659+
660+
PyObject *
661+
add_entry(weakmap_key_type *key, PyObject *value)
662+
{
663+
PyUnstable_EnableTryIncRef(value);
664+
weakmap_type weakmap = ...;
665+
PyMutex_Lock(&mutex);
666+
weakmap_add_entry(weakmap, key, value);
667+
PyMutex_Unlock(&mutex);
668+
Py_RETURN_NONE;
669+
}
670+
671+
PyObject *
672+
get_value(weakmap_key_type *key)
673+
{
674+
weakmap_type weakmap = ...;
675+
PyMutex_Lock(&mutex);
676+
PyObject *result = weakmap_find(weakmap, key);
677+
if (PyUnstable_TryIncRef(result)) {
678+
// `result` is safe to use
679+
PyMutex_Unlock(&mutex);
680+
return result;
681+
}
682+
// if we get here, `result` is starting to be garbage-collected,
683+
// but has not been removed from the weakmap yet
684+
PyMutex_Unlock(&mutex);
685+
return NULL;
686+
}
687+
688+
// tp_dealloc function for weakmap values
689+
void
690+
value_dealloc(PyObject *value)
691+
{
692+
weakmap_type weakmap = ...;
693+
PyMutex_Lock(&mutex);
694+
weakmap_remove_value(weakmap, value);
695+
696+
...
697+
PyMutex_Unlock(&mutex);
698+
}
699+
700+
.. versionadded:: 3.14
701+
702+
.. c:function:: void PyUnstable_EnableTryIncRef(PyObject *obj)
703+
704+
Enables subsequent uses of :c:func:`PyUnstable_TryIncRef` on *obj*. The
705+
caller must hold a :term:`strong reference` to *obj* when calling this.
706+
707+
.. versionadded:: 3.14

Include/cpython/object.h

+6
Original file line numberDiff line numberDiff line change
@@ -544,3 +544,9 @@ PyAPI_FUNC(int) PyUnstable_Object_EnableDeferredRefcount(PyObject *);
544544

545545
/* Check whether the object is immortal. This cannot fail. */
546546
PyAPI_FUNC(int) PyUnstable_IsImmortal(PyObject *);
547+
548+
// Increments the reference count of the object, if it's not zero.
549+
// PyUnstable_EnableTryIncRef() should be called on the object
550+
// before calling this function in order to avoid spurious failures.
551+
PyAPI_FUNC(int) PyUnstable_TryIncRef(PyObject *);
552+
PyAPI_FUNC(void) PyUnstable_EnableTryIncRef(PyObject *);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Add :c:func:`PyUnstable_TryIncRef` and :c:func:`PyUnstable_EnableTryIncRef`
2+
unstable APIs. These are helpers for dealing with unowned references in
3+
a thread-safe way, particularly in the free threading build.

Modules/_testcapi/object.c

+54
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,59 @@ pyobject_enable_deferred_refcount(PyObject *self, PyObject *obj)
131131
return PyLong_FromLong(result);
132132
}
133133

134+
static int MyObject_dealloc_called = 0;
135+
136+
static void
137+
MyObject_dealloc(PyObject *op)
138+
{
139+
// PyUnstable_TryIncRef should return 0 if object is being deallocated
140+
assert(Py_REFCNT(op) == 0);
141+
assert(!PyUnstable_TryIncRef(op));
142+
assert(Py_REFCNT(op) == 0);
143+
144+
MyObject_dealloc_called++;
145+
Py_TYPE(op)->tp_free(op);
146+
}
147+
148+
static PyTypeObject MyType = {
149+
PyVarObject_HEAD_INIT(NULL, 0)
150+
.tp_name = "MyType",
151+
.tp_basicsize = sizeof(PyObject),
152+
.tp_dealloc = MyObject_dealloc,
153+
};
154+
155+
static PyObject *
156+
test_py_try_inc_ref(PyObject *self, PyObject *unused)
157+
{
158+
if (PyType_Ready(&MyType) < 0) {
159+
return NULL;
160+
}
161+
162+
MyObject_dealloc_called = 0;
163+
164+
PyObject *op = PyObject_New(PyObject, &MyType);
165+
if (op == NULL) {
166+
return NULL;
167+
}
168+
169+
PyUnstable_EnableTryIncRef(op);
170+
#ifdef Py_GIL_DISABLED
171+
// PyUnstable_EnableTryIncRef sets the shared flags to
172+
// `_Py_REF_MAYBE_WEAKREF` if the flags are currently zero to ensure that
173+
// the shared reference count is merged on deallocation.
174+
assert((op->ob_ref_shared & _Py_REF_SHARED_FLAG_MASK) >= _Py_REF_MAYBE_WEAKREF);
175+
#endif
176+
177+
if (!PyUnstable_TryIncRef(op)) {
178+
PyErr_SetString(PyExc_AssertionError, "PyUnstable_TryIncRef failed");
179+
Py_DECREF(op);
180+
return NULL;
181+
}
182+
Py_DECREF(op); // undo try-incref
183+
Py_DECREF(op); // dealloc
184+
assert(MyObject_dealloc_called == 1);
185+
Py_RETURN_NONE;
186+
}
134187

135188
static PyMethodDef test_methods[] = {
136189
{"call_pyobject_print", call_pyobject_print, METH_VARARGS},
@@ -139,6 +192,7 @@ static PyMethodDef test_methods[] = {
139192
{"pyobject_print_os_error", pyobject_print_os_error, METH_VARARGS},
140193
{"pyobject_clear_weakrefs_no_callbacks", pyobject_clear_weakrefs_no_callbacks, METH_O},
141194
{"pyobject_enable_deferred_refcount", pyobject_enable_deferred_refcount, METH_O},
195+
{"test_py_try_inc_ref", test_py_try_inc_ref, METH_NOARGS},
142196
{NULL},
143197
};
144198

Objects/object.c

+14
Original file line numberDiff line numberDiff line change
@@ -2588,6 +2588,20 @@ PyUnstable_Object_EnableDeferredRefcount(PyObject *op)
25882588
#endif
25892589
}
25902590

2591+
int
2592+
PyUnstable_TryIncRef(PyObject *op)
2593+
{
2594+
return _Py_TryIncref(op);
2595+
}
2596+
2597+
void
2598+
PyUnstable_EnableTryIncRef(PyObject *op)
2599+
{
2600+
#ifdef Py_GIL_DISABLED
2601+
_PyObject_SetMaybeWeakref(op);
2602+
#endif
2603+
}
2604+
25912605
void
25922606
_Py_ResurrectReference(PyObject *op)
25932607
{

Tools/c-analyzer/cpython/ignored.tsv

+2
Original file line numberDiff line numberDiff line change
@@ -447,6 +447,8 @@ Modules/_testcapi/exceptions.c - PyRecursingInfinitelyError_Type -
447447
Modules/_testcapi/heaptype.c - _testcapimodule -
448448
Modules/_testcapi/mem.c - FmData -
449449
Modules/_testcapi/mem.c - FmHook -
450+
Modules/_testcapi/object.c - MyObject_dealloc_called -
451+
Modules/_testcapi/object.c - MyType -
450452
Modules/_testcapi/structmember.c - test_structmembersType_OldAPI -
451453
Modules/_testcapi/watchers.c - g_dict_watch_events -
452454
Modules/_testcapi/watchers.c - g_dict_watchers_installed -

0 commit comments

Comments
 (0)