| 814 | } |
| 815 | |
| 816 | void finalize() { |
| 817 | join_plugin_preload(); |
| 818 | |
| 819 | if (!Py_IsInitialized()) { |
| 820 | return; |
| 821 | } |
| 822 | |
| 823 | set_gil_state_ready(false); |
| 824 | |
| 825 | if (get_main_thread_state()) { |
| 826 | acquire_gil_main_thread(); |
| 827 | } else { |
| 828 | LOG_WARN("No saved thread state, using PyGILState_Ensure"); |
| 829 | PyGILState_Ensure(); |
| 830 | } |
| 831 | |
| 832 | // Clear all callbacks that hold Python objects (nanobind::object) |
| 833 | // This must be done while GIL is held since nanobind::object |
| 834 | // destructor decrements Python reference counts |
| 835 | lfs::training::ControlBoundary::instance().clear_all(); |
| 836 | |
| 837 | // Clear frame callback if set |
| 838 | clear_frame_callback(); |
| 839 | |
| 840 | // Clear Python UI registries that hold nb::object references |
| 841 | // These singletons would otherwise destroy nb::objects during |
| 842 | // static destruction, after Python is gone |
| 843 | invoke_python_cleanup(); |
| 844 | |
| 845 | PyGC_Collect(); |
| 846 | |
| 847 | // Skip Py_FinalizeEx() - nanobind static destructors need Python alive |
| 848 | } |
| 849 | |
| 850 | bool was_python_used() { |
| 851 | return get_main_thread_state() != nullptr || Py_IsInitialized(); |
no test coverage detected