|
FhSim
3.1.0
Marine systems simulation
|
| ID | 0025 |
| Class | BUG |
| Severity | 2 |
| Status | ready |
| Models | — |
| Found | 2026-09-25, filed from fhsim_fishery FISH-0051 on the owner's decision |
| Decision needed | — (Karl-Johan Reite decided 2026-09-25 to file this as ready: resolve shared resources before InitialConditionSetup) |
ModelAssemblyService::Assemble (src/engine/model/ModelAssemblyService.cpp:59-62) runs the phases in this order:
CreateModelObjects constructs every SimObject; a provider such as Environment calls SetSharedResource from its constructor, which only stores the pointer in m_tempResourceMap (src/simobject/SimObjectOrganizer.cpp:503-517). SimObjectCreationComplete() (src/engine/model/ModelAssemblyWiring.cpp:330) ends the phase but publishes nothing.ResolveInitialConditions calls InitialConditionSetup on each object with an unset state (ModelAssemblyService.cpp:96, per object at src/engine/model/ModelAssemblyInitialConditions.cpp:176). That method may read input ports, so it evaluates other objects' output port functions.CompleteSimObjectSetup first calls SimObjectSetupComplete() (ModelAssemblyService.cpp:103), which is where m_tempResourceMap is copied into the SimManager (SimObjectOrganizer.cpp:47-54), and then each object's FinalSetup (ModelAssemblyService.cpp:106).GetSharedResource reads the SimManager (SimObjectOrganizer.cpp:519-531), so it returns null for every model resource until step 3. The header says as much (include/fhsim/simobject/ISimObjectCreator.h:307 and :317, "not accessible by other
simobjects until FinalSetup"), and FinalSetup is documented as the place to retrieve them (include/fhsim/simobject/SimObjectInclude.h:124-136). But the InitialConditionSetup contract (SimObjectInclude.h:69-122) lets that method read other objects' ports, and nothing tells the object owning the port that its resources are not there yet.
Two consequences seen downstream, in fhsim_fishery:
ToTrawlDoor::OutAngleOfAttack uses the environment it resolves in FinalSetup; a SimObject whose InitialConditionSetup reads Door.AngleOfAttack segfaulted the host (FISH-0051, proven with a test-only probe object). Auv::CalcOutput hit the same thing for real and is guarded with if (m_environment == nullptr) (FISH-0038). Both guards are local workarounds: the Auv guard invents a value, the door guard throws std::logic_error.SensorCtd::InitialConditionSetup calls GetSharedResource("Environment") to seed its lag states at the field values; the log says "CtdOrigin tried to access a shared resource
Environment which is unset or set to a null pointer" and the states open at 0 degC and 0 PSU instead.A model author has no phase in which a resource another object registered is usable before FinalSetup, while InitialConditionSetup of any object may call their port functions. A port that touches a resource crashes the host at model load, or needs a hand-written null guard that must pick between an invented value and an error. The failure depends on which objects have unset states and which ports they read, so it appears only in some inputs.
Resolve shared resources before InitialConditionSetup, in two parts:
m_tempResourceMap into the SimManager from SimObjectSetupComplete() to SimObjectCreationComplete() (SimObjectOrganizer.cpp:42-54). SetSharedResource already rejects any call outside the constructor phase (:505), so every resource exists at that point and publishing earlier changes no value, only when it becomes visible. This alone makes GetSharedResource work inside InitialConditionSetup, which fixes the SensorCtd case.Give every object a resolve hook before step 2. Add to SimObject (SimObjectInclude.h), with an empty default so no existing object changes:
Call it for every object (with NewSimObject set for each, as CompleteSimObjectSetup does) between WirePorts and ResolveInitialConditions in Assemble. Update the ISimObjectCreator.h:307/317 and FinalSetup comments to say resources are available from SharedResourceSetup on. FinalSetup keeps working for objects that do not move.
Part 1 without part 2 still leaves the port case to each object: the port has no creator to resolve lazily with, so it cannot fetch the resource on first use.
Follow-ups downstream, once this is released:
ToTrawlDoor, TrawlDoorBase and Auv from FinalSetup into SharedResourceSetup, then remove the m_environment == nullptr guard in Auv::CalcOutput (FISH-0038) and the throwing guard in ToTrawlDoor::OutAngleOfAttack (FISH-0051). Their proving tests stay and must still pass, now with a valid angle of attack instead of the exception. SensorCtd can drop its second lookup in InitialConditionSetup.A core test with two stub SimObjects in one model: a provider that calls SetSharedResource("Probe", &value) in its constructor and exposes an output port that dereferences a pointer it resolves in SharedResourceSetup, and a consumer with one unset state whose InitialConditionSetup reads that port and writes the value into its state. It must assemble, and the consumer's initial state must equal value. A second case has the consumer call GetSharedResource("Probe") directly in InitialConditionSetup and expects a non-null pointer. Today the first segfaults and the second gets null.
The fhsim_fishery proof is SimObject.TrawlDoor_AngleOfAttackReadBeforeFinalSetupFailsTheRun with its probe Probe/InitialConditionPortReader (tests/probe/). Note for writing the core test: an exception thrown during model assembly escapes fhsim::test::RunTest, because CreateSimulationManagerFromFile builds the model outside its try block, so it does not come back as a TestResult with threw == true; the FhSim executable catches it ("FhSim caught an exception: ...") and exits with status 1.
Low. Part 1 moves when already registered pointers become visible, not what they are; an object that today relies on GetSharedResource returning null before FinalSetup would change behaviour, and none is known (the only such reader seen, SensorCtd, wants the pointer). Part 2 is an empty virtual. The ordering must hold on every assembly path, including the Jacobian checker's rebuilds and the FMU build. ModelStructure::RenderInit (src/engine/model/ModelStructure.cpp:225-231) creates its own organizer and calls both SimObjectCreationComplete and SimObjectSetupComplete; with part 1 it would republish the (empty) temporary map one call earlier, which is harmless.