Skip to content

Version 2.0.0: header-only, freestanding, interrupt-safe - #17

Merged
TragicWarrior merged 2 commits into
masterfrom
v2-embedded
Sep 1, 2026
Merged

TragicWarrior merged 2 commits into
masterfrom
v2-embedded

Conversation

@LarryRuane

Copy link
Copy Markdown
Owner

Modernizes the library around one goal: make it usable on an OS-less, memory-constrained system with no C library at all — the environment protothreads were invented for.

Two commits. The second one exists because CI, on its very first run, found that the library never built with clang.

What changed

Header-only. protothread_sem.c and protothread_lock.c move into their headers as static inline. Nothing to compile or link. This also closes a silent ABI hazard: PT_DEBUG changes the layout of pt_thread_t and pt_func_t, so an application built with PT_DEBUG=0 against a stock libprotothread corrupted memory with no diagnostic (confirmed with ASan). With nothing precompiled, that mismatch cannot occur.

Freestanding. Only <stddef.h>, <stdint.h> and <stdbool.h> are included unconditionally. memset is gone, assert is behind an overridable pt_assert, and <stdlib.h> is behind PT_NO_MALLOC. A program using protothreads, semaphores, locks and timers now needs zero libc symbols at every optimization level under -ffreestanding. PT_NWAIT is finally overridable — at PT_NWAIT=1 the scheduler state goes from 4112 to 20 bytes on a 32-bit target.

Interrupt safety. New PT_CRITICAL_ENTER/PT_CRITICAL_EXIT (splhigh/splx, in effect), no-ops by default, around every list update. Without them an interrupt arriving inside pt_unlink between the chain store and the *head fixup silently and permanently orphans a protothread.

Timers. New protothread_timer.h. The library never reads a clock, so it stays dependency-free; the caller drives it. Deadline comparisons use a signed difference, so a 32-bit millisecond clock is correct across its rollover.

Bug fixes. t->atexit was never initialized, so pt_kill() jumped through a garbage function pointer. The pt_create macro carried a trailing semicolon and broke if/else. Reader-writer locks queued LIFO, starving writers. protothread_create() dereferenced a null malloc.

Tests. The suite was built entirely on assert(), so -DNDEBUG compiled it into a program that verified nothing. All 65 checks now use a check() macro that survives NDEBUG. Also fixed out-of-bounds channel arithmetic (~2MB past an allocation) and random(), which is POSIX and made the suite unbuildable under any -std=cNN.

CI, benchmark, example, docs, license. See the commit messages.

Please look closely at these

  • License change, Apache-2.0 → MIT. Needs your explicit sign-off. The old LICENSE contained only the Apache appendix boilerplate, not the license, so GitHub reported the repo as NOASSERTION and redistributors could not satisfy §4(a). All LeftHand Networks references dropped per your instruction.
  • Three behavior changes, all deliberate: locks are now FIFO; pt_add_ready links before invoking ready_function (required to keep your callback out of the critical section); pt_set_atexit() must now be called after pt_create(), which clears it.
  • Breaking for downstream: no library to link, and the headers no longer leak <stdlib.h>/<string.h>/<assert.h> transitively. README.md has an "Upgrading from 1.x" section.
  • PT_CRITICAL_* is not a complete answer to ISR signalling. It protects list integrity, not the lost-wakeup race between a predicate test and pt_wait's enqueue. Documented under "Lost wakeups", with protothread_pool_example.c showing the deferred-signal pattern that avoids it.

Verification

Test suite passes on gcc and clang, Linux and macOS, over PT_DEBUG/NDEBUG on and off, PT_NWAIT of 1/4/1024, -O0 through -Os, -std=c99 through c23, and under ASan, UBSan and TSan — all with -Wall -Wextra -Werror. Two CI jobs guard claims that are easy to break silently: one asserts a freestanding build still needs no libc symbols, one checks the default cmake build pulls in no pthread dependency and that the installed pkg-config file is consumable.

TODO.md records what was deliberately deferred, including a C++ port (investigated: it nearly works, one malloc cast blocks it, and constructor-skipping plus exception handling need care).

🤖 Generated with Claude Code

https://claude.ai/code/session_01SR7v9BVsFrHiEhQBSqZmAs

LarryRuane and others added 2 commits September 1, 2026 09:56
Modernizes the library around a single goal: make it usable on an OS-less,
memory-constrained system with no C library at all, which is the environment
protothreads were invented for.

Header-only
  Move protothread_sem.c and protothread_lock.c into their headers as
  static inline.  There is no longer anything to compile or link: the
  protothread-static/protothread-shared targets are gone and pkg-config no
  longer emits -lprotothread.  This also closes a silent ABI hazard: PT_DEBUG
  changes the layout of pt_thread_t and pt_func_t, so an application built
  with PT_DEBUG=0 against a stock libprotothread corrupted memory with no
  diagnostic.  With nothing precompiled, that mismatch cannot occur.

