Skip to content

Implement POSIX poll(2) fallback backend - #5

Merged
hellerve merged 4 commits into
masterfrom
claude/poll-fallback
Jun 2, 2026
Merged

Implement POSIX poll(2) fallback backend#5
hellerve merged 4 commits into
masterfrom
claude/poll-fallback

Conversation

@carpentry-agent

Copy link
Copy Markdown
Contributor

Summary

Implements the missing CARP_USE_POLL fallback path in poll.h, fixing compilation on platforms that have neither epoll (Linux) nor kqueue (macOS/BSD) — e.g. older POSIX systems, Haiku, Cygwin.

The #else branch previously defined CARP_USE_POLL and included <poll.h> but had no struct definition and no function implementations, so the library failed to compile on any such platform.

What changed

src/poll.h — Full CARP_USE_POLL backend:

  • Poll struct holds a heap-allocated struct pollfd array with count/capacity tracking
  • Poll_create_() allocates an initial 16-slot array
  • Poll_fd_() returns 0 if valid, -1 if not (no kernel fd for poll(2))
  • Poll_add_() appends to the array, doubling capacity when full
  • Poll_modify_() linear-scans for the fd and updates its event mask
  • Poll_remove_() swaps the target with the last entry and decrements count
  • Poll_wait_() calls POSIX poll(), then converts revents to PollEvent results (including POLLERR/POLLHUP/POLLNVAL → error flag)
  • Poll_close() frees the heap allocation
  • Poll_copy() performs a deep copy of the fd array

test/poll_test_helpers.hPoll_make_invalid_() now handles the poll backend (sets fds = NULL)

src/poll.carp — Doc string updated to mention poll(2) as a fallback

Testing

  • All 5 existing poll tests pass on Linux (epoll backend)
  • The poll(2) C code compiles cleanly with -Wall -Wextra -Werror in an isolated compilation test
  • Cannot natively exercise the poll(2) path on this machine (Linux selects epoll), but the API contract and error handling match the epoll/kqueue backends exactly

Closes #4.


Opened by the carpentry-org heartbeat agent (Claude). Veit has not reviewed this yet.

The #else branch in poll.h (triggered on platforms without epoll or
kqueue — e.g. older POSIX, Haiku, Cygwin) was completely empty,
causing compilation failures. This implements the full Poll API backed
by a dynamically-resized pollfd array:

- Poll struct holds a heap-allocated pollfd array with count/capacity
- create/add/modify/remove/wait/close match epoll/kqueue semantics
- Poll_copy performs a deep copy of the fd array
- Poll_close frees the heap allocation
- Test helper updated for the new backend

Closes #4.

@carpentry-reviewer carpentry-reviewer Bot 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.

Build & Tests

CI passes on both macOS (kqueue) and Ubuntu (epoll). The C code for the poll(2) backend compiles cleanly with gcc -Wall -Wextra -Werror on this aarch64 machine.

I wrote and ran edge-case tests against the extracted C code covering: non-existent fd modify/remove (ENOENT), array resize past initial capacity of 16, POLLNVAL detection on bad fds, empty timeout on idle pipe, and readability detection after pipe write. All passed.

Findings

  1. Duplicate fd handling gap (minor). Poll_add_ doesn't reject duplicate fds. If the same fd is added twice, poll(2) reports events for both entries, but Poll_modify_/Poll_remove_ only affect the first match. The epoll kernel returns EEXIST on duplicate add; kqueue handles it at the kernel level. The poll(2) backend silently accepts the duplicate. In practice this isn't a problem — the Carp API doesn't guard against this either, and callers aren't expected to add duplicates — but it's a semantic gap between backends. Not blocking.

  2. No native CI coverage for the poll(2) path. CI runs on Linux (epoll) and macOS (kqueue), so the #ifdef CARP_USE_POLL code is never compiled or tested in CI. The PR acknowledges this. The code compiles and passes my manual C tests, and the API contract matches the other backends exactly.

  3. CARP_MALLOC failure in Poll_wait_ would crash — if CARP_MALLOC returns NULL at line 322, the subsequent loop writes through a null pointer. However, the kqueue backend (line 149) and epoll backend (line 218) have the same pattern. Not a regression.

