Skip to content

gh-159558: Avoid re-entering an already held critical section in ctypes setters - #159562

Open
johng wants to merge 4 commits into
python:mainfrom
johng:ft-ctypes-reentry
Open

johng wants to merge 4 commits into
python:mainfrom
johng:ft-ctypes-reentry

Conversation

@johng

@johng johng commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Fixing re-entering an already held lock in ctypes. This involves creating lock free version of implementation and also providing wrappers which also lock for callers that do not already hold a critical section.

Micro benchmark estimates.

Operation main this PR
c_int().value = 1 35 ns 22 ns
c_char_p().value = b"x" 39 ns 22 ns
pointer.contents = c_int(2) 86 ns 58 ns
Structure.i = 1 38 ns 24 ns
Structure.p = b"x" 69 ns 54 ns

johng added 4 commits October 16, 2026 18:23
…ree-threaded build

KeepRef() takes the critical section on its target so that it is safe
from any caller, and for a type that keeps a reference to the stored
object it then calls PyCData_GetContainer(), which takes the critical
section on the root object.  The Simple.value setter already holds the
lock on self, taken by its Argument Clinic wrapper, and for a top-level
object the root is self as well, so `c_char_p().value = b"x"` entered the
same critical section three times and `c_int().value = 1` twice (the
"nothing to keep" early return sits inside the locked region).

Add PyCData_GetContainer_lock_held() for a caller that holds the root's
lock, use it from KeepRef_lock_held() when the target is its own root,
and call KeepRef_lock_held() from the setter.

Measured on the free-threaded build: c_int().value = 1 35 -> 22 ns,
c_char_p().value = b"x" 39 -> 22 ns.

(cherry picked from commit 5ca398f)
…nts setter

The setter runs under a two-object critical section on the pointer and
the new target, then calls KeepRef() twice on the pointer and
GetKeepedObjects() on the target; both helpers lock their argument
again.  Call the lock-held variants instead, adding
GetKeepedObjects_lock_held() for the target.

Measured on the free-threaded build: p.contents = c_int(2) 86 -> 58 ns.

(cherry picked from commit 576731e)
PyCData_set() already requires its caller to hold the critical section
on the destination object and asserts it, but finished by calling
KeepRef(), which takes that lock again.  Every store of a Python object
into a Structure or Union field, an Array item or a Pointer item went
through it.  Call KeepRef_lock_held() instead.

Measured on the free-threaded build: Structure int field assignment
38 -> 24 ns, c_char_p field assignment 69 -> 54 ns.

(cherry picked from commit ead0976)
@bedevere-app bedevere-app Bot added the type-feature A feature request or enhancement label Oct 16, 2026
@picnixz
picnixz requested a review from ZeroIntensity October 16, 2026 17:38
@ZeroIntensity

ZeroIntensity commented Oct 17, 2026 •

Copy link
Copy Markdown
Member

Can I see the full benchmark scripts that were used? I'm not opposed to doing this, but the critical section API already has an optimization for exactly this kind of thing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

awaiting review type-feature A feature request or enhancement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants