Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -592,6 +592,7 @@ jobs:
-IPlatform/MbedTls/Interface
-IPlatform/FreeRtos/Interface
-IPlatform/PlusTcp/Interface
-IPlatform/LwipRaw/Interface
-IPlatform/FatFs/Interface
--xml --xml-version=2
Core/Source/
Expand All @@ -602,6 +603,7 @@ jobs:
Platform/MbedTls/Source/
Platform/FreeRtos/Source/
Platform/PlusTcp/Source/
Platform/LwipRaw/Source/
Platform/FatFs/Source/
2> build/cppcheck/cppcheck-report.xml

Expand Down Expand Up @@ -637,6 +639,7 @@ jobs:
-IPlatform/MbedTls/Interface
-IPlatform/FreeRtos/Interface
-IPlatform/PlusTcp/Interface
-IPlatform/LwipRaw/Interface
-IPlatform/FatFs/Interface
Core/Source/
Platform/Atomics/Source/
Expand All @@ -646,6 +649,7 @@ jobs:
Platform/MbedTls/Source/
Platform/FreeRtos/Source/
Platform/PlusTcp/Source/
Platform/LwipRaw/Source/
Platform/FatFs/Source/

- name: Generate cppcheck-misra XML report
Expand All @@ -666,6 +670,7 @@ jobs:
-IPlatform/MbedTls/Interface \
-IPlatform/FreeRtos/Interface \
-IPlatform/PlusTcp/Interface \
-IPlatform/LwipRaw/Interface \
-IPlatform/FatFs/Interface \
--xml --xml-version=2 \
Core/Source/ \
Expand All @@ -676,6 +681,7 @@ jobs:
Platform/MbedTls/Source/ \
Platform/FreeRtos/Source/ \
Platform/PlusTcp/Source/ \
Platform/LwipRaw/Source/ \
Platform/FatFs/Source/ \
2> build/cppcheck-misra/cppcheck-misra-report.xml

Expand Down
2 changes: 2 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,8 @@ live under `Core/Interface/`; platform-specific helpers (the `SolidSyslogPosix*`
| `SolidSyslogNullDatagram.h` | Any code installing a no-op datagram slot (Open/Close are no-ops, SendTo returns `SENT` to drop on the floor so the Store does not fill with undeliverables, MaxPayload returns the IPv6-safe default) | `SolidSyslogNullDatagram_Get` |
| `SolidSyslogPlusTcpDatagram.h` | System setup code on FreeRTOS targets using FreeRTOS-Plus-TCP for UDP | `SolidSyslogPlusTcpDatagram_Create(void)`, `_Destroy(base)` (wraps `FreeRTOS_socket` / `FreeRTOS_sendto` / `FreeRTOS_closesocket`). Instance struct lives in a library-internal static pool (E11). Pool-exhaustion fallback is the shared `SolidSyslogNullDatagram`. |
| `SolidSyslogPlusTcpDatagramErrors.h` | Any code installing an error handler that wants to react to PlusTcpDatagram-specific events (pointer-identity match on `PlusTcpDatagramErrorSource`, switch on `enum SolidSyslogPlusTcpDatagramErrors`) | `enum SolidSyslogPlusTcpDatagramErrors` (`PLUSTCPDATAGRAM_ERROR_*` codes + `PLUSTCPDATAGRAM_ERROR_MAX` bookend), `extern const struct SolidSyslogErrorSource PlusTcpDatagramErrorSource`. Integrators ignore if not handling errors per source. |
| `SolidSyslogLwipRawDatagram.h` | System setup code on **any target using lwIP Raw API** wiring a UDP sender | `SolidSyslogLwipRawDatagram_Create(void)`, `_Destroy(base)` (wraps `udp_new` / `udp_sendto` / `udp_remove`; PBUF_REF zero-copy send; relies on lwIP `ARP_QUEUEING` for cache-miss recovery). Instance struct lives in a library-internal static pool (E11). Pool-exhaustion fallback is the shared `SolidSyslogNullDatagram`. |
| `SolidSyslogLwipRawDatagramErrors.h` | Any code installing an error handler that wants to react to LwipRawDatagram-specific events (pointer-identity match on `LwipRawDatagramErrorSource`, switch on `enum SolidSyslogLwipRawDatagramErrors`) | `enum SolidSyslogLwipRawDatagramErrors` (`LWIPRAWDATAGRAM_ERROR_*` codes + `LWIPRAWDATAGRAM_ERROR_MAX` bookend), `extern const struct SolidSyslogErrorSource LwipRawDatagramErrorSource`. Integrators ignore if not handling errors per source. |
| `SolidSyslogWinsockDatagramErrors.h` | Any code installing an error handler that wants to react to WinsockDatagram-specific events (pointer-identity match on `WinsockDatagramErrorSource`, switch on `enum SolidSyslogWinsockDatagramErrors`) | `enum SolidSyslogWinsockDatagramErrors` (`WINSOCKDATAGRAM_ERROR_*` codes + `WINSOCKDATAGRAM_ERROR_MAX` bookend), `extern const struct SolidSyslogErrorSource WinsockDatagramErrorSource`. Integrators ignore if not handling errors per source. |
| `SolidSyslogPosixDatagramErrors.h` | Any code installing an error handler that wants to react to PosixDatagram-specific events (pointer-identity match on `PosixDatagramErrorSource`, switch on `enum SolidSyslogPosixDatagramErrors`) | `enum SolidSyslogPosixDatagramErrors` (`POSIXDATAGRAM_ERROR_*` codes + `POSIXDATAGRAM_ERROR_MAX` bookend), `extern const struct SolidSyslogErrorSource PosixDatagramErrorSource`. Integrators ignore if not handling errors per source. |
| `SolidSyslogStream.h` | Sender implementors using stream transport | `SolidSyslogStream_Open`, `_Send`, `_Read`, `_Close`, `SolidSyslogSsize` |
Expand Down
4 changes: 2 additions & 2 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -169,12 +169,12 @@ endif()
if(SOLIDSYSLOG_FREERTOS_NET STREQUAL "LWIP")
message(STATUS
"SOLIDSYSLOG_FREERTOS_NET=LWIP: lwIP backend partial — "
"Address+Resolver only; Datagram/TcpStream/BDD arrive in S28.04-S28.06.")
"Address+Resolver+Datagram only; TcpStream/BDD arrive in S28.05-S28.06.")
endif()
if(SOLIDSYSLOG_FREERTOS_NET STREQUAL "BOTH")
message(STATUS
"SOLIDSYSLOG_FREERTOS_NET=BOTH: lwIP backend partial — "
"Address+Resolver only; Datagram/TcpStream/BDD arrive in S28.04-S28.06.")
"Address+Resolver+Datagram only; TcpStream/BDD arrive in S28.05-S28.06.")
endif()

