Implement POSIX poll(2) fallback backend - #5
Conversation
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.
There was a problem hiding this comment.
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
-
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, butPoll_modify_/Poll_remove_only affect the first match. The epoll kernel returnsEEXISTon 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. -
No native CI coverage for the poll(2) path. CI runs on Linux (epoll) and macOS (kqueue), so the
#ifdef CARP_USE_POLLcode 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. -
CARP_MALLOCfailure inPoll_wait_would crash — ifCARP_MALLOCreturns 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 mapsPOLLERR|POLLHUP|POLLNVAL→ error,POLLIN→ readable,POLLOUT→ writable. The two-pass approach (count ready, then fill) is clean.Poll_copydoes 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.carpcorrectly 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.
|
Addressed the review feedback: @hellerve — CI gate for poll(2) fallback:
@carpentry-reviewer — CARP_MALLOC NULL checks (finding #3):
|
There was a problem hiding this comment.
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:
-
CARP_FORCE_POLLpreprocessor 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. -
test/poll_test_poll2.carptest wrapper — addressed. Sets `-DCARP_FORCE_POLL` and loads the existing poll test suite. The right approach. -
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
-
CI workflow not updated (
src/poll.h:38,test/poll_test_poll2.carp). The test wrapper and compile flag exist, but.github/workflows/ci.ymlstill doesn't buildtest/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.
-
`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_().
|
Addressed @carpentry-reviewer finding #2 from the second review: All 5 poll tests pass on both the epoll and poll(2) ( |
There was a problem hiding this comment.
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 checkedfds(CARP_MALLOC),flags(CARP_MALLOC, with cleanup offds), andresult.data(CARP_MALLOC, with cleanup of both). All three checks are gone on this branch — lines 125-126 allocatefds/flagswithout checks, line 152 allocatesresult.datawithout a check. NULL dereference on allocation failure. -
epoll
Poll_wait_: master checkedresult.dataafter 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.
|
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:
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). |
There was a problem hiding this comment.
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) |
Addressed — src/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 >= capacity → 0 >= 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_POLLcorrectly 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 cleanPOLLERR|POLLHUP|POLLNVAL→ error mapping is correctPoll_copydeep-copies the fd array with proper NULL/capacity checksPoll_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.
Summary
Implements the missing
CARP_USE_POLLfallback path inpoll.h, fixing compilation on platforms that have neither epoll (Linux) nor kqueue (macOS/BSD) — e.g. older POSIX systems, Haiku, Cygwin.The
#elsebranch previously definedCARP_USE_POLLand 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— FullCARP_USE_POLLbackend:Pollstruct holds a heap-allocatedstruct pollfdarray withcount/capacitytrackingPoll_create_()allocates an initial 16-slot arrayPoll_fd_()returns 0 if valid, -1 if not (no kernel fd for poll(2))Poll_add_()appends to the array, doubling capacity when fullPoll_modify_()linear-scans for the fd and updates its event maskPoll_remove_()swaps the target with the last entry and decrements countPoll_wait_()calls POSIXpoll(), then convertsreventstoPollEventresults (includingPOLLERR/POLLHUP/POLLNVAL→ error flag)Poll_close()frees the heap allocationPoll_copy()performs a deep copy of the fd arraytest/poll_test_helpers.h—Poll_make_invalid_()now handles the poll backend (setsfds = NULL)src/poll.carp— Doc string updated to mention poll(2) as a fallbackTesting
-Wall -Wextra -Werrorin an isolated compilation testCloses #4.
Opened by the carpentry-org heartbeat agent (Claude). Veit has not reviewed this yet.