WIP: RFC: Hotplug implementation #674

Draft
Youw wants to merge 26 commits from connection-callback into master
Youw commented 2024-04-06 05:32:24 -05:00 (Migrated from github.com)

Resolves: #238

Resolves: #238
Youw commented 2024-04-06 05:32:48 -05:00 (Migrated from github.com)

keeping it as draft for now
still need to fill up a description, etc.

keeping it as draft for now still need to fill up a description, etc.
bearsh (Migrated from github.com) reviewed 2024-11-22 04:09:12 -06:00
@ -915,0 +1083,4 @@
unsigned short dev_vid = desc.idVendor;
unsigned short dev_pid = desc.idProduct;
if ((vendor_id != 0x0 && vendor_id != dev_vid) ||
bearsh (Migrated from github.com) commented 2024-11-22 04:09:12 -06:00

would it be possible/feasible to postpone the processing of the event until libusb has closed the device? I know that might get complicate but otherwise enumerate would not work (at least as long as https://github.com/libusb/libusb/pull/1532 has not been merged).

would it be possible/feasible to postpone the processing of the event until libusb has closed the device? I know that might get complicate but otherwise enumerate would not work (at least as long as https://github.com/libusb/libusb/pull/1532 has not been merged).
Youw (Migrated from github.com) reviewed 2024-11-23 06:07:38 -06:00
@ -915,0 +1083,4 @@
unsigned short dev_vid = desc.idVendor;
unsigned short dev_pid = desc.idProduct;
if ((vendor_id != 0x0 && vendor_id != dev_vid) ||
Youw (Migrated from github.com) commented 2024-11-23 06:07:38 -06:00

I don't follow your thoughts here. Can you elaborate more details?

I don't follow your thoughts here. Can you elaborate more details?
bearsh (Migrated from github.com) reviewed 2024-11-23 09:58:43 -06:00
@ -915,0 +1083,4 @@
unsigned short dev_vid = desc.idVendor;
unsigned short dev_pid = desc.idProduct;
if ((vendor_id != 0x0 && vendor_id != dev_vid) ||
bearsh (Migrated from github.com) commented 2024-11-23 09:58:43 -06:00

well, libusb opens the device and fires the callback, then hidapi tries to get infos from the device but may fail as it is still open by libusb.
if I run my example program with the libusb backend, I don't get e.g. manufacturer infos but with the hidraw backend I get them.

well, libusb opens the device and fires the callback, then hidapi tries to get infos from the device but may fail as it is still open by libusb. if I run my example program with the libusb backend, I don't get e.g. manufacturer infos but with the hidraw backend I get them.
bearsh (Migrated from github.com) reviewed 2024-11-25 07:43:03 -06:00
@ -915,0 +1083,4 @@
unsigned short dev_vid = desc.idVendor;
unsigned short dev_pid = desc.idProduct;
if ((vendor_id != 0x0 && vendor_id != dev_vid) ||
bearsh (Migrated from github.com) commented 2024-11-25 07:43:03 -06:00

I was wrong, libusb does not open the device. but for some reason, hid_enumerate_from_libusb() is not able to open the device in my case (LIBUSB_ERROR_ACCESS) when called by the hotplug handler. If I insert a small delay before libusb_open() it works.

I was wrong, libusb does not open the device. but for some reason, `hid_enumerate_from_libusb()` is not able to open the device in my case (`LIBUSB_ERROR_ACCESS`) when called by the hotplug handler. If I insert a small delay before `libusb_open()` it works.
Youw (Migrated from github.com) reviewed 2024-11-25 09:57:06 -06:00
@ -915,0 +1083,4 @@
unsigned short dev_vid = desc.idVendor;
unsigned short dev_pid = desc.idProduct;
if ((vendor_id != 0x0 && vendor_id != dev_vid) ||
Youw (Migrated from github.com) commented 2024-11-25 09:57:05 -06:00

Interesting finding...
As per documentation - it should be able to use libusb_open from a hotplug callback function.
What version of libusb are you using?

Interesting finding... As per [documentation](https://libusb.sourceforge.io/api-1.0/libusb_hotplug.html) - it should be able to use libusb_open from a hotplug callback function. What version of libusb are you using?
bearsh (Migrated from github.com) reviewed 2024-11-25 10:12:45 -06:00
@ -915,0 +1083,4 @@
unsigned short dev_vid = desc.idVendor;
unsigned short dev_pid = desc.idProduct;
if ((vendor_id != 0x0 && vendor_id != dev_vid) ||
bearsh (Migrated from github.com) commented 2024-11-25 10:12:45 -06:00

I use libusb v1.0.27 and this branch for hidapi. Everything can be found here: https://github.com/bearsh/hid/tree/feature/connection-callback including the test program. The project is a go-wrapper around hiadpi. The test application is in cmd/hid-hotplug.

maybe I should file a bug at libusb... I've created https://github.com/libusb/libusb/issues/1586

I use libusb v1.0.27 and this branch for hidapi. Everything can be found here: https://github.com/bearsh/hid/tree/feature/connection-callback including the test program. The project is a go-wrapper around hiadpi. The test application is in `cmd/hid-hotplug`. ~maybe I should file a bug at libusb...~ I've created https://github.com/libusb/libusb/issues/1586
bearsh (Migrated from github.com) reviewed 2024-11-27 00:29:01 -06:00
@ -915,0 +1083,4 @@
unsigned short dev_vid = desc.idVendor;
unsigned short dev_pid = desc.idProduct;
if ((vendor_id != 0x0 && vendor_id != dev_vid) ||
bearsh (Migrated from github.com) commented 2024-11-27 00:29:01 -06:00

something like the following would solve my issue and is also suggested as workaround by @mcuee in https://github.com/libusb/libusb/issues/1586#issuecomment-2500248425

diff --git a/hidapi/libusb/hid.c b/hidapi/libusb/hid.c
index ca8555c..b1c31c5 100644
--- a/hidapi/libusb/hid.c
+++ b/hidapi/libusb/hid.c
@@ -944,7 +944,7 @@ static int should_enumerate_interface(unsigned short vendor_id, const struct lib
 	return 0;
 }
 
-static struct hid_device_info* hid_enumerate_from_libusb(libusb_device *dev, unsigned short vendor_id, unsigned short product_id)
+static struct hid_device_info* hid_enumerate_from_libusb(libusb_device *dev, unsigned short vendor_id, unsigned short product_id, int hotplug)
 {
 	struct hid_device_info *root = NULL; /* return object */
 	struct hid_device_info *cur_dev = NULL;
@@ -977,7 +977,11 @@ static struct hid_device_info* hid_enumerate_from_libusb(libusb_device *dev, uns
 				if (should_enumerate_interface(dev_vid, intf_desc)) {
 					struct hid_device_info *tmp;
 
-					res = libusb_open(dev, &handle);
+					/* after a hotplug event, retry it 5 time (max 50ms extra latency) */
+					unsigned try = hotplug ? 6 : 1;
+					while (try-- && (res = libusb_open(dev, &handle)) == LIBUSB_ERROR_ACCESS) {
+						usleep(10000);
+					}
 #ifdef __ANDROID__
 					if (handle) {
 						/* There is (a potential) libusb Android backend, in which
@@ -1058,7 +1062,7 @@ struct hid_device_info HID_API_EXPORT *hid_enumerate(unsigned short vendor_id, u
 		return NULL;
 
 	while ((dev = devs[i++]) != NULL) {
-		struct hid_device_info *tmp = hid_enumerate_from_libusb(dev, vendor_id, product_id);
+		struct hid_device_info *tmp = hid_enumerate_from_libusb(dev, vendor_id, product_id, 0);
 		if (cur_dev) {
 			cur_dev->next = tmp;
 		}
@@ -1168,7 +1172,7 @@ static int hid_libusb_hotplug_callback(libusb_context *ctx, libusb_device *devic
 static void process_hotplug_event(struct hid_hotplug_queue* msg)
 {
 	if (msg->event == LIBUSB_HOTPLUG_EVENT_DEVICE_ARRIVED) {
-		struct hid_device_info* info = hid_enumerate_from_libusb(msg->device, 0, 0);
+		struct hid_device_info* info = hid_enumerate_from_libusb(msg->device, 0, 0, 1);
 		struct hid_device_info* info_cur = info;
 		while (info_cur) {
 			/* For each device, call all matching callbacks */

something like the following would solve my issue and is also suggested as workaround by @mcuee in https://github.com/libusb/libusb/issues/1586#issuecomment-2500248425 ```diff diff --git a/hidapi/libusb/hid.c b/hidapi/libusb/hid.c index ca8555c..b1c31c5 100644 --- a/hidapi/libusb/hid.c +++ b/hidapi/libusb/hid.c @@ -944,7 +944,7 @@ static int should_enumerate_interface(unsigned short vendor_id, const struct lib return 0; } -static struct hid_device_info* hid_enumerate_from_libusb(libusb_device *dev, unsigned short vendor_id, unsigned short product_id) +static struct hid_device_info* hid_enumerate_from_libusb(libusb_device *dev, unsigned short vendor_id, unsigned short product_id, int hotplug) { struct hid_device_info *root = NULL; /* return object */ struct hid_device_info *cur_dev = NULL; @@ -977,7 +977,11 @@ static struct hid_device_info* hid_enumerate_from_libusb(libusb_device *dev, uns if (should_enumerate_interface(dev_vid, intf_desc)) { struct hid_device_info *tmp; - res = libusb_open(dev, &handle); + /* after a hotplug event, retry it 5 time (max 50ms extra latency) */ + unsigned try = hotplug ? 6 : 1; + while (try-- && (res = libusb_open(dev, &handle)) == LIBUSB_ERROR_ACCESS) { + usleep(10000); + } #ifdef __ANDROID__ if (handle) { /* There is (a potential) libusb Android backend, in which @@ -1058,7 +1062,7 @@ struct hid_device_info HID_API_EXPORT *hid_enumerate(unsigned short vendor_id, u return NULL; while ((dev = devs[i++]) != NULL) { - struct hid_device_info *tmp = hid_enumerate_from_libusb(dev, vendor_id, product_id); + struct hid_device_info *tmp = hid_enumerate_from_libusb(dev, vendor_id, product_id, 0); if (cur_dev) { cur_dev->next = tmp; } @@ -1168,7 +1172,7 @@ static int hid_libusb_hotplug_callback(libusb_context *ctx, libusb_device *devic static void process_hotplug_event(struct hid_hotplug_queue* msg) { if (msg->event == LIBUSB_HOTPLUG_EVENT_DEVICE_ARRIVED) { - struct hid_device_info* info = hid_enumerate_from_libusb(msg->device, 0, 0); + struct hid_device_info* info = hid_enumerate_from_libusb(msg->device, 0, 0, 1); struct hid_device_info* info_cur = info; while (info_cur) { /* For each device, call all matching callbacks */ ```
mcuee (Migrated from github.com) reviewed 2024-11-27 02:20:38 -06:00
@ -915,0 +1083,4 @@
unsigned short dev_vid = desc.idVendor;
unsigned short dev_pid = desc.idProduct;
if ((vendor_id != 0x0 && vendor_id != dev_vid) ||
mcuee (Migrated from github.com) commented 2024-11-27 02:20:38 -06:00

I do not know anything about Go, but let me try out the Go-wrapper over the weekend to see if I can reproduce the issue under Linux or not.

I do not know anything about Go, but let me try out the Go-wrapper over the weekend to see if I can reproduce the issue under Linux or not.
bearsh commented 2025-04-22 08:22:07 -05:00 (Migrated from github.com)

is there something I can help with to get this merged?

is there something I can help with to get this merged?
k1-801 commented 2025-05-18 11:54:53 -05:00 (Migrated from github.com)

@Youw any chance the branch could be updated to the fresh master?
Also, is there any plan on actually finishing the feature & making it available in mainline? What's missing at this point and how can I help?

@Youw any chance the branch could be updated to the fresh master? Also, is there any plan on actually finishing the feature & making it available in mainline? What's missing at this point and how can I help?
Youw commented 2025-05-18 13:07:22 -05:00 (Migrated from github.com)

Sure, I'll update the branch.

Answering to last two comments: lets make a deal here: I'll start looking into this in more details very shortly (within days, at most weeks) - starting with the overall API design, etc. I'll post all my findings (and issues if any), and if anything needed to be adjusted/fixed - I'll probably be asking for help.

Also, I really hope we would have a good semi-automatic test for this as well (semi-automatic, since it would probably involve some physical device plug/unplug).

If all good when - will merge it into master.

Sure, I'll update the branch. Answering to last two comments: lets make a deal here: I'll start looking into this in more details very shortly (within days, at most weeks) - starting with the overall API design, etc. I'll post all my findings (and issues if any), and if anything needed to be adjusted/fixed - I'll probably be asking for help. Also, I really hope we would have a good semi-automatic test for this as well (semi-automatic, since it would probably involve some physical device plug/unplug). If all good when - will merge it into master.
k1-801 commented 2025-05-18 13:34:14 -05:00 (Migrated from github.com)

Sure, I'll update the branch.

Answering to last two comments: lets make a deal here: I'll start looking into this in more details very shortly (within days, at most weeks) - starting with the overall API design, etc. I'll post all my findings (and issues if any), and if anything needed to be adjusted/fixed - I'll probably be asking for help.

This doesn't really describe my side of the deal, but - sure, sounds good. I'll do what I can to help in getting this ready.

Also, I really hope we would have a good semi-automatic test for this as well (semi-automatic, since it would probably involve some physical device plug/unplug).

This PR does already feature an update to the hidtest utility. The update makes it interactive & allows to test the callbacks. I don't know how to make it any more automatic.

> Sure, I'll update the branch. > > Answering to last two comments: lets make a deal here: I'll start looking into this in more details very shortly (within days, at most weeks) - starting with the overall API design, etc. I'll post all my findings (and issues if any), and if anything needed to be adjusted/fixed - I'll probably be asking for help. This doesn't really describe my side of the deal, but - sure, sounds good. I'll do what I can to help in getting this ready. > Also, I really hope we would have a good semi-automatic test for this as well (semi-automatic, since it would probably involve some physical device plug/unplug). This PR does already feature an update to the hidtest utility. The update makes it interactive & allows to test the callbacks. I don't know how to make it any more automatic.
Youw commented 2025-05-18 14:05:34 -05:00 (Migrated from github.com)

Also, I really hope we would have a good semi-automatic test for this as well (semi-automatic, since it would probably involve some physical device plug/unplug).

This PR does already feature an update to the hidtest utility. The update makes it interactive & allows to test the callbacks. I don't know how to make it any more automatic.

  1. I was thinking having it as a separate test/executable, to keep current hidtest as a simple example
  2. A separate test which would guide the user what to do: plug/wait/unplug/etc. and would try to state if the implementation "passed" the test or "failed".

(No I haven't looke at what the modified hidtest currently does, if it already does something like that - that might be good-enought)

> > Also, I really hope we would have a good semi-automatic test for this as well (semi-automatic, since it would probably involve some physical device plug/unplug). > > This PR does already feature an update to the hidtest utility. The update makes it interactive & allows to test the callbacks. I don't know how to make it any more automatic. 1) I was thinking having it as a separate test/executable, to keep current hidtest as a simple example 2) A separate test which would guide the user what to do: plug/wait/unplug/etc. and would try to state if the implementation "passed" the test or "failed". (No I haven't looke at what the modified hidtest currently does, if it already does something like that - that might be good-enought)
Youw commented 2025-05-18 14:29:27 -05:00 (Migrated from github.com)

I've updated this branch to latest master and fixed conflicts (hopefully right).

Next step - I'll review the API and test the usability.

I've updated this branch to latest master and fixed conflicts (hopefully right). Next step - I'll review the API and test the usability.
k1-801 commented 2025-05-24 20:25:49 -05:00 (Migrated from github.com)
  • I was thinking having it as a separate test/executable, to keep current hidtest as a simple example
  • A separate test which would guide the user what to do: plug/wait/unplug/etc. and would try to state if the implementation "passed" the test or "failed".

I don't think my test app fits the description, and if the current hidtest is used in any automated testing scenario, I shall revert it to how it is in the current release. I'm not sure if the new interactive solution has any use, but it can be preserved under a different subfolder (will require a new cmake target, though).

As for the automated hotplug test, I'm not sure this is even necessary, but I can implement that too. I believe this can wait until after the review of the API - if the API is to change, the test will have to change too.

> * I was thinking having it as a separate test/executable, to keep current hidtest as a simple example > * A separate test which would guide the user what to do: plug/wait/unplug/etc. and would try to state if the implementation "passed" the test or "failed". I don't think my test app fits the description, and if the current hidtest is used in any automated testing scenario, I shall revert it to how it is in the current release. I'm not sure if the new interactive solution has any use, but it can be preserved under a different subfolder (will require a new cmake target, though). As for the automated hotplug test, I'm not sure this is even necessary, but I can implement that too. I believe this can wait until after the review of the API - if the API is to change, the test will have to change too.
awsms commented 2025-10-03 10:26:08 -05:00 (Migrated from github.com)

Next step - I'll review the API and test the usability.

Hi, any news since?

> Next step - I'll review the API and test the usability. Hi, any news since?
CalcProgrammer1 commented 2026-01-14 12:38:40 -06:00 (Migrated from github.com)

We really would like to use this functionality in OpenRGB's upcoming 1.0 release. Is there any plan to merge this into a hidapi release in the near future? If not, we plan on creating a fork called hidapi-hotplug (just hidapi master with the changes from this branch) and building hidapi-hotplug packages to ship alongside OpenRGB 1.0.

I do plan to make OpenRGB 1.0 buildable against standard non-hotplug hidapi but without hotplug capability. Hotplug capability would be a major improvement in user experience for us and @k1-801 has been pushing for it for years. I've been hesitant because of lack of upstream support but I really want to find a way to get this in and maintaining a (hopefully temporary) fork seems like the best action right now.

We really would like to use this functionality in OpenRGB's upcoming 1.0 release. Is there any plan to merge this into a hidapi release in the near future? If not, we plan on creating a fork called hidapi-hotplug (just hidapi master with the changes from this branch) and building hidapi-hotplug packages to ship alongside OpenRGB 1.0. I do plan to make OpenRGB 1.0 buildable against standard non-hotplug hidapi but without hotplug capability. Hotplug capability would be a major improvement in user experience for us and @k1-801 has been pushing for it for years. I've been hesitant because of lack of upstream support but I really want to find a way to get this in and maintaining a (hopefully temporary) fork seems like the best action right now.
Youw commented 2026-01-15 08:19:41 -06:00 (Migrated from github.com)

I failed to find capacity to review this once again, and I apologize for that.
As of right now at least I've updated the branch to latest master.
I do not want to make any promices as to when I get my hands to it (so I don't break that promice again), but this is on my task list, I'm not abandoning the idea to get this done.

I failed to find capacity to review this once again, and I apologize for that. As of right now at least I've updated the branch to latest master. I do not want to make any promices as to when I get my hands to it (so I don't break that promice again), but this _is_ on my task list, I'm not abandoning the idea to get this done.
bearsh commented 2026-01-15 08:56:03 -06:00 (Migrated from github.com)

@CalcProgrammer1 does this mean you have tested the hotplug implementation in-depth? Did you test it with the libusb backend?
I still think something like https://github.com/libusb/hidapi/pull/674#discussion_r1860010559 is needed to make it fully functional...

@CalcProgrammer1 does this mean you have tested the hotplug implementation in-depth? Did you test it with the libusb backend? I still think something like https://github.com/libusb/hidapi/pull/674#discussion_r1860010559 is needed to make it fully functional...
CalcProgrammer1 commented 2026-01-15 10:31:02 -06:00 (Migrated from github.com)

I personally haven't, I tested the hidraw implementation.
However, @k1-801's been working on this hotplug system for years and has
kept it continuously rebased, and his implementation had support for
libusb-based devices as well has hidraw (which I haven't pulled into mine
yet but intend to). There are a handful of devices with odd or broken HID
descriptors that Linux's hidraw driver fails to properly detect, but we can
still access via the libusb-based hidapi.

On Thu, Jan 15, 2026 at 8:56 AM bearsh @.***> wrote:

bearsh left a comment (libusb/hidapi#674)
https://github.com/libusb/hidapi/pull/674#issuecomment-3755264692

@CalcProgrammer1 https://github.com/CalcProgrammer1 does this mean you
have tested the hotplug implementation in-depth? Did you test it with the
libusb backend?
I still think something like #674 (comment)
https://github.com/libusb/hidapi/pull/674#discussion_r1860010559 is
needed to make it fully functional...

—
Reply to this email directly, view it on GitHub
https://github.com/libusb/hidapi/pull/674#issuecomment-3755264692, or
unsubscribe
https://github.com/notifications/unsubscribe-auth/AAIAY7GZGZZO36QVP46G2YT4G6TBVAVCNFSM6AAAAABF2KZ5Z2VHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZTONJVGI3DINRZGI
.
You are receiving this because you were mentioned.Message ID:
@.***>

I personally haven't, I tested the hidraw implementation. However, @k1-801's been working on this hotplug system for years and has kept it continuously rebased, and his implementation had support for libusb-based devices as well has hidraw (which I haven't pulled into mine yet but intend to). There are a handful of devices with odd or broken HID descriptors that Linux's hidraw driver fails to properly detect, but we can still access via the libusb-based hidapi. On Thu, Jan 15, 2026 at 8:56 AM bearsh ***@***.***> wrote: > *bearsh* left a comment (libusb/hidapi#674) > <https://github.com/libusb/hidapi/pull/674#issuecomment-3755264692> > > @CalcProgrammer1 <https://github.com/CalcProgrammer1> does this mean you > have tested the hotplug implementation in-depth? Did you test it with the > libusb backend? > I still think something like #674 (comment) > <https://github.com/libusb/hidapi/pull/674#discussion_r1860010559> is > needed to make it fully functional... > > — > Reply to this email directly, view it on GitHub > <https://github.com/libusb/hidapi/pull/674#issuecomment-3755264692>, or > unsubscribe > <https://github.com/notifications/unsubscribe-auth/AAIAY7GZGZZO36QVP46G2YT4G6TBVAVCNFSM6AAAAABF2KZ5Z2VHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZTONJVGI3DINRZGI> > . > You are receiving this because you were mentioned.Message ID: > ***@***.***> >
CalcProgrammer1 commented 2026-01-18 18:25:56 -06:00 (Migrated from github.com)

I've updated my next_hotplug branch again on OpenRGB, see:

https://gitlab.com/CalcProgrammer1/OpenRGB/-/merge_requests/3162

I pulled in support for libusb-wrapped devices and can confirm that the libusb backend also seems to work. For some reason, certain devices (like my HyperX Quadcast S microphone) use HID but have a descriptor that causes hidraw not to detect it, so the only way to access on Linux is using the libusb backend. We have a wrapper for these devices in OpenRGB, so that it normally links against the hidraw backend and uses that for most devices, but we dynamically load the libusb backend and use a wrapper to call its version of the functions for these select few problem devices.

With this latest push to next_hotplug, I can hotplug (add device on plug in, remove device on unplug) my HyperX Quadcast S microphone via the libusb wrapper as well as a variety of Razer devices I've been using to test the hidraw path. All is working well on Linux. Have not tested other OSes yet.

I've updated my next_hotplug branch again on OpenRGB, see: https://gitlab.com/CalcProgrammer1/OpenRGB/-/merge_requests/3162 I pulled in support for libusb-wrapped devices and can confirm that the libusb backend also seems to work. For some reason, certain devices (like my HyperX Quadcast S microphone) use HID but have a descriptor that causes hidraw not to detect it, so the only way to access on Linux is using the libusb backend. We have a wrapper for these devices in OpenRGB, so that it normally links against the hidraw backend and uses that for most devices, but we dynamically load the libusb backend and use a wrapper to call its version of the functions for these select few problem devices. With this latest push to next_hotplug, I can hotplug (add device on plug in, remove device on unplug) my HyperX Quadcast S microphone via the libusb wrapper as well as a variety of Razer devices I've been using to test the hidraw path. All is working well on Linux. Have not tested other OSes yet.
CalcProgrammer1 commented 2026-01-21 00:28:17 -06:00 (Migrated from github.com)

I built Windows and MacOS builds of my fork today and got successful builds of OpenRGB (next_hotplug branch) on Windows and MacOS. I've confirmed both are working, both inserting and removing.

I built Windows and MacOS builds of my fork today and got successful builds of OpenRGB (next_hotplug branch) on Windows and MacOS. I've confirmed both are working, both inserting and removing.
copilot-pull-request-reviewer[bot] (Migrated from github.com) reviewed 2026-03-01 17:46:19 -06:00
copilot-pull-request-reviewer[bot] (Migrated from github.com) left a comment

Pull request overview

This PR introduces cross-platform hotplug (device arrival/removal) support to hidapi, adding a public callback registration API and implementing platform-specific event backends (Windows cfgmgr32 notifications, macOS IOHIDManager, Linux udev monitor, libusb hotplug), plus extending hidtest to exercise the new functionality.

Changes:

  • Add new public hotplug API (hid_hotplug_register_callback / hid_hotplug_deregister_callback) and related types in hidapi.h.
  • Implement hotplug backends for Windows, macOS, Linux, and libusb; add NetBSD stubs.
  • Extend hidtest with an interactive hotplug testing mode and stress scenarios.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 13 comments.

Show a summary per file
File Description
hidapi/hidapi.h Adds the public hotplug API types, flags, callback signature, and function declarations.
windows/hidapi_cfgmgr32.h Adds cfgmgr32 notification type definitions and function pointer typedefs needed for Windows hotplug.
windows/hid.c Implements Windows hotplug registration, notification callback, and lifecycle cleanup.
mac/hid.c Implements macOS hotplug monitoring thread/runloop, callback list management, and device tracking.
linux/hid.c Implements Linux udev-based hotplug monitor thread and callback dispatch.
libusb/hid.c Implements libusb hotplug callback + internal event queue + callback dispatch thread model.
netbsd/hid.c Adds stub hotplug functions returning unsupported.
hidtest/test.c Adds interactive hotplug tests, keypress handling, and callback stress tests.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

## Pull request overview This PR introduces cross-platform hotplug (device arrival/removal) support to hidapi, adding a public callback registration API and implementing platform-specific event backends (Windows cfgmgr32 notifications, macOS IOHIDManager, Linux udev monitor, libusb hotplug), plus extending `hidtest` to exercise the new functionality. **Changes:** - Add new public hotplug API (`hid_hotplug_register_callback` / `hid_hotplug_deregister_callback`) and related types in `hidapi.h`. - Implement hotplug backends for Windows, macOS, Linux, and libusb; add NetBSD stubs. - Extend `hidtest` with an interactive hotplug testing mode and stress scenarios. ### Reviewed changes Copilot reviewed 8 out of 8 changed files in this pull request and generated 13 comments. <details> <summary>Show a summary per file</summary> | File | Description | | ---- | ----------- | | `hidapi/hidapi.h` | Adds the public hotplug API types, flags, callback signature, and function declarations. | | `windows/hidapi_cfgmgr32.h` | Adds cfgmgr32 notification type definitions and function pointer typedefs needed for Windows hotplug. | | `windows/hid.c` | Implements Windows hotplug registration, notification callback, and lifecycle cleanup. | | `mac/hid.c` | Implements macOS hotplug monitoring thread/runloop, callback list management, and device tracking. | | `linux/hid.c` | Implements Linux udev-based hotplug monitor thread and callback dispatch. | | `libusb/hid.c` | Implements libusb hotplug callback + internal event queue + callback dispatch thread model. | | `netbsd/hid.c` | Adds stub hotplug functions returning unsupported. | | `hidtest/test.c` | Adds interactive hotplug tests, keypress handling, and callback stress tests. | </details> --- 💡 <a href="/libusb/hidapi/new/master?filename=.github/instructions/*.instructions.md" class="Link--inTextBlock" target="_blank" rel="noopener noreferrer">Add Copilot custom instructions</a> for smarter, more guided reviews. <a href="https://docs.github.com/en/copilot/customizing-copilot/adding-repository-custom-instructions-for-github-copilot" class="Link--inTextBlock" target="_blank" rel="noopener noreferrer">Learn how to get started</a>.
@ -261,6 +261,324 @@ extern "C" {
*/
copilot-pull-request-reviewer[bot] (Migrated from github.com) commented 2026-03-01 17:46:18 -06:00

The documentation says it is safe to call hid_hotplug_deregister_callback() on an already deregistered callback. However, current backend implementations return -1 when the callback list is empty / not initialized, which contradicts that contract. Either adjust the implementations to treat “not found / already removed” as success (return 0), or update this documentation to match the actual behavior.

			and can be used to deregister callbacks. Callback handles are unique,
			but reusing a handle after it has been deregistered is not supported
			and may result in an error from hid_hotplug_deregister_callback().
The documentation says it is safe to call `hid_hotplug_deregister_callback()` on an already deregistered callback. However, current backend implementations return -1 when the callback list is empty / not initialized, which contradicts that contract. Either adjust the implementations to treat “not found / already removed” as success (return 0), or update this documentation to match the actual behavior. ```suggestion and can be used to deregister callbacks. Callback handles are unique, but reusing a handle after it has been deregistered is not supported and may result in an error from hid_hotplug_deregister_callback(). ```
@ -336,13 +348,238 @@ int main(int argc, char* argv[])
}
copilot-pull-request-reviewer[bot] (Migrated from github.com) commented 2026-03-01 17:46:18 -06:00

This printf string ends with \n]n, which looks like a typo and will print a stray ] instead of a newline sequence. It should likely be \n\n (or similar) to match the surrounding output formatting.

		printf("Compile-time version is different than runtime version of hidapi.\n\n");
This printf string ends with `\n]n`, which looks like a typo and will print a stray `]` instead of a newline sequence. It should likely be `\n\n` (or similar) to match the surrounding output formatting. ```suggestion printf("Compile-time version is different than runtime version of hidapi.\n\n"); ```
@ -915,0 +1086,4 @@
if ((vendor_id != 0x0 && vendor_id != dev_vid) ||
(product_id != 0x0 && product_id != dev_pid)) {
return NULL;
}
copilot-pull-request-reviewer[bot] (Migrated from github.com) commented 2026-03-01 17:46:17 -06:00

On LIBUSB_HOTPLUG_EVENT_DEVICE_LEFT handling, the code frees removed hid_device_info nodes with free(info). hid_device_info contains heap-allocated fields (path and strings), so this leaks memory. Since info->next is set to NULL, use hid_free_enumeration(info) to free the node and its fields correctly.

				/* Free every removed device (and its internal allocations) */
				hid_free_enumeration(info);
On LIBUSB_HOTPLUG_EVENT_DEVICE_LEFT handling, the code frees removed `hid_device_info` nodes with `free(info)`. `hid_device_info` contains heap-allocated fields (path and strings), so this leaks memory. Since `info->next` is set to NULL, use `hid_free_enumeration(info)` to free the node and its fields correctly. ```suggestion /* Free every removed device (and its internal allocations) */ hid_free_enumeration(info); ```
@ -1053,0 +1551,4 @@
int result = -1;
/* Remove this notification */
copilot-pull-request-reviewer[bot] (Migrated from github.com) commented 2026-03-01 17:46:17 -06:00

If calloc() for the queue message fails, the callback returns without releasing the earlier libusb_ref_device(device) reference, leaking a libusb device refcount. Ensure the failure path calls libusb_unref_device(device) before returning.

	if (NULL == msg) {
		libusb_unref_device(device);
If `calloc()` for the queue message fails, the callback returns without releasing the earlier `libusb_ref_device(device)` reference, leaking a libusb device refcount. Ensure the failure path calls `libusb_unref_device(device)` before returning. ```suggestion if (NULL == msg) { libusb_unref_device(device); ```
@ -1033,6 +1169,276 @@ void HID_API_EXPORT hid_free_enumeration(struct hid_device_info *devs)
}
copilot-pull-request-reviewer[bot] (Migrated from github.com) commented 2026-03-01 17:46:17 -06:00

On device removal handling in the udev hotplug thread, the code frees a removed hid_device_info node with free(info). This leaks the heap-allocated members of hid_device_info (path and strings) which are normally freed by hid_free_enumeration(). Since info->next is already set to NULL, use hid_free_enumeration(info) to free the node correctly.

							hid_free_enumeration(info);
On device removal handling in the udev hotplug thread, the code frees a removed `hid_device_info` node with `free(info)`. This leaks the heap-allocated members of `hid_device_info` (path and strings) which are normally freed by `hid_free_enumeration()`. Since `info->next` is already set to NULL, use `hid_free_enumeration(info)` to free the node correctly. ```suggestion hid_free_enumeration(info); ```
@ -467,0 +574,4 @@
if (!hid_hotplug_context.mutex_ready) {
/* Initialize the mutex as recursive */
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
copilot-pull-request-reviewer[bot] (Migrated from github.com) commented 2026-03-01 17:46:16 -06:00

hid_internal_hotplug_cleanup() calls pthread_join(hid_hotplug_context.thread, NULL) while it is intended to be invoked while holding hid_hotplug_context.mutex. This cleanup can be reached from IOHID callbacks that are executed on the hotplug thread itself; in that case, joining hid_hotplug_context.thread from within the same thread will deadlock. Guard against self-join (e.g., pthread_equal(pthread_self(), hid_hotplug_context.thread)) and/or redesign shutdown so the join happens from a different thread or the hotplug thread is detached.

	/* Wait for hotplug_thread() to end, avoiding self-join deadlock. */
	if (pthread_equal(pthread_self(), hid_hotplug_context.thread)) {
		/* If called from the hotplug thread itself, detach so resources are
		   reclaimed on exit, and do not attempt to join. */
		pthread_detach(hid_hotplug_context.thread);
	} else {
		pthread_join(hid_hotplug_context.thread, NULL);
	}
`hid_internal_hotplug_cleanup()` calls `pthread_join(hid_hotplug_context.thread, NULL)` while it is intended to be invoked while holding `hid_hotplug_context.mutex`. This cleanup can be reached from IOHID callbacks that are executed on the hotplug thread itself; in that case, joining `hid_hotplug_context.thread` from within the same thread will deadlock. Guard against self-join (e.g., `pthread_equal(pthread_self(), hid_hotplug_context.thread)`) and/or redesign shutdown so the join happens from a different thread or the hotplug thread is detached. ```suggestion /* Wait for hotplug_thread() to end, avoiding self-join deadlock. */ if (pthread_equal(pthread_self(), hid_hotplug_context.thread)) { /* If called from the hotplug thread itself, detach so resources are reclaimed on exit, and do not attempt to join. */ pthread_detach(hid_hotplug_context.thread); } else { pthread_join(hid_hotplug_context.thread, NULL); } ```
@ -564,1 +716,3 @@
if (cur_dev == NULL) {
/* A small trick to store an io_service_t tag along with hid_device_info for matching info with unplugged device */
cur_dev_ex = (struct hid_device_info_ex *)calloc(1, sizeof(struct hid_device_info_ex));
if (cur_dev_ex == NULL) {
copilot-pull-request-reviewer[bot] (Migrated from github.com) commented 2026-03-01 17:46:16 -06:00

calloc() result (cur_dev_ex) is dereferenced before being NULL-checked (cur_dev = &(cur_dev_ex->info);). If allocation fails, this is an immediate NULL dereference. Check cur_dev_ex for NULL before accessing its fields (and return NULL on failure).

	if (cur_dev_ex == NULL) {
		return NULL;
	}
	cur_dev = &(cur_dev_ex->info);
`calloc()` result (`cur_dev_ex`) is dereferenced before being NULL-checked (`cur_dev = &(cur_dev_ex->info);`). If allocation fails, this is an immediate NULL dereference. Check `cur_dev_ex` for NULL before accessing its fields (and return NULL on failure). ```suggestion if (cur_dev_ex == NULL) { return NULL; } cur_dev = &(cur_dev_ex->info); ```
copilot-pull-request-reviewer[bot] (Migrated from github.com) commented 2026-03-01 17:46:18 -06:00

hid_exit() only calls hid_internal_hotplug_exit() when hid_mgr is non-NULL. Since hotplug registration does not require hid_mgr to be initialized (and callers may rely on hid_exit() to clean up regardless), this can leak hotplug threads/mutex state. Call hid_internal_hotplug_exit() unconditionally (or at least whenever the hotplug context is initialized).

	}

	hid_internal_hotplug_exit();
`hid_exit()` only calls `hid_internal_hotplug_exit()` when `hid_mgr` is non-NULL. Since hotplug registration does not require `hid_mgr` to be initialized (and callers may rely on `hid_exit()` to clean up regardless), this can leak hotplug threads/mutex state. Call `hid_internal_hotplug_exit()` unconditionally (or at least whenever the hotplug context is initialized). ```suggestion } hid_internal_hotplug_exit(); ```
@ -813,1 +969,4 @@
static void hid_internal_invoke_callbacks(struct hid_device_info *info, hid_hotplug_event event)
{
pthread_mutex_lock(&hid_hotplug_context.mutex);
copilot-pull-request-reviewer[bot] (Migrated from github.com) commented 2026-03-01 17:46:17 -06:00

On device removal, the code detaches a hid_device_info node from hid_hotplug_context.devs and then frees it with free(info). Each hid_device_info contains heap-allocated fields (path, strings), so this leaks memory; also this list was built by create_device_info*() which expects hid_free_enumeration() for cleanup. Set info->next = NULL (already done) and call hid_free_enumeration(info) instead.

			hid_free_enumeration(info);
On device removal, the code detaches a `hid_device_info` node from `hid_hotplug_context.devs` and then frees it with `free(info)`. Each `hid_device_info` contains heap-allocated fields (path, strings), so this leaks memory; also this list was built by `create_device_info*()` which expects `hid_free_enumeration()` for cleanup. Set `info->next = NULL` (already done) and call `hid_free_enumeration(info)` instead. ```suggestion hid_free_enumeration(info); ```
copilot-pull-request-reviewer[bot] (Migrated from github.com) commented 2026-03-01 17:46:19 -06:00

match_ref_to_info() is a file-local helper but is not declared static, which unnecessarily exports a global symbol from this compilation unit (and increases the risk of symbol collisions). Make it static to keep it private to this file.

`match_ref_to_info()` is a file-local helper but is not declared `static`, which unnecessarily exports a global symbol from this compilation unit (and increases the risk of symbol collisions). Make it `static` to keep it private to this file.
copilot-pull-request-reviewer[bot] (Migrated from github.com) commented 2026-03-01 17:46:18 -06:00

When processing a device removal hotplug event, the code frees only the struct hid_device_info node with free(device), but hid_internal_get_device_info() allocates path, serial_number, manufacturer_string, and product_string. This leaks those allocations; use the existing hid_free_enumeration(device) (after ensuring device->next is NULL) to free the node and its fields correctly.

			/* Ensure only this node is freed by hid_free_enumeration() */
			device->next = NULL;
			hid_free_enumeration(device);
When processing a device removal hotplug event, the code frees only the `struct hid_device_info` node with `free(device)`, but `hid_internal_get_device_info()` allocates `path`, `serial_number`, `manufacturer_string`, and `product_string`. This leaks those allocations; use the existing `hid_free_enumeration(device)` (after ensuring `device->next` is NULL) to free the node and its fields correctly. ```suggestion /* Ensure only this node is freed by hid_free_enumeration() */ device->next = NULL; hid_free_enumeration(device); ```
copilot-pull-request-reviewer[bot] (Migrated from github.com) commented 2026-03-01 17:46:16 -06:00

In the first-callback registration path, hid_hotplug_context.devs = hid_enumerate(0, 0) can return NULL (e.g., if hid_init() / DLL resolution fails), but the function continues and calls CM_Register_Notification(...) anyway. That can crash in the failure case and also registers a callback even though initial enumeration failed. Consider calling hid_init() (and failing fast) at the start of hid_hotplug_register_callback(), and/or checking the result of hid_enumerate() and rolling back state on failure.

In the first-callback registration path, `hid_hotplug_context.devs = hid_enumerate(0, 0)` can return NULL (e.g., if `hid_init()` / DLL resolution fails), but the function continues and calls `CM_Register_Notification(...)` anyway. That can crash in the failure case and also registers a callback even though initial enumeration failed. Consider calling `hid_init()` (and failing fast) at the start of `hid_hotplug_register_callback()`, and/or checking the result of `hid_enumerate()` and rolling back state on failure.
copilot-pull-request-reviewer[bot] (Migrated from github.com) commented 2026-03-01 17:46:18 -06:00

On the error returns in the first-callback path (e.g., notify_handle != NULL or CM_Register_Notification(...) != CR_SUCCESS), the newly allocated hotplug_cb (and the devs list from hid_enumerate) are not freed, and hid_hotplug_context.hotplug_cbs may already be set to hotplug_cb. This leaves the hotplug context in a partially-initialized state and leaks memory; the error path should undo any state changes and free allocations before returning.

On the error returns in the first-callback path (e.g., `notify_handle != NULL` or `CM_Register_Notification(...) != CR_SUCCESS`), the newly allocated `hotplug_cb` (and the `devs` list from `hid_enumerate`) are not freed, and `hid_hotplug_context.hotplug_cbs` may already be set to `hotplug_cb`. This leaves the hotplug context in a partially-initialized state and leaks memory; the error path should undo any state changes and free allocations before returning.
k1-801 (Migrated from github.com) reviewed 2026-03-03 01:55:12 -06:00
@ -467,0 +574,4 @@
if (!hid_hotplug_context.mutex_ready) {
/* Initialize the mutex as recursive */
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
k1-801 (Migrated from github.com) commented 2026-03-03 01:55:12 -06:00

This one can be disregarded - not the intended use of the cleanup function.

This one can be disregarded - not the intended use of the cleanup function.
k1-801 (Migrated from github.com) reviewed 2026-03-06 06:46:02 -06:00
@ -261,6 +261,324 @@ extern "C" {
*/
k1-801 (Migrated from github.com) commented 2026-03-06 06:46:02 -06:00

"Safe" as in "won't crash, just returns an error", not as in "will succeed"

"Safe" as in "won't crash, just returns an error", not as in "will succeed"
Youw commented 2026-03-30 16:36:27 -05:00 (Migrated from github.com)

Some more documentation based on current implementation: https://github.com/libusb/hidapi/pull/784

Are we happy with this in general?

Some more documentation based on current implementation: https://github.com/libusb/hidapi/pull/784 Are we happy with this in general?
mcuee commented 2026-03-30 23:39:28 -05:00 (Migrated from github.com)

I think this is a good one to be merged to git master.

I think this is a good one to be merged to git master.
k1-801 commented 2026-03-30 23:54:55 -05:00 (Migrated from github.com)

Some more documentation based on current implementation: #784

image

I actually don't see any reason why it'd be not safe to call hid_open_path from a callback. Hotplug subsystem doesn't really intersect with what happens in device functions. Could you explain further, why it wouldn't be safe? The worst possible case is @bearsh 's scenario, but they already suggested a patch that works for them.

The only thing to really worry about processing directly in the callback is the callback potentially taking too long to process, but that's not related to the device opening call per se.

Additionally, the hid_device_info pointer may not be safe to preserve as-is outside the callback (and so are string pointers inside it), and THAT I'd put in the docs. Any info of interest in it should be copied, as the structure may be deleted unexpectedly after the callback ends. Found a mention of that.

> Some more documentation based on current implementation: #784 <img width="980" height="182" alt="image" src="https://github.com/user-attachments/assets/9eb870ca-9e91-4ad0-b783-9c7d9b683ea4" /> I actually don't see any reason why it'd be not safe to call `hid_open_path` from a callback. Hotplug subsystem doesn't really intersect with what happens in device functions. Could you explain further, why it wouldn't be safe? The worst possible case is @bearsh 's scenario, but they already suggested a patch that works for them. The only thing to really worry about processing directly in the callback is the callback potentially taking too long to process, but that's not related to the device opening call per se. ~~Additionally, the `hid_device_info` pointer may not be safe to preserve as-is outside the callback (and so are string pointers inside it), and THAT I'd put in the docs. Any info of interest in it should be copied, as the structure may be deleted unexpectedly after the callback ends.~~ Found a mention of that.
mcuee commented 2026-03-31 00:02:36 -05:00 (Migrated from github.com)

#784 documentation and hidtest.c can be improved along the way for hotplug. This PR can go in first.

#784 documentation and `hidtest.c` can be improved along the way for hotplug. This PR can go in first. * #784
mcuee (Migrated from github.com) approved these changes 2026-03-31 00:03:44 -05:00
mcuee commented 2026-03-31 00:06:13 -05:00 (Migrated from github.com)

@Youw

I think you can mark this PR as "Ready for review".

@Youw I think you can mark this PR as "Ready for review".
mcuee commented 2026-03-31 00:13:17 -05:00 (Migrated from github.com)

Minor warning when building under VS2026. Not related to this PR though (same for git master).

Maybe it is related to by build command (adapted from github action script). Not so sure here.

PS C:\work\libusb\hidapi_hotplug> .\build_msvc.cmd

C:\work\libusb\hidapi_hotplug>cmake  -B build_msvc/shared -S . -DCMAKE_BUILD_TYPE=RelWithDebInfo -DCMAKE_INSTALL_PREFIX=install/shared -DHIDAPI_WITH_TESTS=ON -DHIDAPI_BUILD_PP_DATA_DUMP=ON -DHIDAPI_BUILD_HIDTEST=ON
-- Building for: Visual Studio 18 2026
-- The C compiler identification is MSVC 19.50.35728.0
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working C compiler: C:/Program Files/Microsoft Visual Studio/18/Community/VC/Tools/MSVC/14.50.35717/bin/Hostx64/x64/cl.exe - skipped
-- Detecting C compile features
-- Detecting C compile features - done
-- hidapi: v0.16.0
-- Building hidtest
-- Configuring done (5.5s)
-- Generating done (0.3s)
-- Build files have been written to: C:/work/libusb/hidapi_hotplug/build_msvc/shared

C:\work\libusb\hidapi_hotplug>cmake --build build_msvc/shared --config RelWithDebInfo
MSBuild version 18.4.0+6e61e96ac for .NET Framework

  1>Checking Build System
  Building Custom Rule C:/work/libusb/hidapi_hotplug/windows/CMakeLists.txt
  hid.c
  hidapi_descriptor_reconstruct.c
  Generating Code...
     Creating library C:/work/libusb/hidapi_hotplug/build_msvc/shared/src/windows/RelWithDebInfo/hidapi.lib and object
  C:/work/libusb/hidapi_hotplug/build_msvc/shared/src/windows/RelWithDebInfo/hidapi.exp
  hidapi_winapi.vcxproj -> C:\work\libusb\hidapi_hotplug\build_msvc\shared\src\windows\RelWithDebInfo\hidapi.dll
  Building Custom Rule C:/work/libusb/hidapi_hotplug/windows/test/CMakeLists.txt
  hid_report_reconstructor_test.c
  hid_report_reconstructor_test.vcxproj -> C:\work\libusb\hidapi_hotplug\build_msvc\shared\src\windows\test\RelWithDebI
  nfo\hid_report_reconstructor_test.exe
  Building Custom Rule C:/work/libusb/hidapi_hotplug/hidtest/CMakeLists.txt
  test.c
  hidtest.vcxproj -> C:\work\libusb\hidapi_hotplug\build_msvc\shared\hidtest\RelWithDebInfo\hidtest.exe
  Building Custom Rule C:/work/libusb/hidapi_hotplug/windows/pp_data_dump/CMakeLists.txt
  pp_data_dump.c
     Creating library C:/work/libusb/hidapi_hotplug/build_msvc/shared/src/windows/pp_data_dump/RelWithDebInfo/pp_data_d
  ump.lib and object C:/work/libusb/hidapi_hotplug/build_msvc/shared/src/windows/pp_data_dump/RelWithDebInfo/pp_data_du
  mp.exp
  pp_data_dump.vcxproj -> C:\work\libusb\hidapi_hotplug\build_msvc\shared\src\windows\pp_data_dump\RelWithDebInfo\pp_da
  ta_dump.exe
  Building Custom Rule C:/work/libusb/hidapi_hotplug/CMakeLists.txt

C:\work\libusb\hidapi_hotplug>cmake  -B build_msvc/static -S . -DCMAKE_BUILD_TYPE=RelWithDebInfo -DCMAKE_INSTALL_PREFIX=install/static -DBUILD_SHARED_LIBS=FALSE -DHIDAPI_WITH_TESTS=ON -DHIDAPI_BUILD_PP_DATA_DUMP=ON -DHIDAPI_BUILD_HIDTEST=ON
-- Building for: Visual Studio 18 2026
-- The C compiler identification is MSVC 19.50.35728.0
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working C compiler: C:/Program Files/Microsoft Visual Studio/18/Community/VC/Tools/MSVC/14.50.35717/bin/Hostx64/x64/cl.exe - skipped
-- Detecting C compile features
-- Detecting C compile features - done
-- hidapi: v0.16.0
-- Building hidtest
-- Configuring done (3.2s)
-- Generating done (0.3s)
-- Build files have been written to: C:/work/libusb/hidapi_hotplug/build_msvc/static

C:\work\libusb\hidapi_hotplug>cmake --build build_msvc/static --config RelWithDebInfo
MSBuild version 18.4.0+6e61e96ac for .NET Framework

  1>Checking Build System
  Building Custom Rule C:/work/libusb/hidapi_hotplug/windows/CMakeLists.txt
  hid.c
  hidapi_descriptor_reconstruct.c
  Generating Code...
hid.obj : warning LNK4006: hid_winapi_descriptor_reconstruct_pp_data already defined in hidapi_descriptor_reconstruct.o
bj; second definition ignored [C:\work\libusb\hidapi_hotplug\build_msvc\static\src\windows\hidapi_winapi.vcxproj]
  hidapi_winapi.vcxproj -> C:\work\libusb\hidapi_hotplug\build_msvc\static\src\windows\RelWithDebInfo\hidapi.lib
  Building Custom Rule C:/work/libusb/hidapi_hotplug/windows/test/CMakeLists.txt
  hid_report_reconstructor_test.c
  hid_report_reconstructor_test.vcxproj -> C:\work\libusb\hidapi_hotplug\build_msvc\static\src\windows\test\RelWithDebI
  nfo\hid_report_reconstructor_test.exe
  Building Custom Rule C:/work/libusb/hidapi_hotplug/hidtest/CMakeLists.txt
  test.c
  hidtest.vcxproj -> C:\work\libusb\hidapi_hotplug\build_msvc\static\hidtest\RelWithDebInfo\hidtest.exe
  Building Custom Rule C:/work/libusb/hidapi_hotplug/windows/pp_data_dump/CMakeLists.txt
  pp_data_dump.c
  pp_data_dump.vcxproj -> C:\work\libusb\hidapi_hotplug\build_msvc\static\src\windows\pp_data_dump\RelWithDebInfo\pp_da
  ta_dump.exe
  Building Custom Rule C:/work/libusb/hidapi_hotplug/CMakeLists.txt
Minor warning when building under VS2026. Not related to this PR though (same for git master). Maybe it is related to by build command (adapted from github action script). Not so sure here. ``` PS C:\work\libusb\hidapi_hotplug> .\build_msvc.cmd C:\work\libusb\hidapi_hotplug>cmake -B build_msvc/shared -S . -DCMAKE_BUILD_TYPE=RelWithDebInfo -DCMAKE_INSTALL_PREFIX=install/shared -DHIDAPI_WITH_TESTS=ON -DHIDAPI_BUILD_PP_DATA_DUMP=ON -DHIDAPI_BUILD_HIDTEST=ON -- Building for: Visual Studio 18 2026 -- The C compiler identification is MSVC 19.50.35728.0 -- Detecting C compiler ABI info -- Detecting C compiler ABI info - done -- Check for working C compiler: C:/Program Files/Microsoft Visual Studio/18/Community/VC/Tools/MSVC/14.50.35717/bin/Hostx64/x64/cl.exe - skipped -- Detecting C compile features -- Detecting C compile features - done -- hidapi: v0.16.0 -- Building hidtest -- Configuring done (5.5s) -- Generating done (0.3s) -- Build files have been written to: C:/work/libusb/hidapi_hotplug/build_msvc/shared C:\work\libusb\hidapi_hotplug>cmake --build build_msvc/shared --config RelWithDebInfo MSBuild version 18.4.0+6e61e96ac for .NET Framework 1>Checking Build System Building Custom Rule C:/work/libusb/hidapi_hotplug/windows/CMakeLists.txt hid.c hidapi_descriptor_reconstruct.c Generating Code... Creating library C:/work/libusb/hidapi_hotplug/build_msvc/shared/src/windows/RelWithDebInfo/hidapi.lib and object C:/work/libusb/hidapi_hotplug/build_msvc/shared/src/windows/RelWithDebInfo/hidapi.exp hidapi_winapi.vcxproj -> C:\work\libusb\hidapi_hotplug\build_msvc\shared\src\windows\RelWithDebInfo\hidapi.dll Building Custom Rule C:/work/libusb/hidapi_hotplug/windows/test/CMakeLists.txt hid_report_reconstructor_test.c hid_report_reconstructor_test.vcxproj -> C:\work\libusb\hidapi_hotplug\build_msvc\shared\src\windows\test\RelWithDebI nfo\hid_report_reconstructor_test.exe Building Custom Rule C:/work/libusb/hidapi_hotplug/hidtest/CMakeLists.txt test.c hidtest.vcxproj -> C:\work\libusb\hidapi_hotplug\build_msvc\shared\hidtest\RelWithDebInfo\hidtest.exe Building Custom Rule C:/work/libusb/hidapi_hotplug/windows/pp_data_dump/CMakeLists.txt pp_data_dump.c Creating library C:/work/libusb/hidapi_hotplug/build_msvc/shared/src/windows/pp_data_dump/RelWithDebInfo/pp_data_d ump.lib and object C:/work/libusb/hidapi_hotplug/build_msvc/shared/src/windows/pp_data_dump/RelWithDebInfo/pp_data_du mp.exp pp_data_dump.vcxproj -> C:\work\libusb\hidapi_hotplug\build_msvc\shared\src\windows\pp_data_dump\RelWithDebInfo\pp_da ta_dump.exe Building Custom Rule C:/work/libusb/hidapi_hotplug/CMakeLists.txt C:\work\libusb\hidapi_hotplug>cmake -B build_msvc/static -S . -DCMAKE_BUILD_TYPE=RelWithDebInfo -DCMAKE_INSTALL_PREFIX=install/static -DBUILD_SHARED_LIBS=FALSE -DHIDAPI_WITH_TESTS=ON -DHIDAPI_BUILD_PP_DATA_DUMP=ON -DHIDAPI_BUILD_HIDTEST=ON -- Building for: Visual Studio 18 2026 -- The C compiler identification is MSVC 19.50.35728.0 -- Detecting C compiler ABI info -- Detecting C compiler ABI info - done -- Check for working C compiler: C:/Program Files/Microsoft Visual Studio/18/Community/VC/Tools/MSVC/14.50.35717/bin/Hostx64/x64/cl.exe - skipped -- Detecting C compile features -- Detecting C compile features - done -- hidapi: v0.16.0 -- Building hidtest -- Configuring done (3.2s) -- Generating done (0.3s) -- Build files have been written to: C:/work/libusb/hidapi_hotplug/build_msvc/static C:\work\libusb\hidapi_hotplug>cmake --build build_msvc/static --config RelWithDebInfo MSBuild version 18.4.0+6e61e96ac for .NET Framework 1>Checking Build System Building Custom Rule C:/work/libusb/hidapi_hotplug/windows/CMakeLists.txt hid.c hidapi_descriptor_reconstruct.c Generating Code... hid.obj : warning LNK4006: hid_winapi_descriptor_reconstruct_pp_data already defined in hidapi_descriptor_reconstruct.o bj; second definition ignored [C:\work\libusb\hidapi_hotplug\build_msvc\static\src\windows\hidapi_winapi.vcxproj] hidapi_winapi.vcxproj -> C:\work\libusb\hidapi_hotplug\build_msvc\static\src\windows\RelWithDebInfo\hidapi.lib Building Custom Rule C:/work/libusb/hidapi_hotplug/windows/test/CMakeLists.txt hid_report_reconstructor_test.c hid_report_reconstructor_test.vcxproj -> C:\work\libusb\hidapi_hotplug\build_msvc\static\src\windows\test\RelWithDebI nfo\hid_report_reconstructor_test.exe Building Custom Rule C:/work/libusb/hidapi_hotplug/hidtest/CMakeLists.txt test.c hidtest.vcxproj -> C:\work\libusb\hidapi_hotplug\build_msvc\static\hidtest\RelWithDebInfo\hidtest.exe Building Custom Rule C:/work/libusb/hidapi_hotplug/windows/pp_data_dump/CMakeLists.txt pp_data_dump.c pp_data_dump.vcxproj -> C:\work\libusb\hidapi_hotplug\build_msvc\static\src\windows\pp_data_dump\RelWithDebInfo\pp_da ta_dump.exe Building Custom Rule C:/work/libusb/hidapi_hotplug/CMakeLists.txt ```
bearsh commented 2026-03-31 01:00:11 -05:00 (Migrated from github.com)

would it be possible to include the patch from https://github.com/libusb/hidapi/pull/674#discussion_r1860010559? or should I submit a separate PR once this is merged?

would it be possible to include the patch from https://github.com/libusb/hidapi/pull/674#discussion_r1860010559? or should I submit a separate PR once this is merged?
k1-801 commented 2026-03-31 05:30:24 -05:00 (Migrated from github.com)

would it be possible to include the patch from #674 (comment)? or should I submit a separate PR once this is merged?

I agree that this might be a worthy addition, if during testing it revealed such a problem.

> would it be possible to include the patch from [#674 (comment)](https://github.com/libusb/hidapi/pull/674#discussion_r1860010559)? or should I submit a separate PR once this is merged? I agree that this might be a worthy addition, if during testing it revealed such a problem.
Youw (Migrated from github.com) reviewed 2026-03-31 05:47:54 -05:00
@ -915,0 +1083,4 @@
unsigned short dev_vid = desc.idVendor;
unsigned short dev_pid = desc.idProduct;
if ((vendor_id != 0x0 && vendor_id != dev_vid) ||
Youw (Migrated from github.com) commented 2026-03-31 05:47:54 -05:00

A general rule: a retry w/o knowing exactly a reason for the behavior is not a good thing - that is a high indication somewhere is a bug, or something fundamentally done wrong...
In any case - lets have it as a separate issue/PR, this PR has too many comments/threads, things easily get lost...

A general rule: a retry w/o knowing exactly a reason for the behavior is not a good thing - that is a high indication somewhere is a bug, or something fundamentally done wrong... In any case - **lets have it as a separate issue/PR**, this PR has too many comments/threads, things easily get lost...
Youw commented 2026-03-31 06:02:20 -05:00 (Migrated from github.com)

@k1-801 lets continue the conversation her: https://github.com/libusb/hidapi/pull/784/

@bearsh see: https://github.com/libusb/hidapi/pull/674#discussion_r3015074686

Before marking this a Ready for Review/Merging to master - at least https://github.com/libusb/hidapi/pull/783 needs to be reviewed/merged

@k1-801 lets continue the conversation her: https://github.com/libusb/hidapi/pull/784/ @bearsh see: https://github.com/libusb/hidapi/pull/674#discussion_r3015074686 Before marking this a Ready for Review/Merging to master - at least https://github.com/libusb/hidapi/pull/783 needs to be reviewed/merged
Youw (Migrated from github.com) reviewed 2026-03-31 06:28:00 -05:00
@ -261,6 +261,324 @@ extern "C" {
*/
Youw (Migrated from github.com) commented 2026-03-31 06:28:00 -05:00

while we want this to be true, I have a few conserns:

  1. why do we wan't to make this possible? one could simply return non-zero from a callback function, and that would effectively deregister, w/o a need to call deregister manually...

  2. register/deregister functions are not really thread-safe with regard to each other, mostly because context/mutex initisalisation in not thread-safe. even if we allow calling deregister from within the callback, there should be a big NOTE regarding the fact, that all calls to register/deregister, including the one from withing the callback function - should be serialized / protected by a mutex or so.

while we want this to be true, I have a few conserns: 1) why do we wan't to make this possible? one could simply return non-zero from a callback function, and that would effectively deregister, w/o a need to call `deregister` manually... 2) `register`/`deregister` functions are not really thread-safe with regard to each other, mostly because context/mutex initisalisation in not thread-safe. even if we allow calling `deregister` from within the callback, there should be a big NOTE regarding the fact, that all calls to `register`/`deregister`, including the one from withing the callback function - should be serialized / protected by a mutex or so.
k1-801 (Migrated from github.com) reviewed 2026-03-31 06:39:06 -05:00
@ -261,6 +261,324 @@ extern "C" {
*/
k1-801 (Migrated from github.com) commented 2026-03-31 06:39:06 -05:00
  1. one could simply return non-zero from a callback function, and that would effectively deregister, w/o a need to call deregister manually

It could be deregistering a different callback than the one currently running, OR the user could need to perform certain actions after the callback is deregistered.

> 1. one could simply return non-zero from a callback function, and that would effectively deregister, w/o a need to call `deregister` manually It could be deregistering a different callback than the one currently running, OR the user could need to perform certain actions after the callback is deregistered.
k1-801 (Migrated from github.com) reviewed 2026-03-31 06:40:52 -05:00
@ -261,6 +261,324 @@ extern "C" {
*/
k1-801 (Migrated from github.com) commented 2026-03-31 06:40:52 -05:00

2. there should be a big NOTE regarding the fact, that all calls to register/deregister, including the one from withing the callback function - should be serialized / protected by a mutex or so.

True. We do need such a note.
Some thread safety did actually exist there and was removed later during review.

> 2\. there should be a big NOTE regarding the fact, that all calls to `register`/`deregister`, including the one from withing the callback function - should be serialized / protected by a mutex or so. True. We do need such a note. Some thread safety did actually exist there and was removed later during review.
Youw (Migrated from github.com) reviewed 2026-03-31 06:53:50 -05:00
@ -467,0 +574,4 @@
if (!hid_hotplug_context.mutex_ready) {
/* Initialize the mutex as recursive */
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
Youw (Migrated from github.com) commented 2026-03-31 06:53:49 -05:00

This cleanup can be reached from IOHID callbacks that are executed on the hotplug thread itself

@copilot elaborate in which scenario ths is possible

> This cleanup can be reached from IOHID callbacks that are executed on the hotplug thread itself @copilot elaborate in which scenario ths is possible
copilot-swe-agent[bot] (Migrated from github.com) reviewed 2026-03-31 06:56:19 -05:00
@ -467,0 +574,4 @@
if (!hid_hotplug_context.mutex_ready) {
/* Initialize the mutex as recursive */
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
copilot-swe-agent[bot] (Migrated from github.com) commented 2026-03-31 06:56:19 -05:00

Here's the exact call chain that causes the deadlock:

The IOHIDManager is scheduled with CFRunLoopGetCurrent() inside hotplug_thread() (line 1113), so all IOHID removal callbacks run on the hotplug thread.

Scenario — a user callback self-deregisters on device removal:

hotplug_thread()
  → CFRunLoopRunInMode()                          ← hotplug thread's run loop
    → hid_internal_hotplug_disconnect_callback()  ← IOHID callback, on hotplug thread
        → pthread_mutex_lock() [recursive OK]
        → hid_internal_invoke_callbacks()
            → user_callback() returns non-zero     ← self-deregister
            → callback->events = 0, cb_list_dirty = 1
            → mutex_in_use = 0                     ← guard reset
            → hid_internal_hotplug_remove_postponed()
              → callback freed, hotplug_cbs == NULL
        → hid_internal_hotplug_cleanup()           ← line 1089
            → mutex_in_use == 0  (guard passes)
            → hotplug_cbs == NULL (proceeds)
            → thread_state = 2, signal run loop
            → pthread_join(hid_hotplug_context.thread, NULL)
              ← *** CALLED FROM WITHIN hotplug_thread ITSELF ***
              ← DEADLOCK (POSIX: self-join is undefined behavior / will hang)

The key: hid_internal_invoke_callbacks() resets mutex_in_use to 0 before returning (line 997), so the mutex_in_use guard in hid_internal_hotplug_cleanup() (line 553) does not prevent the join when the path comes from an IOHID callback.

The fix is to guard against self-join using pthread_equal(pthread_self(), hid_hotplug_context.thread) as suggested — if we're already on the hotplug thread, use pthread_detach instead so resources are reclaimed on exit.

Here's the exact call chain that causes the deadlock: The IOHIDManager is scheduled with `CFRunLoopGetCurrent()` inside `hotplug_thread()` (line 1113), so **all IOHID removal callbacks run on the hotplug thread**. Scenario — a user callback self-deregisters on device removal: ``` hotplug_thread() → CFRunLoopRunInMode() ← hotplug thread's run loop → hid_internal_hotplug_disconnect_callback() ← IOHID callback, on hotplug thread → pthread_mutex_lock() [recursive OK] → hid_internal_invoke_callbacks() → user_callback() returns non-zero ← self-deregister → callback->events = 0, cb_list_dirty = 1 → mutex_in_use = 0 ← guard reset → hid_internal_hotplug_remove_postponed() → callback freed, hotplug_cbs == NULL → hid_internal_hotplug_cleanup() ← line 1089 → mutex_in_use == 0 (guard passes) → hotplug_cbs == NULL (proceeds) → thread_state = 2, signal run loop → pthread_join(hid_hotplug_context.thread, NULL) ← *** CALLED FROM WITHIN hotplug_thread ITSELF *** ← DEADLOCK (POSIX: self-join is undefined behavior / will hang) ``` The key: `hid_internal_invoke_callbacks()` resets `mutex_in_use` to 0 before returning (line 997), so the `mutex_in_use` guard in `hid_internal_hotplug_cleanup()` (line 553) does **not** prevent the join when the path comes from an IOHID callback. The fix is to guard against self-join using `pthread_equal(pthread_self(), hid_hotplug_context.thread)` as suggested — if we're already on the hotplug thread, use `pthread_detach` instead so resources are reclaimed on exit.
Youw (Migrated from github.com) reviewed 2026-03-31 08:18:01 -05:00
Youw (Migrated from github.com) commented 2026-03-31 08:18:01 -05:00
https://github.com/libusb/hidapi/issues/785
Youw (Migrated from github.com) reviewed 2026-03-31 08:30:38 -05:00
@ -467,0 +574,4 @@
if (!hid_hotplug_context.mutex_ready) {
/* Initialize the mutex as recursive */
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
Youw (Migrated from github.com) commented 2026-03-31 08:30:38 -05:00

this is not to be disregarded
I'd even say we're back to a question - do we really want to allow deregister from a callback thread - things like this are dangerous and wer're luch we found it during the review and not later

this is not to be disregarded I'd even say we're back to a question - do we really want to allow deregister from a callback thread - things like this are dangerous and wer're luch we found it during the review and not later
Youw (Migrated from github.com) reviewed 2026-03-31 08:37:07 -05:00
Youw (Migrated from github.com) commented 2026-03-31 08:37:07 -05:00
moved conversation here: https://github.com/libusb/hidapi/issues/786
Youw (Migrated from github.com) reviewed 2026-03-31 14:28:15 -05:00
@ -261,6 +261,324 @@ extern "C" {
*/
Youw (Migrated from github.com) commented 2026-03-31 14:28:15 -05:00

There is also things like: https://github.com/libusb/hidapi/pull/674#discussion_r3015415035 that needs to be resolved first, and taken into account in general

There is also things like: https://github.com/libusb/hidapi/pull/674#discussion_r3015415035 that needs to be resolved first, and taken into account in general
k1-801 (Migrated from github.com) reviewed 2026-04-01 07:02:08 -05:00
@ -467,0 +574,4 @@
if (!hid_hotplug_context.mutex_ready) {
/* Initialize the mutex as recursive */
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
k1-801 (Migrated from github.com) commented 2026-04-01 07:02:08 -05:00

I'd even say we're back to a question - do we really want to allow deregister from a callback thread

I'm okay with removing the comment, even if such a scenario wouldn't cause issues on most platforms. Support for this specific use case was added explicitly (and IIRC is even checked in hidtest now), and it can be theoretically removed with express purpose of forbidding this (which could in theory cause deadlocks), though I would see that as an overkill.

Solving the issue on MacOS would be a better solution, even if we don't see a use for this use case at the moment.

> I'd even say we're back to a question - do we really want to allow deregister from a callback thread I'm okay with removing the comment, even if such a scenario wouldn't cause issues on most platforms. Support for this specific use case was added explicitly (and IIRC is even checked in hidtest now), and it can be theoretically removed with express purpose of forbidding this (which could in theory cause deadlocks), though I would see that as an overkill. Solving the issue on MacOS would be a better solution, even if we don't see a use for this use case *at the moment*.
zakalawe commented 2026-04-15 06:24:49 -05:00 (Migrated from github.com)

Would also be delighted to see this land, FWIW

Would also be delighted to see this land, FWIW
Youw commented 2026-04-24 15:08:19 -05:00 (Migrated from github.com)

I'll refine the suggested behavior a bit in scope of https://github.com/libusb/hidapi/pull/790 (this is documentation-only) - and will make corresponding changes to the code later.

I'll refine the suggested behavior a bit in scope of https://github.com/libusb/hidapi/pull/790 (this is documentation-only) - and will make corresponding changes to the code later.
CalcProgrammer1 commented 2026-07-13 10:08:23 -05:00 (Migrated from github.com)

Any update on hotplug? Is it going to make it into a release anytime soon? I have merged OpenRGB next into master, currently depending on our hidapi-hotplug fork to support hotplugging. I would prefer if there was an official release we could link to but I don't want to delay OpenRGB 1.0 much longer.

Any update on hotplug? Is it going to make it into a release anytime soon? I have merged OpenRGB next into master, currently depending on our hidapi-hotplug fork to support hotplugging. I would prefer if there was an official release we could link to but I don't want to delay OpenRGB 1.0 much longer.
Youw commented 2026-07-13 10:22:20 -05:00 (Migrated from github.com)

I'll spend some time on it right now, but no promices.
As of right now, using a fork for OpenRGB release is a good compromise, but as of the official support - I see too many issues (including documentation/claims) to safely call it "ready for release".

I'll spend some time on it right now, but no promices. As of right now, using a fork for OpenRGB release is a good compromise, but as of the official support - I see too many issues (including documentation/claims) to safely call it "ready for release".
This pull request is marked as a work in progress.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin 'connection-callback:connection-callback'
git switch 'connection-callback'

Merge

Merge the changes and update on Forgejo.

Warning: The "Autodetect manual merge" setting is not enabled for this repository, you will have to mark this pull request as manually merged afterwards.

git switch 'master'
git merge --no-ff 'connection-callback'
git switch 'connection-callback'
git rebase 'master'
git switch 'master'
git merge --ff-only 'connection-callback'
git switch 'connection-callback'
git rebase 'master'
git switch 'master'
git merge --no-ff 'connection-callback'
git switch 'master'
git merge --squash 'connection-callback'
git switch 'master'
git merge --ff-only 'connection-callback'
git switch 'master'
git merge 'connection-callback'
git push origin 'master'
Sign in to join this conversation.
No reviewers
No milestone
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
gh-libusb/hidapi!674
No description provided.