Repository navigation
RFC: Add hid_read_interrupt API for thread-safe read cancellation - #799
Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds a new public HIDAPI mechanism to asynchronously interrupt a blocked hid_read()/hid_read_timeout() call in a thread-safe way (intended to allow clean shutdown of dedicated reader threads without short timeouts/busy polling), and introduces a small hid_read_test utility to exercise the behavior.
Changes:
- Adds
hid_read_interrupt(),hid_is_read_interrupted(), andhid_read_clear_interrupt()to the public header and implements them across all backends. - Updates backend
hid_read_timeout()implementations to return-1with an “operation interrupted” read error when interruption is requested. - Adds a new optional
hid_read_testtool and a top-level CMake option to build it.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
windows/hid.c |
Implements interrupt via a manual-reset event + interlocked flag and integrates it into WaitForMultipleObjects() in hid_read_timeout(). |
netbsd/hid.c |
Implements interrupt via a nonblocking pipe added to the poll() fd set, plus a sticky interrupt flag. |
mac/hid.c |
Implements interrupt via a mutex-protected flag + condition broadcast integrated into the read wait loops. |
linux/hid.c |
Implements interrupt via eventfd added to poll() plus a sticky interrupt flag; adjusts failure cleanup path to call hid_close(). |
libusb/hid.c |
Implements interrupt via a mutex-protected flag + condition broadcast integrated into the read wait loops. |
hidapi/hidapi.h |
Adds the public API declarations and documents the intended thread-safety/semantics. |
hid_read_test/main.cpp |
New command-line tool that opens a device, runs a blocking read thread, and interrupts on Enter/Ctrl+C. |
hid_read_test/CMakeLists.txt |
Adds build/install rules for the new test tool (standalone or as a subdir). |
CMakeLists.txt |
Adds HIDAPI_BUILD_HID_READ_TEST option to include the new tool in the build. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 9 out of 9 changed files in this pull request and generated 8 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
Looks like this branch has conflicts with the current main branch. But I will try to give this draft PR a test later this week. |
Youw
left a comment
There was a problem hiding this comment.
Merged latest master into the branch (bef57f7). Master's virtual-device harness (#728) superseded this branch's early copy of test_virtual_device.h / the uhid provider, so test_read_interrupt.c was ported to the harness's trigger API; it is registered as ReadInterrupt_hidraw (Linux/hidraw only, as before).
Review below: gpt-6-astra (via Codex), with each finding re-checked against the code and the API usage contract.
- major: Windows: an interrupted
ReadFileis still pending whenhid_close()frees its buffers. This extends Copilot's earlier comment. - major: concurrent
hid_read_interrupt()/hid_read_clear_interrupt()can leave the wake-up object signalled with the flag cleared (linux / netbsd / windows). - minor:
hid_read_test: data race ong_terminatebetween two threads. - minor:
test_read_interrupt.c: the hang path returns while the reader thread still points at a stackctx. - doc: wording of the concurrency contract. Separately, the PR description still says
hid_read_interruptis "the only hidapi function safe to call concurrently", which no longer matches the header.
Follow-up suggestion (not a blocker): test_read_interrupt.c only runs on Linux/hidraw because it uses pthread_timedjoin_np. Most of the new wait logic is in the winapi/darwin/libusb backends, and master now has virtual-device providers for all three. Porting the test to test_platform.h threading and registering it per provider would cover them.
Status of the earlier Copilot review comments (checked against current HEAD)
| Comment | Topic | Status |
|---|---|---|
| r3144280258 | windows: comment spelling | fixed |
| r3144280273 | linux: errno clobbered on open failure |
fixed: hid_close() skips close(-1) |
| r3144280278 | linux: eventfd() errno reported as ENOMEM |
fixed |
| r3144280284 | linux: volatile flag |
fixed: __atomic acquire/release |
| r3144280292 | netbsd: volatile flag |
fixed: __atomic acquire/release |
| r3144280301 | header promises acquire semantics | fixed: atomics / Interlocked / mutex in every backend |
| r3144280307 | hid_read_interrupt() called from a signal handler |
fixed: the handler only sets a flag (new thread race noted inline) |
| r3144280313 | CMake minimum version | fixed |
| r3190018988 | linux: blocking read didn't poll the eventfd | fixed: always polls; covered by ReadInterrupt_hidraw |
| r3190019029 | windows: pending ReadFile vs CancelIo in hid_close() |
still valid: a reader thread that exits is protected by the OS cancelling its I/O at thread rundown; a reader that stays alive is not (details inline) |
| r3190019061 | windows: Ctrl+C stuck in cin.get() |
fixed: stdin is read on a helper thread |
| r3190019081 | std::atomic<bool> in a signal handler |
fixed: now sig_atomic_t (but see the inline note on the thread race) |
| r3190019101 | autotools/meson integration of hid_read_test |
not needed: optional CMake-only tool, like src/tests; meson wraps CMake |
| r3190019132 | contradictory "only function" concurrency doc | fixed in the header; the PR description still says "only" |
| r3190019153 | ASan for the new targets | fixed |
| r3190019180 | hid_is_read_interrupted() documented "-1 on error" |
fixed |
Drafted with Claude Code.
Serialize the interrupt-control API contract and make interruption return void. Drain pending Windows reads before close, handle interrupted wakeup syscalls, and make the command-line tool's signal and input lifecycle race-free. Run portable interruption scenarios with each virtual-device provider and configure ASan at target definitions for C and C++ libraries and tools. Keep API, test and workflow documentation consistent with the final behavior. Related to #799, #800 and #146. Assisted-by: Codex:GPT-6
Fetch guest-kernel dependencies from the primary archive with bounded network retries, and omit recommended desktop packages from the VM runner. Related to #799. Assisted-by: Codex:GPT-6
Youw
left a comment
There was a problem hiding this comment.
Codex revalidated the review findings against the maintainer's replies and completed a fresh independent review. No actionable correctness, concurrency, lifecycle, sanitizer-wiring or documentation findings remain. All 24 review threads are resolved; the 14 previously open threads have explicit fix or contract-based explanations.
Validation:
- Linux shared C/static C++ and MSVC ASan builds with warnings treated as errors; 25 local Windows unit tests pass.
- Standalone installed-package tool build and isolated Linux/Windows Enter, EOF, signal and reader-failure shutdown checks pass.
- The standard CI matrix, docs, real hidraw tests, and dedicated Windows/libusb virtual-device suites pass. The dedicated jobs reject missing or skipped suites; Windows includes the pooled-reader close scenario.
macOS runtime qualification requires an entitled host; NetBSD runtime qualification was unavailable in this environment.
Resolves #146 when merged. Related to #800 and #728.
Generated by Codex (GPT-6), with independent review by Codex (GPT-5.6 Sol).
Wake blocked readers with a nonblocking pipe and preserve sticky interrupt state without adding read/write-path locks. Release wake descriptors on failed opens and close. Add hardware-free lifecycle tests and run them in the native FreeBSD C, static and C++ builds. Document the test's scope and configure ASan for FreeBSD targets. Related to #799 and #730. Assisted-by: Codex:GPT-6
Youw
left a comment
There was a problem hiding this comment.
Codex completed a fresh independent review of the FreeBSD hidraw implementation in c564cfd. No actionable findings remain. The wake pipe, sticky state, failure cleanup and tests follow the established serialized-control contract without adding read/write-path locks.
Validation:
- Native FreeBSD CI passes
ReadInterrupt_freebsdin shared C, static C and C++ configurations;hid_read_testlinks in each. Autotools, Meson and manual builds also pass. - Local C/C++ and CMake ASan checks pass all six scenarios with leak detection. These use compatibility headers for unrelated FreeBSD APIs; native CI supplies target-platform qualification.
- The hardware-free suite covers interruption, queued input, pipe saturation, unaffected writes and descriptor cleanup. It does not qualify HID ioctls or enumeration.
The PR description and maintained test documentation include FreeBSD and these qualification limits. All 26 existing review threads remain resolved.
Related to #730; extends the #146 API to the FreeBSD hidraw backend.
Generated by Codex (GPT-6). Independent review by Codex (GPT-5.6 Sol).
Make clear void, reset wake notifications before publishing the cleared state, and use sticky poll readiness instead of redundant read-path loads on Linux, FreeBSD and NetBSD. Keep early checks for buffered/synchronous read paths and exercise all read timeout modes in the shared suite. Remove the standalone reader and FreeBSD socket fixture with their build and documentation references. Keep generic sanitizer configuration in its independent change and preserve the baseline libusb unit tests in CI. Related to #799 and #840. Assisted-by: claude-code:claude-opus-5-5
|
Codex validated the latest review comments and confirmed these dispositions:
The generic sanitizer refactor is independently merged in #840. The hidapitester extension remains local. Independent Claude Opus 5.5 review found no actionable issues in the feature changes, sanitizer split or local tool. Codex confirmed that all eight comments in the published review match the comments already addressed while it was pending. Each thread now has an inline reply confirming its accepted change or explaining the retained behavior or superseded suggestion. Codex squash-merged #840 as Final verification on the rebased head passed: standard builds, documentation, native BSD builds, AppVeyor branch and PR merge. Linux hidraw's two device suites ran under ASan; Windows passed both device suites; libusb passed all 28 tests without skips. The libusb rerun followed an HTTP 429 during driver download, before HIDAPI built. macOS/BSD runtime limits and the unrelated baseline static C++ libusb link limitation are documented in the PR body. All 34 review threads are resolved. The hidapitester extension is committed locally as Generated by Codex (GPT-6). Implementation and independent review by Claude Code (claude-opus-5-5). |
New functions on hid_device, implemented across all five backends (linux, libusb, mac, windows, netbsd): hid_read_interrupt - asynchronously cancel a blocked read hid_is_read_interrupted - query the sticky interrupt state hid_read_clear_interrupt - clear the state, allowing reads to resume hid_read_interrupt is the only hidapi function safe to call concurrently with another function on the same device. Other device operations (write, feature/output reports, info getters) are unaffected. Allows a dedicated reader thread to be stopped without busy-polling on a short timeout. New hid_read_test tool, to check new functionality. Resolves: #146 Assisted-by: Claude:claude-opus-4.7 Assisted-by: claude-code:claude-opus-5-5
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
- linux/hid.c: poll() unconditionally in hid_read_timeout(), including the blocking (milliseconds < 0) path, so hid_read_interrupt() can wake a blocking read via the interrupt eventfd. Previously the blocking path went straight to read() and never observed the interrupt, so a dedicated reader thread blocked in hid_read() could not be woken — defeating the documented interrupt -> join -> hid_close() shutdown. - hidapi/hidapi.h: hid_is_read_interrupted() is documented as returning only 1/0; drop the misleading "-1 on error" wording since no backend has an error path here. - hid_read_test/main.cpp: use a volatile sig_atomic_t flag (the type the standard guarantees safe to write from a signal handler) instead of std::atomic<bool>, and observe it from the main thread via a detached input-wait helper so Ctrl+C reliably triggers shutdown on all platforms (including Windows, where the console read isn't interrupted by signals). - CMakeLists.txt: add the hid_read_test* targets to the ASan target list. Assisted-by: Claude:claude-opus-4.8 Assisted-by: claude-code:claude-opus-5-5
Add a backend-generic HIDAPI test harness and a unit test for the
hid_read_interrupt() / hid_is_read_interrupted() / hid_read_clear_interrupt()
API.
src/tests/test_virtual_device.h
An opaque, backend-agnostic "virtual HID device" interface (create,
inject input, set feature reply, capture output, open via HIDAPI,
destroy). Test scenarios depend only on this, so the same scenarios can
be reused for other backends by adding a provider.
src/tests/test_virtual_device_uhid.c
Linux provider, backed by the kernel /dev/uhid interface: it makes the
kernel expose a real /dev/hidrawN node that the hidraw backend opens.
Answers GET_REPORT and captures OUTPUT/SET_REPORT via an event pump
thread (useful for future read/write/feature tests too).
src/tests/test_read_interrupt.c
Scenarios: normal read delivery, interrupt-before-read (non-blocking
return), blocking read interrupted from another thread (the regression
guard for the hidraw blocking-path fix), timed read interrupted, sticky
interrupt, clear resumes reads, idempotent interrupt/clear. Uses
pthread_timedjoin_np so a regression fails instead of hanging.
The test self-skips (CTest SKIP_RETURN_CODE 77) when no virtual device can
be created (e.g. no 'uhid' module / insufficient privileges), so it never
fails spuriously.
Build/CI:
- HIDAPI_WITH_TESTS is now offered on Linux (default OFF) and builds
src/tests for the hidraw backend.
- The ubuntu-cmake CI job configures with -DHIDAPI_WITH_TESTS=ON, loads
the uhid module (from linux-modules-extra) and runs ctest as root
(LeakSanitizer off, ASan use-after-free detection on).
Assisted-by: Claude:claude-opus-4.8
Assisted-by: claude-code:claude-opus-5-5
Code review found the documented sticky contract -- "once interrupted, subsequent hid_read()/hid_read_timeout() calls return -1 immediately" -- was violated by the mac and libusb backends: both returned queued input reports before checking the interrupt flag, and their reader threads keep queuing new reports after an interrupt. With a device that streams input continuously, the documented shutdown pattern (interrupt -> join reader -> close) never observed -1 on those two backends and the join could hang forever. windows/linux/netbsd already check the flag at entry. - mac, libusb: check read_interrupted at hid_read_timeout() entry, before the queued-report drain, matching the other three backends. A call already blocked when the interrupt arrives keeps its existing behavior (may still return data that raced in). - hidapi.h: tighten the drain-allowance wording to match: only a call already in progress may still return buffered data; calls entered after the interrupt fail immediately, and queued data remains readable after hid_read_clear_interrupt(). - windows: fail hid_open*() if the interrupt event cannot be created -- a NULL handle would make WaitForMultipleObjects() fail instantly, turning a blocking read into a busy spin returning 0. Findings surfaced by a multi-model adversarial code review. Assisted-by: claude-code:claude-fable-5
Serialize the interrupt-control API contract and make interruption return void. Drain pending Windows reads before close, handle interrupted wakeup syscalls, and make the command-line tool's signal and input lifecycle race-free. Run portable interruption scenarios with each virtual-device provider and configure ASan at target definitions for C and C++ libraries and tools. Keep API, test and workflow documentation consistent with the final behavior. Related to #799, #800 and #146. Assisted-by: Codex:GPT-6 Assisted-by: claude-code:claude-opus-5-5
Assisted-by: Codex:GPT-6
Assisted-by: Codex:GPT-6
Wake blocked readers with a nonblocking pipe and preserve sticky interrupt state without adding read/write-path locks. Release wake descriptors on failed opens and close. Add hardware-free lifecycle tests and run them in the native FreeBSD C, static and C++ builds. Document the test's scope and configure ASan for FreeBSD targets. Related to #799 and #730. Assisted-by: Codex:GPT-6
Make clear void, reset wake notifications before publishing the cleared state, and use sticky poll readiness instead of redundant read-path loads on Linux, FreeBSD and NetBSD. Keep early checks for buffered/synchronous read paths and exercise all read timeout modes in the shared suite. Remove the standalone reader and FreeBSD socket fixture with their build and documentation references. Keep generic sanitizer configuration in its independent change and preserve the baseline libusb unit tests in CI. Related to #799 and #840. Assisted-by: claude-code:claude-opus-5-5
Add the FreeBSD hidraw setter, preserve read interruption, and size libusb ring slots for descriptor-sized input reports. Move the shared ring and top-level tests under src, updating build, packaging and documentation references. Extend queue and virtual-device regression checks. Related to libusb#154, libusb#274, libusb#728, libusb#730 and libusb#799. Assisted-by: Codex:gpt-6
Add hid_set_num_input_buffers() across all backends, including FreeBSD hidraw, and share a preallocated input ring between macOS and libusb. Preserve large input reports and read interruption, release resources on initialization failure, and clear device errors after successful setters. Move core/ and tests/ under src/ and update build, distribution, CI, documentation and comment references. Extend queue and virtual-device coverage for validation and resizing. Related to #154. Related to #274. Related to #728. Related to #730. Related to #799. Co-authored-by: Ihor Dutchak <ihor.youw@gmail.com> Assisted-by: Claude Code CLI:claude-opus-4.6 Assisted-by: Claude Code CLI:claude-opus-4.7 Assisted-by: Codex CLI:gpt-5.4 Assisted-by: Codex:gpt-6 Assisted-by: Codex:gpt-6-astra Assisted-by: Claude Code:claude-fable-5-1
Adds read interruption to the Linux hidraw, FreeBSD hidraw, libusb, macOS, Windows and NetBSD backends:
void hid_read_interrupt(hid_device *)wakes a blocked reader and sets a sticky interrupt state.int hid_is_read_interrupted(hid_device *)reports that state as1or0.void hid_read_clear_interrupt(hid_device *)clears the state so reads can resume.Reads started after interruption return
-1until cleared, with an interruption diagnostic available throughhid_read_error(). A read already in progress may return input that was already buffered or queued. Queued input remains available after clear, subject to backend buffer limits. Writes and feature/output reports continue normally.Interrupt, query and clear calls on the same device must be serialized against one another, including repeated calls to the same function. They may run alongside device I/O, but must not overlap
hid_close()orhid_exit(). The device must remain valid until all calls finish. Join the reader before closing the device; clear after the active read returns when deterministic interruption is required.Linux hidraw, FreeBSD hidraw and NetBSD detect interruption through the wake descriptor in their existing poll path. Windows, macOS and libusb check the state before paths that can consume input without waiting. Clear resets the wake notification before publishing the cleared state. Windows preserves a pending overlapped read across interruption and cancels and waits for it during close.
The shared
ReadInterrupt_*tests cover blocking and timed wake-up, sticky interruption with queued input, blocking/nonblocking/timed reads, clear/resume and repeated control calls. Windows also tests closing the device while the thread that issued the interrupted read remains alive. Dedicated Windows/libusb CI rejects missing or skipped device suites.Validation:
Resolves #146.
Related to #730, #800, #728, #839 and #840.
Assisted-by: claude-code:claude-opus-5-5
Assisted-by: Codex:GPT-6