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/hidapi/hidapi.h b/hidapi/hidapi.h index cbc3107d..5d466a2b 100644 --- a/hidapi/hidapi.h +++ b/hidapi/hidapi.h @@ -423,6 +423,55 @@ extern "C" { */ int HID_API_EXPORT HID_API_CALL hid_set_nonblocking(hid_device *dev, int nonblock); + /** @brief Upper bound for hid_set_num_input_buffers(). + + Values passed above this limit are rejected by + hid_set_num_input_buffers(). Guards against + memory-exhaustion via unbounded input report queue growth. + + 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. + + Some HID devices emit input reports in bursts at rates + that exceed the default internal queue capacity, causing + silent report drops on macOS and the libusb Linux backend. + This function allows callers to change how many input + report buffers are retained per device. + + 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(). + @param num_buffers The desired number of input report buffers. + Must be in range [1, HID_API_MAX_NUM_INPUT_BUFFERS]. + + @returns + 0 on success, -1 on failure (invalid parameters or + backend-specific error). Call hid_error(dev) for + details where supported. + */ + int HID_API_EXPORT HID_API_CALL hid_set_num_input_buffers(hid_device *dev, int num_buffers); + /** @brief Send a Feature report to the device. Feature reports are sent over the Control endpoint as a diff --git a/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 d2ceef5d..50d98424 100644 --- a/libusb/hid.c +++ b/libusb/hid.c @@ -49,6 +49,7 @@ #endif #include "hidapi_libusb.h" +#include "../core/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. */ @@ -110,14 +104,22 @@ struct hid_device_ { /* Whether blocking reads are used */ int blocking; /* boolean */ + /* Maximum number of input reports to queue before dropping oldest. */ + int num_input_buffers; + /* Read thread objects */ hidapi_thread_state thread_state; int shutdown_thread; int transfer_loop_finished; struct libusb_transfer *transfer; - /* List of received input reports. */ - struct input_report *input_reports; + /* 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 */ #ifdef DETACH_KERNEL_DRIVER @@ -134,7 +136,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) { @@ -143,6 +144,7 @@ static hid_device *new_hid_device(void) return NULL; dev->blocking = 1; + dev->num_input_buffers = 30; hidapi_thread_state_init(&dev->thread_state); @@ -156,6 +158,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); } @@ -958,37 +963,21 @@ 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, + 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. */ } - 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 > 30) { - return_data(dev, NULL, 0); - } - } hidapi_thread_mutex_unlock(&dev->thread_state); } else if (transfer->status == LIBUSB_TRANSFER_CANCELLED) { @@ -1253,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. */ @@ -1454,20 +1455,11 @@ 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); - return (int)len; + return hidapi_input_ring_pop_into(&dev->input_ring, data, length); } static void cleanup_mutex(void *param) @@ -1495,9 +1487,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; } @@ -1510,11 +1501,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) { @@ -1524,11 +1515,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; } @@ -1574,6 +1565,21 @@ HID_API_EXPORT const wchar_t * HID_API_CALL hid_read_error(hid_device *dev) } + +int HID_API_EXPORT hid_set_num_input_buffers(hid_device *dev, int num_buffers) +{ + if (num_buffers <= 0 || num_buffers > HID_API_MAX_NUM_INPUT_BUFFERS) + return -1; + hidapi_thread_mutex_lock(&dev->thread_state); + 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; +} + int HID_API_EXPORT hid_set_nonblocking(hid_device *dev, int nonblock) { dev->blocking = !nonblock; @@ -1734,12 +1740,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/linux/hid.c b/linux/hid.c index a4dc26f4..6ead604a 100644 --- a/linux/hid.c +++ b/linux/hid.c @@ -1199,6 +1199,17 @@ HID_API_EXPORT const wchar_t * HID_API_CALL hid_read_error(hid_device *dev) return dev->last_read_error_str; } + +int HID_API_EXPORT hid_set_num_input_buffers(hid_device *dev, int num_buffers) +{ + if (num_buffers <= 0 || num_buffers > HID_API_MAX_NUM_INPUT_BUFFERS) { + register_device_error(dev, "num_buffers out of range"); + return -1; + } + (void)num_buffers; + return 0; +} + int HID_API_EXPORT hid_set_nonblocking(hid_device *dev, int nonblock) { /* Do all non-blocking in userspace using poll(), since it looks diff --git a/mac/hid.c b/mac/hid.c index a91bc190..5ad4d295 100644 --- a/mac/hid.c +++ b/mac/hid.c @@ -37,6 +37,7 @@ #include #include "hidapi_darwin.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. @@ -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, @@ -127,16 +122,17 @@ struct hid_device_ { IOOptionBits open_options; int blocking; int disconnected; + int num_input_buffers; CFStringRef run_loop_mode; CFRunLoopRef run_loop; 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 */ @@ -156,11 +152,11 @@ static hid_device *new_hid_device(void) dev->open_options = device_open_options; dev->blocking = 1; dev->disconnected = 0; + dev->num_input_buffers = 30; dev->run_loop_mode = NULL; dev->run_loop = NULL; dev->source = NULL; dev->input_report_buf = NULL; - dev->input_reports = NULL; dev->device_info = NULL; dev->shutdown_thread = 0; dev->last_error_str = NULL; @@ -180,14 +176,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 @@ -877,48 +867,20 @@ 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 > 30) { - return_data(dev, NULL, 0); - } + int push_rc = hidapi_input_ring_push(&dev->input_ring, + 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 @@ -1066,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); @@ -1191,25 +1165,16 @@ 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); - 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) { - while (!dev->input_reports) { + while (dev->input_ring.count == 0) { int res = pthread_cond_wait(cond, mutex); if (res != 0) return res; @@ -1230,7 +1195,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; @@ -1264,9 +1229,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; } @@ -1293,7 +1257,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"); @@ -1316,7 +1280,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 { @@ -1347,6 +1311,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_num_input_buffers(hid_device *dev, int num_buffers) +{ + if (num_buffers <= 0 || num_buffers > HID_API_MAX_NUM_INPUT_BUFFERS) { + register_device_error(dev, "num_buffers out of range"); + return -1; + } + pthread_mutex_lock(&dev->mutex); + 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; +} + int HID_API_EXPORT hid_set_nonblocking(hid_device *dev, int nonblock) { /* All Nonblocking operation is handled by the library. */ @@ -1418,12 +1400,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/netbsd/hid.c b/netbsd/hid.c index a9fca67c..a666217e 100644 --- a/netbsd/hid.c +++ b/netbsd/hid.c @@ -955,6 +955,17 @@ HID_API_EXPORT const wchar_t* HID_API_CALL hid_read_error(hid_device *dev) return dev->last_read_error_str; } + +int HID_API_EXPORT HID_API_CALL hid_set_num_input_buffers(hid_device *dev, int num_buffers) +{ + if (num_buffers <= 0 || num_buffers > HID_API_MAX_NUM_INPUT_BUFFERS) { + register_device_error(dev, "num_buffers out of range"); + return -1; + } + (void)num_buffers; + return 0; +} + int HID_API_EXPORT HID_API_CALL hid_set_nonblocking(hid_device *dev, int nonblock) { dev->blocking = !nonblock; diff --git a/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; +} diff --git a/windows/hid.c b/windows/hid.c index 1e27f10a..a628901e 100644 --- a/windows/hid.c +++ b/windows/hid.c @@ -1257,6 +1257,20 @@ HID_API_EXPORT const wchar_t * HID_API_CALL hid_read_error(hid_device *dev) return dev->last_read_error_str; } + +int HID_API_EXPORT HID_API_CALL hid_set_num_input_buffers(hid_device *dev, int num_buffers) +{ + if (num_buffers <= 0 || num_buffers > HID_API_MAX_NUM_INPUT_BUFFERS) { + register_string_error(dev, L"num_buffers out of range"); + return -1; + } + if (!HidD_SetNumInputBuffers(dev->device_handle, (ULONG)num_buffers)) { + register_winapi_error(dev, L"HidD_SetNumInputBuffers"); + return -1; + } + return 0; +} + int HID_API_EXPORT HID_API_CALL hid_set_nonblocking(hid_device *dev, int nonblock) { dev->blocking = !nonblock;