diff --git a/hidapi/hidapi.h b/hidapi/hidapi.h index cbc3107dd..c14ada29d 100644 --- a/hidapi/hidapi.h +++ b/hidapi/hidapi.h @@ -423,6 +423,44 @@ extern "C" { */ int HID_API_EXPORT HID_API_CALL hid_set_nonblocking(hid_device *dev, int nonblock); + /** @brief Upper bound for hid_set_num_input_buffers(). + + Values passed above this limit are rejected by + hid_set_num_input_buffers(). Guards against + memory-exhaustion via unbounded input report queue growth. + */ + #define HID_API_MAX_NUM_INPUT_BUFFERS 1024 + + /** @brief Set the number of input report buffers queued per device. + + Some HID devices emit input reports in bursts at rates + that exceed the default internal queue capacity, causing + silent report drops on macOS and the libusb Linux backend. + This function allows callers to change how many input + report buffers are retained per device. + + Defaults per backend: + - macOS: 30 reports + - Linux libusb: 30 reports + - Windows: 64 reports (via HidD_SetNumInputBuffers) + - Linux hidraw: kernel-managed, no userspace queue + - NetBSD: kernel-managed, no userspace queue + + Call after hid_open() and before the first hid_read() + to avoid losing reports buffered at open time. + + @ingroup API + @param dev A device handle returned from hid_open(). + @param num_buffers The desired number of input report buffers. + Must be in range [1, HID_API_MAX_NUM_INPUT_BUFFERS]. + + @returns + 0 on success, -1 on failure (invalid parameters or + backend-specific error). Call hid_error(dev) for + details where supported. + */ + int HID_API_EXPORT HID_API_CALL hid_set_num_input_buffers(hid_device *dev, int num_buffers); + /** @brief Send a Feature report to the device. Feature reports are sent over the Control endpoint as a diff --git a/libusb/hid.c b/libusb/hid.c index d2ceef5d3..5e7bf83a7 100644 --- a/libusb/hid.c +++ b/libusb/hid.c @@ -110,6 +110,9 @@ struct hid_device_ { /* Whether blocking reads are used */ int blocking; /* boolean */ + /* Maximum number of input reports to queue before dropping oldest. */ + int num_input_buffers; + /* Read thread objects */ hidapi_thread_state thread_state; int shutdown_thread; @@ -143,6 +146,7 @@ static hid_device *new_hid_device(void) return NULL; dev->blocking = 1; + dev->num_input_buffers = 30; hidapi_thread_state_init(&dev->thread_state); @@ -985,7 +989,7 @@ static void LIBUSB_CALL read_callback(struct libusb_transfer *transfer) /* Pop one off if we've reached 30 in the queue. This way we don't grow forever if the user never reads anything from the device. */ - if (num_queued > 30) { + if (num_queued > dev->num_input_buffers) { return_data(dev, NULL, 0); } } @@ -1574,6 +1578,21 @@ HID_API_EXPORT const wchar_t * HID_API_CALL hid_read_error(hid_device *dev) } + +int HID_API_EXPORT hid_set_num_input_buffers(hid_device *dev, int num_buffers) +{ + /* Note: libusb backend currently has no error reporting infrastructure + (hid_error returns a fixed string). This function returns -1 on + invalid arguments but cannot provide a descriptive error message + until the backend gains error registration. */ + if (num_buffers <= 0 || num_buffers > HID_API_MAX_NUM_INPUT_BUFFERS) + return -1; + hidapi_thread_mutex_lock(&dev->thread_state); + dev->num_input_buffers = num_buffers; + hidapi_thread_mutex_unlock(&dev->thread_state); + return 0; +} + int HID_API_EXPORT hid_set_nonblocking(hid_device *dev, int nonblock) { dev->blocking = !nonblock; diff --git a/linux/hid.c b/linux/hid.c index a4dc26f4e..dea5c16be 100644 --- a/linux/hid.c +++ b/linux/hid.c @@ -1199,6 +1199,21 @@ HID_API_EXPORT const wchar_t * HID_API_CALL hid_read_error(hid_device *dev) return dev->last_read_error_str; } + +int HID_API_EXPORT hid_set_num_input_buffers(hid_device *dev, int num_buffers) +{ + if (num_buffers <= 0 || num_buffers > HID_API_MAX_NUM_INPUT_BUFFERS) { + register_error_str(&dev->last_error_str, "num_buffers out of range"); + return -1; + } + /* No-op on Linux hidraw and BSD backends: the kernel manages input + report buffering and there is no userspace queue to resize. The + call is accepted (returns 0) to preserve a consistent cross-platform + API so callers do not need per-backend conditional code. */ + (void)num_buffers; + return 0; +} + int HID_API_EXPORT hid_set_nonblocking(hid_device *dev, int nonblock) { /* Do all non-blocking in userspace using poll(), since it looks diff --git a/mac/hid.c b/mac/hid.c index a91bc1902..d9a9bb21e 100644 --- a/mac/hid.c +++ b/mac/hid.c @@ -127,6 +127,7 @@ struct hid_device_ { IOOptionBits open_options; int blocking; int disconnected; + int num_input_buffers; CFStringRef run_loop_mode; CFRunLoopRef run_loop; CFRunLoopSourceRef source; @@ -156,6 +157,7 @@ static hid_device *new_hid_device(void) dev->open_options = device_open_options; dev->blocking = 1; dev->disconnected = 0; + dev->num_input_buffers = 30; dev->run_loop_mode = NULL; dev->run_loop = NULL; dev->source = NULL; @@ -908,7 +910,7 @@ static void hid_report_callback(void *context, IOReturn result, void *sender, /* Pop one off if we've reached 30 in the queue. This way we don't grow forever if the user never reads anything from the device. */ - if (num_queued > 30) { + if (num_queued > dev->num_input_buffers) { return_data(dev, NULL, 0); } } @@ -1347,6 +1349,19 @@ HID_API_EXPORT const wchar_t * HID_API_CALL hid_read_error(hid_device *dev) return dev->last_read_error_str; } + +int HID_API_EXPORT hid_set_num_input_buffers(hid_device *dev, int num_buffers) +{ + if (num_buffers <= 0 || num_buffers > HID_API_MAX_NUM_INPUT_BUFFERS) { + register_error_str(&dev->last_error_str, "num_buffers out of range"); + return -1; + } + pthread_mutex_lock(&dev->mutex); + dev->num_input_buffers = num_buffers; + pthread_mutex_unlock(&dev->mutex); + return 0; +} + int HID_API_EXPORT hid_set_nonblocking(hid_device *dev, int nonblock) { /* All Nonblocking operation is handled by the library. */ diff --git a/netbsd/hid.c b/netbsd/hid.c index a9fca67c5..9f2b5e223 100644 --- a/netbsd/hid.c +++ b/netbsd/hid.c @@ -955,6 +955,21 @@ HID_API_EXPORT const wchar_t* HID_API_CALL hid_read_error(hid_device *dev) return dev->last_read_error_str; } + +int HID_API_EXPORT HID_API_CALL hid_set_num_input_buffers(hid_device *dev, int num_buffers) +{ + if (num_buffers <= 0 || num_buffers > HID_API_MAX_NUM_INPUT_BUFFERS) { + register_error_str(&dev->last_error_str, "num_buffers out of range"); + return -1; + } + /* No-op on Linux hidraw and BSD backends: the kernel manages input + report buffering and there is no userspace queue to resize. The + call is accepted (returns 0) to preserve a consistent cross-platform + API so callers do not need per-backend conditional code. */ + (void)num_buffers; + return 0; +} + int HID_API_EXPORT HID_API_CALL hid_set_nonblocking(hid_device *dev, int nonblock) { dev->blocking = !nonblock; diff --git a/windows/hid.c b/windows/hid.c index 1e27f10a4..a628901e1 100644 --- a/windows/hid.c +++ b/windows/hid.c @@ -1257,6 +1257,20 @@ HID_API_EXPORT const wchar_t * HID_API_CALL hid_read_error(hid_device *dev) return dev->last_read_error_str; } + +int HID_API_EXPORT HID_API_CALL hid_set_num_input_buffers(hid_device *dev, int num_buffers) +{ + if (num_buffers <= 0 || num_buffers > HID_API_MAX_NUM_INPUT_BUFFERS) { + register_string_error(dev, L"num_buffers out of range"); + return -1; + } + if (!HidD_SetNumInputBuffers(dev->device_handle, (ULONG)num_buffers)) { + register_winapi_error(dev, L"HidD_SetNumInputBuffers"); + return -1; + } + return 0; +} + int HID_API_EXPORT HID_API_CALL hid_set_nonblocking(hid_device *dev, int nonblock) { dev->blocking = !nonblock;