NEURON
nrnpy.cpp
Go to the documentation of this file.
1 #include "nrnpy.h"
2 #include <../../nrnconf.h>
3 // For Linux and Max OS X,
4 // Solve the problem of not knowing what version of Python the user has by
5 // possibly deferring linking to libnrnpython.so to run time using the proper
6 // Python interface
7 #include <stdio.h>
8 #include <stdlib.h>
9 #include <InterViews/resource.h>
10 #include "nrnoc2iv.h"
11 #include "classreg.h"
12 #include "nonvintblock.h"
13 #include "nrnmpi.h"
14 
15 #include <algorithm>
16 #include <cctype>
17 #include <sstream>
18 
19 namespace neuron::python {
20 // Declared extern in nrnpy.h, defined here.
22 } // namespace neuron::python
23 // Backwards-compatibility hack
25 
26 extern int nrn_nopython;
27 extern std::string nrnpy_pyexe;
28 extern int nrn_is_python_extension;
30 #if DARWIN
31 extern void nrn_possible_mismatched_arch(const char*);
32 #endif
33 
34 #ifdef NRNPYTHON_DYNAMICLOAD
35 #include "nrnwrap_dlfcn.h"
36 extern char* neuron_home;
37 static nrnpython_reg_real_t load_nrnpython();
38 #else
40 #endif
41 
42 void nrnpython() {
43  if (neuron::python::methods.hoc_nrnpython) {
45  } else {
46  hoc_retpushx(0.);
47  }
48 }
49 
50 // Stub class for when Python does not exist
51 static void* p_cons(Object*) {
52  return nullptr;
53 }
54 static void p_destruct(void*) {}
55 
56 #ifdef NRNPYTHON_DYNAMICLOAD
57 static std::string nrnpy_pylib{}, nrnpy_pyversion{};
58 
59 /**
60  * @brief Wrapper that executes a command and captures stdout.
61  *
62  * Throws std::runtime_error if the command does not execute cleanly.
63  */
64 static std::string check_output(std::string command) {
65  std::FILE* const p = popen(command.c_str(), "r");
66  if (!p) {
67  throw std::runtime_error("popen(" + command + ", \"r\") failed");
68  }
69  std::string output;
70  std::array<char, 1024> buffer{};
71  while (std::fgets(buffer.data(), buffer.size() - 1, p)) {
72  output += buffer.data();
73  }
74  if (auto const code = pclose(p)) {
75  std::ostringstream err;
76  err << "'" << command << "' did not terminate cleanly, pclose returned non-zero (" << code
77  << ") after the following output had been read:\n"
78  << output;
79  throw std::runtime_error(err.str());
80  }
81  return output;
82 }
83 
84 // Included in C++20
85 static bool starts_with(std::string_view str, std::string_view prefix) {
86  return str.substr(0, prefix.size()) == prefix;
87 }
88 static bool ends_with(std::string_view str, std::string_view suffix) {
89  return str.size() >= suffix.size() &&
90  str.substr(str.size() - suffix.size(), std::string_view::npos) == suffix;
91 }
92 
93 /**
94  * @brief Figure out which Python to load.
95  *
96  * When dynamic Python support is enabled, NEURON needs to figure out which
97  * libpythonX.Y to load, and then load it followed by the corresponding
98  * libnrnpythonX.Y. This can be steered both using commandline options and by
99  * using environment variables. The logic is as follows:
100  *
101  * * the -pyexe argument to nrniv (special) takes precedence over NRN_PYTHONEXE
102  * and we have to assume that the other NRN_PY* environment variables are not
103  * compatible with it and use nrnpyenv.sh to find compatible values for them,
104  * i.e. -pyexe implies that NRN_PY* are ignored.
105  * * if -pyexe is *not* passed, then we examine the NRN_PY* environment
106  * variables:
107  * * if all of them are set, nrnpyenv.sh is not run and they are assumed to
108  * form a coherent set
109  * * if only some, or none, of them are set, nrnpyenv.sh is run to fill in
110  * the missing values. NRN_PYTHONEXE is an input to nrnpyenv.sh, so if this
111  * is set then we will not search $PATH
112  */
113 static void set_nrnpylib() {
114  std::array<std::pair<std::string&, const char*>, 3> params{
115  {{nrnpy_pylib, "NRN_PYLIB"},
116  {nrnpy_pyexe, "NRN_PYTHONEXE"},
117  {nrnpy_pyversion, "NRN_PYTHONVERSION"}}};
118  auto const all_set = [&params]() {
119  return std::all_of(params.begin(), params.end(), [](auto const& p) {
120  return !p.first.empty();
121  });
122  };
123  if (nrnpy_pyexe.empty()) {
124  // -pyexe was not passed, read from the environment
125  for (auto& [glob_var, env_var]: params) {
126  if (const char* v = std::getenv(env_var)) {
127  glob_var = v;
128  }
129  }
130  if (all_set()) {
131  // the environment specified everything, nothing more to do
132  return;
133  }
134  }
135  // Populate missing values using nrnpyenv.sh. Pass the possibly-null value of nrnpy_pyexe, which
136  // may have come from -pyexe or NRN_PYTHONEXE, to nrnpyenv.sh. Do all of this on rank 0, and
137  // broadcast the results to other ranks afterwards.
138  if (nrnmpi_myid_world == 0) {
139  // Construct a command to execute
140  auto const command = []() -> std::string {
141 #ifdef MINGW
142  std::string bnrnhome{neuron_home}, fnrnhome{neuron_home};
143  std::replace(bnrnhome.begin(), bnrnhome.end(), '/', '\\');
144  std::replace(fnrnhome.begin(), fnrnhome.end(), '\\', '/');
145  return bnrnhome + R"(\mingw\usr\bin\bash )" + fnrnhome + "/bin/nrnpyenv.sh " +
146  nrnpy_pyexe + " --NEURON_HOME=" + fnrnhome;
147 #else
148  return "bash " + std::string{neuron_home} + "/../../bin/nrnpyenv.sh " + nrnpy_pyexe;
149 #endif
150  }();
151  // Execute the command, capture its stdout and wrap that in a C++ stream. This will throw if
152  // the commnand fails.
153  std::istringstream cmd_stdout{check_output(command)};
154  std::string line;
155  // if line is of the form:
156  // export FOO="bar"
157  // then proc_line(x, "FOO") sets x to bar
158  auto const proc_line = [](std::string_view line, auto& glob_var, std::string_view env_var) {
159  std::string_view const suffix{"\""};
160  auto const prefix = "export " + std::string{env_var} + "=\"";
161  if (starts_with(line, prefix) && ends_with(line, suffix)) {
162  line.remove_prefix(prefix.size());
163  line.remove_suffix(suffix.size());
164  if (!glob_var.empty() && glob_var != line) {
165  Printf(fmt::format(
166  "WARNING: overriding {} = {} with {}\n", env_var, glob_var, line)
167  .c_str());
168  }
169  glob_var = line;
170  }
171  };
172  // Process the output of nrnpyenv.sh line by line
173  while (std::getline(cmd_stdout, line)) {
174  for (auto& [glob_var, env_var]: params) {
175  proc_line(line, glob_var, env_var);
176  }
177  }
178  // After having run nrnpyenv.sh, we should know everything about the Python library that is
179  // to be loaded.
180  if (!all_set()) {
181  std::ostringstream err;
182  err << "After running nrnpyenv.sh (" << command << ") with output:\n"
183  << cmd_stdout.str()
184  << "\nwe are still missing information about the Python to be loaded:\n"
185  << " nrnpy_pyexe=" << nrnpy_pyexe << '\n'
186  << " nrnpy_pylib=" << nrnpy_pylib << '\n'
187  << " nrnpy_pyversion=" << nrnpy_pyversion << '\n';
188  throw std::runtime_error(err.str());
189  }
190  }
191 #if NRNMPI
192  if (nrnmpi_numprocs_world > 1) { // 0 broadcasts to everyone else.
193  nrnmpi_str_broadcast_world(nrnpy_pyexe, 0);
194  nrnmpi_str_broadcast_world(nrnpy_pylib, 0);
195  nrnmpi_str_broadcast_world(nrnpy_pyversion, 0);
196  }
197 #endif
198 }
199 #endif
200 
201 /**
202  * @brief Load + register an nrnpython library for a specific Python version.
203  *
204  * This finds the library (if needed because dynamic Python is enabled), opens it and gets + calls
205  * its nrnpython_reg_real method. This ensures that NEURON's global state knows about a Python
206  * implementation.
207  */
209  nrnpython_reg_real_t reg_fn{};
210 #if USE_PYTHON
211  if (!nrn_nopython) {
212 #ifdef NRNPYTHON_DYNAMICLOAD
213  void* handle{};
215  // find the details of the libpythonX.Y.so we are going to load.
216  try {
217  set_nrnpylib();
218  } catch (std::exception const& e) {
219  Fprintf(stderr,
220  fmt::format("Could not determine Python library details: {}\n", e.what())
221  .c_str());
222  exit(1);
223  }
224  handle = dlopen(nrnpy_pylib.c_str(), RTLD_NOW | RTLD_GLOBAL);
225  if (!handle) {
226  Fprintf(stderr,
227  fmt::format("Could not dlopen NRN_PYLIB: {}\n", nrnpy_pylib).c_str());
228 #if DARWIN
229  nrn_possible_mismatched_arch(nrnpy_pylib.c_str());
230 #endif
231  exit(1);
232  }
233  }
235  // Load libnrnpython.X.Y.so
236  reg_fn = load_nrnpython();
237  }
238 #else
239  // Python enabled, but not dynamic
240  reg_fn = nrnpython_reg_real;
241 #endif
242  }
243  if (reg_fn) {
244  // Register Python-specific things in the NEURON global state
245  reg_fn(&neuron::python::methods);
246  // Compatibility hack for legacy MOD file in nrntest
248  return;
249  }
250 #endif
251  // Stub implementation of PythonObject if Python support was not enabled, or a nrnpython library
252  // could not be loaded.
253  class2oc("PythonObject", p_cons, p_destruct, nullptr, nullptr, nullptr);
254 }
255 
256 #ifdef NRNPYTHON_DYNAMICLOAD // to end of file
257 static nrnpython_reg_real_t load_nrnpython() {
258  std::string pyversion{};
259  if (auto const pv10 = nrn_is_python_extension; pv10 > 0) {
260  // pv10 is one of the packed integers like 310 (3.10) or 39 (3.9)
261  auto const factor = (pv10 >= 100) ? 100 : 10;
262  pyversion = std::to_string(pv10 / factor) + "." + std::to_string(pv10 % factor);
263  } else {
264  if (nrnpy_pylib.empty() || nrnpy_pyversion.empty()) {
265  Fprintf(
266  stderr,
267  fmt::format("Do not know what Python to load [nrnpy_pylib={} nrnpy_pyversion={}]\n",
268  nrnpy_pylib,
269  nrnpy_pyversion)
270  .c_str());
271  return nullptr;
272  }
273  pyversion = nrnpy_pyversion;
274  // It's possible to get this far with an incompatible version, if nrnpy_pyversion and
275  // friends were set from the environment to bypass nrnpyenv.sh, and nrniv -python was
276  // launched.
277  auto const& supported_versions = neuron::config::supported_python_versions;
278  auto const iter =
279  std::find(supported_versions.begin(), supported_versions.end(), pyversion);
280  if (iter == supported_versions.end()) {
281  Fprintf(
282  stderr,
283  fmt::format("Python {} is not supported by this NEURON installation (supported:",
284  pyversion)
285  .c_str());
286  for (auto const& good_ver: supported_versions) {
287  Fprintf(stderr, fmt::format(" {}", good_ver).c_str());
288  }
289  Fprintf(stderr,
290  "). If you are seeing this message, your environment probably contains "
291  "NRN_PYLIB, NRN_PYTHONEXE and NRN_PYTHONVERSION settings that are "
292  "incompatible with this NEURON. Try unsetting them.\n");
293  return nullptr;
294  }
295  }
296  // Construct libnrnpythonX.Y.so (or other platforms' equivalent)
297  std::string name;
298  name.append(neuron::config::shared_library_prefix);
299  name.append("nrnpython");
300  name.append(pyversion);
301  name.append(neuron::config::shared_library_suffix);
302 #ifndef MINGW
303  // Build a path from neuron_home on macOS and Linux
304  name = neuron_home + ("/../../lib/" + name);
305 #endif
306  auto* const handle = dlopen(name.c_str(), RTLD_NOW);
307  if (!handle) {
308  Fprintf(stderr, fmt::format("Could not load {}\n", name).c_str());
309  Fprintf(stderr,
310  fmt::format("nrn_is_python_extension={}\n", nrn_is_python_extension).c_str());
311  return nullptr;
312  }
313  auto* const reg = reinterpret_cast<nrnpython_reg_real_t>(dlsym(handle, "nrnpython_reg_real"));
314  if (!reg) {
315  Fprintf(stderr,
316  fmt::format("Could not load registration function from {}\n", name).c_str());
317  }
318  return reg;
319 }
320 #endif
void class2oc(const char *, ctor_f *cons, dtor_f *destruct, Member_func *, Member_ret_obj_func *, Member_ret_str_func *)
Definition: hoc_oop.cpp:1631
#define v
Definition: md1redef.h:11
DLFCN_EXPORT void * dlopen(const char *file, int mode)
Definition: dlfcn.c:331
DLFCN_NOINLINE DLFCN_EXPORT void * dlsym(void *handle, const char *name)
Definition: dlfcn.c:447
#define RTLD_NOW
Definition: dlfcn.h:47
#define RTLD_GLOBAL
Definition: dlfcn.h:56
void hoc_retpushx(double x)
Definition: hocusr.cpp:154
static bool ends_with(const std::string &haystack, const std::string &needle)
Check if haystack ends with needle.
static bool starts_with(const std::string &haystack, const std::string &needle)
Check if haystack starts with needle.
const char * neuron_home
Definition: hoc_init.cpp:227
const char * name
Definition: init.cpp:16
handle_interface< non_owning_identifier< storage > > handle
Non-owning handle to a Mechanism instance.
impl_ptrs methods
Collection of pointers to functions with python-version-specific implementations.
Definition: nrnpy.cpp:21
std::string to_string(const T &obj)
static char suffix[256]
Definition: nocpout.cpp:135
size_t p
void(*)(neuron::python::impl_ptrs *) nrnpython_reg_real_t
Definition: nrnpy.cpp:29
void nrnpython_reg_real(neuron::python::impl_ptrs *)
Populate NEURON state with information from a specific Python.
Definition: nrnpy_p2h.cpp:914
int nrn_nopython
void nrnpython_reg()
Load + register an nrnpython library for a specific Python version.
Definition: nrnpy.cpp:208
int nrn_is_python_extension
Definition: fileio.cpp:810
std::string nrnpy_pyexe
static void p_destruct(void *)
Definition: nrnpy.cpp:54
static void * p_cons(Object *)
Definition: nrnpy.cpp:51
int(* nrnpy_hoccommand_exec)(Object *)
Definition: nrnpy.cpp:24
void nrnpython()
Definition: nrnpy.cpp:42
int nrnmpi_numprocs_world
int nrnmpi_myid_world
int find(const int, const int, const int, const int, const int)
static struct prefix prefix[]
Definition: hocdec.h:173
Collection of pointers to functions with python-version-specific implementations.
Definition: nrnpy.h:25
int(* hoccommand_exec)(Object *)
Definition: nrnpy.h:37
void(* hoc_nrnpython)()
Definition: nrnpy.h:39
Definition: units.cpp:83
int Fprintf(FILE *stream, const char *fmt, Args... args)
Definition: logger.hpp:8
int Printf(const char *fmt, Args... args)
Definition: logger.hpp:18