ACloudViewer  3.9.4
A Modern Library for 3D Data Processing
PythonInterpreter.cpp
Go to the documentation of this file.
1 // ----------------------------------------------------------------------------
2 // - CloudViewer: www.cloudViewer.org -
3 // ----------------------------------------------------------------------------
4 // Copyright (c) 2018-2024 www.cloudViewer.org
5 // SPDX-License-Identifier: MIT
6 // ----------------------------------------------------------------------------
7 
8 #include "PythonInterpreter.h"
10 #include "Runtime/Consoles.h"
11 #include "Utilities.h"
12 
13 #include <pybind11/embed.h>
14 
15 #include <QApplication>
16 #include <QCoreApplication>
17 #include <QDir>
18 #include <QListWidgetItem>
19 #include <QMessageBox>
20 // Qt5/Qt6 Compatibility
21 #include <QtCompat.h>
22 
23 #include <CVLog.h>
24 #include <CVTools.h>
25 
26 #ifdef Q_OS_LINUX
27 #include <cstdio>
28 #include <dlfcn.h>
29 #endif
30 
31 // seems like gcc defines macro with these names
32 #undef major
33 #undef minor
34 
35 namespace py = pybind11;
36 
37 static py::dict CreateGlobals()
38 {
39  py::dict globals;
40  globals["__name__"] = "__main__";
41  // Someday we should require pybind11 > 2.6 and use py::detail::ensure_builtins_in_globals ?
42  globals["__builtins__"] = PyEval_GetBuiltins();
43  return globals;
44 }
45 
46 PythonInterpreter::State::State() : globals(CreateGlobals()), locals() {};
47 
48 //================================================================================
49 
50 PythonInterpreter::PythonInterpreter(QObject *parent) : QObject(parent) {}
51 
52 bool PythonInterpreter::executeFile(const std::string &filepath)
53 {
54  if (m_isExecuting)
55  {
56  return false;
57  }
58  Q_EMIT executionStarted();
59 
60  bool success{true};
61  try
62  {
63  const auto movePolicy = py::return_value_policy::move;
64  py::object newStdout = py::cast(ccConsoleOutput(), movePolicy);
65  py::object newStderr = py::cast(ccConsoleOutput(), movePolicy);
66  PyStdErrOutStreamRedirect r{newStdout, newStderr};
67 
68  py::dict globals = CreateGlobals();
69  globals["__file__"] = filepath;
70  py::eval_file(filepath, globals);
71  }
72  catch (const std::exception &e)
73  {
74  CVLog::Warning(e.what());
75  success = false;
76  }
77 
78  Q_EMIT executionFinished();
79  return success;
80 }
81 
82 template <pybind11::eval_mode mode>
83 void PythonInterpreter::executeCodeString(const std::string &code,
84  QListWidget *output,
85  State &state)
86 {
87  if (m_isExecuting)
88  {
89  return;
90  }
91 
92  Q_EMIT executionStarted();
93  m_isExecuting = true;
94  const QColor orange(255, 100, 0);
95 
96  try
97  {
98  if (output != nullptr)
99  {
100  const auto movePolicy = py::return_value_policy::move;
101  py::object newStdout = py::cast(ListWidgetConsole(output), movePolicy);
102  py::object newStderr = py::cast(ListWidgetConsole(output, orange), movePolicy);
103  PyStdErrOutStreamRedirect redirect{newStdout, newStderr};
104  py::eval<mode>(code, state.globals, state.locals);
105  }
106  else
107  {
108  PyStdErrOutStreamRedirect redirect;
109  py::eval<mode>(code, state.globals, state.locals);
110  }
111  }
112  catch (const std::exception &e)
113  {
114  if (output)
115  {
116  auto message = new QListWidgetItem(e.what());
117  message->setForeground(Qt::red);
118  output->addItem(message);
119  }
120  else
121  {
122  CVLog::Error(e.what());
123  }
124  }
125 
126  m_isExecuting = false;
127  Q_EMIT executionFinished();
128 }
129 
130 void PythonInterpreter::executeCodeWithState(const std::string &code,
131  QListWidget *output,
132  State &state)
133 {
134  executeCodeString<py::eval_mode::eval_statements>(code, output, state);
135 }
136 
137 void PythonInterpreter::executeStatementWithState(const std::string &code,
138  QListWidget *output,
139  State &state)
140 {
141  executeCodeString<py::eval_mode::eval_single_statement>(code, output, state);
142 }
143 
144 void PythonInterpreter::executeCode(const std::string &code, QListWidget *output)
145 {
146  State tmpState;
147  executeCodeWithState(code, output, tmpState);
148 }
149 
150 void PythonInterpreter::executeFunction(const pybind11::object &function)
151 {
152  if (m_isExecuting)
153  {
154  return;
155  }
156 
157  m_isExecuting = true;
158  Q_EMIT executionStarted();
159  try
160  {
161  py::gil_scoped_acquire scopedGil;
162  PyStdErrOutStreamRedirect scopedRedirect;
163  function();
164  }
165  catch (const std::exception &e)
166  {
167  CVLog::Error("Failed to start Python actions: %s", e.what());
168  }
169  m_isExecuting = false;
170  Q_EMIT executionFinished();
171 }
172 
174 {
175  plgPrint() << "Initializing the interpreter with: " << config;
176 
177 #ifdef Q_OS_LINUX
178  // Work-around issue: undefined symbol: PyExc_RecursionError
179  // when trying to import numpy in the intepreter
180  // e.g: https://github.com/numpy/numpy/issues/14946
181  // https://stackoverflow.com/questions/49784583/numpy-import-fails-on-multiarray-extension-library-when-called-from-embedded-pyt
182  // This workaround is weak
183 
184  const auto displaydlopenError = []()
185  {
186  char *error = dlerror();
187  if (error)
188  {
189  plgWarning() << "dlopen error: " << error;
190  }
191  };
192 
193  QDir appDir = QCoreApplication::applicationDirPath();
194  QString libDirPath = QString("%1/lib").arg(appDir.absolutePath());
195  QDir libDir(libDirPath);
196 
197  char soName[25];
198  snprintf(
199  soName, 24, "libpython%d.%d.so*", PythonVersion.versionMajor, PythonVersion.versionMinor);
200 
201  QStringList filters;
202  filters << soName;
203  libDir.setNameFilters(filters);
204  QStringList fileList = libDir.entryList(QDir::Files);
205  if (!fileList.isEmpty())
206  {
207  QString soFilePath = QString("%1/%2").arg(libDirPath, fileList.first());
208  m_libPythonHandle =
209  dlopen(CVTools::FromQString(soFilePath).c_str(), RTLD_LAZY | RTLD_GLOBAL);
210  }
211 
212  if (!m_libPythonHandle)
213  {
214  displaydlopenError();
215  snprintf(soName,
216  24,
217  "libpython%d.%dm.so*",
220  filters.clear();
221  filters << soName;
222  libDir.setNameFilters(filters);
223  fileList = libDir.entryList(QDir::Files);
224  if (!fileList.isEmpty())
225  {
226  QString soFilePath = QString("%1/%2").arg(libDirPath, fileList.first());
227  m_libPythonHandle =
228  dlopen(CVTools::FromQString(soFilePath).c_str(), RTLD_LAZY | RTLD_GLOBAL);
229  }
230 
231  if (!m_libPythonHandle)
232  {
233  displaydlopenError();
234  }
235  }
236 #endif
237  if (config.type() != PythonConfig::Type::System)
238  {
239  // We use PEP 0587 to init the interpreter.
240  // The changes introduced in this PEP allows to handle the error
241  // when the interpreter could not be initialized.
242  //
243  // Before that the python interpreter would simply exit the program
244  // and that could be a bad user experience
245  //
246  // https://www.python.org/dev/peps/pep-0587/
247  // https://docs.python.org/3/c-api/init_config.html#init-python-config
248  m_config = config.pythonCompatiblePaths();
249  PyStatus status;
250 
251  PyConfig pyConfig;
252  PyConfig_InitPythonConfig(&pyConfig);
253  pyConfig.isolated = 1;
254 
255  status = PyConfig_SetString(&pyConfig, &pyConfig.home, m_config.pythonHome());
256  if (PyStatus_Exception(status))
257  {
258  PyConfig_Clear(&pyConfig);
259  throw std::runtime_error(status.err_msg);
260  }
261 
262  status = PyConfig_SetString(&pyConfig, &pyConfig.pythonpath_env, m_config.pythonPath());
263  if (PyStatus_Exception(status))
264  {
265  PyConfig_Clear(&pyConfig);
266  throw std::runtime_error(status.err_msg);
267  }
268 
269  status = PyConfig_Read(&pyConfig);
270  if (PyStatus_Exception(status))
271  {
272  PyConfig_Clear(&pyConfig);
273  throw std::runtime_error(status.err_msg);
274  }
275 
276  status = Py_InitializeFromConfig(&pyConfig);
277  if (PyStatus_Exception(status))
278  {
279  PyConfig_Clear(&pyConfig);
280  throw std::runtime_error(status.err_msg);
281  }
282  }
283  else
284  {
285  Py_Initialize();
286  }
287 
288  // Make sure this module is imported
289  // so that we can later easily construct our consoles.
290  py::module::import("ccinternals");
291 }
292 
294 {
295  return Py_IsInitialized();
296 }
297 
299 {
300  if (Py_IsInitialized())
301  {
302  py::finalize_interpreter();
303 #ifdef Q_OS_LINUX
304  if (m_libPythonHandle)
305  {
306  dlclose(m_libPythonHandle);
307  m_libPythonHandle = nullptr;
308  }
309 #else
310  Q_UNUSED(this);
311 #endif
312  }
313 }
314 
316 {
317  return m_isExecuting;
318 }
319 
321 {
322  return m_config;
323 }
constexpr Version PythonVersion(PY_MAJOR_VERSION, PY_MINOR_VERSION, PY_MICRO_VERSION)
Python Version the plugin was compiled against.
static py::dict CreateGlobals()
PluginLogger< CVLog::LOG_WARNING > plgWarning
Definition: Utilities.h:102
PluginLogger< CVLog::LOG_STANDARD > plgPrint
Definition: Utilities.h:100
static bool Warning(const char *format,...)
Prints out a formatted warning message in console.
Definition: CVLog.cpp:133
static bool Error(const char *format,...)
Display an error dialog with formatted message.
Definition: CVLog.cpp:143
static std::string FromQString(const QString &qs)
Definition: CVTools.cpp:100
Writes messages to the QListWidget given.
Definition: Consoles.h:121
const wchar_t * pythonPath() const
Returns the pythonPath.
const wchar_t * pythonHome() const
Returns the pythonHome.
void executeCode(const std::string &code, QListWidget *output)
void executeFunction(const pybind11::object &function)
void executeCodeWithState(const std::string &code, QListWidget *output, PythonInterpreter::State &state)
const PythonConfigPaths & config() const
void executionFinished()
PythonInterpreter(QObject *parent=nullptr)
static bool IsInitialized()
bool executeFile(const std::string &filePath)
Execution functions (and slots)
void executeStatementWithState(const std::string &code, QListWidget *output, PythonInterpreter::State &state)
void initialize(const PythonConfig &config)
Redirects messages to ACloudViewer's console output.
Definition: Consoles.h:77
static void error(char *msg)
Definition: lsd.c:159
constexpr Rgb red(MAX, 0, 0)
constexpr Rgb orange(MAX, MAX/2, 0)
uint16_t versionMinor
Definition: PythonConfig.h:59
uint16_t versionMajor
Definition: PythonConfig.h:58