FhSim  3.1.0
Marine systems simulation
Loading...
Searching...
No Matches
0221 — OS headers, OS calls and platform #ifdef outside one helper; macOS has no executable path
ID 0221
Class BUG
Severity 2
Status ready

Models: —

Progress: step 1 done in 53bd9ba0 (ThisExePath() macOS branch; ResolveFhSimPaths() calls it). DllWrapperCommon.h builds .dylib names (d0a7e559). Still open: steps 3-5, the other .so spellings (LicenseManager.cpp:266,287, FhsimSlaveCoreLifecycle.cpp:204, ResourceBundler.cpp:183-184, tests/fmu/FMITestCommon.h:163, tests/cmake/fmu_export.cmake:298, tests/CMakeLists.txt:486,511,585), and the macOS FMU test platform folders (darwin64, aarch64-darwin; moved here from FHSIM-0206).

Found: 2026-10-26 portability audit of da1b45b3

Evidence

Rule: doc/agents/portability.md ("Code"): OS headers, direct OS calls and inline platform #ifdef belong in one helper .cpp that implements all platforms. About 144 scan hits in 35 files fall outside such a helper. The two confirmed defects on macOS first.

Confirmed (BUG): the executable path has no macOS branch.

  • src/simobject/utilities/SystemUtils.cpp:87-106 ThisExePath() has a _WIN32 and a __linux__ branch, and #else is #error ThisExePath() is not implemented for the current platform yet. The file is listed unconditionally in src/simobject/utilities/CMakeLists.txt:7, so the simobject library does not compile with Apple Clang.
  • src/fmu/fhFMUexport/ResourceBundler.cpp:36-47 ResolveFhSimPaths() is a second, private copy: #ifdef WIN32 uses GetModuleFileName, the #else branch calls readlink("/proc/self/exe", ...). /proc/self/exe does not exist on macOS, so the call fails, it prints "Unable to resolve executable path" and returns an empty FhSimPaths. The FMU exporter (src/fmu/fhFMUexport/fhFMUExport.cpp:59) cannot find its install root there. It also includes <windows.h>/<unistd.h> itself (ResourceBundler.cpp:22-25).

Duplicate or inline platform code (ARCHITECTURE), by file (hits in brackets):

  • Second and third copies of the library loader (dlopen/LoadLibrary/GetProcAddress), each with its own <windows.h>/<dlfcn.h> includes: src/engine/io/PluginStateObserver.cpp (11), src/engine/license/LicenseManager.cpp (14), include/fhsim/license/LicenseManager.h (4), src/fmu/core/FhsimDllBridge.cpp (6) and FhsimDllBridge.h (4; FhsimDllBridge.h:20 defines LoadLibrary(path) as dlopen), tests/dll/DllAbi_test.cpp (8), tests/fmu/FMITestCommon.h (13), tests/io/install_consumer/ConsumerHost.cpp (10).
  • Public (installed) headers that include <windows.h>, <unistd.h> or <dlfcn.h>: include/fhsim/DllLib.h (4), include/fhsim/dll/DllEntry.h (5), include/fhsim/dll/DllWrapper.h (3), include/fhsim/visual/dll/VisDllEntry.h (5), include/fhsim/visual/dll/FhVisDllWrapper.h (2), include/fhsim/PrintDuringExec.h (2), include/fhsim/NFhSim.h:45 (PATH_DELIMITER), include/fhsim/IStateObserverPlugin.h:86.
  • Inline #ifdef _WIN32 around CRT or POSIX calls: src/simobject/utilities/PrintDuringExec.cpp (7), src/simobject/utilities/InputReader.cpp (4, incl. <sys/stat.h>), src/engine/core/Parse.cpp (2), src/engine/diagnostics/DiagnosticSink.cpp (3, isatty/_isatty), src/engine/integrator/FhIntegrator.cpp (2), src/engine/integrator/IntegratorData.cpp (3, getenv_s, fopen_s), src/engine/core/SimulationBuilder.cpp:41 (GetSystemInfo/sysconf), src/fmu/fhFMUexport/FmuGenerationTimestamp.cpp:13 (gmtime_s/gmtime_r), src/fmu/core/FhsimSlaveCoreLifecycle.cpp (1), src/visual/renderer/FhVisualization.cpp:105-109 and src/visual/ui/FhVisualizationSDL.cpp:386-390 (setenv, guarded by #ifndef _WIN32).
  • Test helpers: tests/fmu/FMITestHelper.h (5), tests/fmu/FMI3TestHelper.h (6; setenv/unsetenv of LD_LIBRARY_PATH), tests/fmu/partial_export/PartialExportLibrary.cpp:9 (export macro), tests/model/AddressSpaceLimit.h (2; see FHSIM-0222), tests/model/DenseJacobianLimit_test.cpp:145 and tests/core/fhUtilities/BatchPool_test.cpp:231 (guarded, tests skip or are absent elsewhere), tests/fmu/FmuArchiveWriter_test.cpp:15 (<unistd.h>; built under if(UNIX) only).

Effect

  • macOS (Apple Clang, arm64): simobject stops at the #error; if it were fixed alone, the FMU exporter still fails at run time.
  • Windows and Linux build today. The cost elsewhere is upkeep: four copies of the library loader and one more ThisExePath must each be fixed for a new platform or a Windows loader rule (ADR 0012 search path).

Possible fix

  1. Add the macOS branch to ThisExePath() (_NSGetExecutablePath, or std::filesystem::canonical of the result) in SystemUtils.cpp, and make ResourceBundler::ResolveFhSimPaths() call ThisExePath().
  2. Mark as helpers with portability-ok: <reason> (acceptable, not defects; they implement all platforms in one place): src/simobject/utilities/SystemUtils.cpp, src/engine/dll/common/DllWrapperCommon.h and src/engine/dll/wrapper/DllWrapper.cpp (15+1 hits), src/visual/dll/wrapper/FhVisDll.cpp (calls that helper), src/apps/fhPlot/BrowserLauncher.cpp (Windows, macOS and Linux/WSL branches), src/fmu/fhFMUexport/FmuBinaryLayout.cpp (Windows, Apple, other), src/visual/renderer/WindowSystem.cpp (all three), include/fhsim/testtools/NullLogFile.h. Compiler-only guards (_MSC_VER in src/engine/core/BatchPool.cpp, OgreDebugGuard.h, IModelVisualizationHooks.h) are not OS code.
  3. Route PluginStateObserver.cpp, LicenseManager.cpp, FhsimDllBridge.cpp and the test loaders through the one loader (DllWrapperCommon.h, or fhlib DynamicLib).
  4. Move the _WIN32 blocks in the lists above into one PlatformUtils.cpp (env, isatty, CPU count, gmtime, set-env) behind small functions, or replace them with standard calls (std::getenv, std::fopen, std::thread::hardware_concurrency).
  5. Public headers: keep <windows.h> out of installed headers where the include is only needed by an inline function.

Step 1 is the fix for the BUG; steps 3-5 are the architecture work and can be split.

Test that would prove it

  • A unit test that ThisExePath() equals std::filesystem::canonical(argv[0]) for the test binary (runs on all platforms).
  • A unit test that ResourceBundler::ResolveFhSimPaths().valid is true (tests/fmu/ResourceBundler_test.cpp:106 already calls it).
  • tools/portability_scan.py <worktree> <base> --all with no unmarked hit outside the helper files.

Risk

Step 1 is low. Steps 3-5 touch the plugin and licence loaders and public headers (contract C-list: header names other repos use), so each needs a Windows (MSVC) build before merge.