Summary
beckhoff_ads_hardware_interface calls AdsDevice::GetHandle(symbolName) and copies only the dereferenced uint32_t into ADSDataLayout::ads_handle. Because the returned AdsHandle (a unique_ptr<uint32_t, ResourceDeleter<uint32_t>>) is a temporary, its deleter runs at the end of the statement and issues SYM_RELEASEHND on the PLC. Every cached handle is therefore stale for the rest of the layout's lifetime.
For top-level GVL and MAIN scalars that other clients keep referenced, TwinCAT's handle table happens not to invalidate the slot when our SYM_RELEASEHND lands, and the stale uint32_t keeps working by accident. For symbols with no other holders, like struct members (GVL_X.struct.field) and array-of-struct elements (GVL_X.arr[i].field), the handle is invalidated on release. The next sum-up-read trips a worker-thread crash inside ros2_control_node.
Store the AdsHandle (for example in std::optional<AdsHandle&) on ADSDataLayout so the resource lives as long as the layout, while also caching the dereferenced uint32_t for the sum-request headers. Construct via emplace because AdsHandle is move-only (the deleter holds a const std::function, blocking copy-assignment).
Summary
beckhoff_ads_hardware_interface calls AdsDevice::GetHandle(symbolName) and copies only the dereferenced uint32_t into ADSDataLayout::ads_handle. Because the returned AdsHandle (a unique_ptr<uint32_t, ResourceDeleter<uint32_t>>) is a temporary, its deleter runs at the end of the statement and issues SYM_RELEASEHND on the PLC. Every cached handle is therefore stale for the rest of the layout's lifetime.
For top-level GVL and MAIN scalars that other clients keep referenced, TwinCAT's handle table happens not to invalidate the slot when our SYM_RELEASEHND lands, and the stale uint32_t keeps working by accident. For symbols with no other holders, like struct members (GVL_X.struct.field) and array-of-struct elements (GVL_X.arr[i].field), the handle is invalidated on release. The next sum-up-read trips a worker-thread crash inside ros2_control_node.
Store the AdsHandle (for example in std::optional<AdsHandle&) on ADSDataLayout so the resource lives as long as the layout, while also caching the dereferenced uint32_t for the sum-request headers. Construct via emplace because AdsHandle is move-only (the deleter holds a const std::function, blocking copy-assignment).