pythonfmu3 0.3.4__py3-none-any.whl → 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. pythonfmu3/_version.py +1 -1
  2. pythonfmu3/builder.py +96 -24
  3. pythonfmu3/fmi3slave.py +23 -19
  4. pythonfmu3/modelexchange.py +17 -2
  5. pythonfmu3/osutil.py +34 -9
  6. pythonfmu3/pythonfmu-export/CMakeLists.txt +36 -4
  7. pythonfmu3/pythonfmu-export/generate_shim.py +88 -0
  8. pythonfmu3/pythonfmu-export/src/CMakeLists.txt +41 -13
  9. pythonfmu3/pythonfmu-export/src/cppfmu/cppfmu_common.hpp +3 -0
  10. pythonfmu3/pythonfmu-export/src/cppfmu/fmi_functions.cpp +6 -4
  11. pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.cpp +560 -571
  12. pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.hpp +20 -7
  13. pythonfmu3/pythonfmu-export/src/shim/fmi3_forwarders.cpp +612 -0
  14. pythonfmu3/pythonfmu-export/src/shim/shim_loader.cpp +157 -0
  15. pythonfmu3/pythonfmu-export/src/shim/shim_loader.hpp +29 -0
  16. pythonfmu3/resources/binaries/x86_64-linux/libpythonfmu-backend.so +0 -0
  17. pythonfmu3/resources/binaries/x86_64-linux/libpythonfmu-export.so +0 -0
  18. pythonfmu3/resources/binaries/x86_64-windows/pythonfmu-export.dll +0 -0
  19. pythonfmu3/tests/data/fmi3_host.c +31 -0
  20. pythonfmu3/tests/fmi3_ctypes_driver.py +137 -0
  21. pythonfmu3/tests/test_builder.py +48 -2
  22. pythonfmu3/tests/test_docs.py +70 -61
  23. pythonfmu3/tests/test_non_python_host.py +75 -0
  24. pythonfmu3/tests/test_osutil.py +23 -0
  25. pythonfmu3/tests/test_variables.py +1 -1
  26. pythonfmu3/tests/test_wrapper_ctypes.py +61 -0
  27. pythonfmu3/variables.py +4 -3
  28. {pythonfmu3-0.3.4.dist-info → pythonfmu3-1.0.0.dist-info}/METADATA +61 -25
  29. {pythonfmu3-0.3.4.dist-info → pythonfmu3-1.0.0.dist-info}/RECORD +33 -23
  30. {pythonfmu3-0.3.4.dist-info → pythonfmu3-1.0.0.dist-info}/WHEEL +1 -1
  31. {pythonfmu3-0.3.4.dist-info → pythonfmu3-1.0.0.dist-info}/entry_points.txt +0 -0
  32. {pythonfmu3-0.3.4.dist-info → pythonfmu3-1.0.0.dist-info}/licenses/LICENSE +0 -0
  33. {pythonfmu3-0.3.4.dist-info → pythonfmu3-1.0.0.dist-info}/top_level.txt +0 -0
@@ -6,32 +6,101 @@
6
6
 
7
7
  #include "cppfmu/cppfmu_cs.hpp"
8
8
 
9
+ #include <algorithm>
10
+ #include <cctype>
9
11
  #include <fstream>
10
- #include <functional>
11
12
  #include <mutex>
12
13
  #include <regex>
13
14
  #include <sstream>
15
+ #include <stdexcept>
14
16
  #include <utility>
15
17
 
16
18
  namespace pythonfmu
