Skip to content

RFC: Add hid_read_interrupt API for thread-safe read cancellation - #799

Merged
Youw merged 11 commits into
masterfrom
read-interrupt
Nov 8, 2026
Merged

Youw merged 11 commits into
masterfrom
read-interrupt

Conversation

@Youw

@Youw Youw commented Apr 26, 2026 •

Copy link
Copy Markdown
Member

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 as 1 or 0.
  • void hid_read_clear_interrupt(hid_device *) clears the state so reads can resume.

Reads started after interruption return -1 until cleared, with an interruption diagnostic available through hid_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() or hid_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

@Youw
Youw requested a review from Copilot April 26, 2026 22:28
@Youw Youw changed the title Add hid_read_interrupt API for thread-safe read cancellation RFC: Add hid_read_interrupt API for thread-safe read cancellation Apr 26, 2026

Copilot AI 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.

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(), and hid_read_clear_interrupt() to the public header and implements them across all backends.
  • Updates backend hid_read_timeout() implementations to return -1 with an “operation interrupted” read error when interruption is requested.
  • Adds a new optional hid_read_test tool 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.

Comment thread windows/hid.c Resolved
Comment thread linux/hid.c Resolved
Comment thread linux/hid.c Resolved
Comment thread linux/hid.c Resolved
Comment thread netbsd/hid.c Resolved
Comment thread hidapi/hidapi.h Resolved
Comment thread hid_read_test/main.cpp Resolved
Comment thread hid_read_test/CMakeLists.txt Resolved
@mcuee mcuee added the enhancement New feature or request label Apr 27, 2026
Youw added a commit that referenced this pull request May 5, 2026
@Youw
Youw requested a review from Copilot May 5, 2026 16:16

Copilot AI 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.

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.

Comment thread linux/hid.c Resolved
Comment thread windows/hid.c Resolved
Comment thread hid_read_test/main.cpp Resolved
Comment thread hid_read_test/main.cpp Resolved
Comment thread CMakeLists.txt Resolved
Comment thread hidapi/hidapi.h Resolved
Comment thread CMakeLists.txt Resolved
Comment thread hidapi/hidapi.h Resolved
@mcuee

mcuee commented Oct 6, 2026

Copy link
Copy Markdown
Member

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 Youw left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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 ReadFile is still pending when hid_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 on g_terminate between two threads.
  • minor: test_read_interrupt.c: the hang path returns while the reader thread still points at a stack ctx.
  • doc: wording of the concurrency contract. Separately, the PR description still says hid_read_interrupt is "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.

Comment thread windows/hid.c Resolved
Comment thread linux/hid.c Resolved
Comment thread hid_read_test/main.cpp Resolved
Comment thread src/tests/test_read_interrupt.c Resolved
Comment thread hidapi/hidapi.h Resolved
Comment thread CMakeLists.txt Resolved
Comment thread hidapi/hidapi.h Resolved
Comment thread hidapi/hidapi.h Resolved
Youw added a commit that referenced this pull request Nov 5, 2026
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
Youw added a commit that referenced this pull request Nov 5, 2026
Use the generic kernel and modules-extra package layout required by the
existing raw-gadget VM tests. Bound setup, module builds and guest execution,
and describe the provider's current setup consistently.

Related to #799 and #728.

Assisted-by: Codex:GPT-6
Youw added a commit that referenced this pull request Nov 5, 2026
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 Youw left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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).

@Youw
Youw marked this pull request as ready for review November 5, 2026 15:04
Comment thread .github/workflows/builds.yml Resolved
Comment thread .github/workflows/builds.yml Resolved
Youw added a commit that referenced this pull request Nov 6, 2026
Pin the runner to Ubuntu 24.04 with matching generic-kernel packages. Use apt with the primary Ubuntu archive and no recommended packages; bound setup, builds and VM execution. Document the packaged raw_gadget and source-built dummy_hcd setup.

Related to #799 and #728.

Assisted-by: Codex:GPT-6
Youw added a commit that referenced this pull request Nov 6, 2026
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 Youw left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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_freebsd in shared C, static C and C++ configurations; hid_read_test links 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).

Youw added a commit that referenced this pull request Nov 7, 2026
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
Youw added a commit that referenced this pull request Nov 7, 2026
Apply ASan to each library, test and example target for C and C++. Export the sanitizer runtime requirement from static libraries and retain MSVC incremental-link settings without changing parent compiler flags.

Related to #799, #728 and #730.

Assisted-by: claude-code:claude-opus-5-5
@Youw

Youw commented Nov 7, 2026 •

Copy link
Copy Markdown
Member Author

Codex validated the latest review comments and confirmed these dispositions:

  • Clear API: hid_read_clear_interrupt() is now void in the header, all six backends, shared tests and the local tool. No artificial result checks remain.
  • Clear ordering (Linux, FreeBSD, NetBSD): the wake descriptor is drained before the cleared flag is published. Windows likewise resets its event before clearing the flag. macOS/libusb retain their existing mutex protection.
  • Early read checks: removed from Linux hidraw, FreeBSD hidraw and NetBSD. Sticky wake-descriptor readiness is checked before consuming input, including queued data. The shared test covers blocking, nonblocking and timed reads in this state. Windows keeps its check because ReadFile may complete synchronously and completed reads have priority in its wait array. macOS/libusb keep theirs because queued reports can be returned before waiting. Removing those checks would violate the sticky-interruption contract.
  • FreeBSD CI formatting (shared, static, C++): superseded by complete removal of the FreeBSD fixture and standalone reader, their CMake options, CI commands and documentation. .builds/freebsd.yml matches master.

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 bc6d898 with Ihor Dutchak recorded as the author, then rebased and pushed #799 as 29b64aa. The feature diff inherits the generic sanitizer configuration from master.

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 d20eecf and remains unpublished.

Generated by Codex (GPT-6). Implementation and independent review by Claude Code (claude-opus-5-5).

Youw and others added 11 commits November 7, 2026 23:26
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
Comment thread .builds/freebsd.yml Resolved
Comment thread .builds/freebsd.yml Resolved
Comment thread .builds/freebsd.yml Resolved
Comment thread freebsd/hid.c Resolved
Comment thread hidapi/hidapi.h Resolved
Comment thread netbsd/hid.c Resolved
Comment thread linux/hid.c Resolved
Comment thread linux/hid.c Resolved
@Youw
Youw merged commit 1daa379 into master Nov 8, 2026
31 of 32 checks passed
@Youw
Youw deleted the read-interrupt branch November 8, 2026 09:59
Youw added a commit to auxcorelabs/hidapi that referenced this pull request Nov 8, 2026
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
Youw added a commit that referenced this pull request Nov 8, 2026
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

add hid_interrupt_read

3 participants