# Build-time tunables (E21: Port-Time Configurability). Integrators override
Expand Down
19 changes: 19 additions & 0 deletions Core/Interface/SolidSyslogTunablesDefaults.h
Original file line number Diff line number Diff line change
Expand Up @@ -569,6 +569,25 @@
#error "SOLIDSYSLOG_LWIP_RAW_RESOLVER_POOL_SIZE must be >= 1"
#endif

/*
* Number of SolidSyslogLwipRawDatagram instances the library's internal
* static pool can simultaneously hold. Each instance carries a single
* struct udp_pcb pointer the wrapper holds across Open/SendTo/Close.
*
* Default 1 — almost all lwIP Raw integrators wire a single datagram into
* a UdpSender. Bump via SOLIDSYSLOG_USER_TUNABLES_FILE if more than one
* is genuinely needed.
*
* Floor: 1. Sub-floor values rejected at compile time.
*/
#ifndef SOLIDSYSLOG_LWIP_RAW_DATAGRAM_POOL_SIZE
#define SOLIDSYSLOG_LWIP_RAW_DATAGRAM_POOL_SIZE 1U
#endif

#if SOLIDSYSLOG_LWIP_RAW_DATAGRAM_POOL_SIZE < 1
#error "SOLIDSYSLOG_LWIP_RAW_DATAGRAM_POOL_SIZE must be >= 1"
#endif