17
19
  {
18
20
 
19
- inline std::string getline(const std::string& fileName)
21
+ namespace
20
22
  {
21
- std::string line;
22
- std::ifstream infile(fileName);
23
- std::getline(infile, line);
24
- return line;
23
+
24
+ // Holds the GIL for the lifetime of the object. Releasing in the destructor guarantees
25
+ // the GIL is given back on every exit path, including C++ exceptions.
26
+ class GilGuard
27
+ {
28
+ public:
29
+ GilGuard()
30
+ : state_(PyGILState_Ensure())
31
+ { }
32
+ ~GilGuard() { PyGILState_Release(state_); }
33
+ GilGuard(const GilGuard&) = delete;
34
+ GilGuard& operator=(const GilGuard&) = delete;
35
+
36
+ private:
37
+ PyGILState_STATE state_;
38
+ };
39
+
40
+ // Owning reference to a PyObject (Py_XDECREF on destruction).
41
+ // Must only be destroyed while the GIL is held; declare it after the GilGuard.
42
+ class PyRef
43
+ {
44
+ public:
45
+ explicit PyRef(PyObject* obj = nullptr)
46
+ : obj_(obj)
47
+ { }
48
+ ~PyRef() { Py_XDECREF(obj_); }
49
+ PyRef(const PyRef&) = delete;
50
+ PyRef& operator=(const PyRef&) = delete;
51
+ PyRef(PyRef&& other) noexcept
52
+ : obj_(other.release())
53
+ { }
54
+
55
+ PyObject* get() const { return obj_; }
56
+ PyObject* release()
57
+ {
58
+ PyObject* tmp = obj_;
59
+ obj_ = nullptr;
60
+ return tmp;
61
+ }
62
+ explicit operator bool() const { return obj_ != nullptr; }
63
+
64
+ private:
65
+ PyObject* obj_;
66
+ };
67
+
68
+ template<typename F>
69
+ auto py_safe_run(F&& f) -> decltype(f())
70
+ {
71
+ GilGuard gil;
72
+ return f();
25
73
  }
26
74
 
27
- inline std::string findClassName(const std::string& fileName)
75
+ // Reads the first two lines of resources/slavemodule.txt: the module name and
76
+ // (for FMUs built by newer versions of PythonFMU3) the name of the slave class.
77
+ std::pair<std::string, std::string> readSlaveModuleInfo(const std::string& fileName)
78
+ {
79
+ std::ifstream infile(fileName);
80
+ if (!infile) {
81
+ throw cppfmu::FatalError(("Unable to open " + fileName).c_str());
82
+ }
83
+ std::string moduleName, className;
84
+ std::getline(infile, moduleName);
85
+ std::getline(infile, className);
86
+ auto trim = [](std::string& s) {
87
+ const auto ws = " \t\r\n";
88
+ s.erase(s.find_last_not_of(ws) + 1);
89
+ s.erase(0, s.find_first_not_of(ws));
90
+ };
91
+ trim(moduleName);
92
+ trim(className);
93
+ return {moduleName, className};
94
+ }
95
+
96
+ // Fallback for FMUs built by older versions that do not record the class name.
97
+ std::string findClassName(const std::string& fileName)
28
98
  {
29
99
  std::string line;
30
100
  std::ifstream infile(fileName);
31
- std::string regexStr(R"(^class (\w+)\(([^)]*\bFmi3Slave(?:Base)?\b[^)]*)\)\s*:)");
32
- while (getline(infile, line)) {
101
+ const std::regex re(R"(^class (\w+)\(([^)]*\bFmi3Slave(?:Base)?\b[^)]*)\)\s*:)");
102
+ while (std::getline(infile, line)) {
33
103
  std::smatch m;
34
- std::regex re(regexStr);
35
104
  if (std::regex_search(line, m, re)) {
36
105
  return m[1];
37
106
  }
@@ -39,63 +108,137 @@ inline std::string findClassName(const std::string& fileName)
39
108
  return "";
40
109
  }
41
110
 
42
- inline void py_safe_run(const std::function<void(PyGILState_STATE gilState)>& f)
111
+ // Returns nullptr (with a Python error set) on failure.
112
+ PyObject* makeVrList(const cppfmu::FMIValueReference* vr, std::size_t nvr)
113
+ {
114
+ PyRef list(PyList_New(static_cast<Py_ssize_t>(nvr)));
115
+ if (!list) return nullptr;
116
+ for (std::size_t i = 0; i < nvr; i++) {
117
+ PyObject* item = PyLong_FromUnsignedLong(vr[i]);
118
+ if (item == nullptr) return nullptr;
119
+ PyList_SetItem(list.get(), static_cast<Py_ssize_t>(i), item); // steals item
120
+ }
121
+ return list.release();
122
+ }
123
+
124
+ template<typename T, typename Convert>
125
+ PyObject* makeValueList(const T* values, std::size_t n, Convert convert)
126
+ {
127
+ PyRef list(PyList_New(static_cast<Py_ssize_t>(n)));
128
+ if (!list) return nullptr;
129
+ for (std::size_t i = 0; i < n; i++) {
130
+ PyObject* item = convert(values[i]);
131
+ if (item == nullptr) return nullptr;
132
+ PyList_SetItem(list.get(), static_cast<Py_ssize_t>(i), item); // steals item
133
+ }
134
+ return list.release();
135
+ }
136
+
137
+ PyObject* makeString(cppfmu::FMIString s)
138
+ {
139
+ if (s == nullptr) {
140
+ Py_RETURN_NONE;
141
+ }
142
+ return PyUnicode_FromString(s);
143
+ }
144
+
145
+ std::string toUtf8(PyObject* str)
146
+ {
147
+ PyRef bytes(str ? PyUnicode_AsEncodedString(str, "utf-8", "replace") : nullptr);
148
+ const char* data = bytes ? PyBytes_AsString(bytes.get()) : nullptr;
149
+ if (data == nullptr) {
150
+ PyErr_Clear();
151
+ return {};
152
+ }
153
+ return data;
154
+ }
155
+
156
+ // Formats the pending Python exception, including its traceback, and clears it.
157
+ std::string fetchPythonError()
158
+ {
159
+ PyObject *type = nullptr, *value = nullptr, *traceback = nullptr;
160
+ PyErr_Fetch(&type, &value, &traceback);
161
+ if (type == nullptr) {
162
+ return "unknown error (no Python exception set)";
163
+ }
164
+ PyErr_NormalizeException(&type, &value, &traceback);
165
+ PyRef pType(type), pValue(value), pTraceback(traceback);
166
+
167
+ std::string result;
168
+ PyRef tbModule(PyImport_ImportModule("traceback"));
169
+ if (tbModule) {
170
+ PyRef lines(PyObject_CallMethod(tbModule.get(), "format_exception", "OOO",
171
+ pType.get(), pValue ? pValue.get() : Py_None, pTraceback ? pTraceback.get() : Py_None));
172
+ if (lines) {
173
+ PyRef sep(PyUnicode_FromString(""));
174
+ PyRef joined(sep ? PyUnicode_Join(sep.get(), lines.get()) : nullptr);
175
+ if (joined) result = toUtf8(joined.get());
176
+ }
177
+ }
178
+ if (result.empty()) {
179
+ PyRef repr(PyObject_Repr(pValue ? pValue.get() : pType.get()));
180
+ result = toUtf8(repr.get());
181
+ if (result.empty()) result = "unknown error";
182
+ }
183
+ PyErr_Clear(); // in case formatting itself raised
184
+ return result;
185
+ }
186
+
187
+
188
+ // Reads `obj.name` as a bool. Returns `fallback` when the attribute is absent.
189
+ // Returns -1 (Python error set) on failure.
190
+ int readBoolAttr(PyObject* obj, const char* name, bool fallback)
43
191
  {
44
- PyGILState_STATE gil_state = PyGILState_Ensure();
45
- f(gil_state);
46
- PyGILState_Release(gil_state);
192
+ if (!PyObject_HasAttrString(obj, name)) return fallback ? 1 : 0;
193
+ PyRef attr(PyObject_GetAttrString(obj, name));
194
+ if (!attr) return -1;
195
+ return PyObject_IsTrue(attr.get());
47
196
  }
48
197
 
198
+ } // namespace
199
+
49
200
  PySlaveInstance::PySlaveInstance(std::string instanceName, std::string resources, const cppfmu::Logger& logger, const bool visible, std::shared_ptr<IPyState> pyState)
50
201
  : pyState_{ std::move(pyState) }
202
+ , visible_(visible)
51
203
  , instanceName_(std::move(instanceName))
52
204
  , resources_(std::move(resources))
53
205
  , logger_(logger)
54
- , visible_(visible)
55
206
  {
56
- py_safe_run([this](PyGILState_STATE gilState) {
57
- // Append resources path to python sys path
58
- PyObject* sys_module = PyImport_ImportModule("sys");
59
- if (sys_module == nullptr) {
60
- handle_py_exception("[ctor] PyImport_ImportModule", gilState);
61
- }
62
- PyObject* sys_path = PyObject_GetAttrString(sys_module, "path");
63
- Py_DECREF(sys_module);
64
- if (sys_path == nullptr) {
65
- handle_py_exception("[ctor] PyObject_GetAttrString", gilState);
66
- }
67
- int success = PyList_Insert(sys_path, 0, PyUnicode_FromString(resources_.c_str()));
68
- Py_DECREF(sys_path);
69
- if (success != 0) {
70
- handle_py_exception("[ctor] PyList_Insert", gilState);
71
- }
207
+ py_safe_run([this]() {
208
+ // The destructor does not run when a constructor throws, so release what was
209
+ // acquired so far before propagating the error.
210
+ try {
211
+ // Prepend resources path to python sys path
212
+ PyRef sysModule(PyImport_ImportModule("sys"));
213
+ if (!sysModule) handle_py_exception("[ctor] PyImport_ImportModule(sys)");
214
+ PyRef sysPath(PyObject_GetAttrString(sysModule.get(), "path"));
215
+ if (!sysPath) handle_py_exception("[ctor] PyObject_GetAttrString(sys.path)");
216
+ PyRef resourcesStr(PyUnicode_FromString(resources_.c_str()));
217
+ if (!resourcesStr || PyList_Insert(sysPath.get(), 0, resourcesStr.get()) != 0) {
218
+ handle_py_exception("[ctor] PyList_Insert");
219
+ }
72
220
 
73
- std::string moduleName = getline(resources_ + "/slavemodule.txt");
74
- PyObject* pModule = PyImport_ImportModule(moduleName.c_str());
75
- if (pModule == nullptr) {
76
- PyErr_Print();
77
- handle_py_exception("[ctor] PyImport_ImportModule", gilState);
78
- }
221
+ const auto info = readSlaveModuleInfo(resources_ + "/slavemodule.txt");
222
+ const std::string& moduleName = info.first;
223
+ PyRef module(PyImport_ImportModule(moduleName.c_str()));
224
+ if (!module) handle_py_exception("[ctor] PyImport_ImportModule(" + moduleName + ")");
79
225
 
80
- std::string className = findClassName(resources_ + "/" + moduleName + ".py");
81
- if (className.empty()) {
82
- cleanPyObject();
83
- throw cppfmu::FatalError("Unable to find class extending Fmi3SlaveBase!");
84
- }
226
+ std::string className = info.second;
227
+ if (className.empty()) {
228
+ className = findClassName(resources_ + "/" + moduleName + ".py");
229
+ }
230
+ if (className.empty()) {
231
+ throw cppfmu::FatalError("Unable to find class extending Fmi3SlaveBase!");
232
+ }
85
233
 
86
- PyObject* pyClassName = Py_BuildValue("s", className.c_str());
87
- if (pyClassName == nullptr) {
88
- handle_py_exception("[ctor] Py_BuildValue", gilState);
89
- }
234
+ pClass_ = PyObject_GetAttrString(module.get(), className.c_str());
235
+ if (pClass_ == nullptr) handle_py_exception("[ctor] PyObject_GetAttr(" + className + ")");
90
236
 
91
- pClass_ = PyObject_GetAttr(pModule, pyClassName);
92
- Py_DECREF(pyClassName);
93
- Py_DECREF(pModule);
94
- if (pClass_ == nullptr) {
95
- handle_py_exception("[ctor] PyObject_GetAttr", gilState);
237
+ initialize();
238
+ } catch (...) {
239
+ cleanPyObject();
240
+ throw;
96
241
  }
97
-
98
- initialize(gilState);
99
242
  });
100
243
  }
101
244
 
@@ -103,568 +246,423 @@ void PySlaveInstance::clearLogBuffer() const
103
246
  {
104
247
  clearLogStrBuffer();
105
248
 
106
- PyObject* debugField = Py_BuildValue("s", "debug");
107
- PyObject* msgField = Py_BuildValue("s", "msg");
108
- PyObject* categoryField = Py_BuildValue("s", "category");
109
- PyObject* statusField = Py_BuildValue("s", "status");
249
+ if (pMessages_ == nullptr || !PyList_Check(pMessages_)) {
250
+ return;
251
+ }
110
252
 
111
- if ( pMessages_ != NULL && PyList_Check(pMessages_) ) {
112
- auto size = PyList_Size(pMessages_);
113
- for (auto i = 0; i < size; i++) {
114
- PyObject* msg = PyList_GetItem(pMessages_, i);
253
+ // Logging must never mask the original problem: keep any pending exception aside.
254
+ PyObject *errType, *errValue, *errTraceback;
255
+ PyErr_Fetch(&errType, &errValue, &errTraceback);
115
256
 
116
- auto debugAttr = PyObject_GetAttr(msg, debugField);
117
- auto msgAttr = PyObject_GetAttr(msg, msgField);
118
- auto categoryAttr = PyObject_GetAttr(msg, categoryField);
119
- auto statusAttr = PyObject_GetAttr(msg, statusField);
257
+ auto& logger = const_cast<cppfmu::Logger&>(logger_);
258
+ const auto size = PyList_Size(pMessages_);
259
+ for (Py_ssize_t i = 0; i < size; i++) {
260
+ PyObject* msg = PyList_GetItem(pMessages_, i); // borrowed
120
261
 
121
- auto statusValue = static_cast<cppfmu::FMIStatus>(PyLong_AsLong(statusAttr));
262
+ PyRef debugAttr(PyObject_GetAttrString(msg, "debug"));
263
+ PyRef msgAttr(PyObject_GetAttrString(msg, "msg"));
264
+ PyRef categoryAttr(PyObject_GetAttrString(msg, "category"));
265
+ PyRef statusAttr(PyObject_GetAttrString(msg, "status"));
266
+ if (!debugAttr || !msgAttr || !categoryAttr || !statusAttr) {
267
+ PyErr_Clear();
268
+ continue;
269
+ }
122
270
 
123
- PyObject* msgValue = PyUnicode_AsEncodedString(msgAttr, "utf-8", nullptr);
124
- char* msgStr = PyBytes_AsString(msgValue);
125
- logStrBuffer.emplace_back(msgValue);
271
+ auto statusValue = static_cast<cppfmu::FMIStatus>(PyLong_AsLong(statusAttr.get()));
126
272
 
127
- const char* categoryStr = "";
128
- if (categoryAttr != Py_None) {
129
- PyObject* categoryValue = PyUnicode_AsEncodedString(categoryAttr, "utf-8", nullptr);
273
+ PyObject* msgValue = PyUnicode_AsEncodedString(msgAttr.get(), "utf-8", "replace");
274
+ if (msgValue == nullptr) {
275
+ PyErr_Clear();
276
+ continue;
277
+ }
278
+ const char* msgStr = PyBytes_AsString(msgValue);
279
+ logStrBuffer.emplace_back(msgValue);
280
+
281
+ const char* categoryStr = "";
282
+ if (categoryAttr.get() != Py_None) {
283
+ PyObject* categoryValue = PyUnicode_AsEncodedString(categoryAttr.get(), "utf-8", "replace");
284
+ if (categoryValue != nullptr) {
130
285
  categoryStr = PyBytes_AsString(categoryValue);
131
286
  logStrBuffer.emplace_back(categoryValue);
132
287
  }
288
+ }
133
289
 
134
- if (PyObject_IsTrue(debugAttr)) {
135
- const_cast<cppfmu::Logger&>(logger_).DebugLog(statusValue, categoryStr, msgStr);
136
- } else {
137
- const_cast<cppfmu::Logger&>(logger_).Log(statusValue, categoryStr, msgStr);
138
- }
290
+ if (PyObject_IsTrue(debugAttr.get()) == 1) {
291
+ logger.DebugLog(statusValue, categoryStr, msgStr);
292
+ } else {
293
+ logger.Log(statusValue, categoryStr, msgStr);
139
294
  }
140
- PyList_SetSlice(pMessages_, 0, size, nullptr);
295
+ PyErr_Clear();
141
296
  }
142
- Py_DECREF(debugField);
143
- Py_DECREF(msgField);
144
- Py_DECREF(categoryField);
145
- Py_DECREF(statusField);
297
+ PyList_SetSlice(pMessages_, 0, size, nullptr);
298
+
299
+ PyErr_Restore(errType, errValue, errTraceback);
146
300
  }
147
301
 
148
- void PySlaveInstance::initialize(PyGILState_STATE gilState)
302
+ void PySlaveInstance::initialize()
149
303
  {
150
- Py_XDECREF(pInstance_);
151
- Py_XDECREF(pMessages_);
304
+ clearLogBuffer();
305
+ Py_CLEAR(pInstance_);
306
+ Py_CLEAR(pMessages_);
152
307
 
153
- PyObject* args = PyTuple_New(0);
154
- PyObject* kwargs = Py_BuildValue("{ss,ss,sn,si}",
308
+ PyRef args(PyTuple_New(0));
309
+ PyRef kwargs(Py_BuildValue("{ss,ss,sn,si}",
155
310
  "instance_name", instanceName_.c_str(),
156
311
  "resources", resources_.c_str(),
157
- "logger", &logger_,
158
- "visible", visible_);
159
- pInstance_ = PyObject_Call(pClass_, args, kwargs);
160
- Py_DECREF(args);
161
- Py_DECREF(kwargs);
162
- if (pInstance_ == nullptr) {
163
- handle_py_exception("[initialize] PyObject_Call", gilState);
164
- }
312
+ "logger", reinterpret_cast<Py_ssize_t>(&logger_),
313
+ "visible", static_cast<int>(visible_)));
314
+ if (!args || !kwargs) handle_py_exception("[initialize] Py_BuildValue");
315
+
316
+ pInstance_ = PyObject_Call(pClass_, args.get(), kwargs.get());
317
+ if (pInstance_ == nullptr) handle_py_exception("[initialize] PyObject_Call");
318
+
165
319
  pMessages_ = PyObject_CallMethod(pInstance_, "_get_log_queue", nullptr);
320
+ if (pMessages_ == nullptr) handle_py_exception("[initialize] _get_log_queue");
321
+ clearLogBuffer();
322
+ }
323
+
324
+ void PySlaveInstance::callVoidMethod(const char* method) const
325
+ {
326
+ PyRef result(PyObject_CallMethod(pInstance_, method, nullptr));
327
+ if (!result) handle_py_exception(std::string("[") + method + "]");
328
+ clearLogBuffer();
329
+ }
330
+
331
+ void PySlaveInstance::callSetter(const char* method, const cppfmu::FMIValueReference* vr, std::size_t nvr, PyObject* valuesList)
332
+ {
333
+ PyRef values(valuesList);
334
+ if (!values) handle_py_exception(std::string("[") + method + "] building values");
335
+ PyRef vrs(makeVrList(vr, nvr));
336
+ if (!vrs) handle_py_exception(std::string("[") + method + "] building value references");
337
+
338
+ PyRef result(PyObject_CallMethod(pInstance_, method, "(OO)", vrs.get(), values.get()));
339
+ if (!result) handle_py_exception(std::string("[") + method + "]");
340
+ clearLogBuffer();
341
+ }
342
+
343
+ PyObject* PySlaveInstance::callGetter(const char* method, const cppfmu::FMIValueReference* vr, std::size_t nvr, std::size_t nValues) const
344
+ {
345
+ PyRef vrs(makeVrList(vr, nvr));
346
+ if (!vrs) handle_py_exception(std::string("[") + method + "] building value references");
347
+
348
+ PyRef refs(PyObject_CallMethod(pInstance_, method, "(O)", vrs.get()));
349
+ if (!refs) handle_py_exception(std::string("[") + method + "]");
350
+ checkListSize(refs.get(), nValues, method);
351
+ return refs.release();
352
+ }
353
+
354
+ PyObject* PySlaveInstance::callListMethod(const char* method, std::size_t expected) const
355
+ {
356
+ PyRef refs(PyObject_CallMethod(pInstance_, method, nullptr));
357
+ if (!refs) handle_py_exception(std::string("[") + method + "]");
358
+ checkListSize(refs.get(), expected, method);
359
+ return refs.release();
360
+ }
361
+
362
+ void PySlaveInstance::checkListSize(PyObject* list, std::size_t expected, const char* method) const
363
+ {
364
+ if (!PyList_Check(list)) {
365
+ clearLogBuffer();
366
+ throw std::runtime_error(std::string("[") + method + "] must return a list");
367
+ }
368
+ const auto actual = static_cast<std::size_t>(PyList_Size(list));
369
+ if (actual < expected) {
370
+ clearLogBuffer();
371
+ std::ostringstream oss;
372
+ oss << "[" << method << "] returned " << actual << " values, but " << expected << " were requested";
373
+ throw std::runtime_error(oss.str());
374
+ }
166
375
  }
167
376
 
168
377
  void PySlaveInstance::SetupExperiment(cppfmu::FMIBoolean, cppfmu::FMIFloat64, cppfmu::FMIFloat64 startTime, cppfmu::FMIBoolean, cppfmu::FMIFloat64)
169
378
  {
170
- py_safe_run([this, startTime](PyGILState_STATE gilState) {
171
- auto f = PyObject_CallMethod(pInstance_, "setup_experiment", "(d)", startTime);
172
- if (f == nullptr) {
173
- handle_py_exception("[setupExperiment] PyObject_CallMethod", gilState);
174
- }
175
- Py_DECREF(f);
379
+ py_safe_run([this, startTime]() {
380
+ PyRef result(PyObject_CallMethod(pInstance_, "setup_experiment", "(d)", startTime));
381
+ if (!result) handle_py_exception("[setup_experiment]");
176
382
  clearLogBuffer();
177
383
  });
178
384
  }
179
385
 
180
386
  void PySlaveInstance::EnterInitializationMode()
181
387
  {
182
- py_safe_run([this](PyGILState_STATE gilState) {
183
- auto f = PyObject_CallMethod(pInstance_, "enter_initialization_mode", nullptr);
184
- if (f == nullptr) {
185
- handle_py_exception("[enterInitializationMode] PyObject_CallMethod", gilState);
186
- }
187
- Py_DECREF(f);
188
- clearLogBuffer();
189
- });
388
+ py_safe_run([this]() { callVoidMethod("enter_initialization_mode"); });
190
389
  }
191
390
 
192
391
  void PySlaveInstance::ExitInitializationMode()
193
392
  {
194
- py_safe_run([this](PyGILState_STATE gilState) {
195
- auto f = PyObject_CallMethod(pInstance_, "exit_initialization_mode", nullptr);
196
- if (f == nullptr) {
197
- handle_py_exception("[exitInitializationMode] PyObject_CallMethod", gilState);
198
- }
199
- Py_DECREF(f);
200
- clearLogBuffer();
201
- });
393
+ py_safe_run([this]() { callVoidMethod("exit_initialization_mode"); });
202
394
  }
203
395
 
204
396
  cppfmu::FMIStatus PySlaveInstance::DoStep(cppfmu::FMIFloat64 currentTime,
205
397
  cppfmu::FMIFloat64 stepSize,
206
- cppfmu::FMIBoolean noSetFmuStatePriorToCurrentPoint,
398
+ cppfmu::FMIBoolean,
207
399
  cppfmu::FMIBoolean* eventHandlingNeeded,
208
400
  cppfmu::FMIBoolean* terminateSimulation,
209
401
  cppfmu::FMIBoolean* earlyReturn,
210
402
  cppfmu::FMIFloat64& endOfStep)
211
403
  {
212
- cppfmu::FMIStatus fmuStatus = cppfmu::FMIOK;
213
- py_safe_run([this, &fmuStatus, currentTime, stepSize, terminateSimulation](PyGILState_STATE gilState) {
214
- auto f = PyObject_CallMethod(pInstance_, "do_step", "(dd)", currentTime, stepSize);
215
- if (f == nullptr) {
216
- handle_py_exception("[doStep] PyObject_CallMethod", gilState);
217
- }
404
+ // The FMI standard requires these outputs to always be written.
405
+ if (eventHandlingNeeded) *eventHandlingNeeded = false;
406
+ if (terminateSimulation) *terminateSimulation = false;
407
+ if (earlyReturn) *earlyReturn = false;
408
+
409
+ return py_safe_run([&]() {
410
+ cppfmu::FMIStatus fmuStatus = cppfmu::FMIOK;
411
+ PyRef result(PyObject_CallMethod(pInstance_, "do_step", "(dd)", currentTime, stepSize));
412
+ if (!result) handle_py_exception("[do_step]");
413
+ PyObject* f = result.get();
218
414
 
219
415
  if (PyObject_HasAttrString(f, "status")) {
220
- PyObject* pyStatus = PyObject_GetAttrString(f, "status");
221
- if (pyStatus) {
222
- fmuStatus = static_cast<cppfmu::FMIStatus>(PyLong_AsLong(pyStatus));
223
- Py_DECREF(pyStatus);
224
- }
416
+ PyRef pyStatus(PyObject_GetAttrString(f, "status"));
417
+ if (!pyStatus) handle_py_exception("[do_step] status");
418
+ fmuStatus = static_cast<cppfmu::FMIStatus>(PyLong_AsLong(pyStatus.get()));
419
+ if (PyErr_Occurred()) handle_py_exception("[do_step] status conversion");
225
420
  } else {
226
- bool status = static_cast<bool>(PyObject_IsTrue(f));
227
- if (!status) {
228
- fmuStatus = cppfmu::FMIDiscard;
229
- }
421
+ const int ok = PyObject_IsTrue(f);
422
+ if (ok < 0) handle_py_exception("[do_step] return value");
423
+ if (!ok) fmuStatus = cppfmu::FMIDiscard;
230
424
  }
231
425
 
232
- if (PyObject_HasAttrString(f, "terminateSimulation")) {
233
- PyObject* pyTerminateSimulation = PyObject_GetAttrString(f, "terminateSimulation");
234
- if (pyTerminateSimulation) {
235
- *terminateSimulation = static_cast<bool>(PyObject_IsTrue(pyTerminateSimulation));
236
- Py_DECREF(pyTerminateSimulation);
237
- }
238
- }
239
- Py_DECREF(f);
426
+ const int ehn = readBoolAttr(f, "eventHandlingNeeded", false);
427
+ const int term = readBoolAttr(f, "terminateSimulation", false);
428
+ const int early = readBoolAttr(f, "earlyReturn", false);
429
+ if (ehn < 0 || term < 0 || early < 0) handle_py_exception("[do_step] result");
430
+
431
+ if (eventHandlingNeeded) *eventHandlingNeeded = ehn == 1;
432
+ if (terminateSimulation) *terminateSimulation = term == 1;
433
+ if (earlyReturn) *earlyReturn = early == 1;
434
+ if (fmuStatus == cppfmu::FMIOK) endOfStep = currentTime + stepSize;
435
+
240
436
  clearLogBuffer();
437
+ return fmuStatus;
241
438
  });
242
-
243
- return fmuStatus;
244
439
  }
245
440
 
246
441
  void PySlaveInstance::Reset()
247
442
  {
248
- py_safe_run([this](PyGILState_STATE gilState) {
249
- initialize(gilState);
250
- });
443
+ py_safe_run([this]() { initialize(); });
251
444
  }
252
445
 
253
446
  void PySlaveInstance::Terminate()
254
447
  {
255
- py_safe_run([this](PyGILState_STATE gilState) {
256
- auto f = PyObject_CallMethod(pInstance_, "terminate", nullptr);
257
- if (f == nullptr) {
258
- handle_py_exception("[terminate] PyObject_CallMethod", gilState);
259
- }
260
- Py_DECREF(f);
261
- clearLogBuffer();
262
- });
448
+ py_safe_run([this]() { callVoidMethod("terminate"); });
263
449
  }
264
450
 
265
451
  void PySlaveInstance::SetFloat64(const cppfmu::FMIValueReference* vr, std::size_t nvr, const cppfmu::FMIFloat64* values, std::size_t nValues)
266
452
  {
267
- py_safe_run([this, &vr, nvr, &values, nValues](PyGILState_STATE gilState) {
268
- PyObject* vrs = PyList_New(nvr);
269
- PyObject* refs = PyList_New(nValues);
270
- for (int i = 0; i < nvr; i++) {
271
- PyList_SetItem(vrs, i, Py_BuildValue("i", vr[i]));
272
- }
273
- for (int i = 0; i < nValues; i++) {
274
- PyList_SetItem(refs, i, Py_BuildValue("d", values[i]));
275
- }
276
-
277
- auto f = PyObject_CallMethod(pInstance_, "set_float64", "(OO)", vrs, refs);
278
- Py_DECREF(vrs);
279
- Py_DECREF(refs);
280
- if (f == nullptr) {
281
- handle_py_exception("[setFloat64] PyObject_CallMethod", gilState);
282
- }
283
- Py_DECREF(f);
284
- clearLogBuffer();
453
+ py_safe_run([&]() {
454
+ callSetter("set_float64", vr, nvr, makeValueList(values, nValues, [](cppfmu::FMIFloat64 v) { return PyFloat_FromDouble(v); }));
285
455
  });
286
456
  }
287
457
 
288
458
  void PySlaveInstance::SetInt32(const cppfmu::FMIValueReference* vr, std::size_t nvr, const cppfmu::FMIInt32* values, std::size_t nValues)
289
459
  {
290
- py_safe_run([this, &vr, nvr, &values, nValues](PyGILState_STATE gilState) {
291
- PyObject* vrs = PyList_New(nvr);
292
- PyObject* refs = PyList_New(nValues);
293
- for (int i = 0; i < nvr; i++) {
294
- PyList_SetItem(vrs, i, Py_BuildValue("i", vr[i]));
295
- }
296
-
297
- for (int i = 0; i < nValues; i++) {
298
- PyList_SetItem(refs, i, Py_BuildValue("i", values[i]));
299
- }
300
-
301
- auto f = PyObject_CallMethod(pInstance_, "set_int32", "(OO)", vrs, refs);
302
- Py_DECREF(vrs);
303
- Py_DECREF(refs);
304
- if (f == nullptr) {
305
- handle_py_exception("[setInt32] PyObject_CallMethod", gilState);
306
- }
307
- Py_DECREF(f);
308
- clearLogBuffer();
460
+ py_safe_run([&]() {
461
+ callSetter("set_int32", vr, nvr, makeValueList(values, nValues, [](cppfmu::FMIInt32 v) { return PyLong_FromLong(v); }));
309
462
  });
310
463
  }
311
464
 
312
465
  void PySlaveInstance::SetInt64(const cppfmu::FMIValueReference* vr, std::size_t nvr, const cppfmu::FMIInt64* values, std::size_t nValues)
313
466
  {
314
- py_safe_run([this, &vr, nvr, &values, nValues](PyGILState_STATE gilState) {
315
- PyObject* vrs = PyList_New(nvr);
316
- PyObject* refs = PyList_New(nValues);
317
- for (int i = 0; i < nvr; i++) {
318
- PyList_SetItem(vrs, i, Py_BuildValue("i", vr[i]));
319
- }
320
- for (int i = 0; i < nValues; i++) {
321
- PyList_SetItem(refs, i, Py_BuildValue("L", values[i]));
322
- }
323
-
324
- auto f = PyObject_CallMethod(pInstance_, "set_int64", "(OO)", vrs, refs);
325
- Py_DECREF(vrs);
326
- Py_DECREF(refs);
327
- if (f == nullptr) {
328
- handle_py_exception("[setInt64] PyObject_CallMethod", gilState);
329
- }
330
- Py_DECREF(f);
331
- clearLogBuffer();
467
+ py_safe_run([&]() {
468
+ callSetter("set_int64", vr, nvr, makeValueList(values, nValues, [](cppfmu::FMIInt64 v) { return PyLong_FromLongLong(v); }));
332
469
  });
333
470
  }
334
471
 
335
472
  void PySlaveInstance::SetUInt64(const cppfmu::FMIValueReference* vr, std::size_t nvr, const cppfmu::FMIUInt64* values, std::size_t nValues)
336
473
  {
337
- py_safe_run([this, &vr, nvr, &values, nValues](PyGILState_STATE gilState) {
338
- PyObject* vrs = PyList_New(nvr);
339
- PyObject* refs = PyList_New(nValues);
340
- for (int i = 0; i < nvr; i++) {
341
- PyList_SetItem(vrs, i, Py_BuildValue("i", vr[i]));
342
- }
343
- for (int i = 0; i < nValues; i++) {
344
- PyList_SetItem(refs, i, Py_BuildValue("K", values[i]));
345
- }
346
-
347
- auto f = PyObject_CallMethod(pInstance_, "set_uint64", "(OO)", vrs, refs);
348
- Py_DECREF(vrs);
349
- Py_DECREF(refs);
350
- if (f == nullptr) {
351
- handle_py_exception("[setUInt64] PyObject_CallMethod", gilState);
352
- }
353
- Py_DECREF(f);
354
- clearLogBuffer();
474
+ py_safe_run([&]() {
475
+ callSetter("set_uint64", vr, nvr, makeValueList(values, nValues, [](cppfmu::FMIUInt64 v) { return PyLong_FromUnsignedLongLong(v); }));
355
476
  });
356
477
  }
357
478
 
358
479
  void PySlaveInstance::SetBoolean(const cppfmu::FMIValueReference* vr, std::size_t nvr, const cppfmu::FMIBoolean* values, std::size_t nValues)
359
480
  {
360
- py_safe_run([this, &vr, nvr, &values, nValues](PyGILState_STATE gilState) {
361
- PyObject* vrs = PyList_New(nvr);
362
- PyObject* refs = PyList_New(nValues);
363
- for (int i = 0; i < nvr; i++) {
364
- PyList_SetItem(vrs, i, Py_BuildValue("i", vr[i]));
365
- }
366
- for (int i = 0; i < nValues; i++) {
367
- PyList_SetItem(refs, i, PyBool_FromLong(values[i]));
368
- }
369
-
370
- auto f = PyObject_CallMethod(pInstance_, "set_boolean", "(OO)", vrs, refs);
371
- Py_DECREF(vrs);
372
- Py_DECREF(refs);
373
- if (f == nullptr) {
374
- handle_py_exception("[setBoolean] PyObject_CallMethod", gilState);
375
- }
376
- Py_DECREF(f);
377
- clearLogBuffer();
481
+ py_safe_run([&]() {
482
+ callSetter("set_boolean", vr, nvr, makeValueList(values, nValues, [](cppfmu::FMIBoolean v) { return PyBool_FromLong(v ? 1 : 0); }));
378
483
  });
379
484
  }
380
485
 
381
486
  void PySlaveInstance::SetString(const cppfmu::FMIValueReference* vr, std::size_t nvr, cppfmu::FMIString const* values, std::size_t nValues)
382
487
  {
383
- py_safe_run([this, &vr, nvr, &values, nValues](PyGILState_STATE gilState) {
384
- PyObject* vrs = PyList_New(nvr);
385
- PyObject* refs = PyList_New(nvr);
386
- for (int i = 0; i < nvr; i++) {
387
- PyList_SetItem(vrs, i, Py_BuildValue("i", vr[i]));
388
- }
389
- for (int i = 0; i < nValues; i++) {
390
- PyList_SetItem(refs, i, Py_BuildValue("s", values[i]));
391
- }
392
-
393
- auto f = PyObject_CallMethod(pInstance_, "set_string", "(OO)", vrs, refs);
394
- Py_DECREF(vrs);
395
- Py_DECREF(refs);
396
- if (f == nullptr) {
397
- handle_py_exception("[setString] PyObject_CallMethod", gilState);
398
- }
399
- Py_DECREF(f);
400
- clearLogBuffer();
488
+ py_safe_run([&]() {
489
+ callSetter("set_string", vr, nvr, makeValueList(values, nValues, makeString));
401
490
  });
402
491
  }
403
492
 
404
493
  void PySlaveInstance::GetFloat64(const cppfmu::FMIValueReference* vr, std::size_t nvr, cppfmu::FMIFloat64* values, std::size_t nValues) const
405
494
  {
406
- py_safe_run([this, &vr, nvr, &values, nValues](PyGILState_STATE gilState) {
407
- PyObject* vrs = PyList_New(nvr);
408
- for (int i = 0; i < nvr; i++) {
409
- PyList_SetItem(vrs, i, Py_BuildValue("i", vr[i]));
410
- }
411
-
412
- auto refs = PyObject_CallMethod(pInstance_, "get_float64", "O", vrs);
413
- Py_DECREF(vrs);
414
- if (refs == nullptr) {
415
- handle_py_exception("[getFloat64] PyObject_CallMethod", gilState);
495
+ py_safe_run([&]() {
496
+ PyRef refs(callGetter("get_float64", vr, nvr, nValues));
497
+ for (std::size_t i = 0; i < nValues; i++) {
498
+ values[i] = PyFloat_AsDouble(PyList_GetItem(refs.get(), i));
416
499
  }
417
-
418
- for (int i = 0; i < nValues; i++) {
419
- PyObject* value = PyList_GetItem(refs, i);
420
- values[i] = PyFloat_AsDouble(value);
421
- }
422
- Py_DECREF(refs);
500
+ if (PyErr_Occurred()) handle_py_exception("[get_float64] conversion");
423
501
  clearLogBuffer();
424
502
  });
425
503
  }
426
504
 
427
505
  void PySlaveInstance::GetInt32(const cppfmu::FMIValueReference* vr, std::size_t nvr, cppfmu::FMIInt32* values, std::size_t nValues) const
428
506
  {
429
- py_safe_run([this, &vr, nvr, &values, nValues](PyGILState_STATE gilState) {
430
- PyObject* vrs = PyList_New(nvr);
431
- for (int i = 0; i < nvr; i++) {
432
- PyList_SetItem(vrs, i, Py_BuildValue("i", vr[i]));
507
+ py_safe_run([&]() {
508
+ PyRef refs(callGetter("get_int32", vr, nvr, nValues));
509
+ for (std::size_t i = 0; i < nValues; i++) {
510
+ values[i] = static_cast<cppfmu::FMIInt32>(PyLong_AsLong(PyList_GetItem(refs.get(), i)));
433
511
  }
434
- auto refs = PyObject_CallMethod(pInstance_, "get_int32", "O", vrs);
435
- Py_DECREF(vrs);
436
- if (refs == nullptr) {
437
- handle_py_exception("[getInt32] PyObject_CallMethod", gilState);
438
- }
439
-
440
- for (int i = 0; i < nValues; i++) {
441
- PyObject* value = PyList_GetItem(refs, i);
442
- values[i] = static_cast<cppfmu::FMIInt32>(PyLong_AsLong(value));
443
- }
444
- Py_DECREF(refs);
512
+ if (PyErr_Occurred()) handle_py_exception("[get_int32] conversion");
445
513
  clearLogBuffer();
446
514
  });
447
515
  }
448
516
 
449
517
  void PySlaveInstance::GetInt64(const cppfmu::FMIValueReference* vr, std::size_t nvr, cppfmu::FMIInt64* values, std::size_t nValues) const
450
518
  {
451
- py_safe_run([this, &vr, nvr, &values, nValues](PyGILState_STATE gilState) {
452
- PyObject* vrs = PyList_New(nvr);
453
- for (int i = 0; i < nvr; i++) {
454
- PyList_SetItem(vrs, i, Py_BuildValue("i", vr[i]));
455
- }
456
- auto refs = PyObject_CallMethod(pInstance_, "get_int64", "O", vrs);
457
- Py_DECREF(vrs);
458
- if (refs == nullptr) {
459
- handle_py_exception("[getInt64] PyObject_CallMethod", gilState);
519
+ py_safe_run([&]() {
520
+ PyRef refs(callGetter("get_int64", vr, nvr, nValues));
521
+ for (std::size_t i = 0; i < nValues; i++) {
522
+ values[i] = static_cast<cppfmu::FMIInt64>(PyLong_AsLongLong(PyList_GetItem(refs.get(), i)));
460
523
  }
461
-
462
- for (int i = 0; i < nValues; i++) {
463
- PyObject* value = PyList_GetItem(refs, i);
464
- values[i] = static_cast<cppfmu::FMIInt64>(PyLong_AsLongLong(value));
465
- }
466
- Py_DECREF(refs);
524
+ if (PyErr_Occurred()) handle_py_exception("[get_int64] conversion");
467
525
  clearLogBuffer();
468
526
  });
469
527
  }
470
528
 
471
529
  void PySlaveInstance::GetUInt64(const cppfmu::FMIValueReference* vr, std::size_t nvr, cppfmu::FMIUInt64* values, std::size_t nValues) const
472
530
  {
473
- py_safe_run([this, &vr, nvr, &values, nValues](PyGILState_STATE gilState) {
474
- PyObject* vrs = PyList_New(nvr);
475
- for (int i = 0; i < nvr; i++) {
476
- PyList_SetItem(vrs, i, Py_BuildValue("i", vr[i]));
477
- }
478
- auto refs = PyObject_CallMethod(pInstance_, "get_uint64", "O", vrs);
479
- Py_DECREF(vrs);
480
- if (refs == nullptr) {
481
- handle_py_exception("[getUInt64] PyObject_CallMethod", gilState);
482
- }
483
-
484
- for (int i = 0; i < nValues; i++) {
485
- PyObject* item = PyList_GetItem(refs, i);
486
- PyObject* value = PyObject_GetAttrString(item, "value");
487
- if (value && PyLong_Check(value))
488
- {
489
- values[i] = static_cast<cppfmu::FMIUInt64>(PyLong_AsUnsignedLongLong(value));
531
+ py_safe_run([&]() {
532
+ PyRef refs(callGetter("get_uint64", vr, nvr, nValues));
533
+ for (std::size_t i = 0; i < nValues; i++) {
534
+ PyObject* item = PyList_GetItem(refs.get(), i);
535
+ if (PyLong_Check(item)) {
536
+ values[i] = static_cast<cppfmu::FMIUInt64>(PyLong_AsUnsignedLongLong(item));
537
+ } else {
538
+ // ctypes.c_uint64 (as returned by Fmi3SlaveBase.get_uint64)
539
+ PyRef value(PyObject_GetAttrString(item, "value"));
540
+ if (!value) handle_py_exception("[get_uint64] value attribute");
541
+ values[i] = static_cast<cppfmu::FMIUInt64>(PyLong_AsUnsignedLongLong(value.get()));
490
542
  }
491
543
  }
492
-
493
- Py_DECREF(refs);
544
+ if (PyErr_Occurred()) handle_py_exception("[get_uint64] conversion");
494
545
  clearLogBuffer();
495
546
  });
496
547
  }
497
548
 
498
549
  void PySlaveInstance::GetBoolean(const cppfmu::FMIValueReference* vr, std::size_t nvr, cppfmu::FMIBoolean* values, std::size_t nValues) const
499
550
  {
500
- py_safe_run([this, &vr, nvr, &values, nValues](PyGILState_STATE gilState) {
501
- PyObject* vrs = PyList_New(nvr);
502
- for (int i = 0; i < nvr; i++) {
503
- PyList_SetItem(vrs, i, Py_BuildValue("i", vr[i]));
504
- }
505
- auto refs = PyObject_CallMethod(pInstance_, "get_boolean", "O", vrs);
506
- Py_DECREF(vrs);
507
- if (refs == nullptr) {
508
- handle_py_exception("[getBoolean] PyObject_CallMethod", gilState);
551
+ py_safe_run([&]() {
552
+ PyRef refs(callGetter("get_boolean", vr, nvr, nValues));
553
+ for (std::size_t i = 0; i < nValues; i++) {
554
+ const int v = PyObject_IsTrue(PyList_GetItem(refs.get(), i));
555
+ if (v < 0) handle_py_exception("[get_boolean] conversion");
556
+ values[i] = v == 1;
509
557
  }
510
-
511
- for (int i = 0; i < nValues; i++) {
512
- PyObject* value = PyList_GetItem(refs, i);
513
- values[i] = PyObject_IsTrue(value);
514
- }
515
- Py_DECREF(refs);
516
558
  clearLogBuffer();
517
559
  });
518
560
  }
519
561
 
520
562
  void PySlaveInstance::GetString(const cppfmu::FMIValueReference* vr, std::size_t nvr, cppfmu::FMIString* values, std::size_t nValues) const
521
563
  {
522
- py_safe_run([this, &vr, nvr, &values, nValues](PyGILState_STATE gilState) {
564
+ py_safe_run([&]() {
565
+ // Strings returned to the importer stay valid until the next GetString call.
523
566
  clearStrBuffer();
524
- PyObject* vrs = PyList_New(nvr);
525
- for (int i = 0; i < nvr; i++) {
526
- PyList_SetItem(vrs, i, Py_BuildValue("i", vr[i]));
527
- }
528
- auto refs = PyObject_CallMethod(pInstance_, "get_string", "O", vrs);
529
- Py_DECREF(vrs);
530
- if (refs == nullptr) {
531
- handle_py_exception("[getString] PyObject_CallMethod", gilState);
532
- }
533
-
534
- for (int i = 0; i < nValues; i++) {
535
- PyObject* value = PyUnicode_AsEncodedString(PyList_GetItem(refs, i), "utf-8", nullptr);
567
+ PyRef refs(callGetter("get_string", vr, nvr, nValues));
568
+ for (std::size_t i = 0; i < nValues; i++) {
569
+ PyObject* value = PyUnicode_AsEncodedString(PyList_GetItem(refs.get(), i), "utf-8", nullptr);
570
+ if (value == nullptr) handle_py_exception("[get_string] conversion");
536
571
  values[i] = PyBytes_AsString(value);
537
572
  strBuffer.emplace_back(value);
538
573
  }
539
- Py_DECREF(refs);
540
574
  clearLogBuffer();
541
575
  });
542
576
  }
543
577
 
544
578
  void PySlaveInstance::GetContinuousStates(cppfmu::FMIFloat64* continuousStates, std::size_t nStates) const
545
579
  {
546
- py_safe_run([this, &continuousStates, nStates](PyGILState_STATE gilState) {
547
- auto f = PyObject_CallMethod(pInstance_, "get_continuous_states", nullptr);
548
- if (f == nullptr) {
549
- handle_py_exception("[get_continuous_states] PyObject_CallMethod", gilState);
550
- }
551
- // Assuming f is a list of floats
580
+ py_safe_run([&]() {
581
+ PyRef refs(callListMethod("get_continuous_states", nStates));
552
582
  for (std::size_t i = 0; i < nStates; i++) {
553
- PyObject* item = PyList_GetItem(f, i);
554
- continuousStates[i] = static_cast<cppfmu::FMIFloat64>(PyFloat_AsDouble(item));
583
+ continuousStates[i] = PyFloat_AsDouble(PyList_GetItem(refs.get(), i));
555
584
  }
556
- Py_DECREF(f);
585
+ if (PyErr_Occurred()) handle_py_exception("[get_continuous_states] conversion");
557
586
  clearLogBuffer();
558
587
  });
559
588
  }
560
589
 
561
590
  void PySlaveInstance::GetContinuousStateDerivatives(cppfmu::FMIFloat64* continuousStateDerivatives, std::size_t nStates) const
562
591
  {
563
- py_safe_run([this, &continuousStateDerivatives, nStates](PyGILState_STATE gilState) {
564
- auto f = PyObject_CallMethod(pInstance_, "get_continuous_state_derivatives", nullptr);
565
- if (f == nullptr) {
566
- PyErr_Print();
567
- handle_py_exception("[get_continuous_state_derivatives] PyObject_CallMethod", gilState);
568
- }
569
- // Assuming f is a list of floats
592
+ py_safe_run([&]() {
593
+ PyRef refs(callListMethod("get_continuous_state_derivatives", nStates));
570
594
  for (std::size_t i = 0; i < nStates; i++) {
571
- PyObject* item = PyList_GetItem(f, i);
572
- continuousStateDerivatives[i] = static_cast<cppfmu::FMIFloat64>(PyFloat_AsDouble(item));
595
+ continuousStateDerivatives[i] = PyFloat_AsDouble(PyList_GetItem(refs.get(), i));
573
596
  }
574
- Py_DECREF(f);
597
+ if (PyErr_Occurred()) handle_py_exception("[get_continuous_state_derivatives] conversion");
575
598
  clearLogBuffer();
576
599
  });
577
600
  }
578
601
 
579
602
  void PySlaveInstance::GetNominalsOfContinuousStates(cppfmu::FMIFloat64* nominalsOfContinuousStates, std::size_t nStates) const
580
603
  {
581
- py_safe_run([this, &nominalsOfContinuousStates, nStates](PyGILState_STATE gilState) {
582
- auto f = PyObject_CallMethod(pInstance_, "get_nominals_of_continuous_states", "(i)", static_cast<int>(nStates));
583
- // Check if f is a valid object
584
- if (f == nullptr) {
585
- handle_py_exception("[getNominalContinuousStates] PyObject_CallMethod", gilState);
586
- }
587
- // Assuming f is a list of floats
604
+ py_safe_run([&]() {
605
+ PyRef refs(PyObject_CallMethod(pInstance_, "get_nominals_of_continuous_states", "(n)", static_cast<Py_ssize_t>(nStates)));
606
+ if (!refs) handle_py_exception("[get_nominals_of_continuous_states]");
607
+ checkListSize(refs.get(), nStates, "get_nominals_of_continuous_states");
588
608
  for (std::size_t i = 0; i < nStates; i++) {
589
- PyObject* item = PyList_GetItem(f, i);
590
- nominalsOfContinuousStates[i] = static_cast<cppfmu::FMIFloat64>(PyFloat_AsDouble(item));
609
+ nominalsOfContinuousStates[i] = PyFloat_AsDouble(PyList_GetItem(refs.get(), i));
591
610
  }
592
- Py_DECREF(f);
611
+ if (PyErr_Occurred()) handle_py_exception("[get_nominals_of_continuous_states] conversion");
593
612
  clearLogBuffer();
594
613
  });
595
614
  }
596
615
 
597
616
  void PySlaveInstance::SetContinuousStates(const cppfmu::FMIFloat64* continuousStates, std::size_t nStates)
598
617
  {
599
- py_safe_run([this, &continuousStates, nStates](PyGILState_STATE gilState) {
600
- PyObject* refs = PyList_New(nStates);
601
- for (int i = 0; i < nStates; i++) {
602
- PyList_SetItem(refs, i, Py_BuildValue("d", continuousStates[i]));
603
- }
604
-
605
- auto f = PyObject_CallMethod(pInstance_, "set_continuous_states", "(O)", refs);
606
- Py_DECREF(refs);
607
- if (f == nullptr) {
608
- handle_py_exception("[setContinuousStates] PyObject_CallMethod", gilState);
609
- }
610
- Py_DECREF(f);
618
+ py_safe_run([&]() {
619
+ PyRef refs(makeValueList(continuousStates, nStates, [](cppfmu::FMIFloat64 v) { return PyFloat_FromDouble(v); }));
620
+ if (!refs) handle_py_exception("[set_continuous_states] building values");
621
+ PyRef result(PyObject_CallMethod(pInstance_, "set_continuous_states", "(O)", refs.get()));
622
+ if (!result) handle_py_exception("[set_continuous_states]");
611
623
  clearLogBuffer();
612
624
  });
613
625
  }
614
626
 
615
627
  void PySlaveInstance::GetNumberOfContinuousStates(std::size_t& nStates) const
616
628
  {
617
- py_safe_run([this, &nStates](PyGILState_STATE gilState) {
618
- auto f = PyObject_CallMethod(pInstance_, "get_number_of_continuous_states", nullptr);
619
- if (f == nullptr) {
620
- handle_py_exception("[getNumberOfContinuousStates] PyObject_CallMethod", gilState);
621
- }
622
- nStates = static_cast<std::size_t>(PyLong_AsLong(f));
623
- Py_DECREF(f);
629
+ py_safe_run([&]() {
630
+ PyRef result(PyObject_CallMethod(pInstance_, "get_number_of_continuous_states", nullptr));
631
+ if (!result) handle_py_exception("[get_number_of_continuous_states]");
632
+ nStates = PyLong_AsSize_t(result.get());
633
+ if (PyErr_Occurred()) handle_py_exception("[get_number_of_continuous_states] conversion");
624
634
  clearLogBuffer();
625
635
  });
626
636
  }
627
637
 
628
638
  void PySlaveInstance::GetNumberOfEventIndicators(std::size_t& nIndicators) const
629
639
  {
630
- py_safe_run([this, &nIndicators](PyGILState_STATE gilState) {
631
- auto f = PyObject_CallMethod(pInstance_, "get_number_of_event_indicators", nullptr);
632
- if (f == nullptr) {
633
- handle_py_exception("[getNumberOfEventIndicators] PyObject_CallMethod", gilState);
634
- }
635
- nIndicators = static_cast<std::size_t>(PyLong_AsLong(f));
636
- Py_DECREF(f);
637
- clearLogBuffer();
640
+ py_safe_run([&]() {
641
+ PyRef result(PyObject_CallMethod(pInstance_, "get_number_of_event_indicators", nullptr));
642
+ if (!result) handle_py_exception("[get_number_of_event_indicators]");
643
+ nIndicators = PyLong_AsSize_t(result.get());
644
+ if (PyErr_Occurred()) handle_py_exception("[get_number_of_event_indicators] conversion");
645
+ clearLogBuffer();
638
646
  });
639
647
  }
640
648
 
641
649
  void PySlaveInstance::GetEventIndicators(cppfmu::FMIFloat64* eventIndicators, std::size_t nIndicators) const
642
650
  {
643
- py_safe_run([this, &eventIndicators, nIndicators](PyGILState_STATE gilState) {
644
- auto f = PyObject_CallMethod(pInstance_, "get_event_indicators", nullptr);
645
- if (f == nullptr) {
646
- handle_py_exception("[getEventIndicators] PyObject_CallMethod", gilState);
647
- }
648
- // Assuming f is a list of floats
651
+ py_safe_run([&]() {
652
+ PyRef refs(callListMethod("get_event_indicators", nIndicators));
649
653
  for (std::size_t i = 0; i < nIndicators; i++) {
650
- PyObject* item = PyList_GetItem(f, i);
651
- eventIndicators[i] = static_cast<cppfmu::FMIFloat64>(PyFloat_AsDouble(item));
654
+ eventIndicators[i] = PyFloat_AsDouble(PyList_GetItem(refs.get(), i));
652
655
  }
653
- Py_DECREF(f);
656
+ if (PyErr_Occurred()) handle_py_exception("[get_event_indicators] conversion");
654
657
  clearLogBuffer();
655
658
  });
656
-
657
659
  }
658
660
 
659
-
660
661
  void PySlaveInstance::SetTime(cppfmu::FMIFloat64 time)
661
662
  {
662
- py_safe_run([this, time](PyGILState_STATE gilState) {
663
- auto f = PyObject_CallMethod(pInstance_, "set_time", "(d)", time);
664
- if (f == nullptr) {
665
- handle_py_exception("[setTime] PyObject_CallMethod", gilState);
666
- }
667
- Py_DECREF(f);
663
+ py_safe_run([this, time]() {
664
+ PyRef result(PyObject_CallMethod(pInstance_, "set_time", "(d)", time));
665
+ if (!result) handle_py_exception("[set_time]");
668
666
  clearLogBuffer();
669
667
  });
670
668
  }
@@ -677,181 +675,181 @@ void PySlaveInstance::UpdateDiscreteStates(
677
675
  cppfmu::FMIBoolean* nextEventTimeDefined,
678
676
  cppfmu::FMIFloat64* nextEventTime)
679
677
  {
680
- py_safe_run([this, discreteStatesNeedUpdate, terminateSimulation, nominalContinuousStatesChanged, valuesOfContinuousStatesChanged, nextEventTimeDefined, nextEventTime](PyGILState_STATE gilState) {
681
- auto f = PyObject_CallMethod(pInstance_, "update_discrete_states", nullptr);
682
- if (f == nullptr) {
683
- handle_py_exception("[updateDiscreteStates] PyObject_CallMethod", gilState);
684
- }
685
-
686
- if (PyObject_HasAttrString(f, "discreteStatesNeedUpdate")) {
687
- PyObject * pyDiscreteStatesNeedUpdate = PyObject_GetAttrString(f, "discreteStatesNeedUpdate");
688
- *discreteStatesNeedUpdate = static_cast<bool>(PyObject_IsTrue(pyDiscreteStatesNeedUpdate));
689
- Py_DECREF(pyDiscreteStatesNeedUpdate);
690
- }
691
- if (PyObject_HasAttrString(f, "terminateSimulation")) {
692
- PyObject* pyTerminateSimulation = PyObject_GetAttrString(f, "terminateSimulation");
693
- *terminateSimulation = static_cast<bool>(PyObject_IsTrue(pyTerminateSimulation));
694
- Py_DECREF(pyTerminateSimulation);
678
+ py_safe_run([&]() {
679
+ PyRef result(PyObject_CallMethod(pInstance_, "update_discrete_states", nullptr));
680
+ if (!result) handle_py_exception("[update_discrete_states]");
681
+ PyObject* f = result.get();
682
+
683
+ struct Field
684
+ {
685
+ const char* name;
686
+ cppfmu::FMIBoolean* out;
687
+ };
688
+ const Field fields[] = {
689
+ {"discreteStatesNeedUpdate", discreteStatesNeedUpdate},
690
+ {"terminateSimulation", terminateSimulation},
691
+ {"nominalContinuousStatesChanged", nominalContinuousStatesChanged},
692
+ {"valuesOfContinuousStatesChanged", valuesOfContinuousStatesChanged},
693
+ {"nextEventTimeDefined", nextEventTimeDefined},
694
+ };
695
+ for (const auto& field : fields) {
696
+ const int v = readBoolAttr(f, field.name, false);
697
+ if (v < 0) handle_py_exception(std::string("[update_discrete_states] ") + field.name);
698
+ if (field.out) *field.out = v == 1;
699
+ }
700
+
701
+ if (nextEventTimeDefined && *nextEventTimeDefined && nextEventTime) {
702
+ PyRef pyNextEventTime(PyObject_GetAttrString(f, "nextEventTime"));
703
+ if (!pyNextEventTime) handle_py_exception("[update_discrete_states] nextEventTime");
704
+ *nextEventTime = PyFloat_AsDouble(pyNextEventTime.get());
705
+ if (PyErr_Occurred()) handle_py_exception("[update_discrete_states] nextEventTime conversion");
695
706
  }
696
- if (PyObject_HasAttrString(f, "nominalContinuousStatesChanged")) {
697
- PyObject* pyNominalContinuousStatesChanged = PyObject_GetAttrString(f, "nominalContinuousStatesChanged");
698
- *nominalContinuousStatesChanged = static_cast<bool>(PyObject_IsTrue(pyNominalContinuousStatesChanged));
699
- Py_DECREF(pyNominalContinuousStatesChanged);
700
- }
701
- if (PyObject_HasAttrString(f, "valuesOfContinuousStatesChanged")) {
702
- PyObject* pyValuesOfContinuousStatesChanged = PyObject_GetAttrString(f, "valuesOfContinuousStatesChanged");
703
- *valuesOfContinuousStatesChanged = static_cast<bool>(PyObject_IsTrue(pyValuesOfContinuousStatesChanged));
704
- Py_DECREF(pyValuesOfContinuousStatesChanged);
705
- }
706
- if (PyObject_HasAttrString(f, "nextEventTimeDefined")) {
707
- PyObject* pyNextEventTimeDefined = PyObject_GetAttrString(f, "nextEventTimeDefined");
708
- *nextEventTimeDefined = static_cast<bool>(PyObject_IsTrue(pyNextEventTimeDefined));
709
- Py_DECREF(pyNextEventTimeDefined);
710
- }
711
- if (nextEventTimeDefined && *nextEventTimeDefined) {
712
- PyObject* pyNextEventTime = PyObject_GetAttrString(f, "nextEventTime");
713
- *nextEventTime = PyFloat_AsDouble(pyNextEventTime);
714
- Py_DECREF(pyNextEventTime);
715
- }
716
-
717
- Py_DECREF(f);
718
707
  clearLogBuffer();
719
708
  });
720
709
  }
721
710
 
722
711
  void PySlaveInstance::GetFMUstate(fmi3FMUState& state)
723
712
  {
724
- py_safe_run([this, &state](PyGILState_STATE gilState) {
725
- auto f = PyObject_CallMethod(pInstance_, "_get_fmu_state", nullptr);
726
- if (f == nullptr) {
727
- handle_py_exception("[_get_fmu_state] PyObject_CallMethod", gilState);
728
- }
729
- state = reinterpret_cast<fmi3FMUState*>(f);
713
+ py_safe_run([&]() {
714
+ PyRef newState(PyObject_CallMethod(pInstance_, "_get_fmu_state", nullptr));
715
+ if (!newState) handle_py_exception("[_get_fmu_state]");
716
+ // FMI 3.0: a non-NULL state was returned by an earlier call and must be overwritten.
717
+ PyObject* previous = reinterpret_cast<PyObject*>(state);
718
+ state = reinterpret_cast<fmi3FMUState>(newState.release());
719
+ Py_XDECREF(previous);
730
720
  clearLogBuffer();
731
721
  });
732
722
  }
733
723
 
734
724
  void PySlaveInstance::SetFMUstate(const fmi3FMUState& state)
735
725
  {
736
- py_safe_run([this, &state](PyGILState_STATE gilState) {
726
+ py_safe_run([&]() {
737
727
  auto pyState = reinterpret_cast<PyObject*>(state);
738
- auto f = PyObject_CallMethod(pInstance_, "_set_fmu_state", "(O)", pyState);
739
- if (f == nullptr) {
740
- handle_py_exception("[_set_fmu_state] PyObject_CallMethod", gilState);
741
- }
728
+ PyRef result(PyObject_CallMethod(pInstance_, "_set_fmu_state", "(O)", pyState));
729
+ if (!result) handle_py_exception("[_set_fmu_state]");
742
730
  clearLogBuffer();
743
731
  });
744
732
  }
745
733
 
746
734
  void PySlaveInstance::FreeFMUstate(fmi3FMUState& state)
747
735
  {
748
- py_safe_run([this, &state](PyGILState_STATE gilState) {
749
- auto f = reinterpret_cast<PyObject*>(state);
750
- Py_XDECREF(f);
736
+ py_safe_run([&]() {
737
+ Py_XDECREF(reinterpret_cast<PyObject*>(state));
738
+ state = nullptr;
751
739
  });
752
740
  }
753
741
 
754
742
  size_t PySlaveInstance::SerializedFMUstateSize(const fmi3FMUState& state)
755
743
  {
756
- size_t size;
757
- py_safe_run([this, &state, &size](PyGILState_STATE gilState) {
744
+ return py_safe_run([&]() {
758
745
  auto pyState = reinterpret_cast<PyObject*>(state);
759
- PyObject* pyStateBytes = PyObject_CallMethod(pClass_, "_fmu_state_to_bytes", "(O)", pyState);
760
- if (pyStateBytes == nullptr) {
761
- handle_py_exception("[SerializedFMUstateSize] PyObject_CallMethod", gilState);
762
- }
763
- size = PyBytes_Size(pyStateBytes);
764
- Py_DECREF(pyStateBytes);
746
+ PyRef bytes(PyObject_CallMethod(pClass_, "_fmu_state_to_bytes", "(O)", pyState));
747
+ if (!bytes) handle_py_exception("[_fmu_state_to_bytes]");
748
+ const Py_ssize_t size = PyBytes_Size(bytes.get());
749
+ if (size < 0) handle_py_exception("[_fmu_state_to_bytes] must return bytes");
765
750
  clearLogBuffer();
751
+ return static_cast<size_t>(size);
766
752
  });
767
- return size;
768
753
  }
769
754
 
770
755
  void PySlaveInstance::SerializeFMUstate(const fmi3FMUState& state, fmi3Byte* bytes, size_t size)
771
756
  {
772
- py_safe_run([this, &state, &bytes, size](PyGILState_STATE gilState) {
757
+ py_safe_run([&]() {
773
758
  auto pyState = reinterpret_cast<PyObject*>(state);
774
- PyObject* pyStateBytes = PyObject_CallMethod(pClass_, "_fmu_state_to_bytes", "(O)", pyState);
775
- if (pyStateBytes == nullptr) {
776
- handle_py_exception("[SerializeFMUstate] PyObject_CallMethod", gilState);
777
- }
778
- char* c = PyBytes_AsString(pyStateBytes);
779
- if (c == nullptr) {
780
- handle_py_exception("[SerializeFMUstate] PyBytes_AsString", gilState);
759
+ PyRef pyBytes(PyObject_CallMethod(pClass_, "_fmu_state_to_bytes", "(O)", pyState));
760
+ if (!pyBytes) handle_py_exception("[_fmu_state_to_bytes]");
761
+ char* data = nullptr;
762
+ Py_ssize_t length = 0;
763
+ if (PyBytes_AsStringAndSize(pyBytes.get(), &data, &length) != 0) {
764
+ handle_py_exception("[_fmu_state_to_bytes] must return bytes");
781
765
  }
782
- for (int i = 0; i < size; i++) {
783
- bytes[i] = c[i];
766
+ if (static_cast<size_t>(length) > size) {
767
+ throw std::runtime_error("[SerializeFMUstate] buffer is smaller than fmi3SerializedFMUStateSize");
784
768
  }
785
- Py_DECREF(pyStateBytes);
769
+ std::copy(data, data + length, bytes);
786
770
  clearLogBuffer();
787
771
  });
788
772
  }
789
773
 
790
774
  void PySlaveInstance::DeSerializeFMUstate(const fmi3Byte bytes[], size_t size, fmi3FMUState& state)
791
775
  {
792
- py_safe_run([this, &bytes, size, &state](PyGILState_STATE gilState) {
793
- char const * castedBytes = reinterpret_cast<char const*>(bytes);
794
- PyObject* pyStateBytes = PyBytes_FromStringAndSize(castedBytes, size);
795
- if (pyStateBytes == nullptr) {
796
- handle_py_exception("[DeSerializeFMUstate] PyBytes_FromStringAndSize", gilState);
797
- }
798
- PyObject* pyState = PyObject_CallMethod(pClass_, "_fmu_state_from_bytes", "(O)", pyStateBytes);
799
- if (pyState == nullptr) {
800
- handle_py_exception("[DeSerializeFMUstate] PyObject_CallMethod", gilState);
801
- }
802
- state = reinterpret_cast<fmi3FMUState*>(pyState);
803
- Py_DECREF(pyStateBytes);
776
+ py_safe_run([&]() {
777
+ PyRef pyBytes(PyBytes_FromStringAndSize(reinterpret_cast<const char*>(bytes), static_cast<Py_ssize_t>(size)));
778
+ if (!pyBytes) handle_py_exception("[DeSerializeFMUstate] PyBytes_FromStringAndSize");
779
+ PyRef pyState(PyObject_CallMethod(pClass_, "_fmu_state_from_bytes", "(O)", pyBytes.get()));
780
+ if (!pyState) handle_py_exception("[_fmu_state_from_bytes]");
781
+ PyObject* previous = reinterpret_cast<PyObject*>(state);
782
+ state = reinterpret_cast<fmi3FMUState>(pyState.release());
783
+ Py_XDECREF(previous);
804
784
  clearLogBuffer();
805
785
  });
806
786
  }
807
787
 
808
- void PySlaveInstance::handle_py_exception(const std::string& what, PyGILState_STATE gilState) const
788
+ void PySlaveInstance::handle_py_exception(const std::string& what) const
809
789
  {
810
- auto err = PyErr_Occurred();
811
- if (err != nullptr) {
812
- cleanPyObject();
813
-
814
- PyObject *pExcType, *pExcValue, *pExcTraceback;
815
- PyErr_Fetch(&pExcType, &pExcValue, &pExcTraceback);
790
+ // Must be called with the GIL held. The GIL is released by the enclosing GilGuard
791
+ // while the exception unwinds; Python objects stay valid so the instance can still
792
+ // be freed (or reset) safely afterwards.
793
+ std::string details = fetchPythonError();
794
+ clearLogBuffer(); // forward messages logged before the failure
816
795
 
817
- std::ostringstream oss;
818
- oss << "Fatal py exception encountered: ";
819
- oss << what << "\n";
820
- if (pExcValue != nullptr) {
821
- PyObject* pRepr = PyObject_Repr(pExcValue);
822
- PyObject* pyStr = PyUnicode_AsEncodedString(pRepr, "utf-8", nullptr);
823
- oss << PyBytes_AsString(pyStr);
824
- Py_DECREF(pyStr);
825
- Py_DECREF(pRepr);
826
- } else {
827
- oss << "unknown error";
828
- }
829
-
830
- PyErr_Clear();
831
-
832
- Py_XDECREF(pExcType);
833
- Py_XDECREF(pExcValue);
834
- Py_XDECREF(pExcTraceback);
835
-
836
- PyGILState_Release(gilState);
837
-
838
- auto msg = oss.str();
839
- throw cppfmu::FatalError(msg.c_str());
840
- }
796
+ std::ostringstream oss;
797
+ oss << "Fatal py exception encountered: " << what << "\n" << details;
798
+ throw cppfmu::FatalError(oss.str().c_str());
841
799
  }
842
800
 
843
801
  PySlaveInstance::~PySlaveInstance()
844
802
  {
845
- py_safe_run([this](PyGILState_STATE gilState) {
846
- cleanPyObject();
847
- });
803
+ py_safe_run([this]() { cleanPyObject(); });
848
804
  }
849
805
 
850
806
  } // namespace pythonfmu
851
807
 
852
808
  namespace {
853
- std::mutex pyStateMutex{};
854
- std::shared_ptr<pythonfmu::PyState> pyState{};
809
+ // Never destroyed (allocated once, no static destructor). On Linux the library's
810
+ // destructor function (onLibraryUnload, run by _dl_fini) resets the state after the C++
811
+ // static destructors have run at exit: a namespace-scope shared_ptr was destroyed by then,
812
+ // and resetting it again decremented a reference count in freed memory, corrupting the
813
+ // heap (glibc then aborted in a later free, e.g. libgfortran's close_units).
814
+ std::mutex& pyStateMutex()
815
+ {
816
+ static auto* const mutex = new std::mutex();
817
+ return *mutex;
818
+ }
819
+
820
+ std::shared_ptr<pythonfmu::PyState>& pyState()
821
+ {
822
+ static auto* const state = new std::shared_ptr<pythonfmu::PyState>();
823
+ return *state;
824
+ }
825
+
826
+ // Turns an FMI 3 resource path (or a legacy file:// URI) into a filesystem path.
827
+ std::string resourcePathFromLocation(const std::string& location)
828
+ {
829
+ std::string path = location;
830
+ const std::string scheme = "file://";
831
+ if (path.compare(0, scheme.size(), scheme) == 0) {
832
+ path.erase(0, scheme.size());
833
+ // file:///C:/... on Windows -> C:/...
834
+ if (path.size() > 2 && path[0] == '/' && path[2] == ':') path.erase(0, 1);
835
+
836
+ // Percent-decode (URIs only; plain FMI 3 paths may legitimately contain '%')
837
+ std::string decoded;
838
+ decoded.reserve(path.size());
839
+ for (std::size_t i = 0; i < path.size(); i++) {
840
+ if (path[i] == '%' && i + 2 < path.size() && std::isxdigit(static_cast<unsigned char>(path[i + 1])) &&
841
+ std::isxdigit(static_cast<unsigned char>(path[i + 2]))) {
842
+ decoded += static_cast<char>(std::stoi(path.substr(i + 1, 2), nullptr, 16));
843
+ i += 2;
844
+ } else {
845
+ decoded += path[i];
846
+ }
847
+ }
848
+ path = decoded;
849
+ }
850
+ while (path.size() > 1 && (path.back() == '/' || path.back() == '\\')) path.pop_back();
851
+ return path;
852
+ }
855
853
  }
856
854
 
857
855
  std::unique_ptr<cppfmu::SlaveInstance> CppfmuInstantiateSlave(
@@ -864,29 +862,19 @@ std::unique_ptr<cppfmu::SlaveInstance> CppfmuInstantiateSlave(
864
862
  cppfmu::FMIBoolean,
865
863
  const cppfmu::Logger& logger)
866
864
  {
867
-
868
- // Convert URI %20 to space
869
- auto resources = std::regex_replace(std::string(fmuResourceLocation), std::regex("%20"), " ");
870
- auto find = resources.find("file://");
871
-
872
- if (find != std::string::npos) {
873
- #ifdef _MSC_VER
874
- resources.replace(find, 8, "");
875
- #else
876
- resources.replace(find, 7, "");
877
- #endif
865
+ if (fmuResourceLocation == nullptr) {
866
+ throw cppfmu::FatalError("No resource path given: PythonFMU3 needs the FMU resources folder");
878
867
  }
868
+ auto resources = resourcePathFromLocation(fmuResourceLocation);
879
869
 
880
- {
881
- auto const ensurePyStateAlive = [&]() {
882
- auto const lock = std::lock_guard{pyStateMutex};
883
- if (nullptr == pyState) pyState = std::make_shared<pythonfmu::PyState>();
884
- };
885
-
886
- ensurePyStateAlive();
887
- return std::make_unique<pythonfmu::PySlaveInstance>(
888
- instanceName, resources, logger, visible, pyState);
889
- }
870
+ auto const ensurePyStateAlive = [&]() {
871
+ auto const lock = std::lock_guard{pyStateMutex()};
872
+ if (nullptr == pyState()) pyState() = std::make_shared<pythonfmu::PyState>();
873
+ return pyState();
874
+ };
875
+
876
+ return std::make_unique<pythonfmu::PySlaveInstance>(
877
+ instanceName, resources, logger, visible, ensurePyStateAlive());
890
878
  }
891
879
 
892
880
  extern "C" {
@@ -897,7 +885,8 @@ extern "C" {
897
885
  // Thus, use DllMain on Windows and __attribute__((destructor)) on Linux for signaling to the PyState about de-initialization.
898
886
  void finalizePythonInterpreter()
899
887
  {
900
- pyState = nullptr;
888
+ auto const lock = std::lock_guard{pyStateMutex()};
889
+ pyState().reset();
901
890
  }
902
891
  }
903
892