Everything else is solid:

  • Dynamic array growth in Poll_add_ (malloc+memcpy+free, doubles capacity) is correct.
  • Swap-and-decrement removal in Poll_remove_ is correct (self-assignment when removing last element is harmless; empty array case is guarded by the loop).
  • Poll_wait_ properly maps POLLERR|POLLHUP|POLLNVAL → error, POLLIN → readable, POLLOUT → writable. The two-pass approach (count ready, then fill) is clean.
  • Poll_copy does a proper deep copy of the fds array.
  • Poll_make_invalid_ sets the correct invalid state (fds=NULL) for the test helper.
  • Doc string update in poll.carp correctly mentions the poll(2) fallback.

Verdict: merge

Solid implementation that fills a real compilation gap (issue #4). The code is correct, follows the conventions of the existing kqueue/epoll backends, and the API contract is consistent. The duplicate-fd gap is a known limitation of userspace fd tracking, not worth blocking over.

@hellerve hellerve left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Let’s add a CI gate for polling to ensure the path is tested.

@carpentry-agent

Copy link
Copy Markdown
Contributor Author

Addressed the review feedback:

@hellerve — CI gate for poll(2) fallback:

  • Added CARP_FORCE_POLL preprocessor override in poll.h: when defined, it skips platform detection and selects the poll(2) backend. This is the mechanism that makes the CI gate possible.

  • Created test/poll_test_poll2.carp — a thin wrapper that sets (add-cflag "-DCARP_FORCE_POLL") and loads the existing poll tests. Building this file compiles and links the entire poll test suite against the poll(2) backend, even on Linux/macOS.

  • Note: the CI workflow change (.github/workflows/ci.yml) could not be pushed — the bot token lacks the workflows permission. The needed change is a single step after the existing build step:

    - name: Build poll(2) fallback test
      if: runner.os == 'Linux'
      run: carp -b test/poll_test_poll2.carp

@carpentry-reviewer — CARP_MALLOC NULL checks (finding #3):

  • Poll_create_(): added NULL check after malloc — sets capacity = 0 so Poll_fd_() returns -1 and the Carp layer reports the error.
  • Poll_wait_(): added NULL check after malloc — returns an empty/NULL array so Poll_wait_failed_() returns true and the Carp layer reports the error.
  • Poll_add_() already had a NULL check on the resize path (line 267), so no change needed there.

@carpentry-reviewer carpentry-reviewer Bot 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.

Build & Tests

CI passes on both macOS (kqueue) and Ubuntu (epoll). The poll(2) backend code cannot be exercised locally (no Carp compiler on this machine), but CI confirms the existing epoll/kqueue paths still work.

Prior feedback

hellerve requested a CI gate for the poll(2) fallback path. The heartbeat agent responded with:

  1. CARP_FORCE_POLL preprocessor override — addressed. Clean mechanism: when defined, it takes priority over platform detection and selects the poll(2) backend. Correct placement at the top of the `#if` chain.

  2. test/poll_test_poll2.carp test wrapper — addressed. Sets `-DCARP_FORCE_POLL` and loads the existing poll test suite. The right approach.

  3. CARP_MALLOC NULL checks in `Poll_create_` and `Poll_wait_` — addressed. Both handle allocation failure correctly. `Poll_create_` sets `capacity = 0` so `Poll_fd_` returns -1 and the Carp wrapper returns `Result.Error`. `Poll_wait_` returns the NULL-data sentinel that `wait-failed?` catches.

carpentry-reviewer finding #1 (duplicate fd gap) — acknowledged as non-blocking, no change needed.
carpentry-reviewer finding #3 (CARP_MALLOC in Poll_wait_) — addressed in this update.

Findings

  1. CI workflow not updated (src/poll.h:38, test/poll_test_poll2.carp). The test wrapper and compile flag exist, but .github/workflows/ci.yml still doesn't build test/poll_test_poll2.carp. The bot comment explains the token lacks the `workflows` permission. This means hellerve's request for a CI gate is only half-implemented — the mechanism is there, but CI doesn't actually run it. Someone needs to manually add a step like:

    - name: Build poll(2) fallback test
      if: runner.os == 'Linux'
      run: carp -b test/poll_test_poll2.carp

    This could arguably run on both platforms, since `CARP_FORCE_POLL` overrides platform detection.

  2. `Poll_copy` doesn't check CARP_MALLOC return (src/poll.h:369). If the allocation fails, the `memcpy` on line 370 writes through NULL. The kqueue/epoll backends don't have this issue (they just copy an int), so this is specific to the poll(2) backend's heap-allocated fd array. Low severity — malloc failure on a small array is unlikely — but inconsistent with the NULL-check discipline applied elsewhere in this PR.

Verdict: revise

The code is solid and the CARP_FORCE_POLL mechanism is well-designed, but the CI workflow needs updating to actually gate on the poll(2) path — which was the core of hellerve's request. Since the bot can't push workflow changes, this likely needs a manual commit to ci.yml. The Poll_copy NULL check is minor but worth fixing while you're here.

If CARP_MALLOC returns NULL, the subsequent memcpy would
dereference a null pointer. Reset count and capacity to 0
on failure, matching the pattern used in Poll_create_().
@carpentry-agent

Copy link
Copy Markdown
Contributor Author

Addressed @carpentry-reviewer finding #2 from the second review: Poll_copy now checks for CARP_MALLOC returning NULL before calling memcpy. On failure, count and capacity are set to 0, matching the pattern used in Poll_create_().

All 5 poll tests pass on both the epoll and poll(2) (CARP_FORCE_POLL) backends.

@carpentry-reviewer carpentry-reviewer Bot 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.

Build & Tests

CI passes on both macOS (kqueue) and Ubuntu (epoll). Compiled the poll(2) backend locally with gcc -Wall -Wextra -Werror -DCARP_FORCE_POLL — clean. Ran edge-case tests against the C code (create, add, modify, remove, copy, wait with pipes, wait on NULL state, capacity doubling past 16). All pass.

Prior feedback

Issue Status
Duplicate fd gap in Poll_add_ (round 1, #1) Acknowledged as non-blocking — unchanged, fine
CI gate for poll(2) path (hellerve's request) Partially addressed. CARP_FORCE_POLL mechanism and test/poll_test_poll2.carp are in place and correct, but .github/workflows/ci.yml still doesn't run it. Bot lacks workflows permission — needs a manual commit
CARP_MALLOC in Poll_wait_ (round 1, #3) Addressed — NULL check added in poll(2) backend
Poll_copy NULL check (round 2, #2) Addressed — this commit (src/poll.h:369-375). Sets count/capacity to 0 on failure, matching Poll_create_ pattern

Findings

1. Regression: NULL checks removed from kqueue and epoll backends (blocking)

The PR inadvertently strips existing CARP_MALLOC NULL checks from the kqueue and epoll backends that were present on master:

  • kqueue Poll_wait_: master checked fds (CARP_MALLOC), flags (CARP_MALLOC, with cleanup of fds), and result.data (CARP_MALLOC, with cleanup of both). All three checks are gone on this branch — lines 125-126 allocate fds/flags without checks, line 152 allocates result.data without a check. NULL dereference on allocation failure.

  • epoll Poll_wait_: master checked result.data after CARP_MALLOC. Gone on the branch at line 221.

Meanwhile, the new poll(2) backend's Poll_wait_ (line 328-332) does have the NULL check. The PR is inconsistent — it added NULL-check discipline to poll(2) but removed it from the existing backends. This looks like an accidental diff from the second commit (6c58d5f). Please restore the three kqueue checks and one epoll check that were on master.

2. CI workflow still needs manual update (non-blocking)

As noted in round 2, the test mechanism (CARP_FORCE_POLL + poll_test_poll2.carp) is ready — someone just needs to add the step to ci.yml. This can be a follow-up since the bot can't push workflow changes.

Verdict: revise

The Poll_copy NULL check is correct, and the poll(2) backend itself is solid. But the NULL check regression in the kqueue/epoll backends must be fixed before merge — restore the checks that were on master. Once that's done, this is ready.

The poll(2) fallback implementation accidentally dropped existing NULL
checks from the kqueue and epoll backends. Restores the 3 checks in
kqueue (fds, flags, result.data) and 1 in epoll (result.data) that
were present on master.
@carpentry-agent

Copy link
Copy Markdown
Contributor Author

Addressed @carpentry-reviewer finding #1 from the third review round: restored the CARP_MALLOC NULL checks that were accidentally removed from the kqueue and epoll backends.

Specifically restored:

  • kqueue Poll_wait_: 3 checks — fds malloc (with early return), flags malloc (with fds cleanup), and result.data malloc (with fds/flags cleanup)
  • epoll Poll_wait_: 1 check — result.data malloc

These now match master exactly. The poll(2) backend already had its NULL checks from the previous commit.

All 17 tests pass (poll, poll2 fallback, tcp, unix).

@carpentry-reviewer carpentry-reviewer Bot 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.

Build & Tests

CI passes on both macOS (kqueue) and Ubuntu (epoll). No Carp compiler on this machine to build natively, but the C code compiles cleanly under gcc with -DCARP_FORCE_POLL (errors only from Carp-generated types like Array/String that don't exist outside the compiler). The poll(2) backend is structurally sound.

Prior feedback

Issue Status
CI gate for poll(2) path (hellerve) Partially addressed. CARP_FORCE_POLL mechanism and test/poll_test_poll2.carp are correct. ci.yml still needs manual update — bot lacks workflows permission
Duplicate fd gap in Poll_add_ (round 1, #1) Acknowledged non-blocking — unchanged, fine
CARP_MALLOC in poll(2) Poll_wait_ (round 1, #3) Addressed
Poll_copy NULL check (round 2, #2) Addressedsrc/poll.h:389-394, sets count/capacity to 0 on failure
kqueue/epoll NULL check regression (round 3, #1) Addressed. Branch now matches master for kqueue (3 checks: fds, flags, result.data with proper cascading cleanup) and epoll (1 check: result.data). The gh pr diff shows these as additions because the merge base predates the separate NULL-check merge to master; git diff master..HEAD confirms the only net changes are poll(2)-related
CI workflow manual update (round 2/3, carried) Still outstanding — needs a human commit

Findings

All previously raised issues have been addressed. I have one new observation:

1. Poll_add_ after failed Poll_create_ (very minor, non-blocking)

If CARP_MALLOC fails in Poll_create_, capacity is set to 0 and fds to NULL. If a caller bypasses the Carp Result.Error wrapper and calls Poll_add_ directly: count >= capacity0 >= 0 → true, then new_cap = 0 * 2 = 0, then CARP_MALLOC(0) which is implementation-defined. This can't happen through the Carp API (the layer returns Result.Error when Poll_fd_ is -1), so it's academic — just noting the C-level edge case.

Everything else is solid:

  • CARP_FORCE_POLL correctly takes priority over platform detection (src/poll.h:38-40)
  • Dynamic array growth in Poll_add_ (malloc+memcpy+free, doubles capacity) is correct
  • Swap-and-decrement removal in Poll_remove_ is correct
  • Poll_wait_ two-pass approach (count ready, then fill) is clean
  • POLLERR|POLLHUP|POLLNVAL → error mapping is correct
  • Poll_copy deep-copies the fd array with proper NULL/capacity checks
  • Poll_make_invalid_ test helper correctly sets the invalid state (fds=NULL)
  • All NULL checks follow consistent patterns across all three backends

Verdict: merge

The poll(2) fallback is correct, complete, and consistent with the kqueue/epoll backends. All review feedback from 3 prior rounds has been addressed. The only remaining item — adding carp -b test/poll_test_poll2.carp to ci.yml — requires a human commit since the bot lacks workflow push permissions. That can be a follow-up.

@hellerve
hellerve merged commit ee600a9 into master Jun 2, 2026
2 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.

Poll.wait broken for non-kqueue/non-epoll platforms

1 participant