From 2884690d6a5ef2dbac3029748b502615f9cf61e2 Mon Sep 17 00:00:00 2001 From: auxcorelabs Date: Fri, 17 Apr 2026 06:43:43 +0000 Subject: [PATCH 1/6] 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 cbc3107d..c3c2502d 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 d2ceef5d..8fc097a3 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 a4dc26f4..88052609 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 a91bc190..823624f9 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 a9fca67c..9fabef19 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 1e27f10a..39dc90da 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/6] 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 8fc097a3..b42f558e 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 88052609..549247be 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 823624f9..7002a7a3 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 9fabef19..b970e724 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 39dc90da..c96e2a30 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/6] 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 c3c2502d..c14ada29 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 b42f558e..5e7bf83a 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 549247be..dea5c16b 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 7002a7a3..d9a9bb21 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 b970e724..9f2b5e22 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 c96e2a30..a628901e 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/6] 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 c14ada29..195e5757 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 eb01d88e..03829fa5 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 5e7bf83a..6257b4a5 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 dea5c16b..4d871ac3 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 9f2b5e22..314dba56 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/6] 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 195e5757..5d466a2b 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 6257b4a5..a7d57fad 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 00000000..59d8b109 --- /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 4d871ac3..6ead604a 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 d9a9bb21..4fdeff62 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 00000000..59d8b109 --- /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 314dba56..a666217e 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; From c0fe4ebc37fb4f357cb412f806439e98e398c440 Mon Sep 17 00:00:00 2001 From: auxcorelabs Date: Sat, 25 Apr 2026 11:38:44 +0000 Subject: [PATCH 6/6] Address review: flat pre-alloc input report ring + runtime slot size MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Switch ring storage from pointer-slot (per-push malloc) to a single flat buffer of (dev->num_input_buffers × slot_size), allocated at device open. slot_size: kIOHIDMaxInputReportSizeKey on macOS, interrupt-IN wMaxPacketSize on libusb, 64 B fallback. Resize re-allocs and memcpys survivors FIFO-order. Pop becomes copy-out (pop_into). Ring header consolidated into core/. Adds tests/test_hidapi_input_ring.c — 21 ASAN tests. --- CMakeLists.txt | 5 + core/hidapi_input_ring.h | 254 ++++++++++ libusb/hid.c | 55 ++- libusb/hidapi_input_ring.h | 213 -------- mac/hid.c | 39 +- mac/hidapi_input_ring.h | 213 -------- subprojects/hidapi_build_cmake/CMakeLists.txt | 2 +- tests/CMakeLists.txt | 5 + tests/test_hidapi_input_ring.c | 458 ++++++++++++++++++ 9 files changed, 772 insertions(+), 472 deletions(-) create mode 100644 core/hidapi_input_ring.h delete mode 100644 libusb/hidapi_input_ring.h delete mode 100644 mac/hidapi_input_ring.h create mode 100644 tests/CMakeLists.txt create mode 100644 tests/test_hidapi_input_ring.c diff --git a/CMakeLists.txt b/CMakeLists.txt index d7086813..194c04a8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -73,12 +73,17 @@ endif() if(WIN32) # so far only Windows has tests option(HIDAPI_WITH_TESTS "Build HIDAPI (unit-)tests" ${IS_DEBUG_BUILD}) +elseif(APPLE OR HIDAPI_WITH_LIBUSB) + option(HIDAPI_WITH_TESTS "Build HIDAPI (unit-)tests" ${IS_DEBUG_BUILD}) else() set(HIDAPI_WITH_TESTS OFF) endif() if(HIDAPI_WITH_TESTS) enable_testing() + if(APPLE OR HIDAPI_WITH_LIBUSB) + add_subdirectory(tests) + endif() endif() if(WIN32) diff --git a/core/hidapi_input_ring.h b/core/hidapi_input_ring.h new file mode 100644 index 00000000..85e028ac --- /dev/null +++ b/core/hidapi_input_ring.h @@ -0,0 +1,254 @@ +/******************************************************* + HIDAPI - Multi-Platform library for + communication with HID devices. + + Internal ring buffer — a bounded FIFO of HID input reports. + Storage is a single pre-allocated flat byte buffer of + (capacity × slot_size) bytes, where slot_size is a backend-specific + runtime upper bound on input report size, determined at open time. + Drop-oldest-when-full semantics. + + All helpers are defined as `static`; each translation unit + that includes this header gets its own private copy. + + ---- Lifecycle ---- + hidapi_input_ring_init(r, capacity, slot_size) is called by each + backend once its runtime upper bound on input report size is known: + - macOS: kIOHIDMaxInputReportSizeKey, an authoritative value + derived from the HID report descriptor by IOKit + - libusb: wMaxPacketSize of the interrupt-IN endpoint — an upper + bound consistent with hidapi's libusb backend, which + only transfers single packets + capacity comes from dev->num_input_buffers (default 30). + The slot array is sized exactly, never to MAX. + + hidapi_input_ring_resize() grows or shrinks the allocation under + the caller-held mutex when hid_set_num_input_buffers() is called. + r->slot_size is fixed for the ring's lifetime; only r->capacity + and (on shrink-below-count) r->count change. + + ---- Concurrency ---- + EVERY helper requires the caller to hold the mutex protecting r. + The helpers do not lock internally. + + ---- Ownership ---- + The ring owns its entire backing storage throughout its lifetime. No + pointer into that storage is ever exposed to callers. Pop copies the + oldest report's bytes into a caller-supplied buffer and advances the + ring head — no borrowed-pointer contract to misuse, no slot lifetime + tied to subsequent ring operations. Push is allocation-free: memcpy + into the next slot's inline bytes. + + Note: after pop_into() advances r->head, the length entry for the + just-vacated slot is left stale in r->lengths[]. This is harmless — + r->lengths[i] is only read when slot i is the current r->head, which + only happens again after a push has overwritten both the storage + bytes AND the length entry for that slot. + *******************************************************/ + +#ifndef HIDAPI_INPUT_RING_H_ +#define HIDAPI_INPUT_RING_H_ + +#include +#include +#include +#include + +struct hidapi_input_ring { + uint8_t *storage; /* capacity × slot_size bytes; one allocation */ + size_t *lengths; /* actual len of each queued report; capacity entries */ + int capacity; /* slot count; changed only by resize() */ + size_t slot_size; /* bytes per slot; fixed at 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 + (push-time evictions + resize shrink drops). + Does not count ENOMEM. */ +}; + +/* PRECONDITION: r->storage == NULL (never-initialized or destroyed). + * 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, size_t slot_size) +{ + if (!r || capacity < 1 || slot_size < 1) + return -1; + if (r->storage) /* double-init guard */ + return -1; + /* Reject capacity × slot_size that would overflow size_t. */ + if ((size_t)capacity > SIZE_MAX / slot_size) + return -1; + + r->storage = (uint8_t *)malloc((size_t)capacity * slot_size); + r->lengths = (size_t *)calloc((size_t)capacity, sizeof(size_t)); + if (!r->storage || !r->lengths) { + free(r->storage); + free(r->lengths); + r->storage = NULL; + r->lengths = NULL; + return -1; + } + r->capacity = capacity; + r->slot_size = slot_size; + r->head = 0; + r->tail = 0; + r->count = 0; + r->dropped = 0; + return 0; +} + +/* PRECONDITION: caller holds r's mutex, or is tearing down. + * Frees storage and lengths; zeros r. Safe on zero-init or + * previously-destroyed rings. */ +static void hidapi_input_ring_destroy(struct hidapi_input_ring *r) +{ + if (!r) return; + free(r->storage); + free(r->lengths); + r->storage = NULL; + r->lengths = NULL; + r->capacity = 0; + r->slot_size = 0; + r->head = 0; + r->tail = 0; + r->count = 0; + r->dropped = 0; +} + +/* Enqueue a copy of [data, data+len). memcpy only — no malloc. + * Evicts oldest when count >= capacity. len == 0 stores a zero-length + * report (lengths[tail]=0, no memcpy). + * + * PRECONDITION: caller holds r's mutex; if len > 0, data != NULL. + * + * Returns 0; -1 on invalid args or len > slot_size. On failure r is + * unchanged and dropped is NOT incremented. */ +static int hidapi_input_ring_push(struct hidapi_input_ring *r, + const uint8_t *data, size_t len) +{ + if (!r || !r->storage || (len > 0 && !data)) + return -1; + if (len > r->slot_size) + return -1; /* oversized report — reject, don't truncate */ + + if (r->count >= r->capacity) { + /* Drop oldest: just advance head. No per-payload free needed. */ + r->head = (r->head + 1) % r->capacity; + r->count--; + r->dropped++; + } + + if (len > 0) { + memcpy(r->storage + (size_t)r->tail * r->slot_size, data, len); + } + r->lengths[r->tail] = len; + r->tail = (r->tail + 1) % r->capacity; + r->count++; + return 0; +} + +/* Copy the oldest report's bytes into [dst, dst + dst_len). If the + * report payload is larger than dst_len, it's truncated to dst_len + * bytes. The report is consumed from the ring regardless. + * + * Copy-out semantics — no borrowed pointer is ever exposed outside + * this helper. This keeps the slot lifetime entirely contained: the + * caller's buffer owns the bytes after return, and the ring is free + * to reuse the vacated slot on the next push. + * + * PRECONDITION: caller holds r's mutex. + * If dst_len > 0, dst != NULL. + * + * Edge case: pop_into(r, NULL, 0) is valid and consumes the head + * slot without copying (to_copy == 0, no memcpy, head++, count--). + * This mirrors the pre-PR ring_pop_into() helper's own behavior at + * length=0. Note: hid_read_timeout() only reaches this code path on + * libusb (mac's hid_read_timeout has an early length==0 guard that + * fails with "Zero buffer/length" before the ring is touched). + * + * Returns: bytes copied (0 <= rc <= dst_len) on success; + * -1 if empty ring or invalid args (including dst_len>0 with dst=NULL). */ +static int hidapi_input_ring_pop_into(struct hidapi_input_ring *r, + uint8_t *dst, size_t dst_len) +{ + if (!r || !r->storage) + return -1; + if (dst_len > 0 && !dst) + return -1; + if (r->count == 0) + return -1; + + size_t payload_len = r->lengths[r->head]; + size_t to_copy = (payload_len < dst_len) ? payload_len : dst_len; + if (to_copy > 0) { + memcpy(dst, r->storage + (size_t)r->head * r->slot_size, to_copy); + } + r->head = (r->head + 1) % r->capacity; + r->count--; + return (int)to_copy; +} + +/* Resize the slot array to new_cap. Allocates a new flat storage buffer + * and lengths array, memcpy's surviving reports' bytes into the new + * storage preserving FIFO order, then frees the old buffers. + * + * On shrink below count, the oldest (count - new_cap) reports are + * evicted — matching the push-time drop-oldest policy. + * + * slot_size is preserved (fixed for the ring's lifetime). + * + * PRECONDITION: caller holds r's mutex; new_cap >= 1. + * On failure r is unchanged. + * + * Returns 0; -1 on invalid args or ENOMEM. */ +static int hidapi_input_ring_resize(struct hidapi_input_ring *r, int new_cap) +{ + if (!r || !r->storage || new_cap < 1) + return -1; + if (new_cap == r->capacity) + return 0; + /* Reject new_cap × slot_size that would overflow size_t. */ + if ((size_t)new_cap > SIZE_MAX / r->slot_size) + return -1; + + uint8_t *new_storage = (uint8_t *)malloc((size_t)new_cap * r->slot_size); + size_t *new_lengths = (size_t *)calloc((size_t)new_cap, sizeof(size_t)); + if (!new_storage || !new_lengths) { + free(new_storage); + free(new_lengths); + return -1; + } + + int keep = (r->count > new_cap) ? new_cap : r->count; + int dropped = r->count - keep; + + /* Copy surviving reports' payload bytes into new storage, packed + * starting at slot 0. Oldest dropped entries are simply not copied. */ + int src = (r->head + dropped) % r->capacity; + for (int dst = 0; dst < keep; dst++) { + if (r->lengths[src] > 0) { + memcpy(new_storage + (size_t)dst * r->slot_size, + r->storage + (size_t)src * r->slot_size, + r->lengths[src]); + } + new_lengths[dst] = r->lengths[src]; + src = (src + 1) % r->capacity; + } + + r->dropped += dropped; + + free(r->storage); + free(r->lengths); + + r->storage = new_storage; + r->lengths = new_lengths; + r->capacity = new_cap; + r->head = 0; + r->count = keep; + r->tail = keep % new_cap; /* wraps to 0 when keep == new_cap */ + + return 0; +} + +#endif /* HIDAPI_INPUT_RING_H_ */ diff --git a/libusb/hid.c b/libusb/hid.c index a7d57fad..50d98424 100644 --- a/libusb/hid.c +++ b/libusb/hid.c @@ -49,7 +49,7 @@ #endif #include "hidapi_libusb.h" -#include "hidapi_input_ring.h" +#include "../core/hidapi_input_ring.h" #ifndef HIDAPI_THREAD_MODEL_INCLUDE #define HIDAPI_THREAD_MODEL_INCLUDE "hidapi_thread_pthread.h" @@ -113,9 +113,12 @@ struct hid_device_ { int transfer_loop_finished; struct libusb_transfer *transfer; - /* 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. */ + /* Input report ring buffer. Flat inline-slot storage sized at + (dev->num_input_buffers × max(wMaxPacketSize, 64)) bytes, + allocated in hidapi_initialize_device() once the interrupt-IN + endpoint is known. The max(., 64) clamp is a defensive fallback + for devices with no interrupt-IN endpoint. Resized under the + device mutex by hid_set_num_input_buffers(). */ struct hidapi_input_ring input_ring; /* Was kernel driver detached by libusb */ @@ -140,11 +143,6 @@ 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; @@ -968,14 +966,17 @@ static void LIBUSB_CALL read_callback(struct libusb_transfer *transfer) hidapi_thread_mutex_lock(&dev->thread_state); 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 { + /* Push rejected. len > slot_size is not expected here + * because the current libusb backend sizes both the + * transfer buffer and ring slot_size from + * input_ep_max_packet_size. libusb has no active error + * channel, so drop silently. */ } - /* Allocation failed; libusb has no active error channel here, so the - * report is dropped silently. */ hidapi_thread_mutex_unlock(&dev->thread_state); } @@ -1241,6 +1242,18 @@ static int hidapi_initialize_device(hid_device *dev, const struct libusb_interfa } } + { + /* Clamp slot_size to at least 64 B if endpoint reports none + * (defensive fallback for non-compliant devices). + * Enhancement: prefer max input report size by going through + * report descriptor once parser is available. */ + size_t slot_size = (dev->input_ep_max_packet_size > 0) + ? (size_t)dev->input_ep_max_packet_size : 64; + if (hidapi_input_ring_init(&dev->input_ring, dev->num_input_buffers, slot_size) != 0) { + return 0; + } + } + hidapi_thread_create(&dev->thread_state, read_thread, dev); /* Wait here for the read thread to be initialized. */ @@ -1446,15 +1459,7 @@ int HID_API_EXPORT hid_write(hid_device *dev, const unsigned char *data, size_t 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) { - 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; + return hidapi_input_ring_pop_into(&dev->input_ring, data, length); } static void cleanup_mutex(void *param) @@ -1566,11 +1571,11 @@ 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) 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); + if (hidapi_input_ring_resize(&dev->input_ring, num_buffers) != 0) { + hidapi_thread_mutex_unlock(&dev->thread_state); + return -1; } + dev->num_input_buffers = num_buffers; hidapi_thread_mutex_unlock(&dev->thread_state); return 0; } diff --git a/libusb/hidapi_input_ring.h b/libusb/hidapi_input_ring.h deleted file mode 100644 index 59d8b109..00000000 --- a/libusb/hidapi_input_ring.h +++ /dev/null @@ -1,213 +0,0 @@ -/******************************************************* - 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/mac/hid.c b/mac/hid.c index 4fdeff62..5ad4d295 100644 --- a/mac/hid.c +++ b/mac/hid.c @@ -37,7 +37,7 @@ #include #include "hidapi_darwin.h" -#include "hidapi_input_ring.h" +#include "../core/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. @@ -148,11 +148,6 @@ 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; @@ -877,7 +872,6 @@ static void hid_report_callback(void *context, IOReturn result, void *sender, pthread_mutex_lock(&dev->mutex); int push_rc = hidapi_input_ring_push(&dev->input_ring, - dev->num_input_buffers, report, (size_t)report_length); if (push_rc == 0) { @@ -1034,6 +1028,18 @@ hid_device * HID_API_EXPORT hid_open_path(const char *path) dev->max_input_report_len = (CFIndex) get_max_report_length(dev->device_handle); dev->input_report_buf = (uint8_t*) calloc(dev->max_input_report_len, sizeof(uint8_t)); + { + /* Clamp slot_size to at least 64 B if device reports none + * (defensive fallback for devices that don't advertise + * kIOHIDMaxInputReportSizeKey). */ + size_t slot_size = (dev->max_input_report_len > 0) + ? (size_t)dev->max_input_report_len : 64; + if (hidapi_input_ring_init(&dev->input_ring, dev->num_input_buffers, slot_size) != 0) { + register_global_error_format("hid_open_path: failed to allocate input report ring"); + goto return_error; + } + } + /* Create the Run Loop Mode for this device. printing the reference seems to work. */ snprintf(str, sizeof(str), "HIDAPI_%p", (void*) dev->device_handle); @@ -1163,15 +1169,7 @@ int HID_API_EXPORT hid_write(hid_device *dev, const unsigned char *data, size_t hold dev->mutex. Returns bytes copied, or -1 if empty. */ static int ring_pop_into(hid_device *dev, unsigned char *data, size_t length) { - 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; + return hidapi_input_ring_pop_into(&dev->input_ring, data, length); } static int cond_wait(hid_device *dev, pthread_cond_t *cond, pthread_mutex_t *mutex) @@ -1321,11 +1319,12 @@ int HID_API_EXPORT hid_set_num_input_buffers(hid_device *dev, int num_buffers) 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); + if (hidapi_input_ring_resize(&dev->input_ring, num_buffers) != 0) { + pthread_mutex_unlock(&dev->mutex); + register_device_error(dev, "failed to resize input report ring"); + return -1; } + dev->num_input_buffers = num_buffers; pthread_mutex_unlock(&dev->mutex); return 0; } diff --git a/mac/hidapi_input_ring.h b/mac/hidapi_input_ring.h deleted file mode 100644 index 59d8b109..00000000 --- a/mac/hidapi_input_ring.h +++ /dev/null @@ -1,213 +0,0 @@ -/******************************************************* - 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/subprojects/hidapi_build_cmake/CMakeLists.txt b/subprojects/hidapi_build_cmake/CMakeLists.txt index 4586ce6a..daf8fc95 100644 --- a/subprojects/hidapi_build_cmake/CMakeLists.txt +++ b/subprojects/hidapi_build_cmake/CMakeLists.txt @@ -3,7 +3,7 @@ project(hidapi LANGUAGES C) file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/root") -foreach(ROOT_ELEMENT CMakeLists.txt hidapi src windows linux mac libusb pc VERSION) +foreach(ROOT_ELEMENT CMakeLists.txt hidapi src windows linux mac libusb pc core tests VERSION) file(COPY "${CMAKE_CURRENT_LIST_DIR}/../../${ROOT_ELEMENT}" DESTINATION "${CMAKE_CURRENT_BINARY_DIR}/root/") endforeach() diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 00000000..0c4bfa03 --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,5 @@ +cmake_minimum_required(VERSION 3.1.3...3.25 FATAL_ERROR) + +add_executable(test_hidapi_input_ring test_hidapi_input_ring.c) +target_include_directories(test_hidapi_input_ring PRIVATE "${PROJECT_SOURCE_DIR}/core") +add_test(NAME hidapi_input_ring COMMAND test_hidapi_input_ring) diff --git a/tests/test_hidapi_input_ring.c b/tests/test_hidapi_input_ring.c new file mode 100644 index 00000000..420dfc88 --- /dev/null +++ b/tests/test_hidapi_input_ring.c @@ -0,0 +1,458 @@ +/* Unit tests for core/hidapi_input_ring.h (flat pre-alloc design). + * Backend-agnostic — includes the header directly. + */ + +#include +#include + +#include "hidapi_input_ring.h" + +#define SLOT_SZ 64 /* plausible HID report size */ + +#define CHECK(expr) do { \ + if (!(expr)) { \ + fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, #expr); \ + return 1; \ + } \ +} while (0) + +static int test_init_destroy(void) { + struct hidapi_input_ring r = {0}; + CHECK(hidapi_input_ring_init(&r, 4, SLOT_SZ) == 0); + CHECK(r.capacity == 4 && r.slot_size == SLOT_SZ && r.count == 0); + CHECK(r.head == 0 && r.tail == 0); + hidapi_input_ring_destroy(&r); + CHECK(r.storage == NULL && r.capacity == 0 && r.slot_size == 0); + return 0; +} + +static int test_init_invalid(void) { + struct hidapi_input_ring r = {0}; + CHECK(hidapi_input_ring_init(&r, 0, SLOT_SZ) == -1); + CHECK(hidapi_input_ring_init(&r, -1, SLOT_SZ) == -1); + CHECK(hidapi_input_ring_init(&r, 4, 0) == -1); /* slot_size=0 */ + CHECK(hidapi_input_ring_init(NULL, 4, SLOT_SZ) == -1); + CHECK(hidapi_input_ring_init(&r, 4, SLOT_SZ) == 0); + CHECK(hidapi_input_ring_init(&r, 4, SLOT_SZ) == -1); /* double-init */ + hidapi_input_ring_destroy(&r); + return 0; +} + +static int test_destroy_idempotent(void) { + hidapi_input_ring_destroy(NULL); + + struct hidapi_input_ring zero = {0}; + hidapi_input_ring_destroy(&zero); + CHECK(zero.storage == NULL && zero.capacity == 0); + + struct hidapi_input_ring r = {0}; + CHECK(hidapi_input_ring_init(&r, 4, SLOT_SZ) == 0); + hidapi_input_ring_destroy(&r); + hidapi_input_ring_destroy(&r); + CHECK(r.storage == NULL); + return 0; +} + +static int test_invalid_args(void) { + /* NULL / uninit / bad-arg rejections on push and pop_into. */ + struct hidapi_input_ring r = {0}; + CHECK(hidapi_input_ring_init(&r, 4, SLOT_SZ) == 0); + + uint8_t v[4] = {1,2,3,4}; + uint8_t buf[SLOT_SZ]; + struct hidapi_input_ring uninit = {0}; + + /* push */ + CHECK(hidapi_input_ring_push(NULL, v, 4) == -1); + CHECK(hidapi_input_ring_push(&r, NULL, 4) == -1); + CHECK(hidapi_input_ring_push(&uninit, v, 4) == -1); + + /* pop_into */ + CHECK(hidapi_input_ring_pop_into(NULL, buf, sizeof(buf)) == -1); + CHECK(hidapi_input_ring_pop_into(&r, NULL, sizeof(buf)) == -1); /* dst_len>0, dst=NULL */ + CHECK(hidapi_input_ring_pop_into(&uninit, buf, sizeof(buf)) == -1); + + CHECK(hidapi_input_ring_push(&r, v, 4) == 0); /* ring still usable */ + hidapi_input_ring_destroy(&r); + return 0; +} + +static int test_push_oversized_report(void) { + /* len > slot_size must be rejected without consuming a slot. */ + struct hidapi_input_ring r = {0}; + CHECK(hidapi_input_ring_init(&r, 4, 8) == 0); /* slot_size=8 */ + + uint8_t small[8] = {0}; + uint8_t big[9] = {0}; + + CHECK(hidapi_input_ring_push(&r, small, 8) == 0); /* fits exactly */ + CHECK(hidapi_input_ring_push(&r, big, 9) == -1); /* overflow → reject */ + CHECK(r.count == 1 && r.dropped == 0); + + hidapi_input_ring_destroy(&r); + return 0; +} + +static int test_init_overflow_reject(void) { + /* capacity × slot_size must not overflow size_t. Guards: + * init: (size_t)capacity > SIZE_MAX / slot_size + * resize: (size_t)new_cap > SIZE_MAX / r->slot_size + * Defensive — in practice slot_size comes from backend descriptors + * capped well below SIZE_MAX, but a misreporting or fuzzed path + * could otherwise drive a wrapped allocation size into malloc. */ + struct hidapi_input_ring r = {0}; + + /* init guard: 2 × SIZE_MAX wraps. */ + CHECK(hidapi_input_ring_init(&r, 2, SIZE_MAX) == -1); + /* r left untouched on failure — all fields still zero-initialized. */ + CHECK(r.storage == NULL); + CHECK(r.lengths == NULL); + CHECK(r.capacity == 0); + CHECK(r.slot_size == 0); + CHECK(r.count == 0 && r.head == 0 && r.tail == 0 && r.dropped == 0); + + /* init now succeeds with sane values. */ + CHECK(hidapi_input_ring_init(&r, 4, SLOT_SZ) == 0); + + /* resize guard: the expression is identical in shape to init's, so + * exercising it from a valid ring is sufficient evidence. The guard + * can only fire with a slot_size far larger than typical HID reports + * (> SIZE_MAX / INT_MAX bytes); at realistic slot sizes the guard is + * unreachable via any int new_cap, so this test temporarily inflates + * r.slot_size to exercise the branch. slot_size is restored before + * destroy to keep ring state consistent. */ + size_t saved_slot_size = r.slot_size; + r.slot_size = SIZE_MAX / 2; + CHECK(hidapi_input_ring_resize(&r, 3) == -1); + r.slot_size = saved_slot_size; + + hidapi_input_ring_destroy(&r); + return 0; +} + +static int test_pop_into_truncation(void) { + /* When dst_len < payload_len, pop_into copies min(payload_len, dst_len) + * bytes and consumes the slot. Return value is bytes copied (not + * original payload size). */ + struct hidapi_input_ring r = {0}; + CHECK(hidapi_input_ring_init(&r, 4, SLOT_SZ) == 0); + + uint8_t payload[32]; + for (int i = 0; i < 32; i++) payload[i] = (uint8_t)(i + 1); + CHECK(hidapi_input_ring_push(&r, payload, 32) == 0); + + uint8_t small_buf[10]; + CHECK(hidapi_input_ring_pop_into(&r, small_buf, sizeof(small_buf)) == 10); + CHECK(memcmp(small_buf, payload, 10) == 0); + CHECK(r.count == 0); /* slot consumed even though payload was truncated */ + + hidapi_input_ring_destroy(&r); + return 0; +} + +static int test_pop_into_null_zero(void) { + /* pop_into(r, NULL, 0) is valid: consumes head slot without copying, + * returns 0. Mirrors pre-PR ring_pop_into() helper semantics at + * length=0. Reachable via hid_read_timeout only on libusb; mac's + * hid_read_timeout has an early length==0 guard that returns before + * touching the ring. */ + struct hidapi_input_ring r = {0}; + CHECK(hidapi_input_ring_init(&r, 4, SLOT_SZ) == 0); + + uint8_t payload[8] = {1,2,3,4,5,6,7,8}; + CHECK(hidapi_input_ring_push(&r, payload, 8) == 0); + CHECK(r.count == 1); + + CHECK(hidapi_input_ring_pop_into(&r, NULL, 0) == 0); + CHECK(r.count == 0); /* slot consumed */ + + hidapi_input_ring_destroy(&r); + return 0; +} + +static int test_push_pop_fifo(void) { + struct hidapi_input_ring r = {0}; + CHECK(hidapi_input_ring_init(&r, 4, SLOT_SZ) == 0); + + uint8_t a[] = {1,2,3}, b[] = {4,5,6,7}, c[] = {8}; + CHECK(hidapi_input_ring_push(&r, a, 3) == 0); + CHECK(hidapi_input_ring_push(&r, b, 4) == 0); + CHECK(hidapi_input_ring_push(&r, c, 1) == 0); + CHECK(r.count == 3 && r.dropped == 0); + + uint8_t buf[SLOT_SZ]; + CHECK(hidapi_input_ring_pop_into(&r, buf, sizeof(buf)) == 3 && memcmp(buf, a, 3) == 0); + CHECK(hidapi_input_ring_pop_into(&r, buf, sizeof(buf)) == 4 && memcmp(buf, b, 4) == 0); + CHECK(hidapi_input_ring_pop_into(&r, buf, sizeof(buf)) == 1 && memcmp(buf, c, 1) == 0); + CHECK(hidapi_input_ring_pop_into(&r, buf, sizeof(buf)) == -1); + hidapi_input_ring_destroy(&r); + return 0; +} + +static int test_drop_oldest_on_full(void) { + struct hidapi_input_ring r = {0}; + CHECK(hidapi_input_ring_init(&r, 4, SLOT_SZ) == 0); + uint8_t v[1]; + for (int i = 0; i < 6; i++) { v[0] = (uint8_t)i; hidapi_input_ring_push(&r, v, 1); } + CHECK(r.count == 4 && r.dropped == 2); + uint8_t buf[SLOT_SZ]; + for (int expect = 2; expect <= 5; expect++) { + CHECK(hidapi_input_ring_pop_into(&r, buf, sizeof(buf)) == 1 && buf[0] == (uint8_t)expect); + } + hidapi_input_ring_destroy(&r); + return 0; +} + +static int test_drop_oldest_varying_lengths(void) { + /* Push reports of different sizes, trigger drop-oldest on a full ring, + * then verify the survivors' lengths are correct. This guards against + * a bug where push forgets to update lengths[tail] and the pop reads + * a stale length from the previously-evicted payload. + * + * cap=2 to force drop-oldest on the 3rd push. + */ + struct hidapi_input_ring r = {0}; + CHECK(hidapi_input_ring_init(&r, 2, SLOT_SZ) == 0); + + uint8_t big[50]; memset(big, 0xAA, 50); + uint8_t med[20]; memset(med, 0xBB, 20); + uint8_t small[3] = {0xC1, 0xC2, 0xC3}; + + CHECK(hidapi_input_ring_push(&r, big, 50) == 0); /* slot 0: 50 B */ + CHECK(hidapi_input_ring_push(&r, med, 20) == 0); /* slot 1: 20 B, full */ + /* Third push evicts big (oldest) and reuses slot 0 for small (3 B). + * lengths[0] must now be 3, not 50. */ + CHECK(hidapi_input_ring_push(&r, small, 3) == 0); + CHECK(r.count == 2 && r.dropped == 1); + + uint8_t buf[SLOT_SZ]; + /* First pop: med (20 B) from slot 1 */ + CHECK(hidapi_input_ring_pop_into(&r, buf, sizeof(buf)) == 20); + CHECK(memcmp(buf, med, 20) == 0); + /* Second pop: small (3 B) from slot 0 — lengths[0] must be 3, not stale 50 */ + CHECK(hidapi_input_ring_pop_into(&r, buf, sizeof(buf)) == 3); + CHECK(memcmp(buf, small, 3) == 0); + + hidapi_input_ring_destroy(&r); + return 0; +} + +static int test_zero_length_report(void) { + struct hidapi_input_ring r = {0}; + CHECK(hidapi_input_ring_init(&r, 4, SLOT_SZ) == 0); + CHECK(hidapi_input_ring_push(&r, NULL, 0) == 0); + uint8_t buf[SLOT_SZ]; + CHECK(hidapi_input_ring_pop_into(&r, buf, sizeof(buf)) == 0); /* 0 bytes copied, report consumed */ + CHECK(r.count == 0); + hidapi_input_ring_destroy(&r); + return 0; +} + +static int test_resize_grow(void) { + struct hidapi_input_ring r = {0}; + CHECK(hidapi_input_ring_init(&r, 4, SLOT_SZ) == 0); + uint8_t v[1]; + for (int i = 0; i < 2; i++) { v[0] = (uint8_t)i; hidapi_input_ring_push(&r, v, 1); } + CHECK(hidapi_input_ring_resize(&r, 8) == 0); + CHECK(r.capacity == 8 && r.slot_size == SLOT_SZ && r.count == 2 && r.head == 0 && r.tail == 2); + uint8_t buf[SLOT_SZ]; + CHECK(hidapi_input_ring_pop_into(&r, buf, sizeof(buf)) == 1 && buf[0] == 0); + CHECK(hidapi_input_ring_pop_into(&r, buf, sizeof(buf)) == 1 && buf[0] == 1); + hidapi_input_ring_destroy(&r); + return 0; +} + +static int test_resize_shrink_below_count(void) { + struct hidapi_input_ring r = {0}; + CHECK(hidapi_input_ring_init(&r, 8, SLOT_SZ) == 0); + uint8_t v[1]; + for (int i = 0; i < 6; i++) { v[0] = (uint8_t)i; hidapi_input_ring_push(&r, v, 1); } + CHECK(hidapi_input_ring_resize(&r, 3) == 0); + CHECK(r.capacity == 3 && r.count == 3 && r.head == 0 && r.tail == 0 && r.dropped == 3); + uint8_t buf[SLOT_SZ]; + for (int expect = 3; expect <= 5; expect++) { + CHECK(hidapi_input_ring_pop_into(&r, buf, sizeof(buf)) == 1 && buf[0] == (uint8_t)expect); + } + hidapi_input_ring_destroy(&r); + return 0; +} + +static int test_resize_shrink_above_count(void) { + struct hidapi_input_ring r = {0}; + CHECK(hidapi_input_ring_init(&r, 8, SLOT_SZ) == 0); + uint8_t v[1]; + for (int i = 0; i < 3; i++) { v[0] = (uint8_t)i; hidapi_input_ring_push(&r, v, 1); } + CHECK(hidapi_input_ring_resize(&r, 5) == 0); + CHECK(r.capacity == 5 && r.count == 3 && r.head == 0 && r.tail == 3 && r.dropped == 0); + + /* Verify FIFO payload order survived the shrink-above-count resize. */ + uint8_t buf[SLOT_SZ]; + for (int expect = 0; expect <= 2; expect++) { + CHECK(hidapi_input_ring_pop_into(&r, buf, sizeof(buf)) == 1 && buf[0] == (uint8_t)expect); + } + hidapi_input_ring_destroy(&r); + return 0; +} + +static int test_resize_same_capacity(void) { + /* No-op path must not mutate state — not even on a wrapped non-empty ring. */ + struct hidapi_input_ring r = {0}; + CHECK(hidapi_input_ring_init(&r, 4, SLOT_SZ) == 0); + + /* Case 1: fresh empty ring. */ + CHECK(hidapi_input_ring_resize(&r, 4) == 0); + CHECK(r.capacity == 4 && r.count == 0 && r.head == 0 && r.tail == 0); + + /* Case 2: wrapped, full ring. */ + uint8_t v[1]; + for (int i = 0; i < 6; i++) { v[0] = (uint8_t)i; hidapi_input_ring_push(&r, v, 1); } + CHECK(r.count == 4 && r.head == 2 && r.tail == 2 && r.dropped == 2); + CHECK(hidapi_input_ring_resize(&r, 4) == 0); + CHECK(r.capacity == 4 && r.count == 4 && r.head == 2 && r.tail == 2 && r.dropped == 2); + + uint8_t buf[SLOT_SZ]; + for (int expect = 2; expect <= 5; expect++) { + CHECK(hidapi_input_ring_pop_into(&r, buf, sizeof(buf)) == 1 && buf[0] == (uint8_t)expect); + } + hidapi_input_ring_destroy(&r); + return 0; +} + +static int test_resize_empty_ring(void) { + struct hidapi_input_ring r = {0}; + CHECK(hidapi_input_ring_init(&r, 4, SLOT_SZ) == 0); + CHECK(hidapi_input_ring_resize(&r, 8) == 0); + CHECK(r.capacity == 8 && r.count == 0 && r.head == 0 && r.tail == 0); + CHECK(hidapi_input_ring_resize(&r, 2) == 0); + CHECK(r.capacity == 2 && r.count == 0 && r.head == 0 && r.tail == 0); + + /* Push-then-pop round-trip proves head/tail/lengths stayed coherent + * through both resizes — a subtle bug could leave metadata valid-looking + * but the post-resize storage misaligned. */ + uint8_t v[1] = {42}; + CHECK(hidapi_input_ring_push(&r, v, 1) == 0); + CHECK(r.count == 1); + uint8_t buf[SLOT_SZ]; + CHECK(hidapi_input_ring_pop_into(&r, buf, sizeof(buf)) == 1 && buf[0] == 42); + CHECK(r.count == 0); + hidapi_input_ring_destroy(&r); + return 0; +} + +static int test_resize_invalid_args(void) { + struct hidapi_input_ring r = {0}; + CHECK(hidapi_input_ring_init(&r, 4, SLOT_SZ) == 0); + CHECK(hidapi_input_ring_resize(&r, 0) == -1); + CHECK(hidapi_input_ring_resize(&r, -1) == -1); + CHECK(hidapi_input_ring_resize(NULL, 4) == -1); + CHECK(r.capacity == 4); + struct hidapi_input_ring uninit = {0}; + CHECK(hidapi_input_ring_resize(&uninit, 4) == -1); + hidapi_input_ring_destroy(&r); + return 0; +} + +static int test_resize_after_wrap(void) { + struct hidapi_input_ring r = {0}; + CHECK(hidapi_input_ring_init(&r, 4, SLOT_SZ) == 0); + uint8_t v[1]; + for (int i = 0; i < 7; i++) { v[0] = (uint8_t)i; hidapi_input_ring_push(&r, v, 1); } + CHECK(r.count == 4 && r.dropped == 3); + CHECK(hidapi_input_ring_resize(&r, 6) == 0); + CHECK(r.capacity == 6 && r.count == 4 && r.head == 0 && r.tail == 4); + uint8_t buf[SLOT_SZ]; + for (int expect = 3; expect <= 6; expect++) { + CHECK(hidapi_input_ring_pop_into(&r, buf, sizeof(buf)) == 1 && buf[0] == (uint8_t)expect); + } + hidapi_input_ring_destroy(&r); + return 0; +} + +static int test_resize_shrink_after_wrap(void) { + /* Hardest resize index-math path: survivor start = (head + dropped) % capacity. */ + struct hidapi_input_ring r = {0}; + CHECK(hidapi_input_ring_init(&r, 4, SLOT_SZ) == 0); + uint8_t v[1]; + for (int i = 0; i < 7; i++) { v[0] = (uint8_t)i; hidapi_input_ring_push(&r, v, 1); } + CHECK(r.count == 4 && r.dropped == 3 && r.head != 0); + CHECK(hidapi_input_ring_resize(&r, 2) == 0); + CHECK(r.capacity == 2 && r.count == 2 && r.head == 0 && r.tail == 0 && r.dropped == 5); + uint8_t buf[SLOT_SZ]; + CHECK(hidapi_input_ring_pop_into(&r, buf, sizeof(buf)) == 1 && buf[0] == 5); + CHECK(hidapi_input_ring_pop_into(&r, buf, sizeof(buf)) == 1 && buf[0] == 6); + hidapi_input_ring_destroy(&r); + return 0; +} + +static int test_stress(void) { + /* 10k mixed push/pop/resize schedule. Push-dominated (6/7 push, 1/7 pop) + * fills the ring around i=90 and keeps it at cap after that, so most + * subsequent pushes hit drop-oldest. Occasional resize (every 500 iter) + * exercises resize-under-stream. FIFO invariant checked on every pop. */ + struct hidapi_input_ring r = {0}; + CHECK(hidapi_input_ring_init(&r, 64, SLOT_SZ) == 0); + uint32_t next_write = 0, next_read = 0; + uint8_t buf[SLOT_SZ]; + for (int i = 0; i < 10000; i++) { + if (i > 0 && (i % 500) == 0) { + int new_cap = ((i / 500) % 2 == 0) ? 32 : 128; + CHECK(hidapi_input_ring_resize(&r, new_cap) == 0); + /* Shrink may have evicted oldest — rebase before any subsequent pop. */ + if (r.count > 0 && next_read < next_write - r.count) { + next_read = next_write - r.count; + } + } + if ((i % 7) == 0 && r.count > 0) { + int got_bytes = hidapi_input_ring_pop_into(&r, buf, sizeof(buf)); + CHECK(got_bytes == (int)sizeof(uint32_t)); + uint32_t got; memcpy(&got, buf, sizeof(got)); + CHECK(got == next_read); + next_read++; + } else { + CHECK(hidapi_input_ring_push(&r, (uint8_t*)&next_write, sizeof(next_write)) == 0); + next_write++; + } + /* Invariant: next_read >= (next_write - r.count). Rebase if a push + * drop-oldest or a resize shrink evicted past it. */ + if (r.count > 0 && next_read < next_write - r.count) { + next_read = next_write - r.count; + } + } + CHECK(r.dropped > 100); + hidapi_input_ring_destroy(&r); + return 0; +} + +int main(void) { + int fails = 0; + struct { const char *name; int (*fn)(void); } tests[] = { + {"init_destroy", test_init_destroy}, + {"init_invalid", test_init_invalid}, + {"destroy_idempotent", test_destroy_idempotent}, + {"invalid_args", test_invalid_args}, + {"push_oversized_report", test_push_oversized_report}, + {"init_overflow_reject", test_init_overflow_reject}, + {"pop_into_truncation", test_pop_into_truncation}, + {"pop_into_null_zero", test_pop_into_null_zero}, + {"push_pop_fifo", test_push_pop_fifo}, + {"drop_oldest_on_full", test_drop_oldest_on_full}, + {"drop_oldest_varying_lengths", test_drop_oldest_varying_lengths}, + {"zero_length_report", test_zero_length_report}, + {"resize_grow", test_resize_grow}, + {"resize_shrink_below_count", test_resize_shrink_below_count}, + {"resize_shrink_above_count", test_resize_shrink_above_count}, + {"resize_same_capacity", test_resize_same_capacity}, + {"resize_empty_ring", test_resize_empty_ring}, + {"resize_invalid_args", test_resize_invalid_args}, + {"resize_after_wrap", test_resize_after_wrap}, + {"resize_shrink_after_wrap", test_resize_shrink_after_wrap}, + {"stress", test_stress}, + }; + for (size_t i = 0; i < sizeof(tests)/sizeof(tests[0]); i++) { + int rc = tests[i].fn(); + printf("%s %s\n", rc == 0 ? "PASS" : "FAIL", tests[i].name); + fails += (rc != 0); + } + printf("\n%d/%zu failed\n", fails, sizeof(tests)/sizeof(tests[0])); + return fails ? 1 : 0; +}