Skip to content

Commit

Permalink
Modernize NMODL -> Python layer. (#1306)
Browse files Browse the repository at this point in the history
The changes are:

* Don't forcefully set the PYTHONPATH.
* Replace false object oriented code with a function (each).
* Replace a global variable, with a function to initialize.
* Use a reference for a pointer that's should never be a nullptr.
  • Loading branch information
1uc committed Jun 12, 2024
1 parent c5ecfea commit 2636b4e
Show file tree
Hide file tree
Showing 11 changed files with 198 additions and 272 deletions.
4 changes: 2 additions & 2 deletions src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -486,7 +486,7 @@ int main(int argc, const char* argv[]) {
if (sympy_conductance || sympy_analytic || sparse_solver_exists(*ast)) {
nmodl::pybind_wrappers::EmbeddedPythonLoader::get_instance()
.api()
->initialize_interpreter();
.initialize_interpreter();
if (sympy_conductance) {
logger->info("Running sympy conductance visitor");
SympyConductanceVisitor().visit_program(*ast);
Expand All @@ -507,7 +507,7 @@ int main(int argc, const char* argv[]) {
}
nmodl::pybind_wrappers::EmbeddedPythonLoader::get_instance()
.api()
->finalize_interpreter();
.finalize_interpreter();
}

{
Expand Down
3 changes: 3 additions & 0 deletions src/pybind/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ configure_file(${CMAKE_CURRENT_SOURCE_DIR}/ode_py.hpp.inc ${CMAKE_CURRENT_BINARY
add_library(pyembed pyembed.cpp)
set_property(TARGET pyembed PROPERTY POSITION_INDEPENDENT_CODE ON)
target_link_libraries(pyembed PRIVATE util)
target_link_libraries(pyembed PRIVATE fmt::fmt)

if(NOT LINK_AGAINST_PYTHON)
add_library(pywrapper SHARED ${CMAKE_CURRENT_SOURCE_DIR}/wrapper.cpp)
Expand All @@ -41,6 +42,8 @@ else()
target_compile_definitions(pyembed PRIVATE NMODL_STATIC_PYWRAPPER=1)
endif()

target_link_libraries(pywrapper PRIVATE fmt::fmt)

target_include_directories(pyembed PRIVATE ${PYBIND11_INCLUDE_DIR} ${PYTHON_INCLUDE_DIRS})
target_include_directories(pywrapper PRIVATE ${pybind11_INCLUDE_DIRS} ${PYTHON_INCLUDE_DIRS})
target_include_directories(pywrapper PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
Expand Down
80 changes: 51 additions & 29 deletions src/pybind/pyembed.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@
*
* SPDX-License-Identifier: Apache-2.0
*/
#include "pybind/pyembed.hpp"

#include <cstdlib>
#include <dlfcn.h>
#include <filesystem>
#include <pybind11/embed.h>


#include "config/config.h"
#include "pybind/pyembed.hpp"
#include "utils/logger.hpp"

#define STRINGIFY(x) #x
Expand All @@ -22,15 +24,43 @@ namespace nmodl {

namespace pybind_wrappers {

using nmodl_init_pybind_wrapper_api_fpointer = decltype(&nmodl_init_pybind_wrapper_api);

bool EmbeddedPythonLoader::have_wrappers() {
#if defined(NMODL_STATIC_PYWRAPPER)
static auto wrapper_api = nmodl::pybind_wrappers::init_pybind_wrap_api();
wrappers = &wrapper_api;
return true;
auto* init = &nmodl_init_pybind_wrapper_api;
#else
wrappers = static_cast<pybind_wrap_api*>(dlsym(RTLD_DEFAULT, "nmodl_wrapper_api"));
return wrappers != nullptr;
auto* init = (nmodl_init_pybind_wrapper_api_fpointer) (dlsym(RTLD_DEFAULT,
"nmodl_init_pybind_wrapper_api"));
#endif

if (init != nullptr) {
wrappers = init();
}

return init != nullptr;
}

void assert_compatible_python_versions() {
// This code is imported and slightly modified from PyBind11 because this
// is primarly in details for internal usage
// License of PyBind11 is BSD-style

std::string compiled_ver = fmt::format("{}.{}", PY_MAJOR_VERSION, PY_MINOR_VERSION);
auto pPy_GetVersion = (const char* (*) (void) ) dlsym(RTLD_DEFAULT, "Py_GetVersion");
if (pPy_GetVersion == nullptr) {
throw std::runtime_error("Unable to find the function `Py_GetVersion`");
}
const char* runtime_ver = pPy_GetVersion();
std::size_t len = compiled_ver.size();
if (std::strncmp(runtime_ver, compiled_ver.c_str(), len) != 0 ||
(runtime_ver[len] >= '0' && runtime_ver[len] <= '9')) {
throw std::runtime_error(
fmt::format("Python version mismatch. nmodl has been compiled with python {} and is "
"being run with python {}",
compiled_ver,
runtime_ver));
}
}

void EmbeddedPythonLoader::load_libraries() {
Expand All @@ -49,26 +79,7 @@ void EmbeddedPythonLoader::load_libraries() {
throw std::runtime_error("Failed to dlopen");
}

// This code is imported from PyBind11 because this is primarly in details for internal usage
// License of PyBind11 is BSD-style
{
std::string compiled_ver = fmt::format("{}.{}", PY_MAJOR_VERSION, PY_MINOR_VERSION);
const char* (*fun)(void) = (const char* (*) (void) ) dlsym(pylib_handle, "Py_GetVersion");
if (fun == nullptr) {
logger->critical("Unable to find the function `Py_GetVersion`");
throw std::runtime_error("Unable to find the function `Py_GetVersion`");
}
const char* runtime_ver = fun();
std::size_t len = compiled_ver.size();
if (std::strncmp(runtime_ver, compiled_ver.c_str(), len) != 0 ||
(runtime_ver[len] >= '0' && runtime_ver[len] <= '9')) {
logger->critical(
"nmodl has been compiled with python {} and is being run with python {}",
compiled_ver,
runtime_ver);
throw std::runtime_error("Python version mismatch between compile-time and runtime.");
}
}
assert_compatible_python_versions();

if (std::getenv("NMODLHOME") == nullptr) {
logger->critical("NMODLHOME environment variable must be set to load embedded python");
Expand All @@ -91,25 +102,36 @@ void EmbeddedPythonLoader::load_libraries() {
}

void EmbeddedPythonLoader::populate_symbols() {
wrappers = static_cast<pybind_wrap_api*>(dlsym(pybind_wrapper_handle, "nmodl_wrapper_api"));
if (!wrappers) {
#if defined(NMODL_STATIC_PYWRAPPER)
auto* init = &nmodl_init_pybind_wrapper_api;
#else
// By now it's been dynamically loaded with `RTLD_GLOBAL`.
auto* init = (nmodl_init_pybind_wrapper_api_fpointer) (dlsym(RTLD_DEFAULT,
"nmodl_init_pybind_wrapper_api"));
#endif

if (!init) {
const auto errstr = dlerror();
logger->critical("Tried but failed to load pybind wrapper symbols");
logger->critical(errstr);
throw std::runtime_error("Failed to dlsym");
}

wrappers = init();
}

void EmbeddedPythonLoader::unload() {
if (pybind_wrapper_handle) {
dlclose(pybind_wrapper_handle);
pybind_wrapper_handle = nullptr;
}
if (pylib_handle) {
dlclose(pylib_handle);
pylib_handle = nullptr;
}
}

const pybind_wrap_api* EmbeddedPythonLoader::api() {
const pybind_wrap_api& EmbeddedPythonLoader::api() {
return wrappers;
}

Expand Down
120 changes: 3 additions & 117 deletions src/pybind/pyembed.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,123 +7,11 @@

#pragma once

#include <pybind11/embed.h>

#include <set>
#include <stdexcept>
#include <string>
#include <vector>
#include "wrapper.hpp"

namespace nmodl {
namespace pybind_wrappers {


struct PythonExecutor {
virtual ~PythonExecutor() {}

virtual void operator()() = 0;
};


struct SolveLinearSystemExecutor: public PythonExecutor {
// input
std::vector<std::string> eq_system;
std::vector<std::string> state_vars;
std::set<std::string> vars;
bool small_system;
bool elimination;
// This is used only if elimination is true. It gives the root for the tmp variables
std::string tmp_unique_prefix;
std::set<std::string> function_calls;
// output
// returns a vector of solutions, i.e. new statements to add to block:
std::vector<std::string> solutions;
// and a vector of new local variables that need to be declared in the block:
std::vector<std::string> new_local_vars;
// may also return a python exception message:
std::string exception_message;
// executor function
void operator()() override;
};


struct SolveNonLinearSystemExecutor: public PythonExecutor {
// input
std::vector<std::string> eq_system;
std::vector<std::string> state_vars;
std::set<std::string> vars;
std::set<std::string> function_calls;
// output
// returns a vector of solutions, i.e. new statements to add to block:
std::vector<std::string> solutions;
// may also return a python exception message:
std::string exception_message;

// executor function
void operator()() override;
};


struct DiffeqSolverExecutor: public PythonExecutor {
// input
std::string node_as_nmodl;
std::string dt_var;
std::set<std::string> vars;
bool use_pade_approx;
std::set<std::string> function_calls;
std::string method;
// output
// returns solution, i.e. new statement to add to block:
std::string solution;
// may also return a python exception message:
std::string exception_message;

// executor function
void operator()() override;
};


struct AnalyticDiffExecutor: public PythonExecutor {
// input
std::vector<std::string> expressions;
std::set<std::string> used_names_in_block;
// output
// returns solution, i.e. new statement to add to block:
std::string solution;
// may also return a python exception message:
std::string exception_message;

// executor function
void operator()() override;
};


SolveLinearSystemExecutor* create_sls_executor_func();
SolveNonLinearSystemExecutor* create_nsls_executor_func();
DiffeqSolverExecutor* create_des_executor_func();
AnalyticDiffExecutor* create_ads_executor_func();
void destroy_sls_executor_func(SolveLinearSystemExecutor* exec);
void destroy_nsls_executor_func(SolveNonLinearSystemExecutor* exec);
void destroy_des_executor_func(DiffeqSolverExecutor* exec);
void destroy_ads_executor_func(AnalyticDiffExecutor* exec);

void initialize_interpreter_func();
void finalize_interpreter_func();

struct pybind_wrap_api {
decltype(&initialize_interpreter_func) initialize_interpreter;
decltype(&finalize_interpreter_func) finalize_interpreter;
decltype(&create_sls_executor_func) create_sls_executor;
decltype(&create_nsls_executor_func) create_nsls_executor;
decltype(&create_des_executor_func) create_des_executor;
decltype(&create_ads_executor_func) create_ads_executor;
decltype(&destroy_sls_executor_func) destroy_sls_executor;
decltype(&destroy_nsls_executor_func) destroy_nsls_executor;
decltype(&destroy_des_executor_func) destroy_des_executor;
decltype(&destroy_ads_executor_func) destroy_ads_executor;
};


/**
* A singleton class handling access to the pybind_wrap_api struct
*
Expand Down Expand Up @@ -154,14 +42,14 @@ class EmbeddedPythonLoader {
* Get access to the container struct for the pointers to the functions in the wrapper library.
* @return a pybind_wrap_api pointer
*/
const pybind_wrap_api* api();
const pybind_wrap_api& api();

~EmbeddedPythonLoader() {
unload();
}

private:
pybind_wrap_api* wrappers = nullptr;
pybind_wrap_api wrappers;

void* pylib_handle = nullptr;
void* pybind_wrapper_handle = nullptr;
Expand All @@ -180,7 +68,5 @@ class EmbeddedPythonLoader {
};


pybind_wrap_api init_pybind_wrap_api() noexcept;

} // namespace pybind_wrappers
} // namespace nmodl
Loading

0 comments on commit 2636b4e

Please sign in to comment.