ChimeraTK-ApplicationCore 04.07.01
Loading...
Searching...
No Matches
PythonModuleManager.cc
Go to the documentation of this file.
1// SPDX-FileCopyrightText: Deutsches Elektronen-Synchrotron DESY, MSK, ChimeraTK Project <chimeratk-support@desy.de>
2// SPDX-License-Identifier: LGPL-3.0-or-later
3
4#include <pybind11/embed.h>
5// pybind11.h must be included first
6
7#include "Application.h"
8#include "ConfigReader.h"
9#include "PyModuleGroup.h"
10#include "PythonModuleManager.h"
11#include "VersionInfo.h"
12
13namespace py = pybind11;
14using namespace py::literals;
15
16namespace ChimeraTK {
17
18 /********************************************************************************************************************/
19
20 namespace detail {
21 struct __attribute__((visibility("hidden"))) PythonModuleManagerStatics {
22 py::scoped_interpreter pyint{false}; // "false" = do not register signal handlers
23 py::exception<boost::thread_interrupted> exceptionObject;
24 std::function<void(const std::unique_ptr<PyModuleGroup>&)> onMainGroupChangeCallback;
25 PythonModuleManagerStatics();
26 };
27
28 /******************************************************************************************************************/
29
30 struct __attribute__((visibility("hidden"))) PythonModuleManagerImpl {
31 static std::unique_ptr<PythonModuleManagerStatics> statics;
32 std::unique_ptr<PyModuleGroup> mainGroup; // this ModuleGroup is presented to Python as "app"
33 std::list<py::object> modules; // Python modules loaded (the language construct, *not* ChimeraTK::Module)
34 std::unique_ptr<py::gil_scoped_release> release;
35 };
36
38 std::unique_ptr<detail::PythonModuleManagerStatics> PythonModuleManagerImpl::statics;
40
41 /******************************************************************************************************************/
42
43 __attribute__((visibility("hidden"))) PythonModuleManagerStatics::PythonModuleManagerStatics() {
44 py::gil_scoped_acquire gil;
45
46 auto locals = py::dict("so_version"_a = ChimeraTK::VersionInfo::soVersion);
47 py::exec(R"(
48 import sys
49 import os
50
51 new_paths = []
52 for p in sys.path:
53 new_paths.append(os.path.join(p, 'ChimeraTK', 'ApplicationCore'+so_version))
54
55 sys.path = new_paths + sys.path # prepend so old system libraries are not found first
56 )",
57 py::globals(), locals);
58 }
59 } // namespace detail
60
61 /********************************************************************************************************************/
62
63 void PythonModuleManager::init() {
64 if(_impl) {
65 return;
66 }
67
68 _impl = std::make_unique<detail::PythonModuleManagerImpl>();
69 if(!_impl->statics) {
70 // Create impl, with scoped_interpreter etc.
71
72 // The impl object outlives the PythonModuleManager, since we need to keep the interpreter instance alive until
73 // the end of the process, even across multiple Application instances (e.g. in tests). Otherwise certain Python
74 // modules like numpy and datetime lead to crashes when loaded multiple times.
75 _impl->statics = std::make_unique<detail::PythonModuleManagerStatics>();
76
77 // boost::thread_interrupted does not have a what() function, so we cannot use py::register_exception() directly
78 // on it. Also py::set_error() seems to be unavailable for our version of pybind11, also there is this bug:
79 // https://github.com/pybind/pybind11/issues/4967
80 // As a work around, we roughly replicated what register_exception_impl() is doing in pybind11 2.11
81
82 // Create the exception object and store it in a static py::object, so the (state-less) lambda passed to
83 // register_exception_translator() below can access it. As long as there is only one PythonModuleManager at a time
84 // this is perfectly fine.
85 _impl->statics->exceptionObject = {py::module::import("__main__"), "ThreadInterrupted"};
86
87 py::register_exception_translator([](std::exception_ptr p) {
88 try {
89 if(p) std::rethrow_exception(p);
90 }
91 catch(const boost::thread_interrupted& e) {
92 detail::PythonModuleManagerImpl::statics->exceptionObject("Thread Interrupted");
93 }
94 });
95
96 // Make sure all Python modules are imported that we need
97 py::exec("import threading, traceback, sys, gc");
98 }
99
100 // create main group object functioning as "app" object on the Python side
101 _impl->mainGroup = std::make_unique<PyModuleGroup>(&Application::getInstance(), ".", "Root for Python Modules");
102
103 // If the bindings have been created already set/replace the "app" object with the newly created main group. This
104 // happens when a previous instance of the PythonModuleManager in the same process has already loaded Python
105 // modules using the ApplicationCore Python bindings. Otherwise the Python bindings are not yet loaded at this
106 // point, so this assignment is done later in PythonModuleManager::setBindings().
107 if(_impl->statics->onMainGroupChangeCallback) {
108 _impl->statics->onMainGroupChangeCallback(_impl->mainGroup);
109 }
110
111 // The scoped_interpreter keeps the GIL by default, so we have to release it here and do the locking explicitly.
112 // In the destructor of the PythonModuleManager (hence when the Application is being destroyed) we have to
113 // acquire the lock again to make sure static objects created internally by pybind11 can be destroyed while
114 // having the GIL (would otherwise lead to an error).
115 _impl->release = std::make_unique<py::gil_scoped_release>();
116 }
117
118 /********************************************************************************************************************/
119
121
122 /********************************************************************************************************************/
123
125 if(!_impl) {
126 return;
127 }
128
129 // terminate all Python ApplicationModule threads
130 for(auto* mod : _impl->mainGroup->getSubmoduleListRecursive()) {
131 mod->terminate();
132 }
133
134 // destroy the gil_scoped_release object, see comment in constructor when creating it
135 _impl->release.reset();
136
137 // de-assign the app object (which points to the root module we are about to destroy)
138 if(_impl->statics->onMainGroupChangeCallback) {
139 _impl->statics->onMainGroupChangeCallback(std::unique_ptr<PyModuleGroup>{nullptr});
140 }
141
142 // unload all Python modules, which will destroy all PythonApplicationModules etc. that have been constructed in
143 // Python code.
144 for(auto& mod : _impl->modules) {
145 auto modname = py::cast<std::string>(mod.attr("__name__"));
146 py::exec("sys.modules.pop('" + modname + "')");
147 }
148 _impl->modules.clear();
149 py::exec("gc.collect()");
150 }
151
152 /********************************************************************************************************************/
153
155 if(_impl) {
156 deinit();
157 }
158 }
159
160 /********************************************************************************************************************/
161
163 auto& config = app.getConfigReader();
164 for(auto& module : config.getModules("PythonModules")) {
165 init();
166
167 auto name = config.get<std::string>("PythonModules/" + module + "/path");
168 py::gil_scoped_acquire gil;
169
170 std::cout << "PythonModuleManager: Loading module " << name << std::endl;
171
172 try {
173 py::object themod = py::module::import(name.c_str());
174 _impl->modules.emplace_back(std::move(themod));
175 }
176 catch(py::error_already_set& err) {
177 throw ChimeraTK::logic_error("Error loading Python module from " + name + ": " + err.what());
178 }
179 }
180 }
181
182 /********************************************************************************************************************/
183
184 void PythonModuleManager::setOnMainGroupChange(std::function<void(const std::unique_ptr<PyModuleGroup>&)> callback) {
185 _impl->statics->onMainGroupChangeCallback = std::move(callback);
186 _impl->statics->onMainGroupChangeCallback(_impl->mainGroup);
187 }
188
189 /********************************************************************************************************************/
190
191} // namespace ChimeraTK
ConfigReader & getConfigReader()
static Application & getInstance()
Obtain instance of the application.
void createModules(Application &app)
called by Application to load all Python modules specified in the ConfigReader XML file
void setOnMainGroupChange(std::function< void(const std::unique_ptr< PyModuleGroup > &)> callback)
Register callback function to get informed about the main PyModuleGroup which is created by the Pytho...
void deinit()
clean up detail::PythonModuleManagerImpl, in particular py::gil_scoped_release
~PythonModuleManager()
need non-default destructor due to incomplete type detail::PythonModuleManagerImpl
PythonModuleManager()
need non-default constructor due to incomplete type detail::PythonModuleManagerImpl
InvalidityTracer application module.