Skip to content

feat: port ring_buf (sys/ring_buffer.h) + pre-public copyright audit (#45)#50

Merged
swoisz merged 9 commits into
mainfrom
feature/ring-buf
Jun 10, 2026
Merged

feat: port ring_buf (sys/ring_buffer.h) + pre-public copyright audit (#45)#50
swoisz merged 9 commits into
mainfrom
feature/ring-buf

Conversation

@swoisz

@swoisz swoisz commented Jun 8, 2026

Copy link
Copy Markdown
Collaborator

Summary

Ports Zephyr's ring_buf (byte-mode) — the canonical companion to the UART IRQ pattern and the byte transport the direct-UART shell (#31) and a modbus serial backend want — and folds in a small pre-public licensing audit that the port's attribution discussion surfaced.

ring_buf port

Near-verbatim port of upstream include/zephyr/sys/ring_buffer.h + lib/utils/ring_buffer.c (Apache-2.0). Pure arithmetic + memcpy; no kernel dependencies, nothing blocks.

  • Byte-mode API: ring_buf_init, RING_BUF_INIT, RING_BUF_DECLARE, reset, size/space/capacity_get, is_empty, put/get/peek, and the zero-copy *_claim/*_finish pairs.
  • Index scheme is upstream's base-offset design (head/tail/base of ring_buf_idx_t; byte offset = head - base with one wrap correction; RING_BUFFER_MAX_SIZE caps size to half the index range so unsigned subtraction disambiguates full vs empty). CONFIG_RING_BUFFER_LARGE widens indices to 32-bit, matching upstream.
  • Adaptations (documented in-file): Boreas include layout; upstream lowercase min()MIN; new toolchain shims in sys/util.h (__ASSERT_NO_MSG, likely/unlikely, __noinit → zero-init storage with a documented limitation note, __deprecated); __ASSERT made self-contained (#include <stdlib.h>).
  • Deliberate divergence: the upstream item-mode API (ring_buf_item_put/_get, RING_BUF_ITEM_DECLARE*) is not ported — it's @deprecated upstream ("use <zephyr/sys/ringq.h>") and unused by the byte-transport consumers this targets.
  • Concurrency is unchanged from upstream: lock-free for single-producer/single-consumer in separate contexts; multiple producers/consumers must serialize externally.

15 tests: accounting, round-trip, capacity saturation (incl. put_claim returning 0 when full), partial get, NULL-discard, peek-no-consume, reset, claim/finish (put+get), surplus return, over-claim -EINVAL, space_get across a wrap, and the wrap-sensitive cases — claim splits at the physical end, put/get spanning the wrap, and a 20000-cycle stream through a 7-byte (non-power-of-two) buffer that crosses the uint16_t index boundary and exercises base advancement.

Pre-public copyright audit

A three-agent review (upstream fidelity, Boreas conformance, adversarial index arithmetic) plus a repo-wide licensing classification. The adversarial probe found zero constructible failures — the UINT16_MAX/2 cap is exact (size=32768 would alias head-base to 0 mod 65536 and is rejected at build), and the non-power-of-two × uint16-wrap case is safe (traced 10 boundary crossings, byte-perfect).

The licensing pass compared every Zephyr-API file against upstream by actual code, not names, and retains the upstream copyright only where copyrightable expression was genuinely copied (Apache-2.0 §4(c)):

  • sys/ring_buffer.h + ring_buf.c(c) 2015 Intel Corporation
  • sys/util.h(c) 2011-2014 Wind River Systems, Inc. (the IS_ENABLED _XXXX##/_YYYY token trick is verbatim)
  • sys/byteorder.h(c) 2015-2016 Intel Corporation (32-bit be/le chaining mirrors upstream)

Files confirmed original and deliberately left un-attributed (false attribution avoided): dlist.h, slist.h, atomic.h, time_units.h, and all of zshell/zsys/zdevice — independent implementations over ESP-IDF/FreeRTOS whose match to Zephyr is API-surface only.

Test plan

  • linux: 221/0 ×3 (206 + 15)
  • ESP32-S3: 250/0 on hardware (235 + 15)
  • clang-format 21.1.8 clean
  • S3 build: ring_buf symbols verified in the ELF

Closes #45

🤖 Generated with Claude Code

swoisz and others added 8 commits June 7, 2026 19:54
Near-verbatim port of upstream's ring_buffer.h + lib/utils/ring_buffer.c
(both Apache-2.0), the canonical companion to the UART IRQ pattern and
the byte transport the direct-UART shell (#31) and a modbus serial
backend want. Pure arithmetic + memcpy; no kernel dependencies, nothing
blocks.

Byte-mode API: ring_buf_init, RING_BUF_DECLARE, reset, size/space/
capacity_get, is_empty, put/get/peek, and the zero-copy claim/finish
pairs. Indices use upstream's base-offset scheme (head/tail/base of
ring_buf_idx_t, byte offset = head-base with one wrap correction,
RING_BUFFER_MAX_SIZE caps size to half the index range so unsigned
subtraction disambiguates full vs empty). CONFIG_RING_BUFFER_LARGE
widens indices to 32-bit, matching upstream.

Adaptations for Boreas (documented in the files):
- include layout (zephyr/sys/util.h) and lowercase upstream min() -> MIN.
- toolchain shims added to sys/util.h: __ASSERT_NO_MSG, likely/unlikely,
  __noinit (mapped to zero-init storage -- correct here, and avoids
  __NOINIT_ATTR wrongly surviving deep sleep), __deprecated. Also made
  __ASSERT self-contained (#include <stdlib.h> for abort()).

Deliberate divergence: the upstream item-mode API (ring_buf_item_put/
_get, RING_BUF_ITEM_DECLARE*) is NOT ported -- it is @deprecated
upstream ("use <zephyr/sys/ringq.h>") and unused by the byte-transport
consumers this targets.

Concurrency is unchanged: lock-free for single-producer/single-consumer
in separate contexts; multiple producers/consumers must serialize
externally.

Tests (15): accounting, round-trip, capacity saturation, partial get,
NULL-discard, peek-no-consume, reset, claim/finish (put+get), surplus
return, over-claim -EINVAL, and the wrap-sensitive cases -- claim splits
at the physical end, put/get spanning the wrap, and a 20000-cycle stream
through a 7-byte (non-power-of-two) buffer exercising base advancement
and the index arithmetic across many wraps.

linux suite: 221/0 x3 (206 + 15).

Closes #45

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tests

Review fan-out (fidelity + conformance + adversarial) findings folded:

- Restore the public RING_BUF_INIT macro (upstream API completeness;
  RING_BUF_DECLARE is now defined in terms of it, matching upstream
  structure). A downstream `struct ring_buf rb = RING_BUF_INIT(...)`
  now compiles.
- Retain upstream's "Copyright (c) 2015 Intel Corporation" line on the
  two verbatim files alongside the Intercreate port line (Apache-2.0
  4(c) preservation for a substantial verbatim derivative; the repo is
  going public).
- Document the __noinit limitation: Boreas wires no .noinit section, so
  no-init/retained-RAM semantics are not available (storage is plain
  zero-init BSS).
- Tests: assert ring_buf_put_claim returns 0 on a full buffer (the
  zero-copy path UART IRQ uses), and assert ring_buf_space_get across a
  wrap (the put.head - get.tail subtraction path).

Adversarial index-arithmetic probe found zero constructible failures:
the UINT16_MAX/2 cap is exact and inclusive (size=32768 would alias
head-base to 0 mod 65536 and is rejected at build/init), and the
non-power-of-two-size x uint16-index-wrap case is safe because only
modular differences are used and tail-base is held in [0,size) by
one base bump per wrap. The 20000-cycle test crosses the uint16
boundary, so it covers that class.

linux suite: 221/0 x3.

Refs #45

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…lic)

A pre-public licensing audit compared every Zephyr-API file against the
upstream original (function bodies and macro expansions, not just
names). Apache-2.0 4(c) requires retaining the upstream copyright only
where actual copyrightable expression was copied.

Retain upstream copyright (genuine verbatim/near-verbatim content):
- sys/util.h: IS_ENABLED's _XXXX##/_YYYY token-paste trick is upstream
  verbatim -> Copyright (c) 2011-2014 Wind River Systems, Inc.
- sys/byteorder.h: the 32-bit be/le composition mirrors upstream's
  canonical chaining -> Copyright (c) 2015-2016 Intel Corporation.
- (sys/ring_buffer.h + ring_buf.c already carry Copyright (c) 2015
  Intel Corporation from the port commit.)

Deliberately NOT attributed (independent reimplementations -- adding an
upstream notice would be false attribution): sys/dlist.h, sys/slist.h
(upstream is macro-generated; ours is hand-written over a different
struct layout), sys/atomic.h (inline __atomic wrappers vs upstream's
extern decls), sys/time_units.h (us-stored k_timeout_t + FreeRTOS ticks
vs upstream's Hz-based z_tmcvt), and all of zshell/zsys/zdevice (the
shell, logging, and device model are independent implementations over
ESP-IDF/FreeRTOS; only the public API surface matches Zephyr).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Folded findings from the code-review fan-out (no correctness bugs were
found; these are robustness/coverage/doc improvements):

- ring_buffer.h + ring_buf.c now #include "sdkconfig.h" explicitly.
  struct ring_buf's index width is selected by CONFIG_RING_BUFFER_LARGE,
  so the config must be visible wherever the struct is defined. It was
  safe before only by a transitive chain (util.h -> esp_attr.h ->
  sdkconfig.h); making it explicit removes a silent cross-TU ABI hazard
  if that chain ever changes, and matches the sibling .c convention.
- util.h: corrected the __noinit comment. The prior rationale was wrong
  (ESP-IDF DOES ship __NOINIT_ATTR + a .noinit section, and that
  attribute survives warm reset, not deep sleep). The real reasons to
  map __noinit to plain BSS: ring_buf never reads unwritten bytes (so
  no-init vs zero-init is invisible), and plain BSS avoids a section
  attribute that does not port to the Mach-O host toolchain. The
  no-init limitation is documented.
- util.h: dropped the unused __deprecated shim (no users; add toolchain
  shims when a real user lands). likely/unlikely kept (unlikely is used
  by ring_buf.c; both are #ifndef-guarded and identical in value to
  ESP-IDF's, so the include-order race is codegen-only).
- test: added test_ring_buf_full_empty_across_wrap -- fills to full /
  drains to empty for 40 cycles (163 KB through a 4 KB buffer, crossing
  the uint16 index wrap ~2.5x), asserting the full (space==0, put
  refused) and empty discriminations AT high `allocated` across the
  wrap. The existing many-wrap test drains within a 7-byte span, so it
  never exercised full-vs-empty near the index wrap.

linux suite: 222/0 x3.

Refs #45

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
util.h was accreting symbols whose upstream homes are
<zephyr/toolchain.h> (likely/unlikely, __weak, ALWAYS_INLINE, __noinit)
and <zephyr/sys/__assert.h> (__ASSERT, __ASSERT_NO_MSG), while the repo
otherwise mirrors upstream include paths exactly -- the devlog TODO
already records a downstream smf port hand-editing upstream #includes
because of this. Thin headers at the upstream paths fix that now, while
they have 1-2 consumers; util.h re-includes both so existing ports keep
working.

Also two correctness fixes folded in:
- __noinit now maps to __NOINIT_ATTR (real .noinit on hardware, no-op
  on the linux target via esp_attr.h's CONFIG_IDF_TARGET_LINUX gate in
  both IDF v5.4 and v5.5). The old empty shim silently gave zero-init
  BSS semantics to any future warm-reset-retention user, and its
  stated Mach-O blocker was false -- the macOS host build of the linux
  target compiles clean.
- likely/unlikely now document the divergence from esp_compiler.h
  (ESP-IDF's are CONFIG_COMPILER_OPTIMIZATION_PERF-gated; Boreas
  matches upstream Zephyr's unconditional __builtin_expect; both are
  #ifndef-guarded so the include-order race is codegen-only).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The header advertises the byte API for thread + ISR use (the UART IRQ
pattern), and the repo convention is that ISR-callable primitives are
K_ISR_SAFE (k_sem, k_msgq, k_event, k_work): ESP-IDF ISRs registered
with ESP_INTR_FLAG_IRAM fire during flash-cache-disabled windows (e.g.
NVS commits), where calling flash-resident code panics with "Cache
disabled but cached memory region accessed". All five functions are now
IRAM-resident (verified via nm: 0x4037xxxx alongside k_sem_give).

The internal __ASSERT_NO_MSGs are replaced with k_panic() checks: the
upstream asserts compile out under its default CONFIG_ASSERT=n, but
Boreas's __ASSERT is always on and its ESP_LOGE+abort failure path is
itself flash-resident (util.h documents it as not IRAM-safe and
prescribes k_panic() -- k_work.c already set the precedent of replacing
upstream asserts on K_ISR_SAFE paths). memcpy is ROM-resident on
ESP32-S3, so the copy loops remain safe.

ring_buf_peek's NULL check is also hoisted above the copy loop so the
documented "Cannot be NULL" contract is enforced on an empty buffer
too, instead of silently returning 0 until data arrives.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…retvals

Doc-only fixes from review (code unchanged, upstream-verbatim):

- Concurrency note rewritten: it overclaimed "lock-free SPSC" without
  stating that no field is volatile and no barriers are issued. Now
  documents that the query inlines read one index from each side
  (tear-free aligned loads, conservatively stale), forbids busy-waiting
  on them (the compiler may hoist the load out of a call-free loop),
  and requires per-handoff k_sem pairing on SMP. Also notes the
  implementation is K_ISR_SAFE.
- RING_BUF_INIT: @warning that size8 > RING_BUFFER_MAX_SIZE is NOT
  checked on this path (unlike RING_BUF_DECLARE/ring_buf_init) and
  silently corrupts; macro body stays upstream-verbatim.
- put_claim/get_claim/peek: @notes that finishing rewinds head to tail,
  so an outstanding claim must be finished before any other same-side
  call (including the copy-mode functions); restored upstream's
  dropped "with a non-zero `size`" qualifier on the peek doc.
- put_finish/get_finish: -EINVAL condition corrected -- the code (here
  and upstream) rejects size > outstanding claimed bytes, not "exceeds
  free space"/"valid bytes" as upstream's doc has always misstated.
- size_get: adopted current upstream main's "available data" wording
  (v3.7's "used space" miscounts outstanding claims); three
  @retval-with-prose fixed to @return (also fixed upstream post-v3.7).
- Removed seven @warnings referencing the unported ring_buf_item_ API;
  RING_BUF_DECLARE gains a file-scope-only @note.
- Kconfig: RING_BUFFER_LARGE costs 12 extra bytes per struct ring_buf
  on the 32-bit target (6 index fields x 2 bytes; measured 20 -> 32),
  not 6 as the help text claimed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two coverage gaps closed:

- test_ring_buf_index_wrap_seeded: the traffic-volume stress tests can
  only reach the default uint16_t 65536 wrap; under
  CONFIG_RING_BUFFER_LARGE the 2^32 wrap was silently untested in
  exactly the config the option exists for. Seeding the indices just
  below the wrap via ring_buf_internal_reset (the technique upstream's
  ringbuffer test suite uses) makes the full/empty discrimination
  coverage index-width-independent. Verified locally: full suite passes
  on the linux target with CONFIG_RING_BUFFER_LARGE=y.
- test_ring_buf_peek_across_wrap_multi_claim_get: peek was only tested
  contiguous-at-offset-0; now covers peek walking both physical
  segments of wrapped data and rewinding via its internal
  get_finish(0), plus multiple get-side claims completed by a single
  finish.

Cleanups: per-byte TEST_ASSERT_EQUAL_UINT8 loops (~300k Unity calls
across the two stress tests) replaced with per-chunk
TEST_ASSERT_EQUAL_UINT8_ARRAY (better failure diagnostics: reports the
element index); put_claim_wraps now drains and verifies its patterned
writes landed at the correct physical offsets instead of leaving them
unchecked; NULL-discard priming replaces drain-into-out + memset; dead
sequence fill dropped from the saturates test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@swoisz

swoisz commented Jun 10, 2026

Copy link
Copy Markdown
Collaborator Author

Review hardening pass (xhigh /code-review)

A 9-angle review with per-finding adversarial verification surfaced 15 findings (20 confirmed of 22 candidates + 2 from a gap sweep; 1 refuted). All are addressed in the 4 commits ending at 0445556. The port's core logic needed zero changes — every inline and copy loop verified field-for-field identical to upstream. The fixes are at the integration boundary:

Behavior changes

  • ISR safety (739fa27): all five ring_buf.c functions are now K_ISR_SAFE (IRAM-resident), matching the k_sem/k_msgq/k_event/k_work convention — the header advertises thread+ISR/UART-IRQ use, and flash-resident code called from an ESP_INTR_FLAG_IRAM ISR during a flash-cache-disabled window (e.g. NVS commit) panics. The internal __ASSERT_NO_MSGs became IRAM-safe k_panic() checks (the __ASSERT failure path is flash-resident); peek's NULL check is hoisted so the "Cannot be NULL" contract holds on an empty buffer too.
  • __noinit semantics (7444a6c): ⚠️ the one change reaching beyond ring_buf. The empty shim (zero-init BSS) is now __NOINIT_ATTR — genuine .noinit on hardware, no-op on the linux target via esp_attr.h's CONFIG_IDF_TARGET_LINUX gate (verified identical in IDF v5.4 and v5.5). The old comment's Mach-O blocker claim was false: the macOS host build of the linux target compiles clean. Future warm-reset-retention ports now get upstream semantics instead of silently reading 0.
  • Header layout (7444a6c): zephyr/toolchain.h and zephyr/sys/__assert.h now exist at their upstream paths (util.h re-includes both for compat), so near-verbatim ports keep their upstream #include shape — the cost the devlog TODO already recorded for the smf port.

Docs (f440e8d, code unchanged)

  • Concurrency note no longer overclaims: documents that queries are tear-free-but-stale one-index-per-side reads, forbids busy-waiting on them (compiler may hoist the load), requires per-handoff k_sem on SMP.
  • RING_BUF_INIT warns it is the one unchecked size path (silent corruption past RING_BUFFER_MAX_SIZE); claim/finish docs warn that finishing rewinds head→tail, voiding outstanding claims (incl. via copy-mode calls and peek).
  • -EINVAL retvals corrected (actual condition: exceeds outstanding claimed bytes — upstream's doc has always misstated this); size_get adopts upstream main's wording; 7 warnings referencing the unported ring_buf_item_ API removed; Kconfig help: RING_BUFFER_LARGE costs 12 bytes/instance on the 32-bit target (measured 20→32), not 6.

Tests (0445556): 16 → 18

  • index_wrap_seeded: seeds indices below the wrap via ring_buf_internal_reset (upstream's own test technique) — full/empty discrimination at the index wrap is now covered independent of index width. Verified locally with CONFIG_RING_BUFFER_LARGE=y: 224/224 pass.
  • peek_across_wrap_multi_claim_get: peek across the physical wrap (two internal claim iterations + the get_finish(0) rewind) and multi-claim get with a single finish — previously unreachable paths.
  • Per-byte assert loops (~300k Unity calls) → per-chunk TEST_ASSERT_EQUAL_UINT8_ARRAY; put_claim_wraps now drains and verifies its writes landed at the right physical offsets.

Verification

  • clang-format 21.1.8 --dry-run -Werror: clean (CI's exact gate)
  • esp32s3 -Werror build: clean; nm confirms all five functions in IRAM (0x4037xxxx)
  • linux target: 224/224 pass (default config and RING_BUFFER_LARGE=y)
  • Local builds on IDF v5.4; CI matrix will additionally exercise v5.5

Possible follow-up (non-blocking): a CI matrix entry building the linux target with RING_BUFFER_LARGE=y, now that the seeded test makes that config meaningful.

🤖 Generated with Claude Code

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Ports Zephyr’s byte-mode ring_buf implementation into Boreas’ zkernel (header + C implementation), adds supporting Zephyr-compat toolchain/assert shims, and introduces a focused Unity test suite to validate wrap/index edge cases.

Changes:

  • Add Zephyr-compatible ring_buf API (sys/ring_buffer.h) and implementation (src/ring_buf.c) with ISR-safe placement.
  • Introduce toolchain/assert compatibility headers and update util/byteorder headers for licensing/attribution adjustments.
  • Add comprehensive ring buffer tests and wire them into the test runner/build.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
components/zkernel/include/boreas/zephyr/sys/ring_buffer.h Adds the Zephyr-compatible byte ring buffer public API and inline helpers.
components/zkernel/src/ring_buf.c Implements the ring buffer core logic (claim/finish + copy put/get/peek).
components/zkernel/Kconfig Adds RING_BUFFER_LARGE option to widen indices to 32-bit.
components/zkernel/include/boreas/zephyr/toolchain.h Adds Zephyr toolchain macro shims (likely/unlikely, ALWAYS_INLINE, __noinit, etc.).
components/zkernel/include/boreas/zephyr/sys/__assert.h Adds Zephyr-compatible __ASSERT / __ASSERT_NO_MSG macros.
components/zkernel/include/boreas/zephyr/sys/util.h Re-exports new compat headers and updates attribution commentary.
components/zkernel/include/boreas/zephyr/sys/byteorder.h Updates attribution commentary for upstream-mirrored logic.
components/zkernel/CMakeLists.txt Builds the new src/ring_buf.c.
test/main/test_ring_buf.c Adds ring buffer unit tests (accounting, wrap, claim/finish, seeded index wrap).
test/main/test_main.c Registers the new ring buffer test group.
test/main/CMakeLists.txt Adds test_ring_buf.c to the test sources.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread components/zkernel/include/boreas/zephyr/toolchain.h Outdated
…e (Copilot)

The file-level comment claimed all definitions are #ifndef-guarded, but
ALWAYS_INLINE is intentionally #undef'd and redefined unconditionally
(the RISC-V -Werror=attributes fix). Call out the exception so the
include-order contract isn't misleading.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@swoisz swoisz merged commit 458ac5f into main Jun 10, 2026
5 checks passed
@swoisz swoisz deleted the feature/ring-buf branch June 10, 2026 20:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Port sys/ring_buffer.h (ring_buf) from upstream

2 participants