/*
* Number of SolidSyslogPlusTcpTcpStream instances the library's
* internal static pool can simultaneously hold. Each instance carries
Expand Down
97 changes: 97 additions & 0 deletions DEVLOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,102 @@
# Dev Log

## 2026-05-27 — S28.04 SolidSyslogLwipRawDatagram

Fourth story in E28 (#463, parent #439). UDP datagram adapter for the
lwIP Raw API. Same three-TU pool shape as S28.03; the new ground was
the lwIP calling convention (`pbuf` allocation discipline + ARP
behaviour) and a test-shape refactor that lifted the lwIP fakes into
their own directory and pinned down a leak invariant.

### Design decisions

1. **`pbuf` strategy — PBUF_REF + `pbuf_alloc` per send.** SendTo
allocates a header from lwIP's `MEMP_PBUF` pool with type
`PBUF_REF`, points `payload` at the caller's buffer, calls
`udp_sendto`, and `pbuf_free`s on every exit. Mid-commit-3 David
reopened the question with a MISRA hat (Rule 21.3, no dynamic
allocation): the alternative was embedding a `struct pbuf` in
`SolidSyslogLwipRawDatagram` and skipping alloc/free entirely.
Conclusion: keep PBUF_REF. Embedding the pbuf would save *one*
`MEMP_PBUF` slot per send, but lwIP's `udp_sendto` still internally
allocates a header pbuf to prepend the UDP header. The asymmetry
doesn't justify the manual `ref`/`type` init and the latent risk
that lwIP later changes assumptions about who owns the pbuf. The
project's no-dynamic-allocation philosophy applies at *our* code
boundary; lwIP's pool is its concern.

2. **ARP cache miss — delegate to lwIP.** No manual ARP probe in
our SendTo (the PlusTcpDatagram pattern, where the host stack does
*not* queue on cache miss). lwIP's `etharp_query` fires the ARP
request and queues the pbuf when `ARP_QUEUEING=1` (the lwIP
compiled-in default), then drops it onto the wire when the reply
lands. First datagram survives. The `ARP_QUEUEING=0` integrator
footgun lives in S28.05's integrator guide, not here. Keeps the
wrapper netif-agnostic — no `netif*` argument, no `netif_default`
dependence.

3. **Failure mapping.** `udp_sendto` → `ERR_OK` maps to `SENT`;
everything else maps to `FAILED`. **No `OVERSIZE`** — lwIP has no
clean oversize signal, and the upstream `Service` algorithm already
honours `MaxPayload()` before reaching SendTo.

4. **`MaxPayload()` returns 1232 unconditionally.** `SOLIDSYSLOG_UDP_IPV6_SAFE_PAYLOAD`,
same as PlusTcp. No `netif_get_mtu()` plumbing — the IPv6-safe
default covers every interface this adapter is realistically
wired against.

### Test-shape work

The test file grew through three refactor passes (commits 4–6 on the
branch) responding to David's feedback. Worth recording the shape it
settled into — applicable to every future pool-allocated adapter:

- **`TEST_BASE` + two `TEST_GROUP_BASE` derivations** —
`SolidSyslogLwipRawDatagram` (Created-only) and
`SolidSyslogLwipRawDatagramOpen` (Open in setup). Most lifecycle
tests don't need to repeat `SolidSyslogDatagram_Open(datagram)` in
the body; the assertion is what matters. The two-group split keeps
setup focused on the precondition each test class shares.

- **Leak invariants in the shared teardown.** `LwipUdpFake_OutstandingPcbCount()`
and `LwipPbufFake_OutstandingPbufCount()` track successful
alloc-minus-free per resource, asserted to be zero in every
teardown. Earned its keep at commit 6 — the invariant caught the
missing `pbuf_free` before the explicit `SendToFreesPbufAfterSendto`
test landed. **Pattern is general** — extend to other adapter
fakes whenever the production owns alloc/free of a host resource.

- **`sendBytes(size_t length = 1U)` helper on the base** — most
SendTo tests just need *a* send, default-1 byte. Tests that observe
length pass it explicitly. Cut ~7 lines per test of `SolidSyslogDatagram_SendTo(datagram, "x", 1, address)`
boilerplate.

### New tunable, new error source

- `SOLIDSYSLOG_LWIP_RAW_DATAGRAM_POOL_SIZE` default `1U` — pool of
one is the dominant integrator shape (single UDP sender).
- `LwipRawDatagramErrorSource` with `POOL_EXHAUSTED` /
`UNKNOWN_DESTROY`, matching every other pool class.

### LwipFakes evolution

Two new fake files: `LwipUdpFake.{c,h}` (`udp_new`, `udp_remove`,
`udp_sendto` + spies + failure injection) and `LwipPbufFake.{c,h}`
(`pbuf_alloc`, `pbuf_free` + spies + alloc-fail injection).
`LwipUpstreamParser` is not pulled in — none of these calls need
real lwIP code. Sources inlined per-test-executable via
`LWIP_FAKE_SOURCES` variable in `Tests/Lwip/CMakeLists.txt`,
matching the FreeRtosFakes / FatFsFakes precedent.

### Next up

S28.05 — `SolidSyslogLwipRawTcpStream`. Stream side of the same
adapter pair: `tcp_new` / `tcp_connect` / `tcp_write` /
`tcp_recv` / `tcp_close`, with the receive callback bridging into
the synchronous `Stream_Read` contract (the part of lwIP Raw that
genuinely differs from sockets — a tiny pending-data ring the
adapter owns).

## 2026-05-26 — S24.12 inline cppcheck-suppress and NOLINT audit

E24 code-hygiene story #461. The session-end of S24.11 left 213 inline
Expand Down
15 changes: 15 additions & 0 deletions Platform/LwipRaw/Interface/SolidSyslogLwipRawDatagram.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#ifndef SOLIDSYSLOGLWIPRAWDATAGRAM_H
#define SOLIDSYSLOGLWIPRAWDATAGRAM_H

#include "ExternC.h"

EXTERN_C_BEGIN

struct SolidSyslogDatagram;

struct SolidSyslogDatagram* SolidSyslogLwipRawDatagram_Create(void);
void SolidSyslogLwipRawDatagram_Destroy(struct SolidSyslogDatagram * base);

EXTERN_C_END

#endif /* SOLIDSYSLOGLWIPRAWDATAGRAM_H */
21 changes: 21 additions & 0 deletions Platform/LwipRaw/Interface/SolidSyslogLwipRawDatagramErrors.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#ifndef SOLIDSYSLOGLWIPRAWDATAGRAMERRORS_H
#define SOLIDSYSLOGLWIPRAWDATAGRAMERRORS_H

