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