From 9526716c82fecc5b3bd51c339674214734affeaa Mon Sep 17 00:00:00 2001 From: Florian Angerer Date: Mon, 4 May 2026 15:30:48 +0200 Subject: [PATCH 1/3] Fix: XDecRefPointerNode and add regression test --- .../src/tests/cpyext/test_list.py | 48 +++++++++++++++---- .../builtins/objects/cext/capi/CExtNodes.java | 20 +++++--- 2 files changed, 51 insertions(+), 17 deletions(-) diff --git a/graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_list.py b/graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_list.py index 98b30b217c..613645a90c 100644 --- a/graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_list.py +++ b/graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_list.py @@ -1,4 +1,4 @@ -# Copyright (c) 2018, 2025, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2018, 2026, Oracle and/or its affiliates. All rights reserved. # DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. # # The Universal Permissive License (UPL), Version 1.0 @@ -167,36 +167,64 @@ class TestPyList(CPyExtTestCase): test_PyList_SetItem = CPyExtFunction( _reference_setitem, lambda: ( - (4, 0, 0), - (4, 3, 5), + (4, 0, 0, False), + (4, 3, 5, False), + (4, 0, 0, True), + (4, 3, 5, True), ), - code='''PyObject* wrap_PyList_SetItem(Py_ssize_t capacity, Py_ssize_t idx, PyObject* new_item) { + code='''PyObject* wrap_PyList_SetItem(Py_ssize_t capacity, Py_ssize_t idx, PyObject* new_item, int init_with_none) { PyObject *newList = PyList_New(capacity); Py_ssize_t i; for (i = 0; i < capacity; i++) { - if (i == idx) { - Py_INCREF(new_item); - PyList_SetItem(newList, i, new_item); - } else { + if (init_with_none || i != idx) { Py_INCREF(Py_None); PyList_SetItem(newList, i, Py_None); } } + Py_INCREF(new_item); + PyList_SetItem(newList, idx, new_item); return newList; } ''', resultspec="O", - argspec='nnO', - arguments=["Py_ssize_t capacity", "Py_ssize_t size", "PyObject* new_item"], + argspec='nnOp', + arguments=["Py_ssize_t capacity", "Py_ssize_t size", "PyObject* new_item", "int init_with_none"], callfunction="wrap_PyList_SetItem", cmpfunc=unhandled_error_compare ) + test_native_list_setitem_decrefs_none = CPyExtFunction( + lambda args: [args[0]], + lambda: ( + ("new item",), + ), + code='''PyObject* wrap_native_list_setitem_decrefs_none(PyObject* new_item) { + PyObject *list = PyList_New(1); + if (list == NULL) { + return NULL; + } + Py_INCREF(Py_None); + PyList_SET_ITEM(list, 0, Py_None); + if (PySequence_SetItem(list, 0, new_item) < 0) { + Py_DECREF(list); + return NULL; + } + return list; + } + ''', + resultspec="O", + argspec='O', + arguments=["PyObject* new_item"], + callfunction="wrap_native_list_setitem_decrefs_none", + cmpfunc=unhandled_error_compare + ) + test_PyList_SET_ITEM = CPyExtFunction( _wrap_list_fun(_reference_SET_ITEM), lambda: ( ([1,2,3,4], 0, _reference_SET_ITEM), ([1,2,3,4], 3, _reference_SET_ITEM), + ([None], 0, 0), ), code='''PyObject* wrap_PyList_SET_ITEM(PyObject* op, Py_ssize_t idx, PyObject* newitem) { Py_INCREF(newitem); diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/capi/CExtNodes.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/capi/CExtNodes.java index 8f96081b8b..a8878d693b 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/capi/CExtNodes.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/capi/CExtNodes.java @@ -852,23 +852,29 @@ static void doDecref(Node inliningTarget, long pointer, if (pointer == NULLPTR) { return; } + // Boxed primitive values do not have a native reference count. if (HandlePointerConverter.pointsToPyFloatHandle(pointer) || HandlePointerConverter.pointsToPyIntHandle(pointer)) { return; } Object object = toPythonNode.execute(inliningTarget, pointer, false); - if (object instanceof PythonObject pythonObject) { - isWrapperProfile.enter(inliningTarget); - updateRefNode.execute(inliningTarget, pythonObject, pythonObject.decRef()); - } else if (CApiContext.isSpecialSingleton(object)) { - isSpecialSingletonProfile.enter(inliningTarget); - } else { - assert object instanceof PythonAbstractNativeObject; + if (object instanceof PythonAbstractNativeObject) { + // Native objects use their native reference count and deallocator. if (CApiTransitions.subNativeRefCount(pointer, 1) == 0) { PythonContext context = PythonContext.get(inliningTarget); var callable = CApiContext.getNativeSymbol(inliningTarget, FUN_PY_DEALLOC); ExternalFunctionInvoker.invokePY_DEALLOC(null, C_API_TIMING, context.ensureNativeContext(), BoundaryCallData.getUncached(), context.getThreadState(PythonLanguage.get(inliningTarget)), callable, pointer); } + } else if (CApiContext.isSpecialSingleton(object)) { + // Special singletons such as PNone are immortal handle-space objects. + isSpecialSingletonProfile.enter(inliningTarget); + } else if (object instanceof PythonObject pythonObject) { + // Managed Python objects keep their reference count in the wrapper. + isWrapperProfile.enter(inliningTarget); + updateRefNode.execute(inliningTarget, pythonObject, pythonObject.decRef()); + } else { + CompilerDirectives.transferToInterpreterAndInvalidate(); + throw CompilerDirectives.shouldNotReachHere("unexpected object for decref: " + object); } } } From 604c8fb3d096ee2fd90e55599099909230c1d5b3 Mon Sep 17 00:00:00 2001 From: Florian Angerer Date: Mon, 4 May 2026 23:24:02 +0200 Subject: [PATCH 2/3] Attach/detach thread in PyGILState_Ensure/Release --- .../com.oracle.graal.python.cext/src/capi.c | 30 ++---- .../com.oracle.graal.python.cext/src/capi.h | 10 ++ .../src/pystate.c | 60 ++++++------ .../src/tests/cpyext/test_thread.py | 3 +- .../cext/PythonCextPyStateBuiltins.java | 96 ++++++++----------- .../objects/cext/capi/CApiContext.java | 77 ++++++++++++++- .../cext/capi/ExternalFunctionSignature.java | 4 +- .../objects/cext/capi/PThreadState.java | 11 ++- .../objects/cext/structs/CConstants.java | 21 ++-- .../graal/python/runtime/PythonContext.java | 43 ++++++++- 10 files changed, 235 insertions(+), 120 deletions(-) diff --git a/graalpython/com.oracle.graal.python.cext/src/capi.c b/graalpython/com.oracle.graal.python.cext/src/capi.c index 3c74df3598..914e96805c 100644 --- a/graalpython/com.oracle.graal.python.cext/src/capi.c +++ b/graalpython/com.oracle.graal.python.cext/src/capi.c @@ -574,26 +574,12 @@ void initialize_hashes(); void _PyFloat_InitState(PyInterpreterState* state); /* - * This is used to allow Truffle to enter/leave the context on native threads - * that were not created by NFI/Truffle/Java and thus not previously attached - * to the context. See e.g. PyGILState_Ensure. This is used by some C - * extensions to allow calling Python APIs from natively created threads. This - * poses a problem if multiple contexts use the same library, since we cannot - * know which context should be entered. CPython has the same problem (see - * https://docs.python.org/3/c-api/init.html#bugs-and-caveats), in particular - * the following quote: - * - * Furthermore, extensions (such as ctypes) using these APIs to allow calling - * of Python code from non-Python created threads will probably be broken - * when using sub-interpreters. - * - * If we try to use the same libpython for multiple contexts, we can only - * behave in a similar (likely broken) way as CPython: natively created threads - * that use the PyGIL_* APIs to allow calling into Python will attach to the - * first interpreter that initialized the C API (and thus set the - * TRUFFLE_CONTEXT pointer) only. + * These functions allow Truffle to enter/leave the context on native threads + * that were not created by Truffle/Java and thus do not have a previously + * entered polyglot context. See e.g. PyGILState_Ensure. */ -Py_LOCAL_SYMBOL TruffleContext* TRUFFLE_CONTEXT; +Py_LOCAL_SYMBOL graalpy_attach_native_thread_func graalpy_attach_native_thread = NULL; +Py_LOCAL_SYMBOL graalpy_detach_native_thread_func graalpy_detach_native_thread = NULL; /* * This is only set during VM shutdown, so on the native side can only be used @@ -603,10 +589,14 @@ Py_LOCAL_SYMBOL TruffleContext* TRUFFLE_CONTEXT; */ Py_LOCAL_SYMBOL int8_t *_graalpy_finalizing = NULL; -PyAPI_FUNC(PyThreadState **) initialize_graal_capi(void **builtin_closures, GCState *gc, PyThreadState *tstate) { +PyAPI_FUNC(PyThreadState **) initialize_graal_capi(void **builtin_closures, GCState *gc, + PyThreadState *tstate, graalpy_attach_native_thread_func attach_native_thread, + graalpy_detach_native_thread_func detach_native_thread) { clock_t t = clock(); _PyGC_InitState(gc); + graalpy_attach_native_thread = attach_native_thread; + graalpy_detach_native_thread = detach_native_thread; /* * Initializing all these global fields with pointers to different contexts diff --git a/graalpython/com.oracle.graal.python.cext/src/capi.h b/graalpython/com.oracle.graal.python.cext/src/capi.h index 151a843299..345d17efd9 100644 --- a/graalpython/com.oracle.graal.python.cext/src/capi.h +++ b/graalpython/com.oracle.graal.python.cext/src/capi.h @@ -65,6 +65,16 @@ #error "don't know how to declare thread local variable" #endif +typedef int (*graalpy_attach_native_thread_func)(void); +typedef void (*graalpy_detach_native_thread_func)(void); + +#define GRAALPY_ATTACH_NATIVE_FAILED (-1) +#define GRAALPY_ATTACH_NATIVE_OWNED 1 +#define GRAALPY_ATTACH_NATIVE_FOREIGN 2 + +extern graalpy_attach_native_thread_func graalpy_attach_native_thread; +extern graalpy_detach_native_thread_func graalpy_detach_native_thread; + #ifdef MS_WINDOWS // define the below, otherwise windows' sdk defines complex to _complex and breaks us #define _COMPLEX_DEFINED diff --git a/graalpython/com.oracle.graal.python.cext/src/pystate.c b/graalpython/com.oracle.graal.python.cext/src/pystate.c index 9a5c1872d1..5d4a72b997 100644 --- a/graalpython/com.oracle.graal.python.cext/src/pystate.c +++ b/graalpython/com.oracle.graal.python.cext/src/pystate.c @@ -31,7 +31,6 @@ #include "capi.h" #include -extern TruffleContext* TRUFFLE_CONTEXT; static THREAD_LOCAL int graalpy_attached_thread = 0; static THREAD_LOCAL int graalpy_gilstate_counter = 0; @@ -2290,22 +2289,22 @@ PyGILState_Check(void) return (tstate == gilstate_tss_get(runtime)); #else // GraalPy change - int attached = 0; /* * PyGILState_Check is allowed to be called from a new thread that didn't yet setup the GIL. - * If we don't attach the thread ourselves, the upcall will work because NFI will attach - * the thread automatically, but it won't create the context which would then break - * subsequent PyGILState_Ensure. + * We must attach it before calling into Java so the upcall has an entered polyglot context. */ - if (TRUFFLE_CONTEXT) { - if ((*TRUFFLE_CONTEXT)->getTruffleEnv(TRUFFLE_CONTEXT) == NULL) { - (*TRUFFLE_CONTEXT)->attachCurrentThread(TRUFFLE_CONTEXT); - attached = 1; + int attached = 0; + if (graalpy_attach_native_thread) { + attached = graalpy_attach_native_thread(); + if (attached == GRAALPY_ATTACH_NATIVE_FAILED) { + return 0; } + } else { + return 0; } int ret = GraalPyPrivate_GILState_Check(); - if (attached) { - (*TRUFFLE_CONTEXT)->detachCurrentThread(TRUFFLE_CONTEXT); + if (attached == GRAALPY_ATTACH_NATIVE_OWNED && graalpy_detach_native_thread) { + graalpy_detach_native_thread(); } return ret; #endif // GraalPy change @@ -2362,13 +2361,18 @@ PyGILState_Ensure(void) return has_gil ? PyGILState_LOCKED : PyGILState_UNLOCKED; #else // GraalPy change - if (TRUFFLE_CONTEXT) { - if ((*TRUFFLE_CONTEXT)->getTruffleEnv(TRUFFLE_CONTEXT) == NULL) { - (*TRUFFLE_CONTEXT)->attachCurrentThread(TRUFFLE_CONTEXT); + if (!graalpy_attach_native_thread) { + Py_FatalError("PyGILState_Ensure called before GraalPy C API initialization"); + } + if (!graalpy_attached_thread) { + int attached = graalpy_attach_native_thread(); + if (attached == GRAALPY_ATTACH_NATIVE_FAILED) { + Py_FatalError("Could not attach native thread to the polyglot context"); + } else if (attached == GRAALPY_ATTACH_NATIVE_OWNED) { graalpy_attached_thread = 1; } - graalpy_gilstate_counter++; } + graalpy_gilstate_counter++; return GraalPyPrivate_GILState_Ensure() ? PyGILState_UNLOCKED : PyGILState_LOCKED; #endif // GraalPy change } @@ -2428,20 +2432,20 @@ PyGILState_Release(PyGILState_STATE oldstate) if (oldstate == PyGILState_UNLOCKED) { GraalPyPrivate_GILState_Release(); } - if (TRUFFLE_CONTEXT) { - graalpy_gilstate_counter--; - if (graalpy_gilstate_counter == 0 && graalpy_attached_thread) { - GraalPyPrivate_BeforeThreadDetach(); - (*TRUFFLE_CONTEXT)->detachCurrentThread(TRUFFLE_CONTEXT); - graalpy_attached_thread = 0; - /* - * The thread state on the Java-side is cleared in GraalPyPrivate_BeforeThreadDetach. - * As part of that the tstate_current pointer should have been set to NULL to make - * sure to fetch a fresh pointer the next time we attach. Just to be sure, we clear - * it here too: - */ - tstate_current = NULL; + graalpy_gilstate_counter--; + if (graalpy_gilstate_counter == 0 && graalpy_attached_thread) { + GraalPyPrivate_BeforeThreadDetach(); + if (graalpy_detach_native_thread) { + graalpy_detach_native_thread(); } + graalpy_attached_thread = 0; + /* + * The thread state on the Java-side is cleared in GraalPyPrivate_BeforeThreadDetach. + * As part of that the tstate_current pointer should have been set to NULL to make + * sure to fetch a fresh pointer the next time we attach. Just to be sure, we clear + * it here too: + */ + tstate_current = NULL; } #endif // GraalPy change } diff --git a/graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_thread.py b/graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_thread.py index 56d23fe9f9..fe01afd7c2 100644 --- a/graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_thread.py +++ b/graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_thread.py @@ -82,8 +82,7 @@ class TestPyThread(CPyExtTestCase): ) -# TODO(native-access) support ENV - thread attach/detach -@unittest.skipIf(True, "Needs pthread") +@unittest.skipIf(sys.platform == 'win32', "Needs pthread") class TestNativeThread(unittest.TestCase): def test_register_new_thread(self): TestThread = CPyExtType( diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextPyStateBuiltins.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextPyStateBuiltins.java index de56b02784..840733bf5a 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextPyStateBuiltins.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextPyStateBuiltins.java @@ -64,6 +64,7 @@ import com.oracle.graal.python.builtins.objects.dict.PDict; import com.oracle.graal.python.builtins.objects.frame.PFrame; import com.oracle.graal.python.builtins.objects.ints.PInt; +import com.oracle.graal.python.builtins.objects.object.PythonObject; import com.oracle.graal.python.builtins.objects.thread.PThread; import com.oracle.graal.python.nodes.PGuards; import com.oracle.graal.python.nodes.PRaiseNode; @@ -85,34 +86,22 @@ public final class PythonCextPyStateBuiltins { - @CApiBuiltin(ret = Int, args = {}, acquireGil = false, call = Ignored) - abstract static class GraalPyPrivate_GILState_Check extends CApiNullaryBuiltinNode { + private static final TruffleLogger LOGGER = CApiContext.getLogger(PythonCextPyStateBuiltins.class); - @Specialization - Object check() { - return PythonContext.get(this).ownsGil() ? 1 : 0; - } + @CApiBuiltin(ret = Int, args = {}, acquireGil = false, call = Ignored) + static int GraalPyPrivate_GILState_Check() { + return PythonContext.get(null).ownsGil() ? 1 : 0; } @CApiBuiltin(ret = Int, args = {}, acquireGil = false, call = Ignored) - abstract static class GraalPyPrivate_GILState_Ensure extends CApiNullaryBuiltinNode { - - @Specialization - static Object save(@Cached GilNode gil) { - boolean acquired = gil.acquire(); - return acquired ? 1 : 0; - } + static int GraalPyPrivate_GILState_Ensure() { + boolean acquired = GilNode.getUncached().acquire(); + return acquired ? 1 : 0; } @CApiBuiltin(ret = Void, args = {}, acquireGil = false, call = Ignored) - abstract static class GraalPyPrivate_GILState_Release extends CApiNullaryBuiltinNode { - - @Specialization - static Object restore( - @Cached GilNode gil) { - gil.release(true); - return PNone.NO_VALUE; - } + static void GraalPyPrivate_GILState_Release() { + GilNode.getUncached().release(true); } /** @@ -121,44 +110,43 @@ static Object restore( * action that eagerly initializes their native 'tstate_current' TLS slot. */ @CApiBuiltin(ret = PyThreadState, args = {Pointer}, acquireGil = false, call = Ignored) - abstract static class GraalPyPrivate_ThreadState_Get extends CApiUnaryBuiltinNode { - private static final TruffleLogger LOGGER = CApiContext.getLogger(GraalPyPrivate_ThreadState_Get.class); - - @Specialization - @TruffleBoundary - static long get(long tstateCurrentPtr) { - PythonContext context = PythonContext.get(null); - PythonThreadState threadState = context.getThreadState(context.getLanguage()); - - /* - * The C caller may have observed 'tstate_current == NULL' before entering this upcall. - * While entering this builtin, the same thread may process a queued thread-local action - * from C API initialization and initialize its native thread state eagerly. So the - * fallback decision made in C can be stale by the time we get here. - */ - if (threadState.isNativeThreadStateInitialized()) { - LOGGER.fine(() -> String.format("Lazy initialization attempt of native thread state for thread %s aborted. Was initialized in the meantime.", Thread.currentThread())); - long nativeThreadState = threadState.getNativePointer(); - assert nativeThreadState != NULLPTR; - return nativeThreadState; - } - - LOGGER.fine(() -> "Lazy (fallback) initialization of native thread state for thread " + Thread.currentThread()); - assert threadState.getNativePointer() == NULLPTR; - long nativeThreadState = PThreadState.getOrCreateNativeThreadState(threadState); - threadState.setNativeThreadLocalVarPointer(tstateCurrentPtr); + static long GraalPyPrivate_ThreadState_Get(long tstateCurrentPtr) { + + PythonContext context = PythonContext.get(null); + PythonThreadState threadState = context.getThreadState(context.getLanguage()); + + /* + * The C caller may have observed 'tstate_current == NULL' before entering this upcall. + * While entering this builtin, the same thread may process a queued thread-local action + * from C API initialization and initialize its native thread state eagerly. So the + * fallback decision made in C can be stale by the time we get here. + */ + if (threadState.isNativeThreadStateInitialized()) { + LOGGER.fine(() -> String.format("Lazy initialization attempt of native thread state for thread %s aborted. Was initialized in the meantime.", Thread.currentThread())); + long nativeThreadState = threadState.getNativePointer(); + assert nativeThreadState != NULLPTR; return nativeThreadState; } + + LOGGER.fine(() -> "Lazy (fallback) initialization of native thread state for thread " + Thread.currentThread()); + assert threadState.getNativePointer() == PythonObject.UNINITIALIZED; + long nativeThreadState = PThreadState.getOrCreateNativeThreadState(threadState); + threadState.setNativeThreadLocalVarPointer(tstateCurrentPtr); + return nativeThreadState; } @CApiBuiltin(ret = Void, args = {}, call = Ignored) - abstract static class GraalPyPrivate_BeforeThreadDetach extends CApiNullaryBuiltinNode { - @Specialization - @TruffleBoundary - Object doIt() { - getContext().disposeThread(Thread.currentThread(), true); - return PNone.NO_VALUE; - } + static Object GraalPyPrivate_BeforeThreadDetach() { + /* + * This is the PyGILState_Release path for native-created threads. CPython clears its own + * auto-TSS key and deletes the PyThreadState here. Our Java thread state is stored in a + * Truffle ContextThreadLocal, which has no per-thread clear operation for a still-alive + * thread. If the same native thread later enters the context again, Truffle may return the + * same PythonThreadState object. Therefore this is a detach, not final thread shutdown: free + * the native PyThreadState, but leave the Java state usable for the next attach. + */ + PythonContext.get(null).disposeThread(Thread.currentThread(), true, false); + return PNone.NO_VALUE; } @CApiBuiltin(ret = PyObjectBorrowed, args = {}, call = Direct) diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/capi/CApiContext.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/capi/CApiContext.java index 7dee2c1dee..84da18715b 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/capi/CApiContext.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/capi/CApiContext.java @@ -56,6 +56,8 @@ import java.io.IOException; import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; import java.lang.invoke.VarHandle; import java.lang.ref.WeakReference; import java.util.ArrayList; @@ -96,6 +98,7 @@ import com.oracle.graal.python.builtins.objects.cext.common.LoadCExtException.ImportException; import com.oracle.graal.python.builtins.objects.cext.common.NativePointer; import com.oracle.graal.python.builtins.objects.cext.copying.NativeLibraryLocator; +import com.oracle.graal.python.builtins.objects.cext.structs.CConstants; import com.oracle.graal.python.builtins.objects.cext.structs.CFields; import com.oracle.graal.python.builtins.objects.cext.structs.CStructAccess; import com.oracle.graal.python.builtins.objects.cext.structs.CStructs; @@ -113,6 +116,7 @@ import com.oracle.graal.python.runtime.nativeaccess.NativeLibrary; import com.oracle.graal.python.runtime.nativeaccess.NativeLibraryLoadException; import com.oracle.graal.python.runtime.nativeaccess.NativeMemory; +import com.oracle.graal.python.runtime.nativeaccess.NativeSimpleType; import com.oracle.graal.python.runtime.nativeaccess.NativeSignature; import com.oracle.graal.python.nodes.ErrorMessages; import com.oracle.graal.python.nodes.PRaiseNode; @@ -138,6 +142,7 @@ import com.oracle.truffle.api.CompilerDirectives.ValueType; import com.oracle.truffle.api.RootCallTarget; import com.oracle.truffle.api.ThreadLocalAction; +import com.oracle.truffle.api.TruffleContext; import com.oracle.truffle.api.TruffleFile; import com.oracle.truffle.api.TruffleLanguage.Env; import com.oracle.truffle.api.TruffleLogger; @@ -777,6 +782,70 @@ void runBackgroundGCTask(PythonContext context) { private Runnable nativeFinalizerRunnable; private Thread nativeFinalizerShutdownHook; + private final ThreadLocal attachedThreadPreviousContext = new ThreadLocal<>(); + + private static final NativeSignature ATTACH_NATIVE_THREAD_SIGNATURE = NativeSignature.create(NativeSimpleType.SINT32); + private static final NativeSignature DETACH_NATIVE_THREAD_SIGNATURE = NativeSignature.create(NativeSimpleType.VOID); + /** Must stay in sync with {@code GRAALPY_ATTACH_NATIVE_FAILED} in {@code capi.h}. */ + private static final int ATTACH_NATIVE_THREAD_FAILED = -1; + /** Must stay in sync with {@code GRAALPY_ATTACH_NATIVE_OWNED} in {@code capi.h}. */ + private static final int ATTACH_NATIVE_THREAD_OWNED = 1; + /** Must stay in sync with {@code GRAALPY_ATTACH_NATIVE_FOREIGN} in {@code capi.h}. */ + private static final int ATTACH_NATIVE_THREAD_FOREIGN = 2; + private static final MethodHandle HANDLE_ATTACH_NATIVE_THREAD; + private static final MethodHandle HANDLE_DETACH_NATIVE_THREAD; + + static { + try { + HANDLE_ATTACH_NATIVE_THREAD = MethodHandles.lookup().findVirtual(CApiContext.class, "attachNativeThread", + MethodType.methodType(int.class)); + HANDLE_DETACH_NATIVE_THREAD = MethodHandles.lookup().findVirtual(CApiContext.class, "detachNativeThread", + MethodType.methodType(void.class)); + } catch (NoSuchMethodException | IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + /** + * Called from native threads before using the C API. Returns + * {@link #ATTACH_NATIVE_THREAD_OWNED} if this method entered the context and a matching + * {@link #detachNativeThread()} is required, {@link #ATTACH_NATIVE_THREAD_FOREIGN} if the context + * was already active on this thread from Truffle/Java/Python, and + * {@link #ATTACH_NATIVE_THREAD_FAILED} if entering failed. + */ + private int attachNativeThread() { + CompilerAsserts.neverPartOfCompilation(); + TruffleContext truffleContext = getContext().getEnv().getContext(); + if (truffleContext.isActive()) { + return ATTACH_NATIVE_THREAD_FOREIGN; + } + try { + Object previousContext = truffleContext.enter(null); + attachedThreadPreviousContext.set(previousContext); + return ATTACH_NATIVE_THREAD_OWNED; + } catch (Throwable t) { + LOGGER.severe("could not attach native thread to polyglot context: " + t.getMessage()); + return ATTACH_NATIVE_THREAD_FAILED; + } + } + + private static boolean assertAttachNativeThreadConstants() { + assert ATTACH_NATIVE_THREAD_FAILED == CConstants.GRAALPY_ATTACH_NATIVE_FAILED.intValue(); + assert ATTACH_NATIVE_THREAD_OWNED == CConstants.GRAALPY_ATTACH_NATIVE_OWNED.intValue(); + assert ATTACH_NATIVE_THREAD_FOREIGN == CConstants.GRAALPY_ATTACH_NATIVE_FOREIGN.intValue(); + return true; + } + + /** + * Called from native immediately before detaching a thread that was previously attached by + * {@link #attachNativeThread()}. + */ + private void detachNativeThread() { + CompilerAsserts.neverPartOfCompilation(); + Object previousContext = attachedThreadPreviousContext.get(); + attachedThreadPreviousContext.remove(); + getContext().getEnv().getContext().leave(null, previousContext); + } @TruffleBoundary public static CApiContext ensureCapiWasLoaded(String reason) { @@ -841,6 +910,7 @@ public static CApiContext ensureCapiWasLoaded(Node node, PythonContext context, // This can happen when other languages restrict multithreading LOGGER.warning(() -> "didn't start the background GC task due to: " + e.getMessage()); } + assert assertAttachNativeThreadConstants(); } catch (ImportException e) { context.setCApiState(PythonContext.CApiState.CANNOT_IMPORT); throw e; @@ -930,6 +1000,10 @@ private static CApiContext loadCApi(Node node, PythonContext context, TruffleStr long gcState = cApiContext.createGCState(); PythonThreadState currentThreadState = context.getThreadState(context.getLanguage()); long nativeThreadState = PThreadState.getOrCreateNativeThreadState(currentThreadState); + long attachNativeThread = cApiContext.registerClosure("attachNativeThread", ATTACH_NATIVE_THREAD_SIGNATURE, + HANDLE_ATTACH_NATIVE_THREAD.bindTo(cApiContext), "attachNativeThread", cApiContext); + long detachNativeThread = cApiContext.registerClosure("detachNativeThread", DETACH_NATIVE_THREAD_SIGNATURE, + HANDLE_DETACH_NATIVE_THREAD.bindTo(cApiContext), "detachNativeThread", cApiContext); long builtinArrayPtr = NativeMemory.mallocPtrArray(PythonCextBuiltinRegistry.builtins.length); try { @@ -939,7 +1013,8 @@ private static CApiContext loadCApi(Node node, PythonContext context, TruffleStr } long nativeThreadLocalVarPointer = ExternalFunctionInvoker.invokeCAPIINIT(null, TIMING_INVOKE_CAPI_INIT, nativeContext, BoundaryCallData.getUncached(), context.getThreadState(context.getLanguage()), - ExternalFunctionSignature.CAPIINIT.bind(nativeContext, initFunction), builtinArrayPtr, gcState, nativeThreadState); + ExternalFunctionSignature.CAPIINIT.bind(nativeContext, initFunction), builtinArrayPtr, + gcState, nativeThreadState, attachNativeThread, detachNativeThread); assert nativeThreadLocalVarPointer != NULLPTR; currentThreadState.setNativeThreadLocalVarPointer(nativeThreadLocalVarPointer); } finally { diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/capi/ExternalFunctionSignature.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/capi/ExternalFunctionSignature.java index 6641a2c84f..0596f1bf6b 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/capi/ExternalFunctionSignature.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/capi/ExternalFunctionSignature.java @@ -160,8 +160,8 @@ public enum ExternalFunctionSignature implements NativeCExtSymbol { MODEXEC(false, Int, Pointer), // typedef PyObject *(*PyInit_mod)(void); MODINIT(false, Pointer), - // typedef PThreadState** (*initialize_graal_capi)(void *, void *, void *); - CAPIINIT(false, Pointer, Pointer, Pointer, Pointer), + // typedef PThreadState** (*initialize_graal_capi)(void *, void *, void *, void *, void *); + CAPIINIT(false, Pointer, Pointer, Pointer, Pointer, Pointer, Pointer), // PyThreadState **GraalPyPrivate_InitThreadStateCurrent(PyThreadState *tstate) INIT_THREAD_STATE_CURRENT(true, Pointer, PyThreadState), // typedef void *(*GraalPyPrivate_GetFinalizeCApiPointer)(void); diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/capi/PThreadState.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/capi/PThreadState.java index 227400150f..d3b3ae097c 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/capi/PThreadState.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/capi/PThreadState.java @@ -196,13 +196,22 @@ public static int growDeallocatingStack(long nativeThreadState, long newCapacity @TruffleBoundary public static void dispose(PythonThreadState threadState) { + dispose(threadState, true); + } + + @TruffleBoundary + public static void dispose(PythonThreadState threadState, boolean markShuttingDown) { long nativeCompanion = threadState.getNativePointer(); if (nativeCompanion == UNINITIALIZED || nativeCompanion == NATIVE_POINTER_FREED) { return; } assert !HandlePointerConverter.pointsToPyHandleSpace(nativeCompanion); - threadState.clearNativePointer(); + if (markShuttingDown) { + threadState.clearNativePointer(); + } else { + threadState.resetNativePointerAfterDetach(); + } long deallocatingState = CStructAccess.getFieldPtr(nativeCompanion, CFields.PyThreadState__graalpy_deallocating); long deallocatingItems = CStructAccess.readPtrField(deallocatingState, CFields.GraalPyDeallocState__items); diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/structs/CConstants.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/structs/CConstants.java index ae7711df44..1f8eab5cc4 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/structs/CConstants.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/structs/CConstants.java @@ -58,24 +58,30 @@ /** * Helper enum to extract constants from the C space. Constants are limited to the range of "long" - * values, but '-1' is not allowed at the moment. + * values. */ @CApiConstants public enum CConstants { PYLONG_BITS_IN_DIGIT, READONLY, CHAR_MIN, + GRAALPY_ATTACH_NATIVE_FAILED, + GRAALPY_ATTACH_NATIVE_OWNED, + GRAALPY_ATTACH_NATIVE_FOREIGN, _PY_NSMALLNEGINTS, _PY_NSMALLPOSINTS; @CompilationFinal(dimensions = 1) public static final CConstants[] VALUES = values(); - @CompilationFinal private long longValue = -1; - @CompilationFinal private int intValue = -1; + private static final long UNRESOLVED = Long.MIN_VALUE; + private static final int INT_OVERFLOW = Integer.MIN_VALUE; + + @CompilationFinal private long longValue = UNRESOLVED; + @CompilationFinal private int intValue = INT_OVERFLOW; public long longValue() { long o = longValue; - if (o == -1) { + if (o == UNRESOLVED) { CompilerDirectives.transferToInterpreterAndInvalidate(); resolve(); return longValue; @@ -89,10 +95,10 @@ public long longValue() { */ public int intValue() { int o = intValue; - if (o == -1) { + if (o == INT_OVERFLOW) { CompilerDirectives.transferToInterpreterAndInvalidate(); resolve(); - if (intValue == -1) { + if (intValue == INT_OVERFLOW) { throw PRaiseNode.raiseStatic(null, SystemError, INTERNAL_INT_OVERFLOW); } return intValue; @@ -112,9 +118,6 @@ private static void resolve() { long[] constants = readLongArrayElements(constantsPointer, 0L, VALUES.length); for (CConstants constant : VALUES) { constant.longValue = constants[constant.ordinal()]; - if (constant.longValue == -1) { - throw PRaiseNode.raiseStatic(null, SystemError, toTruffleStringUncached("internal limitation - cannot extract constants with value '-1'")); - } if ((constant.longValue & 0xFFFF0000L) == 0xDEAD0000L) { throw PRaiseNode.raiseStatic(null, SystemError, toTruffleStringUncached("marker value reached, regenerate C code (mx python-capi)")); } diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/runtime/PythonContext.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/runtime/PythonContext.java index f285b99d13..1b68c9d2de 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/runtime/PythonContext.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/runtime/PythonContext.java @@ -519,6 +519,11 @@ public void clearNativePointer() { this.nativePointer = NATIVE_POINTER_FREED; } + public void resetNativePointerAfterDetach() { + assert this.nativePointer != NATIVE_POINTER_FREED; + this.nativePointer = UNINITIALIZED; + } + public PContextVarsContext getContextVarsContext(Node node) { if (contextVarsContext == null) { contextVarsContext = PFactory.createContextVarsContext(PythonLanguage.get(node)); @@ -532,6 +537,10 @@ public void setContextVarsContext(PContextVarsContext contextVarsContext) { } public void dispose(boolean canRunGuestCode, boolean clearNativeThreadLocalVarPointer) { + dispose(canRunGuestCode, clearNativeThreadLocalVarPointer, true); + } + + public void dispose(boolean canRunGuestCode, boolean clearNativeThreadLocalVarPointer, boolean markShuttingDown) { // This method may be called twice on the same object. /* @@ -546,7 +555,19 @@ public void dispose(boolean canRunGuestCode, boolean clearNativeThreadLocalVarPo dict = null; } - PThreadState.dispose(this); + PThreadState.dispose(this, markShuttingDown); + if (!markShuttingDown) { + caughtException = null; + topframeref = null; + traceFun = null; + tracing = false; + profileFun = null; + profiling = false; + contextVarsContext = null; + runningEventLoop = null; + asyncgenFirstIter = null; + recursionDepth = 0; + } /* * Write 'NULL' to the native thread-local variable used to store the PyThreadState @@ -2763,6 +2784,20 @@ public void initializeNativeThreadState(PythonThreadState pythonThreadState) { } public void disposeThread(Thread thread, boolean canRunGuestCode) { + disposeThread(thread, canRunGuestCode, true); + } + + /** + * Disposes GraalPy state associated with {@code thread}. + * + * {@code markShuttingDown} distinguishes final thread exit from a temporary native-thread + * detach. Truffle's {@link ContextThreadLocal} does not expose a way to clear one language local + * for a still-alive thread, so a native thread that calls {@code PyGILState_Ensure} again after + * {@code PyGILState_Release} may receive the same Java {@link PythonThreadState} object. Such a + * detach must remove the thread from {@link #threadStateMapping} and free the native + * {@code PyThreadState}, but it must not mark the Java thread state as shutting down. + */ + public void disposeThread(Thread thread, boolean canRunGuestCode, boolean markShuttingDown) { CompilerAsserts.neverPartOfCompilation(); // check if there is a live sentinel lock PythonThreadState ts = threadStateMapping.get(thread); @@ -2770,9 +2805,11 @@ public void disposeThread(Thread thread, boolean canRunGuestCode) { // ts already removed, that is valid during context shutdown for daemon threads return; } - ts.shutdown(); + if (markShuttingDown) { + ts.shutdown(); + } threadStateMapping.remove(thread); - ts.dispose(canRunGuestCode, thread == Thread.currentThread()); + ts.dispose(canRunGuestCode, thread == Thread.currentThread(), markShuttingDown); releaseSentinelLock(ts.sentinelLock); getSharedMultiprocessingData().removeChildContextThread(PThread.getThreadId(thread)); } From 692248a381547ca49ed4db02424869b707f9ec0a Mon Sep 17 00:00:00 2001 From: Florian Angerer Date: Fri, 8 May 2026 13:26:22 +0200 Subject: [PATCH 3/3] Do not export graalpy_dealloc_stack_* functions --- graalpython/com.oracle.graal.python.cext/src/capi.c | 6 +++--- graalpython/com.oracle.graal.python.cext/src/capi.h | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/graalpython/com.oracle.graal.python.cext/src/capi.c b/graalpython/com.oracle.graal.python.cext/src/capi.c index 914e96805c..00f0cf3458 100644 --- a/graalpython/com.oracle.graal.python.cext/src/capi.c +++ b/graalpython/com.oracle.graal.python.cext/src/capi.c @@ -100,7 +100,7 @@ typedef struct { // defined in 'unicodeobject.c' void unicode_dealloc(PyObject *unicode); -NO_INLINE void +Py_LOCAL_SYMBOL NO_INLINE void graalpy_dealloc_stack_grow(PyThreadState *tstate) { size_t old_capacity; @@ -119,7 +119,7 @@ graalpy_dealloc_stack_grow(PyThreadState *tstate) } } -void +Py_LOCAL_SYMBOL void graalpy_dealloc_stack_push(PyThreadState *tstate, PyObject *op) { assert(tstate != NULL); @@ -129,7 +129,7 @@ graalpy_dealloc_stack_push(PyThreadState *tstate, PyObject *op) tstate->graalpy_deallocating.items[tstate->graalpy_deallocating.len++] = op; } -void +Py_LOCAL_SYMBOL void graalpy_dealloc_stack_pop(PyThreadState *tstate, PyObject *op) { assert(tstate != NULL); diff --git a/graalpython/com.oracle.graal.python.cext/src/capi.h b/graalpython/com.oracle.graal.python.cext/src/capi.h index 345d17efd9..124141c065 100644 --- a/graalpython/com.oracle.graal.python.cext/src/capi.h +++ b/graalpython/com.oracle.graal.python.cext/src/capi.h @@ -178,9 +178,9 @@ extern THREAD_LOCAL Py_LOCAL_SYMBOL PyThreadState *tstate_current; extern Py_LOCAL_SYMBOL int8_t *_graalpy_finalizing; #define graalpy_finalizing (_graalpy_finalizing != NULL && *_graalpy_finalizing) -void graalpy_dealloc_stack_grow(PyThreadState *tstate); -void graalpy_dealloc_stack_push(PyThreadState *tstate, PyObject *op); -void graalpy_dealloc_stack_pop(PyThreadState *tstate, PyObject *op); +Py_LOCAL_SYMBOL void graalpy_dealloc_stack_grow(PyThreadState *tstate); +Py_LOCAL_SYMBOL void graalpy_dealloc_stack_push(PyThreadState *tstate, PyObject *op); +Py_LOCAL_SYMBOL void graalpy_dealloc_stack_pop(PyThreadState *tstate, PyObject *op); #if (__linux__ && __GNU_LIBRARY__) #include