#include "ExternC.h"

EXTERN_C_BEGIN

struct SolidSyslogErrorSource;

enum SolidSyslogLwipRawDatagramErrors
{
LWIPRAWDATAGRAM_ERROR_POOL_EXHAUSTED,
LWIPRAWDATAGRAM_ERROR_UNKNOWN_DESTROY,
LWIPRAWDATAGRAM_ERROR_MAX
};

extern const struct SolidSyslogErrorSource LwipRawDatagramErrorSource;

EXTERN_C_END

#endif /* SOLIDSYSLOGLWIPRAWDATAGRAMERRORS_H */
110 changes: 110 additions & 0 deletions Platform/LwipRaw/Source/SolidSyslogLwipRawDatagram.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
#include "SolidSyslogLwipRawDatagramPrivate.h"

#include <stdbool.h>
#include <stddef.h>

#include "lwip/arch.h"
#include "lwip/err.h"
#include "lwip/pbuf.h"
#include "lwip/udp.h"
#include "SolidSyslogDatagram.h"
#include "SolidSyslogDatagramDefinition.h"
#include "SolidSyslogLwipRawAddressPrivate.h"
#include "SolidSyslogNullDatagram.h"
#include "SolidSyslogUdpPayload.h"

struct SolidSyslogAddress;

static bool LwipRawDatagram_Open(struct SolidSyslogDatagram* base);
static void LwipRawDatagram_Close(struct SolidSyslogDatagram* base);
static enum SolidSyslogDatagramSendResult LwipRawDatagram_SendTo(
struct SolidSyslogDatagram* base,
const void* buffer,
size_t size,
const struct SolidSyslogAddress* addr
);
static size_t LwipRawDatagram_MaxPayload(struct SolidSyslogDatagram* base);

static inline struct SolidSyslogLwipRawDatagram* LwipRawDatagram_SelfFromBase(struct SolidSyslogDatagram* base);
static inline bool LwipRawDatagram_IsOpen(const struct SolidSyslogLwipRawDatagram* self);

void LwipRawDatagram_Initialise(struct SolidSyslogDatagram* base)
{
struct SolidSyslogLwipRawDatagram* self = LwipRawDatagram_SelfFromBase(base);
self->Base.Open = LwipRawDatagram_Open;
self->Base.Close = LwipRawDatagram_Close;
self->Base.SendTo = LwipRawDatagram_SendTo;
self->Base.MaxPayload = LwipRawDatagram_MaxPayload;
self->Pcb = NULL;
}

static inline struct SolidSyslogLwipRawDatagram* LwipRawDatagram_SelfFromBase(struct SolidSyslogDatagram* base)
{
return (struct SolidSyslogLwipRawDatagram*) base;
}

