diff --git a/GOTCHAS.md b/GOTCHAS.md index 4fc73c1..d68d46c 100644 --- a/GOTCHAS.md +++ b/GOTCHAS.md @@ -60,6 +60,21 @@ - **Fix:** on-target code compares `HyphaIpIPv4Address_t` directly with a local `std::memcmp`/`==` helper (the struct is exactly `sizeof(uint32_t)`). +## 2026-08-09 — Bare-metal firmware link fails: `memmove` undefined (udpard uses it) + +- **Symptom:** M7 (nucleo-cyphal) link error `undefined reference to memmove` when adding the + GetInfo client, even though `memcpy`/`memset` link fine. Fails at link, not compile. +- **Root cause:** `udpardGather` (libudpard) calls standard `memmove` for overlap-safe copies, + but the bare-metal `modules/memory` only supplied `memset` + `memcpy` (both wrapped under + `#if defined(__arm__)` as `extern "C"`). +- **Fix:** added `memory::move` (overlap-safe: forward loop when `dst < src`, backward when + `dst > src`) plus a `void*` overload, and an `extern "C" void *memmove(...)` wrapper in + `modules/memory/source/memmove.cpp`. Registered only for cross builds in + `modules/memory/CMakeLists.txt` (same `$...>` gate as + memset/memcpy) so host tests don't collide with libc's memmove. +- **Gotcha:** the `memory::move` template must branch on `dst < src`/`dst > src` to remain + overlap-safe; a plain forward `copy` loop corrupts overlapping ranges. + ## 2026-08-08 — `on-host-native-gcc` preset cannot build on macOS/Darwin - `g++-13` (homebrew) does not provide `cstddef`/libc++ headers, so any `module-core` TU fails diff --git a/applications/nucleo-cyphal/CMakeLists.txt b/applications/nucleo-cyphal/CMakeLists.txt index dd5bd3f..dc1b16b 100644 --- a/applications/nucleo-cyphal/CMakeLists.txt +++ b/applications/nucleo-cyphal/CMakeLists.txt @@ -2,6 +2,7 @@ add_firmware(NAME nucleo-cyphal SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/source/GlobalContext.cpp ${CMAKE_CURRENT_SOURCE_DIR}/source/CyphalApp.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/source/GetInfoScanner.cpp ${CMAKE_CURRENT_SOURCE_DIR}/source/O1HeapPool.cpp INCLUDES ${CMAKE_CURRENT_SOURCE_DIR}/include diff --git a/applications/nucleo-cyphal/include/CyphalApp.hpp b/applications/nucleo-cyphal/include/CyphalApp.hpp index 4c13b2c..3afaa46 100644 --- a/applications/nucleo-cyphal/include/CyphalApp.hpp +++ b/applications/nucleo-cyphal/include/CyphalApp.hpp @@ -2,6 +2,7 @@ #define APP_CYPHAL_APP_HPP #include "BoardContext.hpp" +#include "GetInfoScanner.hpp" #include "core/Allocator.hpp" #include "jarnax/Loopable.hpp" #include "jarnax/Ticker.hpp" @@ -28,6 +29,10 @@ class CyphalApp final : public jarnax::Loopable, public jarnax::net::ethernet::D // Cyphal/UDP binds the node-ID to the last octet of the node's source IP. static constexpr UdpardNodeID NodeId = 103U; + // GetInfo client scan window (server node-IDs, inclusive). + static constexpr UdpardNodeID ScanFirstNode = 2U; + static constexpr UdpardNodeID ScanLastNode = 10U; + CyphalApp(jarnax::Ticker& ticker, jarnax::BoardContext& board_context); bool Execute() override; @@ -51,6 +56,8 @@ class CyphalApp final : public jarnax::Loopable, public jarnax::net::ethernet::D void ProcessTransmitQueue(); void ServiceDispatcherInit(); void ServiceResponseHandler(struct UdpardRxRPCTransfer const& transfer); + void GetInfoResponseHandler(struct UdpardRxRPCTransfer const& transfer); + void SendGetInfoRequest(UdpardNodeID server_node_id); static UdpardMicrosecond NowUs(jarnax::Ticker const& ticker); @@ -77,7 +84,9 @@ class CyphalApp final : public jarnax::Loopable, public jarnax::net::ethernet::D struct UdpardRxMemoryResources rx_memory_; struct UdpardRxRPCDispatcher service_dispatcher_; struct UdpardRxRPCPort get_info_service_port_; + struct UdpardRxRPCPort get_info_response_port_; HyphaIpIPv4Address_t service_group_address_; + GetInfoScanner get_info_scanner_; bool udpard_initialized_; bool arp_announced_; @@ -87,6 +96,7 @@ class CyphalApp final : public jarnax::Loopable, public jarnax::net::ethernet::D size_t stats_print_counter_; UdpardTransferID heartbeat_transfer_id_; jarnax::Ticks last_heartbeat_ticks_; + jarnax::Ticks last_getinfo_scan_ticks_; }; } // namespace cyphal diff --git a/applications/nucleo-cyphal/include/GetInfoScanner.hpp b/applications/nucleo-cyphal/include/GetInfoScanner.hpp new file mode 100644 index 0000000..98dbc6a --- /dev/null +++ b/applications/nucleo-cyphal/include/GetInfoScanner.hpp @@ -0,0 +1,81 @@ +#ifndef APP_CYPHAL_GET_INFO_SCANNER_HPP +#define APP_CYPHAL_GET_INFO_SCANNER_HPP + +#include +#include + +namespace nucleo { +namespace cyphal { + +/// Sequential scanner over a window of server node-IDs for uavcan.node.GetInfo client queries. +/// +/// The scanner owns: +/// - the scan window [first, last] (inclusive); +/// - a cursor advancing around the window so each Execute() scan tick queries the next node; +/// - a per-server-node transfer-ID counter (Cyphal requires a separate transfer-ID per +/// (service-ID, server node-ID) pair); +/// - the identity of the node with a query currently in flight. +/// +/// It has no dependency on the target, libudpard, or hypha, so it is fully host-testable. +class GetInfoScanner { +public: + static constexpr std::size_t MaxWindowSize = 32U; + + /// Constructs a scanner over [first_node, last_node] inclusive. + /// Both bounds must be in range and first_node <= last_node. + GetInfoScanner(std::uint16_t first_node, std::uint16_t last_node); + + /// Re-arms the scanner to query first_node next, with no pending query. + void Reset(); + + /// True when node lies within the scan window. + bool IsInRange(std::uint16_t node) const; + + /// The next node-ID to query without advancing the cursor. + std::uint16_t PeekNext() const; + + /// Returns the next node-ID to query and advances the cursor (wrapping at last). + std::uint16_t TakeNext(); + + /// True while a query is awaiting a response. + bool HasPending() const; + + /// The node-ID with a query in flight. + std::uint16_t PendingNode() const; + + /// Records that a request was just sent to node. + /// Precondition: IsInRange(node) and not HasPending(). + void SetPending(std::uint16_t node); + + /// Clears the in-flight query (response received or timed out). + void ClearPending(); + + /// Returns the current transfer-ID for node and increments it. + /// A fresh counter is created on first use for a node in the window. + std::uint64_t NextTransferId(std::uint16_t node); + + /// Number of distinct server node-IDs currently tracked for transfer-IDs. + std::size_t TidCount() const; + +private: + struct TidEntry { + std::uint16_t node_id; + std::uint64_t transfer_id; + }; + + TidEntry* FindTid(std::uint16_t node); + TidEntry const* FindTid(std::uint16_t node) const; + + std::uint16_t first_{0U}; + std::uint16_t last_{0U}; + std::uint16_t next_{0U}; + std::uint16_t pending_{0U}; + bool has_pending_{false}; + std::size_t tid_count_{0U}; + TidEntry tids_[MaxWindowSize]; +}; + +} // namespace cyphal +} // namespace nucleo + +#endif // APP_CYPHAL_GET_INFO_SCANNER_HPP \ No newline at end of file diff --git a/applications/nucleo-cyphal/source/CyphalApp.cpp b/applications/nucleo-cyphal/source/CyphalApp.cpp index eb26754..d237cda 100644 --- a/applications/nucleo-cyphal/source/CyphalApp.cpp +++ b/applications/nucleo-cyphal/source/CyphalApp.cpp @@ -1,5 +1,6 @@ #include "CyphalApp.hpp" +#include "GetInfoScanner.hpp" #include "O1HeapPool.hpp" #include "board.hpp" #include "core/Conversions.hpp" @@ -112,7 +113,9 @@ CyphalApp::CyphalApp(jarnax::Ticker& ticker, jarnax::BoardContext& board_context , rx_memory_{} , service_dispatcher_{} , get_info_service_port_{} + , get_info_response_port_{} , service_group_address_{} + , get_info_scanner_{ScanFirstNode, ScanLastNode} , udpard_initialized_{false} , arp_announced_{false} , initialized_{false} @@ -222,6 +225,18 @@ bool CyphalApp::Execute() { PublishHeartbeat(); } + // GetInfo client scan: query the next server node in the window each 5 seconds. + if ((current_ticks.value() - last_getinfo_scan_ticks_.value()) >= 5U * ticker_.GetTicksPerSecond().value()) { + last_getinfo_scan_ticks_ = current_ticks; + if (!get_info_scanner_.HasPending()) { + SendGetInfoRequest(get_info_scanner_.TakeNext()); + } else { + // A previously issued request never got a response; advance anyway. + get_info_scanner_.ClearPending(); + SendGetInfoRequest(get_info_scanner_.TakeNext()); + } + } + ++stats_print_counter_; if (stats_print_counter_ >= 200U) { stats_print_counter_ = 0U; @@ -331,9 +346,19 @@ void CyphalApp::ServiceDispatcherInit() { // The service request port for uavcan.node.GetInfo (request direction = is_request=true). int_fast8_t const listen = udpardRxRPCDispatcherListen(&service_dispatcher_, &get_info_service_port_, GetInfoServiceId, true, - uavcan_node_GetInfo_Response_1_0_EXTENT_BYTES_); + uavcan_node_GetInfo_Request_1_0_EXTENT_BYTES_); if (listen < 0) { - jarnax::print("CyphalApp: udpardRxRPCDispatcherListen failed with %d\r\n", static_cast(listen)); + jarnax::print("CyphalApp: udpardRxRPCDispatcherListen (request) failed with %d\r\n", static_cast(listen)); + return; + } + + // The service response port for uavcan.node.GetInfo (client direction = is_request=false). + int_fast8_t const listen_response = + udpardRxRPCDispatcherListen(&service_dispatcher_, &get_info_response_port_, GetInfoServiceId, false, + uavcan_node_GetInfo_Response_1_0_EXTENT_BYTES_); + if (listen_response < 0) { + jarnax::print("CyphalApp: udpardRxRPCDispatcherListen (response) failed with %d\r\n", + static_cast(listen_response)); return; } @@ -483,13 +508,18 @@ HyphaIpStatus_e CyphalApp::OnReceiveUdp(HyphaIpExternalContext_t context, HyphaI payload.size = payload_size; if (SameIPv4Address(metadata->destination_address, self->service_group_address_)) { - // Service multicast datagram (e.g. a GetInfo request addressed to this node). + // Service multicast datagram (e.g. a GetInfo request addressed to this node, or a + // GetInfo response addressed to us from a scanned server). if (self->service_initialized_) { struct UdpardRxRPCTransfer transfer{}; int_fast8_t const result = udpardRxRPCDispatcherReceive( &self->service_dispatcher_, NowUs(self->ticker_), payload, 0U, nullptr, &transfer); if (result > 0) { - self->ServiceResponseHandler(transfer); + if (transfer.is_request) { + self->ServiceResponseHandler(transfer); + } else { + self->GetInfoResponseHandler(transfer); + } udpardRxFragmentFree(transfer.base.payload, self->rx_memory_.fragment, self->rx_memory_.payload); } } else { @@ -584,6 +614,72 @@ void CyphalApp::ServiceResponseHandler(struct UdpardRxRPCTransfer const& transfe ); } +void CyphalApp::SendGetInfoRequest(UdpardNodeID server_node_id) { + // The GetInfo request is sealed; its serialized payload is always empty. + struct UdpardPayload const payload = { + .size = 0U, + .data = nullptr, + }; + + UdpardTransferID const transfer_id = get_info_scanner_.NextTransferId(server_node_id); + int32_t const result = udpardTxRequest(&tx_, NowUs(ticker_) + 1000000U, UdpardPriorityNominal, GetInfoServiceId, + server_node_id, transfer_id, payload, this); + if (result > 0) { + get_info_scanner_.SetPending(server_node_id); + } else { + // The request was not accepted (e.g. temporary TX capacity); try this node again next cycle. + get_info_scanner_.ClearPending(); + } + jarnax::print("CyphalApp: GetInfo request sent=%d to node %u tid=%llu\r\n", static_cast(result), + static_cast(static_cast(server_node_id)), + static_cast(transfer_id)); +} + +void CyphalApp::GetInfoResponseHandler(struct UdpardRxRPCTransfer const& transfer) { + // Only handle GetInfo *responses* for our service from a node we are scanning. + if (transfer.is_request || transfer.service_id != GetInfoServiceId) { + return; + } + if (!get_info_scanner_.HasPending() || transfer.base.source_node_id != get_info_scanner_.PendingNode()) { + return; + } + get_info_scanner_.ClearPending(); + + // The transfer payload may be fragmented; gather it into a contiguous buffer. + uint8_t buffer[uavcan_node_GetInfo_Response_1_0_EXTENT_BYTES_]{}; + size_t const payload_size = udpardGather(transfer.base.payload, sizeof(buffer), buffer); + if (payload_size == 0U) { + return; + } + + // Memset instead of the generated initialize_(): the response deserializer reads the + // full payload, and all GetInfo fields have zero-valued defaults. + uavcan_node_GetInfo_Response_1_0 info{}; + std::memset(&info, 0, sizeof(info)); + + size_t deserialized_size = payload_size; + int8_t const err = uavcan_node_GetInfo_Response_1_0_deserialize_(&info, buffer, &deserialized_size); + if (err < 0) { + jarnax::print("CyphalApp: GetInfo response deserialize failed with %d for node %u\r\n", static_cast(err), + static_cast(static_cast(transfer.base.source_node_id))); + return; + } + + // Print the node identity as reported by the server. The name is ASCII text but stored in + // a uint8_t array, so copy it into a char buffer for printing. + char const* const name = reinterpret_cast(info.name.elements); + jarnax::print("CyphalApp: GetInfo from node %u: proto=%u.%u hw=%u.%u sw=%u.%u vcs=%llu unique_id=", + static_cast(static_cast(transfer.base.source_node_id)), + static_cast(info.protocol_version.major), static_cast(info.protocol_version.minor), + static_cast(info.hardware_version.major), static_cast(info.hardware_version.minor), + static_cast(info.software_version.major), static_cast(info.software_version.minor), + static_cast(info.software_vcs_revision_id)); + for (size_t i = 0U; i < 16U; ++i) { + jarnax::print("%02X", static_cast(info.unique_id[i])); + } + jarnax::print(" name=%.*s\r\n", static_cast(info.name.count), name); +} + void CyphalApp::PublishHeartbeat() { uavcan_node_Heartbeat_1_0 heartbeat{}; uavcan_node_Heartbeat_1_0_initialize_(&heartbeat); diff --git a/applications/nucleo-cyphal/source/GetInfoScanner.cpp b/applications/nucleo-cyphal/source/GetInfoScanner.cpp new file mode 100644 index 0000000..9e75c05 --- /dev/null +++ b/applications/nucleo-cyphal/source/GetInfoScanner.cpp @@ -0,0 +1,99 @@ +#include "GetInfoScanner.hpp" + +#include + +namespace nucleo { +namespace cyphal { + +GetInfoScanner::GetInfoScanner(std::uint16_t first_node, std::uint16_t last_node) + : first_{first_node} + , last_{last_node} + , next_{first_node} + , pending_{0U} + , has_pending_{false} + , tid_count_{0U} + , tids_{} { + // (first_node, last_node) are guarded by the application configuration. + std::memset(tids_, 0, sizeof(tids_)); +} + +void GetInfoScanner::Reset() { + next_ = first_; + pending_ = 0U; + has_pending_ = false; +} + +bool GetInfoScanner::IsInRange(std::uint16_t node) const { + return (node >= first_) && (node <= last_); +} + +std::uint16_t GetInfoScanner::PeekNext() const { + return next_; +} + +std::uint16_t GetInfoScanner::TakeNext() { + std::uint16_t const node = next_; + next_ = (node == last_) ? first_ : static_cast(node + 1U); + return node; +} + +bool GetInfoScanner::HasPending() const { + return has_pending_; +} + +std::uint16_t GetInfoScanner::PendingNode() const { + return pending_; +} + +void GetInfoScanner::SetPending(std::uint16_t node) { + if (IsInRange(node) && !has_pending_) { + pending_ = node; + has_pending_ = true; + } +} + +void GetInfoScanner::ClearPending() { + pending_ = 0U; + has_pending_ = false; +} + +std::uint64_t GetInfoScanner::NextTransferId(std::uint16_t node) { + if (!IsInRange(node)) { + return 0U; + } + TidEntry* entry = FindTid(node); + if (entry == nullptr) { + if (tid_count_ >= MaxWindowSize) { + return 0U; + } + entry = &tids_[tid_count_++]; + entry->node_id = node; + entry->transfer_id = 0U; + } + return entry->transfer_id++; +} + +std::size_t GetInfoScanner::TidCount() const { + return tid_count_; +} + +GetInfoScanner::TidEntry* GetInfoScanner::FindTid(std::uint16_t node) { + for (std::size_t i = 0U; i < tid_count_; ++i) { + if (tids_[i].node_id == node) { + return &tids_[i]; + } + } + return nullptr; +} + +GetInfoScanner::TidEntry const* GetInfoScanner::FindTid(std::uint16_t node) const { + for (std::size_t i = 0U; i < tid_count_; ++i) { + if (tids_[i].node_id == node) { + return &tids_[i]; + } + } + return nullptr; +} + +} // namespace cyphal +} // namespace nucleo \ No newline at end of file diff --git a/applications/nucleo-cyphal/tests/CMakeLists.txt b/applications/nucleo-cyphal/tests/CMakeLists.txt index e1c4638..1b74203 100644 --- a/applications/nucleo-cyphal/tests/CMakeLists.txt +++ b/applications/nucleo-cyphal/tests/CMakeLists.txt @@ -17,3 +17,14 @@ host_unit_test(NAME cyphal-getinfo-server NO_CONFIGURATIONS NO_BOARDS ) + +host_unit_test(NAME cyphal-getinfo-client + SOURCES + ${CMAKE_CURRENT_SOURCE_DIR}/catch2-cyphal-getinfo-client.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../source/GetInfoScanner.cpp + INCLUDES + ${CMAKE_CURRENT_SOURCE_DIR}/../include + CATCH2 + NO_CONFIGURATIONS + NO_BOARDS +) diff --git a/applications/nucleo-cyphal/tests/catch2-cyphal-getinfo-client.cpp b/applications/nucleo-cyphal/tests/catch2-cyphal-getinfo-client.cpp new file mode 100644 index 0000000..dbdcf6c --- /dev/null +++ b/applications/nucleo-cyphal/tests/catch2-cyphal-getinfo-client.cpp @@ -0,0 +1,104 @@ +#define CATCH_CONFIG_MAIN +#include + +#include "GetInfoScanner.hpp" + +using nucleo::cyphal::GetInfoScanner; + +namespace { + +GetInfoScanner MakeDefaultScanner() { + return GetInfoScanner{2U, 10U}; +} + +} // namespace + +TEST_CASE("GetInfoScanner Empty case: fresh scanner has no pending query", "[cyphal][getinfo][scanner]") { + GetInfoScanner scanner = MakeDefaultScanner(); + REQUIRE_FALSE(scanner.HasPending()); + REQUIRE(scanner.PendingNode() == 0U); + REQUIRE(scanner.PeekNext() == 2U); + REQUIRE(scanner.TidCount() == 0U); +} + +TEST_CASE("GetInfoScanner TakeNext walks the window and wraps around", "[cyphal][getinfo][scanner]") { + GetInfoScanner scanner{4U, 6U}; + REQUIRE(scanner.TakeNext() == 4U); + REQUIRE(scanner.TakeNext() == 5U); + REQUIRE(scanner.TakeNext() == 6U); + REQUIRE(scanner.TakeNext() == 4U); // wraps back to first + REQUIRE(scanner.TakeNext() == 5U); +} + +TEST_CASE("GetInfoScanner single-node window always yields that node", "[cyphal][getinfo][scanner]") { + GetInfoScanner scanner{7U, 7U}; + REQUIRE(scanner.TakeNext() == 7U); + REQUIRE(scanner.TakeNext() == 7U); + REQUIRE(scanner.PeekNext() == 7U); +} + +TEST_CASE("GetInfoScanner IsInRange respects inclusive bounds", "[cyphal][getinfo][scanner]") { + GetInfoScanner scanner{2U, 10U}; + REQUIRE(scanner.IsInRange(2U)); + REQUIRE(scanner.IsInRange(10U)); + REQUIRE(scanner.IsInRange(7U)); + REQUIRE_FALSE(scanner.IsInRange(1U)); + REQUIRE_FALSE(scanner.IsInRange(11U)); + REQUIRE_FALSE(scanner.IsInRange(100U)); +} + +TEST_CASE("GetInfoScanner Reset re-arms cursor and clears pending", "[cyphal][getinfo][scanner]") { + GetInfoScanner scanner{2U, 5U}; + (void)scanner.TakeNext(); + (void)scanner.TakeNext(); + scanner.SetPending(3U); + REQUIRE(scanner.HasPending()); + scanner.Reset(); + REQUIRE_FALSE(scanner.HasPending()); + REQUIRE(scanner.PeekNext() == 2U); +} + +TEST_CASE("GetInfoScanner SetPending only accepts an in-range node", "[cyphal][getinfo][scanner]") { + GetInfoScanner scanner{2U, 10U}; + scanner.SetPending(3U); + REQUIRE(scanner.HasPending()); + REQUIRE(scanner.PendingNode() == 3U); + scanner.ClearPending(); + REQUIRE_FALSE(scanner.HasPending()); + + scanner.SetPending(42U); // outside window + REQUIRE_FALSE(scanner.HasPending()); + + scanner.SetPending(2U); + scanner.SetPending(9U); // already pending + REQUIRE(scanner.PendingNode() == 2U); +} + +TEST_CASE("GetInfoScanner NextTransferId tracks an independent counter per server node", "[cyphal][getinfo][scanner]") { + GetInfoScanner scanner{2U, 10U}; + REQUIRE(scanner.NextTransferId(2U) == 0U); + REQUIRE(scanner.NextTransferId(2U) == 1U); + REQUIRE(scanner.NextTransferId(3U) == 0U); // different node, fresh counter + REQUIRE(scanner.NextTransferId(2U) == 2U); // node 2 counter independent of node 3 + REQUIRE(scanner.NextTransferId(8U) == 0U); + REQUIRE(scanner.TidCount() == 3U); +} + +TEST_CASE("GetInfoScanner NextTransferId ignores out-of-window nodes", "[cyphal][getinfo][scanner]") { + GetInfoScanner scanner{2U, 10U}; + REQUIRE(scanner.NextTransferId(1U) == 0U); + REQUIRE(scanner.NextTransferId(200U) == 0U); + REQUIRE(scanner.TidCount() == 0U); +} + +TEST_CASE("GetInfoScanner full window never exceeds the tracking table", "[cyphal][getinfo][scanner]") { + GetInfoScanner scanner{1U, 254U}; // exceeds MaxWindowSize + std::uint64_t last_tid = 0U; + for (unsigned int i = 0U; i < GetInfoScanner::MaxWindowSize; ++i) { + last_tid = scanner.NextTransferId(static_cast(1U + i)); + } + REQUIRE(scanner.TidCount() == GetInfoScanner::MaxWindowSize); + // The next distinct node cannot be tracked anymore. + REQUIRE(scanner.NextTransferId(GetInfoScanner::MaxWindowSize + 1U) == 0U); + REQUIRE(last_tid == 0U); +} \ No newline at end of file diff --git a/modules/memory/CMakeLists.txt b/modules/memory/CMakeLists.txt index 91936cd..3752f8a 100644 --- a/modules/memory/CMakeLists.txt +++ b/modules/memory/CMakeLists.txt @@ -2,6 +2,7 @@ add_module(NAME memory SOURCES $,${CMAKE_CURRENT_SOURCE_DIR}/source/memset.cpp,> $,${CMAKE_CURRENT_SOURCE_DIR}/source/memcpy.cpp,> + $,${CMAKE_CURRENT_SOURCE_DIR}/source/memmove.cpp,> ${CMAKE_CURRENT_SOURCE_DIR}/source/fill.cpp ${CMAKE_CURRENT_SOURCE_DIR}/source/copy.cpp ${CMAKE_CURRENT_SOURCE_DIR}/source/compare.cpp diff --git a/modules/memory/include/memory.hpp b/modules/memory/include/memory.hpp index 9625222..ca46687 100644 --- a/modules/memory/include/memory.hpp +++ b/modules/memory/include/memory.hpp @@ -21,6 +21,13 @@ extern "C" void *memset(void *dst, int value, std::size_t bytes); /// @param bytes The number of bytes to copy. /// @return Returns dst. extern "C" void *memcpy(void *dst, void const *src, std::size_t bytes); + +/// @brief Standard C compatibility API for memory move (handles overlapping ranges). +/// @param dst The pointer to write the data to. +/// @param src The pointer to read the data from. +/// @param bytes The number of bytes to move. +/// @return Returns dst. +extern "C" void *memmove(void *dst, void const *src, std::size_t bytes); #endif /// The memory namespace @@ -52,6 +59,25 @@ void copy(UNIT_TYPE dst[], UNIT_TYPE const src[], std::size_t count) { } } +/// Moves a source array to destination array of elements, handling overlapping ranges. +/// @tparam UNIT_TYPE The unit type of the element. +/// @param dst The destination array of the elements. +/// @param src The source array of the elements. +/// @param count The count of the number of elements. +template +__attribute__((optimize("O2", "no-tree-loop-distribute-patterns"))) +void move(UNIT_TYPE dst[], UNIT_TYPE const src[], std::size_t count) { + if (dst < src) { + for (std::size_t i = 0; i < count; ++i) { + dst[i] = src[i]; + } + } else if (dst > src) { + for (std::size_t i = count; i > 0U; --i) { + dst[i - 1U] = src[i - 1U]; + } + } +} + /// Compares two array of the same type for specific count /// @tparam UNIT_TYPE The unit type of the element. /// @param dst The destination array of the elements. @@ -114,12 +140,29 @@ int compare(UNIT_TYPE (&lhs)[COUNT], UNIT_TYPE (&rhs)[COUNT]) { return 0; } +/// Moves a source array to destination array of elements, handling overlapping ranges. +/// @tparam UNIT_TYPE The unit type of the element. +/// @tparam COUNT The number of elements in the array. +/// @param dst The destination array of the elements. +/// @param src The source array of the elements. +template +__attribute__((optimize("O2", "no-tree-loop-distribute-patterns"))) +void move(UNIT_TYPE (&dst)[COUNT], UNIT_TYPE const (&src)[COUNT]) { + move(dst, src, COUNT); +} + /// Copies a source array to destination array of unknown types. /// @param dst The destination array of the elements. /// @param src The source array of the elements. /// @param bytes The number of bytes to copy. void copy(void *dst, void const *src, std::size_t bytes); +/// Moves a source array to destination array of unknown types, handling overlapping ranges. +/// @param dst The destination array of the elements. +/// @param src The source array of the elements. +/// @param bytes The number of bytes to move. +void move(void *dst, void const *src, std::size_t bytes); + /// Fills an array of an unknown type with a specific value. /// @param dst The destination array of the elements. /// @param value The byte value to assign to the destination. diff --git a/modules/memory/source/memmove.cpp b/modules/memory/source/memmove.cpp new file mode 100644 index 0000000..1ac8d75 --- /dev/null +++ b/modules/memory/source/memmove.cpp @@ -0,0 +1,16 @@ +#include "memory.hpp" + +namespace memory { + +void move(void *_dst, void const *_src, std::size_t bytes) { + std::uint8_t *dst = static_cast(_dst); + std::uint8_t const *src = static_cast(_src); + memory::move(dst, src, bytes); +} + +} // namespace memory + +extern void *memmove(void *_dst, void const *_src, std::size_t bytes) { + memory::move(_dst, _src, bytes); + return _dst; +} \ No newline at end of file diff --git a/modules/memory/tests/catch2-strings.cpp b/modules/memory/tests/catch2-strings.cpp index 801170d..07d02ae 100644 --- a/modules/memory/tests/catch2-strings.cpp +++ b/modules/memory/tests/catch2-strings.cpp @@ -1,3 +1,4 @@ +#include "memory.hpp" #include "strings.hpp" #include @@ -43,3 +44,44 @@ TEST_CASE("last_character at the end of the string", "[strings]") { char const* const found = last_character(text, '/'); REQUIRE(found == text + 3); } + +TEST_CASE("memory move copies forward when ranges do not overlap", "[memory][move]") { + std::uint8_t buffer[] = {0x01U, 0x02U, 0x03U, 0x04U, 0x05U}; + std::uint8_t src[] = {0x0AU, 0x0BU, 0x0CU}; + memory::move(buffer, src, 3U); + REQUIRE(buffer[0] == 0x0AU); + REQUIRE(buffer[1] == 0x0BU); + REQUIRE(buffer[2] == 0x0CU); + REQUIRE(buffer[3] == 0x04U); // untouched tail + REQUIRE(buffer[4] == 0x05U); +} + +TEST_CASE("memory move handles overlapping ranges (forward shift)", "[memory][move]") { + std::uint8_t buffer[] = {0x01U, 0x02U, 0x03U, 0x04U, 0x05U}; + // Shift the first three bytes one position to the right (overlap, dest > src). + memory::move(buffer + 1, buffer, 3U); + REQUIRE(buffer[0] == 0x01U); + REQUIRE(buffer[1] == 0x01U); + REQUIRE(buffer[2] == 0x02U); + REQUIRE(buffer[3] == 0x03U); + REQUIRE(buffer[4] == 0x05U); +} + +TEST_CASE("memory move handles overlapping ranges (backward shift)", "[memory][move]") { + std::uint8_t buffer[] = {0x01U, 0x02U, 0x03U, 0x04U, 0x05U}; + // Shift the last three bytes one position to the left (overlap, dest < src). + memory::move(buffer + 1, buffer + 2, 3U); + REQUIRE(buffer[0] == 0x01U); + REQUIRE(buffer[1] == 0x03U); + REQUIRE(buffer[2] == 0x04U); + REQUIRE(buffer[3] == 0x05U); + REQUIRE(buffer[4] == 0x05U); // untouched tail +} + +TEST_CASE("memory move of zero bytes does nothing", "[memory][move]") { + std::uint8_t buffer[] = {0x01U, 0x02U, 0x03U}; + memory::move(buffer, buffer, 0U); + REQUIRE(buffer[0] == 0x01U); + REQUIRE(buffer[1] == 0x02U); + REQUIRE(buffer[2] == 0x03U); +}