From b84a93bdfbeab111db748883af3aeb147a505789 Mon Sep 17 00:00:00 2001 From: Pavel Kirienko Date: Tue, 25 Jul 2023 14:46:41 +0300 Subject: [PATCH 01/38] Partially complete reassembler; does not build yet --- libudpard/udpard.c | 389 +++++++++++++++++++++++++++-- libudpard/udpard.h | 9 +- tests/.idea/dictionaries/pavel.xml | 1 + 3 files changed, 371 insertions(+), 28 deletions(-) diff --git a/libudpard/udpard.c b/libudpard/udpard.c index 3bfc7de..5ec92a8 100644 --- a/libudpard/udpard.c +++ b/libudpard/udpard.c @@ -99,6 +99,11 @@ static inline size_t larger(const size_t a, const size_t b) return (a > b) ? a : b; } +static inline uint32_t max32(const uint32_t a, const uint32_t b) +{ + return (a > b) ? a : b; +} + static inline bool isValidMemoryResource(const struct UdpardMemoryResource* const memory) { return (memory != NULL) && (memory->allocate != NULL) && (memory->free != NULL); @@ -330,7 +335,7 @@ static inline byte_t* txSerializeHeader(byte_t* const destination_buffe ptr = txSerializeU64(ptr, meta.transfer_id); UDPARD_ASSERT((frame_index + 0UL) <= HEADER_FRAME_INDEX_MAX); // +0UL is to avoid a compiler warning. ptr = txSerializeU32(ptr, frame_index | (end_of_transfer ? HEADER_FRAME_INDEX_EOT_MASK : 0U)); - ptr = txSerializeU16(ptr, 0); // opaque user data + ptr = txSerializeU16(ptr, 0); // opaque user data // Header CRC in the big endian format. Optimization prospect: the header up to frame_index is constant in // multi-frame transfers, so we don't really need to recompute the CRC from scratch per frame. const uint16_t crc = headerCRCCompute(HEADER_SIZE_BYTES - HEADER_CRC_SIZE_BYTES, destination_buffer); @@ -648,27 +653,13 @@ void udpardTxFree(struct UdpardMemoryResource* const memory, struct UdpardTxItem typedef struct { - TransferMetadata meta; - uint32_t index; - bool end_of_transfer; - struct UdpardConstPayload payload; ///< Also contains the transfer CRC (but not the header CRC). + TransferMetadata meta; + uint32_t index; + bool end_of_transfer; + struct UdpardConstPayload payload; ///< Also contains the transfer CRC (but not the header CRC). + struct UdpardMutablePayload origin; ///< The entirety of the free-able buffer passed from the application. } RxFrame; -typedef struct -{ - struct UdpardTreeNode base; - struct RxFragment* owner; // This is needed only to avoid pointer arithmetic. Ugly but safe. -} RxFragmentTreeNode; - -/// This is designed to be convertible to/from UdpardPayloadFragmentHandle, so that the application could be -/// given a linked list of these objects represented as a list of UdpardPayloadFragmentHandle. -typedef struct RxFragment -{ - struct UdpardPayloadFragmentHandle base; - RxFragmentTreeNode tree; - uint32_t frame_index; -} RxFragment; - /// The primitive deserialization functions are endian-agnostic. static inline const byte_t* txDeserializeU16(const byte_t* const source_buffer, uint16_t* const out_value) { @@ -708,10 +699,11 @@ static inline const byte_t* txDeserializeU64(const byte_t* const source_buffer, } /// This is roughly the inverse of the txSerializeHeader function, but it also handles the frame payload. -static inline bool rxParseFrame(const struct UdpardConstPayload datagram_payload, RxFrame* const out) +static inline bool rxParseFrame(const struct UdpardMutablePayload datagram_payload, RxFrame* const out) { UDPARD_ASSERT((out != NULL) && (datagram_payload.data != NULL)); - bool ok = false; + out->origin = datagram_payload; + bool ok = false; if (datagram_payload.size > 0) // HEADER_SIZE_BYTES may change in the future depending on the header version. { const byte_t* ptr = (const byte_t*) datagram_payload.data; @@ -741,9 +733,9 @@ static inline bool rxParseFrame(const struct UdpardConstPayload datagram_payload (out->payload.size > 0U)); } } + // Parsers for other header versions may be added here later. } - // Parsers for other header versions may be added here later. - if (ok) // Version-agnostic semantics check. + if (ok) // Version-agnostic semantics check. { UDPARD_ASSERT(out->payload.size > 0); // Follows from the prior checks. const bool anonymous = out->meta.src_node_id == UDPARD_NODE_ID_UNSET; @@ -755,6 +747,355 @@ static inline bool rxParseFrame(const struct UdpardConstPayload datagram_payload return ok; } +typedef struct +{ + struct UdpardTreeNode base; + struct RxFragment* this; // This is needed to avoid pointer arithmetic with multiple inheritance. +} RxFragmentTreeNode; + +/// This is designed to be convertible to/from UdpardPayloadFragmentHandle, so that the application could be +/// given a linked list of these objects represented as a list of UdpardPayloadFragmentHandle. +typedef struct RxFragment +{ + struct UdpardPayloadFragmentHandle base; + RxFragmentTreeNode tree; + uint32_t frame_index; +} RxFragment; + +/// A slot keeps the state of a transfer in the process of being reassembled. +/// We use two slots per iface which enables acceptance of frame-interleaved transfers. +/// An RX port has UDPARD_NETWORK_INTERFACE_COUNT_MAX ifaces, each iface has 2 slots. +/// Consider the following examples, where A and B denote distinct transfers of three frames each: +/// +/// A0 A1 A2 B0 B1 B2 -- two transfers without OOO frames; both accepted. +/// A2 A0 A1 B0 B2 B1 -- two transfers with OOO frames; both accepted. +/// A0 A1 B0 A2 B1 B2 -- two transfers with interleaved frames; both accepted (this is why we need 2 buffers). +/// +/// It is assumed that interleaved frames spanning more than two transfers are not possible. +/// +/// In this implementation we postpone the implicit truncation until all fragments of a transfer are received. +/// Early truncation, the way it is done in libcanard with contiguous payload reassembly buffers, +/// is much more difficult to implement if out-of-order reassembly is a requirement. +/// To implement early truncation with out-of-order reassembly, we need to deduce the MTU of the sender per transfer +/// (which is easy as we only need to take note of the payload size of any non-last frame of the transfer), +/// then, based on the MTU, determine the maximum frame index we should accept (higher indexes will be dropped); +/// then, for each fragment (i.e., frame) we need to compute the CRC (including those that are discarded). +/// At the end, when all frames have been observed, combine all CRCs to obtain the final transfer CRC +/// (this is possible because all common CRC functions are linear). +typedef struct +{ + UdpardMicrosecond ts_usec; ///< Timestamp of the earliest frame; UINT64_MAX initially. + UdpardTransferID transfer_id; + uint32_t max_index; ///< Maximum observed frame index in this transfer (so far); zero initially. + uint32_t eot_index; ///< Frame index where the end-of-transfer flag was observed; UINT32_MAX initially. + uint32_t accepted_frames; ///< Number of frames accepted so far. + RxFragmentTreeNode* fragments; +} RxSessionSlot; + +#define RX_SESSION_IFACE_SLOT_COUNT 2 +typedef struct +{ + UdpardMicrosecond ts_usec; ///< The timestamp of the last transfer to arrive on this interface. + RxSessionSlot slots[RX_SESSION_IFACE_SLOT_COUNT]; +} RxSessionIface; + +typedef struct UdpardInternalRxSession +{ + struct UdpardTreeNode base; + /// This shared state is used for redundant transfer deduplication. + /// Redundancies occur as a result of the use of multiple network interfaces, spurious frame duplication along + /// the network path, and trivial forward error correction through duplication (if used by the sender). + UdpardMicrosecond last_ts_usec; + UdpardTransferID last_transfer_id; + /// Each redundant interface maintains its own session state independently. + /// The first interface to receive a transfer takes precedence, thus the redundant group always operates + /// at the speed of the fastest interface. Duplicate transfers delivered by the slower interfaces are discarded. + RxSessionIface ifaces[UDPARD_NETWORK_INTERFACE_COUNT_MAX]; +} UdpardInternalRxSession; + +// NOLINTNEXTLINE(misc-no-recursion) +static inline void rxSessionSlotFree(RxFragment* const self, + struct UdpardMemoryResource* const memory_payload_fragment_handle, + struct UdpardMemoryResource* const memory_payload) +{ + UDPARD_ASSERT((self != NULL) && isValidMemoryResource(memory_payload_fragment_handle) && + (memory_payload->free != NULL)); // allocate() may be NULL in the payload mem resource. + memory_payload->free(memory_payload, self->base.origin.size, self->base.origin.data); + for (uint_fast8_t i = 0; i < 2; i++) + { + RxFragmentTreeNode* const child = (RxFragmentTreeNode*) self->tree.base.lr[i]; + if (child != NULL) + { + rxSessionSlotFree(child->this, memory_payload_fragment_handle, memory_payload); + } + } + memory_payload_fragment_handle->free(memory_payload_fragment_handle, sizeof(RxFragment), self); // self-destruct +} + +static inline void rxSessionSlotRestart(RxSessionSlot* const self, + const UdpardTransferID transfer_id, + struct UdpardMemoryResource* const memory_payload_fragment_handle, + struct UdpardMemoryResource* const memory_payload) +{ + UDPARD_ASSERT((self != NULL) && (memory_payload_fragment_handle != NULL) && (memory_payload != NULL)); + self->ts_usec = UINT64_MAX; // Will be assigned when the first frame of the transfer has arrived. + self->transfer_id = transfer_id; + self->max_index = 0; + self->eot_index = UINT32_MAX; + self->accepted_frames = 0; + if (self->fragments != NULL) + { + rxSessionSlotFree(self->fragments->this, memory_payload_fragment_handle, memory_payload); + self->fragments = NULL; + } +} + +typedef struct +{ + uint32_t frame_index; + bool accepted; + struct UdpardMemoryResource* memory_payload_fragment_handle; +} RxSessionSlotUpdateContext; + +static inline int8_t rxSessionSlotFragmentSearch(void* const user_reference, const struct UdpardTreeNode* node) +{ + UDPARD_ASSERT((user_reference != NULL) && (node != NULL)); + const RxSessionSlotUpdateContext* const ctx = (RxSessionSlotUpdateContext*) user_reference; + RxFragment* const frag = ((RxFragmentTreeNode*) node)->this; + UDPARD_ASSERT((ctx != NULL) && (frag != NULL)); + int8_t out = 0; + if (ctx->frame_index > frag->frame_index) + { + out = +1; + } + if (ctx->frame_index < frag->frame_index) + { + out = -1; + } + return out; +} + +static inline struct UdpardTreeNode* rxSessionSlotFragmentFactory(void* const user_reference) +{ + RxSessionSlotUpdateContext* const ctx = (RxSessionSlotUpdateContext*) user_reference; + UDPARD_ASSERT((ctx != NULL) && isValidMemoryResource(ctx->memory_payload_fragment_handle)); + struct UdpardTreeNode* out = NULL; + RxFragment* const frag = + ctx->memory_payload_fragment_handle->allocate(ctx->memory_payload_fragment_handle, sizeof(RxFragment)); + if (frag != NULL) + { + (void) memset(frag, 0, sizeof(RxFragment)); + out = &frag->tree.base; // this is not an escape bug, we retain the pointer via "this" + frag->frame_index = ctx->frame_index; + frag->tree.this = frag; // <-- right here, see? + ctx->accepted = true; + } + return out; // OOM handled by the caller +} + +/// States outliving each level of recursion while ejecting the transfer from the fragment tree. +typedef struct +{ + struct UdpardPayloadFragmentHandle* head; // Points to the first fragment in the list. + struct UdpardPayloadFragmentHandle* prev; + uint32_t crc; + size_t size; + size_t extent; + struct UdpardMemoryResource* memory_payload_fragment_handle; + struct UdpardMemoryResource* memory_payload; +} RxSessionSlotEjectContext; + +/// NOLINTNEXTLINE(misc-no-recursion) +static inline void rxSessionSlotEject(RxFragment* const frag, RxSessionSlotEjectContext* const ctx) +{ + UDPARD_ASSERT((frag != NULL) && (ctx != NULL)); + if (frag->tree.base.lr[0] != NULL) + { + rxSessionSlotEject(((RxFragmentTreeNode*) frag->tree.base.lr[0])->this, ctx); + } + ctx->size += frag->base.view.size; + ctx->crc = transferCRCAdd(ctx->crc, frag->base.view.size, frag->base.view.data); + ctx->head = (ctx->head == NULL) ? &frag->base : ctx->head; + // Transform the tree into a linked list. + if (ctx->prev != NULL) + { + ctx->prev->next = &frag->base; + } + ctx->prev = &frag->base; + // TODO truncate past the extent + // TODO drop CRC + // TODO free truncated fragments + if (frag->tree.base.lr[1] != NULL) + { + rxSessionSlotEject(((RxFragmentTreeNode*) frag->tree.base.lr[1])->this, ctx); + } +} + +/// This function will either move the frame payload into the session, or free it if it cannot be made use of. +/// Returns: 1 -- transfer available; 0 -- transfer not yet available; <0 -- error. +static inline int_fast8_t rxSessionSlotUpdate(RxSessionSlot* const self, + const RxFrame frame, + struct UdpardRxTransfer* const rx_transfer, + const size_t extent, + struct UdpardMemoryResource* const memory_payload_fragment_handle, + struct UdpardMemoryResource* const memory_payload) +{ + UDPARD_ASSERT((self != NULL) && (self->transfer_id == frame.meta.transfer_id) && (frame.payload.size > 0) && + (rx_transfer != NULL) && isValidMemoryResource(memory_payload_fragment_handle) && + (memory_payload->free != NULL)); + int_fast8_t result = 0; + bool restart = false; + bool release = true; + // FIRST: Update the frame count discovery state in this transfer. Drop transfer if inconsistencies are detected. + self->max_index = max32(self->max_index, frame.index); + if (frame.end_of_transfer) + { + if ((self->eot_index != UINT32_MAX) && (self->eot_index != frame.index)) + { + restart = true; // Inconsistent EOT flag, could be a node-ID conflict. + goto finish; + } + self->eot_index = frame.index; + } + UDPARD_ASSERT(frame.index <= self->max_index); + if (self->max_index > self->eot_index) + { + restart = true; // Frames past EOT, discard the entire transfer because we don't trust it. + goto finish; + } + // SECOND: Insert the fragment into the fragment tree. If it already exists, drop and free the duplicate. + UDPARD_ASSERT((self->max_index <= self->eot_index) && (self->accepted_frames < self->eot_index)); + RxSessionSlotUpdateContext update_ctx = {.frame_index = frame.index, + .accepted = false, + .memory_payload_fragment_handle = memory_payload_fragment_handle}; + RxFragmentTreeNode* const frag = (RxFragmentTreeNode*) cavlSearch((struct UdpardTreeNode**) &self->fragments, + &update_ctx, + &rxSessionSlotFragmentSearch, + &rxSessionSlotFragmentFactory); + UDPARD_ASSERT((self->max_index <= self->eot_index) && (self->accepted_frames <= self->eot_index)); + if (frag == NULL) + { + UDPARD_ASSERT(!update_ctx.accepted); + UDPARD_ASSERT(release); + result = -UDPARD_ERROR_MEMORY; + goto finish; // No reset because there is hope that there will be enough memory when we receive a duplicate. + } + if (update_ctx.accepted) + { + UDPARD_ASSERT(frag->this->frame_index == frame.index); + frag->this->base.view = frame.payload; + frag->this->base.origin = frame.origin; + release = false; // Ownership of the payload buffer has been transferred to the fragment tree. + } + // THIRD: Detect transfer completion. If complete, eject the payload from the fragment tree and check its CRC. + UDPARD_ASSERT(self->fragments != NULL); + if (self->accepted_frames == self->eot_index) + { + // This transfer is done but we don't know yet if it's valid. Either way, we need to prepare for the next one. + restart = true; + RxSessionSlotEjectContext eject_ctx = {.head = NULL, + .prev = NULL, + .crc = TRANSFER_CRC_INITIAL, + .size = 0, + .extent = extent, + .memory_payload_fragment_handle = memory_payload_fragment_handle, + .memory_payload = memory_payload}; + rxSessionSlotEject(self->fragments->this, &eject_ctx); + UDPARD_ASSERT(eject_ctx.head != NULL); + if (TRANSFER_CRC_RESIDUE_BEFORE_OUTPUT_XOR == eject_ctx.crc) + { + result = 1; + rx_transfer->timestamp_usec = self->ts_usec; + rx_transfer->priority = frame.meta.priority; + rx_transfer->source_node_id = frame.meta.src_node_id; + rx_transfer->transfer_id = frame.meta.transfer_id; + rx_transfer->payload_size = eject_ctx.size; + rx_transfer->payload = *eject_ctx.head; + // This is the single-frame transfer optimization suggested by Scott: we free the first fragment handle + // early by moving the contents into the rx_transfer structure by value. + // No need to free the payload buffer because it has been transferred to the transfer. + memory_payload_fragment_handle->free(memory_payload_fragment_handle, sizeof(RxFragment), eject_ctx.head); + // The fragmented transfer payload with its handles (aside from the 1st) has been moved from the slot + // into the rx_transfer. Now we invalidate the pointer to the tree to prevent it from being freed + // when the slot is restarted. + self->fragments = NULL; + } + } +finish: + if (restart) + { + rxSessionSlotRestart(self, frame.meta.transfer_id + 1, memory_payload_fragment_handle, memory_payload); + } + if (release) + { + memory_payload->free(memory_payload, frame.origin.size, frame.origin.data); + } + return result; +} + +/// Returns: 1 -- transfer available; 0 -- transfer not yet available; <0 -- error. +static inline int_fast8_t rxSessionIfaceUpdate(RxSessionIface* const self, + const UdpardMicrosecond ts_usec, + const RxFrame frame, + struct UdpardRxTransfer* const received_transfer, + const size_t extent, + const UdpardMicrosecond transfer_id_timeout_usec, + const struct UdpardRxMemoryResources memory) +{ + UDPARD_ASSERT((self != NULL) && (frame.payload.size > 0)); // Non-empty payload enforced during parsing. + RxSessionSlot* slot = NULL; + // First we should check if there is an existing slot for this transfer; if yes, this is the simplest case. + for (uint_fast8_t i = 0; i < RX_SESSION_IFACE_SLOT_COUNT; i++) + { + if (self->slots[i].transfer_id == frame.meta.transfer_id) + { + slot = &self->slots[i]; + break; + } + } + // If there is no suitable slot, we should check if the transfer is a future one (high transfer-ID), + // or a transfer-ID timeout has occurred. In this case we sacrifice the oldest slot. + if (slot == NULL) + { + bool is_future_tid = true; + RxSessionSlot* victim = &self->slots[0]; + // Check if the newly received transfer-ID is higher than any of the existing slots. + // While we're at it, find the oldest slot as a replacement candidate. + for (uint_fast8_t i = 0; i < RX_SESSION_IFACE_SLOT_COUNT; i++) + { + is_future_tid = is_future_tid && (frame.meta.transfer_id > self->slots[i].transfer_id); + if (self->slots[i].transfer_id < victim->transfer_id) + { + victim = &self->slots[i]; + } + } + const bool is_tid_timeout = (ts_usec - self->ts_usec) > transfer_id_timeout_usec; + if (is_tid_timeout || is_future_tid) + { + rxSessionSlotRestart(victim, frame.meta.transfer_id, memory.payload_fragment_handle, memory.payload); + slot = victim; + UDPARD_ASSERT(slot != NULL); + } + } + // If there is neither a suitable slot nor a new one was created, the frame should be discarded. + bool result = false; + if (slot != NULL) + { + if (slot->ts_usec == UINT64_MAX) + { + slot->ts_usec = ts_usec; // Transfer timestamp is the timestamp of the earliest frame. + } + // TODO RESET HANDLING + result = rxSessionSlotUpdate(slot, // + frame, + received_transfer, + extent, + memory.payload_fragment_handle, + memory.payload); + } + return result; +} + int8_t udpardRxSubscriptionInit(struct UdpardRxSubscription* const self, const UdpardPortID subject_id, const size_t extent, diff --git a/libudpard/udpard.h b/libudpard/udpard.h index 9d167bf..ca19b76 100644 --- a/libudpard/udpard.h +++ b/libudpard/udpard.h @@ -302,7 +302,7 @@ struct UdpardPayloadFragmentHandle /// This entity points to the base buffer that contains this fragment. /// The application can use this pointer to free the outer buffer after the payload has been consumed. /// In the most simple case this field is identical to the "view" field above, but it is not always the case. - struct UdpardMutablePayload owner; + struct UdpardMutablePayload origin; }; /// Cyphal/UDP uses only multicast traffic. @@ -822,7 +822,8 @@ void udpardRxSubscriptionDestroy(struct UdpardRxSubscription* const self); /// /// The function takes ownership of the passed datagram payload buffer. The library will either store it as a /// fragment of the reassembled transfer payload or free it using the corresponding memory resource -/// (see UdpardRxMemoryResources) if the datagram is not needed for reassembly. +/// (see UdpardRxMemoryResources) if the datagram is not needed for reassembly. Because of the ownership transfer, +/// the datagram payload buffer has to be mutable (non-const). /// /// The accepted datagram may either be invalid, carry a non-final part of a multi-frame transfer, /// carry a final part of a valid multi-frame transfer, or carry a valid single-frame transfer. @@ -855,7 +856,7 @@ void udpardRxSubscriptionDestroy(struct UdpardRxSubscription* const self); /// UDPARD_ERROR_ARGUMENT is returned if any of the input arguments are invalid. int8_t udpardRxSubscriptionReceive(struct UdpardRxSubscription* const self, const UdpardMicrosecond timestamp_usec, - const struct UdpardConstPayload datagram_payload, + const struct UdpardMutablePayload datagram_payload, const uint_fast8_t redundant_iface_index, struct UdpardRxTransfer* const received_transfer); @@ -991,7 +992,7 @@ int8_t udpardRxRPCDispatcherCancel(struct UdpardRxRPCDispatcher* const self, int8_t udpardRxRPCDispatcherReceive(struct UdpardRxRPCDispatcher* const self, struct UdpardRxRPC** const service, const UdpardMicrosecond timestamp_usec, - const struct UdpardConstPayload datagram_payload, + const struct UdpardMutablePayload datagram_payload, const uint_fast8_t redundant_iface_index, struct UdpardRxRPCTransfer* const received_transfer); diff --git a/tests/.idea/dictionaries/pavel.xml b/tests/.idea/dictionaries/pavel.xml index 2c334dd..f0b924d 100644 --- a/tests/.idea/dictionaries/pavel.xml +++ b/tests/.idea/dictionaries/pavel.xml @@ -8,6 +8,7 @@ dscp dudpard ghcr + ifaces intravehicular libcanard libgtest From 5dda22676a03fa032063479267e671cdc8871377 Mon Sep 17 00:00:00 2001 From: Pavel Kirienko Date: Tue, 25 Jul 2023 16:14:27 +0300 Subject: [PATCH 02/38] The first rough approximation of the reassembler is done; now we need to test it --- libudpard/udpard.c | 192 +++++++++++++++++++++++++++++++-------------- libudpard/udpard.h | 2 + 2 files changed, 133 insertions(+), 61 deletions(-) diff --git a/libudpard/udpard.c b/libudpard/udpard.c index 5ec92a8..f81107e 100644 --- a/libudpard/udpard.c +++ b/libudpard/udpard.c @@ -109,6 +109,23 @@ static inline bool isValidMemoryResource(const struct UdpardMemoryResource* cons return (memory != NULL) && (memory->allocate != NULL) && (memory->free != NULL); } +static inline void* memAlloc(struct UdpardMemoryResource* const memory, const size_t size) +{ + UDPARD_ASSERT((memory != NULL) && (memory->allocate != NULL)); + return memory->allocate(memory, size); +} + +/// Free as in freedom. +static inline void memFree(struct UdpardMemoryResource* const memory, const size_t size, void* const data) +{ + UDPARD_ASSERT((memory != NULL) && (memory->free != NULL)); // allocate() may be NULL in the payload mem resource. + memory->free(memory, size, data); +} +static inline void memFreePayload(struct UdpardMemoryResource* const memory, const struct UdpardMutablePayload payload) +{ + memFree(memory, payload.size, payload.data); +} + // --------------------------------------------- HEADER CRC --------------------------------------------- #define HEADER_CRC_INITIAL 0xFFFFU @@ -258,7 +275,7 @@ static inline TxItem* txNewItem(struct UdpardMemoryResource* const memory, void* const user_transfer_reference) { UDPARD_ASSERT(memory != NULL); - TxItem* const out = (TxItem*) memory->allocate(memory, sizeof(TxItem) + datagram_payload_size); + TxItem* const out = (TxItem*) memAlloc(memory, sizeof(TxItem) + datagram_payload_size); if (out != NULL) { // No tree linkage by default. @@ -475,7 +492,7 @@ static inline int32_t txPush(struct UdpardTx* const tx, while (head != NULL) { struct UdpardTxItem* const next = head->next_in_transfer; - tx->memory->free(tx->memory, sizeof(TxItem) + head->datagram_payload.size, head); + memFree(tx->memory, sizeof(TxItem) + head->datagram_payload.size, head); head = next; } } @@ -643,7 +660,7 @@ void udpardTxFree(struct UdpardMemoryResource* const memory, struct UdpardTxItem { if ((memory != NULL) && (item != NULL)) { - memory->free(memory, sizeof(TxItem) + item->datagram_payload.size, item); + memFree(memory, sizeof(TxItem) + item->datagram_payload.size, item); } } @@ -789,6 +806,7 @@ typedef struct uint32_t max_index; ///< Maximum observed frame index in this transfer (so far); zero initially. uint32_t eot_index; ///< Frame index where the end-of-transfer flag was observed; UINT32_MAX initially. uint32_t accepted_frames; ///< Number of frames accepted so far. + size_t payload_size; RxFragmentTreeNode* fragments; } RxSessionSlot; @@ -818,9 +836,8 @@ static inline void rxSessionSlotFree(RxFragment* const self, struct UdpardMemoryResource* const memory_payload_fragment_handle, struct UdpardMemoryResource* const memory_payload) { - UDPARD_ASSERT((self != NULL) && isValidMemoryResource(memory_payload_fragment_handle) && - (memory_payload->free != NULL)); // allocate() may be NULL in the payload mem resource. - memory_payload->free(memory_payload, self->base.origin.size, self->base.origin.data); + UDPARD_ASSERT((self != NULL) && isValidMemoryResource(memory_payload_fragment_handle)); + memFreePayload(memory_payload, self->base.origin); for (uint_fast8_t i = 0; i < 2; i++) { RxFragmentTreeNode* const child = (RxFragmentTreeNode*) self->tree.base.lr[i]; @@ -829,7 +846,7 @@ static inline void rxSessionSlotFree(RxFragment* const self, rxSessionSlotFree(child->this, memory_payload_fragment_handle, memory_payload); } } - memory_payload_fragment_handle->free(memory_payload_fragment_handle, sizeof(RxFragment), self); // self-destruct + memFree(memory_payload_fragment_handle, sizeof(RxFragment), self); // self-destruct } static inline void rxSessionSlotRestart(RxSessionSlot* const self, @@ -843,6 +860,7 @@ static inline void rxSessionSlotRestart(RxSessionSlot* const self, self->max_index = 0; self->eot_index = UINT32_MAX; self->accepted_frames = 0; + self->payload_size = 0; if (self->fragments != NULL) { rxSessionSlotFree(self->fragments->this, memory_payload_fragment_handle, memory_payload); @@ -879,9 +897,8 @@ static inline struct UdpardTreeNode* rxSessionSlotFragmentFactory(void* const us { RxSessionSlotUpdateContext* const ctx = (RxSessionSlotUpdateContext*) user_reference; UDPARD_ASSERT((ctx != NULL) && isValidMemoryResource(ctx->memory_payload_fragment_handle)); - struct UdpardTreeNode* out = NULL; - RxFragment* const frag = - ctx->memory_payload_fragment_handle->allocate(ctx->memory_payload_fragment_handle, sizeof(RxFragment)); + struct UdpardTreeNode* out = NULL; + RxFragment* const frag = memAlloc(ctx->memory_payload_fragment_handle, sizeof(RxFragment)); if (frag != NULL) { (void) memset(frag, 0, sizeof(RxFragment)); @@ -897,38 +914,71 @@ static inline struct UdpardTreeNode* rxSessionSlotFragmentFactory(void* const us typedef struct { struct UdpardPayloadFragmentHandle* head; // Points to the first fragment in the list. - struct UdpardPayloadFragmentHandle* prev; + struct UdpardPayloadFragmentHandle* predecessor; uint32_t crc; - size_t size; - size_t extent; + size_t retain_size; + size_t offset; struct UdpardMemoryResource* memory_payload_fragment_handle; struct UdpardMemoryResource* memory_payload; } RxSessionSlotEjectContext; +/// This function finalizes the fragmented transfer payload by doing multiple things in one pass through the tree: +/// +/// - Compute the transfer-CRC. The caller should verify the result. +/// - Build a linked list of fragments ordered by frame index, as the application would expect it. +/// - Truncate the payload according to the specified size limit. +/// - Free the tree nodes and their payload buffers past the size limit. +/// +/// It is guaranteed that the output list is sorted by frame index and contains at least one fragment. +/// The payload of the first fragment may be empty. +/// After this function is invoked, the tree will be destroyed and cannot be used anymore; +/// hence, in the event of invalid transfer being received (bad CRC), the fragments will have to be freed +/// by traversing the linked list instead of the tree. /// NOLINTNEXTLINE(misc-no-recursion) static inline void rxSessionSlotEject(RxFragment* const frag, RxSessionSlotEjectContext* const ctx) { - UDPARD_ASSERT((frag != NULL) && (ctx != NULL)); + UDPARD_ASSERT((frag != NULL) && (ctx != NULL) && isValidMemoryResource(ctx->memory_payload_fragment_handle)); if (frag->tree.base.lr[0] != NULL) { rxSessionSlotEject(((RxFragmentTreeNode*) frag->tree.base.lr[0])->this, ctx); } - ctx->size += frag->base.view.size; - ctx->crc = transferCRCAdd(ctx->crc, frag->base.view.size, frag->base.view.data); - ctx->head = (ctx->head == NULL) ? &frag->base : ctx->head; - // Transform the tree into a linked list. - if (ctx->prev != NULL) + const size_t fragment_size = frag->base.view.size; + const bool first = ctx->head == NULL; + frag->base.next = NULL; // Default state; may be overwritten. + ctx->crc = transferCRCAdd(ctx->crc, fragment_size, frag->base.view.data); + ctx->head = first ? &frag->base : ctx->head; + // Truncate unnecessary payload past the specified limit. This enforces the extent and removes the transfer CRC. + const bool retain = (ctx->offset < ctx->retain_size) || first; + if (retain) { - ctx->prev->next = &frag->base; + UDPARD_ASSERT(ctx->retain_size >= ctx->offset); + frag->base.view.size = smaller(frag->base.view.size, ctx->retain_size - ctx->offset); + if (ctx->predecessor != NULL) + { + ctx->predecessor->next = &frag->base; + } + ctx->predecessor = &frag->base; } - ctx->prev = &frag->base; - // TODO truncate past the extent - // TODO drop CRC - // TODO free truncated fragments + // Adjust the offset of the next fragment and descend into it. Keep the sub-tree alive for now even if not needed. + ctx->offset += fragment_size; if (frag->tree.base.lr[1] != NULL) { rxSessionSlotEject(((RxFragmentTreeNode*) frag->tree.base.lr[1])->this, ctx); } + (void) memset(&frag->tree.base, 0, sizeof(struct UdpardTreeNode)); // Avoid dangling pointers. + // Drop the unneeded fragments and their handles after the sub-tree is fully traversed. + if ((!retain) || (frag->base.view.size == 0)) + { + memFreePayload(ctx->memory_payload, frag->base.origin); + frag->base.origin.data = NULL; + frag->base.origin.size = 0; + frag->base.view.data = NULL; + UDPARD_ASSERT((frag->base.view.size == 0) || (!retain)); + } + if (!retain) + { + memFree(ctx->memory_payload_fragment_handle, sizeof(RxFragment), frag); + } } /// This function will either move the frame payload into the session, or free it if it cannot be made use of. @@ -941,8 +991,7 @@ static inline int_fast8_t rxSessionSlotUpdate(RxSessionSlot* const struct UdpardMemoryResource* const memory_payload) { UDPARD_ASSERT((self != NULL) && (self->transfer_id == frame.meta.transfer_id) && (frame.payload.size > 0) && - (rx_transfer != NULL) && isValidMemoryResource(memory_payload_fragment_handle) && - (memory_payload->free != NULL)); + (rx_transfer != NULL) && isValidMemoryResource(memory_payload_fragment_handle)); int_fast8_t result = 0; bool restart = false; bool release = true; @@ -978,47 +1027,63 @@ static inline int_fast8_t rxSessionSlotUpdate(RxSessionSlot* const UDPARD_ASSERT(!update_ctx.accepted); UDPARD_ASSERT(release); result = -UDPARD_ERROR_MEMORY; - goto finish; // No reset because there is hope that there will be enough memory when we receive a duplicate. + // No restart because there is hope that there will be enough memory when we receive a duplicate. + goto finish; } if (update_ctx.accepted) { UDPARD_ASSERT(frag->this->frame_index == frame.index); frag->this->base.view = frame.payload; frag->this->base.origin = frame.origin; - release = false; // Ownership of the payload buffer has been transferred to the fragment tree. + self->payload_size += frame.payload.size; + release = false; // Ownership of the payload buffer has been transferred to the fragment tree. } // THIRD: Detect transfer completion. If complete, eject the payload from the fragment tree and check its CRC. UDPARD_ASSERT(self->fragments != NULL); if (self->accepted_frames == self->eot_index) { // This transfer is done but we don't know yet if it's valid. Either way, we need to prepare for the next one. - restart = true; - RxSessionSlotEjectContext eject_ctx = {.head = NULL, - .prev = NULL, - .crc = TRANSFER_CRC_INITIAL, - .size = 0, - .extent = extent, - .memory_payload_fragment_handle = memory_payload_fragment_handle, - .memory_payload = memory_payload}; - rxSessionSlotEject(self->fragments->this, &eject_ctx); - UDPARD_ASSERT(eject_ctx.head != NULL); - if (TRANSFER_CRC_RESIDUE_BEFORE_OUTPUT_XOR == eject_ctx.crc) + restart = true; + if (self->payload_size >= TRANSFER_CRC_SIZE_BYTES) { - result = 1; - rx_transfer->timestamp_usec = self->ts_usec; - rx_transfer->priority = frame.meta.priority; - rx_transfer->source_node_id = frame.meta.src_node_id; - rx_transfer->transfer_id = frame.meta.transfer_id; - rx_transfer->payload_size = eject_ctx.size; - rx_transfer->payload = *eject_ctx.head; - // This is the single-frame transfer optimization suggested by Scott: we free the first fragment handle - // early by moving the contents into the rx_transfer structure by value. - // No need to free the payload buffer because it has been transferred to the transfer. - memory_payload_fragment_handle->free(memory_payload_fragment_handle, sizeof(RxFragment), eject_ctx.head); - // The fragmented transfer payload with its handles (aside from the 1st) has been moved from the slot - // into the rx_transfer. Now we invalidate the pointer to the tree to prevent it from being freed - // when the slot is restarted. - self->fragments = NULL; + RxSessionSlotEjectContext eject_ctx = { + .head = NULL, + .predecessor = NULL, + .crc = TRANSFER_CRC_INITIAL, + .retain_size = smaller(self->payload_size - TRANSFER_CRC_SIZE_BYTES, extent), + .offset = 0, + .memory_payload_fragment_handle = memory_payload_fragment_handle, + .memory_payload = memory_payload, + }; + rxSessionSlotEject(self->fragments->this, &eject_ctx); + self->fragments = NULL; // Ejection invalidates the tree. + UDPARD_ASSERT(eject_ctx.offset == self->payload_size); + UDPARD_ASSERT(eject_ctx.head != NULL); + if (TRANSFER_CRC_RESIDUE_BEFORE_OUTPUT_XOR == eject_ctx.crc) + { + result = 1; + rx_transfer->timestamp_usec = self->ts_usec; + rx_transfer->priority = frame.meta.priority; + rx_transfer->source_node_id = frame.meta.src_node_id; + rx_transfer->transfer_id = frame.meta.transfer_id; + rx_transfer->payload_size = eject_ctx.retain_size; + rx_transfer->payload = *eject_ctx.head; // Slice. The derived type fields are not needed. + // This is the single-frame transfer optimization suggested by Scott: we free the first fragment handle + // early by moving the contents into the rx_transfer structure by value. + // No need to free the payload buffer because it has been transferred to the transfer. + memFree(memory_payload_fragment_handle, sizeof(RxFragment), eject_ctx.head); + } + else // The transfer turned out to be invalid. We have to free the fragments. Can't use the tree anymore. + { + struct UdpardPayloadFragmentHandle* handle = eject_ctx.head; + while (handle != NULL) + { + struct UdpardPayloadFragmentHandle* const next = handle->next; + memFreePayload(memory_payload, handle->origin); + memFree(memory_payload_fragment_handle, sizeof(RxFragment), handle); + handle = next; + } + } } } finish: @@ -1028,7 +1093,7 @@ static inline int_fast8_t rxSessionSlotUpdate(RxSessionSlot* const } if (release) { - memory_payload->free(memory_payload, frame.origin.size, frame.origin.data); + memFreePayload(memory_payload, frame.origin); } return result; } @@ -1042,7 +1107,7 @@ static inline int_fast8_t rxSessionIfaceUpdate(RxSessionIface* const const UdpardMicrosecond transfer_id_timeout_usec, const struct UdpardRxMemoryResources memory) { - UDPARD_ASSERT((self != NULL) && (frame.payload.size > 0)); // Non-empty payload enforced during parsing. + UDPARD_ASSERT((self != NULL) && (frame.payload.size > 0) && (received_transfer != NULL)); RxSessionSlot* slot = NULL; // First we should check if there is an existing slot for this transfer; if yes, this is the simplest case. for (uint_fast8_t i = 0; i < RX_SESSION_IFACE_SLOT_COUNT; i++) @@ -1077,22 +1142,26 @@ static inline int_fast8_t rxSessionIfaceUpdate(RxSessionIface* const UDPARD_ASSERT(slot != NULL); } } - // If there is neither a suitable slot nor a new one was created, the frame should be discarded. - bool result = false; + // If there is a suitable slot (perhaps a newly created one for this frame), update it. + // If there is neither a suitable slot nor a new one was created, the frame cannot be used. + int_fast8_t result = 0; if (slot != NULL) { if (slot->ts_usec == UINT64_MAX) { - slot->ts_usec = ts_usec; // Transfer timestamp is the timestamp of the earliest frame. + slot->ts_usec = ts_usec; // Transfer timestamp is the timestamp of the earliest frame. } - // TODO RESET HANDLING - result = rxSessionSlotUpdate(slot, // + result = rxSessionSlotUpdate(slot, // Takes ownership of the frame payload; may free or keep it. frame, received_transfer, extent, memory.payload_fragment_handle, memory.payload); } + else + { + memFreePayload(memory.payload, frame.origin); + } return result; } @@ -1106,5 +1175,6 @@ int8_t udpardRxSubscriptionInit(struct UdpardRxSubscription* const self, (void) extent; (void) memory; (void) rxParseFrame; + (void) rxSessionIfaceUpdate; return 0; } diff --git a/libudpard/udpard.h b/libudpard/udpard.h index ca19b76..238b26e 100644 --- a/libudpard/udpard.h +++ b/libudpard/udpard.h @@ -852,6 +852,8 @@ void udpardRxSubscriptionDestroy(struct UdpardRxSubscription* const self); /// The time complexity is O(log n) where n is the number of remote notes publishing on this subject (topic). /// No data copy takes place. Malformed frames are discarded in constant time. /// +/// This function performs log(n) of recursive calls internally, where n is the number of frames in a transfer. +/// /// UDPARD_ERROR_MEMORY is returned if the function fails to allocate memory. /// UDPARD_ERROR_ARGUMENT is returned if any of the input arguments are invalid. int8_t udpardRxSubscriptionReceive(struct UdpardRxSubscription* const self, From aa8a41709afd5bdbfab0a1d1009081084fbf5dfe Mon Sep 17 00:00:00 2001 From: Pavel Kirienko Date: Tue, 25 Jul 2023 16:17:36 +0300 Subject: [PATCH 03/38] Should build now I suppose --- libudpard/udpard.c | 12 +++-- tests/src/test_intrusive_rx.c | 89 +++++++++++++++++------------------ 2 files changed, 51 insertions(+), 50 deletions(-) diff --git a/libudpard/udpard.c b/libudpard/udpard.c index f81107e..9a1a900 100644 --- a/libudpard/udpard.c +++ b/libudpard/udpard.c @@ -352,7 +352,7 @@ static inline byte_t* txSerializeHeader(byte_t* const destination_buffe ptr = txSerializeU64(ptr, meta.transfer_id); UDPARD_ASSERT((frame_index + 0UL) <= HEADER_FRAME_INDEX_MAX); // +0UL is to avoid a compiler warning. ptr = txSerializeU32(ptr, frame_index | (end_of_transfer ? HEADER_FRAME_INDEX_EOT_MASK : 0U)); - ptr = txSerializeU16(ptr, 0); // opaque user data + ptr = txSerializeU16(ptr, 0); // opaque user data // Header CRC in the big endian format. Optimization prospect: the header up to frame_index is constant in // multi-frame transfers, so we don't really need to recompute the CRC from scratch per frame. const uint16_t crc = headerCRCCompute(HEADER_SIZE_BYTES - HEADER_CRC_SIZE_BYTES, destination_buffer); @@ -752,7 +752,7 @@ static inline bool rxParseFrame(const struct UdpardMutablePayload datagram_paylo } // Parsers for other header versions may be added here later. } - if (ok) // Version-agnostic semantics check. + if (ok) // Version-agnostic semantics check. { UDPARD_ASSERT(out->payload.size > 0); // Follows from the prior checks. const bool anonymous = out->meta.src_node_id == UDPARD_NODE_ID_UNSET; @@ -801,7 +801,7 @@ typedef struct RxFragment /// (this is possible because all common CRC functions are linear). typedef struct { - UdpardMicrosecond ts_usec; ///< Timestamp of the earliest frame; UINT64_MAX initially. + UdpardMicrosecond ts_usec; ///< Timestamp of the earliest frame; UINT64_MAX initially. UdpardTransferID transfer_id; uint32_t max_index; ///< Maximum observed frame index in this transfer (so far); zero initially. uint32_t eot_index; ///< Frame index where the end-of-transfer flag was observed; UINT32_MAX initially. @@ -901,10 +901,11 @@ static inline struct UdpardTreeNode* rxSessionSlotFragmentFactory(void* const us RxFragment* const frag = memAlloc(ctx->memory_payload_fragment_handle, sizeof(RxFragment)); if (frag != NULL) { + // NOLINTNEXTLINE(clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling) (void) memset(frag, 0, sizeof(RxFragment)); out = &frag->tree.base; // this is not an escape bug, we retain the pointer via "this" frag->frame_index = ctx->frame_index; - frag->tree.this = frag; // <-- right here, see? + frag->tree.this = frag; // <-- right here, see? ctx->accepted = true; } return out; // OOM handled by the caller @@ -965,6 +966,7 @@ static inline void rxSessionSlotEject(RxFragment* const frag, RxSessionSlotEject { rxSessionSlotEject(((RxFragmentTreeNode*) frag->tree.base.lr[1])->this, ctx); } + // NOLINTNEXTLINE(clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling) (void) memset(&frag->tree.base, 0, sizeof(struct UdpardTreeNode)); // Avoid dangling pointers. // Drop the unneeded fragments and their handles after the sub-tree is fully traversed. if ((!retain) || (frag->base.view.size == 0)) @@ -1149,7 +1151,7 @@ static inline int_fast8_t rxSessionIfaceUpdate(RxSessionIface* const { if (slot->ts_usec == UINT64_MAX) { - slot->ts_usec = ts_usec; // Transfer timestamp is the timestamp of the earliest frame. + slot->ts_usec = ts_usec; // Transfer timestamp is the timestamp of the earliest frame. } result = rxSessionSlotUpdate(slot, // Takes ownership of the frame payload; may free or keep it. frame, diff --git a/tests/src/test_intrusive_rx.c b/tests/src/test_intrusive_rx.c index 004a37e..f6e30af 100644 --- a/tests/src/test_intrusive_rx.c +++ b/tests/src/test_intrusive_rx.c @@ -18,11 +18,11 @@ // [1, 2, 41, 9, 56, 21, 230, 29, 13, 240, 221, 224, 254, 15, 220, 186, 57, 48, 0, 0, 0, 0, 224, 60] static void testRxParseFrameValidMessage(void) { - const byte_t data[] = {1, 2, 41, 9, 255, 255, 230, 29, 13, 240, 221, 224, - 254, 15, 220, 186, 57, 48, 0, 0, 0, 0, 30, 179, // - 'a', 'b', 'c'}; - RxFrame rxf = {0}; - TEST_ASSERT(rxParseFrame((struct UdpardConstPayload){.data = data, .size = sizeof(data)}, &rxf)); + byte_t data[] = {1, 2, 41, 9, 255, 255, 230, 29, 13, 240, 221, 224, + 254, 15, 220, 186, 57, 48, 0, 0, 0, 0, 30, 179, // + 'a', 'b', 'c'}; + RxFrame rxf = {0}; + TEST_ASSERT(rxParseFrame((struct UdpardMutablePayload){.data = data, .size = sizeof(data)}, &rxf)); TEST_ASSERT_EQUAL_UINT64(UdpardPriorityFast, rxf.meta.priority); TEST_ASSERT_EQUAL_UINT64(2345, rxf.meta.src_node_id); TEST_ASSERT_EQUAL_UINT64(UDPARD_NODE_ID_UNSET, rxf.meta.dst_node_id); @@ -39,11 +39,11 @@ static void testRxParseFrameValidRPCService(void) // frame = UDPFrame(priority=Priority.FAST, transfer_id=0xbadc0ffee0ddf00d, index=6654, end_of_transfer=False, // payload=memoryview(b''), source_node_id=2345, destination_node_id=4567, // data_specifier=ServiceDataSpecifier(role=ServiceDataSpecifier.Role.REQUEST, service_id=123), user_data=0) - const byte_t data[] = {1, 2, 41, 9, 215, 17, 123, 192, 13, 240, 221, 224, - 254, 15, 220, 186, 254, 25, 0, 0, 0, 0, 173, 122, // - 'a', 'b', 'c'}; - RxFrame rxf = {0}; - TEST_ASSERT(rxParseFrame((struct UdpardConstPayload){.data = data, .size = sizeof(data)}, &rxf)); + byte_t data[] = {1, 2, 41, 9, 215, 17, 123, 192, 13, 240, 221, 224, + 254, 15, 220, 186, 254, 25, 0, 0, 0, 0, 173, 122, // + 'a', 'b', 'c'}; + RxFrame rxf = {0}; + TEST_ASSERT(rxParseFrame((struct UdpardMutablePayload){.data = data, .size = sizeof(data)}, &rxf)); TEST_ASSERT_EQUAL_UINT64(UdpardPriorityFast, rxf.meta.priority); TEST_ASSERT_EQUAL_UINT64(2345, rxf.meta.src_node_id); TEST_ASSERT_EQUAL_UINT64(4567, rxf.meta.dst_node_id); @@ -59,11 +59,11 @@ static void testRxParseFrameValidRPCService(void) static void testRxParseFrameValidMessageAnonymous(void) { - const byte_t data[] = {1, 2, 255, 255, 255, 255, 230, 29, 13, 240, 221, 224, - 254, 15, 220, 186, 0, 0, 0, 128, 0, 0, 168, 92, // - 'a', 'b', 'c'}; - RxFrame rxf = {0}; - TEST_ASSERT(rxParseFrame((struct UdpardConstPayload){.data = data, .size = sizeof(data)}, &rxf)); + byte_t data[] = {1, 2, 255, 255, 255, 255, 230, 29, 13, 240, 221, 224, + 254, 15, 220, 186, 0, 0, 0, 128, 0, 0, 168, 92, // + 'a', 'b', 'c'}; + RxFrame rxf = {0}; + TEST_ASSERT(rxParseFrame((struct UdpardMutablePayload){.data = data, .size = sizeof(data)}, &rxf)); TEST_ASSERT_EQUAL_UINT64(UdpardPriorityFast, rxf.meta.priority); TEST_ASSERT_EQUAL_UINT64(UDPARD_NODE_ID_UNSET, rxf.meta.src_node_id); TEST_ASSERT_EQUAL_UINT64(UDPARD_NODE_ID_UNSET, rxf.meta.dst_node_id); @@ -77,38 +77,38 @@ static void testRxParseFrameValidMessageAnonymous(void) static void testRxParseFrameRPCServiceAnonymous(void) { - const byte_t data[] = {1, 2, 255, 255, 215, 17, 123, 192, 13, 240, 221, 224, - 254, 15, 220, 186, 254, 25, 0, 0, 0, 0, 75, 79, // - 'a', 'b', 'c'}; - RxFrame rxf = {0}; - TEST_ASSERT_FALSE(rxParseFrame((struct UdpardConstPayload){.data = data, .size = sizeof(data)}, &rxf)); + byte_t data[] = {1, 2, 255, 255, 215, 17, 123, 192, 13, 240, 221, 224, + 254, 15, 220, 186, 254, 25, 0, 0, 0, 0, 75, 79, // + 'a', 'b', 'c'}; + RxFrame rxf = {0}; + TEST_ASSERT_FALSE(rxParseFrame((struct UdpardMutablePayload){.data = data, .size = sizeof(data)}, &rxf)); } static void testRxParseFrameRPCServiceBroadcast(void) { - const byte_t data[] = {1, 2, 41, 9, 255, 255, 123, 192, 13, 240, 221, 224, - 254, 15, 220, 186, 254, 25, 0, 0, 0, 0, 248, 152, // - 'a', 'b', 'c'}; - RxFrame rxf = {0}; - TEST_ASSERT_FALSE(rxParseFrame((struct UdpardConstPayload){.data = data, .size = sizeof(data)}, &rxf)); + byte_t data[] = {1, 2, 41, 9, 255, 255, 123, 192, 13, 240, 221, 224, + 254, 15, 220, 186, 254, 25, 0, 0, 0, 0, 248, 152, // + 'a', 'b', 'c'}; + RxFrame rxf = {0}; + TEST_ASSERT_FALSE(rxParseFrame((struct UdpardMutablePayload){.data = data, .size = sizeof(data)}, &rxf)); } static void testRxParseFrameAnonymousNonSingleFrame(void) { // Invalid anonymous message frame because EOT not set (multi-frame anonymous transfers are not allowed). - const byte_t data[] = {1, 2, 255, 255, 255, 255, 230, 29, 13, 240, 221, 224, - 254, 15, 220, 186, 0, 0, 0, 0, 0, 0, 147, 6, // - 'a', 'b', 'c'}; - RxFrame rxf = {0}; - TEST_ASSERT_FALSE(rxParseFrame((struct UdpardConstPayload){.data = data, .size = sizeof(data)}, &rxf)); + byte_t data[] = {1, 2, 255, 255, 255, 255, 230, 29, 13, 240, 221, 224, + 254, 15, 220, 186, 0, 0, 0, 0, 0, 0, 147, 6, // + 'a', 'b', 'c'}; + RxFrame rxf = {0}; + TEST_ASSERT_FALSE(rxParseFrame((struct UdpardMutablePayload){.data = data, .size = sizeof(data)}, &rxf)); } static void testRxParseFrameBadHeaderCRC(void) { // Bad header CRC. - const byte_t data[] = {1, 2, 41, 9, 255, 255, 230, 29, 13, 240, 221, 224, - 254, 15, 220, 186, 57, 48, 0, 0, 0, 0, 30, 180, // - 'a', 'b', 'c'}; - RxFrame rxf = {0}; - TEST_ASSERT_FALSE(rxParseFrame((struct UdpardConstPayload){.data = data, .size = sizeof(data)}, &rxf)); + byte_t data[] = {1, 2, 41, 9, 255, 255, 230, 29, 13, 240, 221, 224, + 254, 15, 220, 186, 57, 48, 0, 0, 0, 0, 30, 180, // + 'a', 'b', 'c'}; + RxFrame rxf = {0}; + TEST_ASSERT_FALSE(rxParseFrame((struct UdpardMutablePayload){.data = data, .size = sizeof(data)}, &rxf)); } static void testRxParseFrameUnknownHeaderVersion(void) @@ -116,25 +116,24 @@ static void testRxParseFrameUnknownHeaderVersion(void) // >>> from pycyphal.transport.commons.crc import CRC16CCITT // >>> list(CRC16CCITT.new(bytes( // [0, 2, 41, 9, 56, 21, 230, 29, 13, 240, 221, 224, 254, 15, 220, 186, 57, 48, 0, 0, 0, 0])).value_as_bytes) - const byte_t data[] = {0, 2, 41, 9, 56, 21, 230, 29, 13, 240, 221, 224, - 254, 15, 220, 186, 57, 48, 0, 0, 0, 0, 141, 228, // - 'a', 'b', 'c'}; - RxFrame rxf = {0}; - TEST_ASSERT_FALSE(rxParseFrame((struct UdpardConstPayload){.data = data, .size = sizeof(data)}, &rxf)); + byte_t data[] = {0, 2, 41, 9, 56, 21, 230, 29, 13, 240, 221, 224, + 254, 15, 220, 186, 57, 48, 0, 0, 0, 0, 141, 228, // + 'a', 'b', 'c'}; + RxFrame rxf = {0}; + TEST_ASSERT_FALSE(rxParseFrame((struct UdpardMutablePayload){.data = data, .size = sizeof(data)}, &rxf)); } static void testRxParseFrameHeaderWithoutPayload(void) { - const byte_t data[] = {1, 2, 41, 9, 255, 255, 230, 29, 13, 240, 221, 224, - 254, 15, 220, 186, 57, 48, 0, 0, 0, 0, 30, 179}; - RxFrame rxf = {0}; - TEST_ASSERT_FALSE(rxParseFrame((struct UdpardConstPayload){.data = data, .size = sizeof(data)}, &rxf)); + byte_t data[] = {1, 2, 41, 9, 255, 255, 230, 29, 13, 240, 221, 224, 254, 15, 220, 186, 57, 48, 0, 0, 0, 0, 30, 179}; + RxFrame rxf = {0}; + TEST_ASSERT_FALSE(rxParseFrame((struct UdpardMutablePayload){.data = data, .size = sizeof(data)}, &rxf)); } static void testRxParseFrameEmpty(void) { RxFrame rxf = {0}; - TEST_ASSERT_FALSE(rxParseFrame((struct UdpardConstPayload){.data = "", .size = 0}, &rxf)); + TEST_ASSERT_FALSE(rxParseFrame((struct UdpardMutablePayload){.data = "", .size = 0}, &rxf)); } void setUp(void) {} From 1137a42549e2860e2724c2d7db50e0cf18fcb6d6 Mon Sep 17 00:00:00 2001 From: Pavel Kirienko Date: Tue, 25 Jul 2023 19:20:41 +0300 Subject: [PATCH 04/38] Remove special case for empty transfers --- libudpard/udpard.c | 25 ++++++++--------- libudpard/udpard.h | 2 ++ tests/src/test_intrusive_rx.c | 52 +++++++++++++++++++++-------------- 3 files changed, 45 insertions(+), 34 deletions(-) diff --git a/libudpard/udpard.c b/libudpard/udpard.c index 9a1a900..b0dec15 100644 --- a/libudpard/udpard.c +++ b/libudpard/udpard.c @@ -930,8 +930,7 @@ typedef struct /// - Truncate the payload according to the specified size limit. /// - Free the tree nodes and their payload buffers past the size limit. /// -/// It is guaranteed that the output list is sorted by frame index and contains at least one fragment. -/// The payload of the first fragment may be empty. +/// It is guaranteed that the output list is sorted by frame index. It may be empty. /// After this function is invoked, the tree will be destroyed and cannot be used anymore; /// hence, in the event of invalid transfer being received (bad CRC), the fragments will have to be freed /// by traversing the linked list instead of the tree. @@ -944,15 +943,14 @@ static inline void rxSessionSlotEject(RxFragment* const frag, RxSessionSlotEject rxSessionSlotEject(((RxFragmentTreeNode*) frag->tree.base.lr[0])->this, ctx); } const size_t fragment_size = frag->base.view.size; - const bool first = ctx->head == NULL; frag->base.next = NULL; // Default state; may be overwritten. ctx->crc = transferCRCAdd(ctx->crc, fragment_size, frag->base.view.data); - ctx->head = first ? &frag->base : ctx->head; // Truncate unnecessary payload past the specified limit. This enforces the extent and removes the transfer CRC. - const bool retain = (ctx->offset < ctx->retain_size) || first; + const bool retain = ctx->offset < ctx->retain_size; if (retain) { UDPARD_ASSERT(ctx->retain_size >= ctx->offset); + ctx->head = (ctx->head == NULL) ? &frag->base : ctx->head; frag->base.view.size = smaller(frag->base.view.size, ctx->retain_size - ctx->offset); if (ctx->predecessor != NULL) { @@ -969,17 +967,14 @@ static inline void rxSessionSlotEject(RxFragment* const frag, RxSessionSlotEject // NOLINTNEXTLINE(clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling) (void) memset(&frag->tree.base, 0, sizeof(struct UdpardTreeNode)); // Avoid dangling pointers. // Drop the unneeded fragments and their handles after the sub-tree is fully traversed. - if ((!retain) || (frag->base.view.size == 0)) + if (!retain) { + memFree(ctx->memory_payload_fragment_handle, sizeof(RxFragment), frag); memFreePayload(ctx->memory_payload, frag->base.origin); frag->base.origin.data = NULL; frag->base.origin.size = 0; frag->base.view.data = NULL; - UDPARD_ASSERT((frag->base.view.size == 0) || (!retain)); - } - if (!retain) - { - memFree(ctx->memory_payload_fragment_handle, sizeof(RxFragment), frag); + frag->base.view.size = 0; } } @@ -1060,7 +1055,6 @@ static inline int_fast8_t rxSessionSlotUpdate(RxSessionSlot* const rxSessionSlotEject(self->fragments->this, &eject_ctx); self->fragments = NULL; // Ejection invalidates the tree. UDPARD_ASSERT(eject_ctx.offset == self->payload_size); - UDPARD_ASSERT(eject_ctx.head != NULL); if (TRANSFER_CRC_RESIDUE_BEFORE_OUTPUT_XOR == eject_ctx.crc) { result = 1; @@ -1069,11 +1063,14 @@ static inline int_fast8_t rxSessionSlotUpdate(RxSessionSlot* const rx_transfer->source_node_id = frame.meta.src_node_id; rx_transfer->transfer_id = frame.meta.transfer_id; rx_transfer->payload_size = eject_ctx.retain_size; - rx_transfer->payload = *eject_ctx.head; // Slice. The derived type fields are not needed. + rx_transfer->payload = + (eject_ctx.head != NULL) + ? (*eject_ctx.head) // Slice. The derived type fields are not needed. + : (struct UdpardPayloadFragmentHandle){.next = NULL, .view = {0, NULL}, .origin = {0, NULL}}; // This is the single-frame transfer optimization suggested by Scott: we free the first fragment handle // early by moving the contents into the rx_transfer structure by value. // No need to free the payload buffer because it has been transferred to the transfer. - memFree(memory_payload_fragment_handle, sizeof(RxFragment), eject_ctx.head); + memFree(memory_payload_fragment_handle, sizeof(RxFragment), eject_ctx.head); // May be empty. } else // The transfer turned out to be invalid. We have to free the fragments. Can't use the tree anymore. { diff --git a/libudpard/udpard.h b/libudpard/udpard.h index 238b26e..058e9f2 100644 --- a/libudpard/udpard.h +++ b/libudpard/udpard.h @@ -732,6 +732,8 @@ struct UdpardRxTransfer /// this requires freeing both the handles and the payload buffers they point to. /// Beware that different memory resources may have been used to allocate the handles and the payload buffers; /// the application is responsible for freeing them using the correct memory resource. + /// + /// If the payload is empty, the corresponding buffer pointers may be NULL. size_t payload_size; struct UdpardPayloadFragmentHandle payload; }; diff --git a/tests/src/test_intrusive_rx.c b/tests/src/test_intrusive_rx.c index f6e30af..70da4ed 100644 --- a/tests/src/test_intrusive_rx.c +++ b/tests/src/test_intrusive_rx.c @@ -16,7 +16,7 @@ // data_specifier=MessageDataSpecifier(7654), user_data=0) // >>> list(frame.compile_header_and_payload()[0]) // [1, 2, 41, 9, 56, 21, 230, 29, 13, 240, 221, 224, 254, 15, 220, 186, 57, 48, 0, 0, 0, 0, 224, 60] -static void testRxParseFrameValidMessage(void) +static void testParseFrameValidMessage(void) { byte_t data[] = {1, 2, 41, 9, 255, 255, 230, 29, 13, 240, 221, 224, 254, 15, 220, 186, 57, 48, 0, 0, 0, 0, 30, 179, // @@ -34,7 +34,7 @@ static void testRxParseFrameValidMessage(void) TEST_ASSERT_EQUAL_UINT8_ARRAY("abc", rxf.payload.data, 3); } -static void testRxParseFrameValidRPCService(void) +static void testParseFrameValidRPCService(void) { // frame = UDPFrame(priority=Priority.FAST, transfer_id=0xbadc0ffee0ddf00d, index=6654, end_of_transfer=False, // payload=memoryview(b''), source_node_id=2345, destination_node_id=4567, @@ -57,7 +57,7 @@ static void testRxParseFrameValidRPCService(void) TEST_ASSERT_EQUAL_UINT8_ARRAY("abc", rxf.payload.data, 3); } -static void testRxParseFrameValidMessageAnonymous(void) +static void testParseFrameValidMessageAnonymous(void) { byte_t data[] = {1, 2, 255, 255, 255, 255, 230, 29, 13, 240, 221, 224, 254, 15, 220, 186, 0, 0, 0, 128, 0, 0, 168, 92, // @@ -75,7 +75,7 @@ static void testRxParseFrameValidMessageAnonymous(void) TEST_ASSERT_EQUAL_UINT8_ARRAY("abc", rxf.payload.data, 3); } -static void testRxParseFrameRPCServiceAnonymous(void) +static void testParseFrameRPCServiceAnonymous(void) { byte_t data[] = {1, 2, 255, 255, 215, 17, 123, 192, 13, 240, 221, 224, 254, 15, 220, 186, 254, 25, 0, 0, 0, 0, 75, 79, // @@ -84,7 +84,7 @@ static void testRxParseFrameRPCServiceAnonymous(void) TEST_ASSERT_FALSE(rxParseFrame((struct UdpardMutablePayload){.data = data, .size = sizeof(data)}, &rxf)); } -static void testRxParseFrameRPCServiceBroadcast(void) +static void testParseFrameRPCServiceBroadcast(void) { byte_t data[] = {1, 2, 41, 9, 255, 255, 123, 192, 13, 240, 221, 224, 254, 15, 220, 186, 254, 25, 0, 0, 0, 0, 248, 152, // @@ -93,7 +93,7 @@ static void testRxParseFrameRPCServiceBroadcast(void) TEST_ASSERT_FALSE(rxParseFrame((struct UdpardMutablePayload){.data = data, .size = sizeof(data)}, &rxf)); } -static void testRxParseFrameAnonymousNonSingleFrame(void) +static void testParseFrameAnonymousNonSingleFrame(void) { // Invalid anonymous message frame because EOT not set (multi-frame anonymous transfers are not allowed). byte_t data[] = {1, 2, 255, 255, 255, 255, 230, 29, 13, 240, 221, 224, 254, 15, 220, 186, 0, 0, 0, 0, 0, 0, 147, 6, // @@ -102,7 +102,7 @@ static void testRxParseFrameAnonymousNonSingleFrame(void) TEST_ASSERT_FALSE(rxParseFrame((struct UdpardMutablePayload){.data = data, .size = sizeof(data)}, &rxf)); } -static void testRxParseFrameBadHeaderCRC(void) +static void testParseFrameBadHeaderCRC(void) { // Bad header CRC. byte_t data[] = {1, 2, 41, 9, 255, 255, 230, 29, 13, 240, 221, 224, 254, 15, 220, 186, 57, 48, 0, 0, 0, 0, 30, 180, // @@ -111,7 +111,7 @@ static void testRxParseFrameBadHeaderCRC(void) TEST_ASSERT_FALSE(rxParseFrame((struct UdpardMutablePayload){.data = data, .size = sizeof(data)}, &rxf)); } -static void testRxParseFrameUnknownHeaderVersion(void) +static void testParseFrameUnknownHeaderVersion(void) { // >>> from pycyphal.transport.commons.crc import CRC16CCITT // >>> list(CRC16CCITT.new(bytes( @@ -123,19 +123,29 @@ static void testRxParseFrameUnknownHeaderVersion(void) TEST_ASSERT_FALSE(rxParseFrame((struct UdpardMutablePayload){.data = data, .size = sizeof(data)}, &rxf)); } -static void testRxParseFrameHeaderWithoutPayload(void) +static void testParseFrameHeaderWithoutPayload(void) { byte_t data[] = {1, 2, 41, 9, 255, 255, 230, 29, 13, 240, 221, 224, 254, 15, 220, 186, 57, 48, 0, 0, 0, 0, 30, 179}; RxFrame rxf = {0}; TEST_ASSERT_FALSE(rxParseFrame((struct UdpardMutablePayload){.data = data, .size = sizeof(data)}, &rxf)); } -static void testRxParseFrameEmpty(void) +static void testParseFrameEmpty(void) { RxFrame rxf = {0}; TEST_ASSERT_FALSE(rxParseFrame((struct UdpardMutablePayload){.data = "", .size = 0}, &rxf)); } +static void testSessionSlotRestartEmpty(void) +{ + // TODO +} + +static void testSessionSlotRestartNonEmpty(void) +{ + // TODO +} + void setUp(void) {} void tearDown(void) {} @@ -143,15 +153,17 @@ void tearDown(void) {} int main(void) { UNITY_BEGIN(); - RUN_TEST(testRxParseFrameValidMessage); - RUN_TEST(testRxParseFrameValidRPCService); - RUN_TEST(testRxParseFrameValidMessageAnonymous); - RUN_TEST(testRxParseFrameRPCServiceAnonymous); - RUN_TEST(testRxParseFrameRPCServiceBroadcast); - RUN_TEST(testRxParseFrameAnonymousNonSingleFrame); - RUN_TEST(testRxParseFrameBadHeaderCRC); - RUN_TEST(testRxParseFrameUnknownHeaderVersion); - RUN_TEST(testRxParseFrameHeaderWithoutPayload); - RUN_TEST(testRxParseFrameEmpty); + RUN_TEST(testParseFrameValidMessage); + RUN_TEST(testParseFrameValidRPCService); + RUN_TEST(testParseFrameValidMessageAnonymous); + RUN_TEST(testParseFrameRPCServiceAnonymous); + RUN_TEST(testParseFrameRPCServiceBroadcast); + RUN_TEST(testParseFrameAnonymousNonSingleFrame); + RUN_TEST(testParseFrameBadHeaderCRC); + RUN_TEST(testParseFrameUnknownHeaderVersion); + RUN_TEST(testParseFrameHeaderWithoutPayload); + RUN_TEST(testParseFrameEmpty); + RUN_TEST(testSessionSlotRestartEmpty); + RUN_TEST(testSessionSlotRestartNonEmpty); return UNITY_END(); } From 98c0f53fa1a3fd9a15c2f5cf5b067709f35e569d Mon Sep 17 00:00:00 2001 From: Pavel Kirienko Date: Wed, 26 Jul 2023 12:02:28 +0300 Subject: [PATCH 05/38] Add tests for rxSlotRestart --- libudpard/udpard.c | 358 ++++++++++++++++++---------------- libudpard/udpard.h | 79 ++++---- tests/src/test_intrusive_rx.c | 132 ++++++++++++- tests/src/test_intrusive_tx.c | 63 +++--- 4 files changed, 386 insertions(+), 246 deletions(-) diff --git a/libudpard/udpard.c b/libudpard/udpard.c index b0dec15..b5dba74 100644 --- a/libudpard/udpard.c +++ b/libudpard/udpard.c @@ -35,6 +35,10 @@ typedef uint_least8_t byte_t; ///< For compatibility with platforms where byte static const uint_fast8_t ByteWidth = 8U; static const byte_t ByteMask = 0xFFU; +#define RX_SLOT_COUNT 2 +#define TIMESTAMP_UNSET UINT64_MAX +#define FRAME_INDEX_UNSET UINT32_MAX + typedef struct { enum UdpardPriority priority; @@ -104,7 +108,7 @@ static inline uint32_t max32(const uint32_t a, const uint32_t b) return (a > b) ? a : b; } -static inline bool isValidMemoryResource(const struct UdpardMemoryResource* const memory) +static inline bool memIsValid(const struct UdpardMemoryResource* const memory) { return (memory != NULL) && (memory->allocate != NULL) && (memory->free != NULL); } @@ -370,7 +374,7 @@ static inline TxChain txMakeChain(struct UdpardMemoryResource* const memory, const UdpardMicrosecond deadline_usec, const TransferMetadata meta, const struct UdpardUDPIPEndpoint endpoint, - const struct UdpardConstPayload payload, + const struct UdpardPayload payload, void* const user_transfer_reference) { UDPARD_ASSERT(memory != NULL); @@ -439,7 +443,7 @@ static inline int32_t txPush(struct UdpardTx* const tx, const UdpardMicrosecond deadline_usec, const TransferMetadata meta, const struct UdpardUDPIPEndpoint endpoint, - const struct UdpardConstPayload payload, + const struct UdpardPayload payload, void* const user_transfer_reference) { UDPARD_ASSERT(tx != NULL); @@ -507,7 +511,7 @@ int8_t udpardTxInit(struct UdpardTx* const self, struct UdpardMemoryResource* const memory) { int8_t ret = -UDPARD_ERROR_ARGUMENT; - if ((NULL != self) && (NULL != local_node_id) && isValidMemoryResource(memory)) + if ((NULL != self) && (NULL != local_node_id) && memIsValid(memory)) { ret = 0; // NOLINTNEXTLINE(clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling) @@ -523,13 +527,13 @@ int8_t udpardTxInit(struct UdpardTx* const self, return ret; } -int32_t udpardTxPublish(struct UdpardTx* const self, - const UdpardMicrosecond deadline_usec, - const enum UdpardPriority priority, - const UdpardPortID subject_id, - UdpardTransferID* const transfer_id, - const struct UdpardConstPayload payload, - void* const user_transfer_reference) +int32_t udpardTxPublish(struct UdpardTx* const self, + const UdpardMicrosecond deadline_usec, + const enum UdpardPriority priority, + const UdpardPortID subject_id, + UdpardTransferID* const transfer_id, + const struct UdpardPayload payload, + void* const user_transfer_reference) { int32_t out = -UDPARD_ERROR_ARGUMENT; const bool args_ok = (self != NULL) && (self->local_node_id != NULL) && (priority <= UDPARD_PRIORITY_MAX) && @@ -557,14 +561,14 @@ int32_t udpardTxPublish(struct UdpardTx* const self, return out; } -int32_t udpardTxRequest(struct UdpardTx* const self, - const UdpardMicrosecond deadline_usec, - const enum UdpardPriority priority, - const UdpardPortID service_id, - const UdpardNodeID server_node_id, - UdpardTransferID* const transfer_id, - const struct UdpardConstPayload payload, - void* const user_transfer_reference) +int32_t udpardTxRequest(struct UdpardTx* const self, + const UdpardMicrosecond deadline_usec, + const enum UdpardPriority priority, + const UdpardPortID service_id, + const UdpardNodeID server_node_id, + UdpardTransferID* const transfer_id, + const struct UdpardPayload payload, + void* const user_transfer_reference) { int32_t out = -UDPARD_ERROR_ARGUMENT; const bool args_ok = (self != NULL) && (self->local_node_id != NULL) && (priority <= UDPARD_PRIORITY_MAX) && @@ -593,14 +597,14 @@ int32_t udpardTxRequest(struct UdpardTx* const self, return out; } -int32_t udpardTxRespond(struct UdpardTx* const self, - const UdpardMicrosecond deadline_usec, - const enum UdpardPriority priority, - const UdpardPortID service_id, - const UdpardNodeID client_node_id, - const UdpardTransferID transfer_id, - const struct UdpardConstPayload payload, - void* const user_transfer_reference) +int32_t udpardTxRespond(struct UdpardTx* const self, + const UdpardMicrosecond deadline_usec, + const enum UdpardPriority priority, + const UdpardPortID service_id, + const UdpardNodeID client_node_id, + const UdpardTransferID transfer_id, + const struct UdpardPayload payload, + void* const user_transfer_reference) { int32_t out = -UDPARD_ERROR_ARGUMENT; const bool args_ok = (self != NULL) && (self->local_node_id != NULL) && (priority <= UDPARD_PRIORITY_MAX) && @@ -673,7 +677,7 @@ typedef struct TransferMetadata meta; uint32_t index; bool end_of_transfer; - struct UdpardConstPayload payload; ///< Also contains the transfer CRC (but not the header CRC). + struct UdpardPayload payload; ///< Also contains the transfer CRC (but not the header CRC). struct UdpardMutablePayload origin; ///< The entirety of the free-able buffer passed from the application. } RxFrame; @@ -770,18 +774,26 @@ typedef struct struct RxFragment* this; // This is needed to avoid pointer arithmetic with multiple inheritance. } RxFragmentTreeNode; -/// This is designed to be convertible to/from UdpardPayloadFragmentHandle, so that the application could be -/// given a linked list of these objects represented as a list of UdpardPayloadFragmentHandle. +/// This is designed to be convertible to/from UdpardFragment, so that the application could be +/// given a linked list of these objects represented as a list of UdpardFragment. typedef struct RxFragment { - struct UdpardPayloadFragmentHandle base; - RxFragmentTreeNode tree; - uint32_t frame_index; + struct UdpardFragment base; + RxFragmentTreeNode tree; + uint32_t frame_index; } RxFragment; -/// A slot keeps the state of a transfer in the process of being reassembled. -/// We use two slots per iface which enables acceptance of frame-interleaved transfers. -/// An RX port has UDPARD_NETWORK_INTERFACE_COUNT_MAX ifaces, each iface has 2 slots. +/// Internally, the RX pipeline is arranged as follows: +/// +/// - There is one port per subscription or an RPC-service listener. Within the port, there are N sessions, +/// one session per remote node emitting transfers on this port (i.e., on this subject, or sending +/// request/response of this service). Sessions are constructed dynamically ad-hoc and stored in the heap. +/// +/// - Per session, there are UDPARD_NETWORK_INTERFACE_COUNT_MAX interface states to support interface redundancy. +/// +/// - Per interface, there are RX_SLOT_COUNT slots; a slot keeps the state of a transfer in the process of being +/// reassembled. +/// /// Consider the following examples, where A and B denote distinct transfers of three frames each: /// /// A0 A1 A2 B0 B1 B2 -- two transfers without OOO frames; both accepted. @@ -801,22 +813,22 @@ typedef struct RxFragment /// (this is possible because all common CRC functions are linear). typedef struct { - UdpardMicrosecond ts_usec; ///< Timestamp of the earliest frame; UINT64_MAX initially. + UdpardMicrosecond ts_usec; ///< Timestamp of the earliest frame; TIMESTAMP_UNSET initially. UdpardTransferID transfer_id; - uint32_t max_index; ///< Maximum observed frame index in this transfer (so far); zero initially. - uint32_t eot_index; ///< Frame index where the end-of-transfer flag was observed; UINT32_MAX initially. + uint32_t max_index; ///< Maximum observed frame index in this transfer (so far); zero initially. + uint32_t eot_index; ///< Frame index where the EOT flag was observed; FRAME_INDEX_UNSET initially. uint32_t accepted_frames; ///< Number of frames accepted so far. size_t payload_size; RxFragmentTreeNode* fragments; -} RxSessionSlot; +} RxSlot; -#define RX_SESSION_IFACE_SLOT_COUNT 2 typedef struct { UdpardMicrosecond ts_usec; ///< The timestamp of the last transfer to arrive on this interface. - RxSessionSlot slots[RX_SESSION_IFACE_SLOT_COUNT]; -} RxSessionIface; + RxSlot slots[RX_SLOT_COUNT]; +} RxIface; +/// This type is forward-declared externally, hence why it has such a long name with the udpard prefix. typedef struct UdpardInternalRxSession { struct UdpardTreeNode base; @@ -828,42 +840,43 @@ typedef struct UdpardInternalRxSession /// Each redundant interface maintains its own session state independently. /// The first interface to receive a transfer takes precedence, thus the redundant group always operates /// at the speed of the fastest interface. Duplicate transfers delivered by the slower interfaces are discarded. - RxSessionIface ifaces[UDPARD_NETWORK_INTERFACE_COUNT_MAX]; + RxIface ifaces[UDPARD_NETWORK_INTERFACE_COUNT_MAX]; } UdpardInternalRxSession; // NOLINTNEXTLINE(misc-no-recursion) -static inline void rxSessionSlotFree(RxFragment* const self, - struct UdpardMemoryResource* const memory_payload_fragment_handle, - struct UdpardMemoryResource* const memory_payload) +static inline void rxSlotFree(RxFragment* const self, + struct UdpardMemoryResource* const memory_fragment, + struct UdpardMemoryResource* const memory_payload) { - UDPARD_ASSERT((self != NULL) && isValidMemoryResource(memory_payload_fragment_handle)); + UDPARD_ASSERT((self != NULL) && memIsValid(memory_fragment)); memFreePayload(memory_payload, self->base.origin); for (uint_fast8_t i = 0; i < 2; i++) { RxFragmentTreeNode* const child = (RxFragmentTreeNode*) self->tree.base.lr[i]; if (child != NULL) { - rxSessionSlotFree(child->this, memory_payload_fragment_handle, memory_payload); + UDPARD_ASSERT(child->base.up == &self->tree.base); + rxSlotFree(child->this, memory_fragment, memory_payload); } } - memFree(memory_payload_fragment_handle, sizeof(RxFragment), self); // self-destruct + memFree(memory_fragment, sizeof(RxFragment), self); // self-destruct } -static inline void rxSessionSlotRestart(RxSessionSlot* const self, - const UdpardTransferID transfer_id, - struct UdpardMemoryResource* const memory_payload_fragment_handle, - struct UdpardMemoryResource* const memory_payload) +static inline void rxSlotRestart(RxSlot* const self, + const UdpardTransferID transfer_id, + struct UdpardMemoryResource* const memory_fragment, + struct UdpardMemoryResource* const memory_payload) { - UDPARD_ASSERT((self != NULL) && (memory_payload_fragment_handle != NULL) && (memory_payload != NULL)); - self->ts_usec = UINT64_MAX; // Will be assigned when the first frame of the transfer has arrived. + UDPARD_ASSERT((self != NULL) && (memory_fragment != NULL) && (memory_payload != NULL)); + self->ts_usec = TIMESTAMP_UNSET; // Will be assigned when the first frame of the transfer has arrived. self->transfer_id = transfer_id; self->max_index = 0; - self->eot_index = UINT32_MAX; + self->eot_index = FRAME_INDEX_UNSET; self->accepted_frames = 0; self->payload_size = 0; if (self->fragments != NULL) { - rxSessionSlotFree(self->fragments->this, memory_payload_fragment_handle, memory_payload); + rxSlotFree(self->fragments->this, memory_fragment, memory_payload); self->fragments = NULL; } } @@ -872,14 +885,14 @@ typedef struct { uint32_t frame_index; bool accepted; - struct UdpardMemoryResource* memory_payload_fragment_handle; -} RxSessionSlotUpdateContext; + struct UdpardMemoryResource* memory_fragment; +} RxSlotUpdateContext; -static inline int8_t rxSessionSlotFragmentSearch(void* const user_reference, const struct UdpardTreeNode* node) +static inline int8_t rxSlotFragmentSearch(void* const user_reference, const struct UdpardTreeNode* node) { UDPARD_ASSERT((user_reference != NULL) && (node != NULL)); - const RxSessionSlotUpdateContext* const ctx = (RxSessionSlotUpdateContext*) user_reference; - RxFragment* const frag = ((RxFragmentTreeNode*) node)->this; + const RxSlotUpdateContext* const ctx = (RxSlotUpdateContext*) user_reference; + RxFragment* const frag = ((RxFragmentTreeNode*) node)->this; UDPARD_ASSERT((ctx != NULL) && (frag != NULL)); int8_t out = 0; if (ctx->frame_index > frag->frame_index) @@ -893,12 +906,12 @@ static inline int8_t rxSessionSlotFragmentSearch(void* const user_reference, con return out; } -static inline struct UdpardTreeNode* rxSessionSlotFragmentFactory(void* const user_reference) +static inline struct UdpardTreeNode* rxSlotFragmentFactory(void* const user_reference) { - RxSessionSlotUpdateContext* const ctx = (RxSessionSlotUpdateContext*) user_reference; - UDPARD_ASSERT((ctx != NULL) && isValidMemoryResource(ctx->memory_payload_fragment_handle)); + RxSlotUpdateContext* const ctx = (RxSlotUpdateContext*) user_reference; + UDPARD_ASSERT((ctx != NULL) && memIsValid(ctx->memory_fragment)); struct UdpardTreeNode* out = NULL; - RxFragment* const frag = memAlloc(ctx->memory_payload_fragment_handle, sizeof(RxFragment)); + RxFragment* const frag = memAlloc(ctx->memory_fragment, sizeof(RxFragment)); if (frag != NULL) { // NOLINTNEXTLINE(clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling) @@ -914,33 +927,23 @@ static inline struct UdpardTreeNode* rxSessionSlotFragmentFactory(void* const us /// States outliving each level of recursion while ejecting the transfer from the fragment tree. typedef struct { - struct UdpardPayloadFragmentHandle* head; // Points to the first fragment in the list. - struct UdpardPayloadFragmentHandle* predecessor; - uint32_t crc; - size_t retain_size; - size_t offset; - struct UdpardMemoryResource* memory_payload_fragment_handle; - struct UdpardMemoryResource* memory_payload; -} RxSessionSlotEjectContext; - -/// This function finalizes the fragmented transfer payload by doing multiple things in one pass through the tree: -/// -/// - Compute the transfer-CRC. The caller should verify the result. -/// - Build a linked list of fragments ordered by frame index, as the application would expect it. -/// - Truncate the payload according to the specified size limit. -/// - Free the tree nodes and their payload buffers past the size limit. -/// -/// It is guaranteed that the output list is sorted by frame index. It may be empty. -/// After this function is invoked, the tree will be destroyed and cannot be used anymore; -/// hence, in the event of invalid transfer being received (bad CRC), the fragments will have to be freed -/// by traversing the linked list instead of the tree. + struct UdpardFragment* head; // Points to the first fragment in the list. + struct UdpardFragment* predecessor; + uint32_t crc; + size_t retain_size; + size_t offset; + struct UdpardMemoryResource* memory_fragment; + struct UdpardMemoryResource* memory_payload; +} RxSlotEjectContext; + +/// See rxSlotEject() for details. /// NOLINTNEXTLINE(misc-no-recursion) -static inline void rxSessionSlotEject(RxFragment* const frag, RxSessionSlotEjectContext* const ctx) +static inline void rxSlotEjectFragment(RxFragment* const frag, RxSlotEjectContext* const ctx) { - UDPARD_ASSERT((frag != NULL) && (ctx != NULL) && isValidMemoryResource(ctx->memory_payload_fragment_handle)); + UDPARD_ASSERT((frag != NULL) && (ctx != NULL) && memIsValid(ctx->memory_fragment)); if (frag->tree.base.lr[0] != NULL) { - rxSessionSlotEject(((RxFragmentTreeNode*) frag->tree.base.lr[0])->this, ctx); + rxSlotEjectFragment(((RxFragmentTreeNode*) frag->tree.base.lr[0])->this, ctx); } const size_t fragment_size = frag->base.view.size; frag->base.next = NULL; // Default state; may be overwritten. @@ -962,14 +965,12 @@ static inline void rxSessionSlotEject(RxFragment* const frag, RxSessionSlotEject ctx->offset += fragment_size; if (frag->tree.base.lr[1] != NULL) { - rxSessionSlotEject(((RxFragmentTreeNode*) frag->tree.base.lr[1])->this, ctx); + rxSlotEjectFragment(((RxFragmentTreeNode*) frag->tree.base.lr[1])->this, ctx); } - // NOLINTNEXTLINE(clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling) - (void) memset(&frag->tree.base, 0, sizeof(struct UdpardTreeNode)); // Avoid dangling pointers. // Drop the unneeded fragments and their handles after the sub-tree is fully traversed. if (!retain) { - memFree(ctx->memory_payload_fragment_handle, sizeof(RxFragment), frag); + memFree(ctx->memory_fragment, sizeof(RxFragment), frag); memFreePayload(ctx->memory_payload, frag->base.origin); frag->base.origin.data = NULL; frag->base.origin.size = 0; @@ -978,17 +979,78 @@ static inline void rxSessionSlotEject(RxFragment* const frag, RxSessionSlotEject } } +/// This function finalizes the fragmented transfer payload by doing multiple things in one pass through the tree: +/// +/// - Compute the transfer-CRC. The caller should verify the result. +/// - Build a linked list of fragments ordered by frame index, as the application would expect it. +/// - Truncate the payload according to the specified size limit. +/// - Free the tree nodes and their payload buffers past the size limit. +/// +/// It is guaranteed that the output list is sorted by frame index. It may be empty. +/// After this function is invoked, the tree will be destroyed and cannot be used anymore; +/// hence, in the event of invalid transfer being received (bad CRC), the fragments will have to be freed +/// by traversing the linked list instead of the tree. +static inline int_fast8_t rxSlotEject(RxSlot* const self, + const RxFrame frame, + struct UdpardRxTransfer* const rx_transfer, + const size_t extent, + struct UdpardMemoryResource* const memory_fragment, + struct UdpardMemoryResource* const memory_payload) +{ + int_fast8_t result = 0; + RxSlotEjectContext eject_ctx = { + .head = NULL, + .predecessor = NULL, + .crc = TRANSFER_CRC_INITIAL, + .retain_size = smaller(self->payload_size - TRANSFER_CRC_SIZE_BYTES, extent), + .offset = 0, + .memory_fragment = memory_fragment, + .memory_payload = memory_payload, + }; + rxSlotEjectFragment(self->fragments->this, &eject_ctx); + self->fragments = NULL; // Ejection invalidates the tree. + UDPARD_ASSERT(eject_ctx.offset == self->payload_size); + if (TRANSFER_CRC_RESIDUE_BEFORE_OUTPUT_XOR == eject_ctx.crc) + { + result = 1; + rx_transfer->timestamp_usec = self->ts_usec; + rx_transfer->priority = frame.meta.priority; + rx_transfer->source_node_id = frame.meta.src_node_id; + rx_transfer->transfer_id = frame.meta.transfer_id; + rx_transfer->payload_size = eject_ctx.retain_size; + rx_transfer->payload = (eject_ctx.head != NULL) + ? (*eject_ctx.head) // Slice. The derived type fields are not needed. + : (struct UdpardFragment){.next = NULL, .view = {0, NULL}, .origin = {0, NULL}}; + // This is the single-frame transfer optimization suggested by Scott: we free the first fragment handle + // early by moving the contents into the rx_transfer structure by value. + // No need to free the payload buffer because it has been transferred to the transfer. + memFree(memory_fragment, sizeof(RxFragment), eject_ctx.head); // May be empty. + } + else // The transfer turned out to be invalid. We have to free the fragments. Can't use the tree anymore. + { + struct UdpardFragment* handle = eject_ctx.head; + while (handle != NULL) + { + struct UdpardFragment* const next = handle->next; + memFreePayload(memory_payload, handle->origin); + memFree(memory_fragment, sizeof(RxFragment), handle); + handle = next; + } + } + return result; +} + /// This function will either move the frame payload into the session, or free it if it cannot be made use of. /// Returns: 1 -- transfer available; 0 -- transfer not yet available; <0 -- error. -static inline int_fast8_t rxSessionSlotUpdate(RxSessionSlot* const self, - const RxFrame frame, - struct UdpardRxTransfer* const rx_transfer, - const size_t extent, - struct UdpardMemoryResource* const memory_payload_fragment_handle, - struct UdpardMemoryResource* const memory_payload) +static inline int_fast8_t rxSlotUpdate(RxSlot* const self, + const RxFrame frame, + struct UdpardRxTransfer* const rx_transfer, + const size_t extent, + struct UdpardMemoryResource* const memory_fragment, + struct UdpardMemoryResource* const memory_payload) { UDPARD_ASSERT((self != NULL) && (self->transfer_id == frame.meta.transfer_id) && (frame.payload.size > 0) && - (rx_transfer != NULL) && isValidMemoryResource(memory_payload_fragment_handle)); + (rx_transfer != NULL) && memIsValid(memory_fragment)); int_fast8_t result = 0; bool restart = false; bool release = true; @@ -996,7 +1058,7 @@ static inline int_fast8_t rxSessionSlotUpdate(RxSessionSlot* const self->max_index = max32(self->max_index, frame.index); if (frame.end_of_transfer) { - if ((self->eot_index != UINT32_MAX) && (self->eot_index != frame.index)) + if ((self->eot_index != FRAME_INDEX_UNSET) && (self->eot_index != frame.index)) { restart = true; // Inconsistent EOT flag, could be a node-ID conflict. goto finish; @@ -1011,13 +1073,13 @@ static inline int_fast8_t rxSessionSlotUpdate(RxSessionSlot* const } // SECOND: Insert the fragment into the fragment tree. If it already exists, drop and free the duplicate. UDPARD_ASSERT((self->max_index <= self->eot_index) && (self->accepted_frames < self->eot_index)); - RxSessionSlotUpdateContext update_ctx = {.frame_index = frame.index, - .accepted = false, - .memory_payload_fragment_handle = memory_payload_fragment_handle}; - RxFragmentTreeNode* const frag = (RxFragmentTreeNode*) cavlSearch((struct UdpardTreeNode**) &self->fragments, + RxSlotUpdateContext update_ctx = {.frame_index = frame.index, + .accepted = false, + .memory_fragment = memory_fragment}; + RxFragmentTreeNode* const frag = (RxFragmentTreeNode*) cavlSearch((struct UdpardTreeNode**) &self->fragments, // &update_ctx, - &rxSessionSlotFragmentSearch, - &rxSessionSlotFragmentFactory); + &rxSlotFragmentSearch, + &rxSlotFragmentFactory); UDPARD_ASSERT((self->max_index <= self->eot_index) && (self->accepted_frames <= self->eot_index)); if (frag == NULL) { @@ -1043,52 +1105,13 @@ static inline int_fast8_t rxSessionSlotUpdate(RxSessionSlot* const restart = true; if (self->payload_size >= TRANSFER_CRC_SIZE_BYTES) { - RxSessionSlotEjectContext eject_ctx = { - .head = NULL, - .predecessor = NULL, - .crc = TRANSFER_CRC_INITIAL, - .retain_size = smaller(self->payload_size - TRANSFER_CRC_SIZE_BYTES, extent), - .offset = 0, - .memory_payload_fragment_handle = memory_payload_fragment_handle, - .memory_payload = memory_payload, - }; - rxSessionSlotEject(self->fragments->this, &eject_ctx); - self->fragments = NULL; // Ejection invalidates the tree. - UDPARD_ASSERT(eject_ctx.offset == self->payload_size); - if (TRANSFER_CRC_RESIDUE_BEFORE_OUTPUT_XOR == eject_ctx.crc) - { - result = 1; - rx_transfer->timestamp_usec = self->ts_usec; - rx_transfer->priority = frame.meta.priority; - rx_transfer->source_node_id = frame.meta.src_node_id; - rx_transfer->transfer_id = frame.meta.transfer_id; - rx_transfer->payload_size = eject_ctx.retain_size; - rx_transfer->payload = - (eject_ctx.head != NULL) - ? (*eject_ctx.head) // Slice. The derived type fields are not needed. - : (struct UdpardPayloadFragmentHandle){.next = NULL, .view = {0, NULL}, .origin = {0, NULL}}; - // This is the single-frame transfer optimization suggested by Scott: we free the first fragment handle - // early by moving the contents into the rx_transfer structure by value. - // No need to free the payload buffer because it has been transferred to the transfer. - memFree(memory_payload_fragment_handle, sizeof(RxFragment), eject_ctx.head); // May be empty. - } - else // The transfer turned out to be invalid. We have to free the fragments. Can't use the tree anymore. - { - struct UdpardPayloadFragmentHandle* handle = eject_ctx.head; - while (handle != NULL) - { - struct UdpardPayloadFragmentHandle* const next = handle->next; - memFreePayload(memory_payload, handle->origin); - memFree(memory_payload_fragment_handle, sizeof(RxFragment), handle); - handle = next; - } - } + result = rxSlotEject(self, frame, rx_transfer, extent, memory_fragment, memory_payload); } } finish: if (restart) { - rxSessionSlotRestart(self, frame.meta.transfer_id + 1, memory_payload_fragment_handle, memory_payload); + rxSlotRestart(self, frame.meta.transfer_id + 1, memory_fragment, memory_payload); } if (release) { @@ -1098,18 +1121,18 @@ static inline int_fast8_t rxSessionSlotUpdate(RxSessionSlot* const } /// Returns: 1 -- transfer available; 0 -- transfer not yet available; <0 -- error. -static inline int_fast8_t rxSessionIfaceUpdate(RxSessionIface* const self, - const UdpardMicrosecond ts_usec, - const RxFrame frame, - struct UdpardRxTransfer* const received_transfer, - const size_t extent, - const UdpardMicrosecond transfer_id_timeout_usec, - const struct UdpardRxMemoryResources memory) +static inline int_fast8_t rxIfaceUpdate(RxIface* const self, + const UdpardMicrosecond ts_usec, + const RxFrame frame, + struct UdpardRxTransfer* const received_transfer, + const size_t extent, + const UdpardMicrosecond transfer_id_timeout_usec, + const struct UdpardRxMemoryResources memory) { UDPARD_ASSERT((self != NULL) && (frame.payload.size > 0) && (received_transfer != NULL)); - RxSessionSlot* slot = NULL; + RxSlot* slot = NULL; // First we should check if there is an existing slot for this transfer; if yes, this is the simplest case. - for (uint_fast8_t i = 0; i < RX_SESSION_IFACE_SLOT_COUNT; i++) + for (uint_fast8_t i = 0; i < RX_SLOT_COUNT; i++) { if (self->slots[i].transfer_id == frame.meta.transfer_id) { @@ -1121,11 +1144,11 @@ static inline int_fast8_t rxSessionIfaceUpdate(RxSessionIface* const // or a transfer-ID timeout has occurred. In this case we sacrifice the oldest slot. if (slot == NULL) { - bool is_future_tid = true; - RxSessionSlot* victim = &self->slots[0]; + bool is_future_tid = true; + RxSlot* victim = &self->slots[0]; // Check if the newly received transfer-ID is higher than any of the existing slots. // While we're at it, find the oldest slot as a replacement candidate. - for (uint_fast8_t i = 0; i < RX_SESSION_IFACE_SLOT_COUNT; i++) + for (uint_fast8_t i = 0; i < RX_SLOT_COUNT; i++) { is_future_tid = is_future_tid && (frame.meta.transfer_id > self->slots[i].transfer_id); if (self->slots[i].transfer_id < victim->transfer_id) @@ -1136,7 +1159,7 @@ static inline int_fast8_t rxSessionIfaceUpdate(RxSessionIface* const const bool is_tid_timeout = (ts_usec - self->ts_usec) > transfer_id_timeout_usec; if (is_tid_timeout || is_future_tid) { - rxSessionSlotRestart(victim, frame.meta.transfer_id, memory.payload_fragment_handle, memory.payload); + rxSlotRestart(victim, frame.meta.transfer_id, memory.fragment, memory.payload); slot = victim; UDPARD_ASSERT(slot != NULL); } @@ -1146,16 +1169,11 @@ static inline int_fast8_t rxSessionIfaceUpdate(RxSessionIface* const int_fast8_t result = 0; if (slot != NULL) { - if (slot->ts_usec == UINT64_MAX) + if (slot->ts_usec == TIMESTAMP_UNSET) { slot->ts_usec = ts_usec; // Transfer timestamp is the timestamp of the earliest frame. } - result = rxSessionSlotUpdate(slot, // Takes ownership of the frame payload; may free or keep it. - frame, - received_transfer, - extent, - memory.payload_fragment_handle, - memory.payload); + result = rxSlotUpdate(slot, frame, received_transfer, extent, memory.fragment, memory.payload); } else { @@ -1174,6 +1192,6 @@ int8_t udpardRxSubscriptionInit(struct UdpardRxSubscription* const self, (void) extent; (void) memory; (void) rxParseFrame; - (void) rxSessionIfaceUpdate; + (void) rxIfaceUpdate; return 0; } diff --git a/libudpard/udpard.h b/libudpard/udpard.h index 058e9f2..bb2d741 100644 --- a/libudpard/udpard.h +++ b/libudpard/udpard.h @@ -170,7 +170,7 @@ /// /// - (MTU+library overhead) blocks for the TX and RX pipelines (usually less than 2048 bytes); /// - RX session object sized blocks for the RX pipeline (less than 512 bytes); -/// - RX payload fragment handle sized blocks for the RX pipeline (less than 128 bytes). +/// - RX fragment handle sized blocks for the RX pipeline (less than 128 bytes). /// /// The detailed information is given in the API documentation. /// @@ -271,7 +271,7 @@ struct UdpardMutablePayload void* data; }; -struct UdpardConstPayload +struct UdpardPayload { size_t size; const void* data; @@ -282,22 +282,22 @@ struct UdpardConstPayload /// as well as the payload structure itself, assuming that it is also heap-allocated. /// The model is as follows: /// -/// (payload header) ---> UdpardPayloadFragmentHandle: -/// next ---> UdpardPayloadFragmentHandle... +/// (payload header) ---> UdpardFragment: +/// next ---> UdpardFragment... /// owner ---> (the free()able payload data buffer) /// view ---> (somewhere inside the payload data buffer) /// /// Payloads of received transfers are represented using this type, where each fragment corresponds to a frame. /// The application can either consume them directly or to copy the data into a contiguous buffer beforehand /// at the expense of extra time and memory utilization. -struct UdpardPayloadFragmentHandle +struct UdpardFragment { /// Points to the next fragment in the fragmented buffer; NULL if this is the last fragment. - struct UdpardPayloadFragmentHandle* next; + struct UdpardFragment* next; /// Contains the actual data to be used by the application. /// The memory pointed to by this fragment shall not be freed by the application. - struct UdpardConstPayload view; + struct UdpardPayload view; /// This entity points to the base buffer that contains this fragment. /// The application can use this pointer to free the outer buffer after the payload has been consumed. @@ -527,13 +527,13 @@ int8_t udpardTxInit(struct UdpardTx* const self, /// /// The time complexity is O(p + log e), where p is the amount of payload in the transfer, and e is the number of /// frames already enqueued in the transmission queue. -int32_t udpardTxPublish(struct UdpardTx* const self, - const UdpardMicrosecond deadline_usec, - const enum UdpardPriority priority, - const UdpardPortID subject_id, - UdpardTransferID* const transfer_id, - const struct UdpardConstPayload payload, - void* const user_transfer_reference); +int32_t udpardTxPublish(struct UdpardTx* const self, + const UdpardMicrosecond deadline_usec, + const enum UdpardPriority priority, + const UdpardPortID subject_id, + UdpardTransferID* const transfer_id, + const struct UdpardPayload payload, + void* const user_transfer_reference); /// This is similar to udpardTxPublish except that it is intended for service request transfers. /// It takes the node-ID of the server that is intended to receive the request. @@ -550,27 +550,27 @@ int32_t udpardTxPublish(struct UdpardTx* const self, /// - UDPARD_ERROR_ANONYMOUS if the local node is anonymous (the local node-ID is unset). /// /// Other considerations are the same as for udpardTxPublish. -int32_t udpardTxRequest(struct UdpardTx* const self, - const UdpardMicrosecond deadline_usec, - const enum UdpardPriority priority, - const UdpardPortID service_id, - const UdpardNodeID server_node_id, - UdpardTransferID* const transfer_id, - const struct UdpardConstPayload payload, - void* const user_transfer_reference); +int32_t udpardTxRequest(struct UdpardTx* const self, + const UdpardMicrosecond deadline_usec, + const enum UdpardPriority priority, + const UdpardPortID service_id, + const UdpardNodeID server_node_id, + UdpardTransferID* const transfer_id, + const struct UdpardPayload payload, + void* const user_transfer_reference); /// This is similar to udpardTxRequest except that it takes the node-ID of the client instead of server, /// and the transfer-ID is passed by value rather than by pointer. /// The transfer-ID is passed by value because when responding to an RPC-service request, the server must /// reuse the transfer-ID value of the request (this is to allow the client to match responses with their requests). -int32_t udpardTxRespond(struct UdpardTx* const self, - const UdpardMicrosecond deadline_usec, - const enum UdpardPriority priority, - const UdpardPortID service_id, - const UdpardNodeID client_node_id, - const UdpardTransferID transfer_id, - const struct UdpardConstPayload payload, - void* const user_transfer_reference); +int32_t udpardTxRespond(struct UdpardTx* const self, + const UdpardMicrosecond deadline_usec, + const enum UdpardPriority priority, + const UdpardPortID service_id, + const UdpardNodeID client_node_id, + const UdpardTransferID transfer_id, + const struct UdpardPayload payload, + void* const user_transfer_reference); /// This function accesses the enqueued UDP datagram scheduled for transmission next. The queue itself is not modified /// (i.e., the accessed element is not removed). The application should invoke this function to collect the datagrams @@ -654,7 +654,7 @@ struct UdpardRxPort /// Each session instance takes sizeof(UdpardInternalRxSession) bytes of dynamic memory for itself, /// which is at most 512 bytes on wide-word platforms (on small word size platforms it is usually much smaller). /// On top of that, each session instance holds memory for the transfer payload fragments and small fixed-size - /// metadata objects called "payload fragment handles" (at most 128 bytes large, usually much smaller, + /// metadata objects called "fragment handles" (at most 128 bytes large, usually much smaller, /// depending on the pointer width and the word size), one handle per fragment. /// /// The transfer payload memory is not allocated by the library but rather moved from the application @@ -664,9 +664,9 @@ struct UdpardRxPort /// of the entire datagram payload (including all overheads such as the Cyphal/UDP frame header and possible /// data that spills over the configured extent value for this port). /// If the library does not need the datagram to reassemble the transfer, its payload buffer is freed immediately. - /// There is a 1-to-1 correspondence between the payload fragment handles and the payload fragments. + /// There is a 1-to-1 correspondence between the fragment handles and the payload fragments. /// Remote nodes that emit highly fragmented transfers cause a higher memory utilization in the local node - /// because of the increased number of payload fragment handles and per-datagram overheads. + /// because of the increased number of fragment handles and per-datagram overheads. /// /// In the worst case, the library may keep up to two full transfer payloads in memory at the same time /// (two transfer states are kept to allow acceptance of interleaved frames). @@ -700,9 +700,9 @@ struct UdpardRxMemoryResources /// Each instance is fixed-size, so a trivial zero-fragmentation block allocator is sufficient. struct UdpardMemoryResource* session; - /// The payload fragment handles are allocated per payload fragment; each handle contains a pointer to its fragment. + /// The fragment handles are allocated per payload fragment; each handle contains a pointer to its fragment. /// Each instance is of a very small fixed size, so a trivial zero-fragmentation block allocator is sufficient. - struct UdpardMemoryResource* payload_fragment_handle; + struct UdpardMemoryResource* fragment; /// The library never allocates payload buffers itself, as they are handed over by the application via /// udpardRx*Receive. Once a buffer is handed over, the library may choose to keep it if it is deemed to be @@ -734,8 +734,8 @@ struct UdpardRxTransfer /// the application is responsible for freeing them using the correct memory resource. /// /// If the payload is empty, the corresponding buffer pointers may be NULL. - size_t payload_size; - struct UdpardPayloadFragmentHandle payload; + size_t payload_size; + struct UdpardFragment payload; }; /// This is, essentially, a helper that frees the memory allocated for the payload and its fragment headers @@ -744,7 +744,7 @@ struct UdpardRxTransfer /// /// If any of the arguments are NULL, the function has no effect. void udpardRxTransferFree(struct UdpardRxTransfer* const self, - struct UdpardMemoryResource* const memory_payload_fragment_handle, + struct UdpardMemoryResource* const memory_fragment, struct UdpardMemoryResource* const memory_payload); // --------------------------------------------- SUBJECTS --------------------------------------------- @@ -844,7 +844,7 @@ void udpardRxSubscriptionDestroy(struct UdpardRxSubscription* const self); /// /// 1. A new session state instance is allocated when a new session is initiated. /// -/// 2. A new transfer payload fragment handle is allocated when a new transfer fragment is accepted. +/// 2. A new transfer fragment handle is allocated when a new transfer fragment is accepted. /// /// 3. Allocated objects may occasionally be deallocated at the discretion of the library. /// This behavior does not increase the worst case execution time and does not improve the worst case memory @@ -853,6 +853,7 @@ void udpardRxSubscriptionDestroy(struct UdpardRxSubscription* const self); /// /// The time complexity is O(log n) where n is the number of remote notes publishing on this subject (topic). /// No data copy takes place. Malformed frames are discarded in constant time. +/// Linear time is spent on the CRC verification of the transfer payload when the transfer is complete. /// /// This function performs log(n) of recursive calls internally, where n is the number of frames in a transfer. /// diff --git a/tests/src/test_intrusive_rx.c b/tests/src/test_intrusive_rx.c index 70da4ed..eb8456a 100644 --- a/tests/src/test_intrusive_rx.c +++ b/tests/src/test_intrusive_rx.c @@ -7,6 +7,8 @@ #include "helpers.h" #include +// NOLINTBEGIN(clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling) + // Generate reference data using PyCyphal: // // >>> from pycyphal.transport.udp import UDPFrame @@ -32,6 +34,8 @@ static void testParseFrameValidMessage(void) TEST_ASSERT_FALSE(rxf.end_of_transfer); TEST_ASSERT_EQUAL_UINT64(3, rxf.payload.size); TEST_ASSERT_EQUAL_UINT8_ARRAY("abc", rxf.payload.data, 3); + TEST_ASSERT_EQUAL_UINT64(sizeof(data), rxf.origin.size); + TEST_ASSERT_EQUAL_UINT8_ARRAY(data, rxf.origin.data, sizeof(data)); } static void testParseFrameValidRPCService(void) @@ -55,6 +59,8 @@ static void testParseFrameValidRPCService(void) TEST_ASSERT_FALSE(rxf.end_of_transfer); TEST_ASSERT_EQUAL_UINT64(3, rxf.payload.size); TEST_ASSERT_EQUAL_UINT8_ARRAY("abc", rxf.payload.data, 3); + TEST_ASSERT_EQUAL_UINT64(sizeof(data), rxf.origin.size); + TEST_ASSERT_EQUAL_UINT8_ARRAY(data, rxf.origin.data, sizeof(data)); } static void testParseFrameValidMessageAnonymous(void) @@ -73,6 +79,8 @@ static void testParseFrameValidMessageAnonymous(void) TEST_ASSERT_TRUE(rxf.end_of_transfer); TEST_ASSERT_EQUAL_UINT64(3, rxf.payload.size); TEST_ASSERT_EQUAL_UINT8_ARRAY("abc", rxf.payload.data, 3); + TEST_ASSERT_EQUAL_UINT64(sizeof(data), rxf.origin.size); + TEST_ASSERT_EQUAL_UINT8_ARRAY(data, rxf.origin.data, sizeof(data)); } static void testParseFrameRPCServiceAnonymous(void) @@ -136,12 +144,123 @@ static void testParseFrameEmpty(void) TEST_ASSERT_FALSE(rxParseFrame((struct UdpardMutablePayload){.data = "", .size = 0}, &rxf)); } -static void testSessionSlotRestartEmpty(void) +/// Moves the payload from the origin into a new buffer and attaches is to the newly allocated fragment. +/// This function performs two allocations. This function is infallible. +static RxFragment* makeRxFragment(struct UdpardMemoryResource* const memory_fragment, + struct UdpardMemoryResource* const memory_payload, + const uint32_t frame_index, + const struct UdpardPayload view, + const struct UdpardMutablePayload origin, + RxFragmentTreeNode* const parent) { - // TODO + TEST_PANIC_UNLESS((view.data >= origin.data) && (view.size <= origin.size)); + TEST_PANIC_UNLESS((((const byte_t*) view.data) + view.size) <= (((const byte_t*) origin.data) + origin.size)); + byte_t* const new_origin = (byte_t*) memAlloc(memory_payload, origin.size); + RxFragment* const frag = (RxFragment*) memAlloc(memory_fragment, sizeof(RxFragment)); + if ((new_origin != NULL) && (frag != NULL)) + { + (void) memmove(new_origin, origin.data, origin.size); + (void) memset(frag, 0, sizeof(RxFragment)); + frag->tree.base.lr[0] = NULL; + frag->tree.base.lr[1] = NULL; + frag->tree.base.up = &parent->base; + frag->tree.this = frag; + frag->frame_index = frame_index; + frag->base.view = view; + frag->base.origin.data = new_origin; + frag->base.origin.size = origin.size; + frag->base.view.data = new_origin + (((const byte_t*) view.data) - ((byte_t*) origin.data)); + frag->base.view.size = view.size; + } + else + { + TEST_PANIC("Failed to allocate RxFragment"); + } + return frag; +} + +static void testSlotRestartEmpty(void) +{ + RxSlot slot = { + .ts_usec = 1234567890, + .transfer_id = 0x123456789abcdef0, + .max_index = 546, + .eot_index = 654, + .accepted_frames = 555, + .payload_size = 987, + .fragments = NULL, + }; + InstrumentedAllocator alloc = {0}; + rxSlotRestart(&slot, 0x1122334455667788ULL, &alloc.base, &alloc.base); + TEST_ASSERT_EQUAL(TIMESTAMP_UNSET, slot.ts_usec); + TEST_ASSERT_EQUAL(0x1122334455667788ULL, slot.transfer_id); + TEST_ASSERT_EQUAL(0, slot.max_index); + TEST_ASSERT_EQUAL(FRAME_INDEX_UNSET, slot.eot_index); + TEST_ASSERT_EQUAL(0, slot.accepted_frames); + TEST_ASSERT_EQUAL(0, slot.payload_size); + TEST_ASSERT_EQUAL(NULL, slot.fragments); } -static void testSessionSlotRestartNonEmpty(void) +static void testSlotRestartNonEmpty(void) +{ + InstrumentedAllocator mem_fragment = {0}; + InstrumentedAllocator mem_payload = {0}; + instrumentedAllocatorNew(&mem_fragment); + instrumentedAllocatorNew(&mem_payload); + byte_t data[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; + // + RxSlot slot = { + .ts_usec = 1234567890, + .transfer_id = 0x123456789abcdef0, + .max_index = 546, + .eot_index = 654, + .accepted_frames = 555, + .payload_size = 987, + // + .fragments = &makeRxFragment(&mem_fragment.base, + &mem_payload.base, + 1, + (struct UdpardPayload){.data = &data[2], .size = 2}, + (struct UdpardMutablePayload){.data = data, .size = sizeof(data)}, + NULL) + ->tree, + }; + slot.fragments->base.lr[0] = &makeRxFragment(&mem_fragment.base, + &mem_payload.base, + 0, + (struct UdpardPayload){.data = &data[1], .size = 1}, + (struct UdpardMutablePayload){.data = data, .size = sizeof(data)}, + slot.fragments) + ->tree.base; + slot.fragments->base.lr[1] = &makeRxFragment(&mem_fragment.base, + &mem_payload.base, + 2, + (struct UdpardPayload){.data = &data[3], .size = 3}, + (struct UdpardMutablePayload){.data = data, .size = sizeof(data)}, + slot.fragments) + ->tree.base; + // Initialization done, ensure the memory utilization is as we expect. + TEST_ASSERT_EQUAL(3, mem_payload.allocated_fragments); + TEST_ASSERT_EQUAL(sizeof(data) * 3, mem_payload.allocated_bytes); + TEST_ASSERT_EQUAL(3, mem_fragment.allocated_fragments); + TEST_ASSERT_EQUAL(sizeof(RxFragment) * 3, mem_fragment.allocated_bytes); + // Now we reset the slot, causing all memory to be freed correctly. + rxSlotRestart(&slot, 0x1122334455667788ULL, &mem_fragment.base, &mem_payload.base); + TEST_ASSERT_EQUAL(TIMESTAMP_UNSET, slot.ts_usec); + TEST_ASSERT_EQUAL(0x1122334455667788ULL, slot.transfer_id); + TEST_ASSERT_EQUAL(0, slot.max_index); + TEST_ASSERT_EQUAL(FRAME_INDEX_UNSET, slot.eot_index); + TEST_ASSERT_EQUAL(0, slot.accepted_frames); + TEST_ASSERT_EQUAL(0, slot.payload_size); + TEST_ASSERT_EQUAL(NULL, slot.fragments); + // Ensure all memory was freed. + TEST_ASSERT_EQUAL(0, mem_payload.allocated_fragments); + TEST_ASSERT_EQUAL(0, mem_payload.allocated_bytes); + TEST_ASSERT_EQUAL(0, mem_fragment.allocated_fragments); + TEST_ASSERT_EQUAL(0, mem_fragment.allocated_bytes); +} + +static void testSlotEject(void) { // TODO } @@ -163,7 +282,10 @@ int main(void) RUN_TEST(testParseFrameUnknownHeaderVersion); RUN_TEST(testParseFrameHeaderWithoutPayload); RUN_TEST(testParseFrameEmpty); - RUN_TEST(testSessionSlotRestartEmpty); - RUN_TEST(testSessionSlotRestartNonEmpty); + RUN_TEST(testSlotRestartEmpty); + RUN_TEST(testSlotRestartNonEmpty); + RUN_TEST(testSlotEject); return UNITY_END(); } + +// NOLINTEND(clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling) diff --git a/tests/src/test_intrusive_tx.c b/tests/src/test_intrusive_tx.c index ac84aca..069376e 100644 --- a/tests/src/test_intrusive_tx.c +++ b/tests/src/test_intrusive_tx.c @@ -26,7 +26,7 @@ static const size_t InterstellarWarSize = sizeof(InterstellarWar) - 1; static const byte_t InterstellarWarCRC[4] = {102, 217, 109, 188}; // These aliases cannot be defined in the public API section: https://github.com/OpenCyphal-Garage/libudpard/issues/36 -typedef struct UdpardConstPayload UdpardConstPayload; +typedef struct UdpardPayload UdpardPayload; typedef struct UdpardUDPIPEndpoint UdpardUDPIPEndpoint; typedef struct UdpardTx UdpardTx; typedef struct UdpardTxItem UdpardTxItem; @@ -108,7 +108,7 @@ static void testMakeChainEmpty(void) 1234567890, meta, (UdpardUDPIPEndpoint){.ip_address = 0x0A0B0C0DU, .udp_port = 0x1234}, - (UdpardConstPayload){.size = 0, .data = ""}, + (UdpardPayload){.size = 0, .data = ""}, &user_transfer_referent); TEST_ASSERT_EQUAL(1, alloc.allocated_fragments); TEST_ASSERT_EQUAL(sizeof(TxItem) + HEADER_SIZE_BYTES + 4, alloc.allocated_bytes); @@ -151,7 +151,7 @@ static void testMakeChainSingleMaxMTU(void) 1234567890, meta, (UdpardUDPIPEndpoint){.ip_address = 0x0A0B0C00U, .udp_port = 7474}, - (UdpardConstPayload){.size = DetailOfTheCosmosSize, .data = DetailOfTheCosmos}, + (UdpardPayload){.size = DetailOfTheCosmosSize, .data = DetailOfTheCosmos}, &user_transfer_referent); TEST_ASSERT_EQUAL(1, alloc.allocated_fragments); TEST_ASSERT_EQUAL(sizeof(TxItem) + HEADER_SIZE_BYTES + DetailOfTheCosmosSize + TRANSFER_CRC_SIZE_BYTES, @@ -191,19 +191,18 @@ static void testMakeChainSingleFrameDefaultMTU(void) instrumentedAllocatorNew(&alloc); const byte_t payload[UDPARD_MTU_DEFAULT_MAX_SINGLE_FRAME + 1] = {0}; { // Ensure UDPARD_MTU_DEFAULT_MAX_SINGLE_FRAME bytes fit in a single frame with the default MTU. - const TxChain chain = - txMakeChain(&alloc.base, - (byte_t[]){11, 22, 33, 44, 55, 66, 77, 88}, - UDPARD_MTU_DEFAULT, - 1234567890, - (TransferMetadata){.priority = UdpardPrioritySlow, - .src_node_id = 4321, - .dst_node_id = 5432, - .data_specifier = 7766, - .transfer_id = 0x0123456789ABCDEFULL}, - (UdpardUDPIPEndpoint){.ip_address = 0x0A0B0C00U, .udp_port = 7474}, - (UdpardConstPayload){.size = UDPARD_MTU_DEFAULT_MAX_SINGLE_FRAME, .data = payload}, - NULL); + const TxChain chain = txMakeChain(&alloc.base, + (byte_t[]){11, 22, 33, 44, 55, 66, 77, 88}, + UDPARD_MTU_DEFAULT, + 1234567890, + (TransferMetadata){.priority = UdpardPrioritySlow, + .src_node_id = 4321, + .dst_node_id = 5432, + .data_specifier = 7766, + .transfer_id = 0x0123456789ABCDEFULL}, + (UdpardUDPIPEndpoint){.ip_address = 0x0A0B0C00U, .udp_port = 7474}, + (UdpardPayload){.size = UDPARD_MTU_DEFAULT_MAX_SINGLE_FRAME, .data = payload}, + NULL); TEST_ASSERT_EQUAL(1, alloc.allocated_fragments); TEST_ASSERT_EQUAL(sizeof(TxItem) + HEADER_SIZE_BYTES + UDPARD_MTU_DEFAULT_MAX_SINGLE_FRAME + TRANSFER_CRC_SIZE_BYTES, @@ -229,7 +228,7 @@ static void testMakeChainSingleFrameDefaultMTU(void) .data_specifier = 7766, .transfer_id = 0x0123456789ABCDEFULL}, (UdpardUDPIPEndpoint){.ip_address = 0x0A0B0C00U, .udp_port = 7474}, - (UdpardConstPayload){.size = UDPARD_MTU_DEFAULT_MAX_SINGLE_FRAME + 1, .data = payload}, + (UdpardPayload){.size = UDPARD_MTU_DEFAULT_MAX_SINGLE_FRAME + 1, .data = payload}, NULL); TEST_ASSERT_EQUAL(2, alloc.allocated_fragments); TEST_ASSERT_EQUAL((sizeof(TxItem) + HEADER_SIZE_BYTES) * 2 + UDPARD_MTU_DEFAULT_MAX_SINGLE_FRAME + 1 + @@ -264,7 +263,7 @@ static void testMakeChainThreeFrames(void) 223574680, meta, (UdpardUDPIPEndpoint){.ip_address = 0xBABADEDAU, .udp_port = 0xD0ED}, - (UdpardConstPayload){.size = EtherealStrengthSize, .data = EtherealStrength}, + (UdpardPayload){.size = EtherealStrengthSize, .data = EtherealStrength}, &user_transfer_referent); TEST_ASSERT_EQUAL(3, alloc.allocated_fragments); TEST_ASSERT_EQUAL(3 * (sizeof(TxItem) + HEADER_SIZE_BYTES) + EtherealStrengthSize + 4U, alloc.allocated_bytes); @@ -345,7 +344,7 @@ static void testMakeChainCRCSpill1(void) 223574680, meta, (UdpardUDPIPEndpoint){.ip_address = 0xBABADEDAU, .udp_port = 0xD0ED}, - (UdpardConstPayload){.size = InterstellarWarSize, .data = InterstellarWar}, + (UdpardPayload){.size = InterstellarWarSize, .data = InterstellarWar}, &user_transfer_referent); TEST_ASSERT_EQUAL(2, alloc.allocated_fragments); TEST_ASSERT_EQUAL(2 * (sizeof(TxItem) + HEADER_SIZE_BYTES) + InterstellarWarSize + 4U, alloc.allocated_bytes); @@ -416,7 +415,7 @@ static void testMakeChainCRCSpill2(void) 223574680, meta, (UdpardUDPIPEndpoint){.ip_address = 0xBABADEDAU, .udp_port = 0xD0ED}, - (UdpardConstPayload){.size = InterstellarWarSize, .data = InterstellarWar}, + (UdpardPayload){.size = InterstellarWarSize, .data = InterstellarWar}, &user_transfer_referent); TEST_ASSERT_EQUAL(2, alloc.allocated_fragments); TEST_ASSERT_EQUAL(2 * (sizeof(TxItem) + HEADER_SIZE_BYTES) + InterstellarWarSize + 4U, alloc.allocated_bytes); @@ -487,7 +486,7 @@ static void testMakeChainCRCSpill3(void) 223574680, meta, (UdpardUDPIPEndpoint){.ip_address = 0xBABADEDAU, .udp_port = 0xD0ED}, - (UdpardConstPayload){.size = InterstellarWarSize, .data = InterstellarWar}, + (UdpardPayload){.size = InterstellarWarSize, .data = InterstellarWar}, &user_transfer_referent); TEST_ASSERT_EQUAL(2, alloc.allocated_fragments); TEST_ASSERT_EQUAL(2 * (sizeof(TxItem) + HEADER_SIZE_BYTES) + InterstellarWarSize + 4U, alloc.allocated_bytes); @@ -558,7 +557,7 @@ static void testMakeChainCRCSpillFull(void) 223574680, meta, (UdpardUDPIPEndpoint){.ip_address = 0xBABADEDAU, .udp_port = 0xD0ED}, - (UdpardConstPayload){.size = InterstellarWarSize, .data = InterstellarWar}, + (UdpardPayload){.size = InterstellarWarSize, .data = InterstellarWar}, &user_transfer_referent); TEST_ASSERT_EQUAL(2, alloc.allocated_fragments); TEST_ASSERT_EQUAL(2 * (sizeof(TxItem) + HEADER_SIZE_BYTES) + InterstellarWarSize + 4U, alloc.allocated_bytes); @@ -634,7 +633,7 @@ static void testPushPeekPopFree(void) 1234567890U, meta, (UdpardUDPIPEndpoint){.ip_address = 0xBABADEDAU, .udp_port = 0xD0ED}, - (UdpardConstPayload){.size = EtherealStrengthSize, .data = EtherealStrength}, + (UdpardPayload){.size = EtherealStrengthSize, .data = EtherealStrength}, &user_transfer_referent)); TEST_ASSERT_EQUAL(3, alloc.allocated_fragments); TEST_ASSERT_EQUAL(3 * (sizeof(TxItem) + HEADER_SIZE_BYTES) + EtherealStrengthSize + 4U, alloc.allocated_bytes); @@ -712,7 +711,7 @@ static void testPushPrioritization(void) 0, meta_a, (UdpardUDPIPEndpoint){.ip_address = 0xAAAAAAAA, .udp_port = 0xAAAA}, - (UdpardConstPayload){.size = EtherealStrengthSize, .data = EtherealStrength}, + (UdpardPayload){.size = EtherealStrengthSize, .data = EtherealStrength}, NULL)); TEST_ASSERT_EQUAL(3, alloc.allocated_fragments); TEST_ASSERT_EQUAL(3, tx.queue_size); @@ -732,7 +731,7 @@ static void testPushPrioritization(void) .transfer_id = 100000, }, (UdpardUDPIPEndpoint){.ip_address = 0xBBBBBBBB, .udp_port = 0xBBBB}, - (UdpardConstPayload){.size = DetailOfTheCosmosSize, .data = DetailOfTheCosmos}, + (UdpardPayload){.size = DetailOfTheCosmosSize, .data = DetailOfTheCosmos}, NULL)); TEST_ASSERT_EQUAL(4, alloc.allocated_fragments); TEST_ASSERT_EQUAL(4, tx.queue_size); @@ -752,7 +751,7 @@ static void testPushPrioritization(void) .transfer_id = 10000, }, (UdpardUDPIPEndpoint){.ip_address = 0xCCCCCCCC, .udp_port = 0xCCCC}, - (UdpardConstPayload){.size = InterstellarWarSize, .data = InterstellarWar}, + (UdpardPayload){.size = InterstellarWarSize, .data = InterstellarWar}, NULL)); TEST_ASSERT_EQUAL(5, alloc.allocated_fragments); TEST_ASSERT_EQUAL(5, tx.queue_size); @@ -772,7 +771,7 @@ static void testPushPrioritization(void) .transfer_id = 10001, }, (UdpardUDPIPEndpoint){.ip_address = 0xDDDDDDDD, .udp_port = 0xDDDD}, - (UdpardConstPayload){.size = InterstellarWarSize, .data = InterstellarWar}, + (UdpardPayload){.size = InterstellarWarSize, .data = InterstellarWar}, NULL)); TEST_ASSERT_EQUAL(6, alloc.allocated_fragments); TEST_ASSERT_EQUAL(6, tx.queue_size); @@ -792,7 +791,7 @@ static void testPushPrioritization(void) .transfer_id = 1000, }, (UdpardUDPIPEndpoint){.ip_address = 0xEEEEEEEE, .udp_port = 0xEEEE}, - (UdpardConstPayload){.size = InterstellarWarSize, .data = InterstellarWar}, + (UdpardPayload){.size = InterstellarWarSize, .data = InterstellarWar}, NULL)); TEST_ASSERT_EQUAL(7, alloc.allocated_fragments); TEST_ASSERT_EQUAL(7, tx.queue_size); @@ -881,7 +880,7 @@ static void testPushCapacityLimit(void) 1234567890U, meta, (UdpardUDPIPEndpoint){.ip_address = 0xBABADEDAU, .udp_port = 0xD0ED}, - (UdpardConstPayload){.size = EtherealStrengthSize, .data = EtherealStrength}, + (UdpardPayload){.size = EtherealStrengthSize, .data = EtherealStrength}, NULL)); TEST_ASSERT_EQUAL(0, alloc.allocated_fragments); TEST_ASSERT_EQUAL(0, alloc.allocated_bytes); @@ -916,7 +915,7 @@ static void testPushOOM(void) 1234567890U, meta, (UdpardUDPIPEndpoint){.ip_address = 0xBABADEDAU, .udp_port = 0xD0ED}, - (UdpardConstPayload){.size = EtherealStrengthSize, .data = EtherealStrength}, + (UdpardPayload){.size = EtherealStrengthSize, .data = EtherealStrength}, NULL)); TEST_ASSERT_EQUAL(0, alloc.allocated_fragments); TEST_ASSERT_EQUAL(0, alloc.allocated_bytes); @@ -950,7 +949,7 @@ static void testPushAnonymousMultiFrame(void) 1234567890U, meta, (UdpardUDPIPEndpoint){.ip_address = 0xBABADEDAU, .udp_port = 0xD0ED}, - (UdpardConstPayload){.size = EtherealStrengthSize, .data = EtherealStrength}, + (UdpardPayload){.size = EtherealStrengthSize, .data = EtherealStrength}, NULL)); TEST_ASSERT_EQUAL(0, alloc.allocated_fragments); TEST_ASSERT_EQUAL(0, alloc.allocated_bytes); @@ -984,7 +983,7 @@ static void testPushAnonymousService(void) 1234567890U, meta, (UdpardUDPIPEndpoint){.ip_address = 0xBABADEDAU, .udp_port = 0xD0ED}, - (UdpardConstPayload){.size = EtherealStrengthSize, .data = EtherealStrength}, + (UdpardPayload){.size = EtherealStrengthSize, .data = EtherealStrength}, NULL)); TEST_ASSERT_EQUAL(0, alloc.allocated_fragments); TEST_ASSERT_EQUAL(0, alloc.allocated_bytes); From 77fdca150d1a51c0a346aa5b8a4d10d7c1aa92bd Mon Sep 17 00:00:00 2001 From: Pavel Kirienko Date: Thu, 27 Jul 2023 00:24:23 +0300 Subject: [PATCH 06/38] Finish the slot ejection tests --- libudpard/udpard.c | 115 ++++++++---- libudpard/udpard.h | 11 +- tests/.idea/dictionaries/pavel.xml | 2 + tests/src/test_intrusive_rx.c | 291 ++++++++++++++++++++++++++++- 4 files changed, 373 insertions(+), 46 deletions(-) diff --git a/libudpard/udpard.c b/libudpard/udpard.c index b5dba74..10c221b 100644 --- a/libudpard/udpard.c +++ b/libudpard/udpard.c @@ -783,6 +783,20 @@ typedef struct RxFragment uint32_t frame_index; } RxFragment; +static inline void rxFragmentFree(struct UdpardFragment* const head, + struct UdpardMemoryResource* const memory_fragment, + struct UdpardMemoryResource* const memory_payload) +{ + struct UdpardFragment* handle = head; + while (handle != NULL) + { + struct UdpardFragment* const next = handle->next; + memFreePayload(memory_payload, handle->origin); // May be NULL, is okay. + memFree(memory_fragment, sizeof(RxFragment), handle); + handle = next; + } +} + /// Internally, the RX pipeline is arranged as follows: /// /// - There is one port per subscription or an RPC-service listener. Within the port, there are N sessions, @@ -943,7 +957,10 @@ static inline void rxSlotEjectFragment(RxFragment* const frag, RxSlotEjectContex UDPARD_ASSERT((frag != NULL) && (ctx != NULL) && memIsValid(ctx->memory_fragment)); if (frag->tree.base.lr[0] != NULL) { - rxSlotEjectFragment(((RxFragmentTreeNode*) frag->tree.base.lr[0])->this, ctx); + RxFragment* const child = ((RxFragmentTreeNode*) frag->tree.base.lr[0])->this; + UDPARD_ASSERT(child->frame_index < frag->frame_index); + UDPARD_ASSERT(child->tree.base.up == &frag->tree.base); + rxSlotEjectFragment(child, ctx); } const size_t fragment_size = frag->base.view.size; frag->base.next = NULL; // Default state; may be overwritten. @@ -965,17 +982,16 @@ static inline void rxSlotEjectFragment(RxFragment* const frag, RxSlotEjectContex ctx->offset += fragment_size; if (frag->tree.base.lr[1] != NULL) { - rxSlotEjectFragment(((RxFragmentTreeNode*) frag->tree.base.lr[1])->this, ctx); + RxFragment* const child = ((RxFragmentTreeNode*) frag->tree.base.lr[1])->this; + UDPARD_ASSERT(child->frame_index > frag->frame_index); + UDPARD_ASSERT(child->tree.base.up == &frag->tree.base); + rxSlotEjectFragment(child, ctx); } // Drop the unneeded fragments and their handles after the sub-tree is fully traversed. if (!retain) { - memFree(ctx->memory_fragment, sizeof(RxFragment), frag); memFreePayload(ctx->memory_payload, frag->base.origin); - frag->base.origin.data = NULL; - frag->base.origin.size = 0; - frag->base.view.data = NULL; - frag->base.view.size = 0; + memFree(ctx->memory_fragment, sizeof(RxFragment), frag); } } @@ -990,37 +1006,40 @@ static inline void rxSlotEjectFragment(RxFragment* const frag, RxSlotEjectContex /// After this function is invoked, the tree will be destroyed and cannot be used anymore; /// hence, in the event of invalid transfer being received (bad CRC), the fragments will have to be freed /// by traversing the linked list instead of the tree. -static inline int_fast8_t rxSlotEject(RxSlot* const self, - const RxFrame frame, - struct UdpardRxTransfer* const rx_transfer, - const size_t extent, - struct UdpardMemoryResource* const memory_fragment, - struct UdpardMemoryResource* const memory_payload) +/// +/// The payload shall contain at least the transfer CRC, so the minimum size is TRANSFER_CRC_SIZE_BYTES. +/// There shall be at least one fragment (because a Cyphal transfer contains at least one frame). +/// +/// The return value indicates whether the transfer is valid (CRC is correct). +static inline bool rxSlotEject(size_t* const out_payload_size, + struct UdpardFragment* const out_payload_head, + RxFragmentTreeNode* const fragment_tree, + const size_t received_total_size, // With CRC. + const size_t extent, + struct UdpardMemoryResource* const memory_fragment, + struct UdpardMemoryResource* const memory_payload) { - int_fast8_t result = 0; + UDPARD_ASSERT((received_total_size >= TRANSFER_CRC_SIZE_BYTES) && (fragment_tree != NULL) && + (out_payload_size != NULL) && (out_payload_head != NULL)); + bool result = false; RxSlotEjectContext eject_ctx = { .head = NULL, .predecessor = NULL, .crc = TRANSFER_CRC_INITIAL, - .retain_size = smaller(self->payload_size - TRANSFER_CRC_SIZE_BYTES, extent), + .retain_size = smaller(received_total_size - TRANSFER_CRC_SIZE_BYTES, extent), .offset = 0, .memory_fragment = memory_fragment, .memory_payload = memory_payload, }; - rxSlotEjectFragment(self->fragments->this, &eject_ctx); - self->fragments = NULL; // Ejection invalidates the tree. - UDPARD_ASSERT(eject_ctx.offset == self->payload_size); + rxSlotEjectFragment(fragment_tree->this, &eject_ctx); + UDPARD_ASSERT(eject_ctx.offset == received_total_size); // Ensure we have traversed the entire tree. if (TRANSFER_CRC_RESIDUE_BEFORE_OUTPUT_XOR == eject_ctx.crc) { - result = 1; - rx_transfer->timestamp_usec = self->ts_usec; - rx_transfer->priority = frame.meta.priority; - rx_transfer->source_node_id = frame.meta.src_node_id; - rx_transfer->transfer_id = frame.meta.transfer_id; - rx_transfer->payload_size = eject_ctx.retain_size; - rx_transfer->payload = (eject_ctx.head != NULL) - ? (*eject_ctx.head) // Slice. The derived type fields are not needed. - : (struct UdpardFragment){.next = NULL, .view = {0, NULL}, .origin = {0, NULL}}; + result = true; + *out_payload_size = eject_ctx.retain_size; + *out_payload_head = (eject_ctx.head != NULL) + ? (*eject_ctx.head) // Slice off the derived type fields as they are not needed. + : (struct UdpardFragment){.next = NULL, .view = {0, NULL}, .origin = {0, NULL}}; // This is the single-frame transfer optimization suggested by Scott: we free the first fragment handle // early by moving the contents into the rx_transfer structure by value. // No need to free the payload buffer because it has been transferred to the transfer. @@ -1028,14 +1047,7 @@ static inline int_fast8_t rxSlotEject(RxSlot* const self, } else // The transfer turned out to be invalid. We have to free the fragments. Can't use the tree anymore. { - struct UdpardFragment* handle = eject_ctx.head; - while (handle != NULL) - { - struct UdpardFragment* const next = handle->next; - memFreePayload(memory_payload, handle->origin); - memFree(memory_fragment, sizeof(RxFragment), handle); - handle = next; - } + rxFragmentFree(eject_ctx.head, memory_fragment, memory_payload); } return result; } @@ -1072,7 +1084,7 @@ static inline int_fast8_t rxSlotUpdate(RxSlot* const self, goto finish; } // SECOND: Insert the fragment into the fragment tree. If it already exists, drop and free the duplicate. - UDPARD_ASSERT((self->max_index <= self->eot_index) && (self->accepted_frames < self->eot_index)); + UDPARD_ASSERT((self->max_index <= self->eot_index) && (self->accepted_frames <= self->eot_index)); RxSlotUpdateContext update_ctx = {.frame_index = frame.index, .accepted = false, .memory_fragment = memory_fragment}; @@ -1080,7 +1092,6 @@ static inline int_fast8_t rxSlotUpdate(RxSlot* const self, &update_ctx, &rxSlotFragmentSearch, &rxSlotFragmentFactory); - UDPARD_ASSERT((self->max_index <= self->eot_index) && (self->accepted_frames <= self->eot_index)); if (frag == NULL) { UDPARD_ASSERT(!update_ctx.accepted); @@ -1089,6 +1100,7 @@ static inline int_fast8_t rxSlotUpdate(RxSlot* const self, // No restart because there is hope that there will be enough memory when we receive a duplicate. goto finish; } + UDPARD_ASSERT(self->max_index <= self->eot_index); if (update_ctx.accepted) { UDPARD_ASSERT(frag->this->frame_index == frame.index); @@ -1099,13 +1111,30 @@ static inline int_fast8_t rxSlotUpdate(RxSlot* const self, } // THIRD: Detect transfer completion. If complete, eject the payload from the fragment tree and check its CRC. UDPARD_ASSERT(self->fragments != NULL); - if (self->accepted_frames == self->eot_index) + if (self->accepted_frames > self->eot_index) // Mind the off-by-one: cardinal vs. ordinal. { // This transfer is done but we don't know yet if it's valid. Either way, we need to prepare for the next one. restart = true; if (self->payload_size >= TRANSFER_CRC_SIZE_BYTES) { - result = rxSlotEject(self, frame, rx_transfer, extent, memory_fragment, memory_payload); + // NOLINTNEXTLINE(clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling) + (void) memset(rx_transfer, 0, sizeof(struct UdpardRxTransfer)); // Safety. + rx_transfer->timestamp_usec = self->ts_usec; + rx_transfer->priority = frame.meta.priority; + rx_transfer->source_node_id = frame.meta.src_node_id; + rx_transfer->transfer_id = frame.meta.transfer_id; + // + result = rxSlotEject(&rx_transfer->payload_size, + &rx_transfer->payload, + self->fragments, + self->payload_size, + extent, + memory_fragment, + memory_payload) + ? 1 + : 0; + // The tree is now unusable and the data is moved into rx_transfer. + self->fragments = NULL; } } finish: @@ -1182,6 +1211,14 @@ static inline int_fast8_t rxIfaceUpdate(RxIface* const sel return result; } +void udpardFragmentFree(const struct UdpardFragment head, + struct UdpardMemoryResource* const memory_fragment, + struct UdpardMemoryResource* const memory_payload) +{ + memFreePayload(memory_payload, head.origin); // May be NULL, is okay. + rxFragmentFree(head.next, memory_fragment, memory_payload); // The head is not heap-allocated so not freed. +} + int8_t udpardRxSubscriptionInit(struct UdpardRxSubscription* const self, const UdpardPortID subject_id, const size_t extent, diff --git a/libudpard/udpard.h b/libudpard/udpard.h index bb2d741..1ddcd7d 100644 --- a/libudpard/udpard.h +++ b/libudpard/udpard.h @@ -739,13 +739,16 @@ struct UdpardRxTransfer }; /// This is, essentially, a helper that frees the memory allocated for the payload and its fragment headers -/// using the correct memory resource. The application can do the same thing manually if it has access to the +/// using the correct memory resources. The application can do the same thing manually if it has access to the /// required context to compute the size, or if the memory resource implementation does not require deallocation size. /// +/// The head of the fragment list is passed by value so it is not freed. This is in line with the UdpardRxTransfer +/// design, where the head is stored by value to reduce indirection in small transfers. We call it Scott's Head. +/// /// If any of the arguments are NULL, the function has no effect. -void udpardRxTransferFree(struct UdpardRxTransfer* const self, - struct UdpardMemoryResource* const memory_fragment, - struct UdpardMemoryResource* const memory_payload); +void udpardFragmentFree(const struct UdpardFragment head, + struct UdpardMemoryResource* const memory_fragment, + struct UdpardMemoryResource* const memory_payload); // --------------------------------------------- SUBJECTS --------------------------------------------- diff --git a/tests/.idea/dictionaries/pavel.xml b/tests/.idea/dictionaries/pavel.xml index f0b924d..94ea6f3 100644 --- a/tests/.idea/dictionaries/pavel.xml +++ b/tests/.idea/dictionaries/pavel.xml @@ -8,6 +8,7 @@ dscp dudpard ghcr + iface ifaces intravehicular libcanard @@ -20,6 +21,7 @@ nosonar opencyphal profraw + pycyphal stringmakers udpard udpip diff --git a/tests/src/test_intrusive_rx.c b/tests/src/test_intrusive_rx.c index eb8456a..31c059f 100644 --- a/tests/src/test_intrusive_rx.c +++ b/tests/src/test_intrusive_rx.c @@ -179,6 +179,34 @@ static RxFragment* makeRxFragment(struct UdpardMemoryResource* const memory_frag return frag; } +/// This is a simple helper wrapper that constructs a new fragment using a null-terminated string as a payload. +static RxFragment* makeRxFragmentString(struct UdpardMemoryResource* const memory_fragment, + struct UdpardMemoryResource* const memory_payload, + const uint32_t frame_index, + const char* const payload, + RxFragmentTreeNode* const parent) +{ + const size_t sz = strlen(payload); + return makeRxFragment(memory_fragment, + memory_payload, + frame_index, + (struct UdpardPayload){.data = payload, .size = sz}, + (struct UdpardMutablePayload){.data = (void*) payload, .size = sz}, + parent); +} + +static bool compareMemory(const size_t expected_size, + const void* const expected, + const size_t actual_size, + const void* const actual) +{ + return (expected_size == actual_size) && (memcmp(expected, actual, expected_size) == 0); +} +static bool compareStringWithPayload(const char* const expected, const struct UdpardPayload payload) +{ + return compareMemory(strlen(expected), expected, payload.size, payload.data); +} + static void testSlotRestartEmpty(void) { RxSlot slot = { @@ -260,9 +288,263 @@ static void testSlotRestartNonEmpty(void) TEST_ASSERT_EQUAL(0, mem_fragment.allocated_bytes); } -static void testSlotEject(void) +static void testSlotEjectValidLarge(void) +{ + InstrumentedAllocator mem_fragment = {0}; + InstrumentedAllocator mem_payload = {0}; + instrumentedAllocatorNew(&mem_fragment); + instrumentedAllocatorNew(&mem_payload); + //>>> from pycyphal.transport.commons.crc import CRC32C + //>>> data = ... + //>>> CRC32C.new(data).value_as_bytes + static const char DaShi[] = "Da Shi, have you ever... considered certain ultimate philosophical questions? " + "For example, where does Man come from? " + "Where does Man go? " + "Where does the universe come from? "; + // Build the fragment tree: + // 2 + // / \ + // 1 3 + // / + // 0 + RxFragment* const root = // + makeRxFragmentString(&mem_fragment.base, // + &mem_payload.base, + 2, + "Where does Man go? ", + NULL); + root->tree.base.lr[0] = // + &makeRxFragmentString(&mem_fragment.base, // + &mem_payload.base, + 1, + "For example, where does Man come from? ", + &root->tree) + ->tree.base; + root->tree.base.lr[1] = // + &makeRxFragmentString(&mem_fragment.base, // + &mem_payload.base, + 3, + "Where does the universe come from? xL\xAE\xCB", + &root->tree) + ->tree.base; + root->tree.base.lr[0]->lr[0] = + &makeRxFragmentString(&mem_fragment.base, // + &mem_payload.base, + 0, + "Da Shi, have you ever... considered certain ultimate philosophical questions? ", + ((RxFragmentTreeNode*) root->tree.base.lr[0])) + ->tree.base; + // Initialization done, ensure the memory utilization is as we expect. + TEST_ASSERT_EQUAL(4, mem_payload.allocated_fragments); + TEST_ASSERT_EQUAL(sizeof(DaShi) - 1 + TRANSFER_CRC_SIZE_BYTES, mem_payload.allocated_bytes); + TEST_ASSERT_EQUAL(4, mem_fragment.allocated_fragments); + TEST_ASSERT_EQUAL(sizeof(RxFragment) * 4, mem_fragment.allocated_bytes); + // Eject and verify the payload. + size_t payload_size = 0; + struct UdpardFragment payload = {0}; + TEST_ASSERT(rxSlotEject(&payload_size, + &payload, + &root->tree, + mem_payload.allocated_bytes, + 1024, + &mem_fragment.base, + &mem_payload.base)); + TEST_ASSERT_EQUAL(sizeof(DaShi) - 1, payload_size); // CRC removed! + TEST_ASSERT( // + compareStringWithPayload("Da Shi, have you ever... considered certain ultimate philosophical questions? ", + payload.view)); + TEST_ASSERT(compareStringWithPayload("For example, where does Man come from? ", payload.next->view)); + TEST_ASSERT(compareStringWithPayload("Where does Man go? ", payload.next->next->view)); + TEST_ASSERT(compareStringWithPayload("Where does the universe come from? ", payload.next->next->next->view)); + TEST_ASSERT_NULL(payload.next->next->next->next); + // Check the memory utilization. All payload fragments are still kept, but the first fragment is freed because of + // the Scott's short payload optimization. + TEST_ASSERT_EQUAL(4, mem_payload.allocated_fragments); + TEST_ASSERT_EQUAL(sizeof(DaShi) - 1 + TRANSFER_CRC_SIZE_BYTES, mem_payload.allocated_bytes); + TEST_ASSERT_EQUAL(3, mem_fragment.allocated_fragments); // One gone!!1 + TEST_ASSERT_EQUAL(sizeof(RxFragment) * 3, mem_fragment.allocated_bytes); // yes yes! + // Now, free the payload as the application would. + udpardFragmentFree(payload, &mem_fragment.base, &mem_payload.base); + // All memory shall be free now. As in "free beer". + TEST_ASSERT_EQUAL(0, mem_payload.allocated_fragments); + TEST_ASSERT_EQUAL(0, mem_payload.allocated_bytes); + TEST_ASSERT_EQUAL(0, mem_fragment.allocated_fragments); + TEST_ASSERT_EQUAL(0, mem_fragment.allocated_bytes); +} + +static void testSlotEjectValidSmall(void) +{ + InstrumentedAllocator mem_fragment = {0}; + InstrumentedAllocator mem_payload = {0}; + instrumentedAllocatorNew(&mem_fragment); + instrumentedAllocatorNew(&mem_payload); + //>>> from pycyphal.transport.commons.crc import CRC32C + //>>> data = ... + //>>> CRC32C.new(data).value_as_bytes + static const char BuildSea[] = "Did you build this four-dimensional fragment?\n" + "You told me that you came from the sea. Did you build the sea?\n" + "Are you saying that for you, or at least for your creators, " + "this four-dimensional space is like the sea for us?\n" + "More like a puddle. The sea has gone dry."; + // Build the fragment tree: + // 1 + // / \ + // 0 3 + // / \ + // 2 4 + RxFragment* const root = // + makeRxFragmentString(&mem_fragment.base, // + &mem_payload.base, + 1, + "You told me that you came from the sea. Did you build the sea?\n", + NULL); + root->tree.base.lr[0] = // + &makeRxFragmentString(&mem_fragment.base, // + &mem_payload.base, + 0, + "Did you build this four-dimensional fragment?\n", + &root->tree) + ->tree.base; + root->tree.base.lr[1] = // + &makeRxFragmentString(&mem_fragment.base, // + &mem_payload.base, + 3, + "this four-dimensional space is like the sea for us?\n", + &root->tree) + ->tree.base; + root->tree.base.lr[1]->lr[0] = // + &makeRxFragmentString(&mem_fragment.base, // + &mem_payload.base, + 2, + "Are you saying that for you, or at least for your creators, ", + ((RxFragmentTreeNode*) root->tree.base.lr[1])) + ->tree.base; + root->tree.base.lr[1]->lr[1] = // + &makeRxFragmentString(&mem_fragment.base, // + &mem_payload.base, + 4, + "More like a puddle. The sea has gone dry.\xA2\x93-\xB2", + ((RxFragmentTreeNode*) root->tree.base.lr[1])) + ->tree.base; + // Initialization done, ensure the memory utilization is as we expect. + TEST_ASSERT_EQUAL(5, mem_payload.allocated_fragments); + TEST_ASSERT_EQUAL(sizeof(BuildSea) - 1 + TRANSFER_CRC_SIZE_BYTES, mem_payload.allocated_bytes); + TEST_ASSERT_EQUAL(5, mem_fragment.allocated_fragments); + TEST_ASSERT_EQUAL(sizeof(RxFragment) * 5, mem_fragment.allocated_bytes); + // Eject and verify the payload. Use a small extent and ensure the excess is dropped. + size_t payload_size = 0; + struct UdpardFragment payload = {0}; + TEST_ASSERT(rxSlotEject(&payload_size, + &payload, + &root->tree, + mem_payload.allocated_bytes, + 136, // <-- small extent, rest truncated + &mem_fragment.base, + &mem_payload.base)); + TEST_ASSERT_EQUAL(136, payload_size); // Equals the extent due to the truncation. + TEST_ASSERT(compareStringWithPayload("Did you build this four-dimensional fragment?\n", payload.view)); + TEST_ASSERT(compareStringWithPayload("You told me that you came from the sea. Did you build the sea?\n", + payload.next->view)); + TEST_ASSERT(compareStringWithPayload("Are you saying that for you", payload.next->next->view)); + TEST_ASSERT_NULL(payload.next->next->next); + // Check the memory utilization. + // The first fragment is freed because of the Scott's short payload optimization; + // the two last fragments are freed because of the truncation. + TEST_ASSERT_EQUAL(3, mem_payload.allocated_fragments); + TEST_ASSERT_EQUAL(169, mem_payload.allocated_bytes); // The last block is rounded up. + TEST_ASSERT_EQUAL(2, mem_fragment.allocated_fragments); // One gone!!1 + TEST_ASSERT_EQUAL(sizeof(RxFragment) * 2, mem_fragment.allocated_bytes); + // Now, free the payload as the application would. + udpardFragmentFree(payload, &mem_fragment.base, &mem_payload.base); + // All memory shall be free now. As in "free beer". + TEST_ASSERT_EQUAL(0, mem_payload.allocated_fragments); + TEST_ASSERT_EQUAL(0, mem_payload.allocated_bytes); + TEST_ASSERT_EQUAL(0, mem_fragment.allocated_fragments); + TEST_ASSERT_EQUAL(0, mem_fragment.allocated_bytes); +} + +static void testSlotEjectValidEmpty(void) +{ + InstrumentedAllocator mem_fragment = {0}; + InstrumentedAllocator mem_payload = {0}; + instrumentedAllocatorNew(&mem_fragment); + instrumentedAllocatorNew(&mem_payload); + // Build the fragment tree: + // 1 + // / \ + // 0 2 + RxFragment* const root = makeRxFragmentString(&mem_fragment.base, &mem_payload.base, 1, "BBB", NULL); + root->tree.base.lr[0] = + &makeRxFragmentString(&mem_fragment.base, &mem_payload.base, 0, "AAA", &root->tree)->tree.base; + root->tree.base.lr[1] = + &makeRxFragmentString(&mem_fragment.base, &mem_payload.base, 2, "P\xF5\xA5?", &root->tree)->tree.base; + // Initialization done, ensure the memory utilization is as we expect. + TEST_ASSERT_EQUAL(3, mem_payload.allocated_fragments); + TEST_ASSERT_EQUAL(6 + TRANSFER_CRC_SIZE_BYTES, mem_payload.allocated_bytes); + TEST_ASSERT_EQUAL(3, mem_fragment.allocated_fragments); + TEST_ASSERT_EQUAL(sizeof(RxFragment) * 3, mem_fragment.allocated_bytes); + // Eject and verify the payload. The extent is zero, so all payload is removed. + size_t payload_size = 0; + struct UdpardFragment payload = {0}; + TEST_ASSERT(rxSlotEject(&payload_size, + &payload, + &root->tree, + mem_payload.allocated_bytes, + 0, + &mem_fragment.base, + &mem_payload.base)); + TEST_ASSERT_EQUAL(0, payload_size); // Equals the extent due to the truncation. + TEST_ASSERT_NULL(payload.next); + TEST_ASSERT_EQUAL(0, payload.view.size); + // Check the memory utilization. No memory should be in use by this point. + TEST_ASSERT_EQUAL(0, mem_payload.allocated_fragments); + TEST_ASSERT_EQUAL(0, mem_payload.allocated_bytes); + TEST_ASSERT_EQUAL(0, mem_fragment.allocated_fragments); + TEST_ASSERT_EQUAL(0, mem_fragment.allocated_bytes); + // Now, free the payload as the application would. + udpardFragmentFree(payload, &mem_fragment.base, &mem_payload.base); + // No memory is in use anyway, so no change here. + TEST_ASSERT_EQUAL(0, mem_payload.allocated_fragments); + TEST_ASSERT_EQUAL(0, mem_payload.allocated_bytes); + TEST_ASSERT_EQUAL(0, mem_fragment.allocated_fragments); + TEST_ASSERT_EQUAL(0, mem_fragment.allocated_bytes); +} + +static void testSlotEjectInvalid(void) { - // TODO + InstrumentedAllocator mem_fragment = {0}; + InstrumentedAllocator mem_payload = {0}; + instrumentedAllocatorNew(&mem_fragment); + instrumentedAllocatorNew(&mem_payload); + // Build the fragment tree; no valid CRC here: + // 1 + // / \ + // 0 2 + RxFragment* const root = makeRxFragmentString(&mem_fragment.base, &mem_payload.base, 1, "BBB", NULL); + root->tree.base.lr[0] = + &makeRxFragmentString(&mem_fragment.base, &mem_payload.base, 0, "AAA", &root->tree)->tree.base; + root->tree.base.lr[1] = + &makeRxFragmentString(&mem_fragment.base, &mem_payload.base, 2, "CCC", &root->tree)->tree.base; + // Initialization done, ensure the memory utilization is as we expect. + TEST_ASSERT_EQUAL(3, mem_payload.allocated_fragments); + TEST_ASSERT_EQUAL(9, mem_payload.allocated_bytes); + TEST_ASSERT_EQUAL(3, mem_fragment.allocated_fragments); + TEST_ASSERT_EQUAL(sizeof(RxFragment) * 3, mem_fragment.allocated_bytes); + // Eject and verify the payload. + size_t payload_size = 0; + struct UdpardFragment payload = {0}; + TEST_ASSERT_FALSE(rxSlotEject(&payload_size, + &payload, + &root->tree, + mem_payload.allocated_bytes, + 1000, + &mem_fragment.base, + &mem_payload.base)); + // The call was unsuccessful, so the memory was freed instead of being handed over to the application. + TEST_ASSERT_EQUAL(0, mem_payload.allocated_fragments); + TEST_ASSERT_EQUAL(0, mem_payload.allocated_bytes); + TEST_ASSERT_EQUAL(0, mem_fragment.allocated_fragments); + TEST_ASSERT_EQUAL(0, mem_fragment.allocated_bytes); } void setUp(void) {} @@ -284,7 +566,10 @@ int main(void) RUN_TEST(testParseFrameEmpty); RUN_TEST(testSlotRestartEmpty); RUN_TEST(testSlotRestartNonEmpty); - RUN_TEST(testSlotEject); + RUN_TEST(testSlotEjectValidLarge); + RUN_TEST(testSlotEjectValidSmall); + RUN_TEST(testSlotEjectValidEmpty); + RUN_TEST(testSlotEjectInvalid); return UNITY_END(); } From f9fe9303a56f422cae72fc6a4bc844027d4a93e2 Mon Sep 17 00:00:00 2001 From: Pavel Kirienko Date: Thu, 27 Jul 2023 00:35:02 +0300 Subject: [PATCH 07/38] nit --- tests/src/test_intrusive_rx.c | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/tests/src/test_intrusive_rx.c b/tests/src/test_intrusive_rx.c index 31c059f..7eef2ca 100644 --- a/tests/src/test_intrusive_rx.c +++ b/tests/src/test_intrusive_rx.c @@ -303,7 +303,7 @@ static void testSlotEjectValidLarge(void) "Where does the universe come from? "; // Build the fragment tree: // 2 - // / \ + // / ` // 1 3 // / // 0 @@ -388,9 +388,9 @@ static void testSlotEjectValidSmall(void) "More like a puddle. The sea has gone dry."; // Build the fragment tree: // 1 - // / \ + // / ` // 0 3 - // / \ + // / ` // 2 4 RxFragment* const root = // makeRxFragmentString(&mem_fragment.base, // @@ -471,7 +471,7 @@ static void testSlotEjectValidEmpty(void) instrumentedAllocatorNew(&mem_payload); // Build the fragment tree: // 1 - // / \ + // / ` // 0 2 RxFragment* const root = makeRxFragmentString(&mem_fragment.base, &mem_payload.base, 1, "BBB", NULL); root->tree.base.lr[0] = @@ -518,7 +518,7 @@ static void testSlotEjectInvalid(void) instrumentedAllocatorNew(&mem_payload); // Build the fragment tree; no valid CRC here: // 1 - // / \ + // / ` // 0 2 RxFragment* const root = makeRxFragmentString(&mem_fragment.base, &mem_payload.base, 1, "BBB", NULL); root->tree.base.lr[0] = @@ -547,6 +547,11 @@ static void testSlotEjectInvalid(void) TEST_ASSERT_EQUAL(0, mem_fragment.allocated_bytes); } +static void testSlotUpdateA(void) +{ + // +} + void setUp(void) {} void tearDown(void) {} @@ -570,6 +575,7 @@ int main(void) RUN_TEST(testSlotEjectValidSmall); RUN_TEST(testSlotEjectValidEmpty); RUN_TEST(testSlotEjectInvalid); + RUN_TEST(testSlotUpdateA); return UNITY_END(); } From da7c3c1120475e804a53fc0def019adab66833be Mon Sep 17 00:00:00 2001 From: Pavel Kirienko Date: Thu, 27 Jul 2023 12:06:27 +0300 Subject: [PATCH 08/38] Slight internal refactoring to uphold orthogonality --- libudpard/udpard.c | 93 ++++++++++++++++++------------ tests/.idea/dictionaries/pavel.xml | 1 + tests/src/test_intrusive_rx.c | 76 ++++++++++++------------ 3 files changed, 93 insertions(+), 77 deletions(-) diff --git a/libudpard/udpard.c b/libudpard/udpard.c index 10c221b..3bff6b7 100644 --- a/libudpard/udpard.c +++ b/libudpard/udpard.c @@ -672,13 +672,20 @@ void udpardTxFree(struct UdpardMemoryResource* const memory, struct UdpardTxItem // ================================================= RX PIPELINE ================================================= // ===================================================================================================================== +/// All but the transfer metadata. typedef struct { - TransferMetadata meta; uint32_t index; bool end_of_transfer; struct UdpardPayload payload; ///< Also contains the transfer CRC (but not the header CRC). struct UdpardMutablePayload origin; ///< The entirety of the free-able buffer passed from the application. +} RxFrameBase; + +/// Full frame state. +typedef struct +{ + RxFrameBase base; + TransferMetadata meta; } RxFrame; /// The primitive deserialization functions are endian-agnostic. @@ -723,8 +730,8 @@ static inline const byte_t* txDeserializeU64(const byte_t* const source_buffer, static inline bool rxParseFrame(const struct UdpardMutablePayload datagram_payload, RxFrame* const out) { UDPARD_ASSERT((out != NULL) && (datagram_payload.data != NULL)); - out->origin = datagram_payload; - bool ok = false; + out->base.origin = datagram_payload; + bool ok = false; if (datagram_payload.size > 0) // HEADER_SIZE_BYTES may change in the future depending on the header version. { const byte_t* ptr = (const byte_t*) datagram_payload.data; @@ -736,33 +743,33 @@ static inline bool rxParseFrame(const struct UdpardMutablePayload datagram_paylo const uint_fast8_t prio = *ptr++; if (prio <= UDPARD_PRIORITY_MAX) { - out->meta.priority = (enum UdpardPriority) prio; - ptr = txDeserializeU16(ptr, &out->meta.src_node_id); - ptr = txDeserializeU16(ptr, &out->meta.dst_node_id); - ptr = txDeserializeU16(ptr, &out->meta.data_specifier); - ptr = txDeserializeU64(ptr, &out->meta.transfer_id); - uint32_t index_eot = 0; - ptr = txDeserializeU32(ptr, &index_eot); - out->index = (uint32_t) (index_eot & HEADER_FRAME_INDEX_MASK); - out->end_of_transfer = (index_eot & HEADER_FRAME_INDEX_EOT_MASK) != 0U; + out->meta.priority = (enum UdpardPriority) prio; + ptr = txDeserializeU16(ptr, &out->meta.src_node_id); + ptr = txDeserializeU16(ptr, &out->meta.dst_node_id); + ptr = txDeserializeU16(ptr, &out->meta.data_specifier); + ptr = txDeserializeU64(ptr, &out->meta.transfer_id); + uint32_t index_eot = 0; + ptr = txDeserializeU32(ptr, &index_eot); + out->base.index = (uint32_t) (index_eot & HEADER_FRAME_INDEX_MASK); + out->base.end_of_transfer = (index_eot & HEADER_FRAME_INDEX_EOT_MASK) != 0U; ptr += 2; // Opaque user data. ptr += HEADER_CRC_SIZE_BYTES; - out->payload.data = ptr; - out->payload.size = datagram_payload.size - HEADER_SIZE_BYTES; - ok = true; + out->base.payload.data = ptr; + out->base.payload.size = datagram_payload.size - HEADER_SIZE_BYTES; + ok = true; UDPARD_ASSERT((ptr == (((const byte_t*) datagram_payload.data) + HEADER_SIZE_BYTES)) && - (out->payload.size > 0U)); + (out->base.payload.size > 0U)); } } // Parsers for other header versions may be added here later. } if (ok) // Version-agnostic semantics check. { - UDPARD_ASSERT(out->payload.size > 0); // Follows from the prior checks. + UDPARD_ASSERT(out->base.payload.size > 0); // Follows from the prior checks. const bool anonymous = out->meta.src_node_id == UDPARD_NODE_ID_UNSET; const bool broadcast = out->meta.dst_node_id == UDPARD_NODE_ID_UNSET; const bool service = (out->meta.data_specifier & DATA_SPECIFIER_SERVICE_NOT_MESSAGE_MASK) != 0; - const bool single_frame = (out->index == 0) && out->end_of_transfer; + const bool single_frame = (out->base.index == 0) && out->base.end_of_transfer; ok = service ? ((!broadcast) && (!anonymous)) : (broadcast && ((!anonymous) || single_frame)); } return ok; @@ -1053,16 +1060,18 @@ static inline bool rxSlotEject(size_t* const out_payload_si } /// This function will either move the frame payload into the session, or free it if it cannot be made use of. -/// Returns: 1 -- transfer available; 0 -- transfer not yet available; <0 -- error. -static inline int_fast8_t rxSlotUpdate(RxSlot* const self, - const RxFrame frame, - struct UdpardRxTransfer* const rx_transfer, +/// Upon return, certain state variables may be overwritten, so the caller should not rely on them. +/// Returns: 1 -- transfer available, payload written; 0 -- transfer not yet available; <0 -- error. +static inline int_fast8_t rxSlotAccept(RxSlot* const self, + size_t* const out_transfer_payload_size, + struct UdpardFragment* const out_transfer_payload_head, + const RxFrameBase frame, const size_t extent, struct UdpardMemoryResource* const memory_fragment, struct UdpardMemoryResource* const memory_payload) { - UDPARD_ASSERT((self != NULL) && (self->transfer_id == frame.meta.transfer_id) && (frame.payload.size > 0) && - (rx_transfer != NULL) && memIsValid(memory_fragment)); + UDPARD_ASSERT((self != NULL) && (frame.payload.size > 0) && (out_transfer_payload_size != NULL) && + (out_transfer_payload_head != NULL) && memIsValid(memory_fragment)); int_fast8_t result = 0; bool restart = false; bool release = true; @@ -1080,7 +1089,7 @@ static inline int_fast8_t rxSlotUpdate(RxSlot* const self, UDPARD_ASSERT(frame.index <= self->max_index); if (self->max_index > self->eot_index) { - restart = true; // Frames past EOT, discard the entire transfer because we don't trust it. + restart = true; // Frames past EOT found, discard the entire transfer because we don't trust it anymore. goto finish; } // SECOND: Insert the fragment into the fragment tree. If it already exists, drop and free the duplicate. @@ -1117,15 +1126,8 @@ static inline int_fast8_t rxSlotUpdate(RxSlot* const self, restart = true; if (self->payload_size >= TRANSFER_CRC_SIZE_BYTES) { - // NOLINTNEXTLINE(clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling) - (void) memset(rx_transfer, 0, sizeof(struct UdpardRxTransfer)); // Safety. - rx_transfer->timestamp_usec = self->ts_usec; - rx_transfer->priority = frame.meta.priority; - rx_transfer->source_node_id = frame.meta.src_node_id; - rx_transfer->transfer_id = frame.meta.transfer_id; - // - result = rxSlotEject(&rx_transfer->payload_size, - &rx_transfer->payload, + result = rxSlotEject(out_transfer_payload_size, + out_transfer_payload_head, self->fragments, self->payload_size, extent, @@ -1140,7 +1142,7 @@ static inline int_fast8_t rxSlotUpdate(RxSlot* const self, finish: if (restart) { - rxSlotRestart(self, frame.meta.transfer_id + 1, memory_fragment, memory_payload); + rxSlotRestart(self, self->transfer_id + 1U, memory_fragment, memory_payload); } if (release) { @@ -1158,7 +1160,7 @@ static inline int_fast8_t rxIfaceUpdate(RxIface* const sel const UdpardMicrosecond transfer_id_timeout_usec, const struct UdpardRxMemoryResources memory) { - UDPARD_ASSERT((self != NULL) && (frame.payload.size > 0) && (received_transfer != NULL)); + UDPARD_ASSERT((self != NULL) && (frame.base.payload.size > 0) && (received_transfer != NULL)); RxSlot* slot = NULL; // First we should check if there is an existing slot for this transfer; if yes, this is the simplest case. for (uint_fast8_t i = 0; i < RX_SLOT_COUNT; i++) @@ -1202,11 +1204,26 @@ static inline int_fast8_t rxIfaceUpdate(RxIface* const sel { slot->ts_usec = ts_usec; // Transfer timestamp is the timestamp of the earliest frame. } - result = rxSlotUpdate(slot, frame, received_transfer, extent, memory.fragment, memory.payload); + const UdpardMicrosecond ts = slot->ts_usec; + UDPARD_ASSERT(slot->transfer_id == frame.meta.transfer_id); + result = rxSlotAccept(slot, // May invalidate state variables such as timestamp or transfer-ID. + &received_transfer->payload_size, + &received_transfer->payload, + frame.base, + extent, + memory.fragment, + memory.payload); + if (result > 0) // Transfer successfully received, populate the transfer descriptor for the client. + { + received_transfer->timestamp_usec = ts; + received_transfer->priority = frame.meta.priority; + received_transfer->source_node_id = frame.meta.src_node_id; + received_transfer->transfer_id = frame.meta.transfer_id; + } } else { - memFreePayload(memory.payload, frame.origin); + memFreePayload(memory.payload, frame.base.origin); } return result; } diff --git a/tests/.idea/dictionaries/pavel.xml b/tests/.idea/dictionaries/pavel.xml index 94ea6f3..60e0200 100644 --- a/tests/.idea/dictionaries/pavel.xml +++ b/tests/.idea/dictionaries/pavel.xml @@ -20,6 +20,7 @@ mmcu nosonar opencyphal + prio profraw pycyphal stringmakers diff --git a/tests/src/test_intrusive_rx.c b/tests/src/test_intrusive_rx.c index 7eef2ca..c537abc 100644 --- a/tests/src/test_intrusive_rx.c +++ b/tests/src/test_intrusive_rx.c @@ -30,12 +30,12 @@ static void testParseFrameValidMessage(void) TEST_ASSERT_EQUAL_UINT64(UDPARD_NODE_ID_UNSET, rxf.meta.dst_node_id); TEST_ASSERT_EQUAL_UINT64(7654, rxf.meta.data_specifier); TEST_ASSERT_EQUAL_UINT64(0xbadc0ffee0ddf00d, rxf.meta.transfer_id); - TEST_ASSERT_EQUAL_UINT64(12345, rxf.index); - TEST_ASSERT_FALSE(rxf.end_of_transfer); - TEST_ASSERT_EQUAL_UINT64(3, rxf.payload.size); - TEST_ASSERT_EQUAL_UINT8_ARRAY("abc", rxf.payload.data, 3); - TEST_ASSERT_EQUAL_UINT64(sizeof(data), rxf.origin.size); - TEST_ASSERT_EQUAL_UINT8_ARRAY(data, rxf.origin.data, sizeof(data)); + TEST_ASSERT_EQUAL_UINT64(12345, rxf.base.index); + TEST_ASSERT_FALSE(rxf.base.end_of_transfer); + TEST_ASSERT_EQUAL_UINT64(3, rxf.base.payload.size); + TEST_ASSERT_EQUAL_UINT8_ARRAY("abc", rxf.base.payload.data, 3); + TEST_ASSERT_EQUAL_UINT64(sizeof(data), rxf.base.origin.size); + TEST_ASSERT_EQUAL_UINT8_ARRAY(data, rxf.base.origin.data, sizeof(data)); } static void testParseFrameValidRPCService(void) @@ -55,12 +55,12 @@ static void testParseFrameValidRPCService(void) DATA_SPECIFIER_SERVICE_REQUEST_NOT_RESPONSE_MASK, rxf.meta.data_specifier); TEST_ASSERT_EQUAL_UINT64(0xbadc0ffee0ddf00d, rxf.meta.transfer_id); - TEST_ASSERT_EQUAL_UINT64(6654, rxf.index); - TEST_ASSERT_FALSE(rxf.end_of_transfer); - TEST_ASSERT_EQUAL_UINT64(3, rxf.payload.size); - TEST_ASSERT_EQUAL_UINT8_ARRAY("abc", rxf.payload.data, 3); - TEST_ASSERT_EQUAL_UINT64(sizeof(data), rxf.origin.size); - TEST_ASSERT_EQUAL_UINT8_ARRAY(data, rxf.origin.data, sizeof(data)); + TEST_ASSERT_EQUAL_UINT64(6654, rxf.base.index); + TEST_ASSERT_FALSE(rxf.base.end_of_transfer); + TEST_ASSERT_EQUAL_UINT64(3, rxf.base.payload.size); + TEST_ASSERT_EQUAL_UINT8_ARRAY("abc", rxf.base.payload.data, 3); + TEST_ASSERT_EQUAL_UINT64(sizeof(data), rxf.base.origin.size); + TEST_ASSERT_EQUAL_UINT8_ARRAY(data, rxf.base.origin.data, sizeof(data)); } static void testParseFrameValidMessageAnonymous(void) @@ -75,12 +75,12 @@ static void testParseFrameValidMessageAnonymous(void) TEST_ASSERT_EQUAL_UINT64(UDPARD_NODE_ID_UNSET, rxf.meta.dst_node_id); TEST_ASSERT_EQUAL_UINT64(7654, rxf.meta.data_specifier); TEST_ASSERT_EQUAL_UINT64(0xbadc0ffee0ddf00d, rxf.meta.transfer_id); - TEST_ASSERT_EQUAL_UINT64(0, rxf.index); - TEST_ASSERT_TRUE(rxf.end_of_transfer); - TEST_ASSERT_EQUAL_UINT64(3, rxf.payload.size); - TEST_ASSERT_EQUAL_UINT8_ARRAY("abc", rxf.payload.data, 3); - TEST_ASSERT_EQUAL_UINT64(sizeof(data), rxf.origin.size); - TEST_ASSERT_EQUAL_UINT8_ARRAY(data, rxf.origin.data, sizeof(data)); + TEST_ASSERT_EQUAL_UINT64(0, rxf.base.index); + TEST_ASSERT_TRUE(rxf.base.end_of_transfer); + TEST_ASSERT_EQUAL_UINT64(3, rxf.base.payload.size); + TEST_ASSERT_EQUAL_UINT8_ARRAY("abc", rxf.base.payload.data, 3); + TEST_ASSERT_EQUAL_UINT64(sizeof(data), rxf.base.origin.size); + TEST_ASSERT_EQUAL_UINT8_ARRAY(data, rxf.base.origin.data, sizeof(data)); } static void testParseFrameRPCServiceAnonymous(void) @@ -295,12 +295,8 @@ static void testSlotEjectValidLarge(void) instrumentedAllocatorNew(&mem_fragment); instrumentedAllocatorNew(&mem_payload); //>>> from pycyphal.transport.commons.crc import CRC32C - //>>> data = ... - //>>> CRC32C.new(data).value_as_bytes - static const char DaShi[] = "Da Shi, have you ever... considered certain ultimate philosophical questions? " - "For example, where does Man come from? " - "Where does Man go? " - "Where does the universe come from? "; + //>>> CRC32C.new(data_bytes).value_as_bytes + static const size_t PayloadSize = 171; // Build the fragment tree: // 2 // / ` @@ -336,7 +332,7 @@ static void testSlotEjectValidLarge(void) ->tree.base; // Initialization done, ensure the memory utilization is as we expect. TEST_ASSERT_EQUAL(4, mem_payload.allocated_fragments); - TEST_ASSERT_EQUAL(sizeof(DaShi) - 1 + TRANSFER_CRC_SIZE_BYTES, mem_payload.allocated_bytes); + TEST_ASSERT_EQUAL(PayloadSize + TRANSFER_CRC_SIZE_BYTES, mem_payload.allocated_bytes); TEST_ASSERT_EQUAL(4, mem_fragment.allocated_fragments); TEST_ASSERT_EQUAL(sizeof(RxFragment) * 4, mem_fragment.allocated_bytes); // Eject and verify the payload. @@ -349,8 +345,8 @@ static void testSlotEjectValidLarge(void) 1024, &mem_fragment.base, &mem_payload.base)); - TEST_ASSERT_EQUAL(sizeof(DaShi) - 1, payload_size); // CRC removed! - TEST_ASSERT( // + TEST_ASSERT_EQUAL(PayloadSize, payload_size); // CRC removed! + TEST_ASSERT( // compareStringWithPayload("Da Shi, have you ever... considered certain ultimate philosophical questions? ", payload.view)); TEST_ASSERT(compareStringWithPayload("For example, where does Man come from? ", payload.next->view)); @@ -360,7 +356,7 @@ static void testSlotEjectValidLarge(void) // Check the memory utilization. All payload fragments are still kept, but the first fragment is freed because of // the Scott's short payload optimization. TEST_ASSERT_EQUAL(4, mem_payload.allocated_fragments); - TEST_ASSERT_EQUAL(sizeof(DaShi) - 1 + TRANSFER_CRC_SIZE_BYTES, mem_payload.allocated_bytes); + TEST_ASSERT_EQUAL(PayloadSize + TRANSFER_CRC_SIZE_BYTES, mem_payload.allocated_bytes); TEST_ASSERT_EQUAL(3, mem_fragment.allocated_fragments); // One gone!!1 TEST_ASSERT_EQUAL(sizeof(RxFragment) * 3, mem_fragment.allocated_bytes); // yes yes! // Now, free the payload as the application would. @@ -379,13 +375,8 @@ static void testSlotEjectValidSmall(void) instrumentedAllocatorNew(&mem_fragment); instrumentedAllocatorNew(&mem_payload); //>>> from pycyphal.transport.commons.crc import CRC32C - //>>> data = ... - //>>> CRC32C.new(data).value_as_bytes - static const char BuildSea[] = "Did you build this four-dimensional fragment?\n" - "You told me that you came from the sea. Did you build the sea?\n" - "Are you saying that for you, or at least for your creators, " - "this four-dimensional space is like the sea for us?\n" - "More like a puddle. The sea has gone dry."; + //>>> CRC32C.new(data_bytes).value_as_bytes + static const size_t PayloadSize = 262; // Build the fragment tree: // 1 // / ` @@ -428,7 +419,7 @@ static void testSlotEjectValidSmall(void) ->tree.base; // Initialization done, ensure the memory utilization is as we expect. TEST_ASSERT_EQUAL(5, mem_payload.allocated_fragments); - TEST_ASSERT_EQUAL(sizeof(BuildSea) - 1 + TRANSFER_CRC_SIZE_BYTES, mem_payload.allocated_bytes); + TEST_ASSERT_EQUAL(PayloadSize + TRANSFER_CRC_SIZE_BYTES, mem_payload.allocated_bytes); TEST_ASSERT_EQUAL(5, mem_fragment.allocated_fragments); TEST_ASSERT_EQUAL(sizeof(RxFragment) * 5, mem_fragment.allocated_bytes); // Eject and verify the payload. Use a small extent and ensure the excess is dropped. @@ -547,9 +538,16 @@ static void testSlotEjectInvalid(void) TEST_ASSERT_EQUAL(0, mem_fragment.allocated_bytes); } -static void testSlotUpdateA(void) +static void testSlotAcceptA(void) { - // + InstrumentedAllocator mem_fragment = {0}; + InstrumentedAllocator mem_payload = {0}; + instrumentedAllocatorNew(&mem_fragment); + instrumentedAllocatorNew(&mem_payload); + size_t payload_size = 0; + struct UdpardFragment payload = {0}; + (void) payload_size; + (void) payload; } void setUp(void) {} @@ -575,7 +573,7 @@ int main(void) RUN_TEST(testSlotEjectValidSmall); RUN_TEST(testSlotEjectValidEmpty); RUN_TEST(testSlotEjectInvalid); - RUN_TEST(testSlotUpdateA); + RUN_TEST(testSlotAcceptA); return UNITY_END(); } From deb642687b09a8a152cf2ed9514c5339f5fd3c20 Mon Sep 17 00:00:00 2001 From: Pavel Kirienko Date: Thu, 27 Jul 2023 15:26:32 +0300 Subject: [PATCH 09/38] Add tests for rxSlotAccept() --- libudpard/udpard.c | 1 + tests/src/helpers.h | 6 +- tests/src/test_helpers.c | 8 +- tests/src/test_intrusive_rx.c | 580 ++++++++++++++++++++++++++++++---- 4 files changed, 528 insertions(+), 67 deletions(-) diff --git a/libudpard/udpard.c b/libudpard/udpard.c index 3bff6b7..036037d 100644 --- a/libudpard/udpard.c +++ b/libudpard/udpard.c @@ -1116,6 +1116,7 @@ static inline int_fast8_t rxSlotAccept(RxSlot* const self, frag->this->base.view = frame.payload; frag->this->base.origin = frame.origin; self->payload_size += frame.payload.size; + self->accepted_frames++; release = false; // Ownership of the payload buffer has been transferred to the fragment tree. } // THIRD: Detect transfer completion. If complete, eject the payload from the fragment tree and check its CRC. diff --git a/tests/src/helpers.h b/tests/src/helpers.h index 0a81967..394c8b9 100644 --- a/tests/src/helpers.h +++ b/tests/src/helpers.h @@ -61,6 +61,7 @@ typedef struct uint_least8_t canary[INSTRUMENTED_ALLOCATOR_CANARY_SIZE]; /// The limit can be changed at any moment to control the maximum amount of memory that can be allocated. /// It may be set to a value less than the currently allocated amount. + size_t limit_fragments; size_t limit_bytes; /// The current state of the allocator. size_t allocated_fragments; @@ -72,7 +73,9 @@ static inline void* instrumentedAllocatorAllocate(struct UdpardMemoryResource* c InstrumentedAllocator* const self = (InstrumentedAllocator*) base; TEST_PANIC_UNLESS(self->base.allocate == &instrumentedAllocatorAllocate); void* result = NULL; - if ((size > 0U) && ((self->allocated_bytes + size) <= self->limit_bytes)) + if ((size > 0U) && // + ((self->allocated_bytes + size) <= self->limit_bytes) && // + ((self->allocated_fragments + 1U) <= self->limit_fragments)) { const size_t size_with_canaries = size + ((size_t) INSTRUMENTED_ALLOCATOR_CANARY_SIZE * 2U); void* origin = malloc(size_with_canaries); @@ -142,6 +145,7 @@ static inline void instrumentedAllocatorNew(InstrumentedAllocator* const self) { self->canary[i] = (uint_least8_t) (rand() % (UINT_LEAST8_MAX + 1)); } + self->limit_fragments = SIZE_MAX; self->limit_bytes = SIZE_MAX; self->allocated_fragments = 0U; self->allocated_bytes = 0U; diff --git a/tests/src/test_helpers.c b/tests/src/test_helpers.c index 73d9369..752869b 100644 --- a/tests/src/test_helpers.c +++ b/tests/src/test_helpers.c @@ -19,12 +19,18 @@ static void testInstrumentedAllocator(void) TEST_ASSERT_EQUAL_size_t(2, al.allocated_fragments); TEST_ASSERT_EQUAL_size_t(579, al.allocated_bytes); - al.limit_bytes = 600; + al.limit_bytes = 600; + al.limit_fragments = 2; TEST_ASSERT_EQUAL_PTR(NULL, al.base.allocate(&al.base, 100)); TEST_ASSERT_EQUAL_size_t(2, al.allocated_fragments); TEST_ASSERT_EQUAL_size_t(579, al.allocated_bytes); + TEST_ASSERT_EQUAL_PTR(NULL, al.base.allocate(&al.base, 21)); + TEST_ASSERT_EQUAL_size_t(2, al.allocated_fragments); + TEST_ASSERT_EQUAL_size_t(579, al.allocated_bytes); + al.limit_fragments = 4; + void* c = al.base.allocate(&al.base, 21); TEST_ASSERT_EQUAL_size_t(3, al.allocated_fragments); TEST_ASSERT_EQUAL_size_t(600, al.allocated_bytes); diff --git a/tests/src/test_intrusive_rx.c b/tests/src/test_intrusive_rx.c index c537abc..a4c36af 100644 --- a/tests/src/test_intrusive_rx.c +++ b/tests/src/test_intrusive_rx.c @@ -9,6 +9,110 @@ // NOLINTBEGIN(clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling) +/// Moves the payload from the origin into a new buffer and attaches is to the newly allocated fragment. +/// This function performs two allocations. This function is infallible. +static RxFragment* makeRxFragment(struct UdpardMemoryResource* const memory_fragment, + struct UdpardMemoryResource* const memory_payload, + const uint32_t frame_index, + const struct UdpardPayload view, + const struct UdpardMutablePayload origin, + RxFragmentTreeNode* const parent) +{ + TEST_PANIC_UNLESS((view.data >= origin.data) && (view.size <= origin.size)); + TEST_PANIC_UNLESS((((const byte_t*) view.data) + view.size) <= (((const byte_t*) origin.data) + origin.size)); + byte_t* const new_origin = (byte_t*) memAlloc(memory_payload, origin.size); + RxFragment* const frag = (RxFragment*) memAlloc(memory_fragment, sizeof(RxFragment)); + if ((new_origin != NULL) && (frag != NULL)) + { + (void) memmove(new_origin, origin.data, origin.size); + (void) memset(frag, 0, sizeof(RxFragment)); + frag->tree.base.lr[0] = NULL; + frag->tree.base.lr[1] = NULL; + frag->tree.base.up = &parent->base; + frag->tree.this = frag; + frag->frame_index = frame_index; + frag->base.view = view; + frag->base.origin.data = new_origin; + frag->base.origin.size = origin.size; + frag->base.view.data = new_origin + (((const byte_t*) view.data) - ((byte_t*) origin.data)); + frag->base.view.size = view.size; + } + else + { + TEST_PANIC("Failed to allocate RxFragment"); + } + return frag; +} + +/// This is a simple helper wrapper that constructs a new fragment using a null-terminated string as a payload. +static RxFragment* makeRxFragmentString(struct UdpardMemoryResource* const memory_fragment, + struct UdpardMemoryResource* const memory_payload, + const uint32_t frame_index, + const char* const payload, + RxFragmentTreeNode* const parent) +{ + const size_t sz = strlen(payload); + return makeRxFragment(memory_fragment, + memory_payload, + frame_index, + (struct UdpardPayload){.data = payload, .size = sz}, + (struct UdpardMutablePayload){.data = (void*) payload, .size = sz}, + parent); +} + +static bool compareMemory(const size_t expected_size, + const void* const expected, + const size_t actual_size, + const void* const actual) +{ + return (expected_size == actual_size) && (memcmp(expected, actual, expected_size) == 0); +} +static bool compareStringWithPayload(const char* const expected, const struct UdpardPayload payload) +{ + return compareMemory(strlen(expected), expected, payload.size, payload.data); +} + +static RxFrameBase makeRxFrameBase(struct UdpardMemoryResource* const memory_payload, + const uint32_t frame_index, + const bool end_of_transfer, + const struct UdpardPayload view, + const struct UdpardMutablePayload origin) +{ + TEST_PANIC_UNLESS((view.data >= origin.data) && (view.size <= origin.size)); + TEST_PANIC_UNLESS((((const byte_t*) view.data) + view.size) <= (((const byte_t*) origin.data) + origin.size)); + RxFrameBase out = {0}; + byte_t* const new_origin = (byte_t*) memAlloc(memory_payload, origin.size); + if (new_origin != NULL) + { + (void) memmove(new_origin, origin.data, origin.size); + out.index = frame_index; + out.end_of_transfer = end_of_transfer; + out.origin.data = new_origin; + out.origin.size = origin.size; + out.payload.data = new_origin + (((const byte_t*) view.data) - ((byte_t*) origin.data)); + out.payload.size = view.size; + } + else + { + TEST_PANIC("Failed to allocate payload buffer for RxFrameBase"); + } + return out; +} + +static RxFrameBase makeRxFrameBaseString(struct UdpardMemoryResource* const memory_payload, + const uint32_t frame_index, + const bool end_of_transfer, + const char* const payload) +{ + return makeRxFrameBase(memory_payload, + frame_index, + end_of_transfer, + (struct UdpardPayload){.data = payload, .size = strlen(payload)}, + (struct UdpardMutablePayload){.data = (void*) payload, .size = strlen(payload)}); +} + +// -------------------------------------------------------------------------------------------------------------------- + // Generate reference data using PyCyphal: // // >>> from pycyphal.transport.udp import UDPFrame @@ -144,69 +248,6 @@ static void testParseFrameEmpty(void) TEST_ASSERT_FALSE(rxParseFrame((struct UdpardMutablePayload){.data = "", .size = 0}, &rxf)); } -/// Moves the payload from the origin into a new buffer and attaches is to the newly allocated fragment. -/// This function performs two allocations. This function is infallible. -static RxFragment* makeRxFragment(struct UdpardMemoryResource* const memory_fragment, - struct UdpardMemoryResource* const memory_payload, - const uint32_t frame_index, - const struct UdpardPayload view, - const struct UdpardMutablePayload origin, - RxFragmentTreeNode* const parent) -{ - TEST_PANIC_UNLESS((view.data >= origin.data) && (view.size <= origin.size)); - TEST_PANIC_UNLESS((((const byte_t*) view.data) + view.size) <= (((const byte_t*) origin.data) + origin.size)); - byte_t* const new_origin = (byte_t*) memAlloc(memory_payload, origin.size); - RxFragment* const frag = (RxFragment*) memAlloc(memory_fragment, sizeof(RxFragment)); - if ((new_origin != NULL) && (frag != NULL)) - { - (void) memmove(new_origin, origin.data, origin.size); - (void) memset(frag, 0, sizeof(RxFragment)); - frag->tree.base.lr[0] = NULL; - frag->tree.base.lr[1] = NULL; - frag->tree.base.up = &parent->base; - frag->tree.this = frag; - frag->frame_index = frame_index; - frag->base.view = view; - frag->base.origin.data = new_origin; - frag->base.origin.size = origin.size; - frag->base.view.data = new_origin + (((const byte_t*) view.data) - ((byte_t*) origin.data)); - frag->base.view.size = view.size; - } - else - { - TEST_PANIC("Failed to allocate RxFragment"); - } - return frag; -} - -/// This is a simple helper wrapper that constructs a new fragment using a null-terminated string as a payload. -static RxFragment* makeRxFragmentString(struct UdpardMemoryResource* const memory_fragment, - struct UdpardMemoryResource* const memory_payload, - const uint32_t frame_index, - const char* const payload, - RxFragmentTreeNode* const parent) -{ - const size_t sz = strlen(payload); - return makeRxFragment(memory_fragment, - memory_payload, - frame_index, - (struct UdpardPayload){.data = payload, .size = sz}, - (struct UdpardMutablePayload){.data = (void*) payload, .size = sz}, - parent); -} - -static bool compareMemory(const size_t expected_size, - const void* const expected, - const size_t actual_size, - const void* const actual) -{ - return (expected_size == actual_size) && (memcmp(expected, actual, expected_size) == 0); -} -static bool compareStringWithPayload(const char* const expected, const struct UdpardPayload payload) -{ - return compareMemory(strlen(expected), expected, payload.size, payload.data); -} - static void testSlotRestartEmpty(void) { RxSlot slot = { @@ -544,10 +585,419 @@ static void testSlotAcceptA(void) InstrumentedAllocator mem_payload = {0}; instrumentedAllocatorNew(&mem_fragment); instrumentedAllocatorNew(&mem_payload); + // Set up the RX slot instance we're going to be working with. + RxSlot slot = { + .ts_usec = 1234567890, + .transfer_id = 0x1122334455667788, + .max_index = 0, + .eot_index = FRAME_INDEX_UNSET, + .accepted_frames = 0, + .payload_size = 0, + .fragments = NULL, + }; size_t payload_size = 0; struct UdpardFragment payload = {0}; - (void) payload_size; - (void) payload; + + // === TRANSFER === + // Accept a single-frame transfer. Ownership transferred to the payload object. + //>>> from pycyphal.transport.commons.crc import CRC32C + //>>> CRC32C.new(data_bytes).value_as_bytes + TEST_ASSERT_EQUAL(1, + rxSlotAccept(&slot, + &payload_size, + &payload, + makeRxFrameBaseString(&mem_payload.base, + 0, + true, + "The fish responsible for drying the sea are not here." + "\x04\x1F\x8C\x1F"), + 1000, + &mem_fragment.base, + &mem_payload.base)); + // Verify the memory utilization. Note that the small transfer optimization is in effect: head fragment moved. + TEST_ASSERT_EQUAL(1, mem_payload.allocated_fragments); + TEST_ASSERT_EQUAL(53 + TRANSFER_CRC_SIZE_BYTES, mem_payload.allocated_bytes); + TEST_ASSERT_EQUAL(0, mem_fragment.allocated_fragments); + TEST_ASSERT_EQUAL(0, mem_fragment.allocated_bytes); + // Verify the payload and free it. Note the CRC is not part of the payload, obviously. + TEST_ASSERT_EQUAL(53, payload_size); + TEST_ASSERT(compareStringWithPayload("The fish responsible for drying the sea are not here.", payload.view)); + TEST_ASSERT_NULL(payload.next); + udpardFragmentFree(payload, &mem_fragment.base, &mem_payload.base); + TEST_ASSERT_EQUAL(0, mem_payload.allocated_fragments); + TEST_ASSERT_EQUAL(0, mem_payload.allocated_bytes); + TEST_ASSERT_EQUAL(0, mem_fragment.allocated_fragments); + TEST_ASSERT_EQUAL(0, mem_fragment.allocated_bytes); + // Ensure the slot has been restarted correctly. + TEST_ASSERT_EQUAL(TIMESTAMP_UNSET, slot.ts_usec); + TEST_ASSERT_EQUAL(0x1122334455667789, slot.transfer_id); // INCREMENTED + TEST_ASSERT_EQUAL(0, slot.max_index); + TEST_ASSERT_EQUAL(FRAME_INDEX_UNSET, slot.eot_index); + TEST_ASSERT_EQUAL(0, slot.accepted_frames); + TEST_ASSERT_EQUAL(0, slot.payload_size); + TEST_ASSERT_NULL(slot.fragments); + + // === TRANSFER === + // Accept a multi-frame transfer. Here, frames arrive in order. + TEST_ASSERT_EQUAL(0, + rxSlotAccept(&slot, + &payload_size, + &payload, + makeRxFrameBaseString(&mem_payload.base, + 0, + false, + "We're sorry. What you said is really hard to understand.\n"), + 1000, + &mem_fragment.base, + &mem_payload.base)); + TEST_ASSERT_EQUAL(0, + rxSlotAccept(&slot, + &payload_size, + &payload, + makeRxFrameBaseString(&mem_payload.base, + 1, + false, + "The fish who dried the sea went onto land before they did " + "this. "), + 1000, + &mem_fragment.base, + &mem_payload.base)); + TEST_ASSERT_EQUAL(1, + rxSlotAccept(&slot, + &payload_size, + &payload, + makeRxFrameBaseString(&mem_payload.base, + 2, + true, + "They moved from one dark forest to another dark forest." + "?\xAC(\xBE"), + 1000, + &mem_fragment.base, + &mem_payload.base)); + // Verify the memory utilization. Note that the small transfer optimization is in effect: head fragment moved. + TEST_ASSERT_EQUAL(3, mem_payload.allocated_fragments); + TEST_ASSERT_EQUAL(176 + TRANSFER_CRC_SIZE_BYTES, mem_payload.allocated_bytes); + TEST_ASSERT_EQUAL(2, mem_fragment.allocated_fragments); // One freed. + TEST_ASSERT_EQUAL(sizeof(RxFragment) * 2, mem_fragment.allocated_bytes); + // Verify the payload and free it. Note the CRC is not part of the payload, obviously. + TEST_ASSERT_EQUAL(176, payload_size); + TEST_ASSERT(compareStringWithPayload("We're sorry. What you said is really hard to understand.\n", payload.view)); + TEST_ASSERT_NOT_NULL(payload.next); + TEST_ASSERT(compareStringWithPayload("The fish who dried the sea went onto land before they did this. ", + payload.next->view)); + TEST_ASSERT_NOT_NULL(payload.next->next); + TEST_ASSERT(compareStringWithPayload("They moved from one dark forest to another dark forest.", // + payload.next->next->view)); + TEST_ASSERT_NULL(payload.next->next->next); + udpardFragmentFree(payload, &mem_fragment.base, &mem_payload.base); + TEST_ASSERT_EQUAL(0, mem_payload.allocated_fragments); + TEST_ASSERT_EQUAL(0, mem_payload.allocated_bytes); + TEST_ASSERT_EQUAL(0, mem_fragment.allocated_fragments); + TEST_ASSERT_EQUAL(0, mem_fragment.allocated_bytes); + // Ensure the slot has been restarted correctly. + TEST_ASSERT_EQUAL(TIMESTAMP_UNSET, slot.ts_usec); + TEST_ASSERT_EQUAL(0x112233445566778A, slot.transfer_id); // INCREMENTED + TEST_ASSERT_EQUAL(0, slot.max_index); + TEST_ASSERT_EQUAL(FRAME_INDEX_UNSET, slot.eot_index); + TEST_ASSERT_EQUAL(0, slot.accepted_frames); + TEST_ASSERT_EQUAL(0, slot.payload_size); + TEST_ASSERT_NULL(slot.fragments); + + // === TRANSFER === + // Accept an out-of-order transfer with extent truncation. Frames arrive out-of-order with duplicates. + TEST_ASSERT_EQUAL(0, mem_payload.allocated_fragments); + TEST_ASSERT_EQUAL(0, mem_fragment.allocated_fragments); + TEST_ASSERT_EQUAL(0, + rxSlotAccept(&slot, + &payload_size, + &payload, + makeRxFrameBaseString(&mem_payload.base, // + 2, + true, + "Toss it over." + "K(\xBB\xEE"), + 45, + &mem_fragment.base, + &mem_payload.base)); + TEST_ASSERT_EQUAL(1, mem_payload.allocated_fragments); + TEST_ASSERT_EQUAL(1, mem_fragment.allocated_fragments); + TEST_ASSERT_EQUAL(0, + rxSlotAccept(&slot, + &payload_size, + &payload, + makeRxFrameBaseString(&mem_payload.base, // + 1, + false, + "How do we give it to you?\n"), + 45, + &mem_fragment.base, + &mem_payload.base)); + TEST_ASSERT_EQUAL(2, mem_payload.allocated_fragments); + TEST_ASSERT_EQUAL(2, mem_fragment.allocated_fragments); + TEST_ASSERT_EQUAL(0, + rxSlotAccept(&slot, + &payload_size, + &payload, + makeRxFrameBaseString(&mem_payload.base, // + 1, + false, + "DUPLICATE #1"), + 45, + &mem_fragment.base, + &mem_payload.base)); + TEST_ASSERT_EQUAL(2, mem_payload.allocated_fragments); // NO CHANGE, duplicate discarded. + TEST_ASSERT_EQUAL(2, mem_fragment.allocated_fragments); + TEST_ASSERT_EQUAL(0, + rxSlotAccept(&slot, + &payload_size, + &payload, + makeRxFrameBaseString(&mem_payload.base, // + 2, + true, + "DUPLICATE #2"), + 45, + &mem_fragment.base, + &mem_payload.base)); + TEST_ASSERT_EQUAL(2, mem_payload.allocated_fragments); // NO CHANGE, duplicate discarded. + TEST_ASSERT_EQUAL(2, mem_fragment.allocated_fragments); + TEST_ASSERT_EQUAL(1, // transfer completed + rxSlotAccept(&slot, + &payload_size, + &payload, + makeRxFrameBaseString(&mem_payload.base, // + 0, + false, + "I like fish. Can I have it?\n"), + 45, + &mem_fragment.base, + &mem_payload.base)); + // Verify the memory utilization. Note that the small transfer optimization is in effect: head fragment moved. + // Due to the implicit truncation (the extent is small), the last fragment is already freed. + TEST_ASSERT_EQUAL(2, mem_payload.allocated_fragments); // One freed because of truncation. + TEST_ASSERT_EQUAL(28 + 26, mem_payload.allocated_bytes); + TEST_ASSERT_EQUAL(1, mem_fragment.allocated_fragments); // One freed because truncation, one optimized away. + TEST_ASSERT_EQUAL(sizeof(RxFragment) * 1, mem_fragment.allocated_bytes); + // Verify the payload and free it. Note the CRC is not part of the payload, obviously. + TEST_ASSERT_EQUAL(45, payload_size); // Equals the extent. + TEST_ASSERT(compareStringWithPayload("I like fish. Can I have it?\n", payload.view)); + TEST_ASSERT_NOT_NULL(payload.next); + TEST_ASSERT(compareStringWithPayload("How do we give it", payload.next->view)); // TRUNCATED + TEST_ASSERT_NULL(payload.next->next); + udpardFragmentFree(payload, &mem_fragment.base, &mem_payload.base); + TEST_ASSERT_EQUAL(0, mem_payload.allocated_fragments); + TEST_ASSERT_EQUAL(0, mem_payload.allocated_bytes); + TEST_ASSERT_EQUAL(0, mem_fragment.allocated_fragments); + TEST_ASSERT_EQUAL(0, mem_fragment.allocated_bytes); + // Ensure the slot has been restarted correctly. + TEST_ASSERT_EQUAL(TIMESTAMP_UNSET, slot.ts_usec); + TEST_ASSERT_EQUAL(0x112233445566778B, slot.transfer_id); // INCREMENTED + TEST_ASSERT_EQUAL(0, slot.max_index); + TEST_ASSERT_EQUAL(FRAME_INDEX_UNSET, slot.eot_index); + TEST_ASSERT_EQUAL(0, slot.accepted_frames); + TEST_ASSERT_EQUAL(0, slot.payload_size); + TEST_ASSERT_NULL(slot.fragments); + + // === TRANSFER === + // Shorter than TRANSFER_CRC_SIZE_BYTES, discarded early. + TEST_ASSERT_EQUAL(0, + rxSlotAccept(&slot, + &payload_size, + &payload, + makeRxFrameBaseString(&mem_payload.base, 0, true, ":D"), + 1000, + &mem_fragment.base, + &mem_payload.base)); + TEST_ASSERT_EQUAL(0, mem_payload.allocated_fragments); + TEST_ASSERT_EQUAL(0, mem_payload.allocated_bytes); + TEST_ASSERT_EQUAL(0, mem_fragment.allocated_fragments); + TEST_ASSERT_EQUAL(0, mem_fragment.allocated_bytes); + // Ensure the slot has been restarted correctly. + TEST_ASSERT_EQUAL(TIMESTAMP_UNSET, slot.ts_usec); + TEST_ASSERT_EQUAL(0x112233445566778C, slot.transfer_id); // INCREMENTED + TEST_ASSERT_EQUAL(0, slot.max_index); + TEST_ASSERT_EQUAL(FRAME_INDEX_UNSET, slot.eot_index); + TEST_ASSERT_EQUAL(0, slot.accepted_frames); + TEST_ASSERT_EQUAL(0, slot.payload_size); + TEST_ASSERT_NULL(slot.fragments); + + // === TRANSFER === + // OOM on reception. Note that the payload allocator does not require restrictions as the library does not + // allocate memory for the payload, only for the fragments. + mem_fragment.limit_fragments = 1; // Can only store one fragment, but the transfer requires more. + TEST_ASSERT_EQUAL(0, mem_payload.allocated_fragments); + TEST_ASSERT_EQUAL(0, mem_fragment.allocated_fragments); + TEST_ASSERT_EQUAL(0, + rxSlotAccept(&slot, + &payload_size, + &payload, + makeRxFrameBaseString(&mem_payload.base, // + 2, + true, + "Toss it over." + "K(\xBB\xEE"), + 1000, + &mem_fragment.base, + &mem_payload.base)); + TEST_ASSERT_EQUAL(1, mem_payload.allocated_fragments); + TEST_ASSERT_EQUAL(1, mem_fragment.allocated_fragments); // Limit reached here. Cannot accept next fragment. + TEST_ASSERT_EQUAL(-UDPARD_ERROR_MEMORY, + rxSlotAccept(&slot, + &payload_size, + &payload, + makeRxFrameBaseString(&mem_payload.base, // + 1, + false, + "How do we give it to you?\n"), + 1000, + &mem_fragment.base, + &mem_payload.base)); + TEST_ASSERT_EQUAL(1, mem_payload.allocated_fragments); // Payload not accepted, cannot alloc fragment. + TEST_ASSERT_EQUAL(1, mem_fragment.allocated_fragments); + mem_fragment.limit_fragments = 2; // Lift the limit and repeat the same frame, this time it is accepted. + TEST_ASSERT_EQUAL(0, + rxSlotAccept(&slot, + &payload_size, + &payload, + makeRxFrameBaseString(&mem_payload.base, // + 0, + false, + "I like fish. Can I have it?\n"), + 1000, + &mem_fragment.base, + &mem_payload.base)); + TEST_ASSERT_EQUAL(2, mem_payload.allocated_fragments); // Accepted! + TEST_ASSERT_EQUAL(2, mem_fragment.allocated_fragments); + TEST_ASSERT_EQUAL(-UDPARD_ERROR_MEMORY, // Cannot alloc third fragment. + rxSlotAccept(&slot, + &payload_size, + &payload, + makeRxFrameBaseString(&mem_payload.base, // + 1, + false, + "How do we give it to you?\n"), + 1000, + &mem_fragment.base, + &mem_payload.base)); + TEST_ASSERT_EQUAL(2, mem_payload.allocated_fragments); // Payload not accepted, cannot alloc fragment. + TEST_ASSERT_EQUAL(2, mem_fragment.allocated_fragments); + mem_fragment.limit_fragments = 3; // Lift the limit and repeat the same frame, this time it is accepted. + TEST_ASSERT_EQUAL(1, // transfer completed + rxSlotAccept(&slot, + &payload_size, + &payload, + makeRxFrameBaseString(&mem_payload.base, // + 1, + false, + "How do we give it to you?\n"), + 1000, + &mem_fragment.base, + &mem_payload.base)); + // Verify the memory utilization. Note that the small transfer optimization is in effect: head fragment moved. + TEST_ASSERT_EQUAL(3, mem_payload.allocated_fragments); + TEST_ASSERT_EQUAL(67 + TRANSFER_CRC_SIZE_BYTES, mem_payload.allocated_bytes); + TEST_ASSERT_EQUAL(2, mem_fragment.allocated_fragments); + TEST_ASSERT_EQUAL(sizeof(RxFragment) * 2, mem_fragment.allocated_bytes); + // Verify the payload and free it. Note the CRC is not part of the payload, obviously. + TEST_ASSERT_EQUAL(67, payload_size); // Equals the extent. + TEST_ASSERT(compareStringWithPayload("I like fish. Can I have it?\n", payload.view)); + TEST_ASSERT_NOT_NULL(payload.next); + TEST_ASSERT(compareStringWithPayload("How do we give it to you?\n", payload.next->view)); + TEST_ASSERT_NOT_NULL(payload.next->next); + TEST_ASSERT(compareStringWithPayload("Toss it over.", payload.next->next->view)); + TEST_ASSERT_NULL(payload.next->next->next); + udpardFragmentFree(payload, &mem_fragment.base, &mem_payload.base); + TEST_ASSERT_EQUAL(0, mem_payload.allocated_fragments); + TEST_ASSERT_EQUAL(0, mem_payload.allocated_bytes); + TEST_ASSERT_EQUAL(0, mem_fragment.allocated_fragments); + TEST_ASSERT_EQUAL(0, mem_fragment.allocated_bytes); + // Ensure the slot has been restarted correctly. + TEST_ASSERT_EQUAL(TIMESTAMP_UNSET, slot.ts_usec); + TEST_ASSERT_EQUAL(0x112233445566778D, slot.transfer_id); // INCREMENTED + TEST_ASSERT_EQUAL(0, slot.max_index); + TEST_ASSERT_EQUAL(FRAME_INDEX_UNSET, slot.eot_index); + TEST_ASSERT_EQUAL(0, slot.accepted_frames); + TEST_ASSERT_EQUAL(0, slot.payload_size); + TEST_ASSERT_NULL(slot.fragments); + + // === TRANSFER === + // Inconsistent EOT flag. + TEST_ASSERT_EQUAL(0, mem_payload.allocated_fragments); + TEST_ASSERT_EQUAL(0, mem_fragment.allocated_fragments); + TEST_ASSERT_EQUAL(0, // Just an ordinary transfer passing by, what could go wrong? + rxSlotAccept(&slot, + &payload_size, + &payload, + makeRxFrameBaseString(&mem_payload.base, // + 2, + true, + "Toss it over." + "K(\xBB\xEE"), + 45, + &mem_fragment.base, + &mem_payload.base)); + TEST_ASSERT_EQUAL(1, mem_payload.allocated_fragments); // Okay, accepted, some data stored... + TEST_ASSERT_EQUAL(1, mem_fragment.allocated_fragments); + TEST_ASSERT_EQUAL(0, + rxSlotAccept(&slot, + &payload_size, + &payload, + makeRxFrameBaseString(&mem_payload.base, // + 1, // + true, // SURPRISE! EOT is set in distinct frames! + "How do we give it to you?\n"), + 45, + &mem_fragment.base, + &mem_payload.base)); + TEST_ASSERT_EQUAL(0, mem_payload.allocated_fragments); // This is outrageous. Of course we have to drop everything. + TEST_ASSERT_EQUAL(0, mem_fragment.allocated_fragments); + // Ensure the slot has been restarted correctly. + TEST_ASSERT_EQUAL(TIMESTAMP_UNSET, slot.ts_usec); + TEST_ASSERT_EQUAL(0x112233445566778E, slot.transfer_id); // INCREMENTED + TEST_ASSERT_EQUAL(0, slot.max_index); + TEST_ASSERT_EQUAL(FRAME_INDEX_UNSET, slot.eot_index); + TEST_ASSERT_EQUAL(0, slot.accepted_frames); + TEST_ASSERT_EQUAL(0, slot.payload_size); + TEST_ASSERT_NULL(slot.fragments); + + // === TRANSFER === + // More frames past the EOT; or, in other words, the frame index where EOT is set is not the maximum index. + TEST_ASSERT_EQUAL(0, mem_payload.allocated_fragments); + TEST_ASSERT_EQUAL(0, mem_fragment.allocated_fragments); + TEST_ASSERT_EQUAL(0, + rxSlotAccept(&slot, + &payload_size, + &payload, + makeRxFrameBaseString(&mem_payload.base, // + 2, + true, + "Toss it over." + "K(\xBB\xEE"), + 45, + &mem_fragment.base, + &mem_payload.base)); + TEST_ASSERT_EQUAL(1, mem_payload.allocated_fragments); // Okay, accepted, some data stored... + TEST_ASSERT_EQUAL(1, mem_fragment.allocated_fragments); + TEST_ASSERT_EQUAL(0, + rxSlotAccept(&slot, + &payload_size, + &payload, + makeRxFrameBaseString(&mem_payload.base, // + 3, // SURPRISE! Frame #3 while #2 was EOT! + false, + "How do we give it to you?\n"), + 45, + &mem_fragment.base, + &mem_payload.base)); + TEST_ASSERT_EQUAL(0, mem_payload.allocated_fragments); // This is outrageous. Of course we have to drop everything. + TEST_ASSERT_EQUAL(0, mem_fragment.allocated_fragments); + // Ensure the slot has been restarted correctly. + TEST_ASSERT_EQUAL(TIMESTAMP_UNSET, slot.ts_usec); + TEST_ASSERT_EQUAL(0x112233445566778F, slot.transfer_id); // INCREMENTED + TEST_ASSERT_EQUAL(0, slot.max_index); + TEST_ASSERT_EQUAL(FRAME_INDEX_UNSET, slot.eot_index); + TEST_ASSERT_EQUAL(0, slot.accepted_frames); + TEST_ASSERT_EQUAL(0, slot.payload_size); + TEST_ASSERT_NULL(slot.fragments); } void setUp(void) {} From 8fe690c2a796f092487cb34e6ce88572a2fff6fb Mon Sep 17 00:00:00 2001 From: Pavel Kirienko Date: Thu, 27 Jul 2023 15:41:43 +0300 Subject: [PATCH 10/38] Iface acceptance test WIP --- libudpard/udpard.c | 43 +++++++++++++++++++++++++---------- tests/src/test_intrusive_rx.c | 25 ++++++++++++++++++++ 2 files changed, 56 insertions(+), 12 deletions(-) diff --git a/libudpard/udpard.c b/libudpard/udpard.c index 036037d..b2879a8 100644 --- a/libudpard/udpard.c +++ b/libudpard/udpard.c @@ -1152,14 +1152,17 @@ static inline int_fast8_t rxSlotAccept(RxSlot* const self, return result; } +/// This function is invoked when a new datagram pertaining to a certain session is received on an interface. +/// This function will either move the frame payload into the session, or free it if it cannot be made use of. /// Returns: 1 -- transfer available; 0 -- transfer not yet available; <0 -- error. -static inline int_fast8_t rxIfaceUpdate(RxIface* const self, - const UdpardMicrosecond ts_usec, - const RxFrame frame, - struct UdpardRxTransfer* const received_transfer, - const size_t extent, - const UdpardMicrosecond transfer_id_timeout_usec, - const struct UdpardRxMemoryResources memory) +static inline int_fast8_t rxIfaceAccept(RxIface* const self, + const UdpardMicrosecond ts_usec, + const RxFrame frame, + struct UdpardRxTransfer* const received_transfer, + const size_t extent, + const UdpardMicrosecond transfer_id_timeout_usec, + struct UdpardMemoryResource* const memory_fragment, + struct UdpardMemoryResource* const memory_payload) { UDPARD_ASSERT((self != NULL) && (frame.base.payload.size > 0) && (received_transfer != NULL)); RxSlot* slot = NULL; @@ -1191,7 +1194,7 @@ static inline int_fast8_t rxIfaceUpdate(RxIface* const sel const bool is_tid_timeout = (ts_usec - self->ts_usec) > transfer_id_timeout_usec; if (is_tid_timeout || is_future_tid) { - rxSlotRestart(victim, frame.meta.transfer_id, memory.fragment, memory.payload); + rxSlotRestart(victim, frame.meta.transfer_id, memory_fragment, memory_payload); slot = victim; UDPARD_ASSERT(slot != NULL); } @@ -1212,8 +1215,8 @@ static inline int_fast8_t rxIfaceUpdate(RxIface* const sel &received_transfer->payload, frame.base, extent, - memory.fragment, - memory.payload); + memory_fragment, + memory_payload); if (result > 0) // Transfer successfully received, populate the transfer descriptor for the client. { received_transfer->timestamp_usec = ts; @@ -1224,11 +1227,26 @@ static inline int_fast8_t rxIfaceUpdate(RxIface* const sel } else { - memFreePayload(memory.payload, frame.base.origin); + memFreePayload(memory_payload, frame.base.origin); } return result; } +static inline void rxIfaceInit(RxIface* const self, + struct UdpardMemoryResource* const memory_fragment, + struct UdpardMemoryResource* const memory_payload) +{ + UDPARD_ASSERT(self != NULL); + // NOLINTNEXTLINE(clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling) + (void) memset(self, 0, sizeof(*self)); + self->ts_usec = TIMESTAMP_UNSET; + for (uint_fast8_t i = 0; i < RX_SLOT_COUNT; i++) + { + self->slots[i].fragments = NULL; + rxSlotRestart(&self->slots[i], 0, memory_fragment, memory_payload); + } +} + void udpardFragmentFree(const struct UdpardFragment head, struct UdpardMemoryResource* const memory_fragment, struct UdpardMemoryResource* const memory_payload) @@ -1247,6 +1265,7 @@ int8_t udpardRxSubscriptionInit(struct UdpardRxSubscription* const self, (void) extent; (void) memory; (void) rxParseFrame; - (void) rxIfaceUpdate; + (void) rxIfaceInit; + (void) rxIfaceAccept; return 0; } diff --git a/tests/src/test_intrusive_rx.c b/tests/src/test_intrusive_rx.c index a4c36af..0d7c15f 100644 --- a/tests/src/test_intrusive_rx.c +++ b/tests/src/test_intrusive_rx.c @@ -111,6 +111,16 @@ static RxFrameBase makeRxFrameBaseString(struct UdpardMemoryResource* const memo (struct UdpardMutablePayload){.data = (void*) payload, .size = strlen(payload)}); } +static RxFrame makeRxFrameString(struct UdpardMemoryResource* const memory_payload, + const TransferMetadata meta, + const uint32_t frame_index, + const bool end_of_transfer, + const char* const payload) +{ + return (RxFrame){.base = makeRxFrameBaseString(memory_payload, frame_index, end_of_transfer, payload), + .meta = meta}; +} + // -------------------------------------------------------------------------------------------------------------------- // Generate reference data using PyCyphal: @@ -1000,6 +1010,17 @@ static void testSlotAcceptA(void) TEST_ASSERT_NULL(slot.fragments); } +static void testIfaceAcceptA(void) +{ + InstrumentedAllocator mem_fragment = {0}; + InstrumentedAllocator mem_payload = {0}; + instrumentedAllocatorNew(&mem_fragment); + instrumentedAllocatorNew(&mem_payload); + RxIface iface; + rxIfaceInit(&iface, &mem_fragment.base, &mem_payload.base); + (void) makeRxFrameString; +} + void setUp(void) {} void tearDown(void) {} @@ -1007,6 +1028,7 @@ void tearDown(void) {} int main(void) { UNITY_BEGIN(); + // frame parser RUN_TEST(testParseFrameValidMessage); RUN_TEST(testParseFrameValidRPCService); RUN_TEST(testParseFrameValidMessageAnonymous); @@ -1017,6 +1039,7 @@ int main(void) RUN_TEST(testParseFrameUnknownHeaderVersion); RUN_TEST(testParseFrameHeaderWithoutPayload); RUN_TEST(testParseFrameEmpty); + // slot RUN_TEST(testSlotRestartEmpty); RUN_TEST(testSlotRestartNonEmpty); RUN_TEST(testSlotEjectValidLarge); @@ -1024,6 +1047,8 @@ int main(void) RUN_TEST(testSlotEjectValidEmpty); RUN_TEST(testSlotEjectInvalid); RUN_TEST(testSlotAcceptA); + // iface + RUN_TEST(testIfaceAcceptA); return UNITY_END(); } From 84ec9b54c712449c54632933dcd51d92b7591e46 Mon Sep 17 00:00:00 2001 From: Pavel Kirienko Date: Thu, 27 Jul 2023 21:45:57 +0300 Subject: [PATCH 11/38] Partial tests for rxIfaceAccept --- libudpard/udpard.c | 29 ++-- tests/src/test_intrusive_rx.c | 283 +++++++++++++++++++++++++++++++++- 2 files changed, 302 insertions(+), 10 deletions(-) diff --git a/libudpard/udpard.c b/libudpard/udpard.c index b2879a8..c10db61 100644 --- a/libudpard/udpard.c +++ b/libudpard/udpard.c @@ -38,6 +38,7 @@ static const byte_t ByteMask = 0xFFU; #define RX_SLOT_COUNT 2 #define TIMESTAMP_UNSET UINT64_MAX #define FRAME_INDEX_UNSET UINT32_MAX +#define TRANSFER_ID_UNSET UINT64_MAX typedef struct { @@ -834,10 +835,10 @@ static inline void rxFragmentFree(struct UdpardFragment* const head, /// (this is possible because all common CRC functions are linear). typedef struct { - UdpardMicrosecond ts_usec; ///< Timestamp of the earliest frame; TIMESTAMP_UNSET initially. - UdpardTransferID transfer_id; - uint32_t max_index; ///< Maximum observed frame index in this transfer (so far); zero initially. - uint32_t eot_index; ///< Frame index where the EOT flag was observed; FRAME_INDEX_UNSET initially. + UdpardMicrosecond ts_usec; ///< Timestamp of the earliest frame; TIMESTAMP_UNSET upon restart. + UdpardTransferID transfer_id; ///< When first constructed, this shall be set to UINT64_MAX (unreachable value). + uint32_t max_index; ///< Maximum observed frame index in this transfer (so far); zero upon restart. + uint32_t eot_index; ///< Frame index where the EOT flag was observed; FRAME_INDEX_UNSET upon restart. uint32_t accepted_frames; ///< Number of frames accepted so far. size_t payload_size; RxFragmentTreeNode* fragments; @@ -1143,6 +1144,7 @@ static inline int_fast8_t rxSlotAccept(RxSlot* const self, finish: if (restart) { + // Increment is necessary to weed out duplicate transfers. rxSlotRestart(self, self->transfer_id + 1U, memory_fragment, memory_payload); } if (release) @@ -1185,13 +1187,21 @@ static inline int_fast8_t rxIfaceAccept(RxIface* const self, // While we're at it, find the oldest slot as a replacement candidate. for (uint_fast8_t i = 0; i < RX_SLOT_COUNT; i++) { - is_future_tid = is_future_tid && (frame.meta.transfer_id > self->slots[i].transfer_id); - if (self->slots[i].transfer_id < victim->transfer_id) + RxSlot* const it = &self->slots[i]; + is_future_tid = is_future_tid && ((it->transfer_id < frame.meta.transfer_id) || // Keep state if unused. + (it->transfer_id == TRANSFER_ID_UNSET)); + // The timestamp is UNSET when the slot is waiting for the next transfer (or unused -- special case). + // Such slots are the best candidates for replacement because reusing them does not cause loss of + // transfers that are in the process of being reassembled. If there are no such slots, we must + // sacrifice the one whose first frame has arrived the longest time ago. + if ((it->ts_usec == TIMESTAMP_UNSET) || + ((victim->ts_usec != TIMESTAMP_UNSET) && (it->ts_usec < victim->ts_usec))) { - victim = &self->slots[i]; + victim = it; } } - const bool is_tid_timeout = (ts_usec - self->ts_usec) > transfer_id_timeout_usec; + const bool is_tid_timeout = (self->ts_usec == TIMESTAMP_UNSET) || // + ((ts_usec - self->ts_usec) > transfer_id_timeout_usec); if (is_tid_timeout || is_future_tid) { rxSlotRestart(victim, frame.meta.transfer_id, memory_fragment, memory_payload); @@ -1219,6 +1229,7 @@ static inline int_fast8_t rxIfaceAccept(RxIface* const self, memory_payload); if (result > 0) // Transfer successfully received, populate the transfer descriptor for the client. { + self->ts_usec = ts; received_transfer->timestamp_usec = ts; received_transfer->priority = frame.meta.priority; received_transfer->source_node_id = frame.meta.src_node_id; @@ -1243,7 +1254,7 @@ static inline void rxIfaceInit(RxIface* const self, for (uint_fast8_t i = 0; i < RX_SLOT_COUNT; i++) { self->slots[i].fragments = NULL; - rxSlotRestart(&self->slots[i], 0, memory_fragment, memory_payload); + rxSlotRestart(&self->slots[i], TRANSFER_ID_UNSET, memory_fragment, memory_payload); } } diff --git a/tests/src/test_intrusive_rx.c b/tests/src/test_intrusive_rx.c index 0d7c15f..a50b4ee 100644 --- a/tests/src/test_intrusive_rx.c +++ b/tests/src/test_intrusive_rx.c @@ -1018,7 +1018,288 @@ static void testIfaceAcceptA(void) instrumentedAllocatorNew(&mem_payload); RxIface iface; rxIfaceInit(&iface, &mem_fragment.base, &mem_payload.base); - (void) makeRxFrameString; + TEST_ASSERT_EQUAL(TIMESTAMP_UNSET, iface.ts_usec); + for (size_t i = 0; i < RX_SLOT_COUNT; i++) + { + TEST_ASSERT_EQUAL(TIMESTAMP_UNSET, iface.slots[i].ts_usec); + TEST_ASSERT_EQUAL(TRANSFER_ID_UNSET, iface.slots[i].transfer_id); + TEST_ASSERT_EQUAL(0, iface.slots[i].max_index); + TEST_ASSERT_EQUAL(FRAME_INDEX_UNSET, iface.slots[i].eot_index); + TEST_ASSERT_EQUAL(0, iface.slots[i].accepted_frames); + TEST_ASSERT_EQUAL(0, iface.slots[i].payload_size); + TEST_ASSERT_NULL(iface.slots[i].fragments); + } + struct UdpardRxTransfer transfer = {0}; + + // === TRANSFER === + // A simple single-frame transfer successfully accepted. + TEST_ASSERT_EQUAL(1, + rxIfaceAccept(&iface, + 1234567890, + makeRxFrameString(&mem_payload.base, // + (TransferMetadata){.priority = UdpardPriorityHigh, + .src_node_id = 1234, + .dst_node_id = UDPARD_NODE_ID_UNSET, + .data_specifier = 0x1234, + .transfer_id = 0x1122334455667788U}, + 0, + true, + "I am a tomb." + "\x1F\\\xCDs"), + &transfer, + 1000, + UDPARD_DEFAULT_TRANSFER_ID_TIMEOUT_USEC, + &mem_fragment.base, + &mem_payload.base)); + TEST_ASSERT_EQUAL(1, mem_payload.allocated_fragments); + TEST_ASSERT_EQUAL(0, mem_fragment.allocated_fragments); // Head fragment is not heap-allocated. + // Check the transfer we just accepted. + TEST_ASSERT_EQUAL(1234567890, transfer.timestamp_usec); + TEST_ASSERT_EQUAL(UdpardPriorityHigh, transfer.priority); + TEST_ASSERT_EQUAL(1234, transfer.source_node_id); + TEST_ASSERT_EQUAL(0x1122334455667788U, transfer.transfer_id); + TEST_ASSERT_EQUAL(12, transfer.payload_size); + TEST_ASSERT(compareStringWithPayload("I am a tomb.", transfer.payload.view)); + udpardFragmentFree(transfer.payload, &mem_fragment.base, &mem_payload.base); + TEST_ASSERT_EQUAL(0, mem_payload.allocated_fragments); + TEST_ASSERT_EQUAL(0, mem_fragment.allocated_fragments); + // Check the internal states of the iface. + TEST_ASSERT_EQUAL(1234567890, iface.ts_usec); + TEST_ASSERT_EQUAL(TRANSFER_ID_UNSET, iface.slots[0].transfer_id); // Still unused. + TEST_ASSERT_EQUAL(0x1122334455667789U, iface.slots[1].transfer_id); // Incremented. + + // === TRANSFER === + // Send a duplicate and ensure it is rejected. + TEST_ASSERT_EQUAL(0, // No transfer accepted. + rxIfaceAccept(&iface, + 1234567891, // different timestamp but ignored anyway + makeRxFrameString(&mem_payload.base, // + (TransferMetadata){.priority = UdpardPriorityHigh, + .src_node_id = 1234, + .dst_node_id = UDPARD_NODE_ID_UNSET, + .data_specifier = 0x1234, + .transfer_id = 0x1122334455667788U}, + 0, + true, + "I am a tomb." + "\x1F\\\xCDs"), + &transfer, + 1000, + UDPARD_DEFAULT_TRANSFER_ID_TIMEOUT_USEC, + &mem_fragment.base, + &mem_payload.base)); + TEST_ASSERT_EQUAL(0, mem_payload.allocated_fragments); + TEST_ASSERT_EQUAL(0, mem_fragment.allocated_fragments); + // Check the internal states of the iface. + TEST_ASSERT_EQUAL(1234567890, iface.ts_usec); // same old timestamp + TEST_ASSERT_EQUAL(TRANSFER_ID_UNSET, iface.slots[0].transfer_id); // Still unused. + TEST_ASSERT_EQUAL(0x1122334455667789U, iface.slots[1].transfer_id); // good ol' transfer id + + // === TRANSFER === + // Send a non-duplicate transfer with an invalid CRC using an in-sequence (matching) transfer-ID. + TEST_ASSERT_EQUAL(0, // No transfer accepted. + rxIfaceAccept(&iface, + 1234567892, // different timestamp but ignored anyway + makeRxFrameString(&mem_payload.base, // + (TransferMetadata){.priority = UdpardPriorityHigh, + .src_node_id = 1234, + .dst_node_id = UDPARD_NODE_ID_UNSET, + .data_specifier = 0x1234, + .transfer_id = 0x1122334455667789U}, + 0, + true, + "I am a tomb." + "No CRC here."), + &transfer, + 1000, + UDPARD_DEFAULT_TRANSFER_ID_TIMEOUT_USEC, + &mem_fragment.base, + &mem_payload.base)); + TEST_ASSERT_EQUAL(0, mem_payload.allocated_fragments); + TEST_ASSERT_EQUAL(0, mem_fragment.allocated_fragments); + // Check the internal states of the iface. + TEST_ASSERT_EQUAL(1234567890, iface.ts_usec); // same old timestamp + TEST_ASSERT_EQUAL(TRANSFER_ID_UNSET, iface.slots[0].transfer_id); // Still unused. + TEST_ASSERT_EQUAL(0x112233445566778AU, iface.slots[1].transfer_id); // Incremented. + + // === TRANSFER === + // Send a non-duplicate transfer with an invalid CRC using an out-of-sequence (non-matching) transfer-ID. + // Transfer-ID jumps forward, no existing slot; will use the second one. + TEST_ASSERT_EQUAL(0, // No transfer accepted. + rxIfaceAccept(&iface, + 1234567893, // different timestamp but ignored anyway + makeRxFrameString(&mem_payload.base, // + (TransferMetadata){.priority = UdpardPriorityHigh, + .src_node_id = 1234, + .dst_node_id = UDPARD_NODE_ID_UNSET, + .data_specifier = 0x1234, + .transfer_id = 0x1122334455667790U}, + 0, + true, + "I am a tomb." + "No CRC here, #2."), + &transfer, + 1000, + UDPARD_DEFAULT_TRANSFER_ID_TIMEOUT_USEC, + &mem_fragment.base, + &mem_payload.base)); + TEST_ASSERT_EQUAL(0, mem_payload.allocated_fragments); + TEST_ASSERT_EQUAL(0, mem_fragment.allocated_fragments); + // Check the internal states of the iface. + TEST_ASSERT_EQUAL(1234567890, iface.ts_usec); // same old timestamp + TEST_ASSERT_EQUAL(TRANSFER_ID_UNSET, iface.slots[0].transfer_id); // Still unused. + TEST_ASSERT_EQUAL(0x1122334455667791U, iface.slots[1].transfer_id); // Replaced the old one, it was unneeded. + + // === TRANSFER === (x2) + // Send two interleaving multi-frame out-of-order transfers with duplicate frames: + // B1 A2 A0 B0 A1 + TEST_ASSERT_EQUAL(0, mem_payload.allocated_fragments); + TEST_ASSERT_EQUAL(0, mem_fragment.allocated_fragments); + // B1 + TEST_ASSERT_EQUAL(0, + rxIfaceAccept(&iface, + 2000000010, // Transfer-ID timeout. + makeRxFrameString(&mem_payload.base, // + (TransferMetadata){.priority = UdpardPrioritySlow, + .src_node_id = 2222, + .dst_node_id = UDPARD_NODE_ID_UNSET, + .data_specifier = 0x2222, + .transfer_id = 1001U}, + 1, + true, + "B1" + "g\x8D\x9A\xD7"), + &transfer, + 1000, + UDPARD_DEFAULT_TRANSFER_ID_TIMEOUT_USEC, + &mem_fragment.base, + &mem_payload.base)); + TEST_ASSERT_EQUAL(1234567890, iface.ts_usec); // same old timestamp + TEST_ASSERT_EQUAL(TRANSFER_ID_UNSET, iface.slots[0].transfer_id); // Still unused. + TEST_ASSERT_EQUAL(1001, iface.slots[1].transfer_id); // Replaced the old one, it was unneeded. + TEST_ASSERT_EQUAL(1, mem_payload.allocated_fragments); + TEST_ASSERT_EQUAL(1, mem_fragment.allocated_fragments); + // A2 + TEST_ASSERT_EQUAL(0, + rxIfaceAccept(&iface, + 2000000020, + makeRxFrameString(&mem_payload.base, // + (TransferMetadata){.priority = UdpardPriorityHigh, + .src_node_id = 1111, + .dst_node_id = UDPARD_NODE_ID_UNSET, + .data_specifier = 0x1111, + .transfer_id = 1000U}, + 2, + true, + "A2" + "v\x1E\xBD]"), + &transfer, + 1000, + UDPARD_DEFAULT_TRANSFER_ID_TIMEOUT_USEC, + &mem_fragment.base, + &mem_payload.base)); + TEST_ASSERT_EQUAL(1234567890, iface.ts_usec); // same old timestamp + TEST_ASSERT_EQUAL(1000, iface.slots[0].transfer_id); // Used for A because the other one is taken. + TEST_ASSERT_EQUAL(1001, iface.slots[1].transfer_id); // Keeps B because it is in-progress, can't discard. + TEST_ASSERT_EQUAL(2, mem_payload.allocated_fragments); + TEST_ASSERT_EQUAL(2, mem_fragment.allocated_fragments); + // A0 + TEST_ASSERT_EQUAL(0, + rxIfaceAccept(&iface, + 2000000030, + makeRxFrameString(&mem_payload.base, // + (TransferMetadata){.priority = UdpardPriorityHigh, + .src_node_id = 1111, + .dst_node_id = UDPARD_NODE_ID_UNSET, + .data_specifier = 0x1111, + .transfer_id = 1000U}, + 0, + false, + "A0"), + &transfer, + 1000, + UDPARD_DEFAULT_TRANSFER_ID_TIMEOUT_USEC, + &mem_fragment.base, + &mem_payload.base)); + TEST_ASSERT_EQUAL(1234567890, iface.ts_usec); // same old timestamp + TEST_ASSERT_EQUAL(1000, iface.slots[0].transfer_id); + TEST_ASSERT_EQUAL(1001, iface.slots[1].transfer_id); + TEST_ASSERT_EQUAL(3, mem_payload.allocated_fragments); + TEST_ASSERT_EQUAL(3, mem_fragment.allocated_fragments); + // B0 + TEST_ASSERT_EQUAL(1, + rxIfaceAccept(&iface, + 2000000040, + makeRxFrameString(&mem_payload.base, // + (TransferMetadata){.priority = UdpardPrioritySlow, + .src_node_id = 2222, + .dst_node_id = UDPARD_NODE_ID_UNSET, + .data_specifier = 0x2222, + .transfer_id = 1001U}, + 0, + false, + "B0"), + &transfer, + 1000, + UDPARD_DEFAULT_TRANSFER_ID_TIMEOUT_USEC, + &mem_fragment.base, + &mem_payload.base)); + // TRANSFER B RECEIVED, check it. + TEST_ASSERT_EQUAL(2000000010, iface.ts_usec); + TEST_ASSERT_EQUAL(1000, iface.slots[0].transfer_id); + TEST_ASSERT_EQUAL(1002, iface.slots[1].transfer_id); // Incremented to meet the next transfer. + TEST_ASSERT_EQUAL(4, mem_payload.allocated_fragments); + TEST_ASSERT_EQUAL(3, mem_fragment.allocated_fragments); // One fragment freed because of the head optimization. + // Check the payload. + TEST_ASSERT_EQUAL(2000000010, transfer.timestamp_usec); + TEST_ASSERT_EQUAL(UdpardPrioritySlow, transfer.priority); + TEST_ASSERT_EQUAL(2222, transfer.source_node_id); + TEST_ASSERT_EQUAL(1001, transfer.transfer_id); + TEST_ASSERT_EQUAL(4, transfer.payload_size); + TEST_ASSERT(compareStringWithPayload("B0", transfer.payload.view)); + TEST_ASSERT_NOT_NULL(transfer.payload.next); + TEST_ASSERT(compareStringWithPayload("B1", transfer.payload.next->view)); + TEST_ASSERT_NULL(transfer.payload.next->next); + udpardFragmentFree(transfer.payload, &mem_fragment.base, &mem_payload.base); + TEST_ASSERT_EQUAL(2, mem_payload.allocated_fragments); // Only the remaining A0 A2 are left. + TEST_ASSERT_EQUAL(2, mem_fragment.allocated_fragments); + // A1 + TEST_ASSERT_EQUAL(1, + rxIfaceAccept(&iface, + 2000000050, + makeRxFrameString(&mem_payload.base, // + (TransferMetadata){.priority = UdpardPriorityHigh, + .src_node_id = 1111, + .dst_node_id = UDPARD_NODE_ID_UNSET, + .data_specifier = 0x1111, + .transfer_id = 1000U}, + 1, + false, + "A1"), + &transfer, + 1000, + UDPARD_DEFAULT_TRANSFER_ID_TIMEOUT_USEC, + &mem_fragment.base, + &mem_payload.base)); + // TRANSFER A RECEIVED, check it. + TEST_ASSERT_EQUAL(2000000020, iface.ts_usec); // same old timestamp + TEST_ASSERT_EQUAL(1001, iface.slots[0].transfer_id); // Incremented to meet the next transfer. + TEST_ASSERT_EQUAL(1002, iface.slots[1].transfer_id); + // Check the payload. + TEST_ASSERT_EQUAL(2000000020, transfer.timestamp_usec); + TEST_ASSERT_EQUAL(UdpardPriorityHigh, transfer.priority); + TEST_ASSERT_EQUAL(1111, transfer.source_node_id); + TEST_ASSERT_EQUAL(1000, transfer.transfer_id); + TEST_ASSERT_EQUAL(6, transfer.payload_size); + TEST_ASSERT(compareStringWithPayload("A0", transfer.payload.view)); + TEST_ASSERT_NOT_NULL(transfer.payload.next); + TEST_ASSERT(compareStringWithPayload("A1", transfer.payload.next->view)); + TEST_ASSERT_NOT_NULL(transfer.payload.next->next); + TEST_ASSERT(compareStringWithPayload("A2", transfer.payload.next->next->view)); + TEST_ASSERT_NULL(transfer.payload.next->next->next); + udpardFragmentFree(transfer.payload, &mem_fragment.base, &mem_payload.base); + TEST_ASSERT_EQUAL(0, mem_payload.allocated_fragments); + TEST_ASSERT_EQUAL(0, mem_fragment.allocated_fragments); } void setUp(void) {} From 3151ff548012745dbf57c3fa46cfe1cb95c8afb7 Mon Sep 17 00:00:00 2001 From: Pavel Kirienko Date: Fri, 28 Jul 2023 19:08:39 +0300 Subject: [PATCH 12/38] Refactor the iface acceptor and add more tests --- libudpard/udpard.c | 70 ++++-- tests/src/test_intrusive_rx.c | 404 +++++++++++++++++++++++++++++++--- 2 files changed, 424 insertions(+), 50 deletions(-) diff --git a/libudpard/udpard.c b/libudpard/udpard.c index c10db61..8a85fe6 100644 --- a/libudpard/udpard.c +++ b/libudpard/udpard.c @@ -846,7 +846,7 @@ typedef struct typedef struct { - UdpardMicrosecond ts_usec; ///< The timestamp of the last transfer to arrive on this interface. + UdpardMicrosecond ts_usec; ///< The timestamp of the last valid transfer to arrive on this interface. RxSlot slots[RX_SLOT_COUNT]; } RxIface; @@ -1154,6 +1154,42 @@ static inline int_fast8_t rxSlotAccept(RxSlot* const self, return result; } +/// Whether the supplied transfer-ID is greater than all transfer-IDs in the RX slots. +/// This indicates that the new transfer is not a duplicate and should be accepted. +static inline bool rxIfaceIsFutureTransferID(const RxIface* const self, const UdpardTransferID transfer_id) +{ + bool is_future_tid = true; + for (uint_fast8_t i = 0; i < RX_SLOT_COUNT; i++) // Expected to be unrolled by the compiler. + { + is_future_tid = is_future_tid && ((self->slots[i].transfer_id < transfer_id) || + (self->slots[i].transfer_id == TRANSFER_ID_UNSET)); + } + return is_future_tid; +} + +/// Whether the time that has passed since the last accepted first frame of a transfer exceeds the TID timeout. +/// This indicates that the transfer should be accepted even if its transfer-ID is not greater than all transfer-IDs +/// in the RX slots. +static inline bool rxIfaceCheckTransferIDTimeout(const RxIface* const self, + const UdpardMicrosecond ts_usec, + const UdpardMicrosecond transfer_id_timeout_usec) +{ + // We use the RxIface state here because the RxSlot state is reset between transfers. + // If there is reassembly in progress, we want to use the timestamps from these in-progress transfers, + // as that eliminates the risk of a false-positive TID-timeout detection. + UdpardMicrosecond most_recent_ts_usec = self->ts_usec; + for (uint_fast8_t i = 0; i < RX_SLOT_COUNT; i++) // Expected to be unrolled by the compiler. + { + if ((most_recent_ts_usec == TIMESTAMP_UNSET) || + ((self->slots[i].ts_usec != TIMESTAMP_UNSET) && (self->slots[i].ts_usec > most_recent_ts_usec))) + { + most_recent_ts_usec = self->slots[i].ts_usec; + } + } + return (most_recent_ts_usec == TIMESTAMP_UNSET) || + ((ts_usec >= most_recent_ts_usec) && ((ts_usec - most_recent_ts_usec) > transfer_id_timeout_usec)); +} + /// This function is invoked when a new datagram pertaining to a certain session is received on an interface. /// This function will either move the frame payload into the session, or free it if it cannot be made use of. /// Returns: 1 -- transfer available; 0 -- transfer not yet available; <0 -- error. @@ -1181,28 +1217,22 @@ static inline int_fast8_t rxIfaceAccept(RxIface* const self, // or a transfer-ID timeout has occurred. In this case we sacrifice the oldest slot. if (slot == NULL) { - bool is_future_tid = true; - RxSlot* victim = &self->slots[0]; - // Check if the newly received transfer-ID is higher than any of the existing slots. - // While we're at it, find the oldest slot as a replacement candidate. - for (uint_fast8_t i = 0; i < RX_SLOT_COUNT; i++) + // The timestamp is UNSET when the slot is waiting for the next transfer. + // Such slots are the best candidates for replacement because reusing them does not cause loss of + // transfers that are in the process of being reassembled. If there are no such slots, we must + // sacrifice the one whose first frame has arrived the longest time ago. + RxSlot* victim = &self->slots[0]; + for (uint_fast8_t i = 0; i < RX_SLOT_COUNT; i++) // Expected to be unrolled by the compiler. { - RxSlot* const it = &self->slots[i]; - is_future_tid = is_future_tid && ((it->transfer_id < frame.meta.transfer_id) || // Keep state if unused. - (it->transfer_id == TRANSFER_ID_UNSET)); - // The timestamp is UNSET when the slot is waiting for the next transfer (or unused -- special case). - // Such slots are the best candidates for replacement because reusing them does not cause loss of - // transfers that are in the process of being reassembled. If there are no such slots, we must - // sacrifice the one whose first frame has arrived the longest time ago. - if ((it->ts_usec == TIMESTAMP_UNSET) || - ((victim->ts_usec != TIMESTAMP_UNSET) && (it->ts_usec < victim->ts_usec))) + if ((self->slots[i].ts_usec == TIMESTAMP_UNSET) || + ((victim->ts_usec != TIMESTAMP_UNSET) && (self->slots[i].ts_usec < victim->ts_usec))) { - victim = it; + victim = &self->slots[i]; } } - const bool is_tid_timeout = (self->ts_usec == TIMESTAMP_UNSET) || // - ((ts_usec - self->ts_usec) > transfer_id_timeout_usec); - if (is_tid_timeout || is_future_tid) + const bool is_future_tid = rxIfaceIsFutureTransferID(self, frame.meta.transfer_id); + const bool is_tid_timeout = rxIfaceCheckTransferIDTimeout(self, ts_usec, transfer_id_timeout_usec); + if (is_future_tid || is_tid_timeout) { rxSlotRestart(victim, frame.meta.transfer_id, memory_fragment, memory_payload); slot = victim; @@ -1229,7 +1259,7 @@ static inline int_fast8_t rxIfaceAccept(RxIface* const self, memory_payload); if (result > 0) // Transfer successfully received, populate the transfer descriptor for the client. { - self->ts_usec = ts; + self->ts_usec = ts; // Update the last valid transfer timestamp on this iface. received_transfer->timestamp_usec = ts; received_transfer->priority = frame.meta.priority; received_transfer->source_node_id = frame.meta.src_node_id; diff --git a/tests/src/test_intrusive_rx.c b/tests/src/test_intrusive_rx.c index a50b4ee..6d2654f 100644 --- a/tests/src/test_intrusive_rx.c +++ b/tests/src/test_intrusive_rx.c @@ -1010,6 +1010,103 @@ static void testSlotAcceptA(void) TEST_ASSERT_NULL(slot.fragments); } +static void testIfaceIsFutureTransferID(void) +{ + InstrumentedAllocator mem_fragment = {0}; + InstrumentedAllocator mem_payload = {0}; + instrumentedAllocatorNew(&mem_fragment); + instrumentedAllocatorNew(&mem_payload); + RxIface iface; + rxIfaceInit(&iface, &mem_fragment.base, &mem_payload.base); + TEST_ASSERT_EQUAL(TIMESTAMP_UNSET, iface.ts_usec); + for (size_t i = 0; i < RX_SLOT_COUNT; i++) + { + TEST_ASSERT_EQUAL(TIMESTAMP_UNSET, iface.slots[i].ts_usec); + TEST_ASSERT_EQUAL(TRANSFER_ID_UNSET, iface.slots[i].transfer_id); + TEST_ASSERT_EQUAL(0, iface.slots[i].max_index); + TEST_ASSERT_EQUAL(FRAME_INDEX_UNSET, iface.slots[i].eot_index); + TEST_ASSERT_EQUAL(0, iface.slots[i].accepted_frames); + TEST_ASSERT_EQUAL(0, iface.slots[i].payload_size); + TEST_ASSERT_NULL(iface.slots[i].fragments); + } + TEST_ASSERT_TRUE(rxIfaceIsFutureTransferID(&iface, 0)); + TEST_ASSERT_TRUE(rxIfaceIsFutureTransferID(&iface, 0xFFFFFFFFFFFFFFFFULL)); + iface.slots[0].transfer_id = 100; + TEST_ASSERT_FALSE(rxIfaceIsFutureTransferID(&iface, 99)); + TEST_ASSERT_FALSE(rxIfaceIsFutureTransferID(&iface, 100)); + TEST_ASSERT_TRUE(rxIfaceIsFutureTransferID(&iface, 101)); + iface.slots[0].transfer_id = TRANSFER_ID_UNSET; + iface.slots[1].transfer_id = 100; + TEST_ASSERT_FALSE(rxIfaceIsFutureTransferID(&iface, 99)); + TEST_ASSERT_FALSE(rxIfaceIsFutureTransferID(&iface, 100)); + TEST_ASSERT_TRUE(rxIfaceIsFutureTransferID(&iface, 101)); +} + +static void testIfaceCheckTransferIDTimeout(void) +{ + InstrumentedAllocator mem_fragment = {0}; + InstrumentedAllocator mem_payload = {0}; + instrumentedAllocatorNew(&mem_fragment); + instrumentedAllocatorNew(&mem_payload); + RxIface iface; + rxIfaceInit(&iface, &mem_fragment.base, &mem_payload.base); + TEST_ASSERT_EQUAL(TIMESTAMP_UNSET, iface.ts_usec); + for (size_t i = 0; i < RX_SLOT_COUNT; i++) + { + TEST_ASSERT_EQUAL(TIMESTAMP_UNSET, iface.slots[i].ts_usec); + TEST_ASSERT_EQUAL(TRANSFER_ID_UNSET, iface.slots[i].transfer_id); + TEST_ASSERT_EQUAL(0, iface.slots[i].max_index); + TEST_ASSERT_EQUAL(FRAME_INDEX_UNSET, iface.slots[i].eot_index); + TEST_ASSERT_EQUAL(0, iface.slots[i].accepted_frames); + TEST_ASSERT_EQUAL(0, iface.slots[i].payload_size); + TEST_ASSERT_NULL(iface.slots[i].fragments); + } + // No successful transfers so far, and no slots in progress at the moment. + TEST_ASSERT_TRUE(rxIfaceCheckTransferIDTimeout(&iface, 0, 100)); + TEST_ASSERT_TRUE(rxIfaceCheckTransferIDTimeout(&iface, 1000, 100)); + // Suppose we have on successful transfer now. + iface.ts_usec = 1000; + TEST_ASSERT_FALSE(rxIfaceCheckTransferIDTimeout(&iface, 500, 100)); // TS is in the past! Check overflows. + TEST_ASSERT_FALSE(rxIfaceCheckTransferIDTimeout(&iface, 1000, 100)); + TEST_ASSERT_FALSE(rxIfaceCheckTransferIDTimeout(&iface, 1050, 100)); + TEST_ASSERT_TRUE(rxIfaceCheckTransferIDTimeout(&iface, 1150, 100)); // Yup, this is a timeout. + TEST_ASSERT_TRUE(rxIfaceCheckTransferIDTimeout(&iface, 2150, 100)); // Yup, this is a timeout. + // Suppose there are some slots in progress. + iface.ts_usec = 1000; + iface.slots[0].ts_usec = 2000; + TEST_ASSERT_FALSE(rxIfaceCheckTransferIDTimeout(&iface, 500, 100)); // TS is in the past! Check overflows. + TEST_ASSERT_FALSE(rxIfaceCheckTransferIDTimeout(&iface, 1000, 100)); + TEST_ASSERT_FALSE(rxIfaceCheckTransferIDTimeout(&iface, 1050, 100)); + TEST_ASSERT_FALSE(rxIfaceCheckTransferIDTimeout(&iface, 1150, 100)); // No timeout because of the slot in progress. + TEST_ASSERT_FALSE(rxIfaceCheckTransferIDTimeout(&iface, 2050, 100)); // Nope. + TEST_ASSERT_TRUE(rxIfaceCheckTransferIDTimeout(&iface, 2150, 100)); // Yeah. + TEST_ASSERT_TRUE(rxIfaceCheckTransferIDTimeout(&iface, 3050, 100)); // Ooh. + // More slots in progress. + iface.ts_usec = 1000; + iface.slots[0].ts_usec = 2000; + iface.slots[1].ts_usec = 3000; + TEST_ASSERT_FALSE(rxIfaceCheckTransferIDTimeout(&iface, 500, 100)); // TS is in the past! Check overflows. + TEST_ASSERT_FALSE(rxIfaceCheckTransferIDTimeout(&iface, 1000, 100)); + TEST_ASSERT_FALSE(rxIfaceCheckTransferIDTimeout(&iface, 1050, 100)); + TEST_ASSERT_FALSE(rxIfaceCheckTransferIDTimeout(&iface, 1150, 100)); // No timeout because of the slot in progress. + TEST_ASSERT_FALSE(rxIfaceCheckTransferIDTimeout(&iface, 2050, 100)); // Nope. + TEST_ASSERT_FALSE(rxIfaceCheckTransferIDTimeout(&iface, 2150, 100)); // The other slot is newer. + TEST_ASSERT_FALSE(rxIfaceCheckTransferIDTimeout(&iface, 3050, 100)); // Yes, but not yet. + TEST_ASSERT_TRUE(rxIfaceCheckTransferIDTimeout(&iface, 3150, 100)); // Yes. + // Now suppose there is no successful transfer, but there are some slots in progress. It's all the same. + iface.ts_usec = TIMESTAMP_UNSET; + iface.slots[0].ts_usec = 2000; + iface.slots[1].ts_usec = 3000; + TEST_ASSERT_FALSE(rxIfaceCheckTransferIDTimeout(&iface, 500, 100)); // TS is in the past! Check overflows. + TEST_ASSERT_FALSE(rxIfaceCheckTransferIDTimeout(&iface, 1000, 100)); + TEST_ASSERT_FALSE(rxIfaceCheckTransferIDTimeout(&iface, 1050, 100)); + TEST_ASSERT_FALSE(rxIfaceCheckTransferIDTimeout(&iface, 1150, 100)); // No timeout because of the slot in progress. + TEST_ASSERT_FALSE(rxIfaceCheckTransferIDTimeout(&iface, 2050, 100)); // Nope. + TEST_ASSERT_FALSE(rxIfaceCheckTransferIDTimeout(&iface, 2150, 100)); // The other slot is newer. + TEST_ASSERT_FALSE(rxIfaceCheckTransferIDTimeout(&iface, 3050, 100)); // Yes, but not yet. + TEST_ASSERT_TRUE(rxIfaceCheckTransferIDTimeout(&iface, 3150, 100)); // Ooh yes. +} + static void testIfaceAcceptA(void) { InstrumentedAllocator mem_fragment = {0}; @@ -1151,24 +1248,24 @@ static void testIfaceAcceptA(void) TEST_ASSERT_EQUAL(0x1122334455667791U, iface.slots[1].transfer_id); // Replaced the old one, it was unneeded. // === TRANSFER === (x2) - // Send two interleaving multi-frame out-of-order transfers with duplicate frames: - // B1 A2 A0 B0 A1 + // Send two interleaving multi-frame out-of-order transfers: + // A2 B1 A0 B0 A1 TEST_ASSERT_EQUAL(0, mem_payload.allocated_fragments); TEST_ASSERT_EQUAL(0, mem_fragment.allocated_fragments); - // B1 + // A2 TEST_ASSERT_EQUAL(0, rxIfaceAccept(&iface, - 2000000010, // Transfer-ID timeout. + 2000000020, makeRxFrameString(&mem_payload.base, // - (TransferMetadata){.priority = UdpardPrioritySlow, - .src_node_id = 2222, + (TransferMetadata){.priority = UdpardPriorityHigh, + .src_node_id = 1111, .dst_node_id = UDPARD_NODE_ID_UNSET, - .data_specifier = 0x2222, - .transfer_id = 1001U}, - 1, + .data_specifier = 0x1111, + .transfer_id = 1000U}, + 2, true, - "B1" - "g\x8D\x9A\xD7"), + "A2" + "v\x1E\xBD]"), &transfer, 1000, UDPARD_DEFAULT_TRANSFER_ID_TIMEOUT_USEC, @@ -1176,31 +1273,31 @@ static void testIfaceAcceptA(void) &mem_payload.base)); TEST_ASSERT_EQUAL(1234567890, iface.ts_usec); // same old timestamp TEST_ASSERT_EQUAL(TRANSFER_ID_UNSET, iface.slots[0].transfer_id); // Still unused. - TEST_ASSERT_EQUAL(1001, iface.slots[1].transfer_id); // Replaced the old one, it was unneeded. + TEST_ASSERT_EQUAL(1000, iface.slots[1].transfer_id); // Replaced the old one, it was unneeded. TEST_ASSERT_EQUAL(1, mem_payload.allocated_fragments); TEST_ASSERT_EQUAL(1, mem_fragment.allocated_fragments); - // A2 + // B1 TEST_ASSERT_EQUAL(0, rxIfaceAccept(&iface, - 2000000020, + 2000000010, // Transfer-ID timeout. makeRxFrameString(&mem_payload.base, // - (TransferMetadata){.priority = UdpardPriorityHigh, - .src_node_id = 1111, + (TransferMetadata){.priority = UdpardPrioritySlow, + .src_node_id = 2222, .dst_node_id = UDPARD_NODE_ID_UNSET, - .data_specifier = 0x1111, - .transfer_id = 1000U}, - 2, + .data_specifier = 0x2222, + .transfer_id = 1001U}, + 1, true, - "A2" - "v\x1E\xBD]"), + "B1" + "g\x8D\x9A\xD7"), &transfer, 1000, UDPARD_DEFAULT_TRANSFER_ID_TIMEOUT_USEC, &mem_fragment.base, &mem_payload.base)); TEST_ASSERT_EQUAL(1234567890, iface.ts_usec); // same old timestamp - TEST_ASSERT_EQUAL(1000, iface.slots[0].transfer_id); // Used for A because the other one is taken. - TEST_ASSERT_EQUAL(1001, iface.slots[1].transfer_id); // Keeps B because it is in-progress, can't discard. + TEST_ASSERT_EQUAL(1001, iface.slots[0].transfer_id); // Used for B because the other one is taken. + TEST_ASSERT_EQUAL(1000, iface.slots[1].transfer_id); // Keeps A because it is in-progress, can't discard. TEST_ASSERT_EQUAL(2, mem_payload.allocated_fragments); TEST_ASSERT_EQUAL(2, mem_fragment.allocated_fragments); // A0 @@ -1222,8 +1319,8 @@ static void testIfaceAcceptA(void) &mem_fragment.base, &mem_payload.base)); TEST_ASSERT_EQUAL(1234567890, iface.ts_usec); // same old timestamp - TEST_ASSERT_EQUAL(1000, iface.slots[0].transfer_id); - TEST_ASSERT_EQUAL(1001, iface.slots[1].transfer_id); + TEST_ASSERT_EQUAL(1001, iface.slots[0].transfer_id); + TEST_ASSERT_EQUAL(1000, iface.slots[1].transfer_id); TEST_ASSERT_EQUAL(3, mem_payload.allocated_fragments); TEST_ASSERT_EQUAL(3, mem_fragment.allocated_fragments); // B0 @@ -1246,8 +1343,8 @@ static void testIfaceAcceptA(void) &mem_payload.base)); // TRANSFER B RECEIVED, check it. TEST_ASSERT_EQUAL(2000000010, iface.ts_usec); - TEST_ASSERT_EQUAL(1000, iface.slots[0].transfer_id); - TEST_ASSERT_EQUAL(1002, iface.slots[1].transfer_id); // Incremented to meet the next transfer. + TEST_ASSERT_EQUAL(1002, iface.slots[0].transfer_id); // Incremented to meet the next transfer. + TEST_ASSERT_EQUAL(1000, iface.slots[1].transfer_id); TEST_ASSERT_EQUAL(4, mem_payload.allocated_fragments); TEST_ASSERT_EQUAL(3, mem_fragment.allocated_fragments); // One fragment freed because of the head optimization. // Check the payload. @@ -1282,9 +1379,9 @@ static void testIfaceAcceptA(void) &mem_fragment.base, &mem_payload.base)); // TRANSFER A RECEIVED, check it. - TEST_ASSERT_EQUAL(2000000020, iface.ts_usec); // same old timestamp - TEST_ASSERT_EQUAL(1001, iface.slots[0].transfer_id); // Incremented to meet the next transfer. - TEST_ASSERT_EQUAL(1002, iface.slots[1].transfer_id); + TEST_ASSERT_EQUAL(2000000020, iface.ts_usec); // same old timestamp + TEST_ASSERT_EQUAL(1002, iface.slots[0].transfer_id); + TEST_ASSERT_EQUAL(1001, iface.slots[1].transfer_id); // Incremented to meet the next transfer. // Check the payload. TEST_ASSERT_EQUAL(2000000020, transfer.timestamp_usec); TEST_ASSERT_EQUAL(UdpardPriorityHigh, transfer.priority); @@ -1302,6 +1399,250 @@ static void testIfaceAcceptA(void) TEST_ASSERT_EQUAL(0, mem_fragment.allocated_fragments); } +static void testIfaceAcceptB(void) +{ + InstrumentedAllocator mem_fragment = {0}; + InstrumentedAllocator mem_payload = {0}; + instrumentedAllocatorNew(&mem_fragment); + instrumentedAllocatorNew(&mem_payload); + RxIface iface; + rxIfaceInit(&iface, &mem_fragment.base, &mem_payload.base); + TEST_ASSERT_EQUAL(TIMESTAMP_UNSET, iface.ts_usec); + for (size_t i = 0; i < RX_SLOT_COUNT; i++) + { + TEST_ASSERT_EQUAL(TIMESTAMP_UNSET, iface.slots[i].ts_usec); + TEST_ASSERT_EQUAL(TRANSFER_ID_UNSET, iface.slots[i].transfer_id); + TEST_ASSERT_EQUAL(0, iface.slots[i].max_index); + TEST_ASSERT_EQUAL(FRAME_INDEX_UNSET, iface.slots[i].eot_index); + TEST_ASSERT_EQUAL(0, iface.slots[i].accepted_frames); + TEST_ASSERT_EQUAL(0, iface.slots[i].payload_size); + TEST_ASSERT_NULL(iface.slots[i].fragments); + } + struct UdpardRxTransfer transfer = {0}; + // === TRANSFER === (x3) + // Send three interleaving multi-frame out-of-order transfers (primes for duplicates): + // A2 B1 A0 C0 B0 A1 C0' C1 + // Transfer B will be evicted by C because by the time C0 arrives, transfer B is the oldest one, + // since its timestamp is inherited from B0. + TEST_ASSERT_EQUAL(0, mem_payload.allocated_fragments); + TEST_ASSERT_EQUAL(0, mem_fragment.allocated_fragments); + // A2 + TEST_ASSERT_EQUAL(0, + rxIfaceAccept(&iface, + 2000000020, + makeRxFrameString(&mem_payload.base, // + (TransferMetadata){.priority = UdpardPriorityHigh, + .src_node_id = 1111, + .dst_node_id = UDPARD_NODE_ID_UNSET, + .data_specifier = 0x1111, + .transfer_id = 1000U}, + 2, + true, + "A2" + "v\x1E\xBD]"), + &transfer, + 1000, + UDPARD_DEFAULT_TRANSFER_ID_TIMEOUT_USEC, + &mem_fragment.base, + &mem_payload.base)); + TEST_ASSERT_EQUAL(TIMESTAMP_UNSET, iface.ts_usec); + TEST_ASSERT_EQUAL(TRANSFER_ID_UNSET, iface.slots[0].transfer_id); + TEST_ASSERT_EQUAL(1000, iface.slots[1].transfer_id); + TEST_ASSERT_EQUAL(1, mem_payload.allocated_fragments); + TEST_ASSERT_EQUAL(1, mem_fragment.allocated_fragments); + // B1 + TEST_ASSERT_EQUAL(0, + rxIfaceAccept(&iface, + 2000000010, // Transfer-ID timeout. + makeRxFrameString(&mem_payload.base, // + (TransferMetadata){.priority = UdpardPrioritySlow, + .src_node_id = 2222, + .dst_node_id = UDPARD_NODE_ID_UNSET, + .data_specifier = 0x2222, + .transfer_id = 1001U}, + 1, + true, + "B1" + "g\x8D\x9A\xD7"), + &transfer, + 1000, + UDPARD_DEFAULT_TRANSFER_ID_TIMEOUT_USEC, + &mem_fragment.base, + &mem_payload.base)); + TEST_ASSERT_EQUAL(TIMESTAMP_UNSET, iface.ts_usec); + TEST_ASSERT_EQUAL(1001, iface.slots[0].transfer_id); + TEST_ASSERT_EQUAL(1000, iface.slots[1].transfer_id); + TEST_ASSERT_EQUAL(2, mem_payload.allocated_fragments); + TEST_ASSERT_EQUAL(2, mem_fragment.allocated_fragments); + // A0 + TEST_ASSERT_EQUAL(0, + rxIfaceAccept(&iface, + 2000000030, + makeRxFrameString(&mem_payload.base, // + (TransferMetadata){.priority = UdpardPriorityHigh, + .src_node_id = 1111, + .dst_node_id = UDPARD_NODE_ID_UNSET, + .data_specifier = 0x1111, + .transfer_id = 1000U}, + 0, + false, + "A0"), + &transfer, + 1000, + UDPARD_DEFAULT_TRANSFER_ID_TIMEOUT_USEC, + &mem_fragment.base, + &mem_payload.base)); + TEST_ASSERT_EQUAL(TIMESTAMP_UNSET, iface.ts_usec); + TEST_ASSERT_EQUAL(1001, iface.slots[0].transfer_id); + TEST_ASSERT_EQUAL(1000, iface.slots[1].transfer_id); + TEST_ASSERT_EQUAL(3, mem_payload.allocated_fragments); + TEST_ASSERT_EQUAL(3, mem_fragment.allocated_fragments); + // C0 + TEST_ASSERT_EQUAL(0, + rxIfaceAccept(&iface, + 2000000040, + makeRxFrameString(&mem_payload.base, // + (TransferMetadata){.priority = UdpardPriorityHigh, + .src_node_id = 3333, + .dst_node_id = UDPARD_NODE_ID_UNSET, + .data_specifier = 0x3333, + .transfer_id = 1002U}, + 0, + false, + "C0"), + &transfer, + 1000, + UDPARD_DEFAULT_TRANSFER_ID_TIMEOUT_USEC, + &mem_fragment.base, + &mem_payload.base)); + TEST_ASSERT_EQUAL(TIMESTAMP_UNSET, iface.ts_usec); + TEST_ASSERT_EQUAL(1002, iface.slots[0].transfer_id); // B evicted by C. + TEST_ASSERT_EQUAL(1000, iface.slots[1].transfer_id); + TEST_ASSERT_EQUAL(3, mem_payload.allocated_fragments); // Payload of B is freed, so the usage is unchanged. + TEST_ASSERT_EQUAL(3, mem_fragment.allocated_fragments); + // B0 + TEST_ASSERT_EQUAL(0, // Cannot be accepted because its slot is taken over by C. + rxIfaceAccept(&iface, + 2000000050, + makeRxFrameString(&mem_payload.base, // + (TransferMetadata){.priority = UdpardPrioritySlow, + .src_node_id = 2222, + .dst_node_id = UDPARD_NODE_ID_UNSET, + .data_specifier = 0x2222, + .transfer_id = 1001U}, + 0, + false, + "B0"), + &transfer, + 1000, + UDPARD_DEFAULT_TRANSFER_ID_TIMEOUT_USEC, + &mem_fragment.base, + &mem_payload.base)); + TEST_ASSERT_EQUAL(TIMESTAMP_UNSET, iface.ts_usec); + TEST_ASSERT_EQUAL(1002, iface.slots[0].transfer_id); + TEST_ASSERT_EQUAL(1000, iface.slots[1].transfer_id); + TEST_ASSERT_EQUAL(3, mem_payload.allocated_fragments); // No increase, frame not accepted. + TEST_ASSERT_EQUAL(3, mem_fragment.allocated_fragments); + // A1 + TEST_ASSERT_EQUAL(1, + rxIfaceAccept(&iface, + 2000000050, + makeRxFrameString(&mem_payload.base, // + (TransferMetadata){.priority = UdpardPriorityHigh, + .src_node_id = 1111, + .dst_node_id = UDPARD_NODE_ID_UNSET, + .data_specifier = 0x1111, + .transfer_id = 1000U}, + 1, + false, + "A1"), + &transfer, + 1000, + UDPARD_DEFAULT_TRANSFER_ID_TIMEOUT_USEC, + &mem_fragment.base, + &mem_payload.base)); + // TRANSFER A RECEIVED, check it. + TEST_ASSERT_EQUAL(2000000020, iface.ts_usec); // same old timestamp + TEST_ASSERT_EQUAL(1002, iface.slots[0].transfer_id); + TEST_ASSERT_EQUAL(1001, iface.slots[1].transfer_id); // Incremented to meet the next transfer. + // Check the payload. + TEST_ASSERT_EQUAL(2000000020, transfer.timestamp_usec); + TEST_ASSERT_EQUAL(UdpardPriorityHigh, transfer.priority); + TEST_ASSERT_EQUAL(1111, transfer.source_node_id); + TEST_ASSERT_EQUAL(1000, transfer.transfer_id); + TEST_ASSERT_EQUAL(6, transfer.payload_size); + TEST_ASSERT(compareStringWithPayload("A0", transfer.payload.view)); + TEST_ASSERT_NOT_NULL(transfer.payload.next); + TEST_ASSERT(compareStringWithPayload("A1", transfer.payload.next->view)); + TEST_ASSERT_NOT_NULL(transfer.payload.next->next); + TEST_ASSERT(compareStringWithPayload("A2", transfer.payload.next->next->view)); + TEST_ASSERT_NULL(transfer.payload.next->next->next); + udpardFragmentFree(transfer.payload, &mem_fragment.base, &mem_payload.base); + TEST_ASSERT_EQUAL(1, mem_payload.allocated_fragments); // Some memory is retained for the C0 payload. + TEST_ASSERT_EQUAL(1, mem_fragment.allocated_fragments); + // C0 DUPLICATE + TEST_ASSERT_EQUAL(0, + rxIfaceAccept(&iface, + 2000000060, + makeRxFrameString(&mem_payload.base, // + (TransferMetadata){.priority = UdpardPriorityHigh, + .src_node_id = 3333, + .dst_node_id = UDPARD_NODE_ID_UNSET, + .data_specifier = 0x3333, + .transfer_id = 1002U}, + 0, + false, + "C0 DUPLICATE"), + &transfer, + 1000, + UDPARD_DEFAULT_TRANSFER_ID_TIMEOUT_USEC, + &mem_fragment.base, + &mem_payload.base)); + TEST_ASSERT_EQUAL(2000000020, iface.ts_usec); // Last transfer timestamp. + TEST_ASSERT_EQUAL(1002, iface.slots[0].transfer_id); + TEST_ASSERT_EQUAL(1001, iface.slots[1].transfer_id); + TEST_ASSERT_EQUAL(1, mem_payload.allocated_fragments); // Not accepted, so no change. + TEST_ASSERT_EQUAL(1, mem_fragment.allocated_fragments); + // C1 + TEST_ASSERT_EQUAL(1, + rxIfaceAccept(&iface, + 2000000070, + makeRxFrameString(&mem_payload.base, // + (TransferMetadata){.priority = UdpardPriorityHigh, + .src_node_id = 3333, + .dst_node_id = UDPARD_NODE_ID_UNSET, + .data_specifier = 0x3333, + .transfer_id = 1002U}, + 1, + true, + "C1" + "\xA8\xBF}\x19"), + &transfer, + 1000, + UDPARD_DEFAULT_TRANSFER_ID_TIMEOUT_USEC, + &mem_fragment.base, + &mem_payload.base)); + // TRANSFER C RECEIVED, check it. + TEST_ASSERT_EQUAL(2000000040, iface.ts_usec); + TEST_ASSERT_EQUAL(1003, iface.slots[0].transfer_id); // Incremented to meet the next transfer. + TEST_ASSERT_EQUAL(1001, iface.slots[1].transfer_id); + TEST_ASSERT_EQUAL(2, mem_payload.allocated_fragments); // Keeping two fragments of C. + TEST_ASSERT_EQUAL(1, mem_fragment.allocated_fragments); // Head optimization in effect. + // Check the payload. + TEST_ASSERT_EQUAL(2000000040, transfer.timestamp_usec); + TEST_ASSERT_EQUAL(UdpardPriorityHigh, transfer.priority); + TEST_ASSERT_EQUAL(3333, transfer.source_node_id); + TEST_ASSERT_EQUAL(1002, transfer.transfer_id); + TEST_ASSERT_EQUAL(4, transfer.payload_size); + TEST_ASSERT(compareStringWithPayload("C0", transfer.payload.view)); + TEST_ASSERT_NOT_NULL(transfer.payload.next); + TEST_ASSERT(compareStringWithPayload("C1", transfer.payload.next->view)); + TEST_ASSERT_NULL(transfer.payload.next->next); + udpardFragmentFree(transfer.payload, &mem_fragment.base, &mem_payload.base); + TEST_ASSERT_EQUAL(0, mem_payload.allocated_fragments); // Some memory is retained for the C0 payload. + TEST_ASSERT_EQUAL(0, mem_fragment.allocated_fragments); +} + void setUp(void) {} void tearDown(void) {} @@ -1329,7 +1670,10 @@ int main(void) RUN_TEST(testSlotEjectInvalid); RUN_TEST(testSlotAcceptA); // iface + RUN_TEST(testIfaceIsFutureTransferID); + RUN_TEST(testIfaceCheckTransferIDTimeout); RUN_TEST(testIfaceAcceptA); + RUN_TEST(testIfaceAcceptB); return UNITY_END(); } From 3aa6385e701a4030ae197ab7c2a8368654047ee7 Mon Sep 17 00:00:00 2001 From: Pavel Kirienko Date: Fri, 28 Jul 2023 20:36:10 +0300 Subject: [PATCH 13/38] Increase test coverage --- libudpard/udpard.c | 40 ++++-- tests/src/test_intrusive_rx.c | 252 ++++++++++++++++++++++++++++++++++ 2 files changed, 282 insertions(+), 10 deletions(-) diff --git a/libudpard/udpard.c b/libudpard/udpard.c index 8a85fe6..5766187 100644 --- a/libudpard/udpard.c +++ b/libudpard/udpard.c @@ -1190,6 +1190,35 @@ static inline bool rxIfaceCheckTransferIDTimeout(const RxIface* const self, ((ts_usec >= most_recent_ts_usec) && ((ts_usec - most_recent_ts_usec) > transfer_id_timeout_usec)); } +/// Traverses the list of slot trying to find a slot with a matching transfer-ID that is already IN PROGRESS. +/// If there is no such slot, tries again without the IN PROGRESS requirement. +/// The purpose of this complicated dual check is to support the case where multiple slots have the same +/// transfer-ID, which may occur with interleaved transfers. +static inline RxSlot* rxIfaceFindMatchingSlot(RxIface* const self, const UdpardTransferID transfer_id) +{ + RxSlot* slot = NULL; + for (uint_fast8_t i = 0; i < RX_SLOT_COUNT; i++) + { + if ((self->slots[i].transfer_id == transfer_id) && (self->slots[i].ts_usec != TIMESTAMP_UNSET)) + { + slot = &self->slots[i]; + break; + } + } + if (slot == NULL) + { + for (uint_fast8_t i = 0; i < RX_SLOT_COUNT; i++) + { + if (self->slots[i].transfer_id == transfer_id) + { + slot = &self->slots[i]; + break; + } + } + } + return slot; +} + /// This function is invoked when a new datagram pertaining to a certain session is received on an interface. /// This function will either move the frame payload into the session, or free it if it cannot be made use of. /// Returns: 1 -- transfer available; 0 -- transfer not yet available; <0 -- error. @@ -1203,16 +1232,7 @@ static inline int_fast8_t rxIfaceAccept(RxIface* const self, struct UdpardMemoryResource* const memory_payload) { UDPARD_ASSERT((self != NULL) && (frame.base.payload.size > 0) && (received_transfer != NULL)); - RxSlot* slot = NULL; - // First we should check if there is an existing slot for this transfer; if yes, this is the simplest case. - for (uint_fast8_t i = 0; i < RX_SLOT_COUNT; i++) - { - if (self->slots[i].transfer_id == frame.meta.transfer_id) - { - slot = &self->slots[i]; - break; - } - } + RxSlot* slot = rxIfaceFindMatchingSlot(self, frame.meta.transfer_id); // If there is no suitable slot, we should check if the transfer is a future one (high transfer-ID), // or a transfer-ID timeout has occurred. In this case we sacrifice the oldest slot. if (slot == NULL) diff --git a/tests/src/test_intrusive_rx.c b/tests/src/test_intrusive_rx.c index 6d2654f..ac1881d 100644 --- a/tests/src/test_intrusive_rx.c +++ b/tests/src/test_intrusive_rx.c @@ -1107,6 +1107,11 @@ static void testIfaceCheckTransferIDTimeout(void) TEST_ASSERT_TRUE(rxIfaceCheckTransferIDTimeout(&iface, 3150, 100)); // Ooh yes. } +static void testIfaceFindMatchingSlot(void) +{ + // TODO FIXME add tests +} + static void testIfaceAcceptA(void) { InstrumentedAllocator mem_fragment = {0}; @@ -1643,6 +1648,251 @@ static void testIfaceAcceptB(void) TEST_ASSERT_EQUAL(0, mem_fragment.allocated_fragments); } +static void testIfaceAcceptC(void) +{ + InstrumentedAllocator mem_fragment = {0}; + InstrumentedAllocator mem_payload = {0}; + instrumentedAllocatorNew(&mem_fragment); + instrumentedAllocatorNew(&mem_payload); + RxIface iface; + rxIfaceInit(&iface, &mem_fragment.base, &mem_payload.base); + TEST_ASSERT_EQUAL(TIMESTAMP_UNSET, iface.ts_usec); + for (size_t i = 0; i < RX_SLOT_COUNT; i++) + { + TEST_ASSERT_EQUAL(TIMESTAMP_UNSET, iface.slots[i].ts_usec); + TEST_ASSERT_EQUAL(TRANSFER_ID_UNSET, iface.slots[i].transfer_id); + TEST_ASSERT_EQUAL(0, iface.slots[i].max_index); + TEST_ASSERT_EQUAL(FRAME_INDEX_UNSET, iface.slots[i].eot_index); + TEST_ASSERT_EQUAL(0, iface.slots[i].accepted_frames); + TEST_ASSERT_EQUAL(0, iface.slots[i].payload_size); + TEST_ASSERT_NULL(iface.slots[i].fragments); + } + struct UdpardRxTransfer transfer = {0}; + // === TRANSFER === + // Send interleaving multi-frame transfers such that in the end slots have the same transfer-ID value: + // (primes for duplicates): + // A0 B0 A1 C0 B1 C1 B1' + // The purpose of this test is to ensure that the case of multiple RX slots having the same transfer-ID is + // handled correctly (including correct duplicate detection). + TEST_ASSERT_EQUAL(0, mem_payload.allocated_fragments); + TEST_ASSERT_EQUAL(0, mem_fragment.allocated_fragments); + // A0 + TEST_ASSERT_EQUAL(0, + rxIfaceAccept(&iface, + 2000000010, + makeRxFrameString(&mem_payload.base, // + (TransferMetadata){.priority = UdpardPriorityOptional, + .src_node_id = 1111, + .dst_node_id = UDPARD_NODE_ID_UNSET, + .data_specifier = 0, + .transfer_id = 0xA}, + 0, + false, + "A0"), + &transfer, + 1000, + UDPARD_DEFAULT_TRANSFER_ID_TIMEOUT_USEC, + &mem_fragment.base, + &mem_payload.base)); + TEST_ASSERT_EQUAL(TIMESTAMP_UNSET, iface.ts_usec); + TEST_ASSERT_EQUAL(TRANSFER_ID_UNSET, iface.slots[0].transfer_id); + TEST_ASSERT_EQUAL(0xA, iface.slots[1].transfer_id); + TEST_ASSERT_EQUAL(1, mem_payload.allocated_fragments); + TEST_ASSERT_EQUAL(1, mem_fragment.allocated_fragments); + // B0 + TEST_ASSERT_EQUAL(0, + rxIfaceAccept(&iface, + 2000000020, + makeRxFrameString(&mem_payload.base, // + (TransferMetadata){.priority = UdpardPriorityExceptional, + .src_node_id = 2222, + .dst_node_id = UDPARD_NODE_ID_UNSET, + .data_specifier = 0, + .transfer_id = 0xB}, + 0, + false, + "B0"), + &transfer, + 1000, + UDPARD_DEFAULT_TRANSFER_ID_TIMEOUT_USEC, + &mem_fragment.base, + &mem_payload.base)); + TEST_ASSERT_EQUAL(TIMESTAMP_UNSET, iface.ts_usec); + TEST_ASSERT_EQUAL(0xB, iface.slots[0].transfer_id); + TEST_ASSERT_EQUAL(0xA, iface.slots[1].transfer_id); + TEST_ASSERT_EQUAL(2, mem_payload.allocated_fragments); + TEST_ASSERT_EQUAL(2, mem_fragment.allocated_fragments); + // A1 + TEST_ASSERT_EQUAL(1, + rxIfaceAccept(&iface, + 2000000030, + makeRxFrameString(&mem_payload.base, // + (TransferMetadata){.priority = UdpardPriorityOptional, + .src_node_id = 1111, + .dst_node_id = UDPARD_NODE_ID_UNSET, + .data_specifier = 0, + .transfer_id = 0xA}, + 1, + true, + "A1" + "\xc7\xac_\x81"), + &transfer, + 1000, + UDPARD_DEFAULT_TRANSFER_ID_TIMEOUT_USEC, + &mem_fragment.base, + &mem_payload.base)); + // Check the received transfer. + TEST_ASSERT_EQUAL(2000000010, iface.ts_usec); + TEST_ASSERT_EQUAL(0xB, iface.slots[0].transfer_id); + TEST_ASSERT_EQUAL(0xB, iface.slots[1].transfer_id); // SAME VALUE!!1 + TEST_ASSERT_EQUAL(3, mem_payload.allocated_fragments); + TEST_ASSERT_EQUAL(2, mem_fragment.allocated_fragments); // Head optimization in effect. + TEST_ASSERT_EQUAL(UdpardPriorityOptional, transfer.priority); + TEST_ASSERT_EQUAL(4, transfer.payload_size); + TEST_ASSERT(compareStringWithPayload("A0", transfer.payload.view)); + TEST_ASSERT_NOT_NULL(transfer.payload.next); + TEST_ASSERT(compareStringWithPayload("A1", transfer.payload.next->view)); + TEST_ASSERT_NULL(transfer.payload.next->next); + udpardFragmentFree(transfer.payload, &mem_fragment.base, &mem_payload.base); + TEST_ASSERT_EQUAL(1, mem_payload.allocated_fragments); // B0 still allocated. + TEST_ASSERT_EQUAL(1, mem_fragment.allocated_fragments); + // C0 + TEST_ASSERT_EQUAL(0, + rxIfaceAccept(&iface, + 2000000040, + makeRxFrameString(&mem_payload.base, // + (TransferMetadata){.priority = UdpardPriorityExceptional, + .src_node_id = 3333, + .dst_node_id = UDPARD_NODE_ID_UNSET, + .data_specifier = 0, + .transfer_id = 0xC}, + 0, + false, + "C0"), + &transfer, + 1000, + UDPARD_DEFAULT_TRANSFER_ID_TIMEOUT_USEC, + &mem_fragment.base, + &mem_payload.base)); + TEST_ASSERT_EQUAL(2000000010, iface.ts_usec); // <- unchanged. + TEST_ASSERT_EQUAL(0xB, iface.slots[0].transfer_id); + TEST_ASSERT_EQUAL(0xC, iface.slots[1].transfer_id); // <- reused for C. + TEST_ASSERT_EQUAL(2, mem_payload.allocated_fragments); // Two transfers in transit again: B and C. + TEST_ASSERT_EQUAL(2, mem_fragment.allocated_fragments); + // B1 + TEST_ASSERT_EQUAL(1, + rxIfaceAccept(&iface, + 2000000050, + makeRxFrameString(&mem_payload.base, // + (TransferMetadata){.priority = UdpardPriorityExceptional, + .src_node_id = 2222, + .dst_node_id = UDPARD_NODE_ID_UNSET, + .data_specifier = 0, + .transfer_id = 0xB}, + 1, + true, + "B1" + "g\x8D\x9A\xD7"), + &transfer, + 1000, + UDPARD_DEFAULT_TRANSFER_ID_TIMEOUT_USEC, + &mem_fragment.base, + &mem_payload.base)); + // Check the received transfer. + TEST_ASSERT_EQUAL(2000000020, iface.ts_usec); + TEST_ASSERT_EQUAL(0xC, iface.slots[0].transfer_id); // <-- INCREMENTED, SO + TEST_ASSERT_EQUAL(0xC, iface.slots[1].transfer_id); // WE HAVE TWO IDENTICAL VALUES AGAIN! + TEST_ASSERT_EQUAL(3, mem_payload.allocated_fragments); + TEST_ASSERT_EQUAL(2, mem_fragment.allocated_fragments); + TEST_ASSERT_EQUAL(UdpardPriorityExceptional, transfer.priority); + TEST_ASSERT_EQUAL(4, transfer.payload_size); + TEST_ASSERT(compareStringWithPayload("B0", transfer.payload.view)); + TEST_ASSERT_NOT_NULL(transfer.payload.next); + TEST_ASSERT(compareStringWithPayload("B1", transfer.payload.next->view)); + TEST_ASSERT_NULL(transfer.payload.next->next); + udpardFragmentFree(transfer.payload, &mem_fragment.base, &mem_payload.base); + TEST_ASSERT_EQUAL(1, mem_payload.allocated_fragments); // C0 is still allocated. + TEST_ASSERT_EQUAL(1, mem_fragment.allocated_fragments); + // C1 + // This is the DIFFICULT CASE because we have two RX slots with the same transfer-ID, but THE FIRST ONE IS NOT + // THE ONE THAT WE NEED! Watch what happens next. + TEST_ASSERT_EQUAL(1, + rxIfaceAccept(&iface, + 2000000060, + makeRxFrameString(&mem_payload.base, // + (TransferMetadata){.priority = UdpardPriorityExceptional, + .src_node_id = 3333, + .dst_node_id = UDPARD_NODE_ID_UNSET, + .data_specifier = 0, + .transfer_id = 0xC}, + 1, + true, + "C1" + "\xA8\xBF}\x19"), + &transfer, + 1000, + UDPARD_DEFAULT_TRANSFER_ID_TIMEOUT_USEC, + &mem_fragment.base, + &mem_payload.base)); + // Check the received transfer. + TEST_ASSERT_EQUAL(2000000040, iface.ts_usec); + TEST_ASSERT_EQUAL(0xC, iface.slots[0].transfer_id); // Old, unused. + TEST_ASSERT_EQUAL(0xD, iface.slots[1].transfer_id); // INCREMENTED! + TEST_ASSERT_EQUAL(2, mem_payload.allocated_fragments); + TEST_ASSERT_EQUAL(1, mem_fragment.allocated_fragments); + TEST_ASSERT_EQUAL(UdpardPriorityExceptional, transfer.priority); + TEST_ASSERT_EQUAL(3333, transfer.source_node_id); + TEST_ASSERT_EQUAL(4, transfer.payload_size); + TEST_ASSERT(compareStringWithPayload("C0", transfer.payload.view)); + TEST_ASSERT_NOT_NULL(transfer.payload.next); + TEST_ASSERT(compareStringWithPayload("C1", transfer.payload.next->view)); + TEST_ASSERT_NULL(transfer.payload.next->next); + udpardFragmentFree(transfer.payload, &mem_fragment.base, &mem_payload.base); + TEST_ASSERT_EQUAL(0, mem_payload.allocated_fragments); + TEST_ASSERT_EQUAL(0, mem_fragment.allocated_fragments); + // B0 duplicate multi-frame; shall be rejected. + TEST_ASSERT_EQUAL(0, + rxIfaceAccept(&iface, + 2000000070, + makeRxFrameString(&mem_payload.base, // + (TransferMetadata){.priority = UdpardPriorityExceptional, + .src_node_id = 2222, + .dst_node_id = UDPARD_NODE_ID_UNSET, + .data_specifier = 0, + .transfer_id = 0xB}, + 0, + false, + "B0"), + &transfer, + 1000, + UDPARD_DEFAULT_TRANSFER_ID_TIMEOUT_USEC, + &mem_fragment.base, + &mem_payload.base)); + TEST_ASSERT_EQUAL(0, mem_payload.allocated_fragments); + TEST_ASSERT_EQUAL(0, mem_fragment.allocated_fragments); + // B0 duplicate single-frame; shall be rejected. + TEST_ASSERT_EQUAL(0, + rxIfaceAccept(&iface, + 2000000080, + makeRxFrameString(&mem_payload.base, // + (TransferMetadata){.priority = UdpardPriorityExceptional, + .src_node_id = 2222, + .dst_node_id = UDPARD_NODE_ID_UNSET, + .data_specifier = 0, + .transfer_id = 0xB}, + 0, + true, + "B0" + "g\x8D\x9A\xD7"), + &transfer, + 1000, + UDPARD_DEFAULT_TRANSFER_ID_TIMEOUT_USEC, + &mem_fragment.base, + &mem_payload.base)); + TEST_ASSERT_EQUAL(0, mem_payload.allocated_fragments); + TEST_ASSERT_EQUAL(0, mem_fragment.allocated_fragments); +} + void setUp(void) {} void tearDown(void) {} @@ -1672,8 +1922,10 @@ int main(void) // iface RUN_TEST(testIfaceIsFutureTransferID); RUN_TEST(testIfaceCheckTransferIDTimeout); + RUN_TEST(testIfaceFindMatchingSlot); RUN_TEST(testIfaceAcceptA); RUN_TEST(testIfaceAcceptB); + RUN_TEST(testIfaceAcceptC); return UNITY_END(); } From 6c439de46735fb84b3b83db9e565c6f9011fb7a4 Mon Sep 17 00:00:00 2001 From: Pavel Kirienko Date: Mon, 31 Jul 2023 11:56:39 +0300 Subject: [PATCH 14/38] Add tests for rxIfaceFindMatchingSlot --- libudpard/udpard.c | 19 +++++++++--------- tests/src/test_intrusive_rx.c | 37 +++++++++++++++++++++++++++++++++-- 2 files changed, 44 insertions(+), 12 deletions(-) diff --git a/libudpard/udpard.c b/libudpard/udpard.c index 5766187..e37413b 100644 --- a/libudpard/udpard.c +++ b/libudpard/udpard.c @@ -1190,18 +1190,18 @@ static inline bool rxIfaceCheckTransferIDTimeout(const RxIface* const self, ((ts_usec >= most_recent_ts_usec) && ((ts_usec - most_recent_ts_usec) > transfer_id_timeout_usec)); } -/// Traverses the list of slot trying to find a slot with a matching transfer-ID that is already IN PROGRESS. +/// Traverses the list of slots trying to find a slot with a matching transfer-ID that is already IN PROGRESS. /// If there is no such slot, tries again without the IN PROGRESS requirement. /// The purpose of this complicated dual check is to support the case where multiple slots have the same /// transfer-ID, which may occur with interleaved transfers. -static inline RxSlot* rxIfaceFindMatchingSlot(RxIface* const self, const UdpardTransferID transfer_id) +static inline RxSlot* rxIfaceFindMatchingSlot(RxSlot slots[RX_SLOT_COUNT], const UdpardTransferID transfer_id) { RxSlot* slot = NULL; for (uint_fast8_t i = 0; i < RX_SLOT_COUNT; i++) { - if ((self->slots[i].transfer_id == transfer_id) && (self->slots[i].ts_usec != TIMESTAMP_UNSET)) + if ((slots[i].transfer_id == transfer_id) && (slots[i].ts_usec != TIMESTAMP_UNSET)) { - slot = &self->slots[i]; + slot = &slots[i]; break; } } @@ -1209,9 +1209,9 @@ static inline RxSlot* rxIfaceFindMatchingSlot(RxIface* const self, const UdpardT { for (uint_fast8_t i = 0; i < RX_SLOT_COUNT; i++) { - if (self->slots[i].transfer_id == transfer_id) + if (slots[i].transfer_id == transfer_id) { - slot = &self->slots[i]; + slot = &slots[i]; break; } } @@ -1232,7 +1232,7 @@ static inline int_fast8_t rxIfaceAccept(RxIface* const self, struct UdpardMemoryResource* const memory_payload) { UDPARD_ASSERT((self != NULL) && (frame.base.payload.size > 0) && (received_transfer != NULL)); - RxSlot* slot = rxIfaceFindMatchingSlot(self, frame.meta.transfer_id); + RxSlot* slot = rxIfaceFindMatchingSlot(self->slots, frame.meta.transfer_id); // If there is no suitable slot, we should check if the transfer is a future one (high transfer-ID), // or a transfer-ID timeout has occurred. In this case we sacrifice the oldest slot. if (slot == NULL) @@ -1250,9 +1250,8 @@ static inline int_fast8_t rxIfaceAccept(RxIface* const self, victim = &self->slots[i]; } } - const bool is_future_tid = rxIfaceIsFutureTransferID(self, frame.meta.transfer_id); - const bool is_tid_timeout = rxIfaceCheckTransferIDTimeout(self, ts_usec, transfer_id_timeout_usec); - if (is_future_tid || is_tid_timeout) + if (rxIfaceIsFutureTransferID(self, frame.meta.transfer_id) || + rxIfaceCheckTransferIDTimeout(self, ts_usec, transfer_id_timeout_usec)) { rxSlotRestart(victim, frame.meta.transfer_id, memory_fragment, memory_payload); slot = victim; diff --git a/tests/src/test_intrusive_rx.c b/tests/src/test_intrusive_rx.c index ac1881d..355c580 100644 --- a/tests/src/test_intrusive_rx.c +++ b/tests/src/test_intrusive_rx.c @@ -1109,7 +1109,40 @@ static void testIfaceCheckTransferIDTimeout(void) static void testIfaceFindMatchingSlot(void) { - // TODO FIXME add tests + InstrumentedAllocator mem_fragment = {0}; + InstrumentedAllocator mem_payload = {0}; + instrumentedAllocatorNew(&mem_fragment); + instrumentedAllocatorNew(&mem_payload); + RxSlot slots[RX_SLOT_COUNT] = {0}; + rxSlotRestart(&slots[0], 1000, &mem_fragment.base, &mem_payload.base); + rxSlotRestart(&slots[1], 1001, &mem_fragment.base, &mem_payload.base); + // No matching slot. + TEST_ASSERT_NULL(rxIfaceFindMatchingSlot(slots, 123)); + // Matching slots. + TEST_ASSERT_EQUAL_PTR(&slots[0], rxIfaceFindMatchingSlot(slots, 1000)); + TEST_ASSERT_EQUAL_PTR(&slots[1], rxIfaceFindMatchingSlot(slots, 1001)); + // Identical slots, neither in progress. + slots[0].ts_usec = TIMESTAMP_UNSET; + slots[1].ts_usec = TIMESTAMP_UNSET; + slots[0].transfer_id = 1000; + slots[1].transfer_id = 1000; + TEST_ASSERT_EQUAL_PTR(&slots[0], rxIfaceFindMatchingSlot(slots, 1000)); // First match. + TEST_ASSERT_EQUAL_PTR(NULL, rxIfaceFindMatchingSlot(slots, 1001)); + // Identical slots, one of them in progress. + slots[0].ts_usec = TIMESTAMP_UNSET; + slots[1].ts_usec = 1234567890; + TEST_ASSERT_EQUAL_PTR(&slots[1], rxIfaceFindMatchingSlot(slots, 1000)); + TEST_ASSERT_EQUAL_PTR(NULL, rxIfaceFindMatchingSlot(slots, 1001)); + // The other is in progress now. + slots[0].ts_usec = 1234567890; + slots[1].ts_usec = TIMESTAMP_UNSET; + TEST_ASSERT_EQUAL_PTR(&slots[0], rxIfaceFindMatchingSlot(slots, 1000)); + TEST_ASSERT_EQUAL_PTR(NULL, rxIfaceFindMatchingSlot(slots, 1001)); + // Both in progress, pick first. + slots[0].ts_usec = 1234567890; + slots[1].ts_usec = 2345678901; + TEST_ASSERT_EQUAL_PTR(&slots[0], rxIfaceFindMatchingSlot(slots, 1000)); + TEST_ASSERT_EQUAL_PTR(NULL, rxIfaceFindMatchingSlot(slots, 1001)); } static void testIfaceAcceptA(void) @@ -1669,7 +1702,7 @@ static void testIfaceAcceptC(void) } struct UdpardRxTransfer transfer = {0}; // === TRANSFER === - // Send interleaving multi-frame transfers such that in the end slots have the same transfer-ID value: + // Send interleaving multi-frame transfers such that in the end slots have the same transfer-ID value // (primes for duplicates): // A0 B0 A1 C0 B1 C1 B1' // The purpose of this test is to ensure that the case of multiple RX slots having the same transfer-ID is From 0b40de0d7def45dcf8a816cbd24effb84bf16bb0 Mon Sep 17 00:00:00 2001 From: Pavel Kirienko Date: Mon, 31 Jul 2023 13:05:44 +0300 Subject: [PATCH 15/38] Add session-level frame acceptance logic and transfer deduplication; slightly refactor things to reduce copypasta --- libudpard/udpard.c | 231 ++++++++++++++++----------- libudpard/udpard.h | 9 +- tests/.idea/dictionaries/pavel.xml | 1 + tests/src/test_intrusive_rx.c | 243 +++++++++++++---------------- 4 files changed, 259 insertions(+), 225 deletions(-) diff --git a/libudpard/udpard.c b/libudpard/udpard.c index e37413b..d879c08 100644 --- a/libudpard/udpard.c +++ b/libudpard/udpard.c @@ -776,6 +776,14 @@ static inline bool rxParseFrame(const struct UdpardMutablePayload datagram_paylo return ok; } +/// This helper is needed to minimize the risk of argument swapping when passing these two resources around, +/// as they almost always go side by side. +typedef struct +{ + struct UdpardMemoryResource* fragment; + struct UdpardMemoryResource* payload; +} RxMemory; + typedef struct { struct UdpardTreeNode base; @@ -791,16 +799,14 @@ typedef struct RxFragment uint32_t frame_index; } RxFragment; -static inline void rxFragmentFree(struct UdpardFragment* const head, - struct UdpardMemoryResource* const memory_fragment, - struct UdpardMemoryResource* const memory_payload) +static inline void rxFragmentFree(struct UdpardFragment* const head, const RxMemory memory) { struct UdpardFragment* handle = head; while (handle != NULL) { struct UdpardFragment* const next = handle->next; - memFreePayload(memory_payload, handle->origin); // May be NULL, is okay. - memFree(memory_fragment, sizeof(RxFragment), handle); + memFreePayload(memory.payload, handle->origin); // May be NULL, is okay. + memFree(memory.fragment, sizeof(RxFragment), handle); handle = next; } } @@ -850,7 +856,9 @@ typedef struct RxSlot slots[RX_SLOT_COUNT]; } RxIface; -/// This type is forward-declared externally, hence why it has such a long name with the udpard prefix. +/// This type is forward-declared externally, hence why it has such a long name with the "udpard" prefix. +/// Keep in mind that we have a dedicated session object per remote node per port; this means that the states +/// kept here -- the timestamp and the transfer-ID -- are specific per remote node, as it should be. typedef struct UdpardInternalRxSession { struct UdpardTreeNode base; @@ -866,30 +874,25 @@ typedef struct UdpardInternalRxSession } UdpardInternalRxSession; // NOLINTNEXTLINE(misc-no-recursion) -static inline void rxSlotFree(RxFragment* const self, - struct UdpardMemoryResource* const memory_fragment, - struct UdpardMemoryResource* const memory_payload) +static inline void rxSlotFree(RxFragment* const self, const RxMemory memory) { - UDPARD_ASSERT((self != NULL) && memIsValid(memory_fragment)); - memFreePayload(memory_payload, self->base.origin); + UDPARD_ASSERT(self != NULL); + memFreePayload(memory.payload, self->base.origin); for (uint_fast8_t i = 0; i < 2; i++) { RxFragmentTreeNode* const child = (RxFragmentTreeNode*) self->tree.base.lr[i]; if (child != NULL) { UDPARD_ASSERT(child->base.up == &self->tree.base); - rxSlotFree(child->this, memory_fragment, memory_payload); + rxSlotFree(child->this, memory); } } - memFree(memory_fragment, sizeof(RxFragment), self); // self-destruct + memFree(memory.fragment, sizeof(RxFragment), self); // self-destruct } -static inline void rxSlotRestart(RxSlot* const self, - const UdpardTransferID transfer_id, - struct UdpardMemoryResource* const memory_fragment, - struct UdpardMemoryResource* const memory_payload) +static inline void rxSlotRestart(RxSlot* const self, const UdpardTransferID transfer_id, const RxMemory memory) { - UDPARD_ASSERT((self != NULL) && (memory_fragment != NULL) && (memory_payload != NULL)); + UDPARD_ASSERT(self != NULL); self->ts_usec = TIMESTAMP_UNSET; // Will be assigned when the first frame of the transfer has arrived. self->transfer_id = transfer_id; self->max_index = 0; @@ -898,7 +901,7 @@ static inline void rxSlotRestart(RxSlot* const self, self->payload_size = 0; if (self->fragments != NULL) { - rxSlotFree(self->fragments->this, memory_fragment, memory_payload); + rxSlotFree(self->fragments->this, memory); self->fragments = NULL; } } @@ -949,20 +952,19 @@ static inline struct UdpardTreeNode* rxSlotFragmentFactory(void* const user_refe /// States outliving each level of recursion while ejecting the transfer from the fragment tree. typedef struct { - struct UdpardFragment* head; // Points to the first fragment in the list. - struct UdpardFragment* predecessor; - uint32_t crc; - size_t retain_size; - size_t offset; - struct UdpardMemoryResource* memory_fragment; - struct UdpardMemoryResource* memory_payload; + struct UdpardFragment* head; // Points to the first fragment in the list. + struct UdpardFragment* predecessor; + uint32_t crc; + size_t retain_size; + size_t offset; + RxMemory memory; } RxSlotEjectContext; /// See rxSlotEject() for details. /// NOLINTNEXTLINE(misc-no-recursion) static inline void rxSlotEjectFragment(RxFragment* const frag, RxSlotEjectContext* const ctx) { - UDPARD_ASSERT((frag != NULL) && (ctx != NULL) && memIsValid(ctx->memory_fragment)); + UDPARD_ASSERT((frag != NULL) && (ctx != NULL)); if (frag->tree.base.lr[0] != NULL) { RxFragment* const child = ((RxFragmentTreeNode*) frag->tree.base.lr[0])->this; @@ -998,8 +1000,8 @@ static inline void rxSlotEjectFragment(RxFragment* const frag, RxSlotEjectContex // Drop the unneeded fragments and their handles after the sub-tree is fully traversed. if (!retain) { - memFreePayload(ctx->memory_payload, frag->base.origin); - memFree(ctx->memory_fragment, sizeof(RxFragment), frag); + memFreePayload(ctx->memory.payload, frag->base.origin); + memFree(ctx->memory.fragment, sizeof(RxFragment), frag); } } @@ -1019,25 +1021,23 @@ static inline void rxSlotEjectFragment(RxFragment* const frag, RxSlotEjectContex /// There shall be at least one fragment (because a Cyphal transfer contains at least one frame). /// /// The return value indicates whether the transfer is valid (CRC is correct). -static inline bool rxSlotEject(size_t* const out_payload_size, - struct UdpardFragment* const out_payload_head, - RxFragmentTreeNode* const fragment_tree, - const size_t received_total_size, // With CRC. - const size_t extent, - struct UdpardMemoryResource* const memory_fragment, - struct UdpardMemoryResource* const memory_payload) +static inline bool rxSlotEject(size_t* const out_payload_size, + struct UdpardFragment* const out_payload_head, + RxFragmentTreeNode* const fragment_tree, + const size_t received_total_size, // With CRC. + const size_t extent, + const RxMemory memory) { UDPARD_ASSERT((received_total_size >= TRANSFER_CRC_SIZE_BYTES) && (fragment_tree != NULL) && (out_payload_size != NULL) && (out_payload_head != NULL)); bool result = false; RxSlotEjectContext eject_ctx = { - .head = NULL, - .predecessor = NULL, - .crc = TRANSFER_CRC_INITIAL, - .retain_size = smaller(received_total_size - TRANSFER_CRC_SIZE_BYTES, extent), - .offset = 0, - .memory_fragment = memory_fragment, - .memory_payload = memory_payload, + .head = NULL, + .predecessor = NULL, + .crc = TRANSFER_CRC_INITIAL, + .retain_size = smaller(received_total_size - TRANSFER_CRC_SIZE_BYTES, extent), + .offset = 0, + .memory = memory, }; rxSlotEjectFragment(fragment_tree->this, &eject_ctx); UDPARD_ASSERT(eject_ctx.offset == received_total_size); // Ensure we have traversed the entire tree. @@ -1051,11 +1051,11 @@ static inline bool rxSlotEject(size_t* const out_payload_si // This is the single-frame transfer optimization suggested by Scott: we free the first fragment handle // early by moving the contents into the rx_transfer structure by value. // No need to free the payload buffer because it has been transferred to the transfer. - memFree(memory_fragment, sizeof(RxFragment), eject_ctx.head); // May be empty. + memFree(memory.fragment, sizeof(RxFragment), eject_ctx.head); // May be empty. } else // The transfer turned out to be invalid. We have to free the fragments. Can't use the tree anymore. { - rxFragmentFree(eject_ctx.head, memory_fragment, memory_payload); + rxFragmentFree(eject_ctx.head, memory); } return result; } @@ -1063,16 +1063,15 @@ static inline bool rxSlotEject(size_t* const out_payload_si /// This function will either move the frame payload into the session, or free it if it cannot be made use of. /// Upon return, certain state variables may be overwritten, so the caller should not rely on them. /// Returns: 1 -- transfer available, payload written; 0 -- transfer not yet available; <0 -- error. -static inline int_fast8_t rxSlotAccept(RxSlot* const self, - size_t* const out_transfer_payload_size, - struct UdpardFragment* const out_transfer_payload_head, - const RxFrameBase frame, - const size_t extent, - struct UdpardMemoryResource* const memory_fragment, - struct UdpardMemoryResource* const memory_payload) +static inline int_fast8_t rxSlotAccept(RxSlot* const self, + size_t* const out_transfer_payload_size, + struct UdpardFragment* const out_transfer_payload_head, + const RxFrameBase frame, + const size_t extent, + const RxMemory memory) { UDPARD_ASSERT((self != NULL) && (frame.payload.size > 0) && (out_transfer_payload_size != NULL) && - (out_transfer_payload_head != NULL) && memIsValid(memory_fragment)); + (out_transfer_payload_head != NULL)); int_fast8_t result = 0; bool restart = false; bool release = true; @@ -1097,7 +1096,7 @@ static inline int_fast8_t rxSlotAccept(RxSlot* const self, UDPARD_ASSERT((self->max_index <= self->eot_index) && (self->accepted_frames <= self->eot_index)); RxSlotUpdateContext update_ctx = {.frame_index = frame.index, .accepted = false, - .memory_fragment = memory_fragment}; + .memory_fragment = memory.fragment}; RxFragmentTreeNode* const frag = (RxFragmentTreeNode*) cavlSearch((struct UdpardTreeNode**) &self->fragments, // &update_ctx, &rxSlotFragmentSearch, @@ -1133,8 +1132,7 @@ static inline int_fast8_t rxSlotAccept(RxSlot* const self, self->fragments, self->payload_size, extent, - memory_fragment, - memory_payload) + memory) ? 1 : 0; // The tree is now unusable and the data is moved into rx_transfer. @@ -1145,11 +1143,11 @@ static inline int_fast8_t rxSlotAccept(RxSlot* const self, if (restart) { // Increment is necessary to weed out duplicate transfers. - rxSlotRestart(self, self->transfer_id + 1U, memory_fragment, memory_payload); + rxSlotRestart(self, self->transfer_id + 1U, memory); } if (release) { - memFreePayload(memory_payload, frame.origin); + memFreePayload(memory.payload, frame.origin); } return result; } @@ -1187,7 +1185,7 @@ static inline bool rxIfaceCheckTransferIDTimeout(const RxIface* const self, } } return (most_recent_ts_usec == TIMESTAMP_UNSET) || - ((ts_usec >= most_recent_ts_usec) && ((ts_usec - most_recent_ts_usec) > transfer_id_timeout_usec)); + ((ts_usec >= most_recent_ts_usec) && ((ts_usec - most_recent_ts_usec) >= transfer_id_timeout_usec)); } /// Traverses the list of slots trying to find a slot with a matching transfer-ID that is already IN PROGRESS. @@ -1222,16 +1220,15 @@ static inline RxSlot* rxIfaceFindMatchingSlot(RxSlot slots[RX_SLOT_COUNT], const /// This function is invoked when a new datagram pertaining to a certain session is received on an interface. /// This function will either move the frame payload into the session, or free it if it cannot be made use of. /// Returns: 1 -- transfer available; 0 -- transfer not yet available; <0 -- error. -static inline int_fast8_t rxIfaceAccept(RxIface* const self, - const UdpardMicrosecond ts_usec, - const RxFrame frame, - struct UdpardRxTransfer* const received_transfer, - const size_t extent, - const UdpardMicrosecond transfer_id_timeout_usec, - struct UdpardMemoryResource* const memory_fragment, - struct UdpardMemoryResource* const memory_payload) -{ - UDPARD_ASSERT((self != NULL) && (frame.base.payload.size > 0) && (received_transfer != NULL)); +static inline int_fast8_t rxIfaceAccept(RxIface* const self, + const UdpardMicrosecond ts_usec, + const RxFrame frame, + const size_t extent, + const UdpardMicrosecond transfer_id_timeout_usec, + const RxMemory memory, + struct UdpardRxTransfer* const out_transfer) +{ + UDPARD_ASSERT((self != NULL) && (frame.base.payload.size > 0) && (out_transfer != NULL)); RxSlot* slot = rxIfaceFindMatchingSlot(self->slots, frame.meta.transfer_id); // If there is no suitable slot, we should check if the transfer is a future one (high transfer-ID), // or a transfer-ID timeout has occurred. In this case we sacrifice the oldest slot. @@ -1253,7 +1250,7 @@ static inline int_fast8_t rxIfaceAccept(RxIface* const self, if (rxIfaceIsFutureTransferID(self, frame.meta.transfer_id) || rxIfaceCheckTransferIDTimeout(self, ts_usec, transfer_id_timeout_usec)) { - rxSlotRestart(victim, frame.meta.transfer_id, memory_fragment, memory_payload); + rxSlotRestart(victim, frame.meta.transfer_id, memory); slot = victim; UDPARD_ASSERT(slot != NULL); } @@ -1270,31 +1267,28 @@ static inline int_fast8_t rxIfaceAccept(RxIface* const self, const UdpardMicrosecond ts = slot->ts_usec; UDPARD_ASSERT(slot->transfer_id == frame.meta.transfer_id); result = rxSlotAccept(slot, // May invalidate state variables such as timestamp or transfer-ID. - &received_transfer->payload_size, - &received_transfer->payload, + &out_transfer->payload_size, + &out_transfer->payload, frame.base, extent, - memory_fragment, - memory_payload); + memory); if (result > 0) // Transfer successfully received, populate the transfer descriptor for the client. { - self->ts_usec = ts; // Update the last valid transfer timestamp on this iface. - received_transfer->timestamp_usec = ts; - received_transfer->priority = frame.meta.priority; - received_transfer->source_node_id = frame.meta.src_node_id; - received_transfer->transfer_id = frame.meta.transfer_id; + self->ts_usec = ts; // Update the last valid transfer timestamp on this iface. + out_transfer->timestamp_usec = ts; + out_transfer->priority = frame.meta.priority; + out_transfer->source_node_id = frame.meta.src_node_id; + out_transfer->transfer_id = frame.meta.transfer_id; } } else { - memFreePayload(memory_payload, frame.base.origin); + memFreePayload(memory.payload, frame.base.origin); } return result; } -static inline void rxIfaceInit(RxIface* const self, - struct UdpardMemoryResource* const memory_fragment, - struct UdpardMemoryResource* const memory_payload) +static inline void rxIfaceInit(RxIface* const self, const RxMemory memory) { UDPARD_ASSERT(self != NULL); // NOLINTNEXTLINE(clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling) @@ -1303,7 +1297,61 @@ static inline void rxIfaceInit(RxIface* const self, for (uint_fast8_t i = 0; i < RX_SLOT_COUNT; i++) { self->slots[i].fragments = NULL; - rxSlotRestart(&self->slots[i], TRANSFER_ID_UNSET, memory_fragment, memory_payload); + rxSlotRestart(&self->slots[i], TRANSFER_ID_UNSET, memory); + } +} + +static inline int_fast8_t rxSessionAccept(UdpardInternalRxSession* const self, + const uint_fast8_t redundant_iface_index, + const UdpardMicrosecond ts_usec, + const RxFrame frame, + const size_t extent, + const UdpardMicrosecond transfer_id_timeout_usec, + const RxMemory memory, + struct UdpardRxTransfer* const out_transfer) +{ + UDPARD_ASSERT((self != NULL) && (redundant_iface_index < UDPARD_NETWORK_INTERFACE_COUNT_MAX) && + (out_transfer != NULL)); + int_fast8_t result = rxIfaceAccept(&self->ifaces[redundant_iface_index], + ts_usec, + frame, + extent, + transfer_id_timeout_usec, + memory, + out_transfer); + UDPARD_ASSERT(result <= 1); + if (result > 0) + { + const bool future_tid = (self->last_transfer_id == TRANSFER_ID_UNSET) || // + (out_transfer->transfer_id > self->last_transfer_id); + const bool tid_timeout = (self->last_ts_usec == TIMESTAMP_UNSET) || + ((out_transfer->timestamp_usec >= self->last_ts_usec) && + ((out_transfer->timestamp_usec - self->last_ts_usec) >= transfer_id_timeout_usec)); + if (future_tid || tid_timeout) // Notice that the deduplicator makes no distinction between the interfaces. + { + self->last_ts_usec = out_transfer->timestamp_usec; + self->last_transfer_id = out_transfer->transfer_id; + } + else // This is a duplicate: received from another interface, a FEC retransmission, or a network glitch. + { + result = 0; + memFreePayload(memory.payload, out_transfer->payload.origin); + rxFragmentFree(out_transfer->payload.next, memory); + } + } + return result; +} + +static inline void rxSessionInit(UdpardInternalRxSession* const self, const RxMemory memory) +{ + UDPARD_ASSERT(self != NULL); + // NOLINTNEXTLINE(clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling) + (void) memset(self, 0, sizeof(*self)); + self->last_ts_usec = TIMESTAMP_UNSET; + self->last_transfer_id = TRANSFER_ID_UNSET; + for (uint_fast8_t i = 0; i < UDPARD_NETWORK_INTERFACE_COUNT_MAX; i++) + { + rxIfaceInit(&self->ifaces[i], memory); } } @@ -1311,8 +1359,15 @@ void udpardFragmentFree(const struct UdpardFragment head, struct UdpardMemoryResource* const memory_fragment, struct UdpardMemoryResource* const memory_payload) { - memFreePayload(memory_payload, head.origin); // May be NULL, is okay. - rxFragmentFree(head.next, memory_fragment, memory_payload); // The head is not heap-allocated so not freed. + if ((memory_fragment != NULL) && (memory_payload != NULL)) + { + memFreePayload(memory_payload, head.origin); // May be NULL, is okay. + rxFragmentFree(head.next, // The head is not heap-allocated so not freed. + (RxMemory){ + .fragment = memory_fragment, + .payload = memory_payload, + }); + } } int8_t udpardRxSubscriptionInit(struct UdpardRxSubscription* const self, @@ -1325,7 +1380,7 @@ int8_t udpardRxSubscriptionInit(struct UdpardRxSubscription* const self, (void) extent; (void) memory; (void) rxParseFrame; - (void) rxIfaceInit; - (void) rxIfaceAccept; + (void) rxSessionInit; + (void) rxSessionAccept; return 0; } diff --git a/libudpard/udpard.h b/libudpard/udpard.h index 1ddcd7d..3677bcf 100644 --- a/libudpard/udpard.h +++ b/libudpard/udpard.h @@ -834,12 +834,13 @@ void udpardRxSubscriptionDestroy(struct UdpardRxSubscription* const self); /// carry a final part of a valid multi-frame transfer, or carry a valid single-frame transfer. /// The last two cases are said to complete a transfer. /// -/// If the datagram completes a transfer, the received_transfer argument is filled with the transfer details +/// If the datagram completes a transfer, the out_transfer argument is filled with the transfer details /// and the return value is one. /// The caller is assigned ownership of the transfer payload buffer memory; it has to be freed after use as described /// in the documentation for UdpardRxTransfer. +/// The memory pointed to by out_transfer may be mutated arbitrarily if no transfer is completed. /// -/// If the datagram does not complete a transfer or is malformed, the function returns zero and the received_transfer +/// If the datagram does not complete a transfer or is malformed, the function returns zero and the out_transfer /// is not modified. Observe that malformed frames are not treated as errors, as the local application is not /// responsible for the behavior of external agents producing the datagrams. /// @@ -866,7 +867,7 @@ int8_t udpardRxSubscriptionReceive(struct UdpardRxSubscription* const self, const UdpardMicrosecond timestamp_usec, const struct UdpardMutablePayload datagram_payload, const uint_fast8_t redundant_iface_index, - struct UdpardRxTransfer* const received_transfer); + struct UdpardRxTransfer* const out_transfer); // --------------------------------------------- RPC-SERVICES --------------------------------------------- @@ -1002,7 +1003,7 @@ int8_t udpardRxRPCDispatcherReceive(struct UdpardRxRPCDispatcher* const self, const UdpardMicrosecond timestamp_usec, const struct UdpardMutablePayload datagram_payload, const uint_fast8_t redundant_iface_index, - struct UdpardRxRPCTransfer* const received_transfer); + struct UdpardRxRPCTransfer* const out_transfer); #ifdef __cplusplus } diff --git a/tests/.idea/dictionaries/pavel.xml b/tests/.idea/dictionaries/pavel.xml index 60e0200..f1e8005 100644 --- a/tests/.idea/dictionaries/pavel.xml +++ b/tests/.idea/dictionaries/pavel.xml @@ -4,6 +4,7 @@ baremetal cfamily deallocation + deduplicator discardment dscp dudpard diff --git a/tests/src/test_intrusive_rx.c b/tests/src/test_intrusive_rx.c index 355c580..39b75a6 100644 --- a/tests/src/test_intrusive_rx.c +++ b/tests/src/test_intrusive_rx.c @@ -121,6 +121,11 @@ static RxFrame makeRxFrameString(struct UdpardMemoryResource* const memory_paylo .meta = meta}; } +static RxMemory makeRxMemory(InstrumentedAllocator* const fragment, InstrumentedAllocator* const payload) +{ + return (RxMemory){.fragment = &fragment->base, .payload = &payload->base}; +} + // -------------------------------------------------------------------------------------------------------------------- // Generate reference data using PyCyphal: @@ -270,7 +275,7 @@ static void testSlotRestartEmpty(void) .fragments = NULL, }; InstrumentedAllocator alloc = {0}; - rxSlotRestart(&slot, 0x1122334455667788ULL, &alloc.base, &alloc.base); + rxSlotRestart(&slot, 0x1122334455667788ULL, makeRxMemory(&alloc, &alloc)); TEST_ASSERT_EQUAL(TIMESTAMP_UNSET, slot.ts_usec); TEST_ASSERT_EQUAL(0x1122334455667788ULL, slot.transfer_id); TEST_ASSERT_EQUAL(0, slot.max_index); @@ -324,7 +329,7 @@ static void testSlotRestartNonEmpty(void) TEST_ASSERT_EQUAL(3, mem_fragment.allocated_fragments); TEST_ASSERT_EQUAL(sizeof(RxFragment) * 3, mem_fragment.allocated_bytes); // Now we reset the slot, causing all memory to be freed correctly. - rxSlotRestart(&slot, 0x1122334455667788ULL, &mem_fragment.base, &mem_payload.base); + rxSlotRestart(&slot, 0x1122334455667788ULL, makeRxMemory(&mem_fragment, &mem_payload)); TEST_ASSERT_EQUAL(TIMESTAMP_UNSET, slot.ts_usec); TEST_ASSERT_EQUAL(0x1122334455667788ULL, slot.transfer_id); TEST_ASSERT_EQUAL(0, slot.max_index); @@ -394,8 +399,7 @@ static void testSlotEjectValidLarge(void) &root->tree, mem_payload.allocated_bytes, 1024, - &mem_fragment.base, - &mem_payload.base)); + makeRxMemory(&mem_fragment, &mem_payload))); TEST_ASSERT_EQUAL(PayloadSize, payload_size); // CRC removed! TEST_ASSERT( // compareStringWithPayload("Da Shi, have you ever... considered certain ultimate philosophical questions? ", @@ -481,8 +485,7 @@ static void testSlotEjectValidSmall(void) &root->tree, mem_payload.allocated_bytes, 136, // <-- small extent, rest truncated - &mem_fragment.base, - &mem_payload.base)); + makeRxMemory(&mem_fragment, &mem_payload))); TEST_ASSERT_EQUAL(136, payload_size); // Equals the extent due to the truncation. TEST_ASSERT(compareStringWithPayload("Did you build this four-dimensional fragment?\n", payload.view)); TEST_ASSERT(compareStringWithPayload("You told me that you came from the sea. Did you build the sea?\n", @@ -533,8 +536,7 @@ static void testSlotEjectValidEmpty(void) &root->tree, mem_payload.allocated_bytes, 0, - &mem_fragment.base, - &mem_payload.base)); + makeRxMemory(&mem_fragment, &mem_payload))); TEST_ASSERT_EQUAL(0, payload_size); // Equals the extent due to the truncation. TEST_ASSERT_NULL(payload.next); TEST_ASSERT_EQUAL(0, payload.view.size); @@ -580,8 +582,7 @@ static void testSlotEjectInvalid(void) &root->tree, mem_payload.allocated_bytes, 1000, - &mem_fragment.base, - &mem_payload.base)); + makeRxMemory(&mem_fragment, &mem_payload))); // The call was unsuccessful, so the memory was freed instead of being handed over to the application. TEST_ASSERT_EQUAL(0, mem_payload.allocated_fragments); TEST_ASSERT_EQUAL(0, mem_payload.allocated_bytes); @@ -595,6 +596,7 @@ static void testSlotAcceptA(void) InstrumentedAllocator mem_payload = {0}; instrumentedAllocatorNew(&mem_fragment); instrumentedAllocatorNew(&mem_payload); + const RxMemory mem = makeRxMemory(&mem_fragment, &mem_payload); // Set up the RX slot instance we're going to be working with. RxSlot slot = { .ts_usec = 1234567890, @@ -622,8 +624,7 @@ static void testSlotAcceptA(void) "The fish responsible for drying the sea are not here." "\x04\x1F\x8C\x1F"), 1000, - &mem_fragment.base, - &mem_payload.base)); + mem)); // Verify the memory utilization. Note that the small transfer optimization is in effect: head fragment moved. TEST_ASSERT_EQUAL(1, mem_payload.allocated_fragments); TEST_ASSERT_EQUAL(53 + TRANSFER_CRC_SIZE_BYTES, mem_payload.allocated_bytes); @@ -658,8 +659,7 @@ static void testSlotAcceptA(void) false, "We're sorry. What you said is really hard to understand.\n"), 1000, - &mem_fragment.base, - &mem_payload.base)); + mem)); TEST_ASSERT_EQUAL(0, rxSlotAccept(&slot, &payload_size, @@ -670,8 +670,7 @@ static void testSlotAcceptA(void) "The fish who dried the sea went onto land before they did " "this. "), 1000, - &mem_fragment.base, - &mem_payload.base)); + mem)); TEST_ASSERT_EQUAL(1, rxSlotAccept(&slot, &payload_size, @@ -682,8 +681,7 @@ static void testSlotAcceptA(void) "They moved from one dark forest to another dark forest." "?\xAC(\xBE"), 1000, - &mem_fragment.base, - &mem_payload.base)); + mem)); // Verify the memory utilization. Note that the small transfer optimization is in effect: head fragment moved. TEST_ASSERT_EQUAL(3, mem_payload.allocated_fragments); TEST_ASSERT_EQUAL(176 + TRANSFER_CRC_SIZE_BYTES, mem_payload.allocated_bytes); @@ -727,8 +725,7 @@ static void testSlotAcceptA(void) "Toss it over." "K(\xBB\xEE"), 45, - &mem_fragment.base, - &mem_payload.base)); + mem)); TEST_ASSERT_EQUAL(1, mem_payload.allocated_fragments); TEST_ASSERT_EQUAL(1, mem_fragment.allocated_fragments); TEST_ASSERT_EQUAL(0, @@ -740,8 +737,7 @@ static void testSlotAcceptA(void) false, "How do we give it to you?\n"), 45, - &mem_fragment.base, - &mem_payload.base)); + mem)); TEST_ASSERT_EQUAL(2, mem_payload.allocated_fragments); TEST_ASSERT_EQUAL(2, mem_fragment.allocated_fragments); TEST_ASSERT_EQUAL(0, @@ -753,8 +749,7 @@ static void testSlotAcceptA(void) false, "DUPLICATE #1"), 45, - &mem_fragment.base, - &mem_payload.base)); + mem)); TEST_ASSERT_EQUAL(2, mem_payload.allocated_fragments); // NO CHANGE, duplicate discarded. TEST_ASSERT_EQUAL(2, mem_fragment.allocated_fragments); TEST_ASSERT_EQUAL(0, @@ -766,8 +761,7 @@ static void testSlotAcceptA(void) true, "DUPLICATE #2"), 45, - &mem_fragment.base, - &mem_payload.base)); + mem)); TEST_ASSERT_EQUAL(2, mem_payload.allocated_fragments); // NO CHANGE, duplicate discarded. TEST_ASSERT_EQUAL(2, mem_fragment.allocated_fragments); TEST_ASSERT_EQUAL(1, // transfer completed @@ -779,8 +773,7 @@ static void testSlotAcceptA(void) false, "I like fish. Can I have it?\n"), 45, - &mem_fragment.base, - &mem_payload.base)); + mem)); // Verify the memory utilization. Note that the small transfer optimization is in effect: head fragment moved. // Due to the implicit truncation (the extent is small), the last fragment is already freed. TEST_ASSERT_EQUAL(2, mem_payload.allocated_fragments); // One freed because of truncation. @@ -815,8 +808,7 @@ static void testSlotAcceptA(void) &payload, makeRxFrameBaseString(&mem_payload.base, 0, true, ":D"), 1000, - &mem_fragment.base, - &mem_payload.base)); + mem)); TEST_ASSERT_EQUAL(0, mem_payload.allocated_fragments); TEST_ASSERT_EQUAL(0, mem_payload.allocated_bytes); TEST_ASSERT_EQUAL(0, mem_fragment.allocated_fragments); @@ -846,8 +838,7 @@ static void testSlotAcceptA(void) "Toss it over." "K(\xBB\xEE"), 1000, - &mem_fragment.base, - &mem_payload.base)); + mem)); TEST_ASSERT_EQUAL(1, mem_payload.allocated_fragments); TEST_ASSERT_EQUAL(1, mem_fragment.allocated_fragments); // Limit reached here. Cannot accept next fragment. TEST_ASSERT_EQUAL(-UDPARD_ERROR_MEMORY, @@ -859,8 +850,7 @@ static void testSlotAcceptA(void) false, "How do we give it to you?\n"), 1000, - &mem_fragment.base, - &mem_payload.base)); + mem)); TEST_ASSERT_EQUAL(1, mem_payload.allocated_fragments); // Payload not accepted, cannot alloc fragment. TEST_ASSERT_EQUAL(1, mem_fragment.allocated_fragments); mem_fragment.limit_fragments = 2; // Lift the limit and repeat the same frame, this time it is accepted. @@ -873,8 +863,7 @@ static void testSlotAcceptA(void) false, "I like fish. Can I have it?\n"), 1000, - &mem_fragment.base, - &mem_payload.base)); + mem)); TEST_ASSERT_EQUAL(2, mem_payload.allocated_fragments); // Accepted! TEST_ASSERT_EQUAL(2, mem_fragment.allocated_fragments); TEST_ASSERT_EQUAL(-UDPARD_ERROR_MEMORY, // Cannot alloc third fragment. @@ -886,8 +875,7 @@ static void testSlotAcceptA(void) false, "How do we give it to you?\n"), 1000, - &mem_fragment.base, - &mem_payload.base)); + mem)); TEST_ASSERT_EQUAL(2, mem_payload.allocated_fragments); // Payload not accepted, cannot alloc fragment. TEST_ASSERT_EQUAL(2, mem_fragment.allocated_fragments); mem_fragment.limit_fragments = 3; // Lift the limit and repeat the same frame, this time it is accepted. @@ -900,8 +888,7 @@ static void testSlotAcceptA(void) false, "How do we give it to you?\n"), 1000, - &mem_fragment.base, - &mem_payload.base)); + mem)); // Verify the memory utilization. Note that the small transfer optimization is in effect: head fragment moved. TEST_ASSERT_EQUAL(3, mem_payload.allocated_fragments); TEST_ASSERT_EQUAL(67 + TRANSFER_CRC_SIZE_BYTES, mem_payload.allocated_bytes); @@ -943,8 +930,7 @@ static void testSlotAcceptA(void) "Toss it over." "K(\xBB\xEE"), 45, - &mem_fragment.base, - &mem_payload.base)); + mem)); TEST_ASSERT_EQUAL(1, mem_payload.allocated_fragments); // Okay, accepted, some data stored... TEST_ASSERT_EQUAL(1, mem_fragment.allocated_fragments); TEST_ASSERT_EQUAL(0, @@ -956,8 +942,7 @@ static void testSlotAcceptA(void) true, // SURPRISE! EOT is set in distinct frames! "How do we give it to you?\n"), 45, - &mem_fragment.base, - &mem_payload.base)); + mem)); TEST_ASSERT_EQUAL(0, mem_payload.allocated_fragments); // This is outrageous. Of course we have to drop everything. TEST_ASSERT_EQUAL(0, mem_fragment.allocated_fragments); // Ensure the slot has been restarted correctly. @@ -983,8 +968,7 @@ static void testSlotAcceptA(void) "Toss it over." "K(\xBB\xEE"), 45, - &mem_fragment.base, - &mem_payload.base)); + mem)); TEST_ASSERT_EQUAL(1, mem_payload.allocated_fragments); // Okay, accepted, some data stored... TEST_ASSERT_EQUAL(1, mem_fragment.allocated_fragments); TEST_ASSERT_EQUAL(0, @@ -996,8 +980,7 @@ static void testSlotAcceptA(void) false, "How do we give it to you?\n"), 45, - &mem_fragment.base, - &mem_payload.base)); + mem)); TEST_ASSERT_EQUAL(0, mem_payload.allocated_fragments); // This is outrageous. Of course we have to drop everything. TEST_ASSERT_EQUAL(0, mem_fragment.allocated_fragments); // Ensure the slot has been restarted correctly. @@ -1017,7 +1000,7 @@ static void testIfaceIsFutureTransferID(void) instrumentedAllocatorNew(&mem_fragment); instrumentedAllocatorNew(&mem_payload); RxIface iface; - rxIfaceInit(&iface, &mem_fragment.base, &mem_payload.base); + rxIfaceInit(&iface, makeRxMemory(&mem_fragment, &mem_payload)); TEST_ASSERT_EQUAL(TIMESTAMP_UNSET, iface.ts_usec); for (size_t i = 0; i < RX_SLOT_COUNT; i++) { @@ -1049,7 +1032,7 @@ static void testIfaceCheckTransferIDTimeout(void) instrumentedAllocatorNew(&mem_fragment); instrumentedAllocatorNew(&mem_payload); RxIface iface; - rxIfaceInit(&iface, &mem_fragment.base, &mem_payload.base); + rxIfaceInit(&iface, makeRxMemory(&mem_fragment, &mem_payload)); TEST_ASSERT_EQUAL(TIMESTAMP_UNSET, iface.ts_usec); for (size_t i = 0; i < RX_SLOT_COUNT; i++) { @@ -1114,8 +1097,8 @@ static void testIfaceFindMatchingSlot(void) instrumentedAllocatorNew(&mem_fragment); instrumentedAllocatorNew(&mem_payload); RxSlot slots[RX_SLOT_COUNT] = {0}; - rxSlotRestart(&slots[0], 1000, &mem_fragment.base, &mem_payload.base); - rxSlotRestart(&slots[1], 1001, &mem_fragment.base, &mem_payload.base); + rxSlotRestart(&slots[0], 1000, makeRxMemory(&mem_fragment, &mem_payload)); + rxSlotRestart(&slots[1], 1001, makeRxMemory(&mem_fragment, &mem_payload)); // No matching slot. TEST_ASSERT_NULL(rxIfaceFindMatchingSlot(slots, 123)); // Matching slots. @@ -1151,8 +1134,9 @@ static void testIfaceAcceptA(void) InstrumentedAllocator mem_payload = {0}; instrumentedAllocatorNew(&mem_fragment); instrumentedAllocatorNew(&mem_payload); - RxIface iface; - rxIfaceInit(&iface, &mem_fragment.base, &mem_payload.base); + const RxMemory mem = makeRxMemory(&mem_fragment, &mem_payload); + RxIface iface; + rxIfaceInit(&iface, mem); TEST_ASSERT_EQUAL(TIMESTAMP_UNSET, iface.ts_usec); for (size_t i = 0; i < RX_SLOT_COUNT; i++) { @@ -1181,11 +1165,10 @@ static void testIfaceAcceptA(void) true, "I am a tomb." "\x1F\\\xCDs"), - &transfer, 1000, UDPARD_DEFAULT_TRANSFER_ID_TIMEOUT_USEC, - &mem_fragment.base, - &mem_payload.base)); + mem, + &transfer)); TEST_ASSERT_EQUAL(1, mem_payload.allocated_fragments); TEST_ASSERT_EQUAL(0, mem_fragment.allocated_fragments); // Head fragment is not heap-allocated. // Check the transfer we just accepted. @@ -1218,11 +1201,10 @@ static void testIfaceAcceptA(void) true, "I am a tomb." "\x1F\\\xCDs"), - &transfer, 1000, UDPARD_DEFAULT_TRANSFER_ID_TIMEOUT_USEC, - &mem_fragment.base, - &mem_payload.base)); + mem, + &transfer)); TEST_ASSERT_EQUAL(0, mem_payload.allocated_fragments); TEST_ASSERT_EQUAL(0, mem_fragment.allocated_fragments); // Check the internal states of the iface. @@ -1245,11 +1227,10 @@ static void testIfaceAcceptA(void) true, "I am a tomb." "No CRC here."), - &transfer, 1000, UDPARD_DEFAULT_TRANSFER_ID_TIMEOUT_USEC, - &mem_fragment.base, - &mem_payload.base)); + mem, + &transfer)); TEST_ASSERT_EQUAL(0, mem_payload.allocated_fragments); TEST_ASSERT_EQUAL(0, mem_fragment.allocated_fragments); // Check the internal states of the iface. @@ -1273,11 +1254,10 @@ static void testIfaceAcceptA(void) true, "I am a tomb." "No CRC here, #2."), - &transfer, 1000, UDPARD_DEFAULT_TRANSFER_ID_TIMEOUT_USEC, - &mem_fragment.base, - &mem_payload.base)); + mem, + &transfer)); TEST_ASSERT_EQUAL(0, mem_payload.allocated_fragments); TEST_ASSERT_EQUAL(0, mem_fragment.allocated_fragments); // Check the internal states of the iface. @@ -1304,11 +1284,10 @@ static void testIfaceAcceptA(void) true, "A2" "v\x1E\xBD]"), - &transfer, 1000, UDPARD_DEFAULT_TRANSFER_ID_TIMEOUT_USEC, - &mem_fragment.base, - &mem_payload.base)); + mem, + &transfer)); TEST_ASSERT_EQUAL(1234567890, iface.ts_usec); // same old timestamp TEST_ASSERT_EQUAL(TRANSFER_ID_UNSET, iface.slots[0].transfer_id); // Still unused. TEST_ASSERT_EQUAL(1000, iface.slots[1].transfer_id); // Replaced the old one, it was unneeded. @@ -1328,11 +1307,10 @@ static void testIfaceAcceptA(void) true, "B1" "g\x8D\x9A\xD7"), - &transfer, 1000, UDPARD_DEFAULT_TRANSFER_ID_TIMEOUT_USEC, - &mem_fragment.base, - &mem_payload.base)); + mem, + &transfer)); TEST_ASSERT_EQUAL(1234567890, iface.ts_usec); // same old timestamp TEST_ASSERT_EQUAL(1001, iface.slots[0].transfer_id); // Used for B because the other one is taken. TEST_ASSERT_EQUAL(1000, iface.slots[1].transfer_id); // Keeps A because it is in-progress, can't discard. @@ -1351,11 +1329,10 @@ static void testIfaceAcceptA(void) 0, false, "A0"), - &transfer, 1000, UDPARD_DEFAULT_TRANSFER_ID_TIMEOUT_USEC, - &mem_fragment.base, - &mem_payload.base)); + mem, + &transfer)); TEST_ASSERT_EQUAL(1234567890, iface.ts_usec); // same old timestamp TEST_ASSERT_EQUAL(1001, iface.slots[0].transfer_id); TEST_ASSERT_EQUAL(1000, iface.slots[1].transfer_id); @@ -1374,11 +1351,10 @@ static void testIfaceAcceptA(void) 0, false, "B0"), - &transfer, 1000, UDPARD_DEFAULT_TRANSFER_ID_TIMEOUT_USEC, - &mem_fragment.base, - &mem_payload.base)); + mem, + &transfer)); // TRANSFER B RECEIVED, check it. TEST_ASSERT_EQUAL(2000000010, iface.ts_usec); TEST_ASSERT_EQUAL(1002, iface.slots[0].transfer_id); // Incremented to meet the next transfer. @@ -1411,11 +1387,10 @@ static void testIfaceAcceptA(void) 1, false, "A1"), - &transfer, 1000, UDPARD_DEFAULT_TRANSFER_ID_TIMEOUT_USEC, - &mem_fragment.base, - &mem_payload.base)); + mem, + &transfer)); // TRANSFER A RECEIVED, check it. TEST_ASSERT_EQUAL(2000000020, iface.ts_usec); // same old timestamp TEST_ASSERT_EQUAL(1002, iface.slots[0].transfer_id); @@ -1443,8 +1418,9 @@ static void testIfaceAcceptB(void) InstrumentedAllocator mem_payload = {0}; instrumentedAllocatorNew(&mem_fragment); instrumentedAllocatorNew(&mem_payload); - RxIface iface; - rxIfaceInit(&iface, &mem_fragment.base, &mem_payload.base); + const RxMemory mem = makeRxMemory(&mem_fragment, &mem_payload); + RxIface iface; + rxIfaceInit(&iface, mem); TEST_ASSERT_EQUAL(TIMESTAMP_UNSET, iface.ts_usec); for (size_t i = 0; i < RX_SLOT_COUNT; i++) { @@ -1478,11 +1454,10 @@ static void testIfaceAcceptB(void) true, "A2" "v\x1E\xBD]"), - &transfer, 1000, UDPARD_DEFAULT_TRANSFER_ID_TIMEOUT_USEC, - &mem_fragment.base, - &mem_payload.base)); + mem, + &transfer)); TEST_ASSERT_EQUAL(TIMESTAMP_UNSET, iface.ts_usec); TEST_ASSERT_EQUAL(TRANSFER_ID_UNSET, iface.slots[0].transfer_id); TEST_ASSERT_EQUAL(1000, iface.slots[1].transfer_id); @@ -1502,11 +1477,10 @@ static void testIfaceAcceptB(void) true, "B1" "g\x8D\x9A\xD7"), - &transfer, 1000, UDPARD_DEFAULT_TRANSFER_ID_TIMEOUT_USEC, - &mem_fragment.base, - &mem_payload.base)); + mem, + &transfer)); TEST_ASSERT_EQUAL(TIMESTAMP_UNSET, iface.ts_usec); TEST_ASSERT_EQUAL(1001, iface.slots[0].transfer_id); TEST_ASSERT_EQUAL(1000, iface.slots[1].transfer_id); @@ -1525,11 +1499,10 @@ static void testIfaceAcceptB(void) 0, false, "A0"), - &transfer, 1000, UDPARD_DEFAULT_TRANSFER_ID_TIMEOUT_USEC, - &mem_fragment.base, - &mem_payload.base)); + mem, + &transfer)); TEST_ASSERT_EQUAL(TIMESTAMP_UNSET, iface.ts_usec); TEST_ASSERT_EQUAL(1001, iface.slots[0].transfer_id); TEST_ASSERT_EQUAL(1000, iface.slots[1].transfer_id); @@ -1548,11 +1521,10 @@ static void testIfaceAcceptB(void) 0, false, "C0"), - &transfer, 1000, UDPARD_DEFAULT_TRANSFER_ID_TIMEOUT_USEC, - &mem_fragment.base, - &mem_payload.base)); + mem, + &transfer)); TEST_ASSERT_EQUAL(TIMESTAMP_UNSET, iface.ts_usec); TEST_ASSERT_EQUAL(1002, iface.slots[0].transfer_id); // B evicted by C. TEST_ASSERT_EQUAL(1000, iface.slots[1].transfer_id); @@ -1571,11 +1543,10 @@ static void testIfaceAcceptB(void) 0, false, "B0"), - &transfer, 1000, UDPARD_DEFAULT_TRANSFER_ID_TIMEOUT_USEC, - &mem_fragment.base, - &mem_payload.base)); + mem, + &transfer)); TEST_ASSERT_EQUAL(TIMESTAMP_UNSET, iface.ts_usec); TEST_ASSERT_EQUAL(1002, iface.slots[0].transfer_id); TEST_ASSERT_EQUAL(1000, iface.slots[1].transfer_id); @@ -1594,11 +1565,10 @@ static void testIfaceAcceptB(void) 1, false, "A1"), - &transfer, 1000, UDPARD_DEFAULT_TRANSFER_ID_TIMEOUT_USEC, - &mem_fragment.base, - &mem_payload.base)); + mem, + &transfer)); // TRANSFER A RECEIVED, check it. TEST_ASSERT_EQUAL(2000000020, iface.ts_usec); // same old timestamp TEST_ASSERT_EQUAL(1002, iface.slots[0].transfer_id); @@ -1631,11 +1601,10 @@ static void testIfaceAcceptB(void) 0, false, "C0 DUPLICATE"), - &transfer, 1000, UDPARD_DEFAULT_TRANSFER_ID_TIMEOUT_USEC, - &mem_fragment.base, - &mem_payload.base)); + mem, + &transfer)); TEST_ASSERT_EQUAL(2000000020, iface.ts_usec); // Last transfer timestamp. TEST_ASSERT_EQUAL(1002, iface.slots[0].transfer_id); TEST_ASSERT_EQUAL(1001, iface.slots[1].transfer_id); @@ -1655,11 +1624,10 @@ static void testIfaceAcceptB(void) true, "C1" "\xA8\xBF}\x19"), - &transfer, 1000, UDPARD_DEFAULT_TRANSFER_ID_TIMEOUT_USEC, - &mem_fragment.base, - &mem_payload.base)); + mem, + &transfer)); // TRANSFER C RECEIVED, check it. TEST_ASSERT_EQUAL(2000000040, iface.ts_usec); TEST_ASSERT_EQUAL(1003, iface.slots[0].transfer_id); // Incremented to meet the next transfer. @@ -1687,8 +1655,9 @@ static void testIfaceAcceptC(void) InstrumentedAllocator mem_payload = {0}; instrumentedAllocatorNew(&mem_fragment); instrumentedAllocatorNew(&mem_payload); - RxIface iface; - rxIfaceInit(&iface, &mem_fragment.base, &mem_payload.base); + const RxMemory mem = makeRxMemory(&mem_fragment, &mem_payload); + RxIface iface; + rxIfaceInit(&iface, mem); TEST_ASSERT_EQUAL(TIMESTAMP_UNSET, iface.ts_usec); for (size_t i = 0; i < RX_SLOT_COUNT; i++) { @@ -1722,11 +1691,10 @@ static void testIfaceAcceptC(void) 0, false, "A0"), - &transfer, 1000, UDPARD_DEFAULT_TRANSFER_ID_TIMEOUT_USEC, - &mem_fragment.base, - &mem_payload.base)); + mem, + &transfer)); TEST_ASSERT_EQUAL(TIMESTAMP_UNSET, iface.ts_usec); TEST_ASSERT_EQUAL(TRANSFER_ID_UNSET, iface.slots[0].transfer_id); TEST_ASSERT_EQUAL(0xA, iface.slots[1].transfer_id); @@ -1745,11 +1713,10 @@ static void testIfaceAcceptC(void) 0, false, "B0"), - &transfer, 1000, UDPARD_DEFAULT_TRANSFER_ID_TIMEOUT_USEC, - &mem_fragment.base, - &mem_payload.base)); + mem, + &transfer)); TEST_ASSERT_EQUAL(TIMESTAMP_UNSET, iface.ts_usec); TEST_ASSERT_EQUAL(0xB, iface.slots[0].transfer_id); TEST_ASSERT_EQUAL(0xA, iface.slots[1].transfer_id); @@ -1769,11 +1736,10 @@ static void testIfaceAcceptC(void) true, "A1" "\xc7\xac_\x81"), - &transfer, 1000, UDPARD_DEFAULT_TRANSFER_ID_TIMEOUT_USEC, - &mem_fragment.base, - &mem_payload.base)); + mem, + &transfer)); // Check the received transfer. TEST_ASSERT_EQUAL(2000000010, iface.ts_usec); TEST_ASSERT_EQUAL(0xB, iface.slots[0].transfer_id); @@ -1802,11 +1768,10 @@ static void testIfaceAcceptC(void) 0, false, "C0"), - &transfer, 1000, UDPARD_DEFAULT_TRANSFER_ID_TIMEOUT_USEC, - &mem_fragment.base, - &mem_payload.base)); + mem, + &transfer)); TEST_ASSERT_EQUAL(2000000010, iface.ts_usec); // <- unchanged. TEST_ASSERT_EQUAL(0xB, iface.slots[0].transfer_id); TEST_ASSERT_EQUAL(0xC, iface.slots[1].transfer_id); // <- reused for C. @@ -1826,11 +1791,10 @@ static void testIfaceAcceptC(void) true, "B1" "g\x8D\x9A\xD7"), - &transfer, 1000, UDPARD_DEFAULT_TRANSFER_ID_TIMEOUT_USEC, - &mem_fragment.base, - &mem_payload.base)); + mem, + &transfer)); // Check the received transfer. TEST_ASSERT_EQUAL(2000000020, iface.ts_usec); TEST_ASSERT_EQUAL(0xC, iface.slots[0].transfer_id); // <-- INCREMENTED, SO @@ -1862,11 +1826,10 @@ static void testIfaceAcceptC(void) true, "C1" "\xA8\xBF}\x19"), - &transfer, 1000, UDPARD_DEFAULT_TRANSFER_ID_TIMEOUT_USEC, - &mem_fragment.base, - &mem_payload.base)); + mem, + &transfer)); // Check the received transfer. TEST_ASSERT_EQUAL(2000000040, iface.ts_usec); TEST_ASSERT_EQUAL(0xC, iface.slots[0].transfer_id); // Old, unused. @@ -1896,11 +1859,10 @@ static void testIfaceAcceptC(void) 0, false, "B0"), - &transfer, 1000, UDPARD_DEFAULT_TRANSFER_ID_TIMEOUT_USEC, - &mem_fragment.base, - &mem_payload.base)); + mem, + &transfer)); TEST_ASSERT_EQUAL(0, mem_payload.allocated_fragments); TEST_ASSERT_EQUAL(0, mem_fragment.allocated_fragments); // B0 duplicate single-frame; shall be rejected. @@ -1917,15 +1879,28 @@ static void testIfaceAcceptC(void) true, "B0" "g\x8D\x9A\xD7"), - &transfer, 1000, UDPARD_DEFAULT_TRANSFER_ID_TIMEOUT_USEC, - &mem_fragment.base, - &mem_payload.base)); + mem, + &transfer)); TEST_ASSERT_EQUAL(0, mem_payload.allocated_fragments); TEST_ASSERT_EQUAL(0, mem_fragment.allocated_fragments); } +static void testSessionAcceptA(void) +{ + InstrumentedAllocator mem_fragment = {0}; + InstrumentedAllocator mem_payload = {0}; + instrumentedAllocatorNew(&mem_fragment); + instrumentedAllocatorNew(&mem_payload); + const RxMemory mem = makeRxMemory(&mem_fragment, &mem_payload); + UdpardInternalRxSession session = {0}; + rxSessionInit(&session, mem); + TEST_ASSERT_EQUAL(TIMESTAMP_UNSET, session.last_ts_usec); + TEST_ASSERT_EQUAL(TRANSFER_ID_UNSET, session.last_transfer_id); + // TODO FIXME add tests +} + void setUp(void) {} void tearDown(void) {} @@ -1959,6 +1934,8 @@ int main(void) RUN_TEST(testIfaceAcceptA); RUN_TEST(testIfaceAcceptB); RUN_TEST(testIfaceAcceptC); + // session + RUN_TEST(testSessionAcceptA); return UNITY_END(); } From 8e29b916b4d319e6ce398f1aa7596636f94601e2 Mon Sep 17 00:00:00 2001 From: Pavel Kirienko Date: Mon, 31 Jul 2023 16:20:11 +0300 Subject: [PATCH 16/38] Add test for rxSessionDeduplicate --- libudpard/udpard.c | 46 ++++--- libudpard/udpard.h | 4 +- tests/src/test_intrusive_rx.c | 219 +++++++++++++++++++++------------- 3 files changed, 170 insertions(+), 99 deletions(-) diff --git a/libudpard/udpard.c b/libudpard/udpard.c index d879c08..4ec6117 100644 --- a/libudpard/udpard.c +++ b/libudpard/udpard.c @@ -1301,6 +1301,35 @@ static inline void rxIfaceInit(RxIface* const self, const RxMemory memory) } } +/// Checks if the given transfer should be accepted. If not, the transfer is freed. +/// Internal states are updated. +static inline bool rxSessionDeduplicate(UdpardInternalRxSession* const self, + const UdpardMicrosecond transfer_id_timeout_usec, + struct UdpardRxTransfer* const transfer, + const RxMemory memory) +{ + UDPARD_ASSERT((self != NULL) && (transfer != NULL)); + const bool future_tid = (self->last_transfer_id == TRANSFER_ID_UNSET) || // + (transfer->transfer_id > self->last_transfer_id); + const bool tid_timeout = (self->last_ts_usec == TIMESTAMP_UNSET) || + ((transfer->timestamp_usec >= self->last_ts_usec) && + ((transfer->timestamp_usec - self->last_ts_usec) >= transfer_id_timeout_usec)); + const bool accept = future_tid || tid_timeout; + if (accept) + { + self->last_ts_usec = transfer->timestamp_usec; + self->last_transfer_id = transfer->transfer_id; + } + else // This is a duplicate: received from another interface, a FEC retransmission, or a network glitch. + { + memFreePayload(memory.payload, transfer->payload.origin); + rxFragmentFree(transfer->payload.next, memory); + transfer->payload_size = 0; + transfer->payload = (struct UdpardFragment){.next = NULL, .view = {0}, .origin = {0}}; + } + return accept; +} + static inline int_fast8_t rxSessionAccept(UdpardInternalRxSession* const self, const uint_fast8_t redundant_iface_index, const UdpardMicrosecond ts_usec, @@ -1322,22 +1351,7 @@ static inline int_fast8_t rxSessionAccept(UdpardInternalRxSession* const self, UDPARD_ASSERT(result <= 1); if (result > 0) { - const bool future_tid = (self->last_transfer_id == TRANSFER_ID_UNSET) || // - (out_transfer->transfer_id > self->last_transfer_id); - const bool tid_timeout = (self->last_ts_usec == TIMESTAMP_UNSET) || - ((out_transfer->timestamp_usec >= self->last_ts_usec) && - ((out_transfer->timestamp_usec - self->last_ts_usec) >= transfer_id_timeout_usec)); - if (future_tid || tid_timeout) // Notice that the deduplicator makes no distinction between the interfaces. - { - self->last_ts_usec = out_transfer->timestamp_usec; - self->last_transfer_id = out_transfer->transfer_id; - } - else // This is a duplicate: received from another interface, a FEC retransmission, or a network glitch. - { - result = 0; - memFreePayload(memory.payload, out_transfer->payload.origin); - rxFragmentFree(out_transfer->payload.next, memory); - } + result = rxSessionDeduplicate(self, transfer_id_timeout_usec, out_transfer, memory) ? 1 : 0; } return result; } diff --git a/libudpard/udpard.h b/libudpard/udpard.h index 3677bcf..2430aaf 100644 --- a/libudpard/udpard.h +++ b/libudpard/udpard.h @@ -237,10 +237,10 @@ extern "C" { /// The library supports at most this many redundant network interfaces per Cyphal node. #define UDPARD_NETWORK_INTERFACE_COUNT_MAX 3U -typedef uint64_t UdpardMicrosecond; +typedef uint64_t UdpardMicrosecond; ///< UINT64_MAX is not a valid timestamp value. typedef uint16_t UdpardPortID; typedef uint16_t UdpardNodeID; -typedef uint64_t UdpardTransferID; +typedef uint64_t UdpardTransferID; ///< UINT64_MAX is not a valid transfer-ID value. /// Transfer priority level mnemonics per the recommendations given in the Cyphal Specification. /// For outgoing transfers they are mapped to DSCP values as configured per redundant interface (per UdpardTx instance). diff --git a/tests/src/test_intrusive_rx.c b/tests/src/test_intrusive_rx.c index 39b75a6..092f661 100644 --- a/tests/src/test_intrusive_rx.c +++ b/tests/src/test_intrusive_rx.c @@ -11,17 +11,16 @@ /// Moves the payload from the origin into a new buffer and attaches is to the newly allocated fragment. /// This function performs two allocations. This function is infallible. -static RxFragment* makeRxFragment(struct UdpardMemoryResource* const memory_fragment, - struct UdpardMemoryResource* const memory_payload, - const uint32_t frame_index, - const struct UdpardPayload view, - const struct UdpardMutablePayload origin, - RxFragmentTreeNode* const parent) +static RxFragment* makeRxFragment(const RxMemory memory, + const uint32_t frame_index, + const struct UdpardPayload view, + const struct UdpardMutablePayload origin, + RxFragmentTreeNode* const parent) { TEST_PANIC_UNLESS((view.data >= origin.data) && (view.size <= origin.size)); TEST_PANIC_UNLESS((((const byte_t*) view.data) + view.size) <= (((const byte_t*) origin.data) + origin.size)); - byte_t* const new_origin = (byte_t*) memAlloc(memory_payload, origin.size); - RxFragment* const frag = (RxFragment*) memAlloc(memory_fragment, sizeof(RxFragment)); + byte_t* const new_origin = (byte_t*) memAlloc(memory.payload, origin.size); + RxFragment* const frag = (RxFragment*) memAlloc(memory.fragment, sizeof(RxFragment)); if ((new_origin != NULL) && (frag != NULL)) { (void) memmove(new_origin, origin.data, origin.size); @@ -31,7 +30,6 @@ static RxFragment* makeRxFragment(struct UdpardMemoryResource* const memory_frag frag->tree.base.up = &parent->base; frag->tree.this = frag; frag->frame_index = frame_index; - frag->base.view = view; frag->base.origin.data = new_origin; frag->base.origin.size = origin.size; frag->base.view.data = new_origin + (((const byte_t*) view.data) - ((byte_t*) origin.data)); @@ -45,15 +43,13 @@ static RxFragment* makeRxFragment(struct UdpardMemoryResource* const memory_frag } /// This is a simple helper wrapper that constructs a new fragment using a null-terminated string as a payload. -static RxFragment* makeRxFragmentString(struct UdpardMemoryResource* const memory_fragment, - struct UdpardMemoryResource* const memory_payload, - const uint32_t frame_index, - const char* const payload, - RxFragmentTreeNode* const parent) +static RxFragment* makeRxFragmentString(const RxMemory memory, + const uint32_t frame_index, + const char* const payload, + RxFragmentTreeNode* const parent) { const size_t sz = strlen(payload); - return makeRxFragment(memory_fragment, - memory_payload, + return makeRxFragment(memory, frame_index, (struct UdpardPayload){.data = payload, .size = sz}, (struct UdpardMutablePayload){.data = (void*) payload, .size = sz}, @@ -291,7 +287,8 @@ static void testSlotRestartNonEmpty(void) InstrumentedAllocator mem_payload = {0}; instrumentedAllocatorNew(&mem_fragment); instrumentedAllocatorNew(&mem_payload); - byte_t data[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; + const RxMemory mem = makeRxMemory(&mem_fragment, &mem_payload); + byte_t data[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; // RxSlot slot = { .ts_usec = 1234567890, @@ -301,23 +298,20 @@ static void testSlotRestartNonEmpty(void) .accepted_frames = 555, .payload_size = 987, // - .fragments = &makeRxFragment(&mem_fragment.base, - &mem_payload.base, + .fragments = &makeRxFragment(mem, 1, (struct UdpardPayload){.data = &data[2], .size = 2}, (struct UdpardMutablePayload){.data = data, .size = sizeof(data)}, NULL) ->tree, }; - slot.fragments->base.lr[0] = &makeRxFragment(&mem_fragment.base, - &mem_payload.base, + slot.fragments->base.lr[0] = &makeRxFragment(mem, 0, (struct UdpardPayload){.data = &data[1], .size = 1}, (struct UdpardMutablePayload){.data = data, .size = sizeof(data)}, slot.fragments) ->tree.base; - slot.fragments->base.lr[1] = &makeRxFragment(&mem_fragment.base, - &mem_payload.base, + slot.fragments->base.lr[1] = &makeRxFragment(mem, 2, (struct UdpardPayload){.data = &data[3], .size = 3}, (struct UdpardMutablePayload){.data = data, .size = sizeof(data)}, @@ -350,6 +344,7 @@ static void testSlotEjectValidLarge(void) InstrumentedAllocator mem_payload = {0}; instrumentedAllocatorNew(&mem_fragment); instrumentedAllocatorNew(&mem_payload); + const RxMemory mem = makeRxMemory(&mem_fragment, &mem_payload); //>>> from pycyphal.transport.commons.crc import CRC32C //>>> CRC32C.new(data_bytes).value_as_bytes static const size_t PayloadSize = 171; @@ -359,29 +354,14 @@ static void testSlotEjectValidLarge(void) // 1 3 // / // 0 - RxFragment* const root = // - makeRxFragmentString(&mem_fragment.base, // - &mem_payload.base, - 2, - "Where does Man go? ", - NULL); - root->tree.base.lr[0] = // - &makeRxFragmentString(&mem_fragment.base, // - &mem_payload.base, - 1, - "For example, where does Man come from? ", - &root->tree) - ->tree.base; - root->tree.base.lr[1] = // - &makeRxFragmentString(&mem_fragment.base, // - &mem_payload.base, - 3, - "Where does the universe come from? xL\xAE\xCB", - &root->tree) - ->tree.base; + RxFragment* const root = // + makeRxFragmentString(mem, 2, "Where does Man go? ", NULL); + root->tree.base.lr[0] = // + &makeRxFragmentString(mem, 1, "For example, where does Man come from? ", &root->tree)->tree.base; + root->tree.base.lr[1] = // + &makeRxFragmentString(mem, 3, "Where does the universe come from? xL\xAE\xCB", &root->tree)->tree.base; root->tree.base.lr[0]->lr[0] = - &makeRxFragmentString(&mem_fragment.base, // - &mem_payload.base, + &makeRxFragmentString(mem, // 0, "Da Shi, have you ever... considered certain ultimate philosophical questions? ", ((RxFragmentTreeNode*) root->tree.base.lr[0])) @@ -429,6 +409,7 @@ static void testSlotEjectValidSmall(void) InstrumentedAllocator mem_payload = {0}; instrumentedAllocatorNew(&mem_fragment); instrumentedAllocatorNew(&mem_payload); + const RxMemory mem = makeRxMemory(&mem_fragment, &mem_payload); //>>> from pycyphal.transport.commons.crc import CRC32C //>>> CRC32C.new(data_bytes).value_as_bytes static const size_t PayloadSize = 262; @@ -438,36 +419,20 @@ static void testSlotEjectValidSmall(void) // 0 3 // / ` // 2 4 - RxFragment* const root = // - makeRxFragmentString(&mem_fragment.base, // - &mem_payload.base, - 1, - "You told me that you came from the sea. Did you build the sea?\n", - NULL); - root->tree.base.lr[0] = // - &makeRxFragmentString(&mem_fragment.base, // - &mem_payload.base, - 0, - "Did you build this four-dimensional fragment?\n", - &root->tree) - ->tree.base; - root->tree.base.lr[1] = // - &makeRxFragmentString(&mem_fragment.base, // - &mem_payload.base, - 3, - "this four-dimensional space is like the sea for us?\n", - &root->tree) - ->tree.base; - root->tree.base.lr[1]->lr[0] = // - &makeRxFragmentString(&mem_fragment.base, // - &mem_payload.base, + RxFragment* const root = // + makeRxFragmentString(mem, 1, "You told me that you came from the sea. Did you build the sea?\n", NULL); + root->tree.base.lr[0] = // + &makeRxFragmentString(mem, 0, "Did you build this four-dimensional fragment?\n", &root->tree)->tree.base; + root->tree.base.lr[1] = // + &makeRxFragmentString(mem, 3, "this four-dimensional space is like the sea for us?\n", &root->tree)->tree.base; + root->tree.base.lr[1]->lr[0] = // + &makeRxFragmentString(mem, 2, "Are you saying that for you, or at least for your creators, ", ((RxFragmentTreeNode*) root->tree.base.lr[1])) ->tree.base; - root->tree.base.lr[1]->lr[1] = // - &makeRxFragmentString(&mem_fragment.base, // - &mem_payload.base, + root->tree.base.lr[1]->lr[1] = // + &makeRxFragmentString(mem, 4, "More like a puddle. The sea has gone dry.\xA2\x93-\xB2", ((RxFragmentTreeNode*) root->tree.base.lr[1])) @@ -514,15 +479,14 @@ static void testSlotEjectValidEmpty(void) InstrumentedAllocator mem_payload = {0}; instrumentedAllocatorNew(&mem_fragment); instrumentedAllocatorNew(&mem_payload); + const RxMemory mem = makeRxMemory(&mem_fragment, &mem_payload); // Build the fragment tree: // 1 // / ` // 0 2 - RxFragment* const root = makeRxFragmentString(&mem_fragment.base, &mem_payload.base, 1, "BBB", NULL); - root->tree.base.lr[0] = - &makeRxFragmentString(&mem_fragment.base, &mem_payload.base, 0, "AAA", &root->tree)->tree.base; - root->tree.base.lr[1] = - &makeRxFragmentString(&mem_fragment.base, &mem_payload.base, 2, "P\xF5\xA5?", &root->tree)->tree.base; + RxFragment* const root = makeRxFragmentString(mem, 1, "BBB", NULL); + root->tree.base.lr[0] = &makeRxFragmentString(mem, 0, "AAA", &root->tree)->tree.base; + root->tree.base.lr[1] = &makeRxFragmentString(mem, 2, "P\xF5\xA5?", &root->tree)->tree.base; // Initialization done, ensure the memory utilization is as we expect. TEST_ASSERT_EQUAL(3, mem_payload.allocated_fragments); TEST_ASSERT_EQUAL(6 + TRANSFER_CRC_SIZE_BYTES, mem_payload.allocated_bytes); @@ -560,15 +524,14 @@ static void testSlotEjectInvalid(void) InstrumentedAllocator mem_payload = {0}; instrumentedAllocatorNew(&mem_fragment); instrumentedAllocatorNew(&mem_payload); + const RxMemory mem = makeRxMemory(&mem_fragment, &mem_payload); // Build the fragment tree; no valid CRC here: // 1 // / ` // 0 2 - RxFragment* const root = makeRxFragmentString(&mem_fragment.base, &mem_payload.base, 1, "BBB", NULL); - root->tree.base.lr[0] = - &makeRxFragmentString(&mem_fragment.base, &mem_payload.base, 0, "AAA", &root->tree)->tree.base; - root->tree.base.lr[1] = - &makeRxFragmentString(&mem_fragment.base, &mem_payload.base, 2, "CCC", &root->tree)->tree.base; + RxFragment* const root = makeRxFragmentString(mem, 1, "BBB", NULL); + root->tree.base.lr[0] = &makeRxFragmentString(mem, 0, "AAA", &root->tree)->tree.base; + root->tree.base.lr[1] = &makeRxFragmentString(mem, 2, "CCC", &root->tree)->tree.base; // Initialization done, ensure the memory utilization is as we expect. TEST_ASSERT_EQUAL(3, mem_payload.allocated_fragments); TEST_ASSERT_EQUAL(9, mem_payload.allocated_bytes); @@ -1887,6 +1850,99 @@ static void testIfaceAcceptC(void) TEST_ASSERT_EQUAL(0, mem_fragment.allocated_fragments); } +static void testSessionDeduplicate(void) +{ + InstrumentedAllocator mem_fragment = {0}; + InstrumentedAllocator mem_payload = {0}; + instrumentedAllocatorNew(&mem_fragment); + instrumentedAllocatorNew(&mem_payload); + const RxMemory mem = makeRxMemory(&mem_fragment, &mem_payload); + UdpardInternalRxSession session = {0}; + rxSessionInit(&session, mem); + TEST_ASSERT_EQUAL(TIMESTAMP_UNSET, session.last_ts_usec); + TEST_ASSERT_EQUAL(TRANSFER_ID_UNSET, session.last_transfer_id); + { + struct UdpardFragment* const head = &makeRxFragmentString(mem, 0, "ABC", NULL)->base; + head->next = &makeRxFragmentString(mem, 1, "DEF", NULL)->base; + struct UdpardRxTransfer transfer = {.timestamp_usec = 10000000, + .transfer_id = 0x0DDC0FFEEBADF00D, + .payload_size = 6, + .payload = *head}; + memFree(&mem_fragment.base, sizeof(RxFragment), head); // Cloned, no longer needed. + TEST_ASSERT_EQUAL(2, mem_payload.allocated_fragments); + TEST_ASSERT_EQUAL(1, mem_fragment.allocated_fragments); + // The first transfer after initialization is always accepted. + TEST_ASSERT(rxSessionDeduplicate(&session, UDPARD_DEFAULT_TRANSFER_ID_TIMEOUT_USEC, &transfer, mem)); + // Check the final states. + TEST_ASSERT_EQUAL(6, transfer.payload_size); + TEST_ASSERT_EQUAL(2, mem_payload.allocated_fragments); // The application shall free the payload. + TEST_ASSERT_EQUAL(1, mem_fragment.allocated_fragments); + TEST_ASSERT_EQUAL(10000000, session.last_ts_usec); + TEST_ASSERT_EQUAL(0x0DDC0FFEEBADF00D, session.last_transfer_id); + // Feed the same transfer again; now it is a duplicate and so it is rejected and freed. + transfer.timestamp_usec = 10000001; + TEST_ASSERT_FALSE(rxSessionDeduplicate(&session, UDPARD_DEFAULT_TRANSFER_ID_TIMEOUT_USEC, &transfer, mem)); + TEST_ASSERT_EQUAL(0, mem_payload.allocated_fragments); + TEST_ASSERT_EQUAL(0, mem_fragment.allocated_fragments); + TEST_ASSERT_EQUAL(10000000, session.last_ts_usec); // Timestamp is not updated. + TEST_ASSERT_EQUAL(0x0DDC0FFEEBADF00D, session.last_transfer_id); + } + { + // Emit a duplicate but after the transfer-ID timeout has occurred. Ensure it is accepted. + struct UdpardFragment* const head = &makeRxFragmentString(mem, 0, "ABC", NULL)->base; + head->next = &makeRxFragmentString(mem, 1, "DEF", NULL)->base; + struct UdpardRxTransfer transfer = {.timestamp_usec = 12000000, // TID timeout. + .transfer_id = 0x0DDC0FFEEBADF000, // transfer-ID reduced. + .payload_size = 6, + .payload = *head}; + memFree(&mem_fragment.base, sizeof(RxFragment), head); // Cloned, no longer needed. + TEST_ASSERT_EQUAL(2, mem_payload.allocated_fragments); + TEST_ASSERT_EQUAL(1, mem_fragment.allocated_fragments); + // Accepted due to the TID timeout. + TEST_ASSERT(rxSessionDeduplicate(&session, UDPARD_DEFAULT_TRANSFER_ID_TIMEOUT_USEC, &transfer, mem)); + // Check the final states. + TEST_ASSERT_EQUAL(6, transfer.payload_size); + TEST_ASSERT_EQUAL(2, mem_payload.allocated_fragments); // The application shall free the payload. + TEST_ASSERT_EQUAL(1, mem_fragment.allocated_fragments); + TEST_ASSERT_EQUAL(12000000, session.last_ts_usec); + TEST_ASSERT_EQUAL(0x0DDC0FFEEBADF000, session.last_transfer_id); + // Feed the same transfer again; now it is a duplicate and so it is rejected and freed. + transfer.timestamp_usec = 12000001; + TEST_ASSERT_FALSE(rxSessionDeduplicate(&session, UDPARD_DEFAULT_TRANSFER_ID_TIMEOUT_USEC, &transfer, mem)); + TEST_ASSERT_EQUAL(0, mem_payload.allocated_fragments); + TEST_ASSERT_EQUAL(0, mem_fragment.allocated_fragments); + TEST_ASSERT_EQUAL(12000000, session.last_ts_usec); // Timestamp is not updated. + TEST_ASSERT_EQUAL(0x0DDC0FFEEBADF000, session.last_transfer_id); + } + { + // Ensure another transfer with a greater transfer-ID is accepted immediately. + struct UdpardFragment* const head = &makeRxFragmentString(mem, 0, "ABC", NULL)->base; + head->next = &makeRxFragmentString(mem, 1, "DEF", NULL)->base; + struct UdpardRxTransfer transfer = {.timestamp_usec = 11000000, // Simulate clock jitter. + .transfer_id = 0x0DDC0FFEEBADF001, // Incremented. + .payload_size = 6, + .payload = *head}; + memFree(&mem_fragment.base, sizeof(RxFragment), head); // Cloned, no longer needed. + TEST_ASSERT_EQUAL(2, mem_payload.allocated_fragments); + TEST_ASSERT_EQUAL(1, mem_fragment.allocated_fragments); + // Accepted because TID greater. + TEST_ASSERT(rxSessionDeduplicate(&session, UDPARD_DEFAULT_TRANSFER_ID_TIMEOUT_USEC, &transfer, mem)); + // Check the final states. + TEST_ASSERT_EQUAL(6, transfer.payload_size); + TEST_ASSERT_EQUAL(2, mem_payload.allocated_fragments); // The application shall free the payload. + TEST_ASSERT_EQUAL(1, mem_fragment.allocated_fragments); + TEST_ASSERT_EQUAL(11000000, session.last_ts_usec); // Updated. + TEST_ASSERT_EQUAL(0x0DDC0FFEEBADF001, session.last_transfer_id); // Updated. + // Feed the same transfer again; now it is a duplicate and so it is rejected and freed. + transfer.timestamp_usec = 11000000; + TEST_ASSERT_FALSE(rxSessionDeduplicate(&session, UDPARD_DEFAULT_TRANSFER_ID_TIMEOUT_USEC, &transfer, mem)); + TEST_ASSERT_EQUAL(0, mem_payload.allocated_fragments); + TEST_ASSERT_EQUAL(0, mem_fragment.allocated_fragments); + TEST_ASSERT_EQUAL(11000000, session.last_ts_usec); // Timestamp is not updated. + TEST_ASSERT_EQUAL(0x0DDC0FFEEBADF001, session.last_transfer_id); + } +} + static void testSessionAcceptA(void) { InstrumentedAllocator mem_fragment = {0}; @@ -1935,6 +1991,7 @@ int main(void) RUN_TEST(testIfaceAcceptB); RUN_TEST(testIfaceAcceptC); // session + RUN_TEST(testSessionDeduplicate); RUN_TEST(testSessionAcceptA); return UNITY_END(); } From c12c1d4d45e937c779b88a3ba0932d6084a05cfe Mon Sep 17 00:00:00 2001 From: Pavel Kirienko Date: Mon, 31 Jul 2023 16:32:26 +0300 Subject: [PATCH 17/38] Add session acceptance test with deduplication --- tests/src/test_intrusive_rx.c | 67 ++++++++++++++++++++++++++++++++++- 1 file changed, 66 insertions(+), 1 deletion(-) diff --git a/tests/src/test_intrusive_rx.c b/tests/src/test_intrusive_rx.c index 092f661..f300158 100644 --- a/tests/src/test_intrusive_rx.c +++ b/tests/src/test_intrusive_rx.c @@ -1954,7 +1954,72 @@ static void testSessionAcceptA(void) rxSessionInit(&session, mem); TEST_ASSERT_EQUAL(TIMESTAMP_UNSET, session.last_ts_usec); TEST_ASSERT_EQUAL(TRANSFER_ID_UNSET, session.last_transfer_id); - // TODO FIXME add tests + struct UdpardRxTransfer transfer = {0}; + // Accept a simple transfer through iface #1. + TEST_ASSERT_EQUAL(1, + rxSessionAccept(&session, + 1, + 10000000, + makeRxFrameString(&mem_payload.base, // + (TransferMetadata){.priority = UdpardPriorityExceptional, + .src_node_id = 2222, + .dst_node_id = UDPARD_NODE_ID_UNSET, + .data_specifier = 0, + .transfer_id = 0xB}, + 0, + true, + "Z\xBA\xA1\xBAh"), + 1000, + UDPARD_DEFAULT_TRANSFER_ID_TIMEOUT_USEC, + mem, + &transfer)); + TEST_ASSERT_EQUAL(1, mem_payload.allocated_fragments); + TEST_ASSERT_EQUAL(0, mem_fragment.allocated_fragments); + // Free the payload. + udpardFragmentFree(transfer.payload, &mem_fragment.base, &mem_payload.base); + TEST_ASSERT_EQUAL(0, mem_payload.allocated_fragments); + TEST_ASSERT_EQUAL(0, mem_fragment.allocated_fragments); + // Send the same transfer again through a different iface; it is a duplicate and so it is rejected and freed. + TEST_ASSERT_EQUAL(0, + rxSessionAccept(&session, + 0, + 10000010, + makeRxFrameString(&mem_payload.base, // + (TransferMetadata){.priority = UdpardPriorityExceptional, + .src_node_id = 2222, + .dst_node_id = UDPARD_NODE_ID_UNSET, + .data_specifier = 0, + .transfer_id = 0xB}, + 0, + true, + "Z\xBA\xA1\xBAh"), + 1000, + UDPARD_DEFAULT_TRANSFER_ID_TIMEOUT_USEC, + mem, + &transfer)); + TEST_ASSERT_EQUAL(0, mem_payload.allocated_fragments); + TEST_ASSERT_EQUAL(0, mem_fragment.allocated_fragments); + // Send a valid transfer that should be accepted but we inject an OOM error. + mem_fragment.limit_fragments = 0; + TEST_ASSERT_EQUAL(-UDPARD_ERROR_MEMORY, + rxSessionAccept(&session, + 2, + 12000020, + makeRxFrameString(&mem_payload.base, // + (TransferMetadata){.priority = UdpardPriorityExceptional, + .src_node_id = 2222, + .dst_node_id = UDPARD_NODE_ID_UNSET, + .data_specifier = 0, + .transfer_id = 0xC}, + 0, + true, + "Z\xBA\xA1\xBAh"), + 1000, + UDPARD_DEFAULT_TRANSFER_ID_TIMEOUT_USEC, + mem, + &transfer)); + TEST_ASSERT_EQUAL(0, mem_payload.allocated_fragments); + TEST_ASSERT_EQUAL(0, mem_fragment.allocated_fragments); } void setUp(void) {} From b6443d9c48d0dda3e941f7750569a8bca4038713 Mon Sep 17 00:00:00 2001 From: Pavel Kirienko Date: Mon, 31 Jul 2023 16:37:21 +0300 Subject: [PATCH 18/38] Add sections for clarity --- libudpard/udpard.c | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/libudpard/udpard.c b/libudpard/udpard.c index 4ec6117..e11a5e2 100644 --- a/libudpard/udpard.c +++ b/libudpard/udpard.c @@ -822,6 +822,8 @@ static inline void rxFragmentFree(struct UdpardFragment* const head, const RxMem /// - Per interface, there are RX_SLOT_COUNT slots; a slot keeps the state of a transfer in the process of being /// reassembled. /// +/// Port -> Session -> Interface -> Slot. +/// /// Consider the following examples, where A and B denote distinct transfers of three frames each: /// /// A0 A1 A2 B0 B1 B2 -- two transfers without OOO frames; both accepted. @@ -873,6 +875,8 @@ typedef struct UdpardInternalRxSession RxIface ifaces[UDPARD_NETWORK_INTERFACE_COUNT_MAX]; } UdpardInternalRxSession; +// -------------------------------------------------- RX SLOT -------------------------------------------------- + // NOLINTNEXTLINE(misc-no-recursion) static inline void rxSlotFree(RxFragment* const self, const RxMemory memory) { @@ -1152,6 +1156,8 @@ static inline int_fast8_t rxSlotAccept(RxSlot* const self, return result; } +// -------------------------------------------------- RX IFACE -------------------------------------------------- + /// Whether the supplied transfer-ID is greater than all transfer-IDs in the RX slots. /// This indicates that the new transfer is not a duplicate and should be accepted. static inline bool rxIfaceIsFutureTransferID(const RxIface* const self, const UdpardTransferID transfer_id) @@ -1301,6 +1307,8 @@ static inline void rxIfaceInit(RxIface* const self, const RxMemory memory) } } +// -------------------------------------------------- RX SESSION -------------------------------------------------- + /// Checks if the given transfer should be accepted. If not, the transfer is freed. /// Internal states are updated. static inline bool rxSessionDeduplicate(UdpardInternalRxSession* const self, @@ -1369,6 +1377,12 @@ static inline void rxSessionInit(UdpardInternalRxSession* const self, const RxMe } } +// -------------------------------------------------- RX PORT -------------------------------------------------- + +// TODO + +// -------------------------------------------------- RX API -------------------------------------------------- + void udpardFragmentFree(const struct UdpardFragment head, struct UdpardMemoryResource* const memory_fragment, struct UdpardMemoryResource* const memory_payload) From 102bbbeb8efeee25c41e493e7f99cdceba0f0c11 Mon Sep 17 00:00:00 2001 From: Pavel Kirienko Date: Tue, 1 Aug 2023 19:52:01 +0300 Subject: [PATCH 19/38] Add rxPort implementation --- libudpard/udpard.c | 156 +++++++++++++++++++++++++++-- libudpard/udpard.h | 7 +- tests/.idea/dictionaries/pavel.xml | 1 + tests/src/test_intrusive_rx.c | 19 +++- 4 files changed, 167 insertions(+), 16 deletions(-) diff --git a/libudpard/udpard.c b/libudpard/udpard.c index e11a5e2..bd687c0 100644 --- a/libudpard/udpard.c +++ b/libudpard/udpard.c @@ -131,6 +131,12 @@ static inline void memFreePayload(struct UdpardMemoryResource* const memory, con memFree(memory, payload.size, payload.data); } +static inline void memZero(const size_t size, void* const data) +{ + // NOLINTNEXTLINE(clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling) + (void) memset(data, 0, size); +} + // --------------------------------------------- HEADER CRC --------------------------------------------- #define HEADER_CRC_INITIAL 0xFFFFU @@ -515,8 +521,7 @@ int8_t udpardTxInit(struct UdpardTx* const self, if ((NULL != self) && (NULL != local_node_id) && memIsValid(memory)) { ret = 0; - // NOLINTNEXTLINE(clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling) - (void) memset(self, 0, sizeof(*self)); + memZero(sizeof(*self), self); self->local_node_id = local_node_id; self->queue_capacity = queue_capacity; self->mtu = UDPARD_MTU_DEFAULT; @@ -864,6 +869,8 @@ typedef struct typedef struct UdpardInternalRxSession { struct UdpardTreeNode base; + /// The remote node-ID is needed here as this is the ordering/search key. + UdpardNodeID remote_node_id; /// This shared state is used for redundant transfer deduplication. /// Redundancies occur as a result of the use of multiple network interfaces, spurious frame duplication along /// the network path, and trivial forward error correction through duplication (if used by the sender). @@ -943,8 +950,7 @@ static inline struct UdpardTreeNode* rxSlotFragmentFactory(void* const user_refe RxFragment* const frag = memAlloc(ctx->memory_fragment, sizeof(RxFragment)); if (frag != NULL) { - // NOLINTNEXTLINE(clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling) - (void) memset(frag, 0, sizeof(RxFragment)); + memZero(sizeof(RxFragment), frag); out = &frag->tree.base; // this is not an escape bug, we retain the pointer via "this" frag->frame_index = ctx->frame_index; frag->tree.this = frag; // <-- right here, see? @@ -1297,8 +1303,7 @@ static inline int_fast8_t rxIfaceAccept(RxIface* const self, static inline void rxIfaceInit(RxIface* const self, const RxMemory memory) { UDPARD_ASSERT(self != NULL); - // NOLINTNEXTLINE(clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling) - (void) memset(self, 0, sizeof(*self)); + memZero(sizeof(*self), self); self->ts_usec = TIMESTAMP_UNSET; for (uint_fast8_t i = 0; i < RX_SLOT_COUNT; i++) { @@ -1367,8 +1372,8 @@ static inline int_fast8_t rxSessionAccept(UdpardInternalRxSession* const self, static inline void rxSessionInit(UdpardInternalRxSession* const self, const RxMemory memory) { UDPARD_ASSERT(self != NULL); - // NOLINTNEXTLINE(clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling) - (void) memset(self, 0, sizeof(*self)); + memZero(sizeof(*self), self); + self->remote_node_id = UDPARD_NODE_ID_UNSET; self->last_ts_usec = TIMESTAMP_UNSET; self->last_transfer_id = TRANSFER_ID_UNSET; for (uint_fast8_t i = 0; i < UDPARD_NETWORK_INTERFACE_COUNT_MAX; i++) @@ -1379,7 +1384,136 @@ static inline void rxSessionInit(UdpardInternalRxSession* const self, const RxMe // -------------------------------------------------- RX PORT -------------------------------------------------- -// TODO +typedef struct +{ + UdpardNodeID remote_node_id; + struct UdpardRxMemoryResources memory; +} RxPortSessionSearchContext; + +static inline int8_t rxPortSessionSearch(void* const user_reference, const struct UdpardTreeNode* node) +{ + UDPARD_ASSERT((user_reference != NULL) && (node != NULL)); + const RxPortSessionSearchContext* const ctx = (const RxPortSessionSearchContext*) user_reference; + struct UdpardInternalRxSession* const session = (struct UdpardInternalRxSession*) node; + UDPARD_ASSERT((ctx != NULL) && (session != NULL)); + int8_t out = 0; + if (ctx->remote_node_id > session->remote_node_id) + { + out = +1; + } + if (ctx->remote_node_id < session->remote_node_id) + { + out = -1; + } + return out; +} + +static inline struct UdpardTreeNode* rxPortSessionFactory(void* const user_reference) +{ + const RxPortSessionSearchContext* const ctx = (const RxPortSessionSearchContext*) user_reference; + UDPARD_ASSERT((ctx != NULL) && (ctx->remote_node_id <= UDPARD_NODE_ID_MAX)); + struct UdpardTreeNode* out = NULL; + struct UdpardInternalRxSession* const session = + memAlloc(ctx->memory.session, sizeof(struct UdpardInternalRxSession)); + if (session != NULL) + { + rxSessionInit(session, (RxMemory){.payload = ctx->memory.payload, .fragment = ctx->memory.fragment}); + session->remote_node_id = ctx->remote_node_id; + out = &session->base; + } + return out; // OOM handled by the caller +} + +/// Accepts a frame into a port, possibly creating a new session along the way. +static inline int_fast8_t rxPortAccept(struct UdpardRxPort* const self, + const uint_fast8_t redundant_iface_index, + const UdpardMicrosecond ts_usec, + const RxFrame frame, + const struct UdpardRxMemoryResources memory, + struct UdpardRxTransfer* const out_transfer) +{ + UDPARD_ASSERT((self != NULL) && (redundant_iface_index < UDPARD_NETWORK_INTERFACE_COUNT_MAX) && + (out_transfer != NULL)); + int_fast8_t result = 0; + bool release = true; + if (frame.meta.src_node_id <= UDPARD_NODE_ID_MAX) + { + struct UdpardInternalRxSession* const session = (struct UdpardInternalRxSession*) + cavlSearch((struct UdpardTreeNode**) &self->sessions, + &(RxPortSessionSearchContext){.remote_node_id = frame.meta.src_node_id, .memory = memory}, + &rxPortSessionSearch, + &rxPortSessionFactory); + if (session != NULL) + { + UDPARD_ASSERT(session->remote_node_id == frame.meta.src_node_id); + release = false; // The callee takes ownership of the memory. + result = rxSessionAccept(session, + redundant_iface_index, + ts_usec, + frame, + self->extent, + self->transfer_id_timeout_usec, + (RxMemory){.payload = memory.payload, .fragment = memory.fragment}, + out_transfer); + } + else // Failed to allocate a new session. + { + result = -UDPARD_ERROR_MEMORY; + } + } + else // Anonymous transfers are always accepted unconditionally unless invalid. + { + if (transferCRCCompute(frame.base.payload.size, frame.base.payload.data) == + TRANSFER_CRC_RESIDUE_BEFORE_OUTPUT_XOR) + { + result = 1; + release = false; + memZero(sizeof(*out_transfer), out_transfer); + out_transfer->timestamp_usec = ts_usec; + out_transfer->priority = frame.meta.priority; + out_transfer->source_node_id = frame.meta.src_node_id; + out_transfer->transfer_id = frame.meta.transfer_id; + out_transfer->payload_size = frame.base.payload.size; + out_transfer->payload.next = NULL; + out_transfer->payload.view = frame.base.payload; + out_transfer->payload.origin = frame.base.origin; + } + } + if (release) + { + memFreePayload(memory.payload, frame.base.origin); + } + return result; +} + +/// Accepts a raw frame and, if valid, passes it on to rxPortAccept() for further processing. +static inline int_fast8_t rxPortAcceptFrame(struct UdpardRxPort* const self, + const uint_fast8_t redundant_iface_index, + const UdpardMicrosecond ts_usec, + const struct UdpardMutablePayload datagram_payload, + const struct UdpardRxMemoryResources memory, + struct UdpardRxTransfer* const out_transfer) +{ + int_fast8_t result = 0; + RxFrame frame = {0}; + if (rxParseFrame(datagram_payload, &frame)) + { + result = rxPortAccept(self, redundant_iface_index, ts_usec, frame, memory, out_transfer); + } + else + { + memFreePayload(memory.payload, datagram_payload); + } + return result; +} + +static inline void rxPortInit(struct UdpardRxPort* const self) +{ + memZero(sizeof(*self), self); + self->extent = SIZE_MAX; // Unlimited extent by default. + self->transfer_id_timeout_usec = UDPARD_DEFAULT_TRANSFER_ID_TIMEOUT_USEC; + self->sessions = NULL; +} // -------------------------------------------------- RX API -------------------------------------------------- @@ -1408,7 +1542,7 @@ int8_t udpardRxSubscriptionInit(struct UdpardRxSubscription* const self, (void) extent; (void) memory; (void) rxParseFrame; - (void) rxSessionInit; - (void) rxSessionAccept; + (void) rxPortAccept; + (void) rxPortInit; return 0; } diff --git a/libudpard/udpard.h b/libudpard/udpard.h index 2430aaf..dbe34b1 100644 --- a/libudpard/udpard.h +++ b/libudpard/udpard.h @@ -626,10 +626,6 @@ void udpardTxFree(struct UdpardMemoryResource* const memory, struct UdpardTxItem /// redundant network interfaces. struct UdpardRxPort { - /// For subject ports this is the subject-ID. For RPC-service ports this is the service-ID. - /// READ-ONLY - UdpardPortID port_id; - /// The maximum payload size that can be accepted at this port. /// The rest will be truncated away following the implicit truncation rule defined in the Cyphal specification. /// READ-ONLY @@ -878,6 +874,9 @@ struct UdpardRxRPC /// READ-ONLY struct UdpardTreeNode base; + /// READ-ONLY + UdpardPortID service_id; + /// See UdpardRxPort. /// Use this to change the transfer-ID timeout value for this RPC-service port. struct UdpardRxPort port; diff --git a/tests/.idea/dictionaries/pavel.xml b/tests/.idea/dictionaries/pavel.xml index f1e8005..a40cb7b 100644 --- a/tests/.idea/dictionaries/pavel.xml +++ b/tests/.idea/dictionaries/pavel.xml @@ -2,6 +2,7 @@ baremetal + cavl cfamily deallocation deduplicator diff --git a/tests/src/test_intrusive_rx.c b/tests/src/test_intrusive_rx.c index f300158..5ea89e0 100644 --- a/tests/src/test_intrusive_rx.c +++ b/tests/src/test_intrusive_rx.c @@ -122,7 +122,7 @@ static RxMemory makeRxMemory(InstrumentedAllocator* const fragment, Instrumented return (RxMemory){.fragment = &fragment->base, .payload = &payload->base}; } -// -------------------------------------------------------------------------------------------------------------------- +// -------------------------------------------------- FRAME PARSE -------------------------------------------------- // Generate reference data using PyCyphal: // @@ -259,6 +259,8 @@ static void testParseFrameEmpty(void) TEST_ASSERT_FALSE(rxParseFrame((struct UdpardMutablePayload){.data = "", .size = 0}, &rxf)); } +// -------------------------------------------------- SLOT -------------------------------------------------- + static void testSlotRestartEmpty(void) { RxSlot slot = { @@ -956,6 +958,8 @@ static void testSlotAcceptA(void) TEST_ASSERT_NULL(slot.fragments); } +// -------------------------------------------------- IFACE -------------------------------------------------- + static void testIfaceIsFutureTransferID(void) { InstrumentedAllocator mem_fragment = {0}; @@ -1850,6 +1854,8 @@ static void testIfaceAcceptC(void) TEST_ASSERT_EQUAL(0, mem_fragment.allocated_fragments); } +// -------------------------------------------------- SESSION -------------------------------------------------- + static void testSessionDeduplicate(void) { InstrumentedAllocator mem_fragment = {0}; @@ -2022,6 +2028,15 @@ static void testSessionAcceptA(void) TEST_ASSERT_EQUAL(0, mem_fragment.allocated_fragments); } +// -------------------------------------------------- PORT -------------------------------------------------- + +static inline void testPortAcceptFrameA(void) +{ + // +} + +// --------------------------------------------------------------------------------------------------------------------- + void setUp(void) {} void tearDown(void) {} @@ -2058,6 +2073,8 @@ int main(void) // session RUN_TEST(testSessionDeduplicate); RUN_TEST(testSessionAcceptA); + // port + RUN_TEST(testPortAcceptFrameA); return UNITY_END(); } From ea5d94ce98f7e8590c403109158899f98fcf1d85 Mon Sep 17 00:00:00 2001 From: Pavel Kirienko Date: Tue, 1 Aug 2023 22:16:56 +0300 Subject: [PATCH 20/38] Add port test --- libudpard/udpard.c | 33 ++-- libudpard/udpard.h | 4 +- tests/.idea/dictionaries/pavel.xml | 1 + tests/src/test_intrusive_rx.c | 234 ++++++++++++++++++++++++++++- 4 files changed, 257 insertions(+), 15 deletions(-) diff --git a/libudpard/udpard.c b/libudpard/udpard.c index bd687c0..488e835 100644 --- a/libudpard/udpard.c +++ b/libudpard/udpard.c @@ -191,6 +191,7 @@ static inline uint16_t headerCRCCompute(const size_t size, const void* const dat #define TRANSFER_CRC_INITIAL 0xFFFFFFFFUL #define TRANSFER_CRC_OUTPUT_XOR 0xFFFFFFFFUL #define TRANSFER_CRC_RESIDUE_BEFORE_OUTPUT_XOR 0xB798B438UL +#define TRANSFER_CRC_RESIDUE_AFTER_OUTPUT_XOR (TRANSFER_CRC_RESIDUE_BEFORE_OUTPUT_XOR ^ TRANSFER_CRC_OUTPUT_XOR) #define TRANSFER_CRC_SIZE_BYTES 4U static inline uint32_t transferCRCAddByte(const uint32_t crc, const byte_t byte) @@ -1343,6 +1344,7 @@ static inline bool rxSessionDeduplicate(UdpardInternalRxSession* const self, return accept; } +/// Takes ownership of the frame payload buffer. static inline int_fast8_t rxSessionAccept(UdpardInternalRxSession* const self, const uint_fast8_t redundant_iface_index, const UdpardMicrosecond ts_usec, @@ -1425,6 +1427,7 @@ static inline struct UdpardTreeNode* rxPortSessionFactory(void* const user_refer } /// Accepts a frame into a port, possibly creating a new session along the way. +/// Takes ownership of the frame payload buffer. static inline int_fast8_t rxPortAccept(struct UdpardRxPort* const self, const uint_fast8_t redundant_iface_index, const UdpardMicrosecond ts_usec, @@ -1434,9 +1437,10 @@ static inline int_fast8_t rxPortAccept(struct UdpardRxPort* const self { UDPARD_ASSERT((self != NULL) && (redundant_iface_index < UDPARD_NETWORK_INTERFACE_COUNT_MAX) && (out_transfer != NULL)); - int_fast8_t result = 0; - bool release = true; - if (frame.meta.src_node_id <= UDPARD_NODE_ID_MAX) + int_fast8_t result = 0; + bool release = true; + const bool anonymous = frame.meta.src_node_id == UDPARD_NODE_ID_UNSET; + if (!anonymous) { struct UdpardInternalRxSession* const session = (struct UdpardInternalRxSession*) cavlSearch((struct UdpardTreeNode**) &self->sessions, @@ -1461,22 +1465,27 @@ static inline int_fast8_t rxPortAccept(struct UdpardRxPort* const self result = -UDPARD_ERROR_MEMORY; } } - else // Anonymous transfers are always accepted unconditionally unless invalid. + else // Valid anonymous transfers are always accepted unconditionally. { - if (transferCRCCompute(frame.base.payload.size, frame.base.payload.data) == - TRANSFER_CRC_RESIDUE_BEFORE_OUTPUT_XOR) + const bool size_ok = frame.base.payload.size >= TRANSFER_CRC_SIZE_BYTES; + const bool crc_ok = transferCRCCompute(frame.base.payload.size, frame.base.payload.data) == + TRANSFER_CRC_RESIDUE_AFTER_OUTPUT_XOR; + if (size_ok && crc_ok) { result = 1; release = false; memZero(sizeof(*out_transfer), out_transfer); + // Copy relevant metadata from the frame. Remember that anonymous transfers are always single-frame. out_transfer->timestamp_usec = ts_usec; out_transfer->priority = frame.meta.priority; out_transfer->source_node_id = frame.meta.src_node_id; out_transfer->transfer_id = frame.meta.transfer_id; - out_transfer->payload_size = frame.base.payload.size; - out_transfer->payload.next = NULL; - out_transfer->payload.view = frame.base.payload; - out_transfer->payload.origin = frame.base.origin; + // Manually set up the transfer payload to point to the relevant slice inside the frame payload. + out_transfer->payload.next = NULL; + out_transfer->payload.view.size = frame.base.payload.size - TRANSFER_CRC_SIZE_BYTES; + out_transfer->payload.view.data = frame.base.payload.data; + out_transfer->payload.origin = frame.base.origin; + out_transfer->payload_size = out_transfer->payload.view.size; } } if (release) @@ -1487,6 +1496,7 @@ static inline int_fast8_t rxPortAccept(struct UdpardRxPort* const self } /// Accepts a raw frame and, if valid, passes it on to rxPortAccept() for further processing. +/// Takes ownership of the frame payload buffer. static inline int_fast8_t rxPortAcceptFrame(struct UdpardRxPort* const self, const uint_fast8_t redundant_iface_index, const UdpardMicrosecond ts_usec, @@ -1541,8 +1551,7 @@ int8_t udpardRxSubscriptionInit(struct UdpardRxSubscription* const self, (void) subject_id; (void) extent; (void) memory; - (void) rxParseFrame; - (void) rxPortAccept; + (void) rxPortAcceptFrame; (void) rxPortInit; return 0; } diff --git a/libudpard/udpard.h b/libudpard/udpard.h index dbe34b1..39a5537 100644 --- a/libudpard/udpard.h +++ b/libudpard/udpard.h @@ -809,7 +809,7 @@ int8_t udpardRxSubscriptionInit(struct UdpardRxSubscription* const self, /// Frees all memory held by the subscription instance. /// After invoking this function, the instance is no longer usable. /// Do not forget to close the sockets that were opened for this subscription. -void udpardRxSubscriptionDestroy(struct UdpardRxSubscription* const self); +void udpardRxSubscriptionFree(struct UdpardRxSubscription* const self); /// Datagrams received from the sockets of this subscription are fed into this function. /// @@ -951,7 +951,7 @@ int8_t udpardRxRPCDispatcherInit(struct UdpardRxRPCDispatcher* const self, /// Frees all memory held by the RPC-service dispatcher instance. /// After invoking this function, the instance is no longer usable. /// Do not forget to close the sockets that were opened for this instance. -void udpardRxRPCDispatcherDestroy(struct UdpardRxRPCDispatcher* const self); +void udpardRxRPCDispatcherFree(struct UdpardRxRPCDispatcher* const self); /// This function lets the application register its interest in a particular service-ID and kind (request/response) /// by creating an RPC-service RX port. The service pointer shall retain validity until its unregistration or until diff --git a/tests/.idea/dictionaries/pavel.xml b/tests/.idea/dictionaries/pavel.xml index a40cb7b..9aaa602 100644 --- a/tests/.idea/dictionaries/pavel.xml +++ b/tests/.idea/dictionaries/pavel.xml @@ -25,6 +25,7 @@ prio profraw pycyphal + spdx stringmakers udpard udpip diff --git a/tests/src/test_intrusive_rx.c b/tests/src/test_intrusive_rx.c index 5ea89e0..3dc09e6 100644 --- a/tests/src/test_intrusive_rx.c +++ b/tests/src/test_intrusive_rx.c @@ -122,6 +122,51 @@ static RxMemory makeRxMemory(InstrumentedAllocator* const fragment, Instrumented return (RxMemory){.fragment = &fragment->base, .payload = &payload->base}; } +static struct UdpardMutablePayload makeDatagramPayload(struct UdpardMemoryResource* const memory, + const TransferMetadata meta, + const uint32_t frame_index, + const bool end_of_transfer, + const struct UdpardPayload payload) +{ + struct UdpardMutablePayload pld = {.size = payload.size + HEADER_SIZE_BYTES}; + pld.data = memAlloc(memory, pld.size); + if (pld.data != NULL) + { + (void) memcpy(txSerializeHeader(pld.data, meta, frame_index, end_of_transfer), payload.data, payload.size); + } + else + { + TEST_PANIC("Failed to allocate datagram payload"); + } + return pld; +} + +static struct UdpardMutablePayload makeDatagramPayloadSingleFrame(struct UdpardMemoryResource* const memory, + const TransferMetadata meta, + const struct UdpardPayload payload) +{ + struct UdpardMutablePayload pld = + makeDatagramPayload(memory, + meta, + 0, + true, + (struct UdpardPayload){.data = payload.data, + .size = payload.size + TRANSFER_CRC_SIZE_BYTES}); + TEST_PANIC_UNLESS(pld.size == (payload.size + HEADER_SIZE_BYTES + TRANSFER_CRC_SIZE_BYTES)); + txSerializeU32(((byte_t*) pld.data) + HEADER_SIZE_BYTES + payload.size, + transferCRCCompute(payload.size, payload.data)); + return pld; +} + +static struct UdpardMutablePayload makeDatagramPayloadSingleFrameString(struct UdpardMemoryResource* const memory, + const TransferMetadata meta, + const char* const payload) +{ + return makeDatagramPayloadSingleFrame(memory, + meta, + (struct UdpardPayload){.data = payload, .size = strlen(payload)}); +} + // -------------------------------------------------- FRAME PARSE -------------------------------------------------- // Generate reference data using PyCyphal: @@ -2032,7 +2077,194 @@ static void testSessionAcceptA(void) static inline void testPortAcceptFrameA(void) { - // + InstrumentedAllocator mem_session = {0}; + InstrumentedAllocator mem_fragment = {0}; + InstrumentedAllocator mem_payload = {0}; + instrumentedAllocatorNew(&mem_session); + instrumentedAllocatorNew(&mem_fragment); + instrumentedAllocatorNew(&mem_payload); + const struct UdpardRxMemoryResources mem = {.session = &mem_session.base, + .fragment = &mem_fragment.base, + .payload = &mem_payload.base}; + struct UdpardRxTransfer transfer = {0}; + // Initialize the port. + struct UdpardRxPort port; + rxPortInit(&port); + TEST_ASSERT_EQUAL(SIZE_MAX, port.extent); + TEST_ASSERT_EQUAL(UDPARD_DEFAULT_TRANSFER_ID_TIMEOUT_USEC, port.transfer_id_timeout_usec); + TEST_ASSERT_NULL(port.sessions); + + // Accept valid non-anonymous transfer. + TEST_ASSERT_EQUAL( + 1, + rxPortAcceptFrame(&port, + 1, + 10000000, + makeDatagramPayloadSingleFrameString(&mem_payload.base, // + (TransferMetadata){.priority = UdpardPriorityImmediate, + .src_node_id = 2222, + .dst_node_id = UDPARD_NODE_ID_UNSET, + .data_specifier = 0, + .transfer_id = 0xB}, + "When will the collapse of space in the vicinity of the " + "Solar System into two dimensions cease?"), + mem, + &transfer)); + TEST_ASSERT_EQUAL(1, mem_session.allocated_fragments); + TEST_ASSERT_EQUAL(0, mem_fragment.allocated_fragments); // Head optimization in effect. + TEST_ASSERT_EQUAL(1, mem_payload.allocated_fragments); + // Check the received transfer. + TEST_ASSERT_EQUAL(10000000, transfer.timestamp_usec); + TEST_ASSERT_EQUAL(UdpardPriorityImmediate, transfer.priority); + TEST_ASSERT_EQUAL(2222, transfer.source_node_id); + TEST_ASSERT_EQUAL(0xB, transfer.transfer_id); + TEST_ASSERT_EQUAL(94, transfer.payload_size); + TEST_ASSERT(compareStringWithPayload("When will the collapse of space in the vicinity of the " + "Solar System into two dimensions cease?", + transfer.payload.view)); + // Free the memory. + udpardFragmentFree(transfer.payload, &mem_fragment.base, &mem_payload.base); + TEST_ASSERT_EQUAL(1, mem_session.allocated_fragments); // The session remains. + TEST_ASSERT_EQUAL(0, mem_fragment.allocated_fragments); + TEST_ASSERT_EQUAL(0, mem_payload.allocated_fragments); + + // Send another transfer from another node and see the session count increase. + TEST_ASSERT_EQUAL( + 1, + rxPortAcceptFrame(&port, + 0, + 10000010, + makeDatagramPayloadSingleFrameString(&mem_payload.base, // + (TransferMetadata){.priority = UdpardPriorityImmediate, + .src_node_id = 3333, + .dst_node_id = UDPARD_NODE_ID_UNSET, + .data_specifier = 0, + .transfer_id = 0xC}, + "It will never cease."), + mem, + &transfer)); + TEST_ASSERT_EQUAL(2, mem_session.allocated_fragments); // New session created. + TEST_ASSERT_EQUAL(0, mem_fragment.allocated_fragments); // Head optimization in effect. + TEST_ASSERT_EQUAL(1, mem_payload.allocated_fragments); + // Check the received transfer. + TEST_ASSERT_EQUAL(10000010, transfer.timestamp_usec); + TEST_ASSERT_EQUAL(UdpardPriorityImmediate, transfer.priority); + TEST_ASSERT_EQUAL(3333, transfer.source_node_id); + TEST_ASSERT_EQUAL(0xC, transfer.transfer_id); + TEST_ASSERT_EQUAL(20, transfer.payload_size); + TEST_ASSERT(compareStringWithPayload("It will never cease.", transfer.payload.view)); + // Free the memory. + udpardFragmentFree(transfer.payload, &mem_fragment.base, &mem_payload.base); + TEST_ASSERT_EQUAL(2, mem_session.allocated_fragments); // The sessions remain. + TEST_ASSERT_EQUAL(0, mem_fragment.allocated_fragments); + TEST_ASSERT_EQUAL(0, mem_payload.allocated_fragments); + + // Try sending another frame with no memory left and see it fail during session allocation. + mem_session.limit_fragments = 0; + TEST_ASSERT_EQUAL( + -UDPARD_ERROR_MEMORY, + rxPortAcceptFrame(&port, + 2, + 10000020, + makeDatagramPayloadSingleFrameString(&mem_payload.base, // + (TransferMetadata){.priority = UdpardPriorityImmediate, + .src_node_id = 4444, + .dst_node_id = UDPARD_NODE_ID_UNSET, + .data_specifier = 0, + .transfer_id = 0xD}, + "Cheng Xin shuddered."), + mem, + &transfer)); + TEST_ASSERT_EQUAL(2, mem_session.allocated_fragments); // Not increased. + TEST_ASSERT_EQUAL(0, mem_fragment.allocated_fragments); // Not accepted. + TEST_ASSERT_EQUAL(0, mem_payload.allocated_fragments); // Buffer freed. + + // Anonymous transfers are stateless and do not require session allocation. + mem_session.limit_fragments = 0; + TEST_ASSERT_EQUAL( + 1, + rxPortAcceptFrame(&port, + 2, + 10000030, + makeDatagramPayloadSingleFrameString(&mem_payload.base, // + (TransferMetadata){.priority = UdpardPriorityImmediate, + .src_node_id = UDPARD_NODE_ID_UNSET, + .dst_node_id = UDPARD_NODE_ID_UNSET, + .data_specifier = 0, + .transfer_id = 0xD}, + "Cheng Xin shuddered."), + mem, + &transfer)); + TEST_ASSERT_EQUAL(2, mem_session.allocated_fragments); // Not increased. + TEST_ASSERT_EQUAL(0, mem_fragment.allocated_fragments); // Head optimization in effect. + TEST_ASSERT_EQUAL(1, mem_payload.allocated_fragments); // Frame passed to the application. + // Check the received transfer. + TEST_ASSERT_EQUAL(10000030, transfer.timestamp_usec); + TEST_ASSERT_EQUAL(UdpardPriorityImmediate, transfer.priority); + TEST_ASSERT_EQUAL(UDPARD_NODE_ID_UNSET, transfer.source_node_id); + TEST_ASSERT_EQUAL(0xD, transfer.transfer_id); + TEST_ASSERT_EQUAL(20, transfer.payload_size); + TEST_ASSERT(compareStringWithPayload("Cheng Xin shuddered.", transfer.payload.view)); + // Free the memory. + udpardFragmentFree(transfer.payload, &mem_fragment.base, &mem_payload.base); + TEST_ASSERT_EQUAL(2, mem_session.allocated_fragments); // The sessions remain. + TEST_ASSERT_EQUAL(0, mem_fragment.allocated_fragments); + TEST_ASSERT_EQUAL(0, mem_payload.allocated_fragments); + + // Send invalid anonymous transfers and see them fail. + { // Bad CRC. + struct UdpardMutablePayload datagram = + makeDatagramPayloadSingleFrameString(&mem_payload.base, // + (TransferMetadata){.priority = UdpardPriorityImmediate, + .src_node_id = UDPARD_NODE_ID_UNSET, + .dst_node_id = UDPARD_NODE_ID_UNSET, + .data_specifier = 0, + .transfer_id = 0xE}, + "You are scared? Do you think that in this galaxy, in this universe, " + "only the Solar System is collapsing into two dimensions? Haha..."); + *(((byte_t*) datagram.data) + HEADER_SIZE_BYTES) = 0x00; // Corrupt the payload, CRC invalid. + TEST_ASSERT_EQUAL(0, rxPortAcceptFrame(&port, 0, 10000040, datagram, mem, &transfer)); + TEST_ASSERT_EQUAL(2, mem_session.allocated_fragments); + TEST_ASSERT_EQUAL(0, mem_fragment.allocated_fragments); + TEST_ASSERT_EQUAL(0, mem_payload.allocated_fragments); + } + { // No payload (transfer CRC is always required). + byte_t* const payload = memAlloc(&mem_payload.base, HEADER_SIZE_BYTES); + (void) txSerializeHeader(payload, + (TransferMetadata){.priority = UdpardPriorityImmediate, + .src_node_id = UDPARD_NODE_ID_UNSET, + .dst_node_id = UDPARD_NODE_ID_UNSET, + .data_specifier = 0, + .transfer_id = 0xE}, + 0, + true); + TEST_ASSERT_EQUAL(0, + rxPortAcceptFrame(&port, + 0, + 10000050, + (struct UdpardMutablePayload){.size = HEADER_SIZE_BYTES, .data = payload}, + mem, + &transfer)); + TEST_ASSERT_EQUAL(2, mem_session.allocated_fragments); + TEST_ASSERT_EQUAL(0, mem_fragment.allocated_fragments); + TEST_ASSERT_EQUAL(0, mem_payload.allocated_fragments); + } + + // Send an invalid frame and make sure the memory is freed. + TEST_ASSERT_EQUAL(0, + rxPortAcceptFrame(&port, + 0, + 10000060, + (struct UdpardMutablePayload){.size = HEADER_SIZE_BYTES, + .data = memAlloc(&mem_payload.base, + HEADER_SIZE_BYTES)}, + mem, + &transfer)); + TEST_ASSERT_EQUAL(2, mem_session.allocated_fragments); // Not increased. + TEST_ASSERT_EQUAL(0, mem_fragment.allocated_fragments); // Not accepted. + TEST_ASSERT_EQUAL(0, mem_payload.allocated_fragments); // Buffer freed. + + // TODO: FREE THE PORT INSTANCE -- DEALLOCATE THE SESSIONS. } // --------------------------------------------------------------------------------------------------------------------- From 8605051604f3ca5ca66b700d464b6fa773c5fa17 Mon Sep 17 00:00:00 2001 From: Pavel Kirienko Date: Wed, 2 Aug 2023 18:18:32 +0300 Subject: [PATCH 21/38] Add destructors & tests; the coverage should be at 100% --- libudpard/udpard.c | 107 +++++++++++++++++++++-------- libudpard/udpard.h | 3 + tests/.idea/dictionaries/pavel.xml | 3 +- tests/src/test_intrusive_rx.c | 92 ++++++++++++++++++++++++- 4 files changed, 172 insertions(+), 33 deletions(-) diff --git a/libudpard/udpard.c b/libudpard/udpard.c index 488e835..8e3bc60 100644 --- a/libudpard/udpard.c +++ b/libudpard/udpard.c @@ -805,18 +805,6 @@ typedef struct RxFragment uint32_t frame_index; } RxFragment; -static inline void rxFragmentFree(struct UdpardFragment* const head, const RxMemory memory) -{ - struct UdpardFragment* handle = head; - while (handle != NULL) - { - struct UdpardFragment* const next = handle->next; - memFreePayload(memory.payload, handle->origin); // May be NULL, is okay. - memFree(memory.fragment, sizeof(RxFragment), handle); - handle = next; - } -} - /// Internally, the RX pipeline is arranged as follows: /// /// - There is one port per subscription or an RPC-service listener. Within the port, there are N sessions, @@ -826,9 +814,9 @@ static inline void rxFragmentFree(struct UdpardFragment* const head, const RxMem /// - Per session, there are UDPARD_NETWORK_INTERFACE_COUNT_MAX interface states to support interface redundancy. /// /// - Per interface, there are RX_SLOT_COUNT slots; a slot keeps the state of a transfer in the process of being -/// reassembled. +/// reassembled which includes its payload fragments. /// -/// Port -> Session -> Interface -> Slot. +/// Port -> Session -> Interface -> Slot -> Fragments. /// /// Consider the following examples, where A and B denote distinct transfers of three frames each: /// @@ -883,10 +871,12 @@ typedef struct UdpardInternalRxSession RxIface ifaces[UDPARD_NETWORK_INTERFACE_COUNT_MAX]; } UdpardInternalRxSession; -// -------------------------------------------------- RX SLOT -------------------------------------------------- +// -------------------------------------------------- RX FRAGMENT -------------------------------------------------- +/// Frees all fragments in the tree and their payload buffers. Destroys the passed fragment. +/// This is meant to be invoked on the root of the tree. // NOLINTNEXTLINE(misc-no-recursion) -static inline void rxSlotFree(RxFragment* const self, const RxMemory memory) +static inline void rxFragmentDestroyTree(RxFragment* const self, const RxMemory memory) { UDPARD_ASSERT(self != NULL); memFreePayload(memory.payload, self->base.origin); @@ -896,26 +886,50 @@ static inline void rxSlotFree(RxFragment* const self, const RxMemory memory) if (child != NULL) { UDPARD_ASSERT(child->base.up == &self->tree.base); - rxSlotFree(child->this, memory); + rxFragmentDestroyTree(child->this, memory); } } memFree(memory.fragment, sizeof(RxFragment), self); // self-destruct } +/// Frees all fragments in the list and their payload buffers. Destroys the passed fragment. +/// This is meant to be invoked on the head of the list. +/// This function is needed because when a fragment tree is transformed into a list, the tree structure itself +/// is invalidated and cannot be used to free the fragments anymore. +static inline void rxFragmentDestroyList(struct UdpardFragment* const head, const RxMemory memory) +{ + struct UdpardFragment* handle = head; + while (handle != NULL) + { + struct UdpardFragment* const next = handle->next; + memFreePayload(memory.payload, handle->origin); // May be NULL, is okay. + memFree(memory.fragment, sizeof(RxFragment), handle); + handle = next; + } +} + +// -------------------------------------------------- RX SLOT -------------------------------------------------- + +static inline void rxSlotFree(RxSlot* const self, const RxMemory memory) +{ + UDPARD_ASSERT(self != NULL); + if (self->fragments != NULL) + { + rxFragmentDestroyTree(self->fragments->this, memory); + self->fragments = NULL; + } +} + static inline void rxSlotRestart(RxSlot* const self, const UdpardTransferID transfer_id, const RxMemory memory) { UDPARD_ASSERT(self != NULL); + rxSlotFree(self, memory); self->ts_usec = TIMESTAMP_UNSET; // Will be assigned when the first frame of the transfer has arrived. self->transfer_id = transfer_id; self->max_index = 0; self->eot_index = FRAME_INDEX_UNSET; self->accepted_frames = 0; self->payload_size = 0; - if (self->fragments != NULL) - { - rxSlotFree(self->fragments->this, memory); - self->fragments = NULL; - } } typedef struct @@ -1066,7 +1080,7 @@ static inline bool rxSlotEject(size_t* const out_payload_size, } else // The transfer turned out to be invalid. We have to free the fragments. Can't use the tree anymore. { - rxFragmentFree(eject_ctx.head, memory); + rxFragmentDestroyList(eject_ctx.head, memory); } return result; } @@ -1313,6 +1327,16 @@ static inline void rxIfaceInit(RxIface* const self, const RxMemory memory) } } +/// Frees the iface and all slots in it. The iface instance itself is not freed. +static inline void rxIfaceFree(RxIface* const self, const RxMemory memory) +{ + UDPARD_ASSERT(self != NULL); + for (uint_fast8_t i = 0; i < RX_SLOT_COUNT; i++) + { + rxSlotFree(&self->slots[i], memory); + } +} + // -------------------------------------------------- RX SESSION -------------------------------------------------- /// Checks if the given transfer should be accepted. If not, the transfer is freed. @@ -1337,7 +1361,7 @@ static inline bool rxSessionDeduplicate(UdpardInternalRxSession* const self, else // This is a duplicate: received from another interface, a FEC retransmission, or a network glitch. { memFreePayload(memory.payload, transfer->payload.origin); - rxFragmentFree(transfer->payload.next, memory); + rxFragmentDestroyList(transfer->payload.next, memory); transfer->payload_size = 0; transfer->payload = (struct UdpardFragment){.next = NULL, .view = {0}, .origin = {0}}; } @@ -1384,6 +1408,27 @@ static inline void rxSessionInit(UdpardInternalRxSession* const self, const RxMe } } +/// Frees all ifaces in the session, all children in the session tree recursively, and destroys the session itself. +// NOLINTNEXTLINE(*-no-recursion) +static inline void rxSessionDestroyTree(UdpardInternalRxSession* const self, + const struct UdpardRxMemoryResources memory) +{ + for (uint_fast8_t i = 0; i < UDPARD_NETWORK_INTERFACE_COUNT_MAX; i++) + { + rxIfaceFree(&self->ifaces[i], (RxMemory){.fragment = memory.fragment, .payload = memory.payload}); + } + for (uint_fast8_t i = 0; i < 2; i++) + { + UdpardInternalRxSession* const child = (UdpardInternalRxSession*) self->base.lr[i]; + if (child != NULL) + { + UDPARD_ASSERT(child->base.up == &self->base); + rxSessionDestroyTree(child, memory); + } + } + memFree(memory.session, sizeof(UdpardInternalRxSession), self); +} + // -------------------------------------------------- RX PORT -------------------------------------------------- typedef struct @@ -1525,20 +1570,21 @@ static inline void rxPortInit(struct UdpardRxPort* const self) self->sessions = NULL; } +static inline void rxPortFree(struct UdpardRxPort* const self, const struct UdpardRxMemoryResources memory) +{ + rxSessionDestroyTree(self->sessions, memory); +} + // -------------------------------------------------- RX API -------------------------------------------------- void udpardFragmentFree(const struct UdpardFragment head, struct UdpardMemoryResource* const memory_fragment, struct UdpardMemoryResource* const memory_payload) { - if ((memory_fragment != NULL) && (memory_payload != NULL)) + if ((memory_fragment != NULL) && (memory_payload != NULL)) // The head is not heap-allocated so not freed. { memFreePayload(memory_payload, head.origin); // May be NULL, is okay. - rxFragmentFree(head.next, // The head is not heap-allocated so not freed. - (RxMemory){ - .fragment = memory_fragment, - .payload = memory_payload, - }); + rxFragmentDestroyList(head.next, (RxMemory){.fragment = memory_fragment, .payload = memory_payload}); } } @@ -1553,5 +1599,6 @@ int8_t udpardRxSubscriptionInit(struct UdpardRxSubscription* const self, (void) memory; (void) rxPortAcceptFrame; (void) rxPortInit; + (void) rxPortFree; return 0; } diff --git a/libudpard/udpard.h b/libudpard/udpard.h index 39a5537..f78cfa9 100644 --- a/libudpard/udpard.h +++ b/libudpard/udpard.h @@ -71,6 +71,9 @@ /// As will be shown below, a typical application with R redundant network interfaces and S topic subscriptions needs /// R*(S+2) sockets (or equivalent abstractions provided by the underlying UDP/IP stack). /// +/// As a matter of convention, resource disposal functions are named "free" if the memory of the resource itself is +/// not deallocated, and "destroy" if the memory is deallocated. +/// /// /// Transmission pipeline /// diff --git a/tests/.idea/dictionaries/pavel.xml b/tests/.idea/dictionaries/pavel.xml index 9aaa602..19317bd 100644 --- a/tests/.idea/dictionaries/pavel.xml +++ b/tests/.idea/dictionaries/pavel.xml @@ -9,6 +9,7 @@ discardment dscp dudpard + ffee ghcr iface ifaces @@ -39,4 +40,4 @@ zzzzzz - \ No newline at end of file + diff --git a/tests/src/test_intrusive_rx.c b/tests/src/test_intrusive_rx.c index 3dc09e6..0522bce 100644 --- a/tests/src/test_intrusive_rx.c +++ b/tests/src/test_intrusive_rx.c @@ -141,6 +141,19 @@ static struct UdpardMutablePayload makeDatagramPayload(struct UdpardMemoryResour return pld; } +static struct UdpardMutablePayload makeDatagramPayloadString(struct UdpardMemoryResource* const memory, + const TransferMetadata meta, + const uint32_t frame_index, + const bool end_of_transfer, + const char* const string) +{ + return makeDatagramPayload(memory, + meta, + frame_index, + end_of_transfer, + (struct UdpardPayload){.data = string, .size = strlen(string)}); +} + static struct UdpardMutablePayload makeDatagramPayloadSingleFrame(struct UdpardMemoryResource* const memory, const TransferMetadata meta, const struct UdpardPayload payload) @@ -167,7 +180,7 @@ static struct UdpardMutablePayload makeDatagramPayloadSingleFrameString(struct U (struct UdpardPayload){.data = payload, .size = strlen(payload)}); } -// -------------------------------------------------- FRAME PARSE -------------------------------------------------- +// -------------------------------------------------- FRAME PARSING -------------------------------------------------- // Generate reference data using PyCyphal: // @@ -2264,7 +2277,82 @@ static inline void testPortAcceptFrameA(void) TEST_ASSERT_EQUAL(0, mem_fragment.allocated_fragments); // Not accepted. TEST_ASSERT_EQUAL(0, mem_payload.allocated_fragments); // Buffer freed. - // TODO: FREE THE PORT INSTANCE -- DEALLOCATE THE SESSIONS. + // Send incomplete transfers to see them cleaned up upon destruction. + mem_session.limit_fragments = SIZE_MAX; + TEST_ASSERT_EQUAL(0, + rxPortAcceptFrame(&port, + 0, + 10000070, + makeDatagramPayloadString(&mem_payload.base, // + (TransferMetadata){ + .priority = UdpardPriorityImmediate, + .src_node_id = 10000, + .dst_node_id = UDPARD_NODE_ID_UNSET, + .data_specifier = 0, + .transfer_id = 0xD, + }, + 100, + false, + "What you're saying makes no sense. " + "At least, it doesn't make sense to lower spatial " + "dimensions as a weapon. "), + mem, + &transfer)); + TEST_ASSERT_EQUAL(3, mem_session.allocated_fragments); + TEST_ASSERT_EQUAL(1, mem_fragment.allocated_fragments); + TEST_ASSERT_EQUAL(1, mem_payload.allocated_fragments); + TEST_ASSERT_EQUAL(0, + rxPortAcceptFrame(&port, + 0, + 10000080, + makeDatagramPayloadString(&mem_payload.base, // + (TransferMetadata){ + .priority = UdpardPriorityImmediate, + .src_node_id = 10000, + .dst_node_id = UDPARD_NODE_ID_UNSET, + .data_specifier = 0, + .transfer_id = 0xD, + }, + 101, + false, + "In the long run, that's the sort of attack that " + "would kill the attacker as well as the target. " + "Eventually, the side that initiated attack would " + "also see their own space fall into the " + "two-dimensional abyss they created."), + mem, + &transfer)); + TEST_ASSERT_EQUAL(3, mem_session.allocated_fragments); // Same session because it comes from the same source. + TEST_ASSERT_EQUAL(2, mem_fragment.allocated_fragments); + TEST_ASSERT_EQUAL(2, mem_payload.allocated_fragments); + TEST_ASSERT_EQUAL(0, + rxPortAcceptFrame(&port, + 2, + 10000090, + makeDatagramPayloadString(&mem_payload.base, // + (TransferMetadata){ + .priority = UdpardPriorityImmediate, + .src_node_id = 10001, + .dst_node_id = UDPARD_NODE_ID_UNSET, + .data_specifier = 0, + .transfer_id = 0xD, + }, + 10, + true, + "You're too... kind-hearted."), + mem, + &transfer)); + TEST_ASSERT_EQUAL(4, mem_session.allocated_fragments); // New source. + TEST_ASSERT_EQUAL(3, mem_fragment.allocated_fragments); + TEST_ASSERT_EQUAL(3, mem_payload.allocated_fragments); + TEST_ASSERT_EQUAL(4 * sizeof(UdpardInternalRxSession), mem_session.allocated_bytes); + TEST_ASSERT_EQUAL(3 * sizeof(RxFragment), mem_fragment.allocated_bytes); + + // Free the port instance and ensure all ifaces and sessions are cleaned up. + rxPortFree(&port, mem); + TEST_ASSERT_EQUAL(0, mem_session.allocated_fragments); // All gone. + TEST_ASSERT_EQUAL(0, mem_fragment.allocated_fragments); + TEST_ASSERT_EQUAL(0, mem_payload.allocated_fragments); } // --------------------------------------------------------------------------------------------------------------------- From bc9b93ff628a06e693f296ed8df97815267693b7 Mon Sep 17 00:00:00 2001 From: Pavel Kirienko Date: Wed, 2 Aug 2023 18:20:20 +0300 Subject: [PATCH 22/38] Rename prio -> priority as requested --- libudpard/udpard.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/libudpard/udpard.c b/libudpard/udpard.c index 8e3bc60..132d6f9 100644 --- a/libudpard/udpard.c +++ b/libudpard/udpard.c @@ -747,10 +747,10 @@ static inline bool rxParseFrame(const struct UdpardMutablePayload datagram_paylo if ((datagram_payload.size > HEADER_SIZE_BYTES) && (version == HEADER_VERSION) && (headerCRCCompute(HEADER_SIZE_BYTES, datagram_payload.data) == HEADER_CRC_RESIDUE)) { - const uint_fast8_t prio = *ptr++; - if (prio <= UDPARD_PRIORITY_MAX) + const uint_fast8_t priority = *ptr++; + if (priority <= UDPARD_PRIORITY_MAX) { - out->meta.priority = (enum UdpardPriority) prio; + out->meta.priority = (enum UdpardPriority) priority; ptr = txDeserializeU16(ptr, &out->meta.src_node_id); ptr = txDeserializeU16(ptr, &out->meta.dst_node_id); ptr = txDeserializeU16(ptr, &out->meta.data_specifier); From 81378f08b7658d66620c27a3a84a86a3dd64995f Mon Sep 17 00:00:00 2001 From: Pavel Kirienko Date: Wed, 2 Aug 2023 18:38:32 +0300 Subject: [PATCH 23/38] Suppress a bogus alignment warning --- libudpard/udpard.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libudpard/udpard.c b/libudpard/udpard.c index 132d6f9..654d324 100644 --- a/libudpard/udpard.c +++ b/libudpard/udpard.c @@ -1419,7 +1419,7 @@ static inline void rxSessionDestroyTree(UdpardInternalRxSession* const sel } for (uint_fast8_t i = 0; i < 2; i++) { - UdpardInternalRxSession* const child = (UdpardInternalRxSession*) self->base.lr[i]; + UdpardInternalRxSession* const child = (UdpardInternalRxSession*) (void*) self->base.lr[i]; if (child != NULL) { UDPARD_ASSERT(child->base.up == &self->base); From b2863406c7a4efc8176461cb4a73d3e5f01ea4a0 Mon Sep 17 00:00:00 2001 From: Pavel Kirienko Date: Wed, 2 Aug 2023 18:45:33 +0300 Subject: [PATCH 24/38] SONAR --- libudpard/udpard.c | 37 ++++++++++++++++++++----------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/libudpard/udpard.c b/libudpard/udpard.c index 654d324..97422d3 100644 --- a/libudpard/udpard.c +++ b/libudpard/udpard.c @@ -886,7 +886,7 @@ static inline void rxFragmentDestroyTree(RxFragment* const self, const RxMemory if (child != NULL) { UDPARD_ASSERT(child->base.up == &self->tree.base); - rxFragmentDestroyTree(child->this, memory); + rxFragmentDestroyTree(child->this, memory); // NOSONAR recursion } } memFree(memory.fragment, sizeof(RxFragment), self); // self-destruct @@ -943,7 +943,7 @@ static inline int8_t rxSlotFragmentSearch(void* const user_reference, const stru { UDPARD_ASSERT((user_reference != NULL) && (node != NULL)); const RxSlotUpdateContext* const ctx = (RxSlotUpdateContext*) user_reference; - RxFragment* const frag = ((RxFragmentTreeNode*) node)->this; + const RxFragment* const frag = ((const RxFragmentTreeNode*) node)->this; UDPARD_ASSERT((ctx != NULL) && (frag != NULL)); int8_t out = 0; if (ctx->frame_index > frag->frame_index) @@ -995,7 +995,7 @@ static inline void rxSlotEjectFragment(RxFragment* const frag, RxSlotEjectContex RxFragment* const child = ((RxFragmentTreeNode*) frag->tree.base.lr[0])->this; UDPARD_ASSERT(child->frame_index < frag->frame_index); UDPARD_ASSERT(child->tree.base.up == &frag->tree.base); - rxSlotEjectFragment(child, ctx); + rxSlotEjectFragment(child, ctx); // NOSONAR recursion } const size_t fragment_size = frag->base.view.size; frag->base.next = NULL; // Default state; may be overwritten. @@ -1020,7 +1020,7 @@ static inline void rxSlotEjectFragment(RxFragment* const frag, RxSlotEjectContex RxFragment* const child = ((RxFragmentTreeNode*) frag->tree.base.lr[1])->this; UDPARD_ASSERT(child->frame_index > frag->frame_index); UDPARD_ASSERT(child->tree.base.up == &frag->tree.base); - rxSlotEjectFragment(child, ctx); + rxSlotEjectFragment(child, ctx); // NOSONAR recursion } // Drop the unneeded fragments and their handles after the sub-tree is fully traversed. if (!retain) @@ -1107,7 +1107,7 @@ static inline int_fast8_t rxSlotAccept(RxSlot* const self, if ((self->eot_index != FRAME_INDEX_UNSET) && (self->eot_index != frame.index)) { restart = true; // Inconsistent EOT flag, could be a node-ID conflict. - goto finish; + goto finish; // NOSONAR goto simplifies the control flow and is not prohibited by MISRA C:2012. } self->eot_index = frame.index; } @@ -1115,7 +1115,7 @@ static inline int_fast8_t rxSlotAccept(RxSlot* const self, if (self->max_index > self->eot_index) { restart = true; // Frames past EOT found, discard the entire transfer because we don't trust it anymore. - goto finish; + goto finish; // NOSONAR goto simplifies the control flow and is not prohibited by MISRA C:2012. } // SECOND: Insert the fragment into the fragment tree. If it already exists, drop and free the duplicate. UDPARD_ASSERT((self->max_index <= self->eot_index) && (self->accepted_frames <= self->eot_index)); @@ -1132,7 +1132,7 @@ static inline int_fast8_t rxSlotAccept(RxSlot* const self, UDPARD_ASSERT(release); result = -UDPARD_ERROR_MEMORY; // No restart because there is hope that there will be enough memory when we receive a duplicate. - goto finish; + goto finish; // NOSONAR goto simplifies the control flow and is not prohibited by MISRA C:2012. } UDPARD_ASSERT(self->max_index <= self->eot_index); if (update_ctx.accepted) @@ -1363,7 +1363,9 @@ static inline bool rxSessionDeduplicate(UdpardInternalRxSession* const self, memFreePayload(memory.payload, transfer->payload.origin); rxFragmentDestroyList(transfer->payload.next, memory); transfer->payload_size = 0; - transfer->payload = (struct UdpardFragment){.next = NULL, .view = {0}, .origin = {0}}; + transfer->payload = (struct UdpardFragment){.next = NULL, + .view = {.size = 0, .data = NULL}, + .origin = {.size = 0, .data = NULL}}; } return accept; } @@ -1423,7 +1425,7 @@ static inline void rxSessionDestroyTree(UdpardInternalRxSession* const sel if (child != NULL) { UDPARD_ASSERT(child->base.up == &self->base); - rxSessionDestroyTree(child, memory); + rxSessionDestroyTree(child, memory); // NOSONAR recursion } } memFree(memory.session, sizeof(UdpardInternalRxSession), self); @@ -1437,11 +1439,12 @@ typedef struct struct UdpardRxMemoryResources memory; } RxPortSessionSearchContext; -static inline int8_t rxPortSessionSearch(void* const user_reference, const struct UdpardTreeNode* node) +static inline int8_t rxPortSessionSearch(void* const user_reference, // NOSONAR non-const API + const struct UdpardTreeNode* node) { UDPARD_ASSERT((user_reference != NULL) && (node != NULL)); - const RxPortSessionSearchContext* const ctx = (const RxPortSessionSearchContext*) user_reference; - struct UdpardInternalRxSession* const session = (struct UdpardInternalRxSession*) node; + const RxPortSessionSearchContext* const ctx = (const RxPortSessionSearchContext*) user_reference; + const struct UdpardInternalRxSession* const session = (const struct UdpardInternalRxSession*) (const void*) node; UDPARD_ASSERT((ctx != NULL) && (session != NULL)); int8_t out = 0; if (ctx->remote_node_id > session->remote_node_id) @@ -1455,7 +1458,7 @@ static inline int8_t rxPortSessionSearch(void* const user_reference, const struc return out; } -static inline struct UdpardTreeNode* rxPortSessionFactory(void* const user_reference) +static inline struct UdpardTreeNode* rxPortSessionFactory(void* const user_reference) // NOSONAR non-const API { const RxPortSessionSearchContext* const ctx = (const RxPortSessionSearchContext*) user_reference; UDPARD_ASSERT((ctx != NULL) && (ctx->remote_node_id <= UDPARD_NODE_ID_MAX)); @@ -1487,7 +1490,7 @@ static inline int_fast8_t rxPortAccept(struct UdpardRxPort* const self const bool anonymous = frame.meta.src_node_id == UDPARD_NODE_ID_UNSET; if (!anonymous) { - struct UdpardInternalRxSession* const session = (struct UdpardInternalRxSession*) + struct UdpardInternalRxSession* const session = (struct UdpardInternalRxSession*) (void*) cavlSearch((struct UdpardTreeNode**) &self->sessions, &(RxPortSessionSearchContext){.remote_node_id = frame.meta.src_node_id, .memory = memory}, &rxPortSessionSearch, @@ -1597,8 +1600,8 @@ int8_t udpardRxSubscriptionInit(struct UdpardRxSubscription* const self, (void) subject_id; (void) extent; (void) memory; - (void) rxPortAcceptFrame; - (void) rxPortInit; - (void) rxPortFree; + (void) &rxPortAcceptFrame; + (void) &rxPortInit; + (void) &rxPortFree; return 0; } From f3e35ae341cc8194c756825ebe37df2bf82cfefd Mon Sep 17 00:00:00 2001 From: Pavel Kirienko Date: Wed, 2 Aug 2023 19:04:49 +0300 Subject: [PATCH 25/38] Extract compare32 --- libudpard/udpard.c | 45 +++++++++++++++-------------------- tests/src/test_intrusive_rx.c | 17 +++++++++++++ 2 files changed, 36 insertions(+), 26 deletions(-) diff --git a/libudpard/udpard.c b/libudpard/udpard.c index 97422d3..5cbae9a 100644 --- a/libudpard/udpard.c +++ b/libudpard/udpard.c @@ -109,6 +109,21 @@ static inline uint32_t max32(const uint32_t a, const uint32_t b) return (a > b) ? a : b; } +/// Returns the sign of the subtraction of the operands; zero if equal. This is useful for AVL search. +static inline int_fast8_t compare32(const uint32_t a, const uint32_t b) +{ + int_fast8_t result = 0; + if (a > b) + { + result = +1; + } + if (a < b) + { + result = -1; + } + return result; +} + static inline bool memIsValid(const struct UdpardMemoryResource* const memory) { return (memory != NULL) && (memory->allocate != NULL) && (memory->free != NULL); @@ -942,19 +957,8 @@ typedef struct static inline int8_t rxSlotFragmentSearch(void* const user_reference, const struct UdpardTreeNode* node) { UDPARD_ASSERT((user_reference != NULL) && (node != NULL)); - const RxSlotUpdateContext* const ctx = (RxSlotUpdateContext*) user_reference; - const RxFragment* const frag = ((const RxFragmentTreeNode*) node)->this; - UDPARD_ASSERT((ctx != NULL) && (frag != NULL)); - int8_t out = 0; - if (ctx->frame_index > frag->frame_index) - { - out = +1; - } - if (ctx->frame_index < frag->frame_index) - { - out = -1; - } - return out; + return compare32(((const RxSlotUpdateContext*) user_reference)->frame_index, + ((const RxFragmentTreeNode*) node)->this->frame_index); } static inline struct UdpardTreeNode* rxSlotFragmentFactory(void* const user_reference) @@ -1443,19 +1447,8 @@ static inline int8_t rxPortSessionSearch(void* const user_refer const struct UdpardTreeNode* node) { UDPARD_ASSERT((user_reference != NULL) && (node != NULL)); - const RxPortSessionSearchContext* const ctx = (const RxPortSessionSearchContext*) user_reference; - const struct UdpardInternalRxSession* const session = (const struct UdpardInternalRxSession*) (const void*) node; - UDPARD_ASSERT((ctx != NULL) && (session != NULL)); - int8_t out = 0; - if (ctx->remote_node_id > session->remote_node_id) - { - out = +1; - } - if (ctx->remote_node_id < session->remote_node_id) - { - out = -1; - } - return out; + return compare32(((const RxPortSessionSearchContext*) user_reference)->remote_node_id, + ((const struct UdpardInternalRxSession*) (const void*) node)->remote_node_id); } static inline struct UdpardTreeNode* rxPortSessionFactory(void* const user_reference) // NOSONAR non-const API diff --git a/tests/src/test_intrusive_rx.c b/tests/src/test_intrusive_rx.c index 0522bce..c5391a7 100644 --- a/tests/src/test_intrusive_rx.c +++ b/tests/src/test_intrusive_rx.c @@ -180,6 +180,21 @@ static struct UdpardMutablePayload makeDatagramPayloadSingleFrameString(struct U (struct UdpardPayload){.data = payload, .size = strlen(payload)}); } +// -------------------------------------------------- MISC -------------------------------------------------- + +static void testCompare32(void) +{ + TEST_ASSERT_EQUAL(0, compare32(0, 0)); + TEST_ASSERT_EQUAL(0, compare32(1, 1)); + TEST_ASSERT_EQUAL(0, compare32(0xdeadbeef, 0xdeadbeef)); + TEST_ASSERT_EQUAL(0, compare32(0x0badc0de, 0x0badc0de)); + TEST_ASSERT_EQUAL(0, compare32(0xffffffff, 0xffffffff)); + TEST_ASSERT_EQUAL(+1, compare32(1, 0)); + TEST_ASSERT_EQUAL(+1, compare32(0xffffffff, 0xfffffffe)); + TEST_ASSERT_EQUAL(-1, compare32(0, 1)); + TEST_ASSERT_EQUAL(-1, compare32(0xfffffffe, 0xffffffff)); +} + // -------------------------------------------------- FRAME PARSING -------------------------------------------------- // Generate reference data using PyCyphal: @@ -2364,6 +2379,8 @@ void tearDown(void) {} int main(void) { UNITY_BEGIN(); + // misc + RUN_TEST(testCompare32); // frame parser RUN_TEST(testParseFrameValidMessage); RUN_TEST(testParseFrameValidRPCService); From 1d2527450576113c155b47d718a8eab4c5ea9066 Mon Sep 17 00:00:00 2001 From: Pavel Kirienko Date: Wed, 2 Aug 2023 19:14:28 +0300 Subject: [PATCH 26/38] Remove the dependency on fixed-size 8-bit types to avoid portability restrictions; fixes https://github.com/OpenCyphal-Garage/libudpard/issues/42 --- libudpard/_udpard_cavl.h | 22 +++++++------- libudpard/udpard.c | 28 +++++++++--------- libudpard/udpard.h | 62 ++++++++++++++++++++-------------------- tests/src/hexdump.hpp | 18 ++++++------ tests/src/test_cavl.cpp | 58 ++++++++++++++++++------------------- 5 files changed, 95 insertions(+), 93 deletions(-) diff --git a/libudpard/_udpard_cavl.h b/libudpard/_udpard_cavl.h index 18b5e7a..e8fe2e2 100644 --- a/libudpard/_udpard_cavl.h +++ b/libudpard/_udpard_cavl.h @@ -47,7 +47,7 @@ typedef struct UdpardTreeNode Cavl; /// Returns POSITIVE if the search target is GREATER than the provided node, negative if smaller, zero on match (found). /// Values other than {-1, 0, +1} are not recommended to avoid overflow during the narrowing conversion of the result. -typedef int8_t (*CavlPredicate)(void* user_reference, const Cavl* node); +typedef int_fast8_t (*CavlPredicate)(void* user_reference, const Cavl* node); /// If provided, the factory will be invoked when the sought node does not exist in the tree. /// It is expected to return a new node that will be inserted immediately (without the need to traverse the tree again). @@ -117,13 +117,13 @@ static inline void cavlPrivateRotate(Cavl* const x, const bool r) static inline Cavl* cavlPrivateAdjustBalance(Cavl* const x, const bool increment) { CAVL_ASSERT((x != NULL) && ((x->bf >= -1) && (x->bf <= +1))); - Cavl* out = x; - const int8_t new_bf = (int8_t) (x->bf + (increment ? +1 : -1)); + Cavl* out = x; + const int_fast8_t new_bf = (int_fast8_t) (x->bf + (increment ? +1 : -1)); if ((new_bf < -1) || (new_bf > 1)) { - const bool r = new_bf < 0; // bf<0 if left-heavy --> right rotation is needed. - const int8_t sign = r ? +1 : -1; // Positive if we are rotating right. - Cavl* const z = x->lr[!r]; + const bool r = new_bf < 0; // bf<0 if left-heavy --> right rotation is needed. + const int_fast8_t sign = r ? +1 : -1; // Positive if we are rotating right. + Cavl* const z = x->lr[!r]; CAVL_ASSERT(z != NULL); // Heavy side cannot be empty. // NOLINTNEXTLINE(clang-analyzer-core.NullDereference) if ((z->bf * sign) <= 0) // Parent and child are heavy on the same side or the child is balanced. @@ -132,8 +132,8 @@ static inline Cavl* cavlPrivateAdjustBalance(Cavl* const x, const bool increment cavlPrivateRotate(x, r); if (0 == z->bf) { - x->bf = (int8_t) (-sign); - z->bf = (int8_t) (+sign); + x->bf = (int_fast8_t) (-sign); + z->bf = (int_fast8_t) (+sign); } else { @@ -150,7 +150,7 @@ static inline Cavl* cavlPrivateAdjustBalance(Cavl* const x, const bool increment cavlPrivateRotate(x, r); if ((y->bf * sign) < 0) { - x->bf = (int8_t) (+sign); + x->bf = (int_fast8_t) (+sign); y->bf = 0; z->bf = 0; } @@ -158,7 +158,7 @@ static inline Cavl* cavlPrivateAdjustBalance(Cavl* const x, const bool increment { x->bf = 0; y->bf = 0; - z->bf = (int8_t) (-sign); + z->bf = (int_fast8_t) (-sign); } else { @@ -209,7 +209,7 @@ static inline Cavl* cavlSearch(Cavl** const root, Cavl** n = root; while (*n != NULL) { - const int8_t cmp = predicate(user_reference, *n); + const int_fast8_t cmp = predicate(user_reference, *n); if (0 == cmp) { out = *n; diff --git a/libudpard/udpard.c b/libudpard/udpard.c index 5cbae9a..f672a0f 100644 --- a/libudpard/udpard.c +++ b/libudpard/udpard.c @@ -327,8 +327,8 @@ static inline TxItem* txNewItem(struct UdpardMemoryResource* const memory, /// Frames with identical weight are processed in the FIFO order. /// Frames with higher weight compare smaller (i.e., put on the left side of the tree). -static inline int8_t txAVLPredicate(void* const user_reference, // NOSONAR Cavl API requires pointer to non-const. - const struct UdpardTreeNode* const node) +static inline int_fast8_t txAVLPredicate(void* const user_reference, // NOSONAR Cavl API requires pointer to non-const. + const struct UdpardTreeNode* const node) { const TxItem* const target = (const TxItem*) user_reference; const TxItem* const other = (const TxItem*) (const void*) node; @@ -528,12 +528,12 @@ static inline int32_t txPush(struct UdpardTx* const tx, return out; } -int8_t udpardTxInit(struct UdpardTx* const self, - const UdpardNodeID* const local_node_id, - const size_t queue_capacity, - struct UdpardMemoryResource* const memory) +int_fast8_t udpardTxInit(struct UdpardTx* const self, + const UdpardNodeID* const local_node_id, + const size_t queue_capacity, + struct UdpardMemoryResource* const memory) { - int8_t ret = -UDPARD_ERROR_ARGUMENT; + int_fast8_t ret = -UDPARD_ERROR_ARGUMENT; if ((NULL != self) && (NULL != local_node_id) && memIsValid(memory)) { ret = 0; @@ -954,7 +954,7 @@ typedef struct struct UdpardMemoryResource* memory_fragment; } RxSlotUpdateContext; -static inline int8_t rxSlotFragmentSearch(void* const user_reference, const struct UdpardTreeNode* node) +static inline int_fast8_t rxSlotFragmentSearch(void* const user_reference, const struct UdpardTreeNode* node) { UDPARD_ASSERT((user_reference != NULL) && (node != NULL)); return compare32(((const RxSlotUpdateContext*) user_reference)->frame_index, @@ -1443,8 +1443,8 @@ typedef struct struct UdpardRxMemoryResources memory; } RxPortSessionSearchContext; -static inline int8_t rxPortSessionSearch(void* const user_reference, // NOSONAR non-const API - const struct UdpardTreeNode* node) +static inline int_fast8_t rxPortSessionSearch(void* const user_reference, // NOSONAR non-const API + const struct UdpardTreeNode* node) { UDPARD_ASSERT((user_reference != NULL) && (node != NULL)); return compare32(((const RxPortSessionSearchContext*) user_reference)->remote_node_id, @@ -1584,10 +1584,10 @@ void udpardFragmentFree(const struct UdpardFragment head, } } -int8_t udpardRxSubscriptionInit(struct UdpardRxSubscription* const self, - const UdpardPortID subject_id, - const size_t extent, - const struct UdpardRxMemoryResources memory) +int_fast8_t udpardRxSubscriptionInit(struct UdpardRxSubscription* const self, + const UdpardPortID subject_id, + const size_t extent, + const struct UdpardRxMemoryResources memory) { (void) self; (void) subject_id; diff --git a/libudpard/udpard.h b/libudpard/udpard.h index f78cfa9..1b74fa1 100644 --- a/libudpard/udpard.h +++ b/libudpard/udpard.h @@ -265,7 +265,7 @@ struct UdpardTreeNode { struct UdpardTreeNode* up; ///< Do not access this field. struct UdpardTreeNode* lr[2]; ///< Left and right children of this node may be accessed for tree traversal. - int8_t bf; ///< Do not access this field. + int_fast8_t bf; ///< Do not access this field. }; struct UdpardMutablePayload @@ -474,10 +474,10 @@ struct UdpardTxItem /// To safely discard it, simply pop all enqueued frames from it. /// /// The time complexity is constant. This function does not invoke the dynamic memory manager. -int8_t udpardTxInit(struct UdpardTx* const self, - const UdpardNodeID* const local_node_id, - const size_t queue_capacity, - struct UdpardMemoryResource* const memory); +int_fast8_t udpardTxInit(struct UdpardTx* const self, + const UdpardNodeID* const local_node_id, + const size_t queue_capacity, + struct UdpardMemoryResource* const memory); /// This function serializes a message transfer into a sequence of UDP datagrams and inserts them into the prioritized /// transmission queue at the appropriate position. Afterwards, the application is supposed to take the enqueued frames @@ -804,10 +804,10 @@ struct UdpardRxSubscription /// The return value is a negated UDPARD_ERROR_ARGUMENT if any of the input arguments are invalid. /// /// The time complexity is constant. This function does not invoke the dynamic memory manager. -int8_t udpardRxSubscriptionInit(struct UdpardRxSubscription* const self, - const UdpardPortID subject_id, - const size_t extent, - const struct UdpardRxMemoryResources memory); +int_fast8_t udpardRxSubscriptionInit(struct UdpardRxSubscription* const self, + const UdpardPortID subject_id, + const size_t extent, + const struct UdpardRxMemoryResources memory); /// Frees all memory held by the subscription instance. /// After invoking this function, the instance is no longer usable. @@ -862,11 +862,11 @@ void udpardRxSubscriptionFree(struct UdpardRxSubscription* const self); /// /// UDPARD_ERROR_MEMORY is returned if the function fails to allocate memory. /// UDPARD_ERROR_ARGUMENT is returned if any of the input arguments are invalid. -int8_t udpardRxSubscriptionReceive(struct UdpardRxSubscription* const self, - const UdpardMicrosecond timestamp_usec, - const struct UdpardMutablePayload datagram_payload, - const uint_fast8_t redundant_iface_index, - struct UdpardRxTransfer* const out_transfer); +int_fast8_t udpardRxSubscriptionReceive(struct UdpardRxSubscription* const self, + const UdpardMicrosecond timestamp_usec, + const struct UdpardMutablePayload datagram_payload, + const uint_fast8_t redundant_iface_index, + struct UdpardRxTransfer* const out_transfer); // --------------------------------------------- RPC-SERVICES --------------------------------------------- @@ -947,9 +947,9 @@ struct UdpardRxRPCTransfer /// The return value is a negated UDPARD_ERROR_ARGUMENT if any of the input arguments are invalid. /// /// The time complexity is constant. This function does not invoke the dynamic memory manager. -int8_t udpardRxRPCDispatcherInit(struct UdpardRxRPCDispatcher* const self, - const UdpardNodeID local_node_id, - const struct UdpardRxMemoryResources memory); +int_fast8_t udpardRxRPCDispatcherInit(struct UdpardRxRPCDispatcher* const self, + const UdpardNodeID local_node_id, + const struct UdpardRxMemoryResources memory); /// Frees all memory held by the RPC-service dispatcher instance. /// After invoking this function, the instance is no longer usable. @@ -977,11 +977,11 @@ void udpardRxRPCDispatcherFree(struct UdpardRxRPCDispatcher* const self); /// (request or response). /// This function does not allocate new memory. The function may deallocate memory if such registration already /// existed; the deallocation behavior is specified in the documentation for udpardRxRPCDispatcherCancel. -int8_t udpardRxRPCDispatcherListen(struct UdpardRxRPCDispatcher* const self, - struct UdpardRxRPC* const service, - const UdpardPortID service_id, - const bool is_request, - const size_t extent); +int_fast8_t udpardRxRPCDispatcherListen(struct UdpardRxRPCDispatcher* const self, + struct UdpardRxRPC* const service, + const UdpardPortID service_id, + const bool is_request, + const size_t extent); /// This function reverses the effect of udpardRxRPCDispatcherListen. /// If the registration is found, all its memory is de-allocated (session states and payload buffers). @@ -993,19 +993,19 @@ int8_t udpardRxRPCDispatcherListen(struct UdpardRxRPCDispatcher* const self, /// /// The time complexity is logarithmic from the number of current registration under the specified transfer kind. /// This function does not allocate new memory. -int8_t udpardRxRPCDispatcherCancel(struct UdpardRxRPCDispatcher* const self, - const UdpardPortID service_id, - const bool is_request); +int_fast8_t udpardRxRPCDispatcherCancel(struct UdpardRxRPCDispatcher* const self, + const UdpardPortID service_id, + const bool is_request); /// Datagrams received from the sockets of this service dispatcher are fed into this function. /// It is the analog of udpardRxSubscriptionReceive for RPC-service transfers. /// Please refer to the documentation of udpardRxSubscriptionReceive for the usage information. -int8_t udpardRxRPCDispatcherReceive(struct UdpardRxRPCDispatcher* const self, - struct UdpardRxRPC** const service, - const UdpardMicrosecond timestamp_usec, - const struct UdpardMutablePayload datagram_payload, - const uint_fast8_t redundant_iface_index, - struct UdpardRxRPCTransfer* const out_transfer); +int_fast8_t udpardRxRPCDispatcherReceive(struct UdpardRxRPCDispatcher* const self, + struct UdpardRxRPC** const service, + const UdpardMicrosecond timestamp_usec, + const struct UdpardMutablePayload datagram_payload, + const uint_fast8_t redundant_iface_index, + struct UdpardRxRPCTransfer* const out_transfer); #ifdef __cplusplus } diff --git a/tests/src/hexdump.hpp b/tests/src/hexdump.hpp index 3e44e63..d27bc83 100644 --- a/tests/src/hexdump.hpp +++ b/tests/src/hexdump.hpp @@ -11,14 +11,16 @@ namespace hexdump { -template +using Byte = std::uint_least8_t; + +template [[nodiscard]] std::string hexdump(InputIterator begin, const InputIterator end) { static_assert(BytesPerRow > 0); - static constexpr std::pair PrintableASCIIRange{32, 126}; - std::uint32_t offset = 0; - std::ostringstream output; - bool first = true; + static constexpr std::pair PrintableASCIIRange{32, 126}; + std::uint32_t offset = 0; + std::ostringstream output; + bool first = true; output << std::hex << std::setfill('0'); do { @@ -33,7 +35,7 @@ template output << std::setw(8) << offset << " "; offset += BytesPerRow; auto it = begin; - for (std::uint8_t i = 0; i < BytesPerRow; ++i) + for (Byte i = 0; i < BytesPerRow; ++i) { if (i == 8) { @@ -50,7 +52,7 @@ template } } output << " "; - for (std::uint8_t i = 0; i < BytesPerRow; ++i) + for (Byte i = 0; i < BytesPerRow; ++i) { if (begin != end) { @@ -76,6 +78,6 @@ template [[nodiscard]] inline auto hexdump(const void* const data, const std::size_t size) { - return hexdump(static_cast(data), static_cast(data) + size); + return hexdump(static_cast(data), static_cast(data) + size); } } // namespace hexdump diff --git a/tests/src/test_cavl.cpp b/tests/src/test_cavl.cpp index c944f09..8d816b6 100644 --- a/tests/src/test_cavl.cpp +++ b/tests/src/test_cavl.cpp @@ -31,10 +31,10 @@ struct Node final : Cavl T value{}; - auto checkLinkageUpLeftRightBF(const Cavl* const check_up, - const Cavl* const check_le, - const Cavl* const check_ri, - const std::int8_t check_bf) const -> bool + auto checkLinkageUpLeftRightBF(const Cavl* const check_up, + const Cavl* const check_le, + const Cavl* const check_ri, + const std::int_fast8_t check_bf) const -> bool { return (up == check_up) && // (lr[0] == check_le) && (lr[1] == check_ri) && // @@ -64,7 +64,7 @@ auto search(Node** const root, const Predicate& predicate, const Factory& fac Predicate predicate; Factory factory; - static auto callPredicate(void* const user_reference, const Cavl* const node) -> std::int8_t + static auto callPredicate(void* const user_reference, const Cavl* const node) -> std::int_fast8_t { const auto ret = static_cast(user_reference)->predicate(static_cast&>(*node)); if (ret > 0) @@ -101,20 +101,20 @@ void remove(Node** const root, const Node* const n) } template -auto getHeight(const Node* const n) -> std::uint8_t // NOLINT recursion +auto getHeight(const Node* const n) -> std::uint_fast8_t // NOLINT recursion { - return (n != nullptr) ? static_cast(1U + std::max(getHeight(static_cast*>(n->lr[0])), - getHeight(static_cast*>(n->lr[1])))) + return (n != nullptr) ? static_cast(1U + std::max(getHeight(static_cast*>(n->lr[0])), + getHeight(static_cast*>(n->lr[1])))) : 0; } template -void print(const Node* const nd, const std::uint8_t depth = 0, const char marker = 'T') // NOLINT recursion +void print(const Node* const nd, const std::uint_fast8_t depth = 0, const char marker = 'T') // NOLINT recursion { TEST_ASSERT(10 > getHeight(nd)); // Fail early for malformed cyclic trees, do not overwhelm stdout. if (nd != nullptr) { - print(static_cast*>(nd->lr[0]), static_cast(depth + 1U), 'L'); + print(static_cast*>(nd->lr[0]), static_cast(depth + 1U), 'L'); for (std::uint16_t i = 1U; i < depth; i++) { std::cout << " "; @@ -133,7 +133,7 @@ void print(const Node* const nd, const std::uint8_t depth = 0, const char mar } std::cout << marker << "=" << static_cast(nd->value) // << " [" << static_cast(nd->bf) << "]" << std::endl; - print(static_cast*>(nd->lr[1]), static_cast(depth + 1U), 'R'); + print(static_cast*>(nd->lr[1]), static_cast(depth + 1U), 'R'); } } @@ -211,7 +211,7 @@ auto findBrokenBalanceFactor(const Node* const n) -> const Cavl* // NOLINT r void testCheckAscension() { - using N = Node; + using N = Node; N t{2}; N l{1}; N r{3}; @@ -238,7 +238,7 @@ void testCheckAscension() void testRotation() { - using N = Node; + using N = Node; // Original state: // x.left = a // x.right = z @@ -284,7 +284,7 @@ void testRotation() void testBalancingA() { - using N = Node; + using N = Node; // Double left-right rotation. // X X Y // / ` / ` / ` @@ -334,7 +334,7 @@ void testBalancingA() void testBalancingB() { - using N = Node; + using N = Node; // Without F the handling of Z and Y is more complex; Z flips the sign of its balance factor: // X X Y // / ` / ` / ` @@ -383,7 +383,7 @@ void testBalancingB() void testBalancingC() { - using N = Node; + using N = Node; // Both X and Z are heavy on the same side. // X Z // / ` / ` @@ -434,9 +434,9 @@ void testBalancingC() void testRetracingOnGrowth() { - using N = Node; + using N = Node; std::array t{}; - for (std::uint8_t i = 0; i < 100; i++) + for (std::uint_fast8_t i = 0; i < 100; i++) { t[i].value = i; } @@ -640,7 +640,7 @@ void testRetracingOnGrowth() void testSearchTrivial() { - using N = Node; + using N = Node; // A // B C // D E F G @@ -684,7 +684,7 @@ void testSearchTrivial() void testRemovalA() { - using N = Node; + using N = Node; // 4 // / ` // 2 6 @@ -693,7 +693,7 @@ void testRemovalA() // / ` // 7 9 std::array t{}; - for (std::uint8_t i = 0; i < 10; i++) + for (std::uint_fast8_t i = 0; i < 10; i++) { t[i].value = i; } @@ -1005,7 +1005,7 @@ void testRemovalA() void testMutationManual() { - using N = Node; + using N = Node; // Build a tree with 31 elements from 1 to 31 inclusive by adding new elements successively: // 16 // / ` @@ -1017,13 +1017,13 @@ void testMutationManual() // / ` / ` / ` / ` / ` / ` / ` / ` // 1 3 5 7 9 11 13 15 17 19 21 23 25 27 29 31 std::array t{}; - for (std::uint8_t i = 0; i < 32; i++) + for (std::uint_fast8_t i = 0; i < 32; i++) { t[i].value = i; } // Build the actual tree. N* root = nullptr; - for (std::uint8_t i = 1; i < 32; i++) + for (std::uint_fast8_t i = 1; i < 32; i++) { const auto pred = [&](const N& v) { return t.at(i).value - v.value; }; TEST_ASSERT(nullptr == search(&root, pred)); @@ -1276,16 +1276,16 @@ void testMutationManual() auto getRandomByte() { - return static_cast((0xFFLL * std::rand()) / RAND_MAX); + return static_cast((0xFFLL * std::rand()) / RAND_MAX); } void testMutationRandomized() { - using N = Node; + using N = Node; std::array t{}; for (auto i = 0U; i < 256U; i++) { - t.at(i).value = static_cast(i); + t.at(i).value = static_cast(i); } std::array mask{}; std::size_t size = 0; @@ -1307,7 +1307,7 @@ void testMutationRandomized() }; validate(); - const auto add = [&](const std::uint8_t x) { + const auto add = [&](const std::uint_fast8_t x) { const auto predicate = [&](const N& v) { return x - v.value; }; if (N* const existing = search(&root, predicate)) { @@ -1332,7 +1332,7 @@ void testMutationRandomized() } }; - const auto drop = [&](const std::uint8_t x) { + const auto drop = [&](const std::uint_fast8_t x) { const auto predicate = [&](const N& v) { return x - v.value; }; if (N* const existing = search(&root, predicate)) { From 11b7b975eec2857fee85a376bf9240db369d7a45 Mon Sep 17 00:00:00 2001 From: Pavel Kirienko Date: Wed, 2 Aug 2023 19:17:54 +0300 Subject: [PATCH 27/38] SONAR --- libudpard/udpard.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libudpard/udpard.c b/libudpard/udpard.c index f672a0f..7f0c4df 100644 --- a/libudpard/udpard.c +++ b/libudpard/udpard.c @@ -954,7 +954,8 @@ typedef struct struct UdpardMemoryResource* memory_fragment; } RxSlotUpdateContext; -static inline int_fast8_t rxSlotFragmentSearch(void* const user_reference, const struct UdpardTreeNode* node) +static inline int_fast8_t rxSlotFragmentSearch(void* const user_reference, // NOSONAR Cavl API requires non-const. + const struct UdpardTreeNode* node) { UDPARD_ASSERT((user_reference != NULL) && (node != NULL)); return compare32(((const RxSlotUpdateContext*) user_reference)->frame_index, From 75dfe91b1f6ca306a5110c2984186d6c5a97e8e3 Mon Sep 17 00:00:00 2001 From: Pavel Kirienko Date: Thu, 3 Aug 2023 14:43:28 +0300 Subject: [PATCH 28/38] Fix a minor naming consistency issue: udpardFragmentFree -> udpardRxFragmentFree --- libudpard/udpard.c | 6 +++--- libudpard/udpard.h | 12 +++++------ tests/src/test_intrusive_rx.c | 38 +++++++++++++++++------------------ 3 files changed, 28 insertions(+), 28 deletions(-) diff --git a/libudpard/udpard.c b/libudpard/udpard.c index 7f0c4df..f2e41ec 100644 --- a/libudpard/udpard.c +++ b/libudpard/udpard.c @@ -1574,9 +1574,9 @@ static inline void rxPortFree(struct UdpardRxPort* const self, const struct Udpa // -------------------------------------------------- RX API -------------------------------------------------- -void udpardFragmentFree(const struct UdpardFragment head, - struct UdpardMemoryResource* const memory_fragment, - struct UdpardMemoryResource* const memory_payload) +void udpardRxFragmentFree(const struct UdpardFragment head, + struct UdpardMemoryResource* const memory_fragment, + struct UdpardMemoryResource* const memory_payload) { if ((memory_fragment != NULL) && (memory_payload != NULL)) // The head is not heap-allocated so not freed. { diff --git a/libudpard/udpard.h b/libudpard/udpard.h index 1b74fa1..8d789e5 100644 --- a/libudpard/udpard.h +++ b/libudpard/udpard.h @@ -286,9 +286,9 @@ struct UdpardPayload /// The model is as follows: /// /// (payload header) ---> UdpardFragment: -/// next ---> UdpardFragment... -/// owner ---> (the free()able payload data buffer) -/// view ---> (somewhere inside the payload data buffer) +/// next ---> UdpardFragment... +/// origin ---> (the free()able payload data buffer) +/// view ---> (somewhere inside the payload data buffer) /// /// Payloads of received transfers are represented using this type, where each fragment corresponds to a frame. /// The application can either consume them directly or to copy the data into a contiguous buffer beforehand @@ -745,9 +745,9 @@ struct UdpardRxTransfer /// design, where the head is stored by value to reduce indirection in small transfers. We call it Scott's Head. /// /// If any of the arguments are NULL, the function has no effect. -void udpardFragmentFree(const struct UdpardFragment head, - struct UdpardMemoryResource* const memory_fragment, - struct UdpardMemoryResource* const memory_payload); +void udpardRxFragmentFree(const struct UdpardFragment head, + struct UdpardMemoryResource* const memory_fragment, + struct UdpardMemoryResource* const memory_payload); // --------------------------------------------- SUBJECTS --------------------------------------------- diff --git a/tests/src/test_intrusive_rx.c b/tests/src/test_intrusive_rx.c index c5391a7..c8e0ac1 100644 --- a/tests/src/test_intrusive_rx.c +++ b/tests/src/test_intrusive_rx.c @@ -470,7 +470,7 @@ static void testSlotEjectValidLarge(void) TEST_ASSERT_EQUAL(3, mem_fragment.allocated_fragments); // One gone!!1 TEST_ASSERT_EQUAL(sizeof(RxFragment) * 3, mem_fragment.allocated_bytes); // yes yes! // Now, free the payload as the application would. - udpardFragmentFree(payload, &mem_fragment.base, &mem_payload.base); + udpardRxFragmentFree(payload, &mem_fragment.base, &mem_payload.base); // All memory shall be free now. As in "free beer". TEST_ASSERT_EQUAL(0, mem_payload.allocated_fragments); TEST_ASSERT_EQUAL(0, mem_payload.allocated_bytes); @@ -540,7 +540,7 @@ static void testSlotEjectValidSmall(void) TEST_ASSERT_EQUAL(2, mem_fragment.allocated_fragments); // One gone!!1 TEST_ASSERT_EQUAL(sizeof(RxFragment) * 2, mem_fragment.allocated_bytes); // Now, free the payload as the application would. - udpardFragmentFree(payload, &mem_fragment.base, &mem_payload.base); + udpardRxFragmentFree(payload, &mem_fragment.base, &mem_payload.base); // All memory shall be free now. As in "free beer". TEST_ASSERT_EQUAL(0, mem_payload.allocated_fragments); TEST_ASSERT_EQUAL(0, mem_payload.allocated_bytes); @@ -585,7 +585,7 @@ static void testSlotEjectValidEmpty(void) TEST_ASSERT_EQUAL(0, mem_fragment.allocated_fragments); TEST_ASSERT_EQUAL(0, mem_fragment.allocated_bytes); // Now, free the payload as the application would. - udpardFragmentFree(payload, &mem_fragment.base, &mem_payload.base); + udpardRxFragmentFree(payload, &mem_fragment.base, &mem_payload.base); // No memory is in use anyway, so no change here. TEST_ASSERT_EQUAL(0, mem_payload.allocated_fragments); TEST_ASSERT_EQUAL(0, mem_payload.allocated_bytes); @@ -672,7 +672,7 @@ static void testSlotAcceptA(void) TEST_ASSERT_EQUAL(53, payload_size); TEST_ASSERT(compareStringWithPayload("The fish responsible for drying the sea are not here.", payload.view)); TEST_ASSERT_NULL(payload.next); - udpardFragmentFree(payload, &mem_fragment.base, &mem_payload.base); + udpardRxFragmentFree(payload, &mem_fragment.base, &mem_payload.base); TEST_ASSERT_EQUAL(0, mem_payload.allocated_fragments); TEST_ASSERT_EQUAL(0, mem_payload.allocated_bytes); TEST_ASSERT_EQUAL(0, mem_fragment.allocated_fragments); @@ -735,7 +735,7 @@ static void testSlotAcceptA(void) TEST_ASSERT(compareStringWithPayload("They moved from one dark forest to another dark forest.", // payload.next->next->view)); TEST_ASSERT_NULL(payload.next->next->next); - udpardFragmentFree(payload, &mem_fragment.base, &mem_payload.base); + udpardRxFragmentFree(payload, &mem_fragment.base, &mem_payload.base); TEST_ASSERT_EQUAL(0, mem_payload.allocated_fragments); TEST_ASSERT_EQUAL(0, mem_payload.allocated_bytes); TEST_ASSERT_EQUAL(0, mem_fragment.allocated_fragments); @@ -824,7 +824,7 @@ static void testSlotAcceptA(void) TEST_ASSERT_NOT_NULL(payload.next); TEST_ASSERT(compareStringWithPayload("How do we give it", payload.next->view)); // TRUNCATED TEST_ASSERT_NULL(payload.next->next); - udpardFragmentFree(payload, &mem_fragment.base, &mem_payload.base); + udpardRxFragmentFree(payload, &mem_fragment.base, &mem_payload.base); TEST_ASSERT_EQUAL(0, mem_payload.allocated_fragments); TEST_ASSERT_EQUAL(0, mem_payload.allocated_bytes); TEST_ASSERT_EQUAL(0, mem_fragment.allocated_fragments); @@ -940,7 +940,7 @@ static void testSlotAcceptA(void) TEST_ASSERT_NOT_NULL(payload.next->next); TEST_ASSERT(compareStringWithPayload("Toss it over.", payload.next->next->view)); TEST_ASSERT_NULL(payload.next->next->next); - udpardFragmentFree(payload, &mem_fragment.base, &mem_payload.base); + udpardRxFragmentFree(payload, &mem_fragment.base, &mem_payload.base); TEST_ASSERT_EQUAL(0, mem_payload.allocated_fragments); TEST_ASSERT_EQUAL(0, mem_payload.allocated_bytes); TEST_ASSERT_EQUAL(0, mem_fragment.allocated_fragments); @@ -1218,7 +1218,7 @@ static void testIfaceAcceptA(void) TEST_ASSERT_EQUAL(0x1122334455667788U, transfer.transfer_id); TEST_ASSERT_EQUAL(12, transfer.payload_size); TEST_ASSERT(compareStringWithPayload("I am a tomb.", transfer.payload.view)); - udpardFragmentFree(transfer.payload, &mem_fragment.base, &mem_payload.base); + udpardRxFragmentFree(transfer.payload, &mem_fragment.base, &mem_payload.base); TEST_ASSERT_EQUAL(0, mem_payload.allocated_fragments); TEST_ASSERT_EQUAL(0, mem_fragment.allocated_fragments); // Check the internal states of the iface. @@ -1411,7 +1411,7 @@ static void testIfaceAcceptA(void) TEST_ASSERT_NOT_NULL(transfer.payload.next); TEST_ASSERT(compareStringWithPayload("B1", transfer.payload.next->view)); TEST_ASSERT_NULL(transfer.payload.next->next); - udpardFragmentFree(transfer.payload, &mem_fragment.base, &mem_payload.base); + udpardRxFragmentFree(transfer.payload, &mem_fragment.base, &mem_payload.base); TEST_ASSERT_EQUAL(2, mem_payload.allocated_fragments); // Only the remaining A0 A2 are left. TEST_ASSERT_EQUAL(2, mem_fragment.allocated_fragments); // A1 @@ -1447,7 +1447,7 @@ static void testIfaceAcceptA(void) TEST_ASSERT_NOT_NULL(transfer.payload.next->next); TEST_ASSERT(compareStringWithPayload("A2", transfer.payload.next->next->view)); TEST_ASSERT_NULL(transfer.payload.next->next->next); - udpardFragmentFree(transfer.payload, &mem_fragment.base, &mem_payload.base); + udpardRxFragmentFree(transfer.payload, &mem_fragment.base, &mem_payload.base); TEST_ASSERT_EQUAL(0, mem_payload.allocated_fragments); TEST_ASSERT_EQUAL(0, mem_fragment.allocated_fragments); } @@ -1625,7 +1625,7 @@ static void testIfaceAcceptB(void) TEST_ASSERT_NOT_NULL(transfer.payload.next->next); TEST_ASSERT(compareStringWithPayload("A2", transfer.payload.next->next->view)); TEST_ASSERT_NULL(transfer.payload.next->next->next); - udpardFragmentFree(transfer.payload, &mem_fragment.base, &mem_payload.base); + udpardRxFragmentFree(transfer.payload, &mem_fragment.base, &mem_payload.base); TEST_ASSERT_EQUAL(1, mem_payload.allocated_fragments); // Some memory is retained for the C0 payload. TEST_ASSERT_EQUAL(1, mem_fragment.allocated_fragments); // C0 DUPLICATE @@ -1684,7 +1684,7 @@ static void testIfaceAcceptB(void) TEST_ASSERT_NOT_NULL(transfer.payload.next); TEST_ASSERT(compareStringWithPayload("C1", transfer.payload.next->view)); TEST_ASSERT_NULL(transfer.payload.next->next); - udpardFragmentFree(transfer.payload, &mem_fragment.base, &mem_payload.base); + udpardRxFragmentFree(transfer.payload, &mem_fragment.base, &mem_payload.base); TEST_ASSERT_EQUAL(0, mem_payload.allocated_fragments); // Some memory is retained for the C0 payload. TEST_ASSERT_EQUAL(0, mem_fragment.allocated_fragments); } @@ -1792,7 +1792,7 @@ static void testIfaceAcceptC(void) TEST_ASSERT_NOT_NULL(transfer.payload.next); TEST_ASSERT(compareStringWithPayload("A1", transfer.payload.next->view)); TEST_ASSERT_NULL(transfer.payload.next->next); - udpardFragmentFree(transfer.payload, &mem_fragment.base, &mem_payload.base); + udpardRxFragmentFree(transfer.payload, &mem_fragment.base, &mem_payload.base); TEST_ASSERT_EQUAL(1, mem_payload.allocated_fragments); // B0 still allocated. TEST_ASSERT_EQUAL(1, mem_fragment.allocated_fragments); // C0 @@ -1847,7 +1847,7 @@ static void testIfaceAcceptC(void) TEST_ASSERT_NOT_NULL(transfer.payload.next); TEST_ASSERT(compareStringWithPayload("B1", transfer.payload.next->view)); TEST_ASSERT_NULL(transfer.payload.next->next); - udpardFragmentFree(transfer.payload, &mem_fragment.base, &mem_payload.base); + udpardRxFragmentFree(transfer.payload, &mem_fragment.base, &mem_payload.base); TEST_ASSERT_EQUAL(1, mem_payload.allocated_fragments); // C0 is still allocated. TEST_ASSERT_EQUAL(1, mem_fragment.allocated_fragments); // C1 @@ -1883,7 +1883,7 @@ static void testIfaceAcceptC(void) TEST_ASSERT_NOT_NULL(transfer.payload.next); TEST_ASSERT(compareStringWithPayload("C1", transfer.payload.next->view)); TEST_ASSERT_NULL(transfer.payload.next->next); - udpardFragmentFree(transfer.payload, &mem_fragment.base, &mem_payload.base); + udpardRxFragmentFree(transfer.payload, &mem_fragment.base, &mem_payload.base); TEST_ASSERT_EQUAL(0, mem_payload.allocated_fragments); TEST_ASSERT_EQUAL(0, mem_fragment.allocated_fragments); // B0 duplicate multi-frame; shall be rejected. @@ -2055,7 +2055,7 @@ static void testSessionAcceptA(void) TEST_ASSERT_EQUAL(1, mem_payload.allocated_fragments); TEST_ASSERT_EQUAL(0, mem_fragment.allocated_fragments); // Free the payload. - udpardFragmentFree(transfer.payload, &mem_fragment.base, &mem_payload.base); + udpardRxFragmentFree(transfer.payload, &mem_fragment.base, &mem_payload.base); TEST_ASSERT_EQUAL(0, mem_payload.allocated_fragments); TEST_ASSERT_EQUAL(0, mem_fragment.allocated_fragments); // Send the same transfer again through a different iface; it is a duplicate and so it is rejected and freed. @@ -2151,7 +2151,7 @@ static inline void testPortAcceptFrameA(void) "Solar System into two dimensions cease?", transfer.payload.view)); // Free the memory. - udpardFragmentFree(transfer.payload, &mem_fragment.base, &mem_payload.base); + udpardRxFragmentFree(transfer.payload, &mem_fragment.base, &mem_payload.base); TEST_ASSERT_EQUAL(1, mem_session.allocated_fragments); // The session remains. TEST_ASSERT_EQUAL(0, mem_fragment.allocated_fragments); TEST_ASSERT_EQUAL(0, mem_payload.allocated_fragments); @@ -2182,7 +2182,7 @@ static inline void testPortAcceptFrameA(void) TEST_ASSERT_EQUAL(20, transfer.payload_size); TEST_ASSERT(compareStringWithPayload("It will never cease.", transfer.payload.view)); // Free the memory. - udpardFragmentFree(transfer.payload, &mem_fragment.base, &mem_payload.base); + udpardRxFragmentFree(transfer.payload, &mem_fragment.base, &mem_payload.base); TEST_ASSERT_EQUAL(2, mem_session.allocated_fragments); // The sessions remain. TEST_ASSERT_EQUAL(0, mem_fragment.allocated_fragments); TEST_ASSERT_EQUAL(0, mem_payload.allocated_fragments); @@ -2234,7 +2234,7 @@ static inline void testPortAcceptFrameA(void) TEST_ASSERT_EQUAL(20, transfer.payload_size); TEST_ASSERT(compareStringWithPayload("Cheng Xin shuddered.", transfer.payload.view)); // Free the memory. - udpardFragmentFree(transfer.payload, &mem_fragment.base, &mem_payload.base); + udpardRxFragmentFree(transfer.payload, &mem_fragment.base, &mem_payload.base); TEST_ASSERT_EQUAL(2, mem_session.allocated_fragments); // The sessions remain. TEST_ASSERT_EQUAL(0, mem_fragment.allocated_fragments); TEST_ASSERT_EQUAL(0, mem_payload.allocated_fragments); From 1d87ac863df660bdd0c1e7b59acee17b21caa7b9 Mon Sep 17 00:00:00 2001 From: Pavel Kirienko Date: Tue, 8 Aug 2023 13:30:55 +0300 Subject: [PATCH 29/38] Implement UdpardMemoryDeleter; switch MemoryResource and MemoryDeleter to value semantics #sonar --- libudpard/udpard.c | 96 +++++------ libudpard/udpard.h | 69 ++++---- tests/.idea/dictionaries/pavel.xml | 6 +- tests/src/helpers.h | 44 ++--- tests/src/test_helpers.c | 22 +-- tests/src/test_intrusive_rx.c | 260 +++++++++++++++-------------- tests/src/test_intrusive_tx.c | 195 ++++++++++++---------- tests/src/test_tx.cpp | 67 ++++---- 8 files changed, 395 insertions(+), 364 deletions(-) diff --git a/libudpard/udpard.c b/libudpard/udpard.c index f2e41ec..934b5d6 100644 --- a/libudpard/udpard.c +++ b/libudpard/udpard.c @@ -124,26 +124,22 @@ static inline int_fast8_t compare32(const uint32_t a, const uint32_t b) return result; } -static inline bool memIsValid(const struct UdpardMemoryResource* const memory) +static inline void* memAlloc(const struct UdpardMemoryResource memory, const size_t size) { - return (memory != NULL) && (memory->allocate != NULL) && (memory->free != NULL); + UDPARD_ASSERT(memory.allocate != NULL); + return memory.allocate(memory.user_reference, size); } -static inline void* memAlloc(struct UdpardMemoryResource* const memory, const size_t size) +static inline void memFree(const struct UdpardMemoryResource memory, const size_t size, void* const data) { - UDPARD_ASSERT((memory != NULL) && (memory->allocate != NULL)); - return memory->allocate(memory, size); + UDPARD_ASSERT(memory.free != NULL); + memory.free(memory.user_reference, size, data); } -/// Free as in freedom. -static inline void memFree(struct UdpardMemoryResource* const memory, const size_t size, void* const data) +static inline void memFreePayload(const struct UdpardMemoryDeleter memory, const struct UdpardMutablePayload payload) { - UDPARD_ASSERT((memory != NULL) && (memory->free != NULL)); // allocate() may be NULL in the payload mem resource. - memory->free(memory, size, data); -} -static inline void memFreePayload(struct UdpardMemoryResource* const memory, const struct UdpardMutablePayload payload) -{ - memFree(memory, payload.size, payload.data); + UDPARD_ASSERT(memory.free != NULL); + memory.free(memory.user_reference, payload.size, payload.data); } static inline void memZero(const size_t size, void* const data) @@ -293,15 +289,14 @@ typedef struct size_t count; } TxChain; -static inline TxItem* txNewItem(struct UdpardMemoryResource* const memory, - const uint_least8_t dscp_value_per_priority[UDPARD_PRIORITY_MAX + 1U], - const UdpardMicrosecond deadline_usec, - const enum UdpardPriority priority, - const struct UdpardUDPIPEndpoint endpoint, - const size_t datagram_payload_size, - void* const user_transfer_reference) +static inline TxItem* txNewItem(const struct UdpardMemoryResource memory, + const uint_least8_t dscp_value_per_priority[UDPARD_PRIORITY_MAX + 1U], + const UdpardMicrosecond deadline_usec, + const enum UdpardPriority priority, + const struct UdpardUDPIPEndpoint endpoint, + const size_t datagram_payload_size, + void* const user_transfer_reference) { - UDPARD_ASSERT(memory != NULL); TxItem* const out = (TxItem*) memAlloc(memory, sizeof(TxItem) + datagram_payload_size); if (out != NULL) { @@ -391,16 +386,15 @@ static inline byte_t* txSerializeHeader(byte_t* const destination_buffe /// Produces a chain of Tx queue items for later insertion into the Tx queue. The tail is NULL if OOM. /// The caller is responsible for freeing the memory allocated for the chain. -static inline TxChain txMakeChain(struct UdpardMemoryResource* const memory, - const uint_least8_t dscp_value_per_priority[UDPARD_PRIORITY_MAX + 1U], - const size_t mtu, - const UdpardMicrosecond deadline_usec, - const TransferMetadata meta, - const struct UdpardUDPIPEndpoint endpoint, - const struct UdpardPayload payload, - void* const user_transfer_reference) -{ - UDPARD_ASSERT(memory != NULL); +static inline TxChain txMakeChain(const struct UdpardMemoryResource memory, + const uint_least8_t dscp_value_per_priority[UDPARD_PRIORITY_MAX + 1U], + const size_t mtu, + const UdpardMicrosecond deadline_usec, + const TransferMetadata meta, + const struct UdpardUDPIPEndpoint endpoint, + const struct UdpardPayload payload, + void* const user_transfer_reference) +{ UDPARD_ASSERT(mtu > 0); UDPARD_ASSERT((payload.data != NULL) || (payload.size == 0U)); const size_t payload_size_with_crc = payload.size + TRANSFER_CRC_SIZE_BYTES; @@ -528,13 +522,13 @@ static inline int32_t txPush(struct UdpardTx* const tx, return out; } -int_fast8_t udpardTxInit(struct UdpardTx* const self, - const UdpardNodeID* const local_node_id, - const size_t queue_capacity, - struct UdpardMemoryResource* const memory) +int_fast8_t udpardTxInit(struct UdpardTx* const self, + const UdpardNodeID* const local_node_id, + const size_t queue_capacity, + const struct UdpardMemoryResource memory) { int_fast8_t ret = -UDPARD_ERROR_ARGUMENT; - if ((NULL != self) && (NULL != local_node_id) && memIsValid(memory)) + if ((NULL != self) && (NULL != local_node_id) && (memory.allocate != NULL) && (memory.free != NULL)) { ret = 0; memZero(sizeof(*self), self); @@ -682,9 +676,9 @@ struct UdpardTxItem* udpardTxPop(struct UdpardTx* const self, const struct Udpar return out; } -void udpardTxFree(struct UdpardMemoryResource* const memory, struct UdpardTxItem* const item) +void udpardTxFree(const struct UdpardMemoryResource memory, struct UdpardTxItem* const item) { - if ((memory != NULL) && (item != NULL)) + if (item != NULL) { memFree(memory, sizeof(TxItem) + item->datagram_payload.size, item); } @@ -801,8 +795,8 @@ static inline bool rxParseFrame(const struct UdpardMutablePayload datagram_paylo /// as they almost always go side by side. typedef struct { - struct UdpardMemoryResource* fragment; - struct UdpardMemoryResource* payload; + struct UdpardMemoryResource fragment; + struct UdpardMemoryDeleter payload; } RxMemory; typedef struct @@ -949,9 +943,9 @@ static inline void rxSlotRestart(RxSlot* const self, const UdpardTransferID tran typedef struct { - uint32_t frame_index; - bool accepted; - struct UdpardMemoryResource* memory_fragment; + uint32_t frame_index; + bool accepted; + struct UdpardMemoryResource memory_fragment; } RxSlotUpdateContext; static inline int_fast8_t rxSlotFragmentSearch(void* const user_reference, // NOSONAR Cavl API requires non-const. @@ -965,7 +959,7 @@ static inline int_fast8_t rxSlotFragmentSearch(void* const user_reference, // N static inline struct UdpardTreeNode* rxSlotFragmentFactory(void* const user_reference) { RxSlotUpdateContext* const ctx = (RxSlotUpdateContext*) user_reference; - UDPARD_ASSERT((ctx != NULL) && memIsValid(ctx->memory_fragment)); + UDPARD_ASSERT((ctx != NULL) && (ctx->memory_fragment.allocate != NULL) && (ctx->memory_fragment.free != NULL)); struct UdpardTreeNode* out = NULL; RxFragment* const frag = memAlloc(ctx->memory_fragment, sizeof(RxFragment)); if (frag != NULL) @@ -1574,15 +1568,13 @@ static inline void rxPortFree(struct UdpardRxPort* const self, const struct Udpa // -------------------------------------------------- RX API -------------------------------------------------- -void udpardRxFragmentFree(const struct UdpardFragment head, - struct UdpardMemoryResource* const memory_fragment, - struct UdpardMemoryResource* const memory_payload) +void udpardRxFragmentFree(const struct UdpardFragment head, + const struct UdpardMemoryResource memory_fragment, + const struct UdpardMemoryDeleter memory_payload) { - if ((memory_fragment != NULL) && (memory_payload != NULL)) // The head is not heap-allocated so not freed. - { - memFreePayload(memory_payload, head.origin); // May be NULL, is okay. - rxFragmentDestroyList(head.next, (RxMemory){.fragment = memory_fragment, .payload = memory_payload}); - } + // The head is not heap-allocated so not freed. + memFreePayload(memory_payload, head.origin); // May be NULL, is okay. + rxFragmentDestroyList(head.next, (RxMemory){.fragment = memory_fragment, .payload = memory_payload}); } int_fast8_t udpardRxSubscriptionInit(struct UdpardRxSubscription* const self, diff --git a/libudpard/udpard.h b/libudpard/udpard.h index 8d789e5..8fc6062 100644 --- a/libudpard/udpard.h +++ b/libudpard/udpard.h @@ -320,17 +320,22 @@ struct UdpardUDPIPEndpoint // ================================================= MEMORY RESOURCE ================================================= // ===================================================================================================================== -struct UdpardMemoryResource; - /// A pointer to the memory allocation function. The semantics are similar to malloc(): /// - The returned pointer shall point to an uninitialized block of memory that is at least "size" bytes large. /// - If there is not enough memory, the returned pointer shall be NULL. /// - The memory shall be aligned at least at max_align_t. /// - The execution time should be constant (O(1)). /// - The worst-case memory consumption (worst fragmentation) should be understood by the developer. +/// /// If the standard dynamic memory manager of the target platform does not satisfy the above requirements, -/// consider using O1Heap: https://github.com/pavel-kirienko/o1heap. -typedef void* (*UdpardMemoryAllocate)(struct UdpardMemoryResource* const self, const size_t size); +/// consider using O1Heap: https://github.com/pavel-kirienko/o1heap. Alternatively, some applications may prefer to +/// use a set of fixed-size block pool allocators (see the high-level overview for details). +/// +/// The time complexity models given in the API documentation are made on the assumption that the memory management +/// functions have constant complexity O(1). +/// +/// The value of the user reference is taken from the corresponding field of the memory resource structure. +typedef void* (*UdpardMemoryAllocate)(void* const user_reference, const size_t size); /// The counterpart of the above -- this function is invoked to return previously allocated memory to the allocator. /// The size argument contains the amount of memory that was originally requested via the allocation function. @@ -338,20 +343,27 @@ typedef void* (*UdpardMemoryAllocate)(struct UdpardMemoryResource* const self, c /// - The pointer was previously returned by the allocation function. /// - The pointer may be NULL, in which case the function shall have no effect. /// - The execution time should be constant (O(1)). -typedef void (*UdpardMemoryFree)(struct UdpardMemoryResource* const self, const size_t size, void* const pointer); +/// +/// The value of the user reference is taken from the corresponding field of the memory resource structure. +typedef void (*UdpardMemoryFree)(void* const user_reference, const size_t size, void* const pointer); + +/// A kind of memory resource that can only be used to free memory previously allocated by the user. +/// Instances are mostly intended to be passed by value. +struct UdpardMemoryDeleter +{ + void* user_reference; ///< Passed as the first argument. + UdpardMemoryFree free; ///< Shall be a valid pointer. +}; /// A memory resource encapsulates the dynamic memory allocation and deallocation facilities. -/// The time complexity models given in the API documentation are made on the assumption that the memory management -/// functions have constant complexity O(1). /// Note that the library allocates a large amount of small fixed-size objects for bookkeeping purposes; -/// allocators for them can be implemented using fixed-size block pools to eliminate memory fragmentation. +/// allocators for them can be implemented using fixed-size block pools to eliminate extrinsic memory fragmentation. +/// Instances are mostly intended to be passed by value. struct UdpardMemoryResource { - /// The function pointers shall be valid at all times. - UdpardMemoryAllocate allocate; - UdpardMemoryFree free; - /// This is an opaque pointer that can be freely utilized by the user for arbitrary needs. - void* user_reference; + void* user_reference; ///< Passed as the first argument. + UdpardMemoryFree free; ///< Shall be a valid pointer. + UdpardMemoryAllocate allocate; ///< Shall be a valid pointer. }; // ===================================================================================================================== @@ -413,8 +425,8 @@ struct UdpardTx /// There is exactly one allocation per enqueued item, each allocation contains both the UdpardTxItem /// and its payload, hence the size is variable. /// In a simple application there would be just one memory resource shared by all parts of the library. - /// If the application knows its MTU, it can use block allocation to avoid fragmentation. - struct UdpardMemoryResource* memory; + /// If the application knows its MTU, it can use block allocation to avoid extrinsic fragmentation. + struct UdpardMemoryResource memory; /// The number of frames that are currently contained in the queue, initially zero. /// READ-ONLY @@ -474,10 +486,10 @@ struct UdpardTxItem /// To safely discard it, simply pop all enqueued frames from it. /// /// The time complexity is constant. This function does not invoke the dynamic memory manager. -int_fast8_t udpardTxInit(struct UdpardTx* const self, - const UdpardNodeID* const local_node_id, - const size_t queue_capacity, - struct UdpardMemoryResource* const memory); +int_fast8_t udpardTxInit(struct UdpardTx* const self, + const UdpardNodeID* const local_node_id, + const size_t queue_capacity, + const struct UdpardMemoryResource memory); /// This function serializes a message transfer into a sequence of UDP datagrams and inserts them into the prioritized /// transmission queue at the appropriate position. Afterwards, the application is supposed to take the enqueued frames @@ -615,8 +627,8 @@ struct UdpardTxItem* udpardTxPop(struct UdpardTx* const self, const struct Udpar /// This is a simple helper that frees the memory allocated for the item with the correct size. /// It is needed because the application does not have access to the required context to compute the size. /// If the chosen allocator does not leverage the size information, the deallocation function can be invoked directly. -/// If any of the arguments are NULL, the function has no effect. The time complexity is constant. -void udpardTxFree(struct UdpardMemoryResource* const memory, struct UdpardTxItem* const item); +/// If the item argument is NULL, the function has no effect. The time complexity is constant. +void udpardTxFree(const struct UdpardMemoryResource memory, struct UdpardTxItem* const item); // ===================================================================================================================== // ================================================= RX PIPELINE ================================================= @@ -697,18 +709,17 @@ struct UdpardRxMemoryResources { /// The session memory resource is used to provide memory for the session instances described above. /// Each instance is fixed-size, so a trivial zero-fragmentation block allocator is sufficient. - struct UdpardMemoryResource* session; + struct UdpardMemoryResource session; /// The fragment handles are allocated per payload fragment; each handle contains a pointer to its fragment. /// Each instance is of a very small fixed size, so a trivial zero-fragmentation block allocator is sufficient. - struct UdpardMemoryResource* fragment; + struct UdpardMemoryResource fragment; /// The library never allocates payload buffers itself, as they are handed over by the application via /// udpardRx*Receive. Once a buffer is handed over, the library may choose to keep it if it is deemed to be /// necessary to complete a transfer reassembly, or to discard it if it is deemed to be unnecessary. - /// Discarded payload buffers are freed using this memory resource. - /// As this resource is never used to allocate memory, the "allocate" pointer can be NULL. - struct UdpardMemoryResource* payload; + /// Discarded payload buffers are freed using this object. + struct UdpardMemoryDeleter payload; }; /// Represents a received Cyphal transfer. @@ -745,9 +756,9 @@ struct UdpardRxTransfer /// design, where the head is stored by value to reduce indirection in small transfers. We call it Scott's Head. /// /// If any of the arguments are NULL, the function has no effect. -void udpardRxFragmentFree(const struct UdpardFragment head, - struct UdpardMemoryResource* const memory_fragment, - struct UdpardMemoryResource* const memory_payload); +void udpardRxFragmentFree(const struct UdpardFragment head, + struct UdpardMemoryResource const memory_fragment, + struct UdpardMemoryDeleter const memory_payload); // --------------------------------------------- SUBJECTS --------------------------------------------- diff --git a/tests/.idea/dictionaries/pavel.xml b/tests/.idea/dictionaries/pavel.xml index 19317bd..9c14631 100644 --- a/tests/.idea/dictionaries/pavel.xml +++ b/tests/.idea/dictionaries/pavel.xml @@ -4,16 +4,19 @@ baremetal cavl cfamily + cyphal deallocation deduplicator discardment dscp + dsdl dudpard ffee ghcr iface ifaces intravehicular + kirienko libcanard libgtest libudpard @@ -23,6 +26,7 @@ mmcu nosonar opencyphal + pard prio profraw pycyphal @@ -40,4 +44,4 @@ zzzzzz - + \ No newline at end of file diff --git a/tests/src/helpers.h b/tests/src/helpers.h index 394c8b9..291d785 100644 --- a/tests/src/helpers.h +++ b/tests/src/helpers.h @@ -38,17 +38,17 @@ extern "C" { } \ } while (0) -static inline void* dummyAllocatorAllocate(struct UdpardMemoryResource* const self, const size_t size) +static inline void* dummyAllocatorAllocate(void* const user_reference, const size_t size) { - (void) self; + (void) user_reference; (void) size; return NULL; } -static inline void dummyAllocatorFree(struct UdpardMemoryResource* const self, const size_t size, void* const pointer) +static inline void dummyAllocatorFree(void* const user_reference, const size_t size, void* const pointer) { + (void) user_reference; (void) size; - TEST_PANIC_UNLESS(self != NULL); TEST_PANIC_UNLESS(pointer == NULL); } @@ -57,8 +57,7 @@ static inline void dummyAllocatorFree(struct UdpardMemoryResource* const self, c #define INSTRUMENTED_ALLOCATOR_CANARY_SIZE 1024U typedef struct { - struct UdpardMemoryResource base; - uint_least8_t canary[INSTRUMENTED_ALLOCATOR_CANARY_SIZE]; + uint_least8_t canary[INSTRUMENTED_ALLOCATOR_CANARY_SIZE]; /// The limit can be changed at any moment to control the maximum amount of memory that can be allocated. /// It may be set to a value less than the currently allocated amount. size_t limit_fragments; @@ -68,11 +67,10 @@ typedef struct size_t allocated_bytes; } InstrumentedAllocator; -static inline void* instrumentedAllocatorAllocate(struct UdpardMemoryResource* const base, const size_t size) +static inline void* instrumentedAllocatorAllocate(void* const user_reference, const size_t size) { - InstrumentedAllocator* const self = (InstrumentedAllocator*) base; - TEST_PANIC_UNLESS(self->base.allocate == &instrumentedAllocatorAllocate); - void* result = NULL; + InstrumentedAllocator* const self = (InstrumentedAllocator*) user_reference; + void* result = NULL; if ((size > 0U) && // ((self->allocated_bytes + size) <= self->limit_bytes) && // ((self->allocated_fragments + 1U) <= self->limit_fragments)) @@ -101,13 +99,9 @@ static inline void* instrumentedAllocatorAllocate(struct UdpardMemoryResource* c return result; } -static inline void instrumentedAllocatorFree(struct UdpardMemoryResource* const base, - const size_t size, - void* const pointer) +static inline void instrumentedAllocatorFree(void* const user_reference, const size_t size, void* const pointer) { - InstrumentedAllocator* const self = (InstrumentedAllocator*) base; - TEST_PANIC_UNLESS(self->base.allocate == &instrumentedAllocatorAllocate); - TEST_PANIC_UNLESS(self->base.free == &instrumentedAllocatorFree); + InstrumentedAllocator* const self = (InstrumentedAllocator*) user_reference; if (pointer != NULL) { uint_least8_t* p = ((uint_least8_t*) pointer) - INSTRUMENTED_ALLOCATOR_CANARY_SIZE; @@ -138,9 +132,6 @@ static inline void instrumentedAllocatorFree(struct UdpardMemoryResource* const /// By default, the limit is unrestricted (set to the maximum possible value). static inline void instrumentedAllocatorNew(InstrumentedAllocator* const self) { - self->base.allocate = &instrumentedAllocatorAllocate; - self->base.free = &instrumentedAllocatorFree; - self->base.user_reference = NULL; for (size_t i = 0; i < INSTRUMENTED_ALLOCATOR_CANARY_SIZE; i++) { self->canary[i] = (uint_least8_t) (rand() % (UINT_LEAST8_MAX + 1)); @@ -151,6 +142,21 @@ static inline void instrumentedAllocatorNew(InstrumentedAllocator* const self) self->allocated_bytes = 0U; } +static inline struct UdpardMemoryResource instrumentedAllocatorMakeMemoryResource( + const InstrumentedAllocator* const self) +{ + const struct UdpardMemoryResource out = {.user_reference = (void*) self, + .free = &instrumentedAllocatorFree, + .allocate = &instrumentedAllocatorAllocate}; + return out; +} + +static inline struct UdpardMemoryDeleter instrumentedAllocatorMakeMemoryDeleter(const InstrumentedAllocator* const self) +{ + const struct UdpardMemoryDeleter out = {.user_reference = (void*) self, .free = &instrumentedAllocatorFree}; + return out; +} + #ifdef __cplusplus } #endif diff --git a/tests/src/test_helpers.c b/tests/src/test_helpers.c index 752869b..e14d5a3 100644 --- a/tests/src/test_helpers.c +++ b/tests/src/test_helpers.c @@ -11,47 +11,49 @@ static void testInstrumentedAllocator(void) TEST_ASSERT_EQUAL_size_t(0, al.allocated_fragments); TEST_ASSERT_EQUAL_size_t(SIZE_MAX, al.limit_bytes); - void* a = al.base.allocate(&al.base, 123); + const struct UdpardMemoryResource resource = instrumentedAllocatorMakeMemoryResource(&al); + + void* a = resource.allocate(resource.user_reference, 123); TEST_ASSERT_EQUAL_size_t(1, al.allocated_fragments); TEST_ASSERT_EQUAL_size_t(123, al.allocated_bytes); - void* b = al.base.allocate(&al.base, 456); + void* b = resource.allocate(resource.user_reference, 456); TEST_ASSERT_EQUAL_size_t(2, al.allocated_fragments); TEST_ASSERT_EQUAL_size_t(579, al.allocated_bytes); al.limit_bytes = 600; al.limit_fragments = 2; - TEST_ASSERT_EQUAL_PTR(NULL, al.base.allocate(&al.base, 100)); + TEST_ASSERT_EQUAL_PTR(NULL, resource.allocate(resource.user_reference, 100)); TEST_ASSERT_EQUAL_size_t(2, al.allocated_fragments); TEST_ASSERT_EQUAL_size_t(579, al.allocated_bytes); - TEST_ASSERT_EQUAL_PTR(NULL, al.base.allocate(&al.base, 21)); + TEST_ASSERT_EQUAL_PTR(NULL, resource.allocate(resource.user_reference, 21)); TEST_ASSERT_EQUAL_size_t(2, al.allocated_fragments); TEST_ASSERT_EQUAL_size_t(579, al.allocated_bytes); al.limit_fragments = 4; - void* c = al.base.allocate(&al.base, 21); + void* c = resource.allocate(resource.user_reference, 21); TEST_ASSERT_EQUAL_size_t(3, al.allocated_fragments); TEST_ASSERT_EQUAL_size_t(600, al.allocated_bytes); - al.base.free(&al.base, 123, a); + resource.free(resource.user_reference, 123, a); TEST_ASSERT_EQUAL_size_t(2, al.allocated_fragments); TEST_ASSERT_EQUAL_size_t(477, al.allocated_bytes); - void* d = al.base.allocate(&al.base, 100); + void* d = resource.allocate(resource.user_reference, 100); TEST_ASSERT_EQUAL_size_t(3, al.allocated_fragments); TEST_ASSERT_EQUAL_size_t(577, al.allocated_bytes); - al.base.free(&al.base, 21, c); + resource.free(resource.user_reference, 21, c); TEST_ASSERT_EQUAL_size_t(2, al.allocated_fragments); TEST_ASSERT_EQUAL_size_t(556, al.allocated_bytes); - al.base.free(&al.base, 100, d); + resource.free(resource.user_reference, 100, d); TEST_ASSERT_EQUAL_size_t(1, al.allocated_fragments); TEST_ASSERT_EQUAL_size_t(456, al.allocated_bytes); - al.base.free(&al.base, 456, b); + resource.free(resource.user_reference, 456, b); TEST_ASSERT_EQUAL_size_t(0, al.allocated_fragments); TEST_ASSERT_EQUAL_size_t(0, al.allocated_bytes); } diff --git a/tests/src/test_intrusive_rx.c b/tests/src/test_intrusive_rx.c index c8e0ac1..ec34aa9 100644 --- a/tests/src/test_intrusive_rx.c +++ b/tests/src/test_intrusive_rx.c @@ -19,7 +19,7 @@ static RxFragment* makeRxFragment(const RxMemory memory, { TEST_PANIC_UNLESS((view.data >= origin.data) && (view.size <= origin.size)); TEST_PANIC_UNLESS((((const byte_t*) view.data) + view.size) <= (((const byte_t*) origin.data) + origin.size)); - byte_t* const new_origin = (byte_t*) memAlloc(memory.payload, origin.size); + byte_t* const new_origin = (byte_t*) instrumentedAllocatorAllocate(memory.payload.user_reference, origin.size); RxFragment* const frag = (RxFragment*) memAlloc(memory.fragment, sizeof(RxFragment)); if ((new_origin != NULL) && (frag != NULL)) { @@ -68,16 +68,16 @@ static bool compareStringWithPayload(const char* const expected, const struct Ud return compareMemory(strlen(expected), expected, payload.size, payload.data); } -static RxFrameBase makeRxFrameBase(struct UdpardMemoryResource* const memory_payload, - const uint32_t frame_index, - const bool end_of_transfer, - const struct UdpardPayload view, - const struct UdpardMutablePayload origin) +static RxFrameBase makeRxFrameBase(InstrumentedAllocator* const memory_payload, + const uint32_t frame_index, + const bool end_of_transfer, + const struct UdpardPayload view, + const struct UdpardMutablePayload origin) { TEST_PANIC_UNLESS((view.data >= origin.data) && (view.size <= origin.size)); TEST_PANIC_UNLESS((((const byte_t*) view.data) + view.size) <= (((const byte_t*) origin.data) + origin.size)); RxFrameBase out = {0}; - byte_t* const new_origin = (byte_t*) memAlloc(memory_payload, origin.size); + byte_t* const new_origin = (byte_t*) instrumentedAllocatorAllocate(memory_payload, origin.size); if (new_origin != NULL) { (void) memmove(new_origin, origin.data, origin.size); @@ -95,41 +95,41 @@ static RxFrameBase makeRxFrameBase(struct UdpardMemoryResource* const memory_pay return out; } -static RxFrameBase makeRxFrameBaseString(struct UdpardMemoryResource* const memory_payload, - const uint32_t frame_index, - const bool end_of_transfer, - const char* const payload) +static RxFrameBase makeRxFrameBaseString(InstrumentedAllocator* const memory, + const uint32_t frame_index, + const bool end_of_transfer, + const char* const payload) { - return makeRxFrameBase(memory_payload, + return makeRxFrameBase(memory, frame_index, end_of_transfer, (struct UdpardPayload){.data = payload, .size = strlen(payload)}, (struct UdpardMutablePayload){.data = (void*) payload, .size = strlen(payload)}); } -static RxFrame makeRxFrameString(struct UdpardMemoryResource* const memory_payload, - const TransferMetadata meta, - const uint32_t frame_index, - const bool end_of_transfer, - const char* const payload) +static RxFrame makeRxFrameString(InstrumentedAllocator* const memory, + const TransferMetadata meta, + const uint32_t frame_index, + const bool end_of_transfer, + const char* const payload) { - return (RxFrame){.base = makeRxFrameBaseString(memory_payload, frame_index, end_of_transfer, payload), - .meta = meta}; + return (RxFrame){.base = makeRxFrameBaseString(memory, frame_index, end_of_transfer, payload), .meta = meta}; } static RxMemory makeRxMemory(InstrumentedAllocator* const fragment, InstrumentedAllocator* const payload) { - return (RxMemory){.fragment = &fragment->base, .payload = &payload->base}; + return (RxMemory){.fragment = instrumentedAllocatorMakeMemoryResource(fragment), + .payload = instrumentedAllocatorMakeMemoryDeleter(payload)}; } -static struct UdpardMutablePayload makeDatagramPayload(struct UdpardMemoryResource* const memory, - const TransferMetadata meta, - const uint32_t frame_index, - const bool end_of_transfer, - const struct UdpardPayload payload) +static struct UdpardMutablePayload makeDatagramPayload(InstrumentedAllocator* const memory, + const TransferMetadata meta, + const uint32_t frame_index, + const bool end_of_transfer, + const struct UdpardPayload payload) { struct UdpardMutablePayload pld = {.size = payload.size + HEADER_SIZE_BYTES}; - pld.data = memAlloc(memory, pld.size); + pld.data = instrumentedAllocatorAllocate(memory, pld.size); if (pld.data != NULL) { (void) memcpy(txSerializeHeader(pld.data, meta, frame_index, end_of_transfer), payload.data, payload.size); @@ -141,11 +141,11 @@ static struct UdpardMutablePayload makeDatagramPayload(struct UdpardMemoryResour return pld; } -static struct UdpardMutablePayload makeDatagramPayloadString(struct UdpardMemoryResource* const memory, - const TransferMetadata meta, - const uint32_t frame_index, - const bool end_of_transfer, - const char* const string) +static struct UdpardMutablePayload makeDatagramPayloadString(InstrumentedAllocator* const memory, + const TransferMetadata meta, + const uint32_t frame_index, + const bool end_of_transfer, + const char* const string) { return makeDatagramPayload(memory, meta, @@ -154,9 +154,9 @@ static struct UdpardMutablePayload makeDatagramPayloadString(struct UdpardMemory (struct UdpardPayload){.data = string, .size = strlen(string)}); } -static struct UdpardMutablePayload makeDatagramPayloadSingleFrame(struct UdpardMemoryResource* const memory, - const TransferMetadata meta, - const struct UdpardPayload payload) +static struct UdpardMutablePayload makeDatagramPayloadSingleFrame(InstrumentedAllocator* const memory, + const TransferMetadata meta, + const struct UdpardPayload payload) { struct UdpardMutablePayload pld = makeDatagramPayload(memory, @@ -171,9 +171,9 @@ static struct UdpardMutablePayload makeDatagramPayloadSingleFrame(struct UdpardM return pld; } -static struct UdpardMutablePayload makeDatagramPayloadSingleFrameString(struct UdpardMemoryResource* const memory, - const TransferMetadata meta, - const char* const payload) +static struct UdpardMutablePayload makeDatagramPayloadSingleFrameString(InstrumentedAllocator* const memory, + const TransferMetadata meta, + const char* const payload) { return makeDatagramPayloadSingleFrame(memory, meta, @@ -470,7 +470,7 @@ static void testSlotEjectValidLarge(void) TEST_ASSERT_EQUAL(3, mem_fragment.allocated_fragments); // One gone!!1 TEST_ASSERT_EQUAL(sizeof(RxFragment) * 3, mem_fragment.allocated_bytes); // yes yes! // Now, free the payload as the application would. - udpardRxFragmentFree(payload, &mem_fragment.base, &mem_payload.base); + udpardRxFragmentFree(payload, mem.fragment, mem.payload); // All memory shall be free now. As in "free beer". TEST_ASSERT_EQUAL(0, mem_payload.allocated_fragments); TEST_ASSERT_EQUAL(0, mem_payload.allocated_bytes); @@ -540,7 +540,7 @@ static void testSlotEjectValidSmall(void) TEST_ASSERT_EQUAL(2, mem_fragment.allocated_fragments); // One gone!!1 TEST_ASSERT_EQUAL(sizeof(RxFragment) * 2, mem_fragment.allocated_bytes); // Now, free the payload as the application would. - udpardRxFragmentFree(payload, &mem_fragment.base, &mem_payload.base); + udpardRxFragmentFree(payload, mem.fragment, mem.payload); // All memory shall be free now. As in "free beer". TEST_ASSERT_EQUAL(0, mem_payload.allocated_fragments); TEST_ASSERT_EQUAL(0, mem_payload.allocated_bytes); @@ -585,7 +585,7 @@ static void testSlotEjectValidEmpty(void) TEST_ASSERT_EQUAL(0, mem_fragment.allocated_fragments); TEST_ASSERT_EQUAL(0, mem_fragment.allocated_bytes); // Now, free the payload as the application would. - udpardRxFragmentFree(payload, &mem_fragment.base, &mem_payload.base); + udpardRxFragmentFree(payload, mem.fragment, mem.payload); // No memory is in use anyway, so no change here. TEST_ASSERT_EQUAL(0, mem_payload.allocated_fragments); TEST_ASSERT_EQUAL(0, mem_payload.allocated_bytes); @@ -656,7 +656,7 @@ static void testSlotAcceptA(void) rxSlotAccept(&slot, &payload_size, &payload, - makeRxFrameBaseString(&mem_payload.base, + makeRxFrameBaseString(&mem_payload, 0, true, "The fish responsible for drying the sea are not here." @@ -672,7 +672,7 @@ static void testSlotAcceptA(void) TEST_ASSERT_EQUAL(53, payload_size); TEST_ASSERT(compareStringWithPayload("The fish responsible for drying the sea are not here.", payload.view)); TEST_ASSERT_NULL(payload.next); - udpardRxFragmentFree(payload, &mem_fragment.base, &mem_payload.base); + udpardRxFragmentFree(payload, mem.fragment, mem.payload); TEST_ASSERT_EQUAL(0, mem_payload.allocated_fragments); TEST_ASSERT_EQUAL(0, mem_payload.allocated_bytes); TEST_ASSERT_EQUAL(0, mem_fragment.allocated_fragments); @@ -692,7 +692,7 @@ static void testSlotAcceptA(void) rxSlotAccept(&slot, &payload_size, &payload, - makeRxFrameBaseString(&mem_payload.base, + makeRxFrameBaseString(&mem_payload, 0, false, "We're sorry. What you said is really hard to understand.\n"), @@ -702,7 +702,7 @@ static void testSlotAcceptA(void) rxSlotAccept(&slot, &payload_size, &payload, - makeRxFrameBaseString(&mem_payload.base, + makeRxFrameBaseString(&mem_payload, 1, false, "The fish who dried the sea went onto land before they did " @@ -713,7 +713,7 @@ static void testSlotAcceptA(void) rxSlotAccept(&slot, &payload_size, &payload, - makeRxFrameBaseString(&mem_payload.base, + makeRxFrameBaseString(&mem_payload, 2, true, "They moved from one dark forest to another dark forest." @@ -735,7 +735,7 @@ static void testSlotAcceptA(void) TEST_ASSERT(compareStringWithPayload("They moved from one dark forest to another dark forest.", // payload.next->next->view)); TEST_ASSERT_NULL(payload.next->next->next); - udpardRxFragmentFree(payload, &mem_fragment.base, &mem_payload.base); + udpardRxFragmentFree(payload, mem.fragment, mem.payload); TEST_ASSERT_EQUAL(0, mem_payload.allocated_fragments); TEST_ASSERT_EQUAL(0, mem_payload.allocated_bytes); TEST_ASSERT_EQUAL(0, mem_fragment.allocated_fragments); @@ -757,7 +757,7 @@ static void testSlotAcceptA(void) rxSlotAccept(&slot, &payload_size, &payload, - makeRxFrameBaseString(&mem_payload.base, // + makeRxFrameBaseString(&mem_payload, // 2, true, "Toss it over." @@ -770,7 +770,7 @@ static void testSlotAcceptA(void) rxSlotAccept(&slot, &payload_size, &payload, - makeRxFrameBaseString(&mem_payload.base, // + makeRxFrameBaseString(&mem_payload, // 1, false, "How do we give it to you?\n"), @@ -782,7 +782,7 @@ static void testSlotAcceptA(void) rxSlotAccept(&slot, &payload_size, &payload, - makeRxFrameBaseString(&mem_payload.base, // + makeRxFrameBaseString(&mem_payload, // 1, false, "DUPLICATE #1"), @@ -794,7 +794,7 @@ static void testSlotAcceptA(void) rxSlotAccept(&slot, &payload_size, &payload, - makeRxFrameBaseString(&mem_payload.base, // + makeRxFrameBaseString(&mem_payload, // 2, true, "DUPLICATE #2"), @@ -806,7 +806,7 @@ static void testSlotAcceptA(void) rxSlotAccept(&slot, &payload_size, &payload, - makeRxFrameBaseString(&mem_payload.base, // + makeRxFrameBaseString(&mem_payload, // 0, false, "I like fish. Can I have it?\n"), @@ -824,7 +824,7 @@ static void testSlotAcceptA(void) TEST_ASSERT_NOT_NULL(payload.next); TEST_ASSERT(compareStringWithPayload("How do we give it", payload.next->view)); // TRUNCATED TEST_ASSERT_NULL(payload.next->next); - udpardRxFragmentFree(payload, &mem_fragment.base, &mem_payload.base); + udpardRxFragmentFree(payload, mem.fragment, mem.payload); TEST_ASSERT_EQUAL(0, mem_payload.allocated_fragments); TEST_ASSERT_EQUAL(0, mem_payload.allocated_bytes); TEST_ASSERT_EQUAL(0, mem_fragment.allocated_fragments); @@ -844,7 +844,7 @@ static void testSlotAcceptA(void) rxSlotAccept(&slot, &payload_size, &payload, - makeRxFrameBaseString(&mem_payload.base, 0, true, ":D"), + makeRxFrameBaseString(&mem_payload, 0, true, ":D"), 1000, mem)); TEST_ASSERT_EQUAL(0, mem_payload.allocated_fragments); @@ -870,7 +870,7 @@ static void testSlotAcceptA(void) rxSlotAccept(&slot, &payload_size, &payload, - makeRxFrameBaseString(&mem_payload.base, // + makeRxFrameBaseString(&mem_payload, // 2, true, "Toss it over." @@ -883,7 +883,7 @@ static void testSlotAcceptA(void) rxSlotAccept(&slot, &payload_size, &payload, - makeRxFrameBaseString(&mem_payload.base, // + makeRxFrameBaseString(&mem_payload, // 1, false, "How do we give it to you?\n"), @@ -896,7 +896,7 @@ static void testSlotAcceptA(void) rxSlotAccept(&slot, &payload_size, &payload, - makeRxFrameBaseString(&mem_payload.base, // + makeRxFrameBaseString(&mem_payload, // 0, false, "I like fish. Can I have it?\n"), @@ -908,7 +908,7 @@ static void testSlotAcceptA(void) rxSlotAccept(&slot, &payload_size, &payload, - makeRxFrameBaseString(&mem_payload.base, // + makeRxFrameBaseString(&mem_payload, // 1, false, "How do we give it to you?\n"), @@ -921,7 +921,7 @@ static void testSlotAcceptA(void) rxSlotAccept(&slot, &payload_size, &payload, - makeRxFrameBaseString(&mem_payload.base, // + makeRxFrameBaseString(&mem_payload, // 1, false, "How do we give it to you?\n"), @@ -940,7 +940,7 @@ static void testSlotAcceptA(void) TEST_ASSERT_NOT_NULL(payload.next->next); TEST_ASSERT(compareStringWithPayload("Toss it over.", payload.next->next->view)); TEST_ASSERT_NULL(payload.next->next->next); - udpardRxFragmentFree(payload, &mem_fragment.base, &mem_payload.base); + udpardRxFragmentFree(payload, mem.fragment, mem.payload); TEST_ASSERT_EQUAL(0, mem_payload.allocated_fragments); TEST_ASSERT_EQUAL(0, mem_payload.allocated_bytes); TEST_ASSERT_EQUAL(0, mem_fragment.allocated_fragments); @@ -962,7 +962,7 @@ static void testSlotAcceptA(void) rxSlotAccept(&slot, &payload_size, &payload, - makeRxFrameBaseString(&mem_payload.base, // + makeRxFrameBaseString(&mem_payload, // 2, true, "Toss it over." @@ -975,9 +975,9 @@ static void testSlotAcceptA(void) rxSlotAccept(&slot, &payload_size, &payload, - makeRxFrameBaseString(&mem_payload.base, // - 1, // - true, // SURPRISE! EOT is set in distinct frames! + makeRxFrameBaseString(&mem_payload, // + 1, // + true, // SURPRISE! EOT is set in distinct frames! "How do we give it to you?\n"), 45, mem)); @@ -1000,7 +1000,7 @@ static void testSlotAcceptA(void) rxSlotAccept(&slot, &payload_size, &payload, - makeRxFrameBaseString(&mem_payload.base, // + makeRxFrameBaseString(&mem_payload, // 2, true, "Toss it over." @@ -1013,8 +1013,8 @@ static void testSlotAcceptA(void) rxSlotAccept(&slot, &payload_size, &payload, - makeRxFrameBaseString(&mem_payload.base, // - 3, // SURPRISE! Frame #3 while #2 was EOT! + makeRxFrameBaseString(&mem_payload, // + 3, // SURPRISE! Frame #3 while #2 was EOT! false, "How do we give it to you?\n"), 45, @@ -1195,7 +1195,7 @@ static void testIfaceAcceptA(void) TEST_ASSERT_EQUAL(1, rxIfaceAccept(&iface, 1234567890, - makeRxFrameString(&mem_payload.base, // + makeRxFrameString(&mem_payload, // (TransferMetadata){.priority = UdpardPriorityHigh, .src_node_id = 1234, .dst_node_id = UDPARD_NODE_ID_UNSET, @@ -1218,7 +1218,7 @@ static void testIfaceAcceptA(void) TEST_ASSERT_EQUAL(0x1122334455667788U, transfer.transfer_id); TEST_ASSERT_EQUAL(12, transfer.payload_size); TEST_ASSERT(compareStringWithPayload("I am a tomb.", transfer.payload.view)); - udpardRxFragmentFree(transfer.payload, &mem_fragment.base, &mem_payload.base); + udpardRxFragmentFree(transfer.payload, mem.fragment, mem.payload); TEST_ASSERT_EQUAL(0, mem_payload.allocated_fragments); TEST_ASSERT_EQUAL(0, mem_fragment.allocated_fragments); // Check the internal states of the iface. @@ -1230,8 +1230,8 @@ static void testIfaceAcceptA(void) // Send a duplicate and ensure it is rejected. TEST_ASSERT_EQUAL(0, // No transfer accepted. rxIfaceAccept(&iface, - 1234567891, // different timestamp but ignored anyway - makeRxFrameString(&mem_payload.base, // + 1234567891, // different timestamp but ignored anyway + makeRxFrameString(&mem_payload, // (TransferMetadata){.priority = UdpardPriorityHigh, .src_node_id = 1234, .dst_node_id = UDPARD_NODE_ID_UNSET, @@ -1256,8 +1256,8 @@ static void testIfaceAcceptA(void) // Send a non-duplicate transfer with an invalid CRC using an in-sequence (matching) transfer-ID. TEST_ASSERT_EQUAL(0, // No transfer accepted. rxIfaceAccept(&iface, - 1234567892, // different timestamp but ignored anyway - makeRxFrameString(&mem_payload.base, // + 1234567892, // different timestamp but ignored anyway + makeRxFrameString(&mem_payload, // (TransferMetadata){.priority = UdpardPriorityHigh, .src_node_id = 1234, .dst_node_id = UDPARD_NODE_ID_UNSET, @@ -1283,8 +1283,8 @@ static void testIfaceAcceptA(void) // Transfer-ID jumps forward, no existing slot; will use the second one. TEST_ASSERT_EQUAL(0, // No transfer accepted. rxIfaceAccept(&iface, - 1234567893, // different timestamp but ignored anyway - makeRxFrameString(&mem_payload.base, // + 1234567893, // different timestamp but ignored anyway + makeRxFrameString(&mem_payload, // (TransferMetadata){.priority = UdpardPriorityHigh, .src_node_id = 1234, .dst_node_id = UDPARD_NODE_ID_UNSET, @@ -1314,7 +1314,7 @@ static void testIfaceAcceptA(void) TEST_ASSERT_EQUAL(0, rxIfaceAccept(&iface, 2000000020, - makeRxFrameString(&mem_payload.base, // + makeRxFrameString(&mem_payload, // (TransferMetadata){.priority = UdpardPriorityHigh, .src_node_id = 1111, .dst_node_id = UDPARD_NODE_ID_UNSET, @@ -1336,8 +1336,8 @@ static void testIfaceAcceptA(void) // B1 TEST_ASSERT_EQUAL(0, rxIfaceAccept(&iface, - 2000000010, // Transfer-ID timeout. - makeRxFrameString(&mem_payload.base, // + 2000000010, // Transfer-ID timeout. + makeRxFrameString(&mem_payload, // (TransferMetadata){.priority = UdpardPrioritySlow, .src_node_id = 2222, .dst_node_id = UDPARD_NODE_ID_UNSET, @@ -1360,7 +1360,7 @@ static void testIfaceAcceptA(void) TEST_ASSERT_EQUAL(0, rxIfaceAccept(&iface, 2000000030, - makeRxFrameString(&mem_payload.base, // + makeRxFrameString(&mem_payload, // (TransferMetadata){.priority = UdpardPriorityHigh, .src_node_id = 1111, .dst_node_id = UDPARD_NODE_ID_UNSET, @@ -1382,7 +1382,7 @@ static void testIfaceAcceptA(void) TEST_ASSERT_EQUAL(1, rxIfaceAccept(&iface, 2000000040, - makeRxFrameString(&mem_payload.base, // + makeRxFrameString(&mem_payload, // (TransferMetadata){.priority = UdpardPrioritySlow, .src_node_id = 2222, .dst_node_id = UDPARD_NODE_ID_UNSET, @@ -1411,14 +1411,14 @@ static void testIfaceAcceptA(void) TEST_ASSERT_NOT_NULL(transfer.payload.next); TEST_ASSERT(compareStringWithPayload("B1", transfer.payload.next->view)); TEST_ASSERT_NULL(transfer.payload.next->next); - udpardRxFragmentFree(transfer.payload, &mem_fragment.base, &mem_payload.base); + udpardRxFragmentFree(transfer.payload, mem.fragment, mem.payload); TEST_ASSERT_EQUAL(2, mem_payload.allocated_fragments); // Only the remaining A0 A2 are left. TEST_ASSERT_EQUAL(2, mem_fragment.allocated_fragments); // A1 TEST_ASSERT_EQUAL(1, rxIfaceAccept(&iface, 2000000050, - makeRxFrameString(&mem_payload.base, // + makeRxFrameString(&mem_payload, // (TransferMetadata){.priority = UdpardPriorityHigh, .src_node_id = 1111, .dst_node_id = UDPARD_NODE_ID_UNSET, @@ -1447,7 +1447,7 @@ static void testIfaceAcceptA(void) TEST_ASSERT_NOT_NULL(transfer.payload.next->next); TEST_ASSERT(compareStringWithPayload("A2", transfer.payload.next->next->view)); TEST_ASSERT_NULL(transfer.payload.next->next->next); - udpardRxFragmentFree(transfer.payload, &mem_fragment.base, &mem_payload.base); + udpardRxFragmentFree(transfer.payload, mem.fragment, mem.payload); TEST_ASSERT_EQUAL(0, mem_payload.allocated_fragments); TEST_ASSERT_EQUAL(0, mem_fragment.allocated_fragments); } @@ -1484,7 +1484,7 @@ static void testIfaceAcceptB(void) TEST_ASSERT_EQUAL(0, rxIfaceAccept(&iface, 2000000020, - makeRxFrameString(&mem_payload.base, // + makeRxFrameString(&mem_payload, // (TransferMetadata){.priority = UdpardPriorityHigh, .src_node_id = 1111, .dst_node_id = UDPARD_NODE_ID_UNSET, @@ -1506,8 +1506,8 @@ static void testIfaceAcceptB(void) // B1 TEST_ASSERT_EQUAL(0, rxIfaceAccept(&iface, - 2000000010, // Transfer-ID timeout. - makeRxFrameString(&mem_payload.base, // + 2000000010, // Transfer-ID timeout. + makeRxFrameString(&mem_payload, // (TransferMetadata){.priority = UdpardPrioritySlow, .src_node_id = 2222, .dst_node_id = UDPARD_NODE_ID_UNSET, @@ -1530,7 +1530,7 @@ static void testIfaceAcceptB(void) TEST_ASSERT_EQUAL(0, rxIfaceAccept(&iface, 2000000030, - makeRxFrameString(&mem_payload.base, // + makeRxFrameString(&mem_payload, // (TransferMetadata){.priority = UdpardPriorityHigh, .src_node_id = 1111, .dst_node_id = UDPARD_NODE_ID_UNSET, @@ -1552,7 +1552,7 @@ static void testIfaceAcceptB(void) TEST_ASSERT_EQUAL(0, rxIfaceAccept(&iface, 2000000040, - makeRxFrameString(&mem_payload.base, // + makeRxFrameString(&mem_payload, // (TransferMetadata){.priority = UdpardPriorityHigh, .src_node_id = 3333, .dst_node_id = UDPARD_NODE_ID_UNSET, @@ -1574,7 +1574,7 @@ static void testIfaceAcceptB(void) TEST_ASSERT_EQUAL(0, // Cannot be accepted because its slot is taken over by C. rxIfaceAccept(&iface, 2000000050, - makeRxFrameString(&mem_payload.base, // + makeRxFrameString(&mem_payload, // (TransferMetadata){.priority = UdpardPrioritySlow, .src_node_id = 2222, .dst_node_id = UDPARD_NODE_ID_UNSET, @@ -1596,7 +1596,7 @@ static void testIfaceAcceptB(void) TEST_ASSERT_EQUAL(1, rxIfaceAccept(&iface, 2000000050, - makeRxFrameString(&mem_payload.base, // + makeRxFrameString(&mem_payload, // (TransferMetadata){.priority = UdpardPriorityHigh, .src_node_id = 1111, .dst_node_id = UDPARD_NODE_ID_UNSET, @@ -1625,14 +1625,14 @@ static void testIfaceAcceptB(void) TEST_ASSERT_NOT_NULL(transfer.payload.next->next); TEST_ASSERT(compareStringWithPayload("A2", transfer.payload.next->next->view)); TEST_ASSERT_NULL(transfer.payload.next->next->next); - udpardRxFragmentFree(transfer.payload, &mem_fragment.base, &mem_payload.base); + udpardRxFragmentFree(transfer.payload, mem.fragment, mem.payload); TEST_ASSERT_EQUAL(1, mem_payload.allocated_fragments); // Some memory is retained for the C0 payload. TEST_ASSERT_EQUAL(1, mem_fragment.allocated_fragments); // C0 DUPLICATE TEST_ASSERT_EQUAL(0, rxIfaceAccept(&iface, 2000000060, - makeRxFrameString(&mem_payload.base, // + makeRxFrameString(&mem_payload, // (TransferMetadata){.priority = UdpardPriorityHigh, .src_node_id = 3333, .dst_node_id = UDPARD_NODE_ID_UNSET, @@ -1654,7 +1654,7 @@ static void testIfaceAcceptB(void) TEST_ASSERT_EQUAL(1, rxIfaceAccept(&iface, 2000000070, - makeRxFrameString(&mem_payload.base, // + makeRxFrameString(&mem_payload, // (TransferMetadata){.priority = UdpardPriorityHigh, .src_node_id = 3333, .dst_node_id = UDPARD_NODE_ID_UNSET, @@ -1684,7 +1684,7 @@ static void testIfaceAcceptB(void) TEST_ASSERT_NOT_NULL(transfer.payload.next); TEST_ASSERT(compareStringWithPayload("C1", transfer.payload.next->view)); TEST_ASSERT_NULL(transfer.payload.next->next); - udpardRxFragmentFree(transfer.payload, &mem_fragment.base, &mem_payload.base); + udpardRxFragmentFree(transfer.payload, mem.fragment, mem.payload); TEST_ASSERT_EQUAL(0, mem_payload.allocated_fragments); // Some memory is retained for the C0 payload. TEST_ASSERT_EQUAL(0, mem_fragment.allocated_fragments); } @@ -1722,7 +1722,7 @@ static void testIfaceAcceptC(void) TEST_ASSERT_EQUAL(0, rxIfaceAccept(&iface, 2000000010, - makeRxFrameString(&mem_payload.base, // + makeRxFrameString(&mem_payload, // (TransferMetadata){.priority = UdpardPriorityOptional, .src_node_id = 1111, .dst_node_id = UDPARD_NODE_ID_UNSET, @@ -1744,7 +1744,7 @@ static void testIfaceAcceptC(void) TEST_ASSERT_EQUAL(0, rxIfaceAccept(&iface, 2000000020, - makeRxFrameString(&mem_payload.base, // + makeRxFrameString(&mem_payload, // (TransferMetadata){.priority = UdpardPriorityExceptional, .src_node_id = 2222, .dst_node_id = UDPARD_NODE_ID_UNSET, @@ -1766,7 +1766,7 @@ static void testIfaceAcceptC(void) TEST_ASSERT_EQUAL(1, rxIfaceAccept(&iface, 2000000030, - makeRxFrameString(&mem_payload.base, // + makeRxFrameString(&mem_payload, // (TransferMetadata){.priority = UdpardPriorityOptional, .src_node_id = 1111, .dst_node_id = UDPARD_NODE_ID_UNSET, @@ -1792,14 +1792,14 @@ static void testIfaceAcceptC(void) TEST_ASSERT_NOT_NULL(transfer.payload.next); TEST_ASSERT(compareStringWithPayload("A1", transfer.payload.next->view)); TEST_ASSERT_NULL(transfer.payload.next->next); - udpardRxFragmentFree(transfer.payload, &mem_fragment.base, &mem_payload.base); + udpardRxFragmentFree(transfer.payload, mem.fragment, mem.payload); TEST_ASSERT_EQUAL(1, mem_payload.allocated_fragments); // B0 still allocated. TEST_ASSERT_EQUAL(1, mem_fragment.allocated_fragments); // C0 TEST_ASSERT_EQUAL(0, rxIfaceAccept(&iface, 2000000040, - makeRxFrameString(&mem_payload.base, // + makeRxFrameString(&mem_payload, // (TransferMetadata){.priority = UdpardPriorityExceptional, .src_node_id = 3333, .dst_node_id = UDPARD_NODE_ID_UNSET, @@ -1821,7 +1821,7 @@ static void testIfaceAcceptC(void) TEST_ASSERT_EQUAL(1, rxIfaceAccept(&iface, 2000000050, - makeRxFrameString(&mem_payload.base, // + makeRxFrameString(&mem_payload, // (TransferMetadata){.priority = UdpardPriorityExceptional, .src_node_id = 2222, .dst_node_id = UDPARD_NODE_ID_UNSET, @@ -1847,7 +1847,7 @@ static void testIfaceAcceptC(void) TEST_ASSERT_NOT_NULL(transfer.payload.next); TEST_ASSERT(compareStringWithPayload("B1", transfer.payload.next->view)); TEST_ASSERT_NULL(transfer.payload.next->next); - udpardRxFragmentFree(transfer.payload, &mem_fragment.base, &mem_payload.base); + udpardRxFragmentFree(transfer.payload, mem.fragment, mem.payload); TEST_ASSERT_EQUAL(1, mem_payload.allocated_fragments); // C0 is still allocated. TEST_ASSERT_EQUAL(1, mem_fragment.allocated_fragments); // C1 @@ -1856,7 +1856,7 @@ static void testIfaceAcceptC(void) TEST_ASSERT_EQUAL(1, rxIfaceAccept(&iface, 2000000060, - makeRxFrameString(&mem_payload.base, // + makeRxFrameString(&mem_payload, // (TransferMetadata){.priority = UdpardPriorityExceptional, .src_node_id = 3333, .dst_node_id = UDPARD_NODE_ID_UNSET, @@ -1883,14 +1883,14 @@ static void testIfaceAcceptC(void) TEST_ASSERT_NOT_NULL(transfer.payload.next); TEST_ASSERT(compareStringWithPayload("C1", transfer.payload.next->view)); TEST_ASSERT_NULL(transfer.payload.next->next); - udpardRxFragmentFree(transfer.payload, &mem_fragment.base, &mem_payload.base); + udpardRxFragmentFree(transfer.payload, mem.fragment, mem.payload); TEST_ASSERT_EQUAL(0, mem_payload.allocated_fragments); TEST_ASSERT_EQUAL(0, mem_fragment.allocated_fragments); // B0 duplicate multi-frame; shall be rejected. TEST_ASSERT_EQUAL(0, rxIfaceAccept(&iface, 2000000070, - makeRxFrameString(&mem_payload.base, // + makeRxFrameString(&mem_payload, // (TransferMetadata){.priority = UdpardPriorityExceptional, .src_node_id = 2222, .dst_node_id = UDPARD_NODE_ID_UNSET, @@ -1909,7 +1909,7 @@ static void testIfaceAcceptC(void) TEST_ASSERT_EQUAL(0, rxIfaceAccept(&iface, 2000000080, - makeRxFrameString(&mem_payload.base, // + makeRxFrameString(&mem_payload, // (TransferMetadata){.priority = UdpardPriorityExceptional, .src_node_id = 2222, .dst_node_id = UDPARD_NODE_ID_UNSET, @@ -1947,7 +1947,7 @@ static void testSessionDeduplicate(void) .transfer_id = 0x0DDC0FFEEBADF00D, .payload_size = 6, .payload = *head}; - memFree(&mem_fragment.base, sizeof(RxFragment), head); // Cloned, no longer needed. + memFree(mem.fragment, sizeof(RxFragment), head); // Cloned, no longer needed. TEST_ASSERT_EQUAL(2, mem_payload.allocated_fragments); TEST_ASSERT_EQUAL(1, mem_fragment.allocated_fragments); // The first transfer after initialization is always accepted. @@ -1974,7 +1974,7 @@ static void testSessionDeduplicate(void) .transfer_id = 0x0DDC0FFEEBADF000, // transfer-ID reduced. .payload_size = 6, .payload = *head}; - memFree(&mem_fragment.base, sizeof(RxFragment), head); // Cloned, no longer needed. + memFree(mem.fragment, sizeof(RxFragment), head); // Cloned, no longer needed. TEST_ASSERT_EQUAL(2, mem_payload.allocated_fragments); TEST_ASSERT_EQUAL(1, mem_fragment.allocated_fragments); // Accepted due to the TID timeout. @@ -2001,7 +2001,7 @@ static void testSessionDeduplicate(void) .transfer_id = 0x0DDC0FFEEBADF001, // Incremented. .payload_size = 6, .payload = *head}; - memFree(&mem_fragment.base, sizeof(RxFragment), head); // Cloned, no longer needed. + memFree(mem.fragment, sizeof(RxFragment), head); // Cloned, no longer needed. TEST_ASSERT_EQUAL(2, mem_payload.allocated_fragments); TEST_ASSERT_EQUAL(1, mem_fragment.allocated_fragments); // Accepted because TID greater. @@ -2039,7 +2039,7 @@ static void testSessionAcceptA(void) rxSessionAccept(&session, 1, 10000000, - makeRxFrameString(&mem_payload.base, // + makeRxFrameString(&mem_payload, // (TransferMetadata){.priority = UdpardPriorityExceptional, .src_node_id = 2222, .dst_node_id = UDPARD_NODE_ID_UNSET, @@ -2055,7 +2055,7 @@ static void testSessionAcceptA(void) TEST_ASSERT_EQUAL(1, mem_payload.allocated_fragments); TEST_ASSERT_EQUAL(0, mem_fragment.allocated_fragments); // Free the payload. - udpardRxFragmentFree(transfer.payload, &mem_fragment.base, &mem_payload.base); + udpardRxFragmentFree(transfer.payload, mem.fragment, mem.payload); TEST_ASSERT_EQUAL(0, mem_payload.allocated_fragments); TEST_ASSERT_EQUAL(0, mem_fragment.allocated_fragments); // Send the same transfer again through a different iface; it is a duplicate and so it is rejected and freed. @@ -2063,7 +2063,7 @@ static void testSessionAcceptA(void) rxSessionAccept(&session, 0, 10000010, - makeRxFrameString(&mem_payload.base, // + makeRxFrameString(&mem_payload, // (TransferMetadata){.priority = UdpardPriorityExceptional, .src_node_id = 2222, .dst_node_id = UDPARD_NODE_ID_UNSET, @@ -2084,7 +2084,7 @@ static void testSessionAcceptA(void) rxSessionAccept(&session, 2, 12000020, - makeRxFrameString(&mem_payload.base, // + makeRxFrameString(&mem_payload, // (TransferMetadata){.priority = UdpardPriorityExceptional, .src_node_id = 2222, .dst_node_id = UDPARD_NODE_ID_UNSET, @@ -2111,9 +2111,9 @@ static inline void testPortAcceptFrameA(void) instrumentedAllocatorNew(&mem_session); instrumentedAllocatorNew(&mem_fragment); instrumentedAllocatorNew(&mem_payload); - const struct UdpardRxMemoryResources mem = {.session = &mem_session.base, - .fragment = &mem_fragment.base, - .payload = &mem_payload.base}; + const struct UdpardRxMemoryResources mem = {.session = instrumentedAllocatorMakeMemoryResource(&mem_session), // + .fragment = instrumentedAllocatorMakeMemoryResource(&mem_fragment), + .payload = instrumentedAllocatorMakeMemoryDeleter(&mem_payload)}; struct UdpardRxTransfer transfer = {0}; // Initialize the port. struct UdpardRxPort port; @@ -2128,7 +2128,7 @@ static inline void testPortAcceptFrameA(void) rxPortAcceptFrame(&port, 1, 10000000, - makeDatagramPayloadSingleFrameString(&mem_payload.base, // + makeDatagramPayloadSingleFrameString(&mem_payload, // (TransferMetadata){.priority = UdpardPriorityImmediate, .src_node_id = 2222, .dst_node_id = UDPARD_NODE_ID_UNSET, @@ -2151,7 +2151,7 @@ static inline void testPortAcceptFrameA(void) "Solar System into two dimensions cease?", transfer.payload.view)); // Free the memory. - udpardRxFragmentFree(transfer.payload, &mem_fragment.base, &mem_payload.base); + udpardRxFragmentFree(transfer.payload, mem.fragment, mem.payload); TEST_ASSERT_EQUAL(1, mem_session.allocated_fragments); // The session remains. TEST_ASSERT_EQUAL(0, mem_fragment.allocated_fragments); TEST_ASSERT_EQUAL(0, mem_payload.allocated_fragments); @@ -2162,7 +2162,7 @@ static inline void testPortAcceptFrameA(void) rxPortAcceptFrame(&port, 0, 10000010, - makeDatagramPayloadSingleFrameString(&mem_payload.base, // + makeDatagramPayloadSingleFrameString(&mem_payload, // (TransferMetadata){.priority = UdpardPriorityImmediate, .src_node_id = 3333, .dst_node_id = UDPARD_NODE_ID_UNSET, @@ -2182,7 +2182,7 @@ static inline void testPortAcceptFrameA(void) TEST_ASSERT_EQUAL(20, transfer.payload_size); TEST_ASSERT(compareStringWithPayload("It will never cease.", transfer.payload.view)); // Free the memory. - udpardRxFragmentFree(transfer.payload, &mem_fragment.base, &mem_payload.base); + udpardRxFragmentFree(transfer.payload, mem.fragment, mem.payload); TEST_ASSERT_EQUAL(2, mem_session.allocated_fragments); // The sessions remain. TEST_ASSERT_EQUAL(0, mem_fragment.allocated_fragments); TEST_ASSERT_EQUAL(0, mem_payload.allocated_fragments); @@ -2194,7 +2194,7 @@ static inline void testPortAcceptFrameA(void) rxPortAcceptFrame(&port, 2, 10000020, - makeDatagramPayloadSingleFrameString(&mem_payload.base, // + makeDatagramPayloadSingleFrameString(&mem_payload, // (TransferMetadata){.priority = UdpardPriorityImmediate, .src_node_id = 4444, .dst_node_id = UDPARD_NODE_ID_UNSET, @@ -2214,7 +2214,7 @@ static inline void testPortAcceptFrameA(void) rxPortAcceptFrame(&port, 2, 10000030, - makeDatagramPayloadSingleFrameString(&mem_payload.base, // + makeDatagramPayloadSingleFrameString(&mem_payload, // (TransferMetadata){.priority = UdpardPriorityImmediate, .src_node_id = UDPARD_NODE_ID_UNSET, .dst_node_id = UDPARD_NODE_ID_UNSET, @@ -2234,7 +2234,7 @@ static inline void testPortAcceptFrameA(void) TEST_ASSERT_EQUAL(20, transfer.payload_size); TEST_ASSERT(compareStringWithPayload("Cheng Xin shuddered.", transfer.payload.view)); // Free the memory. - udpardRxFragmentFree(transfer.payload, &mem_fragment.base, &mem_payload.base); + udpardRxFragmentFree(transfer.payload, mem.fragment, mem.payload); TEST_ASSERT_EQUAL(2, mem_session.allocated_fragments); // The sessions remain. TEST_ASSERT_EQUAL(0, mem_fragment.allocated_fragments); TEST_ASSERT_EQUAL(0, mem_payload.allocated_fragments); @@ -2242,7 +2242,7 @@ static inline void testPortAcceptFrameA(void) // Send invalid anonymous transfers and see them fail. { // Bad CRC. struct UdpardMutablePayload datagram = - makeDatagramPayloadSingleFrameString(&mem_payload.base, // + makeDatagramPayloadSingleFrameString(&mem_payload, // (TransferMetadata){.priority = UdpardPriorityImmediate, .src_node_id = UDPARD_NODE_ID_UNSET, .dst_node_id = UDPARD_NODE_ID_UNSET, @@ -2257,7 +2257,7 @@ static inline void testPortAcceptFrameA(void) TEST_ASSERT_EQUAL(0, mem_payload.allocated_fragments); } { // No payload (transfer CRC is always required). - byte_t* const payload = memAlloc(&mem_payload.base, HEADER_SIZE_BYTES); + byte_t* const payload = instrumentedAllocatorAllocate(&mem_payload, HEADER_SIZE_BYTES); (void) txSerializeHeader(payload, (TransferMetadata){.priority = UdpardPriorityImmediate, .src_node_id = UDPARD_NODE_ID_UNSET, @@ -2283,9 +2283,11 @@ static inline void testPortAcceptFrameA(void) rxPortAcceptFrame(&port, 0, 10000060, - (struct UdpardMutablePayload){.size = HEADER_SIZE_BYTES, - .data = memAlloc(&mem_payload.base, - HEADER_SIZE_BYTES)}, + (struct + UdpardMutablePayload){.size = HEADER_SIZE_BYTES, + .data = + instrumentedAllocatorAllocate(&mem_payload, + HEADER_SIZE_BYTES)}, mem, &transfer)); TEST_ASSERT_EQUAL(2, mem_session.allocated_fragments); // Not increased. @@ -2298,7 +2300,7 @@ static inline void testPortAcceptFrameA(void) rxPortAcceptFrame(&port, 0, 10000070, - makeDatagramPayloadString(&mem_payload.base, // + makeDatagramPayloadString(&mem_payload, // (TransferMetadata){ .priority = UdpardPriorityImmediate, .src_node_id = 10000, @@ -2320,7 +2322,7 @@ static inline void testPortAcceptFrameA(void) rxPortAcceptFrame(&port, 0, 10000080, - makeDatagramPayloadString(&mem_payload.base, // + makeDatagramPayloadString(&mem_payload, // (TransferMetadata){ .priority = UdpardPriorityImmediate, .src_node_id = 10000, @@ -2344,7 +2346,7 @@ static inline void testPortAcceptFrameA(void) rxPortAcceptFrame(&port, 2, 10000090, - makeDatagramPayloadString(&mem_payload.base, // + makeDatagramPayloadString(&mem_payload, // (TransferMetadata){ .priority = UdpardPriorityImmediate, .src_node_id = 10001, diff --git a/tests/src/test_intrusive_tx.c b/tests/src/test_intrusive_tx.c index 069376e..bdd57d7 100644 --- a/tests/src/test_intrusive_tx.c +++ b/tests/src/test_intrusive_tx.c @@ -94,15 +94,16 @@ static void testMakeChainEmpty(void) { InstrumentedAllocator alloc; instrumentedAllocatorNew(&alloc); - char user_transfer_referent = '\0'; - const TransferMetadata meta = { - .priority = UdpardPriorityFast, - .src_node_id = 1234, - .dst_node_id = 2345, - .data_specifier = 5432, - .transfer_id = 0xBADC0FFEE0DDF00DULL, + struct UdpardMemoryResource mem = instrumentedAllocatorMakeMemoryResource(&alloc); + char user_transfer_referent = '\0'; + const TransferMetadata meta = { + .priority = UdpardPriorityFast, + .src_node_id = 1234, + .dst_node_id = 2345, + .data_specifier = 5432, + .transfer_id = 0xBADC0FFEE0DDF00DULL, }; - const TxChain chain = txMakeChain(&alloc.base, + const TxChain chain = txMakeChain(mem, (byte_t[]){11, 22, 33, 44, 55, 66, 77, 88}, 30, 1234567890, @@ -129,7 +130,7 @@ static void testMakeChainEmpty(void) (byte_t*) (chain.head->base.datagram_payload.data) + HEADER_SIZE_BYTES, 4)); TEST_ASSERT_EQUAL(&user_transfer_referent, chain.head->base.user_transfer_reference); - alloc.base.free(&alloc.base, sizeof(TxItem) + HEADER_SIZE_BYTES + 4, chain.head); + memFree(mem, sizeof(TxItem) + HEADER_SIZE_BYTES + 4, chain.head); TEST_ASSERT_EQUAL(0, alloc.allocated_fragments); } @@ -137,15 +138,16 @@ static void testMakeChainSingleMaxMTU(void) { InstrumentedAllocator alloc; instrumentedAllocatorNew(&alloc); - char user_transfer_referent = '\0'; - const TransferMetadata meta = { - .priority = UdpardPrioritySlow, - .src_node_id = 4321, - .dst_node_id = 5432, - .data_specifier = 7766, - .transfer_id = 0x0123456789ABCDEFULL, + struct UdpardMemoryResource mem = instrumentedAllocatorMakeMemoryResource(&alloc); + char user_transfer_referent = '\0'; + const TransferMetadata meta = { + .priority = UdpardPrioritySlow, + .src_node_id = 4321, + .dst_node_id = 5432, + .data_specifier = 7766, + .transfer_id = 0x0123456789ABCDEFULL, }; - const TxChain chain = txMakeChain(&alloc.base, + const TxChain chain = txMakeChain(mem, (byte_t[]){11, 22, 33, 44, 55, 66, 77, 88}, DetailOfTheCosmosSize + TRANSFER_CRC_SIZE_BYTES, 1234567890, @@ -179,9 +181,7 @@ static void testMakeChainSingleMaxMTU(void) DetailOfTheCosmosSize, TRANSFER_CRC_SIZE_BYTES)); TEST_ASSERT_EQUAL(&user_transfer_referent, chain.head->base.user_transfer_reference); - alloc.base.free(&alloc.base, - sizeof(TxItem) + HEADER_SIZE_BYTES + DetailOfTheCosmosSize + TRANSFER_CRC_SIZE_BYTES, - chain.head); + memFree(mem, sizeof(TxItem) + HEADER_SIZE_BYTES + DetailOfTheCosmosSize + TRANSFER_CRC_SIZE_BYTES, chain.head); TEST_ASSERT_EQUAL(0, alloc.allocated_fragments); } @@ -189,9 +189,10 @@ static void testMakeChainSingleFrameDefaultMTU(void) { InstrumentedAllocator alloc; instrumentedAllocatorNew(&alloc); - const byte_t payload[UDPARD_MTU_DEFAULT_MAX_SINGLE_FRAME + 1] = {0}; + struct UdpardMemoryResource mem = instrumentedAllocatorMakeMemoryResource(&alloc); + const byte_t payload[UDPARD_MTU_DEFAULT_MAX_SINGLE_FRAME + 1] = {0}; { // Ensure UDPARD_MTU_DEFAULT_MAX_SINGLE_FRAME bytes fit in a single frame with the default MTU. - const TxChain chain = txMakeChain(&alloc.base, + const TxChain chain = txMakeChain(mem, (byte_t[]){11, 22, 33, 44, 55, 66, 77, 88}, UDPARD_MTU_DEFAULT, 1234567890, @@ -210,15 +211,14 @@ static void testMakeChainSingleFrameDefaultMTU(void) TEST_ASSERT_EQUAL(1, chain.count); TEST_ASSERT_EQUAL(chain.head, chain.tail); TEST_ASSERT_EQUAL(NULL, chain.head->base.next_in_transfer); - alloc.base.free(&alloc.base, - sizeof(TxItem) + HEADER_SIZE_BYTES + UDPARD_MTU_DEFAULT_MAX_SINGLE_FRAME + - TRANSFER_CRC_SIZE_BYTES, - chain.head); + memFree(mem, + sizeof(TxItem) + HEADER_SIZE_BYTES + UDPARD_MTU_DEFAULT_MAX_SINGLE_FRAME + TRANSFER_CRC_SIZE_BYTES, + chain.head); TEST_ASSERT_EQUAL(0, alloc.allocated_fragments); } { // Increase the payload by 1 byte and ensure it spills over. const TxChain chain = - txMakeChain(&alloc.base, + txMakeChain(mem, (byte_t[]){11, 22, 33, 44, 55, 66, 77, 88}, UDPARD_MTU_DEFAULT, 1234567890, @@ -238,8 +238,8 @@ static void testMakeChainSingleFrameDefaultMTU(void) TEST_ASSERT_NOT_EQUAL(chain.head, chain.tail); TEST_ASSERT_EQUAL((UdpardTxItem*) chain.tail, chain.head->base.next_in_transfer); TEST_ASSERT_EQUAL(NULL, chain.tail->base.next_in_transfer); - alloc.base.free(&alloc.base, sizeof(TxItem) + HEADER_SIZE_BYTES + UDPARD_MTU_DEFAULT, chain.head); - alloc.base.free(&alloc.base, alloc.allocated_bytes, chain.tail); + memFree(mem, sizeof(TxItem) + HEADER_SIZE_BYTES + UDPARD_MTU_DEFAULT, chain.head); + memFree(mem, alloc.allocated_bytes, chain.tail); TEST_ASSERT_EQUAL(0, alloc.allocated_fragments); } } @@ -248,16 +248,17 @@ static void testMakeChainThreeFrames(void) { InstrumentedAllocator alloc; instrumentedAllocatorNew(&alloc); - char user_transfer_referent = '\0'; - const TransferMetadata meta = { - .priority = UdpardPriorityNominal, - .src_node_id = 4321, - .dst_node_id = 5432, - .data_specifier = 7766, - .transfer_id = 0x0123456789ABCDEFULL, + struct UdpardMemoryResource mem = instrumentedAllocatorMakeMemoryResource(&alloc); + char user_transfer_referent = '\0'; + const TransferMetadata meta = { + .priority = UdpardPriorityNominal, + .src_node_id = 4321, + .dst_node_id = 5432, + .data_specifier = 7766, + .transfer_id = 0x0123456789ABCDEFULL, }; const size_t mtu = (EtherealStrengthSize + 4U + 3U) / 3U; // Force payload split into three frames. - const TxChain chain = txMakeChain(&alloc.base, + const TxChain chain = txMakeChain(mem, (byte_t[]){11, 22, 33, 44, 55, 66, 77, 88}, mtu, 223574680, @@ -319,9 +320,9 @@ static void testMakeChainThreeFrames(void) TEST_ASSERT_EQUAL(&user_transfer_referent, third->user_transfer_reference); // Clean up. - alloc.base.free(&alloc.base, sizeof(TxItem) + HEADER_SIZE_BYTES + mtu, (void*) first); - alloc.base.free(&alloc.base, sizeof(TxItem) + HEADER_SIZE_BYTES + mtu, (void*) second); - alloc.base.free(&alloc.base, alloc.allocated_bytes, (void*) third); + memFree(mem, sizeof(TxItem) + HEADER_SIZE_BYTES + mtu, (void*) first); + memFree(mem, sizeof(TxItem) + HEADER_SIZE_BYTES + mtu, (void*) second); + memFree(mem, alloc.allocated_bytes, (void*) third); TEST_ASSERT_EQUAL(0, alloc.allocated_fragments); } @@ -329,16 +330,17 @@ static void testMakeChainCRCSpill1(void) { InstrumentedAllocator alloc; instrumentedAllocatorNew(&alloc); - char user_transfer_referent = '\0'; - const TransferMetadata meta = { - .priority = UdpardPriorityNominal, - .src_node_id = 4321, - .dst_node_id = 5432, - .data_specifier = 7766, - .transfer_id = 0x0123456789ABCDEFULL, + struct UdpardMemoryResource mem = instrumentedAllocatorMakeMemoryResource(&alloc); + char user_transfer_referent = '\0'; + const TransferMetadata meta = { + .priority = UdpardPriorityNominal, + .src_node_id = 4321, + .dst_node_id = 5432, + .data_specifier = 7766, + .transfer_id = 0x0123456789ABCDEFULL, }; const size_t mtu = InterstellarWarSize + 3U; - const TxChain chain = txMakeChain(&alloc.base, + const TxChain chain = txMakeChain(mem, (byte_t[]){11, 22, 33, 44, 55, 66, 77, 88}, mtu, 223574680, @@ -391,8 +393,8 @@ static void testMakeChainCRCSpill1(void) TEST_ASSERT_EQUAL(&user_transfer_referent, chain.tail->base.user_transfer_reference); // Clean up. - alloc.base.free(&alloc.base, sizeof(TxItem) + HEADER_SIZE_BYTES + mtu, chain.head); - alloc.base.free(&alloc.base, alloc.allocated_bytes, chain.tail); + memFree(mem, sizeof(TxItem) + HEADER_SIZE_BYTES + mtu, chain.head); + memFree(mem, alloc.allocated_bytes, chain.tail); TEST_ASSERT_EQUAL(0, alloc.allocated_fragments); } @@ -400,16 +402,17 @@ static void testMakeChainCRCSpill2(void) { InstrumentedAllocator alloc; instrumentedAllocatorNew(&alloc); - char user_transfer_referent = '\0'; - const TransferMetadata meta = { - .priority = UdpardPriorityNominal, - .src_node_id = 4321, - .dst_node_id = 5432, - .data_specifier = 7766, - .transfer_id = 0x0123456789ABCDEFULL, + struct UdpardMemoryResource mem = instrumentedAllocatorMakeMemoryResource(&alloc); + char user_transfer_referent = '\0'; + const TransferMetadata meta = { + .priority = UdpardPriorityNominal, + .src_node_id = 4321, + .dst_node_id = 5432, + .data_specifier = 7766, + .transfer_id = 0x0123456789ABCDEFULL, }; const size_t mtu = InterstellarWarSize + 2U; - const TxChain chain = txMakeChain(&alloc.base, + const TxChain chain = txMakeChain(mem, (byte_t[]){11, 22, 33, 44, 55, 66, 77, 88}, mtu, 223574680, @@ -462,8 +465,8 @@ static void testMakeChainCRCSpill2(void) TEST_ASSERT_EQUAL(&user_transfer_referent, chain.tail->base.user_transfer_reference); // Clean up. - alloc.base.free(&alloc.base, sizeof(TxItem) + HEADER_SIZE_BYTES + mtu, chain.head); - alloc.base.free(&alloc.base, alloc.allocated_bytes, chain.tail); + memFree(mem, sizeof(TxItem) + HEADER_SIZE_BYTES + mtu, chain.head); + memFree(mem, alloc.allocated_bytes, chain.tail); TEST_ASSERT_EQUAL(0, alloc.allocated_fragments); } @@ -471,16 +474,17 @@ static void testMakeChainCRCSpill3(void) { InstrumentedAllocator alloc; instrumentedAllocatorNew(&alloc); - char user_transfer_referent = '\0'; - const TransferMetadata meta = { - .priority = UdpardPriorityNominal, - .src_node_id = 4321, - .dst_node_id = 5432, - .data_specifier = 7766, - .transfer_id = 0x0123456789ABCDEFULL, + struct UdpardMemoryResource mem = instrumentedAllocatorMakeMemoryResource(&alloc); + char user_transfer_referent = '\0'; + const TransferMetadata meta = { + .priority = UdpardPriorityNominal, + .src_node_id = 4321, + .dst_node_id = 5432, + .data_specifier = 7766, + .transfer_id = 0x0123456789ABCDEFULL, }; const size_t mtu = InterstellarWarSize + 1U; - const TxChain chain = txMakeChain(&alloc.base, + const TxChain chain = txMakeChain(mem, (byte_t[]){11, 22, 33, 44, 55, 66, 77, 88}, mtu, 223574680, @@ -533,8 +537,8 @@ static void testMakeChainCRCSpill3(void) TEST_ASSERT_EQUAL(&user_transfer_referent, chain.tail->base.user_transfer_reference); // Clean up. - alloc.base.free(&alloc.base, sizeof(TxItem) + HEADER_SIZE_BYTES + mtu, chain.head); - alloc.base.free(&alloc.base, alloc.allocated_bytes, chain.tail); + memFree(mem, sizeof(TxItem) + HEADER_SIZE_BYTES + mtu, chain.head); + memFree(mem, alloc.allocated_bytes, chain.tail); TEST_ASSERT_EQUAL(0, alloc.allocated_fragments); } @@ -542,16 +546,17 @@ static void testMakeChainCRCSpillFull(void) { InstrumentedAllocator alloc; instrumentedAllocatorNew(&alloc); - char user_transfer_referent = '\0'; - const TransferMetadata meta = { - .priority = UdpardPriorityNominal, - .src_node_id = 4321, - .dst_node_id = 5432, - .data_specifier = 7766, - .transfer_id = 0x0123456789ABCDEFULL, + struct UdpardMemoryResource mem = instrumentedAllocatorMakeMemoryResource(&alloc); + char user_transfer_referent = '\0'; + const TransferMetadata meta = { + .priority = UdpardPriorityNominal, + .src_node_id = 4321, + .dst_node_id = 5432, + .data_specifier = 7766, + .transfer_id = 0x0123456789ABCDEFULL, }; const size_t mtu = InterstellarWarSize; - const TxChain chain = txMakeChain(&alloc.base, + const TxChain chain = txMakeChain(mem, (byte_t[]){11, 22, 33, 44, 55, 66, 77, 88}, mtu, 223574680, @@ -600,8 +605,8 @@ static void testMakeChainCRCSpillFull(void) TEST_ASSERT_EQUAL(&user_transfer_referent, chain.tail->base.user_transfer_reference); // Clean up. - alloc.base.free(&alloc.base, sizeof(TxItem) + HEADER_SIZE_BYTES + mtu, chain.head); - alloc.base.free(&alloc.base, alloc.allocated_bytes, chain.tail); + memFree(mem, sizeof(TxItem) + HEADER_SIZE_BYTES + mtu, chain.head); + memFree(mem, alloc.allocated_bytes, chain.tail); TEST_ASSERT_EQUAL(0, alloc.allocated_fragments); } @@ -609,14 +614,15 @@ static void testPushPeekPopFree(void) { InstrumentedAllocator alloc; instrumentedAllocatorNew(&alloc); - const UdpardNodeID node_id = 1234; + struct UdpardMemoryResource mem = instrumentedAllocatorMakeMemoryResource(&alloc); + const UdpardNodeID node_id = 1234; // UdpardTx tx = { .local_node_id = &node_id, .queue_capacity = 3, .mtu = (EtherealStrengthSize + 4U + 3U) / 3U, .dscp_value_per_priority = {0, 1, 2, 3, 4, 5, 6, 7}, - .memory = &alloc.base, + .memory = mem, .queue_size = 0, .root = NULL, }; @@ -687,14 +693,15 @@ static void testPushPrioritization(void) { InstrumentedAllocator alloc; instrumentedAllocatorNew(&alloc); - const UdpardNodeID node_id = 1234; + struct UdpardMemoryResource mem = instrumentedAllocatorMakeMemoryResource(&alloc); + const UdpardNodeID node_id = 1234; // UdpardTx tx = { .local_node_id = &node_id, .queue_capacity = 7, .mtu = 140, // This is chosen to match the test data. .dscp_value_per_priority = {0, 1, 2, 3, 4, 5, 6, 7}, - .memory = &alloc.base, + .memory = mem, .queue_size = 0, .root = NULL, }; @@ -857,14 +864,15 @@ static void testPushCapacityLimit(void) { InstrumentedAllocator alloc; instrumentedAllocatorNew(&alloc); - const UdpardNodeID node_id = 1234; + struct UdpardMemoryResource mem = instrumentedAllocatorMakeMemoryResource(&alloc); + const UdpardNodeID node_id = 1234; // UdpardTx tx = { .local_node_id = &node_id, .queue_capacity = 2, .mtu = 10U, .dscp_value_per_priority = {0, 1, 2, 3, 4, 5, 6, 7}, - .memory = &alloc.base, + .memory = mem, .queue_size = 0, .root = NULL, }; @@ -891,14 +899,15 @@ static void testPushOOM(void) { InstrumentedAllocator alloc; instrumentedAllocatorNew(&alloc); - const UdpardNodeID node_id = 1234; + struct UdpardMemoryResource mem = instrumentedAllocatorMakeMemoryResource(&alloc); + const UdpardNodeID node_id = 1234; // UdpardTx tx = { .local_node_id = &node_id, .queue_capacity = 10000U, .mtu = (EtherealStrengthSize + 4U + 3U) / 3U, .dscp_value_per_priority = {0, 1, 2, 3, 4, 5, 6, 7}, - .memory = &alloc.base, + .memory = mem, .queue_size = 0, .root = NULL, }; @@ -926,14 +935,15 @@ static void testPushAnonymousMultiFrame(void) { InstrumentedAllocator alloc; instrumentedAllocatorNew(&alloc); - const UdpardNodeID node_id = 0xFFFFU; + struct UdpardMemoryResource mem = instrumentedAllocatorMakeMemoryResource(&alloc); + const UdpardNodeID node_id = 0xFFFFU; // UdpardTx tx = { .local_node_id = &node_id, .queue_capacity = 10000U, .mtu = (EtherealStrengthSize + 4U + 3U) / 3U, .dscp_value_per_priority = {0, 1, 2, 3, 4, 5, 6, 7}, - .memory = &alloc.base, + .memory = mem, .queue_size = 0, .root = NULL, }; @@ -960,14 +970,15 @@ static void testPushAnonymousService(void) { InstrumentedAllocator alloc; instrumentedAllocatorNew(&alloc); - const UdpardNodeID node_id = 0xFFFFU; + struct UdpardMemoryResource mem = instrumentedAllocatorMakeMemoryResource(&alloc); + const UdpardNodeID node_id = 0xFFFFU; // UdpardTx tx = { .local_node_id = &node_id, .queue_capacity = 10000, .mtu = 1500, .dscp_value_per_priority = {0, 1, 2, 3, 4, 5, 6, 7}, - .memory = &alloc.base, + .memory = mem, .queue_size = 0, .root = NULL, }; diff --git a/tests/src/test_tx.cpp b/tests/src/test_tx.cpp index 6648833..990ab45 100644 --- a/tests/src/test_tx.cpp +++ b/tests/src/test_tx.cpp @@ -29,49 +29,49 @@ void testInit() std::monostate user_referent; const UdpardNodeID node_id = 0; { - UdpardMemoryResource memory{ - .allocate = &dummyAllocatorAllocate, - .free = &dummyAllocatorFree, + const UdpardMemoryResource memory{ .user_reference = &user_referent, + .free = &dummyAllocatorFree, + .allocate = &dummyAllocatorAllocate, }; - TEST_ASSERT_EQUAL(-UDPARD_ERROR_ARGUMENT, udpardTxInit(nullptr, &node_id, 0, &memory)); + TEST_ASSERT_EQUAL(-UDPARD_ERROR_ARGUMENT, udpardTxInit(nullptr, &node_id, 0, memory)); } { - UdpardTx tx{}; - UdpardMemoryResource memory{ - .allocate = &dummyAllocatorAllocate, - .free = &dummyAllocatorFree, + UdpardTx tx{}; + const UdpardMemoryResource memory{ .user_reference = &user_referent, + .free = &dummyAllocatorFree, + .allocate = &dummyAllocatorAllocate, }; - TEST_ASSERT_EQUAL(-UDPARD_ERROR_ARGUMENT, udpardTxInit(&tx, nullptr, 0, &memory)); + TEST_ASSERT_EQUAL(-UDPARD_ERROR_ARGUMENT, udpardTxInit(&tx, nullptr, 0, memory)); } { - UdpardTx tx{}; - UdpardMemoryResource memory{ - .allocate = nullptr, - .free = &dummyAllocatorFree, + UdpardTx tx{}; + const UdpardMemoryResource memory{ .user_reference = &user_referent, + .free = &dummyAllocatorFree, + .allocate = nullptr, }; - TEST_ASSERT_EQUAL(-UDPARD_ERROR_ARGUMENT, udpardTxInit(&tx, &node_id, 0, &memory)); + TEST_ASSERT_EQUAL(-UDPARD_ERROR_ARGUMENT, udpardTxInit(&tx, &node_id, 0, memory)); } { - UdpardTx tx{}; - UdpardMemoryResource memory{ - .allocate = &dummyAllocatorAllocate, - .free = nullptr, + UdpardTx tx{}; + const UdpardMemoryResource memory{ .user_reference = &user_referent, + .free = nullptr, + .allocate = &dummyAllocatorAllocate, }; - TEST_ASSERT_EQUAL(-UDPARD_ERROR_ARGUMENT, udpardTxInit(&tx, &node_id, 0, &memory)); + TEST_ASSERT_EQUAL(-UDPARD_ERROR_ARGUMENT, udpardTxInit(&tx, &node_id, 0, memory)); } { - UdpardTx tx{}; - UdpardMemoryResource memory{ - .allocate = &dummyAllocatorAllocate, - .free = &dummyAllocatorFree, + UdpardTx tx{}; + const UdpardMemoryResource memory{ .user_reference = &user_referent, + .free = &dummyAllocatorFree, + .allocate = &dummyAllocatorAllocate, }; - TEST_ASSERT_EQUAL(0, udpardTxInit(&tx, &node_id, 0, &memory)); - TEST_ASSERT_EQUAL(&user_referent, tx.memory->user_reference); + TEST_ASSERT_EQUAL(0, udpardTxInit(&tx, &node_id, 0, memory)); + TEST_ASSERT_EQUAL(&user_referent, tx.memory.user_reference); TEST_ASSERT_EQUAL(UDPARD_MTU_DEFAULT, tx.mtu); } } @@ -80,14 +80,15 @@ void testPublish() { InstrumentedAllocator alloc; instrumentedAllocatorNew(&alloc); - const UdpardNodeID node_id = 1234; + const struct UdpardMemoryResource mem = instrumentedAllocatorMakeMemoryResource(&alloc); + const UdpardNodeID node_id = 1234; // UdpardTx tx{ .local_node_id = &node_id, .queue_capacity = 1U, .mtu = UDPARD_MTU_DEFAULT, .dscp_value_per_priority = {0, 1, 2, 3, 4, 5, 6, 7}, - .memory = &alloc.base, + .memory = mem, .queue_size = 0, .root = nullptr, }; @@ -227,14 +228,15 @@ void testRequest() { InstrumentedAllocator alloc; instrumentedAllocatorNew(&alloc); - const UdpardNodeID node_id = 1234; + const UdpardMemoryResource mem = instrumentedAllocatorMakeMemoryResource(&alloc); + const UdpardNodeID node_id = 1234; // UdpardTx tx{ .local_node_id = &node_id, .queue_capacity = 1U, .mtu = UDPARD_MTU_DEFAULT, .dscp_value_per_priority = {0, 1, 2, 3, 4, 5, 6, 7}, - .memory = &alloc.base, + .memory = mem, .queue_size = 0, .root = nullptr, }; @@ -394,14 +396,15 @@ void testRespond() { InstrumentedAllocator alloc; instrumentedAllocatorNew(&alloc); - const UdpardNodeID node_id = 1234; + const UdpardMemoryResource mem = instrumentedAllocatorMakeMemoryResource(&alloc); + const UdpardNodeID node_id = 1234; // UdpardTx tx{ .local_node_id = &node_id, .queue_capacity = 1U, .mtu = UDPARD_MTU_DEFAULT, .dscp_value_per_priority = {0, 1, 2, 3, 4, 5, 6, 7}, - .memory = &alloc.base, + .memory = mem, .queue_size = 0, .root = nullptr, }; @@ -541,7 +544,7 @@ void testPeekPopFreeNULL() // Just make sure we don't crash. { TEST_ASSERT_EQUAL(nullptr, udpardTxPeek(nullptr)); TEST_ASSERT_EQUAL(nullptr, udpardTxPop(nullptr, nullptr)); - udpardTxFree(nullptr, nullptr); + udpardTxFree({}, nullptr); } } // namespace From 89563f56d1e65b548eaa51c9357346840023d8e8 Mon Sep 17 00:00:00 2001 From: Pavel Kirienko Date: Tue, 8 Aug 2023 13:39:47 +0300 Subject: [PATCH 30/38] Add clarifying comments --- libudpard/udpard.c | 22 ++++++++++++---------- libudpard/udpard.h | 7 ++++--- tests/src/test_intrusive_rx.c | 3 ++- 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/libudpard/udpard.c b/libudpard/udpard.c index 934b5d6..08755d9 100644 --- a/libudpard/udpard.c +++ b/libudpard/udpard.c @@ -818,7 +818,8 @@ typedef struct RxFragment /// /// - There is one port per subscription or an RPC-service listener. Within the port, there are N sessions, /// one session per remote node emitting transfers on this port (i.e., on this subject, or sending -/// request/response of this service). Sessions are constructed dynamically ad-hoc and stored in the heap. +/// request/response of this service). Sessions are constructed dynamically in memory provided by +/// UdpardMemoryResource. /// /// - Per session, there are UDPARD_NETWORK_INTERFACE_COUNT_MAX interface states to support interface redundancy. /// @@ -827,17 +828,18 @@ typedef struct RxFragment /// /// Port -> Session -> Interface -> Slot -> Fragments. /// -/// Consider the following examples, where A and B denote distinct transfers of three frames each: +/// Consider the following examples, where A,B,C denote distinct multi-frame transfers: /// -/// A0 A1 A2 B0 B1 B2 -- two transfers without OOO frames; both accepted. -/// A2 A0 A1 B0 B2 B1 -- two transfers with OOO frames; both accepted. -/// A0 A1 B0 A2 B1 B2 -- two transfers with interleaved frames; both accepted (this is why we need 2 buffers). -/// -/// It is assumed that interleaved frames spanning more than two transfers are not possible. +/// A0 A1 A2 B0 B1 B2 -- two transfers without OOO frames; both accepted +/// A2 A0 A1 B0 B2 B1 -- two transfers with OOO frames; both accepted +/// A0 A1 B0 A2 B1 B2 -- two transfers with interleaved frames; both accepted (this is why we need 2 slots) +/// B1 A2 A0 C0 B0 A1 C1 -- B evicted by C; A and C accepted, B dropped (to accept B we would need 3 slots) +/// B0 A0 A1 C0 B1 A2 C1 -- ditto +/// A0 A1 C0 B0 A2 C1 B1 -- A evicted by B; B and C accepted, A dropped /// /// In this implementation we postpone the implicit truncation until all fragments of a transfer are received. -/// Early truncation, the way it is done in libcanard with contiguous payload reassembly buffers, -/// is much more difficult to implement if out-of-order reassembly is a requirement. +/// Early truncation such that excess payload is not stored in memory at all is difficult to implement if +/// out-of-order reassembly is a requirement. /// To implement early truncation with out-of-order reassembly, we need to deduce the MTU of the sender per transfer /// (which is easy as we only need to take note of the payload size of any non-last frame of the transfer), /// then, based on the MTU, determine the maximum frame index we should accept (higher indexes will be dropped); @@ -1084,7 +1086,7 @@ static inline bool rxSlotEject(size_t* const out_payload_size, return result; } -/// This function will either move the frame payload into the session, or free it if it cannot be made use of. +/// This function will either move the frame payload into the session, or free it if it can't be used. /// Upon return, certain state variables may be overwritten, so the caller should not rely on them. /// Returns: 1 -- transfer available, payload written; 0 -- transfer not yet available; <0 -- error. static inline int_fast8_t rxSlotAccept(RxSlot* const self, diff --git a/libudpard/udpard.h b/libudpard/udpard.h index 8fc6062..8a5c289 100644 --- a/libudpard/udpard.h +++ b/libudpard/udpard.h @@ -331,14 +331,15 @@ struct UdpardUDPIPEndpoint /// consider using O1Heap: https://github.com/pavel-kirienko/o1heap. Alternatively, some applications may prefer to /// use a set of fixed-size block pool allocators (see the high-level overview for details). /// -/// The time complexity models given in the API documentation are made on the assumption that the memory management -/// functions have constant complexity O(1). +/// The API documentation is written on the assumption that the memory management functions have constant +/// complexity and are non-blocking. /// /// The value of the user reference is taken from the corresponding field of the memory resource structure. typedef void* (*UdpardMemoryAllocate)(void* const user_reference, const size_t size); /// The counterpart of the above -- this function is invoked to return previously allocated memory to the allocator. -/// The size argument contains the amount of memory that was originally requested via the allocation function. +/// The size argument contains the amount of memory that was originally requested via the allocation function; +/// its value is undefined if the pointer is NULL. /// The semantics are similar to free(): /// - The pointer was previously returned by the allocation function. /// - The pointer may be NULL, in which case the function shall have no effect. diff --git a/tests/src/test_intrusive_rx.c b/tests/src/test_intrusive_rx.c index ec34aa9..c7bde54 100644 --- a/tests/src/test_intrusive_rx.c +++ b/tests/src/test_intrusive_rx.c @@ -1476,6 +1476,7 @@ static void testIfaceAcceptB(void) // === TRANSFER === (x3) // Send three interleaving multi-frame out-of-order transfers (primes for duplicates): // A2 B1 A0 C0 B0 A1 C0' C1 + // A2 arrives before B1 but its timestamp is higher. // Transfer B will be evicted by C because by the time C0 arrives, transfer B is the oldest one, // since its timestamp is inherited from B0. TEST_ASSERT_EQUAL(0, mem_payload.allocated_fragments); @@ -1506,7 +1507,7 @@ static void testIfaceAcceptB(void) // B1 TEST_ASSERT_EQUAL(0, rxIfaceAccept(&iface, - 2000000010, // Transfer-ID timeout. + 2000000010, // TIME REORDERING -- lower than previous. makeRxFrameString(&mem_payload, // (TransferMetadata){.priority = UdpardPrioritySlow, .src_node_id = 2222, From 92044da6962464c1447e958994b10782e214e2c7 Mon Sep 17 00:00:00 2001 From: Pavel Kirienko Date: Tue, 8 Aug 2023 16:33:39 +0300 Subject: [PATCH 31/38] Remove typedef struct UdpardInternalRxSession --- libudpard/udpard.c | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/libudpard/udpard.c b/libudpard/udpard.c index 08755d9..2450607 100644 --- a/libudpard/udpard.c +++ b/libudpard/udpard.c @@ -866,7 +866,7 @@ typedef struct /// This type is forward-declared externally, hence why it has such a long name with the "udpard" prefix. /// Keep in mind that we have a dedicated session object per remote node per port; this means that the states /// kept here -- the timestamp and the transfer-ID -- are specific per remote node, as it should be. -typedef struct UdpardInternalRxSession +struct UdpardInternalRxSession { struct UdpardTreeNode base; /// The remote node-ID is needed here as this is the ordering/search key. @@ -880,7 +880,7 @@ typedef struct UdpardInternalRxSession /// The first interface to receive a transfer takes precedence, thus the redundant group always operates /// at the speed of the fastest interface. Duplicate transfers delivered by the slower interfaces are discarded. RxIface ifaces[UDPARD_NETWORK_INTERFACE_COUNT_MAX]; -} UdpardInternalRxSession; +}; // -------------------------------------------------- RX FRAGMENT -------------------------------------------------- @@ -1342,10 +1342,10 @@ static inline void rxIfaceFree(RxIface* const self, const RxMemory memory) /// Checks if the given transfer should be accepted. If not, the transfer is freed. /// Internal states are updated. -static inline bool rxSessionDeduplicate(UdpardInternalRxSession* const self, - const UdpardMicrosecond transfer_id_timeout_usec, - struct UdpardRxTransfer* const transfer, - const RxMemory memory) +static inline bool rxSessionDeduplicate(struct UdpardInternalRxSession* const self, + const UdpardMicrosecond transfer_id_timeout_usec, + struct UdpardRxTransfer* const transfer, + const RxMemory memory) { UDPARD_ASSERT((self != NULL) && (transfer != NULL)); const bool future_tid = (self->last_transfer_id == TRANSFER_ID_UNSET) || // @@ -1372,14 +1372,14 @@ static inline bool rxSessionDeduplicate(UdpardInternalRxSession* const self, } /// Takes ownership of the frame payload buffer. -static inline int_fast8_t rxSessionAccept(UdpardInternalRxSession* const self, - const uint_fast8_t redundant_iface_index, - const UdpardMicrosecond ts_usec, - const RxFrame frame, - const size_t extent, - const UdpardMicrosecond transfer_id_timeout_usec, - const RxMemory memory, - struct UdpardRxTransfer* const out_transfer) +static inline int_fast8_t rxSessionAccept(struct UdpardInternalRxSession* const self, + const uint_fast8_t redundant_iface_index, + const UdpardMicrosecond ts_usec, + const RxFrame frame, + const size_t extent, + const UdpardMicrosecond transfer_id_timeout_usec, + const RxMemory memory, + struct UdpardRxTransfer* const out_transfer) { UDPARD_ASSERT((self != NULL) && (redundant_iface_index < UDPARD_NETWORK_INTERFACE_COUNT_MAX) && (out_transfer != NULL)); @@ -1398,7 +1398,7 @@ static inline int_fast8_t rxSessionAccept(UdpardInternalRxSession* const self, return result; } -static inline void rxSessionInit(UdpardInternalRxSession* const self, const RxMemory memory) +static inline void rxSessionInit(struct UdpardInternalRxSession* const self, const RxMemory memory) { UDPARD_ASSERT(self != NULL); memZero(sizeof(*self), self); @@ -1413,8 +1413,8 @@ static inline void rxSessionInit(UdpardInternalRxSession* const self, const RxMe /// Frees all ifaces in the session, all children in the session tree recursively, and destroys the session itself. // NOLINTNEXTLINE(*-no-recursion) -static inline void rxSessionDestroyTree(UdpardInternalRxSession* const self, - const struct UdpardRxMemoryResources memory) +static inline void rxSessionDestroyTree(struct UdpardInternalRxSession* const self, + const struct UdpardRxMemoryResources memory) { for (uint_fast8_t i = 0; i < UDPARD_NETWORK_INTERFACE_COUNT_MAX; i++) { @@ -1422,14 +1422,14 @@ static inline void rxSessionDestroyTree(UdpardInternalRxSession* const sel } for (uint_fast8_t i = 0; i < 2; i++) { - UdpardInternalRxSession* const child = (UdpardInternalRxSession*) (void*) self->base.lr[i]; + struct UdpardInternalRxSession* const child = (struct UdpardInternalRxSession*) (void*) self->base.lr[i]; if (child != NULL) { UDPARD_ASSERT(child->base.up == &self->base); rxSessionDestroyTree(child, memory); // NOSONAR recursion } } - memFree(memory.session, sizeof(UdpardInternalRxSession), self); + memFree(memory.session, sizeof(struct UdpardInternalRxSession), self); } // -------------------------------------------------- RX PORT -------------------------------------------------- From 87e49bcd8a346dae3b1d9784a0441dede361f131 Mon Sep 17 00:00:00 2001 From: Pavel Kirienko Date: Tue, 8 Aug 2023 16:43:41 +0300 Subject: [PATCH 32/38] Rename free --> deallocate because Sonar detected a compliance issue --- libudpard/udpard.c | 13 +++++++------ libudpard/udpard.h | 12 ++++++------ tests/src/helpers.h | 9 +++++---- tests/src/test_helpers.c | 8 ++++---- tests/src/test_intrusive_rx.c | 10 +++++----- tests/src/test_tx.cpp | 10 +++++----- 6 files changed, 32 insertions(+), 30 deletions(-) diff --git a/libudpard/udpard.c b/libudpard/udpard.c index 2450607..7085d7f 100644 --- a/libudpard/udpard.c +++ b/libudpard/udpard.c @@ -132,14 +132,14 @@ static inline void* memAlloc(const struct UdpardMemoryResource memory, const siz static inline void memFree(const struct UdpardMemoryResource memory, const size_t size, void* const data) { - UDPARD_ASSERT(memory.free != NULL); - memory.free(memory.user_reference, size, data); + UDPARD_ASSERT(memory.deallocate != NULL); + memory.deallocate(memory.user_reference, size, data); } static inline void memFreePayload(const struct UdpardMemoryDeleter memory, const struct UdpardMutablePayload payload) { - UDPARD_ASSERT(memory.free != NULL); - memory.free(memory.user_reference, payload.size, payload.data); + UDPARD_ASSERT(memory.deallocate != NULL); + memory.deallocate(memory.user_reference, payload.size, payload.data); } static inline void memZero(const size_t size, void* const data) @@ -528,7 +528,7 @@ int_fast8_t udpardTxInit(struct UdpardTx* const self, const struct UdpardMemoryResource memory) { int_fast8_t ret = -UDPARD_ERROR_ARGUMENT; - if ((NULL != self) && (NULL != local_node_id) && (memory.allocate != NULL) && (memory.free != NULL)) + if ((NULL != self) && (NULL != local_node_id) && (memory.allocate != NULL) && (memory.deallocate != NULL)) { ret = 0; memZero(sizeof(*self), self); @@ -961,7 +961,8 @@ static inline int_fast8_t rxSlotFragmentSearch(void* const user_reference, // N static inline struct UdpardTreeNode* rxSlotFragmentFactory(void* const user_reference) { RxSlotUpdateContext* const ctx = (RxSlotUpdateContext*) user_reference; - UDPARD_ASSERT((ctx != NULL) && (ctx->memory_fragment.allocate != NULL) && (ctx->memory_fragment.free != NULL)); + UDPARD_ASSERT((ctx != NULL) && (ctx->memory_fragment.allocate != NULL) && + (ctx->memory_fragment.deallocate != NULL)); struct UdpardTreeNode* out = NULL; RxFragment* const frag = memAlloc(ctx->memory_fragment, sizeof(RxFragment)); if (frag != NULL) diff --git a/libudpard/udpard.h b/libudpard/udpard.h index 8a5c289..d830098 100644 --- a/libudpard/udpard.h +++ b/libudpard/udpard.h @@ -346,14 +346,14 @@ typedef void* (*UdpardMemoryAllocate)(void* const user_reference, const size_t s /// - The execution time should be constant (O(1)). /// /// The value of the user reference is taken from the corresponding field of the memory resource structure. -typedef void (*UdpardMemoryFree)(void* const user_reference, const size_t size, void* const pointer); +typedef void (*UdpardMemoryDeallocate)(void* const user_reference, const size_t size, void* const pointer); /// A kind of memory resource that can only be used to free memory previously allocated by the user. /// Instances are mostly intended to be passed by value. struct UdpardMemoryDeleter { - void* user_reference; ///< Passed as the first argument. - UdpardMemoryFree free; ///< Shall be a valid pointer. + void* user_reference; ///< Passed as the first argument. + UdpardMemoryDeallocate deallocate; ///< Shall be a valid pointer. }; /// A memory resource encapsulates the dynamic memory allocation and deallocation facilities. @@ -362,9 +362,9 @@ struct UdpardMemoryDeleter /// Instances are mostly intended to be passed by value. struct UdpardMemoryResource { - void* user_reference; ///< Passed as the first argument. - UdpardMemoryFree free; ///< Shall be a valid pointer. - UdpardMemoryAllocate allocate; ///< Shall be a valid pointer. + void* user_reference; ///< Passed as the first argument. + UdpardMemoryDeallocate deallocate; ///< Shall be a valid pointer. + UdpardMemoryAllocate allocate; ///< Shall be a valid pointer. }; // ===================================================================================================================== diff --git a/tests/src/helpers.h b/tests/src/helpers.h index 291d785..87d881e 100644 --- a/tests/src/helpers.h +++ b/tests/src/helpers.h @@ -45,7 +45,7 @@ static inline void* dummyAllocatorAllocate(void* const user_reference, const siz return NULL; } -static inline void dummyAllocatorFree(void* const user_reference, const size_t size, void* const pointer) +static inline void dummyAllocatorDeallocate(void* const user_reference, const size_t size, void* const pointer) { (void) user_reference; (void) size; @@ -99,7 +99,7 @@ static inline void* instrumentedAllocatorAllocate(void* const user_reference, co return result; } -static inline void instrumentedAllocatorFree(void* const user_reference, const size_t size, void* const pointer) +static inline void instrumentedAllocatorDeallocate(void* const user_reference, const size_t size, void* const pointer) { InstrumentedAllocator* const self = (InstrumentedAllocator*) user_reference; if (pointer != NULL) @@ -146,14 +146,15 @@ static inline struct UdpardMemoryResource instrumentedAllocatorMakeMemoryResourc const InstrumentedAllocator* const self) { const struct UdpardMemoryResource out = {.user_reference = (void*) self, - .free = &instrumentedAllocatorFree, + .deallocate = &instrumentedAllocatorDeallocate, .allocate = &instrumentedAllocatorAllocate}; return out; } static inline struct UdpardMemoryDeleter instrumentedAllocatorMakeMemoryDeleter(const InstrumentedAllocator* const self) { - const struct UdpardMemoryDeleter out = {.user_reference = (void*) self, .free = &instrumentedAllocatorFree}; + const struct UdpardMemoryDeleter out = {.user_reference = (void*) self, + .deallocate = &instrumentedAllocatorDeallocate}; return out; } diff --git a/tests/src/test_helpers.c b/tests/src/test_helpers.c index e14d5a3..0fe09ec 100644 --- a/tests/src/test_helpers.c +++ b/tests/src/test_helpers.c @@ -37,7 +37,7 @@ static void testInstrumentedAllocator(void) TEST_ASSERT_EQUAL_size_t(3, al.allocated_fragments); TEST_ASSERT_EQUAL_size_t(600, al.allocated_bytes); - resource.free(resource.user_reference, 123, a); + resource.deallocate(resource.user_reference, 123, a); TEST_ASSERT_EQUAL_size_t(2, al.allocated_fragments); TEST_ASSERT_EQUAL_size_t(477, al.allocated_bytes); @@ -45,15 +45,15 @@ static void testInstrumentedAllocator(void) TEST_ASSERT_EQUAL_size_t(3, al.allocated_fragments); TEST_ASSERT_EQUAL_size_t(577, al.allocated_bytes); - resource.free(resource.user_reference, 21, c); + resource.deallocate(resource.user_reference, 21, c); TEST_ASSERT_EQUAL_size_t(2, al.allocated_fragments); TEST_ASSERT_EQUAL_size_t(556, al.allocated_bytes); - resource.free(resource.user_reference, 100, d); + resource.deallocate(resource.user_reference, 100, d); TEST_ASSERT_EQUAL_size_t(1, al.allocated_fragments); TEST_ASSERT_EQUAL_size_t(456, al.allocated_bytes); - resource.free(resource.user_reference, 456, b); + resource.deallocate(resource.user_reference, 456, b); TEST_ASSERT_EQUAL_size_t(0, al.allocated_fragments); TEST_ASSERT_EQUAL_size_t(0, al.allocated_bytes); } diff --git a/tests/src/test_intrusive_rx.c b/tests/src/test_intrusive_rx.c index c7bde54..8330114 100644 --- a/tests/src/test_intrusive_rx.c +++ b/tests/src/test_intrusive_rx.c @@ -1936,8 +1936,8 @@ static void testSessionDeduplicate(void) InstrumentedAllocator mem_payload = {0}; instrumentedAllocatorNew(&mem_fragment); instrumentedAllocatorNew(&mem_payload); - const RxMemory mem = makeRxMemory(&mem_fragment, &mem_payload); - UdpardInternalRxSession session = {0}; + const RxMemory mem = makeRxMemory(&mem_fragment, &mem_payload); + struct UdpardInternalRxSession session = {0}; rxSessionInit(&session, mem); TEST_ASSERT_EQUAL(TIMESTAMP_UNSET, session.last_ts_usec); TEST_ASSERT_EQUAL(TRANSFER_ID_UNSET, session.last_transfer_id); @@ -2029,8 +2029,8 @@ static void testSessionAcceptA(void) InstrumentedAllocator mem_payload = {0}; instrumentedAllocatorNew(&mem_fragment); instrumentedAllocatorNew(&mem_payload); - const RxMemory mem = makeRxMemory(&mem_fragment, &mem_payload); - UdpardInternalRxSession session = {0}; + const RxMemory mem = makeRxMemory(&mem_fragment, &mem_payload); + struct UdpardInternalRxSession session = {0}; rxSessionInit(&session, mem); TEST_ASSERT_EQUAL(TIMESTAMP_UNSET, session.last_ts_usec); TEST_ASSERT_EQUAL(TRANSFER_ID_UNSET, session.last_transfer_id); @@ -2363,7 +2363,7 @@ static inline void testPortAcceptFrameA(void) TEST_ASSERT_EQUAL(4, mem_session.allocated_fragments); // New source. TEST_ASSERT_EQUAL(3, mem_fragment.allocated_fragments); TEST_ASSERT_EQUAL(3, mem_payload.allocated_fragments); - TEST_ASSERT_EQUAL(4 * sizeof(UdpardInternalRxSession), mem_session.allocated_bytes); + TEST_ASSERT_EQUAL(4 * sizeof(struct UdpardInternalRxSession), mem_session.allocated_bytes); TEST_ASSERT_EQUAL(3 * sizeof(RxFragment), mem_fragment.allocated_bytes); // Free the port instance and ensure all ifaces and sessions are cleaned up. diff --git a/tests/src/test_tx.cpp b/tests/src/test_tx.cpp index 990ab45..29a413d 100644 --- a/tests/src/test_tx.cpp +++ b/tests/src/test_tx.cpp @@ -31,7 +31,7 @@ void testInit() { const UdpardMemoryResource memory{ .user_reference = &user_referent, - .free = &dummyAllocatorFree, + .deallocate = &dummyAllocatorDeallocate, .allocate = &dummyAllocatorAllocate, }; TEST_ASSERT_EQUAL(-UDPARD_ERROR_ARGUMENT, udpardTxInit(nullptr, &node_id, 0, memory)); @@ -40,7 +40,7 @@ void testInit() UdpardTx tx{}; const UdpardMemoryResource memory{ .user_reference = &user_referent, - .free = &dummyAllocatorFree, + .deallocate = &dummyAllocatorDeallocate, .allocate = &dummyAllocatorAllocate, }; TEST_ASSERT_EQUAL(-UDPARD_ERROR_ARGUMENT, udpardTxInit(&tx, nullptr, 0, memory)); @@ -49,7 +49,7 @@ void testInit() UdpardTx tx{}; const UdpardMemoryResource memory{ .user_reference = &user_referent, - .free = &dummyAllocatorFree, + .deallocate = &dummyAllocatorDeallocate, .allocate = nullptr, }; TEST_ASSERT_EQUAL(-UDPARD_ERROR_ARGUMENT, udpardTxInit(&tx, &node_id, 0, memory)); @@ -58,7 +58,7 @@ void testInit() UdpardTx tx{}; const UdpardMemoryResource memory{ .user_reference = &user_referent, - .free = nullptr, + .deallocate = nullptr, .allocate = &dummyAllocatorAllocate, }; TEST_ASSERT_EQUAL(-UDPARD_ERROR_ARGUMENT, udpardTxInit(&tx, &node_id, 0, memory)); @@ -67,7 +67,7 @@ void testInit() UdpardTx tx{}; const UdpardMemoryResource memory{ .user_reference = &user_referent, - .free = &dummyAllocatorFree, + .deallocate = &dummyAllocatorDeallocate, .allocate = &dummyAllocatorAllocate, }; TEST_ASSERT_EQUAL(0, udpardTxInit(&tx, &node_id, 0, memory)); From 6e4a207502d8bcd36052378ae9e2d4a9632b4937 Mon Sep 17 00:00:00 2001 From: Pavel Kirienko Date: Tue, 8 Aug 2023 17:12:03 +0300 Subject: [PATCH 33/38] Add comments --- libudpard/udpard.c | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/libudpard/udpard.c b/libudpard/udpard.c index 7085d7f..0fe06cf 100644 --- a/libudpard/udpard.c +++ b/libudpard/udpard.c @@ -886,7 +886,7 @@ struct UdpardInternalRxSession /// Frees all fragments in the tree and their payload buffers. Destroys the passed fragment. /// This is meant to be invoked on the root of the tree. -// NOLINTNEXTLINE(misc-no-recursion) +// NOLINTNEXTLINE(misc-no-recursion) MISRA C:2012 rule 17.2 static inline void rxFragmentDestroyTree(RxFragment* const self, const RxMemory memory) { UDPARD_ASSERT(self != NULL); @@ -988,7 +988,7 @@ typedef struct } RxSlotEjectContext; /// See rxSlotEject() for details. -/// NOLINTNEXTLINE(misc-no-recursion) +/// NOLINTNEXTLINE(misc-no-recursion) MISRA C:2012 rule 17.2 static inline void rxSlotEjectFragment(RxFragment* const frag, RxSlotEjectContext* const ctx) { UDPARD_ASSERT((frag != NULL) && (ctx != NULL)); @@ -1048,6 +1048,7 @@ static inline void rxSlotEjectFragment(RxFragment* const frag, RxSlotEjectContex /// There shall be at least one fragment (because a Cyphal transfer contains at least one frame). /// /// The return value indicates whether the transfer is valid (CRC is correct). +/// This function performs ~log(n) of recursive calls internally, where n is the number of fragments. static inline bool rxSlotEject(size_t* const out_payload_size, struct UdpardFragment* const out_payload_head, RxFragmentTreeNode* const fragment_tree, @@ -1413,7 +1414,7 @@ static inline void rxSessionInit(struct UdpardInternalRxSession* const self, con } /// Frees all ifaces in the session, all children in the session tree recursively, and destroys the session itself. -// NOLINTNEXTLINE(*-no-recursion) +// NOLINTNEXTLINE(*-no-recursion) MISRA C:2012 rule 17.2 static inline void rxSessionDestroyTree(struct UdpardInternalRxSession* const self, const struct UdpardRxMemoryResources memory) { From 8fa5a4b966e84ddb538529d4beeafe691c83a2ef Mon Sep 17 00:00:00 2001 From: Pavel Kirienko Date: Tue, 8 Aug 2023 17:29:51 +0300 Subject: [PATCH 34/38] Split rxPortAccept into two cases: general case and anonymous case #sonar --- libudpard/udpard.c | 116 ++++++++++++++++++++++++--------------------- 1 file changed, 63 insertions(+), 53 deletions(-) diff --git a/libudpard/udpard.c b/libudpard/udpard.c index 0fe06cf..994894e 100644 --- a/libudpard/udpard.c +++ b/libudpard/udpard.c @@ -1467,7 +1467,7 @@ static inline struct UdpardTreeNode* rxPortSessionFactory(void* const user_refer } /// Accepts a frame into a port, possibly creating a new session along the way. -/// Takes ownership of the frame payload buffer. +/// The frame shall not be anonymous. Takes ownership of the frame payload buffer. static inline int_fast8_t rxPortAccept(struct UdpardRxPort* const self, const uint_fast8_t redundant_iface_index, const UdpardMicrosecond ts_usec, @@ -1476,61 +1476,64 @@ static inline int_fast8_t rxPortAccept(struct UdpardRxPort* const self struct UdpardRxTransfer* const out_transfer) { UDPARD_ASSERT((self != NULL) && (redundant_iface_index < UDPARD_NETWORK_INTERFACE_COUNT_MAX) && - (out_transfer != NULL)); - int_fast8_t result = 0; - bool release = true; - const bool anonymous = frame.meta.src_node_id == UDPARD_NODE_ID_UNSET; - if (!anonymous) + (out_transfer != NULL) && (frame.meta.src_node_id != UDPARD_NODE_ID_UNSET)); + int_fast8_t result = 0; + struct UdpardInternalRxSession* const session = (struct UdpardInternalRxSession*) (void*) + cavlSearch((struct UdpardTreeNode**) &self->sessions, + &(RxPortSessionSearchContext){.remote_node_id = frame.meta.src_node_id, .memory = memory}, + &rxPortSessionSearch, + &rxPortSessionFactory); + if (session != NULL) { - struct UdpardInternalRxSession* const session = (struct UdpardInternalRxSession*) (void*) - cavlSearch((struct UdpardTreeNode**) &self->sessions, - &(RxPortSessionSearchContext){.remote_node_id = frame.meta.src_node_id, .memory = memory}, - &rxPortSessionSearch, - &rxPortSessionFactory); - if (session != NULL) - { - UDPARD_ASSERT(session->remote_node_id == frame.meta.src_node_id); - release = false; // The callee takes ownership of the memory. - result = rxSessionAccept(session, - redundant_iface_index, - ts_usec, - frame, - self->extent, - self->transfer_id_timeout_usec, - (RxMemory){.payload = memory.payload, .fragment = memory.fragment}, - out_transfer); - } - else // Failed to allocate a new session. - { - result = -UDPARD_ERROR_MEMORY; - } + UDPARD_ASSERT(session->remote_node_id == frame.meta.src_node_id); + result = rxSessionAccept(session, // The callee takes ownership of the memory. + redundant_iface_index, + ts_usec, + frame, + self->extent, + self->transfer_id_timeout_usec, + (RxMemory){.payload = memory.payload, .fragment = memory.fragment}, + out_transfer); } - else // Valid anonymous transfers are always accepted unconditionally. + else // Failed to allocate a new session. { - const bool size_ok = frame.base.payload.size >= TRANSFER_CRC_SIZE_BYTES; - const bool crc_ok = transferCRCCompute(frame.base.payload.size, frame.base.payload.data) == - TRANSFER_CRC_RESIDUE_AFTER_OUTPUT_XOR; - if (size_ok && crc_ok) - { - result = 1; - release = false; - memZero(sizeof(*out_transfer), out_transfer); - // Copy relevant metadata from the frame. Remember that anonymous transfers are always single-frame. - out_transfer->timestamp_usec = ts_usec; - out_transfer->priority = frame.meta.priority; - out_transfer->source_node_id = frame.meta.src_node_id; - out_transfer->transfer_id = frame.meta.transfer_id; - // Manually set up the transfer payload to point to the relevant slice inside the frame payload. - out_transfer->payload.next = NULL; - out_transfer->payload.view.size = frame.base.payload.size - TRANSFER_CRC_SIZE_BYTES; - out_transfer->payload.view.data = frame.base.payload.data; - out_transfer->payload.origin = frame.base.origin; - out_transfer->payload_size = out_transfer->payload.view.size; - } + result = -UDPARD_ERROR_MEMORY; + memFreePayload(memory.payload, frame.base.origin); } - if (release) + return result; +} + +/// A special case of rxPortAccept() for anonymous transfers. Accepts all transfers unconditionally. +/// Does not allocate new memory. Takes ownership of the frame payload buffer. +static inline int_fast8_t rxPortAcceptAnonymous(const UdpardMicrosecond ts_usec, + const RxFrame frame, + const struct UdpardMemoryDeleter memory, + struct UdpardRxTransfer* const out_transfer) +{ + UDPARD_ASSERT((out_transfer != NULL) && (frame.meta.src_node_id == UDPARD_NODE_ID_UNSET)); + int_fast8_t result = 0; + const bool size_ok = frame.base.payload.size >= TRANSFER_CRC_SIZE_BYTES; + const bool crc_ok = + transferCRCCompute(frame.base.payload.size, frame.base.payload.data) == TRANSFER_CRC_RESIDUE_AFTER_OUTPUT_XOR; + if (size_ok && crc_ok) { - memFreePayload(memory.payload, frame.base.origin); + result = 1; + memZero(sizeof(*out_transfer), out_transfer); + // Copy relevant metadata from the frame. Remember that anonymous transfers are always single-frame. + out_transfer->timestamp_usec = ts_usec; + out_transfer->priority = frame.meta.priority; + out_transfer->source_node_id = frame.meta.src_node_id; + out_transfer->transfer_id = frame.meta.transfer_id; + // Manually set up the transfer payload to point to the relevant slice inside the frame payload. + out_transfer->payload.next = NULL; + out_transfer->payload.view.size = frame.base.payload.size - TRANSFER_CRC_SIZE_BYTES; + out_transfer->payload.view.data = frame.base.payload.data; + out_transfer->payload.origin = frame.base.origin; + out_transfer->payload_size = out_transfer->payload.view.size; + } + else + { + memFreePayload(memory, frame.base.origin); } return result; } @@ -1548,9 +1551,16 @@ static inline int_fast8_t rxPortAcceptFrame(struct UdpardRxPort* const RxFrame frame = {0}; if (rxParseFrame(datagram_payload, &frame)) { - result = rxPortAccept(self, redundant_iface_index, ts_usec, frame, memory, out_transfer); + if (frame.meta.src_node_id != UDPARD_NODE_ID_UNSET) + { + result = rxPortAccept(self, redundant_iface_index, ts_usec, frame, memory, out_transfer); + } + else + { + result = rxPortAcceptAnonymous(ts_usec, frame, memory.payload, out_transfer); + } } - else + else // Malformed datagram or unsupported header version, drop. { memFreePayload(memory.payload, datagram_payload); } From 306acf60d1871c90d8bb3a60859bbed24593d65e Mon Sep 17 00:00:00 2001 From: Pavel Kirienko Date: Tue, 8 Aug 2023 17:39:53 +0300 Subject: [PATCH 35/38] Comment --- libudpard/udpard.h | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/libudpard/udpard.h b/libudpard/udpard.h index d830098..0f18753 100644 --- a/libudpard/udpard.h +++ b/libudpard/udpard.h @@ -113,7 +113,9 @@ /// Said pipelines are entirely independent from each other and can be operated from different threads, /// as they share no resources. /// -/// The reception pipeline is able to accept datagrams with arbitrary MTU. +/// The reception pipeline is able to accept datagrams with arbitrary MTU, frames delivered out-of-order (OOO) with +/// arbitrary duplication, including duplication of non-adjacent frames, and/or frames interleaved between adjacent +/// transfers. The support for OOO reassembly is particularly interesting when simple repetition coding FEC is used. /// /// The application should instantiate one subscription instance per subject it needs to receive messages from, /// irrespective of the number of redundant interfaces. There needs to be one socket (or a similar abstraction @@ -758,8 +760,8 @@ struct UdpardRxTransfer /// /// If any of the arguments are NULL, the function has no effect. void udpardRxFragmentFree(const struct UdpardFragment head, - struct UdpardMemoryResource const memory_fragment, - struct UdpardMemoryDeleter const memory_payload); + const struct UdpardMemoryResource memory_fragment, + const struct UdpardMemoryDeleter memory_payload); // --------------------------------------------- SUBJECTS --------------------------------------------- From 23358bdfac18d7031ca5a65ca3aa7d455431b6ac Mon Sep 17 00:00:00 2001 From: Pavel Kirienko Date: Wed, 9 Aug 2023 12:23:08 +0300 Subject: [PATCH 36/38] Document the maximum recursion depth explicitly --- libudpard/udpard.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/libudpard/udpard.c b/libudpard/udpard.c index 994894e..5c70795 100644 --- a/libudpard/udpard.c +++ b/libudpard/udpard.c @@ -886,6 +886,7 @@ struct UdpardInternalRxSession /// Frees all fragments in the tree and their payload buffers. Destroys the passed fragment. /// This is meant to be invoked on the root of the tree. +/// The maximum recursion depth is ceil(1.44*log2(FRAME_INDEX_MAX+1)-0.328) = 22 levels. // NOLINTNEXTLINE(misc-no-recursion) MISRA C:2012 rule 17.2 static inline void rxFragmentDestroyTree(RxFragment* const self, const RxMemory memory) { @@ -988,6 +989,7 @@ typedef struct } RxSlotEjectContext; /// See rxSlotEject() for details. +/// The maximum recursion depth is ceil(1.44*log2(FRAME_INDEX_MAX+1)-0.328) = 22 levels. /// NOLINTNEXTLINE(misc-no-recursion) MISRA C:2012 rule 17.2 static inline void rxSlotEjectFragment(RxFragment* const frag, RxSlotEjectContext* const ctx) { @@ -1048,7 +1050,6 @@ static inline void rxSlotEjectFragment(RxFragment* const frag, RxSlotEjectContex /// There shall be at least one fragment (because a Cyphal transfer contains at least one frame). /// /// The return value indicates whether the transfer is valid (CRC is correct). -/// This function performs ~log(n) of recursive calls internally, where n is the number of fragments. static inline bool rxSlotEject(size_t* const out_payload_size, struct UdpardFragment* const out_payload_head, RxFragmentTreeNode* const fragment_tree, @@ -1414,6 +1415,7 @@ static inline void rxSessionInit(struct UdpardInternalRxSession* const self, con } /// Frees all ifaces in the session, all children in the session tree recursively, and destroys the session itself. +/// The maximum recursion depth is ceil(1.44*log2(UDPARD_NODE_ID_MAX+1)-0.328) = 23 levels. // NOLINTNEXTLINE(*-no-recursion) MISRA C:2012 rule 17.2 static inline void rxSessionDestroyTree(struct UdpardInternalRxSession* const self, const struct UdpardRxMemoryResources memory) From 74b30b6f0147da88583eebe8c1a8f55095ac5f34 Mon Sep 17 00:00:00 2001 From: Pavel Kirienko Date: Wed, 9 Aug 2023 16:02:31 +0300 Subject: [PATCH 37/38] Split rxSlotAccept into thre functions and remove goto --- libudpard/udpard.c | 112 +++++++++++++++++++++++++++++++-------------- 1 file changed, 78 insertions(+), 34 deletions(-) diff --git a/libudpard/udpard.c b/libudpard/udpard.c index 5c70795..473ca55 100644 --- a/libudpard/udpard.c +++ b/libudpard/udpard.c @@ -944,6 +944,13 @@ static inline void rxSlotRestart(RxSlot* const self, const UdpardTransferID tran self->payload_size = 0; } +/// This is a helper for rxSlotRestart that restarts the transfer for the next transfer-ID value. +/// The transfer-ID increment is necessary to weed out duplicate transfers. +static inline void rxSlotRestartAdvance(RxSlot* const self, const RxMemory memory) +{ + rxSlotRestart(self, self->transfer_id + 1U, memory); +} + typedef struct { uint32_t frame_index; @@ -1089,71 +1096,77 @@ static inline bool rxSlotEject(size_t* const out_payload_size, return result; } -/// This function will either move the frame payload into the session, or free it if it can't be used. -/// Upon return, certain state variables may be overwritten, so the caller should not rely on them. -/// Returns: 1 -- transfer available, payload written; 0 -- transfer not yet available; <0 -- error. -static inline int_fast8_t rxSlotAccept(RxSlot* const self, - size_t* const out_transfer_payload_size, - struct UdpardFragment* const out_transfer_payload_head, - const RxFrameBase frame, - const size_t extent, - const RxMemory memory) +/// Update the frame count discovery state in this transfer. +/// Returns true on success, false if inconsistencies are detected and the slot should be restarted. +static inline bool rxSlotAccept_UpdateFrameCountDiscovery(RxSlot* const self, const RxFrameBase frame) { - UDPARD_ASSERT((self != NULL) && (frame.payload.size > 0) && (out_transfer_payload_size != NULL) && - (out_transfer_payload_head != NULL)); - int_fast8_t result = 0; - bool restart = false; - bool release = true; - // FIRST: Update the frame count discovery state in this transfer. Drop transfer if inconsistencies are detected. + UDPARD_ASSERT((self != NULL) && (frame.payload.size > 0)); + bool ok = true; self->max_index = max32(self->max_index, frame.index); if (frame.end_of_transfer) { if ((self->eot_index != FRAME_INDEX_UNSET) && (self->eot_index != frame.index)) { - restart = true; // Inconsistent EOT flag, could be a node-ID conflict. - goto finish; // NOSONAR goto simplifies the control flow and is not prohibited by MISRA C:2012. + ok = false; // Inconsistent EOT flag, could be a node-ID conflict. } self->eot_index = frame.index; } UDPARD_ASSERT(frame.index <= self->max_index); if (self->max_index > self->eot_index) { - restart = true; // Frames past EOT found, discard the entire transfer because we don't trust it anymore. - goto finish; // NOSONAR goto simplifies the control flow and is not prohibited by MISRA C:2012. + ok = false; // Frames past EOT found, discard the entire transfer because we don't trust it anymore. } - // SECOND: Insert the fragment into the fragment tree. If it already exists, drop and free the duplicate. - UDPARD_ASSERT((self->max_index <= self->eot_index) && (self->accepted_frames <= self->eot_index)); + return ok; +} + +/// Insert the fragment into the fragment tree. If it already exists, drop and free the duplicate. +/// Returns 0 if the fragment is not needed, 1 if it is needed, negative on error. +/// The fragment shall be deallocated unless the return value is 1. +static inline int_fast8_t rxSlotAccept_InsertFragment(RxSlot* const self, + const RxFrameBase frame, + const RxMemory memory) +{ + UDPARD_ASSERT((self != NULL) && (frame.payload.size > 0) && (self->max_index <= self->eot_index) && + (self->accepted_frames <= self->eot_index)); RxSlotUpdateContext update_ctx = {.frame_index = frame.index, .accepted = false, .memory_fragment = memory.fragment}; - RxFragmentTreeNode* const frag = (RxFragmentTreeNode*) cavlSearch((struct UdpardTreeNode**) &self->fragments, // + RxFragmentTreeNode* const frag = (RxFragmentTreeNode*) cavlSearch((struct UdpardTreeNode**) &self->fragments, // &update_ctx, &rxSlotFragmentSearch, &rxSlotFragmentFactory); + int_fast8_t result = update_ctx.accepted ? 1 : 0; if (frag == NULL) { UDPARD_ASSERT(!update_ctx.accepted); - UDPARD_ASSERT(release); result = -UDPARD_ERROR_MEMORY; // No restart because there is hope that there will be enough memory when we receive a duplicate. - goto finish; // NOSONAR goto simplifies the control flow and is not prohibited by MISRA C:2012. } UDPARD_ASSERT(self->max_index <= self->eot_index); if (update_ctx.accepted) { - UDPARD_ASSERT(frag->this->frame_index == frame.index); + UDPARD_ASSERT((result > 0) && (frag->this->frame_index == frame.index)); frag->this->base.view = frame.payload; frag->this->base.origin = frame.origin; self->payload_size += frame.payload.size; self->accepted_frames++; - release = false; // Ownership of the payload buffer has been transferred to the fragment tree. } - // THIRD: Detect transfer completion. If complete, eject the payload from the fragment tree and check its CRC. - UDPARD_ASSERT(self->fragments != NULL); + return result; +} + +/// Detect transfer completion. If complete, eject the payload from the fragment tree and check its CRC. +/// The return value is passed over from rxSlotEject. +static inline int_fast8_t rxSlotAccept_FinalizeMaybe(RxSlot* const self, + size_t* const out_transfer_payload_size, + struct UdpardFragment* const out_transfer_payload_head, + const size_t extent, + const RxMemory memory) +{ + UDPARD_ASSERT((self != NULL) && (out_transfer_payload_size != NULL) && (out_transfer_payload_head != NULL) && + (self->fragments != NULL)); + int_fast8_t result = 0; if (self->accepted_frames > self->eot_index) // Mind the off-by-one: cardinal vs. ordinal. { - // This transfer is done but we don't know yet if it's valid. Either way, we need to prepare for the next one. - restart = true; if (self->payload_size >= TRANSFER_CRC_SIZE_BYTES) { result = rxSlotEject(out_transfer_payload_size, @@ -1167,17 +1180,48 @@ static inline int_fast8_t rxSlotAccept(RxSlot* const self, // The tree is now unusable and the data is moved into rx_transfer. self->fragments = NULL; } + rxSlotRestartAdvance(self, memory); // Restart needed even if invalid. } -finish: - if (restart) + return result; +} + +/// This function will either move the frame payload into the session, or free it if it can't be used. +/// Upon return, certain state fields may be overwritten, so the caller should not rely on them. +/// Returns: 1 -- transfer available, payload written; 0 -- transfer not yet available; <0 -- error. +static inline int_fast8_t rxSlotAccept(RxSlot* const self, + size_t* const out_transfer_payload_size, + struct UdpardFragment* const out_transfer_payload_head, + const RxFrameBase frame, + const size_t extent, + const RxMemory memory) +{ + UDPARD_ASSERT((self != NULL) && (frame.payload.size > 0) && (out_transfer_payload_size != NULL) && + (out_transfer_payload_head != NULL)); + int_fast8_t result = 0; + bool release = true; + if (rxSlotAccept_UpdateFrameCountDiscovery(self, frame)) { - // Increment is necessary to weed out duplicate transfers. - rxSlotRestart(self, self->transfer_id + 1U, memory); + result = rxSlotAccept_InsertFragment(self, frame, memory); + UDPARD_ASSERT(result <= 1); + if (result > 0) + { + release = false; + result = rxSlotAccept_FinalizeMaybe(self, // + out_transfer_payload_size, + out_transfer_payload_head, + extent, + memory); + } + } + else + { + rxSlotRestartAdvance(self, memory); } if (release) { memFreePayload(memory.payload, frame.origin); } + UDPARD_ASSERT(result <= 1); return result; } From fd7f93044e30bbc944aa45127257f13b7178136a Mon Sep 17 00:00:00 2001 From: Pavel Kirienko Date: Wed, 9 Aug 2023 16:09:07 +0300 Subject: [PATCH 38/38] SONAR --- libudpard/udpard.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libudpard/udpard.c b/libudpard/udpard.c index 473ca55..123208a 100644 --- a/libudpard/udpard.c +++ b/libudpard/udpard.c @@ -1098,7 +1098,7 @@ static inline bool rxSlotEject(size_t* const out_payload_size, /// Update the frame count discovery state in this transfer. /// Returns true on success, false if inconsistencies are detected and the slot should be restarted. -static inline bool rxSlotAccept_UpdateFrameCountDiscovery(RxSlot* const self, const RxFrameBase frame) +static inline bool rxSlotAccept_UpdateFrameCount(RxSlot* const self, const RxFrameBase frame) { UDPARD_ASSERT((self != NULL) && (frame.payload.size > 0)); bool ok = true; @@ -1199,7 +1199,7 @@ static inline int_fast8_t rxSlotAccept(RxSlot* const self, (out_transfer_payload_head != NULL)); int_fast8_t result = 0; bool release = true; - if (rxSlotAccept_UpdateFrameCountDiscovery(self, frame)) + if (rxSlotAccept_UpdateFrameCount(self, frame)) { result = rxSlotAccept_InsertFragment(self, frame, memory); UDPARD_ASSERT(result <= 1);