diff --git a/setup.py b/setup.py index b0d1aa9..8616138 100644 --- a/setup.py +++ b/setup.py @@ -98,6 +98,7 @@ os.path.join(src_test_py_dir, 'jpy_obj_test.py'), os.path.join(src_test_py_dir, 'jpy_eval_exec_test.py'), os.path.join(src_test_py_dir, 'jpy_mt_eval_exec_test.py'), + os.path.join(src_test_py_dir, 'jpy_gil_test.py'), ] # e.g. jdk_home_dir = '/home/marta/jdk1.7.0_15' diff --git a/src/main/c/jni/org_jpy_PyLib.c b/src/main/c/jni/org_jpy_PyLib.c index 45f9641..28c2b23 100644 --- a/src/main/c/jni/org_jpy_PyLib.c +++ b/src/main/c/jni/org_jpy_PyLib.c @@ -149,6 +149,71 @@ JNIEXPORT jboolean JNICALL Java_org_jpy_PyLib_isPythonRunning return init && JPy_Module != NULL; } +/** + * Class: org_jpy_PyLib + * Method: isGILEnabled + * Signature: ()Z + */ +JNIEXPORT jboolean JNICALL Java_org_jpy_PyLib_isGILEnabled + (JNIEnv* jenv, jclass jLibClass) +{ + // Check if the GIL is enabled at runtime. This is important for Python 3.13+ free-threaded builds + // where the GIL can be enabled with PYTHON_GIL=1 or when importing an extension module not declared as safe without + // the GIL. + // For standard Python builds (non-free-threaded), the GIL is always enabled. + + #ifdef Py_GIL_DISABLED + // Py_GIL_DISABLED indicates this is a free-threaded build that supports running without the GIL. + // We need to acquire the GIL before calling Python C API functions, even when checking if the GIL + // is enabled. This is because the Python C API is not thread-safe without the GIL. + // + // We call Python's sys._is_gil_enabled() instead of attempting to use internal C API functions. + // While Python 3.13+ has internal functions for checking GIL status, they are not part of the + // stable/public C API and might require including internal headers (e.g., pycore_*.h files). + // The sys._is_gil_enabled() function is the documented, stable way to check GIL status from both + // Python code and C extensions in Python 3.13+ free-threaded builds. + + jboolean result = JNI_TRUE; // Default to GIL enabled + PyObject* sys_module = NULL; + PyObject* is_gil_enabled_func = NULL; + PyObject* call_result = NULL; + + JPy_BEGIN_GIL_STATE(JNI_TRUE) + + sys_module = PyImport_ImportModule("sys"); + if (sys_module == NULL) { + PyErr_Clear(); + goto cleanup; + } + + is_gil_enabled_func = PyObject_GetAttrString(sys_module, "_is_gil_enabled"); + if (is_gil_enabled_func == NULL) { + PyErr_Clear(); + goto cleanup; + } + + call_result = PyObject_CallNoArgs(is_gil_enabled_func); + if (call_result == NULL) { + PyErr_Clear(); + goto cleanup; + } + + result = PyObject_IsTrue(call_result) ? JNI_TRUE : JNI_FALSE; + + cleanup: + Py_XDECREF(call_result); + Py_XDECREF(is_gil_enabled_func); + Py_XDECREF(sys_module); + + JPy_END_GIL_STATE + + return result; + #else + // Standard Python build - GIL is always enabled + return JNI_TRUE; + #endif +} + #define MAX_PYTHON_HOME 256 wchar_t staticPythonHome[MAX_PYTHON_HOME]; @@ -2725,7 +2790,7 @@ static int format_python_traceback(PyTracebackObject *tb, char **buf, int *bufLe } cnt++; if (err == 0 && cnt <= PYLIB_RECURSIVE_CUTOFF) { - pyObjUtf8 = format_displayline( + pyObjUtf8 = format_displayline( co->co_filename, tb_lineno, co->co_name); diff --git a/src/main/c/jni/org_jpy_PyLib.h b/src/main/c/jni/org_jpy_PyLib.h index d849818..37eb1d4 100644 --- a/src/main/c/jni/org_jpy_PyLib.h +++ b/src/main/c/jni/org_jpy_PyLib.h @@ -15,6 +15,14 @@ extern "C" { JNIEXPORT jboolean JNICALL Java_org_jpy_PyLib_isPythonRunning (JNIEnv *, jclass); +/* + * Class: org_jpy_PyLib + * Method: isGILEnabled + * Signature: ()Z + */ +JNIEXPORT jboolean JNICALL Java_org_jpy_PyLib_isGILEnabled + (JNIEnv *, jclass); + /* * Class: org_jpy_PyLib * Method: startPython0 diff --git a/src/main/java/org/jpy/PyLib.java b/src/main/java/org/jpy/PyLib.java index 2f07a06..2d46b4b 100644 --- a/src/main/java/org/jpy/PyLib.java +++ b/src/main/java/org/jpy/PyLib.java @@ -161,6 +161,14 @@ public static void assertPythonRuns() { */ public static native boolean isPythonRunning(); + /** + * @return {@code true} if the Python GIL (Global Interpreter Lock) is enabled at runtime. + * For standard Python builds, this always returns {@code true}. + * For Python 3.13+ free-threaded builds, this returns the actual GIL status which can be + * controlled via the PYTHON_GIL environment variable. + */ + public static native boolean isGILEnabled(); + /** * Delegates to {@link #startPython(int, String...)} with {@code flags = Diag.F_OFF}. */ diff --git a/src/test/python/jpy_gil_test.py b/src/test/python/jpy_gil_test.py new file mode 100644 index 0000000..d6e0684 --- /dev/null +++ b/src/test/python/jpy_gil_test.py @@ -0,0 +1,71 @@ +import unittest +import sys + +import jpyutil + +jpyutil.init_jvm(jvm_maxmem='512M', jvm_classpath=['target/test-classes', 'target/classes']) +import jpy + + +class TestGIL(unittest.TestCase): + """ + Tests for GIL-related functionality in PyLib + """ + + def setUp(self): + self.PyLib = jpy.get_type('org.jpy.PyLib') + self.assertIsNotNone(self.PyLib) + + def test_isGILEnabled(self): + """ + Test that PyLib.isGILEnabled() returns the correct GIL status. + Verifies the JNI implementation matches Python's own sys._is_gil_enabled(). + """ + # Get GIL status from Java/JNI + gil_enabled_from_jni = self.PyLib.isGILEnabled() + self.assertIsInstance(gil_enabled_from_jni, bool) + + # Get expected GIL status from Python + # For Python 3.13+ free-threaded builds, check sys._is_gil_enabled() + # For older versions or standard builds, GIL is always enabled + if hasattr(sys, '_is_gil_enabled'): + expected_gil_enabled = sys._is_gil_enabled() + else: + # Older Python or standard build - GIL is always enabled + expected_gil_enabled = True + + # Verify they match + self.assertEqual( + gil_enabled_from_jni, + expected_gil_enabled, + f"PyLib.isGILEnabled() returned {gil_enabled_from_jni}, " + f"but Python's sys._is_gil_enabled() indicates {expected_gil_enabled}" + ) + + def test_isGILEnabled_always_returns_boolean(self): + """ + Test that isGILEnabled() always returns a boolean value. + """ + result = self.PyLib.isGILEnabled() + self.assertIsInstance(result, bool) + + def test_isGILEnabled_standard_python(self): + """ + Test that for standard Python builds (non-free-threaded), + isGILEnabled() returns True. + """ + # Check if this is a free-threaded build + is_free_threaded = hasattr(sys, '_is_gil_enabled') + + if not is_free_threaded: + # For standard Python builds, GIL should always be enabled + self.assertTrue( + self.PyLib.isGILEnabled(), + "GIL should always be enabled in standard Python builds" + ) + + +if __name__ == '__main__': + print('\nRunning ' + __file__) + unittest.main() +