From 1c46d965fabfcd909b889e76c37f19d78e3c870e Mon Sep 17 00:00:00 2001 From: Stephen Smith Date: Thu, 12 Nov 2026 19:33:10 +0000 Subject: [PATCH 1/2] Load FMUs from non-Python importers on Linux and macOS The wrapper library was not linked against libpython on Linux/macOS, so it could only be loaded into a process that already had Python (e.g. FMPy). Plain importers (OMSimulator, fmusim, Simulink, ...) failed with "undefined symbol: _Py_NoneStruct". The FMU library is now a small shim with no Python dependency. On the first instantiation it opens libpython with RTLD_GLOBAL if the host does not already provide it, then loads the real implementation (libpythonfmu-backend, built with FMI3_FUNCTION_PREFIX) from the same folder and forwards all FMI calls. libpython is looked up via PYTHONFMU_PYTHON_LIBRARY, then the build interpreter's library recorded in resources/pythonfmu_libpython.txt, then standard names for Python 3.8-3.14. Windows is unchanged (links python3.dll). - generate_shim.py generates the forwarders from fmi3FunctionTypes.h - builder only renames the entry-point library and records the libpython hint - tests: run a built FMU from a plain C host; docs: install.md section Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01X8Wmw797DJpgrMFmT6n8CG --- docs/install.md | 23 + pythonfmu3/builder.py | 48 +- pythonfmu3/pythonfmu-export/generate_shim.py | 88 +++ .../pythonfmu-export/src/CMakeLists.txt | 54 +- .../src/shim/fmi3_forwarders.cpp | 612 ++++++++++++++++++ .../pythonfmu-export/src/shim/shim_loader.cpp | 157 +++++ .../pythonfmu-export/src/shim/shim_loader.hpp | 29 + pythonfmu3/tests/data/fmi3_host.c | 31 + pythonfmu3/tests/fmi3_ctypes_driver.py | 2 +- pythonfmu3/tests/test_builder.py | 2 +- pythonfmu3/tests/test_non_python_host.py | 75 +++ 11 files changed, 1099 insertions(+), 22 deletions(-) create mode 100644 pythonfmu3/pythonfmu-export/generate_shim.py create mode 100644 pythonfmu3/pythonfmu-export/src/shim/fmi3_forwarders.cpp create mode 100644 pythonfmu3/pythonfmu-export/src/shim/shim_loader.cpp create mode 100644 pythonfmu3/pythonfmu-export/src/shim/shim_loader.hpp create mode 100644 pythonfmu3/tests/data/fmi3_host.c create mode 100644 pythonfmu3/tests/test_non_python_host.py diff --git a/docs/install.md b/docs/install.md index e38c5fc..b3f1981 100644 --- a/docs/install.md +++ b/docs/install.md @@ -28,3 +28,26 @@ or, ```bash pip install . ``` + +## Running FMUs outside Python (Linux and macOS) + +An FMU built with PythonFMU3 runs Python code, so the machine that simulates it needs a +Python 3 installation with a **shared** Python library (`libpython3.x.so` on Linux, +`libpython3.x.dylib` or a `Python.framework` on macOS), plus any packages the model imports. + +When the importer is itself a Python program (e.g. FMPy), the FMU uses that interpreter. +Otherwise (OMSimulator, `fmusim`, Simulink, ...) the FMU loads `libpython` itself, looking in +this order: + +1. the `PYTHONFMU_PYTHON_LIBRARY` environment variable, e.g. + `export PYTHONFMU_PYTHON_LIBRARY=/usr/lib/x86_64-linux-gnu/libpython3.12.so.1.0`; +2. the library of the Python that built the FMU (recorded in + `resources/pythonfmu_libpython.txt`); +3. standard library names for Python 3.8 to 3.14 (and the usual Homebrew and + python.org framework locations on macOS). + +If none can be loaded, instantiation fails with a message listing the paths tried. +Interpreters built without a shared library (some `pyenv` builds, for example) cannot be +used; with `pyenv`, reinstall with `PYTHON_CONFIGURE_OPTS="--enable-shared"`. + +On Windows the FMU links `python3.dll`, which must be on the `PATH`. diff --git a/pythonfmu3/builder.py b/pythonfmu3/builder.py index 602dadb..9a43c0f 100644 --- a/pythonfmu3/builder.py +++ b/pythonfmu3/builder.py @@ -6,10 +6,11 @@ import re import shutil import sys +import sysconfig import tempfile import zipfile from pathlib import Path -from typing import Iterable, Optional, Tuple, Union +from typing import Iterable, List, Optional, Tuple, Union from xml.dom.minidom import parseString from xml.etree.ElementTree import Element, SubElement, tostring from .osutil import get_lib_extension, get_platform @@ -95,6 +96,29 @@ def get_model_description(filepath: Path, module_name: str) -> Tuple[str, Elemen return instance.modelName, instance.to_xml(), class_name +def _is_fmu_entry_library(path: Path) -> bool: + """True for the library exporting the FMI functions (renamed to the model identifier).""" + return path.stem in ("pythonfmu-export", "libpythonfmu-export") + + +def _libpython_candidates() -> List[str]: + """Paths of the shared libpython of the running interpreter, if it has one.""" + if sys.platform == "win32": + return [] # the Windows library links python3.dll directly + libdir = sysconfig.get_config_var("LIBDIR") or "" + framework_prefix = sysconfig.get_config_var("PYTHONFRAMEWORKPREFIX") or "" + candidates = [] + for name in (sysconfig.get_config_var("INSTSONAME"), sysconfig.get_config_var("LDLIBRARY")): + if not name or name.endswith(".a"): + continue # statically linked interpreter: nothing to point at + for base in (libdir, framework_prefix): + if base: + candidate = Path(base) / name + if candidate.is_file() and str(candidate) not in candidates: + candidates.append(str(candidate)) + return candidates + + class FmuBuilder: @staticmethod @@ -200,6 +224,15 @@ def build_FMU( relative_f = f.relative_to(temp_dir) zip_fmu.write(f, arcname=(resource / relative_f)) + # Hint for the Linux/macOS shim: where the Python that built this FMU + # keeps its shared library (used when the importer is not Python itself). + libpython_hints = _libpython_candidates() + if libpython_hints: + zip_fmu.writestr( + str(resource.joinpath("pythonfmu_libpython.txt")), + "\n".join(libpython_hints) + "\n", + ) + # Add information for the Python loader: line 1 is the module, line 2 the class. # Older wrappers only read the first line, so this stays backward compatible. zip_fmu.writestr( @@ -226,12 +259,13 @@ def build_FMU( src_binaries.rglob("*.dylib"), ): relative_f = f.relative_to(src_binaries) - arcname = ( - binaries - / relative_f.parent - / f"{model_identifier}{relative_f.suffix}" - ) - zip_fmu.write(f, arcname=arcname) + # Only the FMI entry-point library takes the model identifier's name; + # helper libraries (the Linux/macOS backend) keep their own name. + if _is_fmu_entry_library(f): + name = f"{model_identifier}{relative_f.suffix}" + else: + name = relative_f.name + zip_fmu.write(f, arcname=(binaries / relative_f.parent / name)) # Add the documentation folder if documentation_folder is not None: diff --git a/pythonfmu3/pythonfmu-export/generate_shim.py b/pythonfmu3/pythonfmu-export/generate_shim.py new file mode 100644 index 0000000..13d6dad --- /dev/null +++ b/pythonfmu3/pythonfmu-export/generate_shim.py @@ -0,0 +1,88 @@ +"""Generate src/shim/fmi3_forwarders.cpp from src/fmi/fmi3FunctionTypes.h. + +The shim is the library the importer loads on Linux/macOS. It has no link-time +dependency on libpython: on instantiation it loads libpython (unless the host process +already provides it, e.g. when the importer is itself written in Python) and then the +real implementation (the "backend" library), and forwards every FMI call to it. + +Re-run this script after updating the FMI headers: + python generate_shim.py +""" +import re +from pathlib import Path + +HERE = Path(__file__).parent +TYPES_HEADER = HERE / "src" / "fmi" / "fmi3FunctionTypes.h" +OUTPUT = HERE / "src" / "shim" / "fmi3_forwarders.cpp" + +TYPEDEF_RE = re.compile(r"typedef\s+([\w\s\*]+?)\s*\b(fmi3\w+)TYPE\s*\((.*?)\)\s*;", re.S) +INSTANTIATE = { + "fmi3InstantiateModelExchange", + "fmi3InstantiateCoSimulation", + "fmi3InstantiateScheduledExecution", +} + + +def param_names(params: str): + params = " ".join(params.split()) + if params in ("", "void"): + return [] + return [re.findall(r"\w+", p.replace("[]", ""))[-1] for p in params.split(",")] + + +def main(): + functions = [ + (" ".join(ret.split()), name, " ".join(params.split())) + for ret, name, params in TYPEDEF_RE.findall(TYPES_HEADER.read_text()) + ] + forwarded = [f for f in functions if f[1] != "fmi3GetVersion"] + + out = [ + "// GENERATED by generate_shim.py from fmi/fmi3FunctionTypes.h - do not edit by hand.", + '#include "shim/shim_loader.hpp"', + "", + "namespace pythonfmu_shim", + "{", + "const char* const kFunctionNames[] = {", + *[f' "{name}",' for _, name, _ in forwarded], + "};", + f"const std::size_t kFunctionCount = {len(forwarded)};", + "} // namespace pythonfmu_shim", + "", + 'extern "C" {', + "", + "FMI3_Export const char* fmi3GetVersion(void)", + "{", + " return fmi3Version;", + "}", + "", + ] + for index, (ret, name, params) in enumerate(forwarded): + names = param_names(params) + args = ", ".join(names) + out.append(f"FMI3_Export {ret} {name}({params})") + out.append("{") + if name in INSTANTIATE: + out.append( + " if (!pythonfmu_shim::ensureBackendLoaded(resourcePath, instanceEnvironment, logMessage)) return nullptr;" + ) + out.append(f" const auto fn = reinterpret_cast<{name}TYPE*>(pythonfmu_shim::function({index}));") + if ret == "void": + out.append(f" if (fn) fn({args});") + elif ret == "fmi3Status": + out.append(" if (!fn) return fmi3Error;") + out.append(f" return fn({args});") + else: + out.append(" if (!fn) return nullptr;") + out.append(f" return fn({args});") + out.append("}") + out.append("") + out.append('} // extern "C"') + out.append("") + OUTPUT.parent.mkdir(exist_ok=True) + OUTPUT.write_text("\n".join(out)) + print(f"wrote {OUTPUT.relative_to(HERE)} ({len(functions)} functions)") + + +if __name__ == "__main__": + main() diff --git a/pythonfmu3/pythonfmu-export/src/CMakeLists.txt b/pythonfmu3/pythonfmu-export/src/CMakeLists.txt index b755cd4..09fc716 100644 --- a/pythonfmu3/pythonfmu-export/src/CMakeLists.txt +++ b/pythonfmu3/pythonfmu-export/src/CMakeLists.txt @@ -1,4 +1,3 @@ - set(headers cppfmu/cppfmu_cs.hpp cppfmu/cppfmu_common.hpp @@ -17,29 +16,58 @@ set(sources pythonfmu/PySlaveInstance.cpp ) -add_library(pythonfmu-export ${sources} ${headers}) -target_compile_features(pythonfmu-export PUBLIC "cxx_std_17") +set(BINARY_OUTPUT_DIR "${CMAKE_SOURCE_DIR}/../resources/binaries/${TARGET_PLATFORM}") -target_include_directories(pythonfmu-export +if (WIN32) + # Windows: a single library, linked against the version-independent python3.dll. + set(PYTHONFMU_IMPL_TARGET pythonfmu-export) +else () + # Linux/macOS: the Python-dependent implementation lives in a "backend" library + # (FMI functions renamed with a prefix). The FMU library itself (pythonfmu-export, + # renamed to by the builder) is a small shim with no link-time + # dependency on libpython, so non-Python importers can load it. See shim/shim_loader.cpp. + set(PYTHONFMU_IMPL_TARGET pythonfmu-backend) +endif () + +add_library(${PYTHONFMU_IMPL_TARGET} ${sources} ${headers}) +target_compile_features(${PYTHONFMU_IMPL_TARGET} PUBLIC "cxx_std_17") +target_include_directories(${PYTHONFMU_IMPL_TARGET} PRIVATE "${Python3_INCLUDE_DIRS}" "${CMAKE_CURRENT_SOURCE_DIR}" ) if (WIN32) - target_link_libraries(pythonfmu-export PRIVATE ${Python3_LIBRARIES}) -elseif (APPLE) - set_target_properties(pythonfmu-export PROPERTIES LINK_FLAGS "-undefined dynamic_lookup") -endif () - -if (WIN32) - set_target_properties(pythonfmu-export + target_link_libraries(${PYTHONFMU_IMPL_TARGET} PRIVATE ${Python3_LIBRARIES}) + set_target_properties(${PYTHONFMU_IMPL_TARGET} PROPERTIES - RUNTIME_OUTPUT_DIRECTORY_RELEASE "${CMAKE_SOURCE_DIR}/../resources/binaries/${TARGET_PLATFORM}" + RUNTIME_OUTPUT_DIRECTORY_RELEASE "${BINARY_OUTPUT_DIR}" ) else () + # Python symbols are resolved at load time against the process (the host Python, or + # the libpython the shim opened with RTLD_GLOBAL). + target_compile_definitions(${PYTHONFMU_IMPL_TARGET} PRIVATE FMI3_FUNCTION_PREFIX=pythonfmu_backend_) + if (APPLE) + set_target_properties(${PYTHONFMU_IMPL_TARGET} PROPERTIES LINK_FLAGS "-undefined dynamic_lookup") + endif () + set_target_properties(${PYTHONFMU_IMPL_TARGET} + PROPERTIES + LIBRARY_OUTPUT_DIRECTORY "${BINARY_OUTPUT_DIR}" + ) + + add_library(pythonfmu-export + shim/shim_loader.cpp + shim/shim_loader.hpp + shim/fmi3_forwarders.cpp + ) + target_compile_features(pythonfmu-export PUBLIC "cxx_std_17") + target_include_directories(pythonfmu-export PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}") + target_link_libraries(pythonfmu-export PRIVATE ${CMAKE_DL_LIBS}) set_target_properties(pythonfmu-export PROPERTIES - LIBRARY_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/../resources/binaries/${TARGET_PLATFORM}" + LIBRARY_OUTPUT_DIRECTORY "${BINARY_OUTPUT_DIR}" + # Only the FMI functions (marked FMI3_Export) are exported + CXX_VISIBILITY_PRESET hidden + VISIBILITY_INLINES_HIDDEN ON ) endif () diff --git a/pythonfmu3/pythonfmu-export/src/shim/fmi3_forwarders.cpp b/pythonfmu3/pythonfmu-export/src/shim/fmi3_forwarders.cpp new file mode 100644 index 0000000..daa3b07 --- /dev/null +++ b/pythonfmu3/pythonfmu-export/src/shim/fmi3_forwarders.cpp @@ -0,0 +1,612 @@ +// GENERATED by generate_shim.py from fmi/fmi3FunctionTypes.h - do not edit by hand. +#include "shim/shim_loader.hpp" + +namespace pythonfmu_shim +{ +const char* const kFunctionNames[] = { + "fmi3SetDebugLogging", + "fmi3InstantiateModelExchange", + "fmi3InstantiateCoSimulation", + "fmi3InstantiateScheduledExecution", + "fmi3FreeInstance", + "fmi3EnterInitializationMode", + "fmi3ExitInitializationMode", + "fmi3EnterEventMode", + "fmi3Terminate", + "fmi3Reset", + "fmi3GetFloat32", + "fmi3GetFloat64", + "fmi3GetInt8", + "fmi3GetUInt8", + "fmi3GetInt16", + "fmi3GetUInt16", + "fmi3GetInt32", + "fmi3GetUInt32", + "fmi3GetInt64", + "fmi3GetUInt64", + "fmi3GetBoolean", + "fmi3GetString", + "fmi3GetBinary", + "fmi3GetClock", + "fmi3SetFloat32", + "fmi3SetFloat64", + "fmi3SetInt8", + "fmi3SetUInt8", + "fmi3SetInt16", + "fmi3SetUInt16", + "fmi3SetInt32", + "fmi3SetUInt32", + "fmi3SetInt64", + "fmi3SetUInt64", + "fmi3SetBoolean", + "fmi3SetString", + "fmi3SetBinary", + "fmi3SetClock", + "fmi3GetNumberOfVariableDependencies", + "fmi3GetVariableDependencies", + "fmi3GetFMUState", + "fmi3SetFMUState", + "fmi3FreeFMUState", + "fmi3SerializedFMUStateSize", + "fmi3SerializeFMUState", + "fmi3DeserializeFMUState", + "fmi3GetDirectionalDerivative", + "fmi3GetAdjointDerivative", + "fmi3EnterConfigurationMode", + "fmi3ExitConfigurationMode", + "fmi3GetIntervalDecimal", + "fmi3GetIntervalFraction", + "fmi3GetShiftDecimal", + "fmi3GetShiftFraction", + "fmi3SetIntervalDecimal", + "fmi3SetIntervalFraction", + "fmi3SetShiftDecimal", + "fmi3SetShiftFraction", + "fmi3EvaluateDiscreteStates", + "fmi3UpdateDiscreteStates", + "fmi3EnterContinuousTimeMode", + "fmi3CompletedIntegratorStep", + "fmi3SetTime", + "fmi3SetContinuousStates", + "fmi3GetContinuousStateDerivatives", + "fmi3GetEventIndicators", + "fmi3GetContinuousStates", + "fmi3GetNominalsOfContinuousStates", + "fmi3GetNumberOfEventIndicators", + "fmi3GetNumberOfContinuousStates", + "fmi3EnterStepMode", + "fmi3GetOutputDerivatives", + "fmi3DoStep", + "fmi3ActivateModelPartition", +}; +const std::size_t kFunctionCount = 74; +} // namespace pythonfmu_shim + +extern "C" { + +FMI3_Export const char* fmi3GetVersion(void) +{ + return fmi3Version; +} + +FMI3_Export fmi3Status fmi3SetDebugLogging(fmi3Instance instance, fmi3Boolean loggingOn, size_t nCategories, const fmi3String categories[]) +{ + const auto fn = reinterpret_cast(pythonfmu_shim::function(0)); + if (!fn) return fmi3Error; + return fn(instance, loggingOn, nCategories, categories); +} + +FMI3_Export fmi3Instance fmi3InstantiateModelExchange(fmi3String instanceName, fmi3String instantiationToken, fmi3String resourcePath, fmi3Boolean visible, fmi3Boolean loggingOn, fmi3InstanceEnvironment instanceEnvironment, fmi3LogMessageCallback logMessage) +{ + if (!pythonfmu_shim::ensureBackendLoaded(resourcePath, instanceEnvironment, logMessage)) return nullptr; + const auto fn = reinterpret_cast(pythonfmu_shim::function(1)); + if (!fn) return nullptr; + return fn(instanceName, instantiationToken, resourcePath, visible, loggingOn, instanceEnvironment, logMessage); +} + +FMI3_Export fmi3Instance fmi3InstantiateCoSimulation(fmi3String instanceName, fmi3String instantiationToken, fmi3String resourcePath, fmi3Boolean visible, fmi3Boolean loggingOn, fmi3Boolean eventModeUsed, fmi3Boolean earlyReturnAllowed, const fmi3ValueReference requiredIntermediateVariables[], size_t nRequiredIntermediateVariables, fmi3InstanceEnvironment instanceEnvironment, fmi3LogMessageCallback logMessage, fmi3IntermediateUpdateCallback intermediateUpdate) +{ + if (!pythonfmu_shim::ensureBackendLoaded(resourcePath, instanceEnvironment, logMessage)) return nullptr; + const auto fn = reinterpret_cast(pythonfmu_shim::function(2)); + if (!fn) return nullptr; + return fn(instanceName, instantiationToken, resourcePath, visible, loggingOn, eventModeUsed, earlyReturnAllowed, requiredIntermediateVariables, nRequiredIntermediateVariables, instanceEnvironment, logMessage, intermediateUpdate); +} + +FMI3_Export fmi3Instance fmi3InstantiateScheduledExecution(fmi3String instanceName, fmi3String instantiationToken, fmi3String resourcePath, fmi3Boolean visible, fmi3Boolean loggingOn, fmi3InstanceEnvironment instanceEnvironment, fmi3LogMessageCallback logMessage, fmi3ClockUpdateCallback clockUpdate, fmi3LockPreemptionCallback lockPreemption, fmi3UnlockPreemptionCallback unlockPreemption) +{ + if (!pythonfmu_shim::ensureBackendLoaded(resourcePath, instanceEnvironment, logMessage)) return nullptr; + const auto fn = reinterpret_cast(pythonfmu_shim::function(3)); + if (!fn) return nullptr; + return fn(instanceName, instantiationToken, resourcePath, visible, loggingOn, instanceEnvironment, logMessage, clockUpdate, lockPreemption, unlockPreemption); +} + +FMI3_Export void fmi3FreeInstance(fmi3Instance instance) +{ + const auto fn = reinterpret_cast(pythonfmu_shim::function(4)); + if (fn) fn(instance); +} + +FMI3_Export fmi3Status fmi3EnterInitializationMode(fmi3Instance instance, fmi3Boolean toleranceDefined, fmi3Float64 tolerance, fmi3Float64 startTime, fmi3Boolean stopTimeDefined, fmi3Float64 stopTime) +{ + const auto fn = reinterpret_cast(pythonfmu_shim::function(5)); + if (!fn) return fmi3Error; + return fn(instance, toleranceDefined, tolerance, startTime, stopTimeDefined, stopTime); +} + +FMI3_Export fmi3Status fmi3ExitInitializationMode(fmi3Instance instance) +{ + const auto fn = reinterpret_cast(pythonfmu_shim::function(6)); + if (!fn) return fmi3Error; + return fn(instance); +} + +FMI3_Export fmi3Status fmi3EnterEventMode(fmi3Instance instance) +{ + const auto fn = reinterpret_cast(pythonfmu_shim::function(7)); + if (!fn) return fmi3Error; + return fn(instance); +} + +FMI3_Export fmi3Status fmi3Terminate(fmi3Instance instance) +{ + const auto fn = reinterpret_cast(pythonfmu_shim::function(8)); + if (!fn) return fmi3Error; + return fn(instance); +} + +FMI3_Export fmi3Status fmi3Reset(fmi3Instance instance) +{ + const auto fn = reinterpret_cast(pythonfmu_shim::function(9)); + if (!fn) return fmi3Error; + return fn(instance); +} + +FMI3_Export fmi3Status fmi3GetFloat32(fmi3Instance instance, const fmi3ValueReference valueReferences[], size_t nValueReferences, fmi3Float32 values[], size_t nValues) +{ + const auto fn = reinterpret_cast(pythonfmu_shim::function(10)); + if (!fn) return fmi3Error; + return fn(instance, valueReferences, nValueReferences, values, nValues); +} + +FMI3_Export fmi3Status fmi3GetFloat64(fmi3Instance instance, const fmi3ValueReference valueReferences[], size_t nValueReferences, fmi3Float64 values[], size_t nValues) +{ + const auto fn = reinterpret_cast(pythonfmu_shim::function(11)); + if (!fn) return fmi3Error; + return fn(instance, valueReferences, nValueReferences, values, nValues); +} + +FMI3_Export fmi3Status fmi3GetInt8(fmi3Instance instance, const fmi3ValueReference valueReferences[], size_t nValueReferences, fmi3Int8 values[], size_t nValues) +{ + const auto fn = reinterpret_cast(pythonfmu_shim::function(12)); + if (!fn) return fmi3Error; + return fn(instance, valueReferences, nValueReferences, values, nValues); +} + +FMI3_Export fmi3Status fmi3GetUInt8(fmi3Instance instance, const fmi3ValueReference valueReferences[], size_t nValueReferences, fmi3UInt8 values[], size_t nValues) +{ + const auto fn = reinterpret_cast(pythonfmu_shim::function(13)); + if (!fn) return fmi3Error; + return fn(instance, valueReferences, nValueReferences, values, nValues); +} + +FMI3_Export fmi3Status fmi3GetInt16(fmi3Instance instance, const fmi3ValueReference valueReferences[], size_t nValueReferences, fmi3Int16 values[], size_t nValues) +{ + const auto fn = reinterpret_cast(pythonfmu_shim::function(14)); + if (!fn) return fmi3Error; + return fn(instance, valueReferences, nValueReferences, values, nValues); +} + +FMI3_Export fmi3Status fmi3GetUInt16(fmi3Instance instance, const fmi3ValueReference valueReferences[], size_t nValueReferences, fmi3UInt16 values[], size_t nValues) +{ + const auto fn = reinterpret_cast(pythonfmu_shim::function(15)); + if (!fn) return fmi3Error; + return fn(instance, valueReferences, nValueReferences, values, nValues); +} + +FMI3_Export fmi3Status fmi3GetInt32(fmi3Instance instance, const fmi3ValueReference valueReferences[], size_t nValueReferences, fmi3Int32 values[], size_t nValues) +{ + const auto fn = reinterpret_cast(pythonfmu_shim::function(16)); + if (!fn) return fmi3Error; + return fn(instance, valueReferences, nValueReferences, values, nValues); +} + +FMI3_Export fmi3Status fmi3GetUInt32(fmi3Instance instance, const fmi3ValueReference valueReferences[], size_t nValueReferences, fmi3UInt32 values[], size_t nValues) +{ + const auto fn = reinterpret_cast(pythonfmu_shim::function(17)); + if (!fn) return fmi3Error; + return fn(instance, valueReferences, nValueReferences, values, nValues); +} + +FMI3_Export fmi3Status fmi3GetInt64(fmi3Instance instance, const fmi3ValueReference valueReferences[], size_t nValueReferences, fmi3Int64 values[], size_t nValues) +{ + const auto fn = reinterpret_cast(pythonfmu_shim::function(18)); + if (!fn) return fmi3Error; + return fn(instance, valueReferences, nValueReferences, values, nValues); +} + +FMI3_Export fmi3Status fmi3GetUInt64(fmi3Instance instance, const fmi3ValueReference valueReferences[], size_t nValueReferences, fmi3UInt64 values[], size_t nValues) +{ + const auto fn = reinterpret_cast(pythonfmu_shim::function(19)); + if (!fn) return fmi3Error; + return fn(instance, valueReferences, nValueReferences, values, nValues); +} + +FMI3_Export fmi3Status fmi3GetBoolean(fmi3Instance instance, const fmi3ValueReference valueReferences[], size_t nValueReferences, fmi3Boolean values[], size_t nValues) +{ + const auto fn = reinterpret_cast(pythonfmu_shim::function(20)); + if (!fn) return fmi3Error; + return fn(instance, valueReferences, nValueReferences, values, nValues); +} + +FMI3_Export fmi3Status fmi3GetString(fmi3Instance instance, const fmi3ValueReference valueReferences[], size_t nValueReferences, fmi3String values[], size_t nValues) +{ + const auto fn = reinterpret_cast(pythonfmu_shim::function(21)); + if (!fn) return fmi3Error; + return fn(instance, valueReferences, nValueReferences, values, nValues); +} + +FMI3_Export fmi3Status fmi3GetBinary(fmi3Instance instance, const fmi3ValueReference valueReferences[], size_t nValueReferences, size_t valueSizes[], fmi3Binary values[], size_t nValues) +{ + const auto fn = reinterpret_cast(pythonfmu_shim::function(22)); + if (!fn) return fmi3Error; + return fn(instance, valueReferences, nValueReferences, valueSizes, values, nValues); +} + +FMI3_Export fmi3Status fmi3GetClock(fmi3Instance instance, const fmi3ValueReference valueReferences[], size_t nValueReferences, fmi3Clock values[]) +{ + const auto fn = reinterpret_cast(pythonfmu_shim::function(23)); + if (!fn) return fmi3Error; + return fn(instance, valueReferences, nValueReferences, values); +} + +FMI3_Export fmi3Status fmi3SetFloat32(fmi3Instance instance, const fmi3ValueReference valueReferences[], size_t nValueReferences, const fmi3Float32 values[], size_t nValues) +{ + const auto fn = reinterpret_cast(pythonfmu_shim::function(24)); + if (!fn) return fmi3Error; + return fn(instance, valueReferences, nValueReferences, values, nValues); +} + +FMI3_Export fmi3Status fmi3SetFloat64(fmi3Instance instance, const fmi3ValueReference valueReferences[], size_t nValueReferences, const fmi3Float64 values[], size_t nValues) +{ + const auto fn = reinterpret_cast(pythonfmu_shim::function(25)); + if (!fn) return fmi3Error; + return fn(instance, valueReferences, nValueReferences, values, nValues); +} + +FMI3_Export fmi3Status fmi3SetInt8(fmi3Instance instance, const fmi3ValueReference valueReferences[], size_t nValueReferences, const fmi3Int8 values[], size_t nValues) +{ + const auto fn = reinterpret_cast(pythonfmu_shim::function(26)); + if (!fn) return fmi3Error; + return fn(instance, valueReferences, nValueReferences, values, nValues); +} + +FMI3_Export fmi3Status fmi3SetUInt8(fmi3Instance instance, const fmi3ValueReference valueReferences[], size_t nValueReferences, const fmi3UInt8 values[], size_t nValues) +{ + const auto fn = reinterpret_cast(pythonfmu_shim::function(27)); + if (!fn) return fmi3Error; + return fn(instance, valueReferences, nValueReferences, values, nValues); +} + +FMI3_Export fmi3Status fmi3SetInt16(fmi3Instance instance, const fmi3ValueReference valueReferences[], size_t nValueReferences, const fmi3Int16 values[], size_t nValues) +{ + const auto fn = reinterpret_cast(pythonfmu_shim::function(28)); + if (!fn) return fmi3Error; + return fn(instance, valueReferences, nValueReferences, values, nValues); +} + +FMI3_Export fmi3Status fmi3SetUInt16(fmi3Instance instance, const fmi3ValueReference valueReferences[], size_t nValueReferences, const fmi3UInt16 values[], size_t nValues) +{ + const auto fn = reinterpret_cast(pythonfmu_shim::function(29)); + if (!fn) return fmi3Error; + return fn(instance, valueReferences, nValueReferences, values, nValues); +} + +FMI3_Export fmi3Status fmi3SetInt32(fmi3Instance instance, const fmi3ValueReference valueReferences[], size_t nValueReferences, const fmi3Int32 values[], size_t nValues) +{ + const auto fn = reinterpret_cast(pythonfmu_shim::function(30)); + if (!fn) return fmi3Error; + return fn(instance, valueReferences, nValueReferences, values, nValues); +} + +FMI3_Export fmi3Status fmi3SetUInt32(fmi3Instance instance, const fmi3ValueReference valueReferences[], size_t nValueReferences, const fmi3UInt32 values[], size_t nValues) +{ + const auto fn = reinterpret_cast(pythonfmu_shim::function(31)); + if (!fn) return fmi3Error; + return fn(instance, valueReferences, nValueReferences, values, nValues); +} + +FMI3_Export fmi3Status fmi3SetInt64(fmi3Instance instance, const fmi3ValueReference valueReferences[], size_t nValueReferences, const fmi3Int64 values[], size_t nValues) +{ + const auto fn = reinterpret_cast(pythonfmu_shim::function(32)); + if (!fn) return fmi3Error; + return fn(instance, valueReferences, nValueReferences, values, nValues); +} + +FMI3_Export fmi3Status fmi3SetUInt64(fmi3Instance instance, const fmi3ValueReference valueReferences[], size_t nValueReferences, const fmi3UInt64 values[], size_t nValues) +{ + const auto fn = reinterpret_cast(pythonfmu_shim::function(33)); + if (!fn) return fmi3Error; + return fn(instance, valueReferences, nValueReferences, values, nValues); +} + +FMI3_Export fmi3Status fmi3SetBoolean(fmi3Instance instance, const fmi3ValueReference valueReferences[], size_t nValueReferences, const fmi3Boolean values[], size_t nValues) +{ + const auto fn = reinterpret_cast(pythonfmu_shim::function(34)); + if (!fn) return fmi3Error; + return fn(instance, valueReferences, nValueReferences, values, nValues); +} + +FMI3_Export fmi3Status fmi3SetString(fmi3Instance instance, const fmi3ValueReference valueReferences[], size_t nValueReferences, const fmi3String values[], size_t nValues) +{ + const auto fn = reinterpret_cast(pythonfmu_shim::function(35)); + if (!fn) return fmi3Error; + return fn(instance, valueReferences, nValueReferences, values, nValues); +} + +FMI3_Export fmi3Status fmi3SetBinary(fmi3Instance instance, const fmi3ValueReference valueReferences[], size_t nValueReferences, const size_t valueSizes[], const fmi3Binary values[], size_t nValues) +{ + const auto fn = reinterpret_cast(pythonfmu_shim::function(36)); + if (!fn) return fmi3Error; + return fn(instance, valueReferences, nValueReferences, valueSizes, values, nValues); +} + +FMI3_Export fmi3Status fmi3SetClock(fmi3Instance instance, const fmi3ValueReference valueReferences[], size_t nValueReferences, const fmi3Clock values[]) +{ + const auto fn = reinterpret_cast(pythonfmu_shim::function(37)); + if (!fn) return fmi3Error; + return fn(instance, valueReferences, nValueReferences, values); +} + +FMI3_Export fmi3Status fmi3GetNumberOfVariableDependencies(fmi3Instance instance, fmi3ValueReference valueReference, size_t* nDependencies) +{ + const auto fn = reinterpret_cast(pythonfmu_shim::function(38)); + if (!fn) return fmi3Error; + return fn(instance, valueReference, nDependencies); +} + +FMI3_Export fmi3Status fmi3GetVariableDependencies(fmi3Instance instance, fmi3ValueReference dependent, size_t elementIndicesOfDependent[], fmi3ValueReference independents[], size_t elementIndicesOfIndependents[], fmi3DependencyKind dependencyKinds[], size_t nDependencies) +{ + const auto fn = reinterpret_cast(pythonfmu_shim::function(39)); + if (!fn) return fmi3Error; + return fn(instance, dependent, elementIndicesOfDependent, independents, elementIndicesOfIndependents, dependencyKinds, nDependencies); +} + +FMI3_Export fmi3Status fmi3GetFMUState(fmi3Instance instance, fmi3FMUState* FMUState) +{ + const auto fn = reinterpret_cast(pythonfmu_shim::function(40)); + if (!fn) return fmi3Error; + return fn(instance, FMUState); +} + +FMI3_Export fmi3Status fmi3SetFMUState(fmi3Instance instance, fmi3FMUState FMUState) +{ + const auto fn = reinterpret_cast(pythonfmu_shim::function(41)); + if (!fn) return fmi3Error; + return fn(instance, FMUState); +} + +FMI3_Export fmi3Status fmi3FreeFMUState(fmi3Instance instance, fmi3FMUState* FMUState) +{ + const auto fn = reinterpret_cast(pythonfmu_shim::function(42)); + if (!fn) return fmi3Error; + return fn(instance, FMUState); +} + +FMI3_Export fmi3Status fmi3SerializedFMUStateSize(fmi3Instance instance, fmi3FMUState FMUState, size_t* size) +{ + const auto fn = reinterpret_cast(pythonfmu_shim::function(43)); + if (!fn) return fmi3Error; + return fn(instance, FMUState, size); +} + +FMI3_Export fmi3Status fmi3SerializeFMUState(fmi3Instance instance, fmi3FMUState FMUState, fmi3Byte serializedState[], size_t size) +{ + const auto fn = reinterpret_cast(pythonfmu_shim::function(44)); + if (!fn) return fmi3Error; + return fn(instance, FMUState, serializedState, size); +} + +FMI3_Export fmi3Status fmi3DeserializeFMUState(fmi3Instance instance, const fmi3Byte serializedState[], size_t size, fmi3FMUState* FMUState) +{ + const auto fn = reinterpret_cast(pythonfmu_shim::function(45)); + if (!fn) return fmi3Error; + return fn(instance, serializedState, size, FMUState); +} + +FMI3_Export fmi3Status fmi3GetDirectionalDerivative(fmi3Instance instance, const fmi3ValueReference unknowns[], size_t nUnknowns, const fmi3ValueReference knowns[], size_t nKnowns, const fmi3Float64 seed[], size_t nSeed, fmi3Float64 sensitivity[], size_t nSensitivity) +{ + const auto fn = reinterpret_cast(pythonfmu_shim::function(46)); + if (!fn) return fmi3Error; + return fn(instance, unknowns, nUnknowns, knowns, nKnowns, seed, nSeed, sensitivity, nSensitivity); +} + +FMI3_Export fmi3Status fmi3GetAdjointDerivative(fmi3Instance instance, const fmi3ValueReference unknowns[], size_t nUnknowns, const fmi3ValueReference knowns[], size_t nKnowns, const fmi3Float64 seed[], size_t nSeed, fmi3Float64 sensitivity[], size_t nSensitivity) +{ + const auto fn = reinterpret_cast(pythonfmu_shim::function(47)); + if (!fn) return fmi3Error; + return fn(instance, unknowns, nUnknowns, knowns, nKnowns, seed, nSeed, sensitivity, nSensitivity); +} + +FMI3_Export fmi3Status fmi3EnterConfigurationMode(fmi3Instance instance) +{ + const auto fn = reinterpret_cast(pythonfmu_shim::function(48)); + if (!fn) return fmi3Error; + return fn(instance); +} + +FMI3_Export fmi3Status fmi3ExitConfigurationMode(fmi3Instance instance) +{ + const auto fn = reinterpret_cast(pythonfmu_shim::function(49)); + if (!fn) return fmi3Error; + return fn(instance); +} + +FMI3_Export fmi3Status fmi3GetIntervalDecimal(fmi3Instance instance, const fmi3ValueReference valueReferences[], size_t nValueReferences, fmi3Float64 intervals[], fmi3IntervalQualifier qualifiers[]) +{ + const auto fn = reinterpret_cast(pythonfmu_shim::function(50)); + if (!fn) return fmi3Error; + return fn(instance, valueReferences, nValueReferences, intervals, qualifiers); +} + +FMI3_Export fmi3Status fmi3GetIntervalFraction(fmi3Instance instance, const fmi3ValueReference valueReferences[], size_t nValueReferences, fmi3UInt64 counters[], fmi3UInt64 resolutions[], fmi3IntervalQualifier qualifiers[]) +{ + const auto fn = reinterpret_cast(pythonfmu_shim::function(51)); + if (!fn) return fmi3Error; + return fn(instance, valueReferences, nValueReferences, counters, resolutions, qualifiers); +} + +FMI3_Export fmi3Status fmi3GetShiftDecimal(fmi3Instance instance, const fmi3ValueReference valueReferences[], size_t nValueReferences, fmi3Float64 shifts[]) +{ + const auto fn = reinterpret_cast(pythonfmu_shim::function(52)); + if (!fn) return fmi3Error; + return fn(instance, valueReferences, nValueReferences, shifts); +} + +FMI3_Export fmi3Status fmi3GetShiftFraction(fmi3Instance instance, const fmi3ValueReference valueReferences[], size_t nValueReferences, fmi3UInt64 counters[], fmi3UInt64 resolutions[]) +{ + const auto fn = reinterpret_cast(pythonfmu_shim::function(53)); + if (!fn) return fmi3Error; + return fn(instance, valueReferences, nValueReferences, counters, resolutions); +} + +FMI3_Export fmi3Status fmi3SetIntervalDecimal(fmi3Instance instance, const fmi3ValueReference valueReferences[], size_t nValueReferences, const fmi3Float64 intervals[]) +{ + const auto fn = reinterpret_cast(pythonfmu_shim::function(54)); + if (!fn) return fmi3Error; + return fn(instance, valueReferences, nValueReferences, intervals); +} + +FMI3_Export fmi3Status fmi3SetIntervalFraction(fmi3Instance instance, const fmi3ValueReference valueReferences[], size_t nValueReferences, const fmi3UInt64 counters[], const fmi3UInt64 resolutions[]) +{ + const auto fn = reinterpret_cast(pythonfmu_shim::function(55)); + if (!fn) return fmi3Error; + return fn(instance, valueReferences, nValueReferences, counters, resolutions); +} + +FMI3_Export fmi3Status fmi3SetShiftDecimal(fmi3Instance instance, const fmi3ValueReference valueReferences[], size_t nValueReferences, const fmi3Float64 shifts[]) +{ + const auto fn = reinterpret_cast(pythonfmu_shim::function(56)); + if (!fn) return fmi3Error; + return fn(instance, valueReferences, nValueReferences, shifts); +} + +FMI3_Export fmi3Status fmi3SetShiftFraction(fmi3Instance instance, const fmi3ValueReference valueReferences[], size_t nValueReferences, const fmi3UInt64 counters[], const fmi3UInt64 resolutions[]) +{ + const auto fn = reinterpret_cast(pythonfmu_shim::function(57)); + if (!fn) return fmi3Error; + return fn(instance, valueReferences, nValueReferences, counters, resolutions); +} + +FMI3_Export fmi3Status fmi3EvaluateDiscreteStates(fmi3Instance instance) +{ + const auto fn = reinterpret_cast(pythonfmu_shim::function(58)); + if (!fn) return fmi3Error; + return fn(instance); +} + +FMI3_Export fmi3Status fmi3UpdateDiscreteStates(fmi3Instance instance, fmi3Boolean* discreteStatesNeedUpdate, fmi3Boolean* terminateSimulation, fmi3Boolean* nominalsOfContinuousStatesChanged, fmi3Boolean* valuesOfContinuousStatesChanged, fmi3Boolean* nextEventTimeDefined, fmi3Float64* nextEventTime) +{ + const auto fn = reinterpret_cast(pythonfmu_shim::function(59)); + if (!fn) return fmi3Error; + return fn(instance, discreteStatesNeedUpdate, terminateSimulation, nominalsOfContinuousStatesChanged, valuesOfContinuousStatesChanged, nextEventTimeDefined, nextEventTime); +} + +FMI3_Export fmi3Status fmi3EnterContinuousTimeMode(fmi3Instance instance) +{ + const auto fn = reinterpret_cast(pythonfmu_shim::function(60)); + if (!fn) return fmi3Error; + return fn(instance); +} + +FMI3_Export fmi3Status fmi3CompletedIntegratorStep(fmi3Instance instance, fmi3Boolean noSetFMUStatePriorToCurrentPoint, fmi3Boolean* enterEventMode, fmi3Boolean* terminateSimulation) +{ + const auto fn = reinterpret_cast(pythonfmu_shim::function(61)); + if (!fn) return fmi3Error; + return fn(instance, noSetFMUStatePriorToCurrentPoint, enterEventMode, terminateSimulation); +} + +FMI3_Export fmi3Status fmi3SetTime(fmi3Instance instance, fmi3Float64 time) +{ + const auto fn = reinterpret_cast(pythonfmu_shim::function(62)); + if (!fn) return fmi3Error; + return fn(instance, time); +} + +FMI3_Export fmi3Status fmi3SetContinuousStates(fmi3Instance instance, const fmi3Float64 continuousStates[], size_t nContinuousStates) +{ + const auto fn = reinterpret_cast(pythonfmu_shim::function(63)); + if (!fn) return fmi3Error; + return fn(instance, continuousStates, nContinuousStates); +} + +FMI3_Export fmi3Status fmi3GetContinuousStateDerivatives(fmi3Instance instance, fmi3Float64 derivatives[], size_t nContinuousStates) +{ + const auto fn = reinterpret_cast(pythonfmu_shim::function(64)); + if (!fn) return fmi3Error; + return fn(instance, derivatives, nContinuousStates); +} + +FMI3_Export fmi3Status fmi3GetEventIndicators(fmi3Instance instance, fmi3Float64 eventIndicators[], size_t nEventIndicators) +{ + const auto fn = reinterpret_cast(pythonfmu_shim::function(65)); + if (!fn) return fmi3Error; + return fn(instance, eventIndicators, nEventIndicators); +} + +FMI3_Export fmi3Status fmi3GetContinuousStates(fmi3Instance instance, fmi3Float64 continuousStates[], size_t nContinuousStates) +{ + const auto fn = reinterpret_cast(pythonfmu_shim::function(66)); + if (!fn) return fmi3Error; + return fn(instance, continuousStates, nContinuousStates); +} + +FMI3_Export fmi3Status fmi3GetNominalsOfContinuousStates(fmi3Instance instance, fmi3Float64 nominals[], size_t nContinuousStates) +{ + const auto fn = reinterpret_cast(pythonfmu_shim::function(67)); + if (!fn) return fmi3Error; + return fn(instance, nominals, nContinuousStates); +} + +FMI3_Export fmi3Status fmi3GetNumberOfEventIndicators(fmi3Instance instance, size_t* nEventIndicators) +{ + const auto fn = reinterpret_cast(pythonfmu_shim::function(68)); + if (!fn) return fmi3Error; + return fn(instance, nEventIndicators); +} + +FMI3_Export fmi3Status fmi3GetNumberOfContinuousStates(fmi3Instance instance, size_t* nContinuousStates) +{ + const auto fn = reinterpret_cast(pythonfmu_shim::function(69)); + if (!fn) return fmi3Error; + return fn(instance, nContinuousStates); +} + +FMI3_Export fmi3Status fmi3EnterStepMode(fmi3Instance instance) +{ + const auto fn = reinterpret_cast(pythonfmu_shim::function(70)); + if (!fn) return fmi3Error; + return fn(instance); +} + +FMI3_Export fmi3Status fmi3GetOutputDerivatives(fmi3Instance instance, const fmi3ValueReference valueReferences[], size_t nValueReferences, const fmi3Int32 orders[], fmi3Float64 values[], size_t nValues) +{ + const auto fn = reinterpret_cast(pythonfmu_shim::function(71)); + if (!fn) return fmi3Error; + return fn(instance, valueReferences, nValueReferences, orders, values, nValues); +} + +FMI3_Export fmi3Status fmi3DoStep(fmi3Instance instance, fmi3Float64 currentCommunicationPoint, fmi3Float64 communicationStepSize, fmi3Boolean noSetFMUStatePriorToCurrentPoint, fmi3Boolean* eventHandlingNeeded, fmi3Boolean* terminateSimulation, fmi3Boolean* earlyReturn, fmi3Float64* lastSuccessfulTime) +{ + const auto fn = reinterpret_cast(pythonfmu_shim::function(72)); + if (!fn) return fmi3Error; + return fn(instance, currentCommunicationPoint, communicationStepSize, noSetFMUStatePriorToCurrentPoint, eventHandlingNeeded, terminateSimulation, earlyReturn, lastSuccessfulTime); +} + +FMI3_Export fmi3Status fmi3ActivateModelPartition(fmi3Instance instance, fmi3ValueReference clockReference, fmi3Float64 activationTime) +{ + const auto fn = reinterpret_cast(pythonfmu_shim::function(73)); + if (!fn) return fmi3Error; + return fn(instance, clockReference, activationTime); +} + +} // extern "C" diff --git a/pythonfmu3/pythonfmu-export/src/shim/shim_loader.cpp b/pythonfmu3/pythonfmu-export/src/shim/shim_loader.cpp new file mode 100644 index 0000000..ab09d26 --- /dev/null +++ b/pythonfmu3/pythonfmu-export/src/shim/shim_loader.cpp @@ -0,0 +1,157 @@ +// Linux/macOS loader shim. +// +// The importer loads this library (binaries//.so|.dylib). It +// deliberately has no link-time dependency on Python, so it can be loaded by any FMI +// importer (OMSimulator, fmusim, Simulink, ...), not just Python-based ones. +// +// On the first instantiation it: +// 1. makes the Python C API available process-wide: if the host process does not +// already provide it (Python-based importers such as FMPy do), libpython is opened +// with RTLD_GLOBAL, which also lets extension modules such as numpy resolve it; +// 2. loads the backend library (libpythonfmu-backend) from the same folder as this +// library, and resolves the FMI functions it implements. +// +// libpython is searched for in this order: +// - the PYTHONFMU_PYTHON_LIBRARY environment variable (a full path), +// - resources/pythonfmu_libpython.txt (written by the builder: the library of the +// Python that built the FMU, one candidate path per line), +// - common library names for Python 3.8 - 3.14. + +#include "shim/shim_loader.hpp" + +#include + +#include +#include +#include +#include +#include +#include + +namespace pythonfmu_shim +{ +namespace +{ + +#ifdef __APPLE__ +const char* const kBackendFile = "libpythonfmu-backend.dylib"; +#else +const char* const kBackendFile = "libpythonfmu-backend.so"; +#endif + +std::mutex g_mutex; +bool g_loaded = false; +std::vector g_functions; + +bool pythonAlreadyAvailable() +{ + return dlsym(RTLD_DEFAULT, "Py_IsInitialized") != nullptr; +} + +std::string directoryOfThisLibrary() +{ + Dl_info info{}; + if (dladdr(reinterpret_cast(&directoryOfThisLibrary), &info) == 0 || info.dli_fname == nullptr) { + return "."; + } + std::string path = info.dli_fname; + const auto slash = path.find_last_of('/'); + return slash == std::string::npos ? "." : path.substr(0, slash); +} + +std::string resourceDirectory(const char* resourcePath) +{ + if (resourcePath == nullptr) return {}; + std::string path = resourcePath; + const std::string scheme = "file://"; + if (path.compare(0, scheme.size(), scheme) == 0) path.erase(0, scheme.size()); + while (path.size() > 1 && path.back() == '/') path.pop_back(); + return path; +} + +std::vector libpythonCandidates(const std::string& resources) +{ + std::vector candidates; + if (const char* env = std::getenv("PYTHONFMU_PYTHON_LIBRARY")) { + if (*env != '\0') candidates.emplace_back(env); + } + if (!resources.empty()) { + std::ifstream hints(resources + "/pythonfmu_libpython.txt"); + std::string line; + while (std::getline(hints, line)) { + while (!line.empty() && (line.back() == '\r' || line.back() == ' ')) line.pop_back(); + if (!line.empty()) candidates.push_back(line); + } + } + for (int minor = 14; minor >= 8; --minor) { + const std::string v = "3." + std::to_string(minor); +#ifdef __APPLE__ + candidates.push_back("libpython" + v + ".dylib"); + candidates.push_back("/opt/homebrew/opt/python@" + v + "/Frameworks/Python.framework/Versions/" + v + "/Python"); + candidates.push_back("/usr/local/opt/python@" + v + "/Frameworks/Python.framework/Versions/" + v + "/Python"); + candidates.push_back("/Library/Frameworks/Python.framework/Versions/" + v + "/Python"); +#else + candidates.push_back("libpython" + v + ".so.1.0"); + candidates.push_back("libpython" + v + ".so"); +#endif + } + return candidates; +} + +void report(fmi3InstanceEnvironment env, fmi3LogMessageCallback logMessage, const std::string& msg) +{ + if (logMessage != nullptr) logMessage(env, fmi3Fatal, "logStatusFatal", msg.c_str()); +} + +} // namespace + +bool ensureBackendLoaded(fmi3String resourcePath, fmi3InstanceEnvironment env, fmi3LogMessageCallback logMessage) +{ + std::lock_guard lock(g_mutex); + if (g_loaded) return true; + + if (!pythonAlreadyAvailable()) { + std::ostringstream errors; + bool found = false; + for (const auto& candidate : libpythonCandidates(resourceDirectory(resourcePath))) { + if (dlopen(candidate.c_str(), RTLD_NOW | RTLD_GLOBAL) != nullptr && pythonAlreadyAvailable()) { + found = true; + break; + } + const char* err = dlerror(); + errors << "\n " << candidate << ": " << (err ? err : "loaded, but Py_IsInitialized not found"); + } + if (!found) { + report(env, logMessage, + "PythonFMU3: unable to load the Python shared library (libpython). Set the " + "PYTHONFMU_PYTHON_LIBRARY environment variable to its full path. Tried:" + errors.str()); + return false; + } + } + + const std::string backendPath = directoryOfThisLibrary() + "/" + kBackendFile; + // Never closed: the backend keeps the Python interpreter (and a thread) alive. + void* backend = dlopen(backendPath.c_str(), RTLD_NOW | RTLD_LOCAL); + if (backend == nullptr) { + const char* err = dlerror(); + report(env, logMessage, "PythonFMU3: unable to load " + backendPath + ": " + (err ? err : "unknown error")); + return false; + } + + std::vector functions(kFunctionCount, nullptr); + for (std::size_t i = 0; i < kFunctionCount; i++) { + functions[i] = dlsym(backend, (std::string(kBackendPrefix) + kFunctionNames[i]).c_str()); + } + g_functions = std::move(functions); + g_loaded = true; + return true; +} + +void* function(std::size_t index) +{ + // g_functions is written once, under the mutex, before any instance exists; every + // later call is made on an instance and therefore happens after that write. + return index < g_functions.size() ? g_functions[index] : nullptr; +} + +} // namespace pythonfmu_shim diff --git a/pythonfmu3/pythonfmu-export/src/shim/shim_loader.hpp b/pythonfmu3/pythonfmu-export/src/shim/shim_loader.hpp new file mode 100644 index 0000000..0eb6f7b --- /dev/null +++ b/pythonfmu3/pythonfmu-export/src/shim/shim_loader.hpp @@ -0,0 +1,29 @@ +#ifndef PYTHONFMU_SHIM_LOADER_HPP +#define PYTHONFMU_SHIM_LOADER_HPP + +// Linux/macOS only. See shim_loader.cpp. + +#include "fmi/fmi3Functions.h" + +#include + +namespace pythonfmu_shim +{ + +// Symbol prefix the backend library is compiled with (FMI3_FUNCTION_PREFIX). +constexpr const char* kBackendPrefix = "pythonfmu_backend_"; + +// Generated (fmi3_forwarders.cpp): FMI functions forwarded to the backend, by index. +extern const char* const kFunctionNames[]; +extern const std::size_t kFunctionCount; + +// Makes libpython available (if needed) and loads the backend library. Thread-safe and +// idempotent. On failure, reports through logMessage (if non-null) and returns false. +bool ensureBackendLoaded(fmi3String resourcePath, fmi3InstanceEnvironment env, fmi3LogMessageCallback logMessage); + +// Backend implementation of kFunctionNames[index], or nullptr if not loaded/available. +void* function(std::size_t index); + +} // namespace pythonfmu_shim + +#endif diff --git a/pythonfmu3/tests/data/fmi3_host.c b/pythonfmu3/tests/data/fmi3_host.c new file mode 100644 index 0000000..14b3f8e --- /dev/null +++ b/pythonfmu3/tests/data/fmi3_host.c @@ -0,0 +1,31 @@ +/* Minimal non-Python FMI 3 importer used by test_non_python_host.py. + usage: fmi3_host (runs pythonslave.py for 3 steps) */ +#include +#include +#include +#include "fmi3Functions.h" + +static void logger(fmi3InstanceEnvironment env, fmi3Status s, fmi3String cat, fmi3String msg) { + printf("[log %d %s] %s\n", s, cat ? cat : "", msg); +} +#define LOAD(name) name##TYPE* name = (name##TYPE*)dlsym(h, #name); if (!name) { printf("missing %s\n", #name); return 1; } + +int main(int argc, char** argv) { + void* h = dlopen(argv[1], RTLD_NOW | RTLD_LOCAL); + if (!h) { printf("dlopen failed: %s\n", dlerror()); return 1; } + LOAD(fmi3GetVersion) LOAD(fmi3InstantiateCoSimulation) LOAD(fmi3EnterInitializationMode) + LOAD(fmi3ExitInitializationMode) LOAD(fmi3DoStep) LOAD(fmi3GetFloat64) LOAD(fmi3FreeInstance) + printf("version %s\n", fmi3GetVersion()); + fmi3Instance c = fmi3InstantiateCoSimulation("inst", "", argv[2], fmi3False, fmi3True, fmi3False, fmi3False, + NULL, 0, NULL, logger, NULL); + if (!c) { printf("instantiate failed\n"); return 2; } + fmi3EnterInitializationMode(c, fmi3False, 0, 0, fmi3False, 0); + fmi3ExitInitializationMode(c); + fmi3Boolean ehn, term, early; fmi3Float64 last; + for (int i = 0; i < 3; i++) fmi3DoStep(c, i * 0.5, 0.5, fmi3True, &ehn, &term, &early, &last); + fmi3ValueReference vr = 6; fmi3Float64 out = -1; + fmi3Status st = fmi3GetFloat64(c, &vr, 1, &out, 1); + printf("status %d realOut %g\n", st, out); + fmi3FreeInstance(c); + return (st == fmi3OK && out == 1.5) ? 0 : 3; +} diff --git a/pythonfmu3/tests/fmi3_ctypes_driver.py b/pythonfmu3/tests/fmi3_ctypes_driver.py index 7555648..f77e170 100644 --- a/pythonfmu3/tests/fmi3_ctypes_driver.py +++ b/pythonfmu3/tests/fmi3_ctypes_driver.py @@ -7,7 +7,7 @@ from pathlib import Path scenario, fmu_dir = sys.argv[1], Path(sys.argv[2]) -lib_path = next((fmu_dir / "binaries").rglob("*.so")) +lib_path = next(p for p in (fmu_dir / "binaries").rglob("*.so") if "pythonfmu-backend" not in p.name) lib = C.CDLL(str(lib_path)) LOGCB = C.CFUNCTYPE(None, C.c_void_p, C.c_int, C.c_char_p, C.c_char_p) diff --git a/pythonfmu3/tests/test_builder.py b/pythonfmu3/tests/test_builder.py index 9fd73eb..03c8d63 100644 --- a/pythonfmu3/tests/test_builder.py +++ b/pythonfmu3/tests/test_builder.py @@ -62,7 +62,7 @@ def test_zip_content(tmp_path): pkg_folder = Path(pythonfmu3.__path__[0]) for f in pkg_folder.rglob("*.py"): relative_f = f.relative_to(pkg_folder).as_posix() - if "test" not in relative_f: + if "test" not in relative_f and not relative_f.startswith("pythonfmu-export/"): assert "/".join(("resources", "pythonfmu3", relative_f)) in names assert len(names) >= nfiles # Library + python script + XML + module name + sources diff --git a/pythonfmu3/tests/test_non_python_host.py b/pythonfmu3/tests/test_non_python_host.py new file mode 100644 index 0000000..a7a8fae --- /dev/null +++ b/pythonfmu3/tests/test_non_python_host.py @@ -0,0 +1,75 @@ +"""Load a built FMU from a plain C program, i.e. without a Python interpreter in the host. + +This is how most FMI importers (OMSimulator, fmusim, Simulink, ...) use an FMU. The +Linux/macOS shim must locate libpython itself in that case. +""" +import os +import shutil +import subprocess +import sys +import zipfile +from pathlib import Path + +import pytest + +import pythonfmu3 +from pythonfmu3.builder import FmuBuilder +from pythonfmu3.osutil import get_lib_extension, get_platform + +HERE = Path(__file__).parent +CC = shutil.which("cc") or shutil.which("gcc") or shutil.which("clang") + +pytestmark = [ + pytest.mark.skipif(sys.platform == "win32", reason="the shim is only used on Linux/macOS"), + pytest.mark.skipif(not FmuBuilder.has_binary(), reason="needs the compiled wrapper"), + pytest.mark.skipif(CC is None, reason="needs a C compiler"), +] + + +@pytest.fixture(scope="module") +def host(tmp_path_factory): + out = tmp_path_factory.mktemp("host") / "fmi3_host" + include = Path(pythonfmu3.__path__[0]) / "pythonfmu-export" / "src" / "fmi" + libs = ["-ldl"] if sys.platform.startswith("linux") else [] + subprocess.run([CC, f"-I{include}", str(HERE / "data" / "fmi3_host.c"), "-o", str(out), *libs], check=True) + return out + + +@pytest.fixture(scope="module") +def fmu_dir(tmp_path_factory): + dest = tmp_path_factory.mktemp("fmu") + fmu = FmuBuilder.build_FMU(HERE / "slaves" / "pythonslave.py", dest=dest) + target = dest / "extracted" + with zipfile.ZipFile(fmu) as z: + z.extractall(target) + return target + + +def _run(host, fmu_dir, env): + library = fmu_dir / "binaries" / get_platform() / f"PythonSlave.{get_lib_extension()}" + # Minimal environment: nothing Python-related is inherited from the test process + base = {k: v for k, v in os.environ.items() if k in ("PATH", "HOME", "LANG", "TMPDIR")} + return subprocess.run( + [str(host), str(library), str(fmu_dir / "resources")], + capture_output=True, text=True, timeout=120, env={**base, **env}, + ) + + +def test_runs_without_python_host(host, fmu_dir): + proc = _run(host, fmu_dir, {}) + assert proc.returncode == 0, proc.stdout + proc.stderr + assert "realOut 1.5" in proc.stdout + + +def test_library_path_from_environment(host, fmu_dir, tmp_path): + hints = fmu_dir / "resources" / "pythonfmu_libpython.txt" + if not hints.exists(): + pytest.skip("building interpreter has no shared libpython") + library = hints.read_text().splitlines()[0] + backup = tmp_path / "hints.txt" + shutil.move(hints, backup) + try: + proc = _run(host, fmu_dir, {"PYTHONFMU_PYTHON_LIBRARY": library}) + finally: + shutil.move(backup, hints) + assert proc.returncode == 0, proc.stdout + proc.stderr From 2d099be271db5d83b93c920c4e9fb39a2d1be71a Mon Sep 17 00:00:00 2001 From: Stephen Smith Date: Thu, 12 Nov 2026 19:34:32 +0000 Subject: [PATCH 2/2] Document running FMUs in native importers and add a changelog - README and docs index: FMUs now run in any FMI 3.0 importer, what the simulating machine needs, and PYTHONFMU_PYTHON_LIBRARY - dependencies.md: which Python environment the FMU uses per importer type (incl. virtualenv + PYTHONPATH) - docs/changelog.md (in mkdocs nav) listing the user-facing changes on this branch and fix/wrapper-robustness Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01X8Wmw797DJpgrMFmT6n8CG --- README.md | 20 ++++++++++++++++++++ docs/changelog.md | 32 ++++++++++++++++++++++++++++++++ docs/dependencies.md | 14 +++++++++++++- docs/index.md | 7 +++++++ mkdocs.yml | 1 + 5 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 docs/changelog.md diff --git a/README.md b/README.md index b295c8b..5c3e8d9 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,26 @@ This project is a fork of the original PythonFMU repository available at https:/ Please take a look at the examples to see the supported features. We currently support both the Model exchange and Cosimulation Interfaces. +See the [changelog](docs/changelog.md) for what changed in each release. + +### Where can I run the FMU? + +In any FMI 3.0 importer: Python-based tools such as FMPy, and native tools such as +OMSimulator, `fmusim` or Simulink. + +The machine running the simulation needs Python 3 with a **shared** Python library, plus +the packages your model imports (see `pythonfmu3 deploy` below). Inside a Python-based +importer the FMU uses that interpreter. Otherwise, on Linux and macOS the FMU finds and +loads `libpython` itself; if it picks the wrong one, or none, point it at the right library: + +```bash +export PYTHONFMU_PYTHON_LIBRARY=/usr/lib/x86_64-linux-gnu/libpython3.12.so.1.0 +``` + +On Windows, `python3.dll` must be on the `PATH`. See +[Running FMUs outside Python](docs/install.md#running-fmus-outside-python-linux-and-macos) +for the full lookup order. + ### How do I build an FMU from python code? 1. Install `pythonfmu3` package: diff --git a/docs/changelog.md b/docs/changelog.md new file mode 100644 index 0000000..0eaeb5c --- /dev/null +++ b/docs/changelog.md @@ -0,0 +1,32 @@ +# Changelog + +## Unreleased + +### Added +- FMUs can be loaded by native (non-Python) FMI importers on Linux and macOS, such as + OMSimulator, `fmusim` and Simulink. Previously they only worked inside a Python-based + importer such as FMPy. See + [Running FMUs outside Python](install.md#running-fmus-outside-python-linux-and-macos). +- `PYTHONFMU_PYTHON_LIBRARY` environment variable to choose the Python library the FMU loads. +- Python errors raised by a model are reported with their full traceback in the FMU log. + +### Changed +- On Linux and macOS, `binaries//` now holds two libraries: `` + (the FMI entry point) and `libpythonfmu-backend`. Keep them together. +- `resources/slavemodule.txt` also records the model class name, and + `resources/pythonfmu_libpython.txt` records the building interpreter's Python library. +- The builder finds the model class by importing the script, so class definitions whose + base classes span several lines work. A script must define a single FMU class. + +### Fixed +- Crash (use-after-free) when freeing an instance after a Python exception. +- Possible deadlock when instantiation failed without a Python exception. +- Out-of-bounds reads when an importer requested more values than the model returned. +- `fmi3DoStep` now always sets `eventHandlingNeeded`, `terminateSimulation` and + `earlyReturn`, and honours the matching `Fmi3StepResult` fields. +- Memory leaks in logging, FMU-state handling and failed instantiation. +- Crash when the importer passes no log callback (allowed by FMI 3.0). +- Default CoSimulation/ModelExchange capability flags in the model description when + building from Python (e.g. `canHandleVariableCommunicationStepSize` came out `false`). +- `--terminals` packed the wrong file as `terminalsAndIcons.xml`. +- Array variable setters on NumPy 2.5 or later. diff --git a/docs/dependencies.md b/docs/dependencies.md index 23f33fb..f691ff8 100644 --- a/docs/dependencies.md +++ b/docs/dependencies.md @@ -28,4 +28,16 @@ optional arguments: -f FMU, --file FMU Path to the Python FMU. -e ENVIRONMENT, --env ENVIRONMENT Requirements or environment file. -``` \ No newline at end of file +``` + +### Which Python environment does the FMU use? + +- **Python-based importer** (e.g. FMPy): the interpreter running the importer, with its + packages. Run `pythonfmu3 deploy` in that same environment. +- **Native importer on Linux/macOS** (OMSimulator, `fmusim`, Simulink, ...): the Python + whose `libpython` the FMU loads (see + [Running FMUs outside Python](install.md#running-fmus-outside-python-linux-and-macos)). + That is a base installation, not a virtual environment: packages installed only in a + venv are not found unless you add its `site-packages` folder to `PYTHONPATH`, e.g. + `export PYTHONPATH=/path/to/venv/lib/python3.12/site-packages`. +- **Windows**: the Python providing the `python3.dll` found on the `PATH`. diff --git a/docs/index.md b/docs/index.md index 5adf2f9..6100c88 100644 --- a/docs/index.md +++ b/docs/index.md @@ -85,12 +85,19 @@ import fmpy result = fmpy.simulate_fmu('MyModel.fmu', stop_time=10.0) ``` +The FMU also runs in native FMI 3.0 importers such as OMSimulator, `fmusim` or Simulink. +The simulating machine needs Python 3 with a shared Python library and your model's +packages; see [Running FMUs outside Python](install.md#running-fmus-outside-python-linux-and-macos). + ## Architecture PythonFMU3 uses a hybrid architecture combining the performance of C++ with the flexibility of Python: - **C++ Runtime**: High-performance FMI interface implementation - **Embedded Python**: Isolated Python interpreter for model execution +- **Python loader (Linux/macOS)**: the FMU's library has no link-time dependency on + Python. It uses the host's interpreter when there is one, and otherwise loads + `libpython` on first instantiation, so native (non-Python) importers can load the FMU - **Smart Bridging**: Efficient data exchange between C++ and Python - **Memory Management**: Automatic resource cleanup and garbage collection diff --git a/mkdocs.yml b/mkdocs.yml index dde0647..87ab0a0 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -5,4 +5,5 @@ nav: - Co-simulation: usage.md - Model Exchange: usageMX.md - Dependencies: dependencies.md + - Changelog: changelog.md theme: readthedocs \ No newline at end of file