Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
67 changes: 66 additions & 1 deletion src/main/c/jni/org_jpy_PyLib.c
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Comment on lines +173 to +174
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is quite true. The documentation at https://docs.python.org/3/library/sys.html#sys._is_gil_enabled says:

CPython implementation detail: It is not guaranteed to exist in all implementations of Python.

In general, I think the _ prefix in a context like this means it's not part of the stable API.

I do think we should strive to work with other implementations of Python.

That said, as you mention, it's possible that python doesn't provide a stable way to call this from C API yet.

I think we should at least note we'd prefer to call stable C API here, and create a follow-up in the future (/ link to our issue / link to c python issue).


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];

Expand Down Expand Up @@ -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);
Expand Down
8 changes: 8 additions & 0 deletions src/main/c/jni/org_jpy_PyLib.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions src/main/java/org/jpy/PyLib.java
Original file line number Diff line number Diff line change
Expand Up @@ -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}.
*/
Expand Down
71 changes: 71 additions & 0 deletions src/test/python/jpy_gil_test.py
Original file line number Diff line number Diff line change
@@ -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()

Loading