Version 2.0.0: header-only, freestanding, interrupt-safe - #17
Merged
Merged
Conversation
LarryRuane
force-pushed
the
v2-embedded
branch
from
September 1, 2026 00:00
694f582 to
d911e30
Compare
This was referenced Sep 1, 2026
Closed
LarryRuane
force-pushed
the
v2-embedded
branch
from
September 1, 2026 15:38
d911e30 to
2c663a4
Compare
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
LarryRuane
force-pushed
the
v2-embedded
branch
from
September 1, 2026 15:56
2c663a4 to
f38d014
Compare
TragicWarrior
approved these changes
Sep 1, 2026
TragicWarrior
left a comment
Collaborator
There was a problem hiding this comment.
Looks good and I'm okay with the license change.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.candprotothread_lock.cmove into their headers asstatic inline. Nothing to compile or link. This also closes a silent ABI hazard:PT_DEBUGchanges the layout ofpt_thread_tandpt_func_t, so an application built withPT_DEBUG=0against a stocklibprotothreadcorrupted 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.memsetis gone,assertis behind an overridablept_assert, and<stdlib.h>is behindPT_NO_MALLOC. A program using protothreads, semaphores, locks and timers now needs zero libc symbols at every optimization level under-ffreestanding.PT_NWAITis finally overridable — atPT_NWAIT=1the 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 insidept_unlinkbetween the chain store and the*headfixup 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->atexitwas never initialized, sopt_kill()jumped through a garbage function pointer. Thept_createmacro carried a trailing semicolon and brokeif/else. Reader-writer locks queued LIFO, starving writers.protothread_create()dereferenced a nullmalloc.Tests. The suite was built entirely on
assert(), so-DNDEBUGcompiled it into a program that verified nothing. All 65 checks now use acheck()macro that survivesNDEBUG. Also fixed out-of-bounds channel arithmetic (~2MB past an allocation) andrandom(), 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
LICENSEcontained only the Apache appendix boilerplate, not the license, so GitHub reported the repo asNOASSERTIONand redistributors could not satisfy §4(a). AllLeftHand Networksreferences dropped per your instruction.pt_add_readylinks before invokingready_function(required to keep your callback out of the critical section);pt_set_atexit()must now be called afterpt_create(), which clears it.<stdlib.h>/<string.h>/<assert.h>transitively.README.mdhas 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 andpt_wait's enqueue. Documented under "Lost wakeups", withprotothread_pool_example.cshowing the deferred-signal pattern that avoids it.Verification
Test suite passes on gcc and clang, Linux and macOS, over
PT_DEBUG/NDEBUGon and off,PT_NWAITof 1/4/1024,-O0through-Os,-std=c99throughc23, 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.mdrecords what was deliberately deferred, including a C++ port (investigated: it nearly works, onemalloccast blocks it, and constructor-skipping plus exception handling need care).🤖 Generated with Claude Code
https://claude.ai/code/session_01SR7v9BVsFrHiEhQBSqZmAs