From 2884690d6a5ef2dbac3029748b502615f9cf61e2 Mon Sep 17 00:00:00 2001 From: auxcorelabs Date: Fri, 17 Apr 2026 06:43:43 +0000 Subject: [PATCH 1/5] Add hid_set_input_report_buffer_size() API Exposes a new public function to resize the per-device input report queue. High-throughput HID devices (medical telemetry, high-poll-rate gaming peripherals, data acquisition hardware) emit bursts of input reports that exceed the current hardcoded queue sizes (30 on macOS and the libusb backend, 64 on Windows). When a burst exceeds the queue, reports are silently dropped with no error indication to the caller. This adds: - hid_set_input_report_buffer_size(dev, size) in hidapi.h - HID_API_MAX_INPUT_REPORT_BUFFER_SIZE (1024) cap to prevent unbounded memory growth Per-backend behavior: - macOS: resizes the userspace IOHIDQueue-fed report queue - Linux libusb: resizes the userspace report queue - Windows: wraps HidD_SetNumInputBuffers (parity with existing behavior) - Linux hidraw: no-op (kernel manages buffering) - NetBSD: no-op (kernel manages buffering) Defaults are unchanged, so existing callers are unaffected. Values outside [1, HID_API_MAX_INPUT_REPORT_BUFFER_SIZE] are rejected with -1. Thread-safe on macOS (dev->mutex) and libusb (dev->thread_state), matching the locks used by the respective report callbacks. Addresses the same need as closed issue #154 (HidD_SetNumInputBuffers exposure) and complements #725 (callback-based input API). --- hidapi/hidapi.h | 38 ++++++++++++++++++++++++++++++++++++++ libusb/hid.c | 23 ++++++++++++++++++++++- linux/hid.c | 18 ++++++++++++++++++ mac/hid.c | 21 ++++++++++++++++++++- netbsd/hid.c | 18 ++++++++++++++++++ windows/hid.c | 18 ++++++++++++++++++ 6 files changed, 134 insertions(+), 2 deletions(-) diff --git a/hidapi/hidapi.h b/hidapi/hidapi.h index cbc3107dd..c3c2502de 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_input_report_buffer_size(). + + Values passed above this limit are rejected by + hid_set_input_report_buffer_size(). Guards against + memory-exhaustion via unbounded input report queue growth. + */ + #define HID_API_MAX_INPUT_REPORT_BUFFER_SIZE 1024 + + /** @brief Set the size of the input report buffer/queue. + + 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 resize the per-device + input report buffer. + + 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 buffer_size The desired buffer size in reports. + Must be in range [1, HID_API_MAX_INPUT_REPORT_BUFFER_SIZE]. + + @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_input_report_buffer_size(hid_device *dev, int buffer_size); + /** @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..8fc097a3a 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 input_report_buffer_size; + /* 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->input_report_buffer_size = 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->input_report_buffer_size) { return_data(dev, NULL, 0); } } @@ -1574,6 +1578,23 @@ HID_API_EXPORT const wchar_t * HID_API_CALL hid_read_error(hid_device *dev) } + +int HID_API_EXPORT hid_set_input_report_buffer_size(hid_device *dev, int buffer_size) +{ + /* 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 (!dev) + return -1; + if (buffer_size <= 0 || buffer_size > HID_API_MAX_INPUT_REPORT_BUFFER_SIZE) + return -1; + hidapi_thread_mutex_lock(&dev->thread_state); + dev->input_report_buffer_size = buffer_size; + 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..880526095 100644 --- a/linux/hid.c +++ b/linux/hid.c @@ -1199,6 +1199,24 @@ 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_input_report_buffer_size(hid_device *dev, int buffer_size) +{ + if (!dev) { + register_global_error("Device is NULL"); + return -1; + } + if (buffer_size <= 0 || buffer_size > HID_API_MAX_INPUT_REPORT_BUFFER_SIZE) { + register_error_str(&dev->last_error_str, "buffer_size out of range"); + return -1; + } + /* hidraw: kernel manages the input report buffer, no userspace queue + to resize. Accept the call to preserve a consistent cross-platform + API so callers do not need per-backend conditional code. */ + (void)buffer_size; + 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..823624f96 100644 --- a/mac/hid.c +++ b/mac/hid.c @@ -127,6 +127,7 @@ struct hid_device_ { IOOptionBits open_options; int blocking; int disconnected; + int input_report_buffer_size; 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->input_report_buffer_size = 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->input_report_buffer_size) { return_data(dev, NULL, 0); } } @@ -1347,6 +1349,23 @@ 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_input_report_buffer_size(hid_device *dev, int buffer_size) +{ + if (!dev) { + register_global_error("Device is NULL"); + return -1; + } + if (buffer_size <= 0 || buffer_size > HID_API_MAX_INPUT_REPORT_BUFFER_SIZE) { + register_error_str(&dev->last_error_str, "buffer_size out of range"); + return -1; + } + pthread_mutex_lock(&dev->mutex); + dev->input_report_buffer_size = buffer_size; + 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..9fabef199 100644 --- a/netbsd/hid.c +++ b/netbsd/hid.c @@ -955,6 +955,24 @@ 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_input_report_buffer_size(hid_device *dev, int buffer_size) +{ + if (!dev) { + register_global_error("Device is NULL"); + return -1; + } + if (buffer_size <= 0 || buffer_size > HID_API_MAX_INPUT_REPORT_BUFFER_SIZE) { + register_error_str(&dev->last_error_str, "buffer_size out of range"); + return -1; + } + /* NetBSD: kernel manages the input report buffer, no userspace queue + to resize. Accept the call to preserve a consistent cross-platform + API so callers do not need per-backend conditional code. */ + (void)buffer_size; + 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..39dc90da0 100644 --- a/windows/hid.c +++ b/windows/hid.c @@ -1257,6 +1257,24 @@ 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_input_report_buffer_size(hid_device *dev, int buffer_size) +{ + if (!dev) { + register_global_error(L"Device is NULL"); + return -1; + } + if (buffer_size <= 0 || buffer_size > HID_API_MAX_INPUT_REPORT_BUFFER_SIZE) { + register_string_error(dev, L"buffer_size out of range"); + return -1; + } + if (!HidD_SetNumInputBuffers(dev->device_handle, (ULONG)buffer_size)) { + 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; From f376588a972884487ccb4b99b6cd18b6fe3e62fe Mon Sep 17 00:00:00 2001 From: auxcorelabs Date: Fri, 17 Apr 2026 16:10:11 +0000 Subject: [PATCH 2/5] Address review: remove NULL checks, clarify no-op backends Per maintainer feedback on PR #787: - Remove if (!dev) validation from all 5 backends. hidapi convention is that device functions trust the caller to pass a valid handle; only hid_close is permitted to accept NULL. - Reword the inline comment in linux/hid.c and netbsd/hid.c to lead with "No-op" so the caller-visible behavior is explicit at the implementation site. --- libusb/hid.c | 2 -- linux/hid.c | 9 +++------ mac/hid.c | 4 ---- netbsd/hid.c | 9 +++------ windows/hid.c | 4 ---- 5 files changed, 6 insertions(+), 22 deletions(-) diff --git a/libusb/hid.c b/libusb/hid.c index 8fc097a3a..b42f558e1 100644 --- a/libusb/hid.c +++ b/libusb/hid.c @@ -1585,8 +1585,6 @@ int HID_API_EXPORT hid_set_input_report_buffer_size(hid_device *dev, int buffer_ (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 (!dev) - return -1; if (buffer_size <= 0 || buffer_size > HID_API_MAX_INPUT_REPORT_BUFFER_SIZE) return -1; hidapi_thread_mutex_lock(&dev->thread_state); diff --git a/linux/hid.c b/linux/hid.c index 880526095..549247bec 100644 --- a/linux/hid.c +++ b/linux/hid.c @@ -1202,16 +1202,13 @@ HID_API_EXPORT const wchar_t * HID_API_CALL hid_read_error(hid_device *dev) int HID_API_EXPORT hid_set_input_report_buffer_size(hid_device *dev, int buffer_size) { - if (!dev) { - register_global_error("Device is NULL"); - return -1; - } if (buffer_size <= 0 || buffer_size > HID_API_MAX_INPUT_REPORT_BUFFER_SIZE) { register_error_str(&dev->last_error_str, "buffer_size out of range"); return -1; } - /* hidraw: kernel manages the input report buffer, no userspace queue - to resize. Accept the call to preserve a consistent cross-platform + /* 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)buffer_size; return 0; diff --git a/mac/hid.c b/mac/hid.c index 823624f96..7002a7a3c 100644 --- a/mac/hid.c +++ b/mac/hid.c @@ -1352,10 +1352,6 @@ HID_API_EXPORT const wchar_t * HID_API_CALL hid_read_error(hid_device *dev) int HID_API_EXPORT hid_set_input_report_buffer_size(hid_device *dev, int buffer_size) { - if (!dev) { - register_global_error("Device is NULL"); - return -1; - } if (buffer_size <= 0 || buffer_size > HID_API_MAX_INPUT_REPORT_BUFFER_SIZE) { register_error_str(&dev->last_error_str, "buffer_size out of range"); return -1; diff --git a/netbsd/hid.c b/netbsd/hid.c index 9fabef199..b970e724c 100644 --- a/netbsd/hid.c +++ b/netbsd/hid.c @@ -958,16 +958,13 @@ HID_API_EXPORT const wchar_t* HID_API_CALL hid_read_error(hid_device *dev) int HID_API_EXPORT HID_API_CALL hid_set_input_report_buffer_size(hid_device *dev, int buffer_size) { - if (!dev) { - register_global_error("Device is NULL"); - return -1; - } if (buffer_size <= 0 || buffer_size > HID_API_MAX_INPUT_REPORT_BUFFER_SIZE) { register_error_str(&dev->last_error_str, "buffer_size out of range"); return -1; } - /* NetBSD: kernel manages the input report buffer, no userspace queue - to resize. Accept the call to preserve a consistent cross-platform + /* 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)buffer_size; return 0; diff --git a/windows/hid.c b/windows/hid.c index 39dc90da0..c96e2a30a 100644 --- a/windows/hid.c +++ b/windows/hid.c @@ -1260,10 +1260,6 @@ HID_API_EXPORT const wchar_t * HID_API_CALL hid_read_error(hid_device *dev) int HID_API_EXPORT HID_API_CALL hid_set_input_report_buffer_size(hid_device *dev, int buffer_size) { - if (!dev) { - register_global_error(L"Device is NULL"); - return -1; - } if (buffer_size <= 0 || buffer_size > HID_API_MAX_INPUT_REPORT_BUFFER_SIZE) { register_string_error(dev, L"buffer_size out of range"); return -1; From 747151f8b3a77288e5c0cdcf660824d3d181bf88 Mon Sep 17 00:00:00 2001 From: auxcorelabs Date: Sun, 19 Apr 2026 05:36:04 +0000 Subject: [PATCH 3/5] Rename to hid_set_num_input_buffers to avoid ambiguity. This function controls the number of input report buffers, not their byte size. - Function: hid_set_input_report_buffer_size -> hid_set_num_input_buffers - Macro: HID_API_MAX_INPUT_REPORT_BUFFER_SIZE -> HID_API_MAX_NUM_INPUT_BUFFERS - Parameter: buffer_size -> num_buffers - Error string: "buffer_size out of range" -> "num_buffers out of range" --- hidapi/hidapi.h | 18 +++++++++--------- libusb/hid.c | 12 ++++++------ linux/hid.c | 8 ++++---- mac/hid.c | 14 +++++++------- netbsd/hid.c | 8 ++++---- windows/hid.c | 8 ++++---- 6 files changed, 34 insertions(+), 34 deletions(-) diff --git a/hidapi/hidapi.h b/hidapi/hidapi.h index c3c2502de..c14ada29d 100644 --- a/hidapi/hidapi.h +++ b/hidapi/hidapi.h @@ -423,21 +423,21 @@ extern "C" { */ int HID_API_EXPORT HID_API_CALL hid_set_nonblocking(hid_device *dev, int nonblock); - /** @brief Upper bound for hid_set_input_report_buffer_size(). + /** @brief Upper bound for hid_set_num_input_buffers(). Values passed above this limit are rejected by - hid_set_input_report_buffer_size(). Guards against + hid_set_num_input_buffers(). Guards against memory-exhaustion via unbounded input report queue growth. */ - #define HID_API_MAX_INPUT_REPORT_BUFFER_SIZE 1024 + #define HID_API_MAX_NUM_INPUT_BUFFERS 1024 - /** @brief Set the size of the input report buffer/queue. + /** @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 resize the per-device - input report buffer. + This function allows callers to change how many input + report buffers are retained per device. Defaults per backend: - macOS: 30 reports @@ -451,15 +451,15 @@ extern "C" { @ingroup API @param dev A device handle returned from hid_open(). - @param buffer_size The desired buffer size in reports. - Must be in range [1, HID_API_MAX_INPUT_REPORT_BUFFER_SIZE]. + @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_input_report_buffer_size(hid_device *dev, int buffer_size); + 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. diff --git a/libusb/hid.c b/libusb/hid.c index b42f558e1..5e7bf83a7 100644 --- a/libusb/hid.c +++ b/libusb/hid.c @@ -111,7 +111,7 @@ struct hid_device_ { int blocking; /* boolean */ /* Maximum number of input reports to queue before dropping oldest. */ - int input_report_buffer_size; + int num_input_buffers; /* Read thread objects */ hidapi_thread_state thread_state; @@ -146,7 +146,7 @@ static hid_device *new_hid_device(void) return NULL; dev->blocking = 1; - dev->input_report_buffer_size = 30; + dev->num_input_buffers = 30; hidapi_thread_state_init(&dev->thread_state); @@ -989,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 > dev->input_report_buffer_size) { + if (num_queued > dev->num_input_buffers) { return_data(dev, NULL, 0); } } @@ -1579,16 +1579,16 @@ HID_API_EXPORT const wchar_t * HID_API_CALL hid_read_error(hid_device *dev) -int HID_API_EXPORT hid_set_input_report_buffer_size(hid_device *dev, int buffer_size) +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 (buffer_size <= 0 || buffer_size > HID_API_MAX_INPUT_REPORT_BUFFER_SIZE) + if (num_buffers <= 0 || num_buffers > HID_API_MAX_NUM_INPUT_BUFFERS) return -1; hidapi_thread_mutex_lock(&dev->thread_state); - dev->input_report_buffer_size = buffer_size; + dev->num_input_buffers = num_buffers; hidapi_thread_mutex_unlock(&dev->thread_state); return 0; } diff --git a/linux/hid.c b/linux/hid.c index 549247bec..dea5c16be 100644 --- a/linux/hid.c +++ b/linux/hid.c @@ -1200,17 +1200,17 @@ HID_API_EXPORT const wchar_t * HID_API_CALL hid_read_error(hid_device *dev) } -int HID_API_EXPORT hid_set_input_report_buffer_size(hid_device *dev, int buffer_size) +int HID_API_EXPORT hid_set_num_input_buffers(hid_device *dev, int num_buffers) { - if (buffer_size <= 0 || buffer_size > HID_API_MAX_INPUT_REPORT_BUFFER_SIZE) { - register_error_str(&dev->last_error_str, "buffer_size out of range"); + 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)buffer_size; + (void)num_buffers; return 0; } diff --git a/mac/hid.c b/mac/hid.c index 7002a7a3c..d9a9bb21e 100644 --- a/mac/hid.c +++ b/mac/hid.c @@ -127,7 +127,7 @@ struct hid_device_ { IOOptionBits open_options; int blocking; int disconnected; - int input_report_buffer_size; + int num_input_buffers; CFStringRef run_loop_mode; CFRunLoopRef run_loop; CFRunLoopSourceRef source; @@ -157,7 +157,7 @@ static hid_device *new_hid_device(void) dev->open_options = device_open_options; dev->blocking = 1; dev->disconnected = 0; - dev->input_report_buffer_size = 30; + dev->num_input_buffers = 30; dev->run_loop_mode = NULL; dev->run_loop = NULL; dev->source = NULL; @@ -910,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 > dev->input_report_buffer_size) { + if (num_queued > dev->num_input_buffers) { return_data(dev, NULL, 0); } } @@ -1350,14 +1350,14 @@ HID_API_EXPORT const wchar_t * HID_API_CALL hid_read_error(hid_device *dev) } -int HID_API_EXPORT hid_set_input_report_buffer_size(hid_device *dev, int buffer_size) +int HID_API_EXPORT hid_set_num_input_buffers(hid_device *dev, int num_buffers) { - if (buffer_size <= 0 || buffer_size > HID_API_MAX_INPUT_REPORT_BUFFER_SIZE) { - register_error_str(&dev->last_error_str, "buffer_size out of range"); + 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->input_report_buffer_size = buffer_size; + dev->num_input_buffers = num_buffers; pthread_mutex_unlock(&dev->mutex); return 0; } diff --git a/netbsd/hid.c b/netbsd/hid.c index b970e724c..9f2b5e223 100644 --- a/netbsd/hid.c +++ b/netbsd/hid.c @@ -956,17 +956,17 @@ HID_API_EXPORT const wchar_t* HID_API_CALL hid_read_error(hid_device *dev) } -int HID_API_EXPORT HID_API_CALL hid_set_input_report_buffer_size(hid_device *dev, int buffer_size) +int HID_API_EXPORT HID_API_CALL hid_set_num_input_buffers(hid_device *dev, int num_buffers) { - if (buffer_size <= 0 || buffer_size > HID_API_MAX_INPUT_REPORT_BUFFER_SIZE) { - register_error_str(&dev->last_error_str, "buffer_size out of range"); + 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)buffer_size; + (void)num_buffers; return 0; } diff --git a/windows/hid.c b/windows/hid.c index c96e2a30a..a628901e1 100644 --- a/windows/hid.c +++ b/windows/hid.c @@ -1258,13 +1258,13 @@ HID_API_EXPORT const wchar_t * HID_API_CALL hid_read_error(hid_device *dev) } -int HID_API_EXPORT HID_API_CALL hid_set_input_report_buffer_size(hid_device *dev, int buffer_size) +int HID_API_EXPORT HID_API_CALL hid_set_num_input_buffers(hid_device *dev, int num_buffers) { - if (buffer_size <= 0 || buffer_size > HID_API_MAX_INPUT_REPORT_BUFFER_SIZE) { - register_string_error(dev, L"buffer_size out of range"); + 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)buffer_size)) { + if (!HidD_SetNumInputBuffers(dev->device_handle, (ULONG)num_buffers)) { register_winapi_error(dev, L"HidD_SetNumInputBuffers"); return -1; } From 4364aed7d1be01d46043e082f89414fe817b24fe Mon Sep 17 00:00:00 2001 From: auxcorelabs Date: Mon, 20 Apr 2026 09:48:37 +0000 Subject: [PATCH 4/5] Address review: relocate cross-backend docs to @note, strip backend-file commentary, add hidtest coverage - hidapi/hidapi.h: replace the Defaults per backend list with an @note Per-backend behavior block covering macOS / Windows / libusb / hidraw / uhid semantics, ranges, and defaults. Per @Youw, the public header is the canonical place for the cross-backend contract. - linux/hid.c, netbsd/hid.c: drop the comment that cross-referenced other backends. The (void)num_buffers; idiom and the header contract speak for themselves. - libusb/hid.c: drop the self-scoped no-error-registration note for the same reason. - hidtest/test.c: add a compile-time symbol reference and a runtime call hid_set_num_input_buffers(handle, 500) right after hid_open() succeeds, per @mcuee. Both guarded on HID_API_VERSION >= 0.16.0 so they activate in the 0.16 release cycle, matching the precedent of hid_send_output_report at 0.15.0. --- hidapi/hidapi.h | 24 +++++++++++++++--------- hidtest/test.c | 10 ++++++++++ libusb/hid.c | 4 ---- linux/hid.c | 4 ---- netbsd/hid.c | 4 ---- 5 files changed, 25 insertions(+), 21 deletions(-) diff --git a/hidapi/hidapi.h b/hidapi/hidapi.h index c14ada29d..195e5757e 100644 --- a/hidapi/hidapi.h +++ b/hidapi/hidapi.h @@ -439,15 +439,21 @@ extern "C" { 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. + Call after hid_open() and before the first hid_read() to + avoid losing reports buffered at open time. + + @note Per-backend behavior: + - **macOS (IOKit)** and **Linux libusb**: resizes the + userspace input-report queue. Default: 30 reports. + - **Windows**: forwards to HidD_SetNumInputBuffers(), + which resizes the kernel HID ring buffer. The kernel + accepts values in the range [2, 512]; requests outside + this range return -1. Default: 64 reports. + - **Linux hidraw** and **NetBSD uhid**: the call is + accepted (returns 0) and validated against + HID_API_MAX_NUM_INPUT_BUFFERS, but has no effect (no-op) — + these kernels manage the input report buffer internally + and expose no userspace resize. @ingroup API @param dev A device handle returned from hid_open(). diff --git a/hidtest/test.c b/hidtest/test.c index eb01d88e0..03829fa58 100644 --- a/hidtest/test.c +++ b/hidtest/test.c @@ -144,6 +144,9 @@ int main(int argc, char* argv[]) (void)&hid_get_input_report; #if HID_API_VERSION >= HID_API_MAKE_VERSION(0, 15, 0) (void)&hid_send_output_report; +#endif +#if HID_API_VERSION >= HID_API_MAKE_VERSION(0, 16, 0) + (void)&hid_set_num_input_buffers; #endif (void)&hid_get_feature_report; (void)&hid_send_feature_report; @@ -198,6 +201,13 @@ int main(int argc, char* argv[]) return 1; } +#if HID_API_VERSION >= HID_API_MAKE_VERSION(0, 16, 0) + res = hid_set_num_input_buffers(handle, 500); + if (res < 0) { + printf("Unable to set input buffers: %ls\n", hid_error(handle)); + } +#endif + #if defined(_WIN32) && HID_API_VERSION >= HID_API_MAKE_VERSION(0, 15, 0) hid_winapi_set_write_timeout(handle, 5000); #endif diff --git a/libusb/hid.c b/libusb/hid.c index 5e7bf83a7..6257b4a58 100644 --- a/libusb/hid.c +++ b/libusb/hid.c @@ -1581,10 +1581,6 @@ 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); diff --git a/linux/hid.c b/linux/hid.c index dea5c16be..4d871ac34 100644 --- a/linux/hid.c +++ b/linux/hid.c @@ -1206,10 +1206,6 @@ int HID_API_EXPORT hid_set_num_input_buffers(hid_device *dev, int num_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; } diff --git a/netbsd/hid.c b/netbsd/hid.c index 9f2b5e223..314dba567 100644 --- a/netbsd/hid.c +++ b/netbsd/hid.c @@ -962,10 +962,6 @@ int HID_API_EXPORT HID_API_CALL hid_set_num_input_buffers(hid_device *dev, int n 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; } From f786335a4a0352e5146952e0c08e0a6207342ae8 Mon Sep 17 00:00:00 2001 From: auxcorelabs Date: Wed, 22 Apr 2026 15:57:59 +0000 Subject: [PATCH 5/5] Address review: helper consistency, overridable cap, ring buffer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per-backend helper consistency: * linux/hid.c, netbsd/hid.c, mac/hid.c: setter uses register_device_error() instead of register_error_str() directly. Build-time override: * hidapi/hidapi.h: wrap HID_API_MAX_NUM_INPUT_BUFFERS in #ifndef so downstreams can set the cap via -DHID_API_MAX_NUM_INPUT_BUFFERS=. Ring buffer input queue: * New static-in-header helper hidapi_input_ring_*, present in libusb/ and mac/ as byte-identical copies. * libusb/hid.c, mac/hid.c: replace struct input_report * linked list with fixed-size ring. Enqueue is O(1); eviction is inline in push; the setter shrinks via drop_oldest so dev->num_input_buffers is the exact steady-state cap. * ABI unchanged (hid_device is opaque in hidapi.h). * Allocation failure in the read callback is now handled — the previous code had an unchecked malloc() that would segfault. libusb has no active error channel so the drop is silent there; mac calls register_device_error. --- hidapi/hidapi.h | 5 + libusb/hid.c | 108 ++++++++----------- libusb/hidapi_input_ring.h | 213 +++++++++++++++++++++++++++++++++++++ linux/hid.c | 2 +- mac/hid.c | 120 ++++++++------------- mac/hidapi_input_ring.h | 213 +++++++++++++++++++++++++++++++++++++ netbsd/hid.c | 2 +- 7 files changed, 520 insertions(+), 143 deletions(-) create mode 100644 libusb/hidapi_input_ring.h create mode 100644 mac/hidapi_input_ring.h diff --git a/hidapi/hidapi.h b/hidapi/hidapi.h index 195e5757e..5d466a2b7 100644 --- a/hidapi/hidapi.h +++ b/hidapi/hidapi.h @@ -428,8 +428,13 @@ extern "C" { Values passed above this limit are rejected by hid_set_num_input_buffers(). Guards against memory-exhaustion via unbounded input report queue growth. + + May be overridden at build time via + -DHID_API_MAX_NUM_INPUT_BUFFERS=. */ + #ifndef HID_API_MAX_NUM_INPUT_BUFFERS #define HID_API_MAX_NUM_INPUT_BUFFERS 1024 + #endif /** @brief Set the number of input report buffers queued per device. diff --git a/libusb/hid.c b/libusb/hid.c index 6257b4a58..a7d57fad4 100644 --- a/libusb/hid.c +++ b/libusb/hid.c @@ -49,6 +49,7 @@ #endif #include "hidapi_libusb.h" +#include "hidapi_input_ring.h" #ifndef HIDAPI_THREAD_MODEL_INCLUDE #define HIDAPI_THREAD_MODEL_INCLUDE "hidapi_thread_pthread.h" @@ -77,13 +78,6 @@ libusb HIDAPI programs are encouraged to use the interface number instead to differentiate between interfaces on a composite HID device. */ /*#define INVASIVE_GET_USAGE*/ -/* Linked List of input reports received from the device. */ -struct input_report { - uint8_t *data; - size_t len; - struct input_report *next; -}; - struct hid_device_ { /* Handle to the actual device. */ @@ -119,8 +113,10 @@ struct hid_device_ { int transfer_loop_finished; struct libusb_transfer *transfer; - /* List of received input reports. */ - struct input_report *input_reports; + /* Input report ring buffer. Backing array sized at + HID_API_MAX_NUM_INPUT_BUFFERS at device open; logical cap + (drop-oldest threshold) is dev->num_input_buffers. */ + struct hidapi_input_ring input_ring; /* Was kernel driver detached by libusb */ #ifdef DETACH_KERNEL_DRIVER @@ -137,7 +133,6 @@ static struct hid_api_version api_version = { static libusb_context *usb_context = NULL; uint16_t get_usb_code_for_current_locale(void); -static int return_data(hid_device *dev, unsigned char *data, size_t length); static hid_device *new_hid_device(void) { @@ -145,6 +140,11 @@ static hid_device *new_hid_device(void) if (!dev) return NULL; + if (hidapi_input_ring_init(&dev->input_ring, HID_API_MAX_NUM_INPUT_BUFFERS) != 0) { + free(dev); + return NULL; + } + dev->blocking = 1; dev->num_input_buffers = 30; @@ -160,6 +160,9 @@ static void free_hid_device(hid_device *dev) hid_free_enumeration(dev->device_info); + /* Free any queued input reports and the ring backing array. */ + hidapi_input_ring_destroy(&dev->input_ring); + /* Free the device itself */ free(dev); } @@ -962,37 +965,18 @@ static void LIBUSB_CALL read_callback(struct libusb_transfer *transfer) if (transfer->status == LIBUSB_TRANSFER_COMPLETED) { - struct input_report *rpt = (struct input_report*) malloc(sizeof(*rpt)); - rpt->data = (uint8_t*) malloc(transfer->actual_length); - memcpy(rpt->data, transfer->buffer, transfer->actual_length); - rpt->len = transfer->actual_length; - rpt->next = NULL; - hidapi_thread_mutex_lock(&dev->thread_state); - /* Attach the new report object to the end of the list. */ - if (dev->input_reports == NULL) { - /* The list is empty. Put it at the root. */ - dev->input_reports = rpt; + int push_rc = hidapi_input_ring_push(&dev->input_ring, + dev->num_input_buffers, + transfer->buffer, + (size_t)transfer->actual_length); + if (push_rc == 0) { hidapi_thread_cond_signal(&dev->thread_state); } - else { - /* Find the end of the list and attach. */ - struct input_report *cur = dev->input_reports; - int num_queued = 0; - while (cur->next != NULL) { - cur = cur->next; - num_queued++; - } - cur->next = rpt; + /* Allocation failed; libusb has no active error channel here, so the + * report is dropped silently. */ - /* 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 > dev->num_input_buffers) { - return_data(dev, NULL, 0); - } - } hidapi_thread_mutex_unlock(&dev->thread_state); } else if (transfer->status == LIBUSB_TRANSFER_CANCELLED) { @@ -1458,19 +1442,18 @@ int HID_API_EXPORT hid_write(hid_device *dev, const unsigned char *data, size_t return actual_length; } -/* Helper function, to simplify hid_read(). - This should be called with dev->mutex locked. */ -static int return_data(hid_device *dev, unsigned char *data, size_t length) +/* Pop one report from the ring into (data, length). Caller must + hold dev->thread_state. Returns bytes copied, or -1 if empty. */ +static int ring_pop_into(hid_device *dev, unsigned char *data, size_t length) { - /* Copy the data out of the linked list item (rpt) into the - return buffer (data), and delete the liked list item. */ - struct input_report *rpt = dev->input_reports; - size_t len = (length < rpt->len)? length: rpt->len; - if (len > 0) - memcpy(data, rpt->data, len); - dev->input_reports = rpt->next; - free(rpt->data); - free(rpt); + uint8_t *rpt_data; + size_t rpt_len; + if (hidapi_input_ring_pop(&dev->input_ring, &rpt_data, &rpt_len) != 0) + return -1; + size_t len = (length < rpt_len) ? length : rpt_len; + if (len > 0 && data) + memcpy(data, rpt_data, len); + free(rpt_data); return (int)len; } @@ -1499,9 +1482,8 @@ int HID_API_EXPORT hid_read_timeout(hid_device *dev, unsigned char *data, size_t bytes_read = -1; /* There's an input report queued up. Return it. */ - if (dev->input_reports) { - /* Return the first one */ - bytes_read = return_data(dev, data, length); + if (dev->input_ring.count > 0) { + bytes_read = ring_pop_into(dev, data, length); goto ret; } @@ -1514,11 +1496,11 @@ int HID_API_EXPORT hid_read_timeout(hid_device *dev, unsigned char *data, size_t if (milliseconds == -1) { /* Blocking */ - while (!dev->input_reports && !dev->shutdown_thread) { + while (dev->input_ring.count == 0 && !dev->shutdown_thread) { hidapi_thread_cond_wait(&dev->thread_state); } - if (dev->input_reports) { - bytes_read = return_data(dev, data, length); + if (dev->input_ring.count > 0) { + bytes_read = ring_pop_into(dev, data, length); } } else if (milliseconds > 0) { @@ -1528,11 +1510,11 @@ int HID_API_EXPORT hid_read_timeout(hid_device *dev, unsigned char *data, size_t hidapi_thread_gettime(&ts); hidapi_thread_addtime(&ts, milliseconds); - while (!dev->input_reports && !dev->shutdown_thread) { + while (dev->input_ring.count == 0 && !dev->shutdown_thread) { res = hidapi_thread_cond_timedwait(&dev->thread_state, &ts); if (res == 0) { - if (dev->input_reports) { - bytes_read = return_data(dev, data, length); + if (dev->input_ring.count > 0) { + bytes_read = ring_pop_into(dev, data, length); break; } @@ -1585,6 +1567,10 @@ int HID_API_EXPORT hid_set_num_input_buffers(hid_device *dev, int num_buffers) return -1; hidapi_thread_mutex_lock(&dev->thread_state); dev->num_input_buffers = num_buffers; + if (dev->input_ring.count > num_buffers) { + hidapi_input_ring_drop_oldest(&dev->input_ring, + dev->input_ring.count - num_buffers); + } hidapi_thread_mutex_unlock(&dev->thread_state); return 0; } @@ -1749,12 +1735,8 @@ void HID_API_EXPORT hid_close(hid_device *dev) /* Close the handle */ libusb_close(dev->device_handle); - /* Clear out the queue of received reports. */ - hidapi_thread_mutex_lock(&dev->thread_state); - while (dev->input_reports) { - return_data(dev, NULL, 0); - } - hidapi_thread_mutex_unlock(&dev->thread_state); + /* Queued reports are freed inside free_hid_device via + hidapi_input_ring_destroy. */ free_hid_device(dev); } diff --git a/libusb/hidapi_input_ring.h b/libusb/hidapi_input_ring.h new file mode 100644 index 000000000..59d8b109b --- /dev/null +++ b/libusb/hidapi_input_ring.h @@ -0,0 +1,213 @@ +/******************************************************* + HIDAPI - Multi-Platform library for + communication with HID devices. + + Internal ring buffer — a bounded FIFO of variable-length byte + buffers, with drop-oldest-when-full semantics. Used by the mac + (IOKit) and libusb hidapi backends to queue input reports produced + asynchronously by the device's delivery path, until the caller + drains them via hid_read() / hid_read_timeout(). + + All helpers are defined as `static` so each translation unit that + includes this header gets its own private copy — no symbols are + exported from the shared library. + + ---- Lifecycle ---- + hidapi_input_ring_init(r, CAPACITY) is called once at ring-owner open + (for input-report use: at hid_open(), with CAPACITY = + HID_API_MAX_NUM_INPUT_BUFFERS). The backing array is sized to the + absolute maximum capacity and never reallocated for the ring's + lifetime — the only runtime knob is the LOGICAL cap (drop-oldest + threshold) passed into hidapi_input_ring_push() per call. For input + reports the logical cap lives in dev->num_input_buffers. + + Preconditions for init(): `r->slots` must be NULL (never-initialized + or previously-destroyed state). Callers typically embed `struct + hidapi_input_ring` inside a struct allocated by `calloc`, which + zeros `r->slots` and satisfies this automatically. Other fields + may hold garbage — init overwrites them. + + ---- Concurrency ---- + EVERY helper here requires the caller to hold a mutex protecting + the ring. The helpers do not lock internally. For input-report + rings that mutex is the device's queue mutex (pthread_mutex_t on + mac, hidapi_thread_state on libusb); for other uses, the caller + chooses. + + ---- Ownership ---- + Each slot owns its data allocation (malloc'd in push, freed on + evict / pop / destroy). Zero-length reports are stored with + .data == NULL and .len == 0 — no allocation occurs. + *******************************************************/ + +#ifndef HIDAPI_INPUT_RING_H_ +#define HIDAPI_INPUT_RING_H_ + +#include +#include +#include +#include + +struct hidapi_input_ring_slot { + uint8_t *data; + size_t len; +}; + +struct hidapi_input_ring { + struct hidapi_input_ring_slot *slots; + int capacity; /* allocated slot count; fixed after init */ + int head; /* oldest report index (dequeue side) */ + int tail; /* next free slot index (enqueue side) */ + int count; /* valid reports currently queued */ + uint64_t dropped; /* reports dropped by queue policy */ + /* (cap evictions + shrinking setter). */ + /* Does not count ENOMEM. */ +}; + +/* PRECONDITION: r must be in the never-initialized or destroyed + * state (r->slots == NULL). Other fields may hold + * garbage — init overwrites them. Re-init on an + * already-initialized ring is rejected to prevent + * leaking the previous allocation. + * POSTCONDITION on success: r is empty with `capacity` slots allocated. + * Returns 0 on success, -1 on invalid arg, double-init, or ENOMEM. */ +static int hidapi_input_ring_init(struct hidapi_input_ring *r, int capacity) +{ + if (!r || capacity < 1) + return -1; + if (r->slots) /* double-init guard — prevents leak */ + return -1; + r->slots = (struct hidapi_input_ring_slot *) + calloc((size_t)capacity, sizeof(struct hidapi_input_ring_slot)); + if (!r->slots) + return -1; + r->capacity = capacity; + r->head = 0; + r->tail = 0; + r->count = 0; + r->dropped = 0; + return 0; +} + +/* PRECONDITION: caller holds the device's queue mutex, OR is tearing + * down the device such that no other thread can reach r. + * POSTCONDITION: r is zeroed. All queued report data is freed. + * + * Safe to call on a zero-initialized or previously-destroyed ring. + * Not safe on uninitialized stack memory — callers must zero-init + * the struct before first use (see init() precondition). */ +static void hidapi_input_ring_destroy(struct hidapi_input_ring *r) +{ + if (!r) return; + if (r->slots) { + int idx = r->head; + for (int i = 0; i < r->count; i++) { + free(r->slots[idx].data); + r->slots[idx].data = NULL; + r->slots[idx].len = 0; + idx = (idx + 1) % r->capacity; + } + free(r->slots); + } + r->slots = NULL; + r->capacity = 0; + r->count = 0; + r->head = 0; + r->tail = 0; + r->dropped = 0; +} + +/* Enqueue a copy of [data, data+len). Evicts oldest reports while + * r->count >= logical_cap, incrementing r->dropped per evicted report. + * For len == 0, stores (NULL, 0) — no allocation. + * + * PRECONDITION: caller holds the mutex protecting r. + * 1 <= logical_cap <= r->capacity. + * If len > 0, data must not be NULL. + * + * Returns 0 on success, -1 on invalid args or ENOMEM. On ENOMEM the + * ring is unchanged and dropped is NOT incremented. */ +static int hidapi_input_ring_push(struct hidapi_input_ring *r, int logical_cap, + const uint8_t *data, size_t len) +{ + if (!r || !r->slots || logical_cap < 1 || logical_cap > r->capacity || + (len > 0 && !data)) /* NULL-data guard (UB in memcpy otherwise) */ + return -1; + + /* Allocate BEFORE touching ring state so ENOMEM leaves r unchanged. + * For zero-length reports we skip allocation; free(NULL) is safe + * and pop() distinguishes via the slot's .len field. */ + uint8_t *copy = NULL; + if (len > 0) { + copy = (uint8_t *)malloc(len); + if (!copy) + return -1; /* ENOMEM — ring unchanged, dropped NOT incremented */ + memcpy(copy, data, len); + } + + /* Evict oldest while at or over the logical cap. Terminates because + * logical_cap >= 1 (precondition) and count decreases each iter. */ + while (r->count >= logical_cap) { + free(r->slots[r->head].data); + r->slots[r->head].data = NULL; + r->slots[r->head].len = 0; + r->head = (r->head + 1) % r->capacity; + r->count--; + r->dropped++; + } + + r->slots[r->tail].data = copy; + r->slots[r->tail].len = len; + r->tail = (r->tail + 1) % r->capacity; + r->count++; + return 0; +} + +/* Remove the oldest report into (*out_data, *out_len). + * Caller owns the returned *out_data and must free() it + * (free(NULL) is safe and expected for zero-length reports). + * + * PRECONDITION: caller holds the mutex protecting r. + * + * Returns 0 on success, -1 if empty or on invalid args. */ +static int hidapi_input_ring_pop(struct hidapi_input_ring *r, + uint8_t **out_data, size_t *out_len) +{ + if (!r || !r->slots || !out_data || !out_len) + return -1; + if (r->count == 0) + return -1; + *out_data = r->slots[r->head].data; + *out_len = r->slots[r->head].len; + r->slots[r->head].data = NULL; + r->slots[r->head].len = 0; + r->head = (r->head + 1) % r->capacity; + r->count--; + return 0; +} + +/* Drop the N oldest reports. Used by hid_set_num_input_buffers() when + * shrinking the logical cap. Increments r->dropped by N. + * + * PRECONDITION: caller holds the mutex protecting r. + * 0 <= n <= r->count. + * + * Invalid n (out of range) is silently ignored — matches the project + * convention of not using runtime asserts in backend code. Callers + * must meet the precondition; violations are caller bugs. */ +static void hidapi_input_ring_drop_oldest(struct hidapi_input_ring *r, int n) +{ + if (!r || !r->slots || n < 0 || n > r->count) + return; + + for (int i = 0; i < n; i++) { + free(r->slots[r->head].data); + r->slots[r->head].data = NULL; + r->slots[r->head].len = 0; + r->head = (r->head + 1) % r->capacity; + r->count--; + r->dropped++; + } +} + +#endif /* HIDAPI_INPUT_RING_H_ */ diff --git a/linux/hid.c b/linux/hid.c index 4d871ac34..6ead604af 100644 --- a/linux/hid.c +++ b/linux/hid.c @@ -1203,7 +1203,7 @@ 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) { if (num_buffers <= 0 || num_buffers > HID_API_MAX_NUM_INPUT_BUFFERS) { - register_error_str(&dev->last_error_str, "num_buffers out of range"); + register_device_error(dev, "num_buffers out of range"); return -1; } (void)num_buffers; diff --git a/mac/hid.c b/mac/hid.c index d9a9bb21e..4fdeff62b 100644 --- a/mac/hid.c +++ b/mac/hid.c @@ -37,6 +37,7 @@ #include #include "hidapi_darwin.h" +#include "hidapi_input_ring.h" /* Barrier implementation because Mac OSX doesn't have pthread_barrier. It also doesn't have clock_gettime(). So much for POSIX and SUSv2. @@ -100,14 +101,8 @@ static int pthread_barrier_wait(pthread_barrier_t *barrier) } } -static int return_data(hid_device *dev, unsigned char *data, size_t length); /* Linked List of input reports received from the device. */ -struct input_report { - uint8_t *data; - size_t len; - struct input_report *next; -}; static struct hid_api_version api_version = { .major = HID_API_VERSION_MAJOR, @@ -133,11 +128,11 @@ struct hid_device_ { CFRunLoopSourceRef source; uint8_t *input_report_buf; CFIndex max_input_report_len; - struct input_report *input_reports; + struct hidapi_input_ring input_ring; struct hid_device_info* device_info; pthread_t thread; - pthread_mutex_t mutex; /* Protects input_reports */ + pthread_mutex_t mutex; /* Protects input_ring */ pthread_cond_t condition; pthread_barrier_t barrier; /* Ensures correct startup sequence */ pthread_barrier_t shutdown_barrier; /* Ensures correct shutdown sequence */ @@ -153,6 +148,11 @@ static hid_device *new_hid_device(void) return NULL; } + if (hidapi_input_ring_init(&dev->input_ring, HID_API_MAX_NUM_INPUT_BUFFERS) != 0) { + free(dev); + return NULL; + } + dev->device_handle = NULL; dev->open_options = device_open_options; dev->blocking = 1; @@ -162,7 +162,6 @@ static hid_device *new_hid_device(void) dev->run_loop = NULL; dev->source = NULL; dev->input_report_buf = NULL; - dev->input_reports = NULL; dev->device_info = NULL; dev->shutdown_thread = 0; dev->last_error_str = NULL; @@ -182,14 +181,8 @@ static void free_hid_device(hid_device *dev) if (!dev) return; - /* Delete any input reports still left over. */ - struct input_report *rpt = dev->input_reports; - while (rpt) { - struct input_report *next = rpt->next; - free(rpt->data); - free(rpt); - rpt = next; - } + /* Free any queued input reports and the ring backing array. */ + hidapi_input_ring_destroy(&dev->input_ring); /* Free the string and the report buffer. The check for NULL is necessary here as CFRelease() doesn't handle NULL like @@ -879,48 +872,21 @@ static void hid_report_callback(void *context, IOReturn result, void *sender, (void) report_type; (void) report_id; - struct input_report *rpt; hid_device *dev = (hid_device*) context; - /* Make a new Input Report object */ - rpt = (struct input_report*) calloc(1, sizeof(struct input_report)); - rpt->data = (uint8_t*) calloc(1, report_length); - memcpy(rpt->data, report, report_length); - rpt->len = report_length; - rpt->next = NULL; - - /* Lock this section */ pthread_mutex_lock(&dev->mutex); - /* Attach the new report object to the end of the list. */ - if (dev->input_reports == NULL) { - /* The list is empty. Put it at the root. */ - dev->input_reports = rpt; - } - else { - /* Find the end of the list and attach. */ - struct input_report *cur = dev->input_reports; - int num_queued = 0; - while (cur->next != NULL) { - cur = cur->next; - num_queued++; - } - cur->next = rpt; - - /* 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 > dev->num_input_buffers) { - return_data(dev, NULL, 0); - } + int push_rc = hidapi_input_ring_push(&dev->input_ring, + dev->num_input_buffers, + report, + (size_t)report_length); + if (push_rc == 0) { + pthread_cond_signal(&dev->condition); + } else { + register_device_error(dev, "input queue allocation failed"); } - /* Signal a waiting thread that there is data. */ - pthread_cond_signal(&dev->condition); - - /* Unlock */ pthread_mutex_unlock(&dev->mutex); - } /* This gets called when the read_thread's run loop gets signaled by @@ -1193,25 +1159,24 @@ int HID_API_EXPORT hid_write(hid_device *dev, const unsigned char *data, size_t return set_report(dev, kIOHIDReportTypeOutput, data, length); } -/* Helper function, so that this isn't duplicated in hid_read(). */ -static int return_data(hid_device *dev, unsigned char *data, size_t length) +/* Pop one report from the ring into (data, length). Caller must + hold dev->mutex. Returns bytes copied, or -1 if empty. */ +static int ring_pop_into(hid_device *dev, unsigned char *data, size_t length) { - /* Copy the data out of the linked list item (rpt) into the - return buffer (data), and delete the liked list item. */ - struct input_report *rpt = dev->input_reports; - size_t len = (length < rpt->len)? length: rpt->len; - if (data != NULL) { - memcpy(data, rpt->data, len); - } - dev->input_reports = rpt->next; - free(rpt->data); - free(rpt); + uint8_t *rpt_data; + size_t rpt_len; + if (hidapi_input_ring_pop(&dev->input_ring, &rpt_data, &rpt_len) != 0) + return -1; + size_t len = (length < rpt_len) ? length : rpt_len; + if (len > 0 && data != NULL) + memcpy(data, rpt_data, len); + free(rpt_data); return (int) len; } static int cond_wait(hid_device *dev, pthread_cond_t *cond, pthread_mutex_t *mutex) { - while (!dev->input_reports) { + while (dev->input_ring.count == 0) { int res = pthread_cond_wait(cond, mutex); if (res != 0) return res; @@ -1232,7 +1197,7 @@ static int cond_wait(hid_device *dev, pthread_cond_t *cond, pthread_mutex_t *mut static int cond_timedwait(hid_device *dev, pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime) { - while (!dev->input_reports) { + while (dev->input_ring.count == 0) { int res = pthread_cond_timedwait(cond, mutex, abstime); if (res != 0) return res; @@ -1266,9 +1231,8 @@ int HID_API_EXPORT hid_read_timeout(hid_device *dev, unsigned char *data, size_t pthread_mutex_lock(&dev->mutex); /* There's an input report queued up. Return it. */ - if (dev->input_reports) { - /* Return the first one */ - bytes_read = return_data(dev, data, length); + if (dev->input_ring.count > 0) { + bytes_read = ring_pop_into(dev, data, length); goto ret; } @@ -1295,7 +1259,7 @@ int HID_API_EXPORT hid_read_timeout(hid_device *dev, unsigned char *data, size_t int res; res = cond_wait(dev, &dev->condition, &dev->mutex); if (res == 0) - bytes_read = return_data(dev, data, length); + bytes_read = ring_pop_into(dev, data, length); else { /* There was an error, or a device disconnection. */ register_error_str(&dev->last_read_error_str, "hid_read_timeout: error waiting for more data"); @@ -1318,7 +1282,7 @@ int HID_API_EXPORT hid_read_timeout(hid_device *dev, unsigned char *data, size_t res = cond_timedwait(dev, &dev->condition, &dev->mutex, &ts); if (res == 0) { - bytes_read = return_data(dev, data, length); + bytes_read = ring_pop_into(dev, data, length); } else if (res == ETIMEDOUT) { bytes_read = 0; } else { @@ -1353,11 +1317,15 @@ 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) { if (num_buffers <= 0 || num_buffers > HID_API_MAX_NUM_INPUT_BUFFERS) { - register_error_str(&dev->last_error_str, "num_buffers out of range"); + register_device_error(dev, "num_buffers out of range"); return -1; } pthread_mutex_lock(&dev->mutex); dev->num_input_buffers = num_buffers; + if (dev->input_ring.count > num_buffers) { + hidapi_input_ring_drop_oldest(&dev->input_ring, + dev->input_ring.count - num_buffers); + } pthread_mutex_unlock(&dev->mutex); return 0; } @@ -1433,12 +1401,8 @@ void HID_API_EXPORT hid_close(hid_device *dev) IOHIDDeviceClose(dev->device_handle, dev->open_options); } - /* Clear out the queue of received reports. */ - pthread_mutex_lock(&dev->mutex); - while (dev->input_reports) { - return_data(dev, NULL, 0); - } - pthread_mutex_unlock(&dev->mutex); + /* Queued reports are freed inside free_hid_device via + hidapi_input_ring_destroy. */ CFRelease(dev->device_handle); free_hid_device(dev); diff --git a/mac/hidapi_input_ring.h b/mac/hidapi_input_ring.h new file mode 100644 index 000000000..59d8b109b --- /dev/null +++ b/mac/hidapi_input_ring.h @@ -0,0 +1,213 @@ +/******************************************************* + HIDAPI - Multi-Platform library for + communication with HID devices. + + Internal ring buffer — a bounded FIFO of variable-length byte + buffers, with drop-oldest-when-full semantics. Used by the mac + (IOKit) and libusb hidapi backends to queue input reports produced + asynchronously by the device's delivery path, until the caller + drains them via hid_read() / hid_read_timeout(). + + All helpers are defined as `static` so each translation unit that + includes this header gets its own private copy — no symbols are + exported from the shared library. + + ---- Lifecycle ---- + hidapi_input_ring_init(r, CAPACITY) is called once at ring-owner open + (for input-report use: at hid_open(), with CAPACITY = + HID_API_MAX_NUM_INPUT_BUFFERS). The backing array is sized to the + absolute maximum capacity and never reallocated for the ring's + lifetime — the only runtime knob is the LOGICAL cap (drop-oldest + threshold) passed into hidapi_input_ring_push() per call. For input + reports the logical cap lives in dev->num_input_buffers. + + Preconditions for init(): `r->slots` must be NULL (never-initialized + or previously-destroyed state). Callers typically embed `struct + hidapi_input_ring` inside a struct allocated by `calloc`, which + zeros `r->slots` and satisfies this automatically. Other fields + may hold garbage — init overwrites them. + + ---- Concurrency ---- + EVERY helper here requires the caller to hold a mutex protecting + the ring. The helpers do not lock internally. For input-report + rings that mutex is the device's queue mutex (pthread_mutex_t on + mac, hidapi_thread_state on libusb); for other uses, the caller + chooses. + + ---- Ownership ---- + Each slot owns its data allocation (malloc'd in push, freed on + evict / pop / destroy). Zero-length reports are stored with + .data == NULL and .len == 0 — no allocation occurs. + *******************************************************/ + +#ifndef HIDAPI_INPUT_RING_H_ +#define HIDAPI_INPUT_RING_H_ + +#include +#include +#include +#include + +struct hidapi_input_ring_slot { + uint8_t *data; + size_t len; +}; + +struct hidapi_input_ring { + struct hidapi_input_ring_slot *slots; + int capacity; /* allocated slot count; fixed after init */ + int head; /* oldest report index (dequeue side) */ + int tail; /* next free slot index (enqueue side) */ + int count; /* valid reports currently queued */ + uint64_t dropped; /* reports dropped by queue policy */ + /* (cap evictions + shrinking setter). */ + /* Does not count ENOMEM. */ +}; + +/* PRECONDITION: r must be in the never-initialized or destroyed + * state (r->slots == NULL). Other fields may hold + * garbage — init overwrites them. Re-init on an + * already-initialized ring is rejected to prevent + * leaking the previous allocation. + * POSTCONDITION on success: r is empty with `capacity` slots allocated. + * Returns 0 on success, -1 on invalid arg, double-init, or ENOMEM. */ +static int hidapi_input_ring_init(struct hidapi_input_ring *r, int capacity) +{ + if (!r || capacity < 1) + return -1; + if (r->slots) /* double-init guard — prevents leak */ + return -1; + r->slots = (struct hidapi_input_ring_slot *) + calloc((size_t)capacity, sizeof(struct hidapi_input_ring_slot)); + if (!r->slots) + return -1; + r->capacity = capacity; + r->head = 0; + r->tail = 0; + r->count = 0; + r->dropped = 0; + return 0; +} + +/* PRECONDITION: caller holds the device's queue mutex, OR is tearing + * down the device such that no other thread can reach r. + * POSTCONDITION: r is zeroed. All queued report data is freed. + * + * Safe to call on a zero-initialized or previously-destroyed ring. + * Not safe on uninitialized stack memory — callers must zero-init + * the struct before first use (see init() precondition). */ +static void hidapi_input_ring_destroy(struct hidapi_input_ring *r) +{ + if (!r) return; + if (r->slots) { + int idx = r->head; + for (int i = 0; i < r->count; i++) { + free(r->slots[idx].data); + r->slots[idx].data = NULL; + r->slots[idx].len = 0; + idx = (idx + 1) % r->capacity; + } + free(r->slots); + } + r->slots = NULL; + r->capacity = 0; + r->count = 0; + r->head = 0; + r->tail = 0; + r->dropped = 0; +} + +/* Enqueue a copy of [data, data+len). Evicts oldest reports while + * r->count >= logical_cap, incrementing r->dropped per evicted report. + * For len == 0, stores (NULL, 0) — no allocation. + * + * PRECONDITION: caller holds the mutex protecting r. + * 1 <= logical_cap <= r->capacity. + * If len > 0, data must not be NULL. + * + * Returns 0 on success, -1 on invalid args or ENOMEM. On ENOMEM the + * ring is unchanged and dropped is NOT incremented. */ +static int hidapi_input_ring_push(struct hidapi_input_ring *r, int logical_cap, + const uint8_t *data, size_t len) +{ + if (!r || !r->slots || logical_cap < 1 || logical_cap > r->capacity || + (len > 0 && !data)) /* NULL-data guard (UB in memcpy otherwise) */ + return -1; + + /* Allocate BEFORE touching ring state so ENOMEM leaves r unchanged. + * For zero-length reports we skip allocation; free(NULL) is safe + * and pop() distinguishes via the slot's .len field. */ + uint8_t *copy = NULL; + if (len > 0) { + copy = (uint8_t *)malloc(len); + if (!copy) + return -1; /* ENOMEM — ring unchanged, dropped NOT incremented */ + memcpy(copy, data, len); + } + + /* Evict oldest while at or over the logical cap. Terminates because + * logical_cap >= 1 (precondition) and count decreases each iter. */ + while (r->count >= logical_cap) { + free(r->slots[r->head].data); + r->slots[r->head].data = NULL; + r->slots[r->head].len = 0; + r->head = (r->head + 1) % r->capacity; + r->count--; + r->dropped++; + } + + r->slots[r->tail].data = copy; + r->slots[r->tail].len = len; + r->tail = (r->tail + 1) % r->capacity; + r->count++; + return 0; +} + +/* Remove the oldest report into (*out_data, *out_len). + * Caller owns the returned *out_data and must free() it + * (free(NULL) is safe and expected for zero-length reports). + * + * PRECONDITION: caller holds the mutex protecting r. + * + * Returns 0 on success, -1 if empty or on invalid args. */ +static int hidapi_input_ring_pop(struct hidapi_input_ring *r, + uint8_t **out_data, size_t *out_len) +{ + if (!r || !r->slots || !out_data || !out_len) + return -1; + if (r->count == 0) + return -1; + *out_data = r->slots[r->head].data; + *out_len = r->slots[r->head].len; + r->slots[r->head].data = NULL; + r->slots[r->head].len = 0; + r->head = (r->head + 1) % r->capacity; + r->count--; + return 0; +} + +/* Drop the N oldest reports. Used by hid_set_num_input_buffers() when + * shrinking the logical cap. Increments r->dropped by N. + * + * PRECONDITION: caller holds the mutex protecting r. + * 0 <= n <= r->count. + * + * Invalid n (out of range) is silently ignored — matches the project + * convention of not using runtime asserts in backend code. Callers + * must meet the precondition; violations are caller bugs. */ +static void hidapi_input_ring_drop_oldest(struct hidapi_input_ring *r, int n) +{ + if (!r || !r->slots || n < 0 || n > r->count) + return; + + for (int i = 0; i < n; i++) { + free(r->slots[r->head].data); + r->slots[r->head].data = NULL; + r->slots[r->head].len = 0; + r->head = (r->head + 1) % r->capacity; + r->count--; + r->dropped++; + } +} + +#endif /* HIDAPI_INPUT_RING_H_ */ diff --git a/netbsd/hid.c b/netbsd/hid.c index 314dba567..a666217e5 100644 --- a/netbsd/hid.c +++ b/netbsd/hid.c @@ -959,7 +959,7 @@ HID_API_EXPORT const wchar_t* HID_API_CALL hid_read_error(hid_device *dev) 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"); + register_device_error(dev, "num_buffers out of range"); return -1; } (void)num_buffers;