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..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. */ @@ -110,14 +104,19 @@ 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. 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 @@ -134,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) { @@ -142,7 +140,13 @@ 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; hidapi_thread_state_init(&dev->thread_state); @@ -156,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); } @@ -958,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 > 30) { - return_data(dev, NULL, 0); - } - } hidapi_thread_mutex_unlock(&dev->thread_state); } else if (transfer->status == LIBUSB_TRANSFER_CANCELLED) { @@ -1454,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; } @@ -1495,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; } @@ -1510,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) { @@ -1524,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; } @@ -1574,6 +1560,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); + 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; +} + int HID_API_EXPORT hid_set_nonblocking(hid_device *dev, int nonblock) { dev->blocking = !nonblock; @@ -1734,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 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..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, @@ -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 */ @@ -152,15 +148,20 @@ 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; 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 +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 @@ -877,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 > 30) { - 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 @@ -1191,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; @@ -1230,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; @@ -1264,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; } @@ -1293,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"); @@ -1316,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 { @@ -1347,6 +1313,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_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); + 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; +} + int HID_API_EXPORT hid_set_nonblocking(hid_device *dev, int nonblock) { /* All Nonblocking operation is handled by the library. */ @@ -1418,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 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/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;