diff --git a/pythonfmu3/pythonfmu-export/src/pythonfmu/IPyState.hpp b/pythonfmu3/pythonfmu-export/src/pythonfmu/IPyState.hpp new file mode 100644 index 0000000..38375ab --- /dev/null +++ b/pythonfmu3/pythonfmu-export/src/pythonfmu/IPyState.hpp @@ -0,0 +1,18 @@ +#ifndef PYTHONFMU_IPYTHONSTATE_HPP +#define PYTHONFMU_IPYTHONSTATE_HPP + +namespace pythonfmu +{ + class IPyState + { + public: + IPyState() = default; + IPyState(IPyState const& other) = delete; + IPyState(IPyState&& other) = delete; + IPyState& operator=(IPyState const& other) = delete; + IPyState& operator=(IPyState&& other) = delete; + virtual ~IPyState() = default; + }; +} + +#endif //PYTHONFMU_IPYTHONSTATE_HPP \ No newline at end of file diff --git a/pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.cpp b/pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.cpp index acf0f91..fc8cb2a 100644 --- a/pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.cpp +++ b/pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.cpp @@ -1,4 +1,5 @@ +#include "pythonfmu/IPyState.hpp" #include "pythonfmu/PySlaveInstance.hpp" #include "pythonfmu/PyState.hpp" @@ -7,6 +8,7 @@ #include #include +#include #include #include #include @@ -44,8 +46,9 @@ inline void py_safe_run(const std::function& f) PyGILState_Release(gil_state); } -PySlaveInstance::PySlaveInstance(std::string instanceName, std::string resources, const cppfmu::Logger& logger, const bool visible) - : instanceName_(std::move(instanceName)) +PySlaveInstance::PySlaveInstance(std::string instanceName, std::string resources, const cppfmu::Logger& logger, const bool visible, std::shared_ptr pyState) + : pyState_{ std::move(pyState) } + , instanceName_(std::move(instanceName)) , resources_(std::move(resources)) , logger_(logger) , visible_(visible) @@ -625,7 +628,10 @@ PySlaveInstance::~PySlaveInstance() } // namespace pythonfmu -std::unique_ptr pyState = nullptr; +namespace { + std::mutex pyStateMutex{}; + std::shared_ptr pyState{}; +} std::unique_ptr CppfmuInstantiateSlave( cppfmu::FMIString instanceName, @@ -650,9 +656,57 @@ std::unique_ptr CppfmuInstantiateSlave( #endif } - if (pyState == nullptr) { - pyState = std::make_unique(); + { + auto const ensurePyStateAlive = [&]() { + auto const lock = std::lock_guard{pyStateMutex}; + if (nullptr == pyState) pyState = std::make_shared(); + }; + + ensurePyStateAlive(); + return std::make_unique( + instanceName, resources, logger, visible, pyState); } +} + +extern "C" { + + // The PyState instance owns it's own thread for constructing and destroying the Py* from the same thread. + // Creation of an std::thread increments ref counter of a shared library. So, when the client unloads the library + // the library won't be freed, as std::thread is alive, and the std::thread itself waits for de-initialization request. + // Thus, use DllMain on Windows and __attribute__((destructor)) on Linux for signaling to the PyState about de-initialization. + void finalizePythonInterpreter() + { + pyState = nullptr; + } +} - return std::make_unique(instanceName, resources, logger, visible); +namespace +{ +#ifdef _WIN32 +#include + + BOOL APIENTRY DllMain(HMODULE hModule, + DWORD ul_reason_for_call, + LPVOID lpReserved) + { + switch (ul_reason_for_call) { + case DLL_PROCESS_ATTACH: + break; + case DLL_THREAD_ATTACH: + case DLL_THREAD_DETACH: + break; + case DLL_PROCESS_DETACH: + finalizePythonInterpreter(); + break; + } + return TRUE; +} +#elif defined(__linux__) + __attribute__((destructor)) void onLibraryUnload() + { + finalizePythonInterpreter(); +} +#else +#error port the code +#endif } diff --git a/pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.hpp b/pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.hpp index a42cb22..028e6fe 100644 --- a/pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.hpp +++ b/pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.hpp @@ -3,6 +3,7 @@ #define PYTHONFMU_SLAVEINSTANCE_HPP #include "cppfmu/cppfmu_cs.hpp" +#include "pythonfmu/IPyState.hpp" #include #include @@ -15,7 +16,7 @@ class PySlaveInstance : public cppfmu::SlaveInstance { public: - PySlaveInstance(std::string instanceName, std::string resources, const cppfmu::Logger& logger, bool visible); + PySlaveInstance(std::string instanceName, std::string resources, const cppfmu::Logger& logger, bool visible, std::shared_ptr pyState); void initialize(PyGILState_STATE gilState); @@ -53,6 +54,7 @@ class PySlaveInstance : public cppfmu::SlaveInstance ~PySlaveInstance() override; private: + std::shared_ptr pyState_; PyObject* pClass_; PyObject* pInstance_{}; PyObject* pMessages_{}; diff --git a/pythonfmu3/pythonfmu-export/src/pythonfmu/PyState.hpp b/pythonfmu3/pythonfmu-export/src/pythonfmu/PyState.hpp index 2abc948..ae64364 100644 --- a/pythonfmu3/pythonfmu-export/src/pythonfmu/PyState.hpp +++ b/pythonfmu3/pythonfmu-export/src/pythonfmu/PyState.hpp @@ -2,40 +2,84 @@ #ifndef PYTHONFMU_PYTHONSTATE_HPP #define PYTHONFMU_PYTHONSTATE_HPP +#include "IPyState.hpp" #include +#include #include +#include +#include namespace pythonfmu { - -class PyState -{ -public: - PyState() + class PyState : public IPyState { - was_initialized_ = Py_IsInitialized(); + public: + PyState() + : _initDeinitPyThread_{ &PyState::Worker, this } + { + auto lock = std::unique_lock{ mutex_ }; + conditionalVariable_.wait(lock, [&] { return consructed_; }); + } - if (!was_initialized_) { - Py_SetProgramName(L"./PythonFMU"); - Py_Initialize(); - PyEval_InitThreads(); - _mainPyThread = PyEval_SaveThread(); + ~PyState() override + { + { + auto const lock = std::lock_guard{ mutex_ }; + destroyRequested_ = true; + } + conditionalVariable_.notify_one(); + if (_initDeinitPyThread_.joinable()) _initDeinitPyThread_.join(); } - } - ~PyState() - { - if (!was_initialized_) { - PyEval_RestoreThread(_mainPyThread); - Py_Finalize(); + private: + + // In accordance to the documentation https://docs.python.org/3/c-api/init.html#c.Py_FinalizeEx + // the Py_Initialize/Py_Finalize should be called from the same + // thread. The FMI standard allows to call fmi functions from different threads, also different threads + // could be used for loading unloading the FMU libraries. There is a deadlock, when the simulation tool + // unloads the FMU library from the thread which is differs from the thread where the Py_Initialize was called. + // Create a new thread which is used for calling Py_Initialize and Py_Deinitialize. The thread waits for + // notification from destructor and then calls Py_Deinitialize. + + void Worker() + { + // It will be nullptr in case when some other tool already called Py_IsInitialized. + // There is no need to call Py_Finalize thus, this thread can exit ASAP. + auto const mainPyThread = []() -> PyThreadState* { + auto const justInitialized = !Py_IsInitialized(); + if (justInitialized) { + Py_SetProgramName(L"./PythonFMU"); + Py_Initialize(); +#if PY_VERSION_HEX < 0x03070000 + PyEval_InitThreads(); +#endif + return PyEval_SaveThread(); + } + return nullptr; + }(); + + { + auto const lock = std::lock_guard{ mutex_ }; + consructed_ = true; + } + conditionalVariable_.notify_one(); + + if (nullptr != mainPyThread) { + auto lock = std::unique_lock{ mutex_ }; + conditionalVariable_.wait(lock, [&] { return destroyRequested_; }); + + PyEval_RestoreThread(mainPyThread); + Py_Finalize(); + } } - } -private: - bool was_initialized_; - PyThreadState* _mainPyThread; -}; + bool consructed_ = false; + bool destroyRequested_ = false; + std::condition_variable conditionalVariable_; + std::mutex mutex_; + std::thread _initDeinitPyThread_; + }; } // namespace pythonfmu -#endif //PYTHONFMU_PYTHONSTATE_HPP +#endif //PYTHONFMU_PYTHONSTATE_HPP \ No newline at end of file