void LwipRawDatagram_Cleanup(struct SolidSyslogDatagram* base)
{
LwipRawDatagram_Close(base);
/* Overwrite the abstract base with the shared NullDatagram vtable so
* use-after-destroy is a safe no-op rather than a NULL-fn-pointer crash. */
*base = *SolidSyslogNullDatagram_Get();
}

static void LwipRawDatagram_Close(struct SolidSyslogDatagram* base)
{
struct SolidSyslogLwipRawDatagram* self = LwipRawDatagram_SelfFromBase(base);
if (LwipRawDatagram_IsOpen(self))
{
udp_remove(self->Pcb);
self->Pcb = NULL;
}
}

static bool LwipRawDatagram_Open(struct SolidSyslogDatagram* base)
{
struct SolidSyslogLwipRawDatagram* self = LwipRawDatagram_SelfFromBase(base);
if (!LwipRawDatagram_IsOpen(self))
{
self->Pcb = udp_new();
}
return LwipRawDatagram_IsOpen(self);
}

static inline bool LwipRawDatagram_IsOpen(const struct SolidSyslogLwipRawDatagram* self)
{
return self->Pcb != NULL;
}

static enum SolidSyslogDatagramSendResult LwipRawDatagram_SendTo(
struct SolidSyslogDatagram* base,
const void* buffer,
size_t size,
const struct SolidSyslogAddress* addr
)
{
struct SolidSyslogLwipRawDatagram* self = LwipRawDatagram_SelfFromBase(base);
enum SolidSyslogDatagramSendResult result = SOLIDSYSLOG_DATAGRAM_SEND_RESULT_FAILED;
if (LwipRawDatagram_IsOpen(self))
{
struct pbuf* p = pbuf_alloc(PBUF_TRANSPORT, (u16_t) size, PBUF_REF);
if (p != NULL)
{
const struct SolidSyslogLwipRawAddress* dst = SolidSyslogLwipRawAddress_AsConst(addr);
p->payload = (void*) buffer;
err_t err = udp_sendto(self->Pcb, p, &dst->Ip, dst->Port);
(void) pbuf_free(p);
if (err == ERR_OK)
{
result = SOLIDSYSLOG_DATAGRAM_SEND_RESULT_SENT;
}
}
}
return result;
}

static size_t LwipRawDatagram_MaxPayload(struct SolidSyslogDatagram* base)
{
(void) base;
return SOLIDSYSLOG_UDP_IPV6_SAFE_PAYLOAD;
}
23 changes: 23 additions & 0 deletions Platform/LwipRaw/Source/SolidSyslogLwipRawDatagramMessages.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#include <stdint.h>

#include "SolidSyslogError.h"
#include "SolidSyslogLwipRawDatagramErrors.h"

static const char* LwipRawDatagramError_AsString(uint8_t code)
{
static const char* const messages[LWIPRAWDATAGRAM_ERROR_MAX] = {
[LWIPRAWDATAGRAM_ERROR_POOL_EXHAUSTED] =
"SolidSyslogLwipRawDatagram_Create pool exhausted; returning fallback NullDatagram",
[LWIPRAWDATAGRAM_ERROR_UNKNOWN_DESTROY] =
"SolidSyslogLwipRawDatagram_Destroy called with a handle not issued by this pool",
};
const char* result = "unknown";
if (code < (uint8_t) LWIPRAWDATAGRAM_ERROR_MAX)
{
enum SolidSyslogLwipRawDatagramErrors typed = (enum SolidSyslogLwipRawDatagramErrors) code;
result = messages[typed];
}
return result;
}

const struct SolidSyslogErrorSource LwipRawDatagramErrorSource = {"LwipRawDatagram", LwipRawDatagramError_AsString};
17 changes: 17 additions & 0 deletions Platform/LwipRaw/Source/SolidSyslogLwipRawDatagramPrivate.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#ifndef SOLIDSYSLOGLWIPRAWDATAGRAMPRIVATE_H
#define SOLIDSYSLOGLWIPRAWDATAGRAMPRIVATE_H

#include "SolidSyslogDatagramDefinition.h"

struct udp_pcb;

struct SolidSyslogLwipRawDatagram
{
struct SolidSyslogDatagram Base;
struct udp_pcb* Pcb;
};

void LwipRawDatagram_Initialise(struct SolidSyslogDatagram* base);
void LwipRawDatagram_Cleanup(struct SolidSyslogDatagram* base);

#endif /* SOLIDSYSLOGLWIPRAWDATAGRAMPRIVATE_H */
Loading
Loading