-
Notifications
You must be signed in to change notification settings - Fork 18
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: free-threaded Python (3.13.0+) support #165
feat: free-threaded Python (3.13.0+) support #165
Conversation
404fb91
to
a8008de
Compare
0efa657
to
b5b17ab
Compare
6e49f85
to
6299c87
Compare
4045833
to
6a0f8fe
Compare
src/main/c/jpy_jtype.c
Outdated
#ifdef Py_GIL_DISABLED | ||
typedef struct { | ||
PyMutex lock; | ||
PyThreadState* owner; | ||
int recursion_level; | ||
} ReentrantLock; | ||
|
||
static void acquire_lock(ReentrantLock* self) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this a pattern that you found suggested by C python, or elsewhere?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
C Python doesn't offer an reentrant lock primitive. This is a pattern I found somewhere else.
src/main/java/org/jpy/PyObject.java
Outdated
@@ -71,7 +71,7 @@ public static int cleanup() { | |||
PyObject(long pointer, boolean fromJNI) { | |||
state = new PyObjectState(pointer); | |||
if (fromJNI) { | |||
if (CLEANUP_ON_INIT && PyLib.hasGil()) { | |||
if (CLEANUP_ON_INIT) { | |||
REFERENCES.cleanupOnlyUseFromGIL(); // only performs *one* cleanup |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we are no longer using the GIL, then we should rename this function and instead have comments on why it is correct. If we are on 3.12 mode why are we still correct?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This change is made to fix a bug based on the following two observations:
- Because we now release the GIL whenever we cross into Java from Python (by @niloc132), the PyLib.hasGIL() test will always fail in Python GIL enabled mode, which results in that the cleanup method will never get called anymore. (BTW, in the GIL disabled mode, PyLib.hasGIL() returns true).
- As part of the same enhancement change by @niloc132, he made sure that the cleanup is done under GIL.
private int cleanupOnlyUseFromGIL(long[] buffer) {
return PyLib.ensureGil(() -> {
int index = 0;
while (index < buffer.length) {
final Reference<? extends PyObject> reference = referenceQueue.poll();
if (reference == null) {
...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jmao-denver is right, there are several layers of questionable here, and the latest round here is my fault.
- latest stable release: hasGil is always false here, so this block never executes
- prior to the "release gil while in Java" change: hasGil is always true, so this always runs.
Why did it even test to begin with? Why does it clean up all other object any time any PyObject is about to be passed in to Java (instead of just once either just before or just after calling Java)? Couldn't say. I think removing the check is correct (either just before the ft patch, or as part of it), and we should reevaluate what is even happening here separately.
Agreed on renaming cleanupOnlyUseFromGIL.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was an oversight on my part during the "drop GIL" review.
I made the argument during the review that we might prefer to have some way to keep the GIL during "quick" java calls, or in other cases where we explicitly want to keep the GIL. I might argue that our call from JType_CreateJavaPyObject
into the constructor of PyObject
should keep the GIL.
The reason we need this to only be called into the context of the GIL (historically) is because we need the GIL when calling PyLib.decRef
; I'm assuming in the case of free-threaded (/GIL-less) builds, Py_DECREF is thread safe. And it's not only that Py_DECREF that needs to be thread-safe, the arbitrary python deallocation code that can be executed as part of a decRef to 0 needs to be thread-safe.
In the context of free-threaded / GIL-less builds, maybe the functions need to be renamed to be something like PyLib.isThreadSafe
/ PyLib.ensureThreadSafe
, or something like that.
@@ -29,3 +29,32 @@ jobs: | |||
|
|||
- name: Run Test | |||
run: python setup.py test | |||
|
|||
test-free-threaded: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see windows-specific and mac-specific code, but I don't see CI checks for the platforms. There do appear to be windows runners. https://docs.github.com/en/actions/using-github-hosted-runners/using-github-hosted-runners/about-github-hosted-runners
It isn't the highest priority, but I am noting it.
.github/workflows/build.yml
Outdated
fail-fast: false | ||
matrix: | ||
info: | ||
- { machine: 'ubuntu-20.04', python: '3.13t', arch: 'amd64', cmd: '.github/env/Linux/bdist-wheel.sh' } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Noting that the latest ubuntu LTS is 24.04.1
. Not sure how much it matters here.
.github/workflows/build.yml
Outdated
fail-fast: false | ||
matrix: | ||
info: | ||
- { machine: 'ubuntu-20.04', python: '3.13t', arch: 'aarch64', cmd: '.github/env/Linux/bdist-wheel.sh' } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
see note about the latest LTS version
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
20.04 goes EOL 4/23/2025. We may consider bumping to at least 22.04 which.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@devinrsmith is almost certain changing to 22.04 will make the wheels not installation due to some incompatibility in the GLIBC. Will file a follow-up ticket.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We are currently targeting manylinux_2_17, which makes it compatible with GLIBC version 2.17+; afaict, the building of these artifacts needs to happen on a system w/ GLIBC version 2.17, and updating to 22.04 or later breaks this. We should have a ticket about GLIBC version support, and when we will move on past 2.17.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
pypa/cibuildwheel#1772 has relevant upstream comments about the topic.
Co-authored-by: Chip Kent <[email protected]>
.github/workflows/check.yml
Outdated
source .venv/bin/activate | ||
uv pip install pip | ||
echo $JAVA_HOME | ||
echo PATH=$PATH >> $GITHUB_ENV |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks like it is still using GITHUB_ENV?
|
||
debug_build = sysconfig.get_config_var('Py_DEBUG') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is not correct in the context of how you are using it. It needs to be checked if it's set to "1"; at least in my builds, it's explicitly set to "0":
python3.8 -m sysconfig | grep Py_DEBUG
Py_DEBUG = "0"
After some more reading, I actually think that sys.abiflags
is the more appropriate place to check, since it's specifically mentioned as being set for debug builds, https://docs.python.org/3.10/using/configure.html#python-debug-build:
Effects of a debug build:
- Add
d
to sys.abiflags.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry for my flip-flopping; it actually looks like .get_config_var
is correct, since it seems to return an int when used like this. That said, maybe we should still prefer sys.abiflags
since it's more explicitly documented?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not confident that Py_DEBUG and Py_REF_DEBUG macros
are guaranteed to be equivalent to "config vars"; that said, it looks like sys.abiflags
doesn't work on Windows, so I guess get_config_var is the best way for now.
It looks like there was a patch many years ago to add an official way to look up if it was built with debug, but it was not merged. python/cpython#69443
@@ -71,8 +71,8 @@ public static int cleanup() { | |||
PyObject(long pointer, boolean fromJNI) { | |||
state = new PyObjectState(pointer); | |||
if (fromJNI) { | |||
if (CLEANUP_ON_INIT && PyLib.hasGil()) { | |||
REFERENCES.cleanupOnlyUseFromGIL(); // only performs *one* cleanup | |||
if (CLEANUP_ON_INIT) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Although originally unintentional (I think), this is a change in behavior vs the current release. I might suggest Colin or I follow up with a PR, unless this is newly tested in this PR?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It isn't explicitly tested. But the code path does get executed implicitly in jpy_typeconv_test_pyobj.py
.
Fixes #163