WIP: RFC: Add hid_read_interrupt API for thread-safe read cancellation #799
No reviewers
Labels
No labels
3rdparty
Android
API
Author does not respond
bsd
bug
build system/CI
ci-virtual-device
CMake
Contributions Welcome
Core
dependencies
device-specific
documentation
don't_merge
duplicate
enhancement
Example
Haiku
hidraw
hotplug
info required
invalid
libusb
macOS
Misc
Not an issue
question
Release
Test
Test GUI
Windows
wontfix
No milestone
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
gh-libusb/hidapi!799
Loading…
Reference in a new issue
No description provided.
Delete branch "read-interrupt"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
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
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 smallhid_read_testutility to exercise the behavior.Changes:
hid_read_interrupt(),hid_is_read_interrupted(), andhid_read_clear_interrupt()to the public header and implements them across all backends.hid_read_timeout()implementations to return-1with an “operation interrupted” read error when interruption is requested.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
windows/hid.cWaitForMultipleObjects()inhid_read_timeout().netbsd/hid.cpoll()fd set, plus a sticky interrupt flag.mac/hid.clinux/hid.ceventfdadded topoll()plus a sticky interrupt flag; adjusts failure cleanup path to callhid_close().libusb/hid.chidapi/hidapi.hhid_read_test/main.cpphid_read_test/CMakeLists.txtCMakeLists.txtHIDAPI_BUILD_HID_READ_TESToption to include the new tool in the build.💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
@ -0,0 +1,54 @@cmake_minimum_required(VERSION 3.1.3...3.25 FATAL_ERROR)The CMake minimum version here (3.8) is higher than the repository’s top-level minimum (3.1.3). Enabling
HIDAPI_BUILD_HID_READ_TESTwill therefore make the overall build require a newer CMake than advertised. Consider aligning thiscmake_minimum_required()with the top-level requirement unless this tool genuinely needs newer CMake features.@ -0,0 +59,4 @@/* async-signal-safe: a single store to a volatile sig_atomic_t. The mainthread polls g_terminate and calls hid_read_interrupt() from there. */g_terminate = 1;}on_signal()callshid_read_interrupt()directly from a signal handler. That is not async-signal-safe on POSIX (e.g. mac/libusb implementations take a pthread mutex/condvar), and can deadlock or crash if the signal interrupts code holding the same lock. Consider making the handler only set an atomic flag (or write to a self-pipe) and have the main thread (or a dedicated signal-wait thread usingsigwait) callhid_read_interrupt()from a safe context.@ -426,0 +473,4 @@Returns the current interrupt state set by @ref hid_read_interruptand @ref hid_read_clear_interrupt. Suitable for cross-threadobservation (read with acquire semantics).The comment promises cross-thread observation with acquire semantics, but at least the Linux and NetBSD backends implement the interrupt state as a plain
int/volatile intwithout atomics or locking. Either strengthen the implementations to actually provide atomic/acquire semantics everywhere, or relax/reword this guarantee in the public header to match what is implemented.dev->interruptedis accessed concurrently fromhid_read_interrupt(),hid_read_clear_interrupt(), andhid_read_timeout()using plain (volatile) loads/stores.volatiledoes not make this thread-safe in C; this introduces a data race/UB and breaks the API guarantee thathid_read_interrupt()is safe to call concurrently with a blocked read. Use an actual atomic (C11stdatomic.h, compiler atomics) or protect the flag with a mutex, so both the state and the wakeup mechanism have well-defined cross-thread semantics.new_hid_device()can now fail becauseeventfd()failed (e.g., EMFILE/ENFILE/EPERM), but the caller (hid_open_path) still treats anyNULLreturn as an allocation failure and overwriteserrno/global error accordingly. Consider preserving the realerrnofromeventfd()and reporting a more accurate error (or providing a fallback wakeup mechanism) so device opens don’t fail with a misleading "Couldn't allocate memory" message.@ -1146,1 +1159,4 @@return -1;}int bytes_read;In the open-failure path, calling
hid_close(dev)before formatting the error will clobbererrnobecausehid_close()unconditionally callsclose(dev->device_handle)even whendevice_handle == -1. That makes the reported error message likely showEBADFinstead of the realopen()failure. Capture/restoreerrnoaroundhid_close(), or makehid_close()skipclose()when the handle is negative.@ -52,9 +52,15 @@ struct hid_device_ {wchar_t *last_read_error_str;dev->interruptedis read inhid_read_timeout()and written inhid_read_interrupt()/hid_read_clear_interrupt()without any synchronization. Marking itvolatiledoes not make these operations atomic or race-free; this is undefined behavior ifhid_read_interrupt()is called from another thread, which is the main purpose of the new API. Switch to an atomic flag or guard access with a mutex (and keep the pipe strictly for wakeups).Spelling/grammar in the comment: “enought” → “enough”, and “very common” likely intended as “common”.
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.
@ -96,3 +107,3 @@if(NOT MSVC)# MSVC doesn't recognize those options, other compilers - requiring itforeach(HIDAPI_TARGET hidapi_winapi hidapi_darwin hidapi_hidraw hidapi_libusb hidtest_hidraw hidtest_libusb hidtest)foreach(HIDAPI_TARGET hidapi_winapi hidapi_darwin hidapi_hidraw hidapi_libusb hidtest_hidraw hidtest_libusb hidtest hid_read_test_hidraw hid_read_test_libusb hid_read_test test_read_interrupt)When
HIDAPI_ENABLE_ASAN=ON, the newhid_read_test*targets are not added to the sanitizer target list below, unlike the existing example/test executables. That makes this new shutdown test tool much less useful for catching the memory bugs this PR could introduce.@ -97,3 +108,3 @@# MSVC doesn't recognize those options, other compilers - requiring itforeach(HIDAPI_TARGET hidapi_winapi hidapi_darwin hidapi_hidraw hidapi_libusb hidtest_hidraw hidtest_libusb hidtest)foreach(HIDAPI_TARGET hidapi_winapi hidapi_darwin hidapi_hidraw hidapi_libusb hidtest_hidraw hidtest_libusb hidtest hid_read_test_hidraw hid_read_test_libusb hid_read_test test_read_interrupt)if(TARGET ${HIDAPI_TARGET})This only wires the new tool into the CMake build. The other supported source build paths still do not reference
hid_read_test(for exampleMakefile.am:42,configure.ac:249, and the Meson wrapper atmeson.build:1-23), so the PR's new tool is not actually buildable or distributable outside the CMake path.@ -0,0 +1,172 @@/*******************************************************This is documented as async-signal-safe, but
std::atomic<bool>::storeis only safe in a signal handler when the atomic is lock-free, which is not guaranteed foratomic<bool>. On targets where it falls back to an internal lock, pressing Ctrl+C can deadlock inside the handler.@ -0,0 +132,4 @@#ifdef _WIN32std::signal(SIGINT, on_signal);#ifdef SIGTERMstd::signal(SIGTERM, on_signal);The Windows signal handler only sets
g_terminate, but that flag is never observed anywhere and this branch does not have the POSIXsigaction(..., sa_flags = 0)logic that makesstd::cin.get()return on a signal. In practiceCtrl+Ccan leave the main thread stuck incin.get()and never callhid_read_interrupt(), so the advertised shutdown path is unreliable on Windows.@ -423,6 +423,91 @@ extern "C" {*/These docs say
hid_read_interrupt()is the onlyhid_devicefunction that may be called concurrently with another device operation, buthid_is_read_interrupted()is immediately documented below as suitable for cross-thread observation. That makes the new thread-safety contract self-contradictory for callers.All five backends implement this as a pure flag read and only ever return 0 or 1; there is no error path and no backend sets
hid_error(dev)here. Documenting-1 on erroras part of the public contract is therefore misleading for API consumers.@ -1153,2 +1164,2 @@properly report device disconnection through read() whenin non-blocking mode. */{/* Always poll() - even for a blocking read (milliseconds < 0) -This only polls the interrupt eventfd when
milliseconds >= 0. A normal blockinghid_read()/hid_read_timeout(..., -1)still goes straight toread(), sohid_read_interrupt()cannot wake the common blocking-read path on the hidraw backend and the reader thread will keep hanging until the device produces input or disconnects.Returning here leaves the overlapped
ReadFileowningdev->olanddev->read_bufafter the reader thread exits. The documentedhid_read_interrupt()-> join reader ->hid_close()flow is then unsafe on Windows, becausehid_close()still usesCancelIo, which only cancels I/O issued by the calling thread, so the pending read started on the reader thread may still be in flight while those buffers are being freed.View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.Merge
Merge the changes and update on Forgejo.Warning: The "Autodetect manual merge" setting is not enabled for this repository, you will have to mark this pull request as manually merged afterwards.