Freestanding
  Only <stddef.h>, <stdint.h> and <stdbool.h> are included unconditionally,
  all of which a freestanding implementation must provide.  memset is gone,
  assert is behind an overridable pt_assert, and <stdlib.h> with
  protothread_create/protothread_free is behind PT_NO_MALLOC.  A program
  using protothreads, semaphores and locks now requires zero libc symbols at
  every optimization level under -ffreestanding.

  PT_NWAIT is finally overridable.  The wait table cost 8KB per
  protothread_t and could not be changed; at PT_NWAIT=1 the scheduler state
  is 20 bytes on a 32-bit target.

Interrupt safety
  Add PT_CRITICAL_ENTER/PT_CRITICAL_EXIT (splhigh/splx, in effect), no-ops
  by default, wrapped around every list update.  Without them an interrupt
  arriving inside pt_unlink between the chain store and the *head fixup
  could silently and permanently orphan a protothread: removed from its wait
  queue, never made ready, unreachable by any later signal, with no
  assertion.  Note that this protects list integrity only; the separate
  lost-wakeup race between a predicate test and pt_wait's enqueue is
  documented, and protothread_pool_example.c shows the deferred-signal
  pattern that avoids it.

Fixes
  - pt_create_thread did not initialize t->atexit, so pt_kill() on a thread
    whose storage came from malloc or the stack jumped through a garbage
    function pointer.
  - The pt_create macro carried a trailing semicolon, so it could not be
    used as the body of an if without breaking the following else.
  - Reader-writer locks queued waiters LIFO and only ever examined the head,
    so a steady stream of readers starved a waiting writer and later writers
    overtook earlier ones.  The queue is now a circular FIFO and requests
    are granted in arrival order, at no change in the size of pt_lock_t.
  - protothread_create() dereferenced a null malloc result.
  - protothread_deinit() warned about an unused parameter under NDEBUG.

Timers
  New protothread_timer.h: pt_sleep(), pt_timer_run(), pt_timer_next() and
  pt_timer_cancel().  The library never reads a clock, so it stays
  dependency-free; the caller drives it from a tick interrupt or an idle
  loop.  Deadline comparisons use a signed difference, so a 32-bit
  millisecond clock behaves correctly across its rollover.

Tests
  The suite was built entirely on assert(), so -DNDEBUG compiled it into a
  program that verified nothing.  All 65 checks now use a check() macro that
  reports file, line and expression and aborts regardless of NDEBUG.  Two
  test bugs fixed: channel addresses were computed roughly 2MB past the end
  of a single-object allocation, and random() is POSIX rather than ISO C, so
  the suite would not build under any -std=cNN.  The suite now passes in 18
  configurations, including ASan+UBSan and -std=c99 through c23, all under
  -Wall -Wextra -Werror.

Versioning and license
  There was no version: no tags, no releases, and a hardcoded 1.0 in
  CMakeLists that pkg-config advertised regardless of any change.  The
  version now lives in protothread.h as the single source of truth and the
  build reads it, so a vendored header can identify itself.

  LICENSE contained only the Apache appendix boilerplate, not the license,
  so GitHub reported the repository as NOASSERTION and redistributors could
  not satisfy section 4(a).  Relicensed to MIT with the full canonical text,
  SPDX identifiers on every file, and the third-party FindPROTOTHREAD.cmake
  labeled BSL-1.0.

Benchmarks and examples
  protothread_bench.c compares against POSIX threads so the README's numbers
  can be reproduced rather than taken on faith; protothread_pool_example.c
  shows protothreads coordinating a pthread pool on a multi-core machine.
  Both need pthreads, so neither is built by default.

This is a breaking change for anyone linking the old library or relying on
transitive includes; see "Upgrading from 1.x" in README.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SR7v9BVsFrHiEhQBSqZmAs
The library did not compile with clang at all.  clang rejects an indirect
goto in a function that contains no address-of-label expression:

    error: indirect goto in function with no address-of-label expressions

A protothread function that never blocks has no pt_wait(), pt_yield() or
pt_call() to supply one, so pt_resume()'s "goto *label" stood alone and the
translation unit failed.  gcc accepts this, which is why it went unnoticed;
the README's claim of "gcc or clang" was simply wrong.

pt_resume() now contains a dead address-of-label expression, which satisfies
clang and costs nothing: the object code is byte-for-byte identical, and a
freestanding build still resolves to zero libc symbols under both compilers.

The workflow covers gcc and clang on Linux plus Apple clang on macOS, over
PT_DEBUG and NDEBUG on and off, PT_NWAIT of 1, 4 and 1024, -O0 through -Os,
-std=c99 through c23, and ASan, UBSan and TSan, all under -Wall -Wextra
-Werror.  Two jobs guard claims that are easy to break by accident: one
asserts a freestanding build still needs no libc symbols, and one checks
that the default cmake build produces no pthread-dependent targets and that
the installed pkg-config file is consumable.

The C23 jobs probe for the flag spelling the compiler accepts: gcc only
learned "-std=c23" in gcc 14, and ubuntu-24.04 ships gcc 13, which calls it
"-std=c2x".  Probing rather than pinning a runner image keeps this working
as the images move.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SR7v9BVsFrHiEhQBSqZmAs

@TragicWarrior TragicWarrior left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Looks good and I'm okay with the license change.

@TragicWarrior
TragicWarrior merged commit bb8081e into master Sep 1, 2026
20 checks passed
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.

2 participants