Skip to content

fix: don't destruct module objects in atexit #5688

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

Merged
merged 14 commits into from
May 25, 2025

Conversation

b-pass
Copy link
Contributor

@b-pass b-pass commented May 23, 2025

Description

Likely fix for #5687.

The module objects created by MODULE_INIT macros are stored in static pybind11::object, which will try to DECREF them when the program exits, which is likely to crash because the interpreter has probably shut down by then.

The solution is to just store the pointer, so that there is nothing to destruct during atexit.


📚 Documentation preview 📚: https://pybind11--5688.org.readthedocs.build/

henryiii
henryiii previously approved these changes May 24, 2025
@henryiii henryiii changed the title Immortalize modules on init fix: immortalize modules on init May 24, 2025
@henryiii henryiii dismissed their stale review May 24, 2025 04:52

Can't properly be made immortal

@b-pass b-pass changed the title fix: immortalize modules on init fix: don't destruct module objects in atexit May 24, 2025
@rwgk
Copy link
Collaborator

rwgk commented May 24, 2025

@b-pass I added commit 8736e1a

I got started with that because I wanted to replace the auto here, to improve readability:

-        static auto def = []() {                                                                  \
+        static PyModuleDef *def = []() {                                                          \

I'm still reviewing.

@rwgk
Copy link
Collaborator

rwgk commented May 24, 2025

I pushed another cleanup commit. I overlooked that part before.

The CI to watch is: https://github.com/pybind/pybind11/actions/runs/15230269745

EDIT: The CI run for commit 8736e1a succeeded (after one round of deflaking).

@rwgk
Copy link
Collaborator

rwgk commented May 24, 2025

@b-pass, see my last three question. Could you please help me getting my head around: What exactly happens in that code when there are sub-interpreters? What variables should be static, and what variables cannot be static?

@rwgk
Copy link
Collaborator

rwgk commented May 24, 2025

Unrelated to this PR: @henryiii there was this flake: https://github.com/pybind/pybind11/actions/runs/15230269745/job/42837274697

Not sure if you want to keep track?

(I just triggered a rerun.) EDIT: The rerun succeeded: https://github.com/pybind/pybind11/actions/runs/15230269745/job/42837982511

@b-pass
Copy link
Contributor Author

b-pass commented May 24, 2025

@b-pass, see my last three question. Could you please help me getting my head around: What exactly happens in that code when there are sub-interpreters? What variables should be static, and what variables cannot be static?

Sub-interpreters rerun PyInit_ the first time the module is imported in the sub-interpreter. That could (and we have a test that does) happen in multiple threads at the same time. Everything here should be static. The only non-static step is running PyModuleDef_Init which converts the def into a PyObject.

The resulting PyObject should, IMO, be treated like any other Pyobject which means it should not be used across interpreters. However, it would be safe to do so based on the current implementation of PyModuleDef_Init. IMO it's defensive against future CPython changes to just treat it like any PyObject and so just call init every time and return it.

@rwgk
Copy link
Collaborator

rwgk commented May 24, 2025

@b-pass, see my last three question. Could you please help me getting my head around: What exactly happens in that code when there are sub-interpreters? What variables should be static, and what variables cannot be static?

Sub-interpreters rerun PyInit_ the first time the module is imported in the sub-interpreter. That could (and we have a test that does) happen in multiple threads at the same time. Everything here should be static. The only non-static step is running PyModuleDef_Init which converts the def into a PyObject.

The resulting PyObject should, IMO, be treated like any other Pyobject which means it should not be used across interpreters. However, it would be safe to do so based on the current implementation of PyModuleDef_Init. IMO it's defensive against future CPython changes to just treat it like any PyObject and so just call init every time and return it.

Do you think it would make sense to distill this into a comment? (I'm not sure what's the best place to put it.)

b-pass added 2 commits May 24, 2025 17:48
- Get rid of initialize_multiphase_module_def and just initialize it here in the function.
- Make a simple slots init
- Mkae these function statics instead of globals
- Get rid of the lambda and just use the PyModuleDef member initializer.
@henryiii
Copy link
Collaborator

henryiii commented May 25, 2025

I'm tracking flakes in #5674. Those tests were intended to test the GIL, have so far resisted temptation to just skip them in free-threading. They need a lock somewhere. If someone has an idea, let me know. :)

I haven't been able to recreate these errors locally; I've tried the pytest free threading plugin, increasing the number of threads, and changing timeouts.

Copy link
Collaborator

@rwgk rwgk left a comment

Choose a reason for hiding this comment

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

Very nice, thanks a lot for the cleanup! Looks good to me, just one small oversight, and a question.

Copy link
Collaborator

@henryiii henryiii left a comment

Choose a reason for hiding this comment

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

Sorry, this didn't send earlier.

@@ -1438,19 +1477,16 @@ class module_ : public object {
PyModule_AddObject(ptr(), name, obj.inc_ref().ptr() /* steals a reference */);
}

using module_def = PyModuleDef; // TODO: Can this be removed (it was needed only for Python 2)?
Copy link
Collaborator

Choose a reason for hiding this comment

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

While this isn't heavily used, it is used, so maybe we should keep it around just a little longer? Especially since we are in the RC phase?

Longer term, we have a collection of these defines used for Py2 compatibility, and it would be really nice to list them as deprecated and ideally make using them produce a deprecation warning.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Thanks for catching this! I'll put it back, with a deprecation comment.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Done: commit 93d093c

I tried using PYBIND11_DEPRECATED but unfortunately that doesn't work:

https://chatgpt.com/share/683351d0-14a8-8008-9d31-0fd287dfc198

Copy link
Collaborator

@rwgk rwgk left a comment

Choose a reason for hiding this comment

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

Looks Great To Me. Thanks @b-pass and @henryiii!

(I'll be outside for the rest of the day. Please feel free to merge if all tests pass.)

@henryiii henryiii merged commit 03b4a9e into pybind:master May 25, 2025
74 checks passed
@github-actions github-actions bot added the needs changelog Possibly needs a changelog entry label May 25, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
needs changelog Possibly needs a changelog entry
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants