Potential solution to MacOS hid_init/hid_exit issue #666

Open
opened 2024-03-11 15:05:24 -05:00 by k1-801 · 8 comments
k1-801 commented 2024-03-11 15:05:24 -05:00 (Migrated from github.com)

One of the known thread-safety-related limitations on MacOS is that hid_init and hid_exit have to be called on the same thread. The only thing hid_exit really does there is destroy a HidManager. We could just leave that on the end user's shoulders, or we could resolve it internally. The problem might only arise if the user's application accidentally runs hid_init in a side thread before the main thread even gets a chance to call it (example: a static object constructor in C++ might do that).

We can't, of course, track the thread it was created in to ensure it closes in it - but, theoretically, this issue could be easily mitigated by giving the manager it's own thread to live in, which apparently would always be suspended by the sleeping RunLoop waiting for events, sacrificing zero processing power and only a small amount of ram. The manager would be created AND destroyed within the thread's function, guaranteed to be in the same thread. hid_init would have to start the thread and then wait at a barrier for the manager to be created. Then hid_exit would essentially be built the same way as hid_close, sending an event to stop the thread. That's about it.

One of the known thread-safety-related limitations on MacOS is that hid_init and hid_exit have to be called on the same thread. The only thing hid_exit really does there is destroy a HidManager. We could just leave that on the end user's shoulders, or we could resolve it internally. The problem might only arise if the user's application accidentally runs hid_init in a side thread before the main thread even gets a chance to call it (example: a static object constructor in C++ might do that). We can't, of course, track the thread it was created in to ensure it closes in it - but, theoretically, this issue could be easily mitigated by giving the manager it's own thread to live in, which apparently would always be suspended by the sleeping RunLoop waiting for events, sacrificing zero processing power and only a small amount of ram. The manager would be created AND destroyed within the thread's function, guaranteed to be in the same thread. hid_init would have to start the thread and then wait at a barrier for the manager to be created. Then hid_exit would essentially be built the same way as hid_close, sending an event to stop the thread. That's about it.
Youw commented 2024-04-06 04:14:52 -05:00 (Migrated from github.com)

All true.

I was thinking about having its own thread for init/deinit on macOS.
Just never had a chance to get it done.

All true. I was thinking about having its own thread for init/deinit on macOS. Just never had a chance to get it done.
tresf commented 2024-06-07 11:53:15 -05:00 (Migrated from github.com)

We could just leave that on the end user's shoulders

Ran into this recently after some new threading was added to a downstream application. We decided to wrap calls documented here https://github.com/libusb/hidapi/issues/45#issuecomment-1546803030 into a single thread, which seems to have fixed the issue. Since we're actually downstream of downstream, we submitted the patch to the library we depend upon that wraps hidapi functionality. I'm not sure if the patch will be accepted, but I wanted to offer gratitude for documenting this.

> We could just leave that on the end user's shoulders Ran into this recently after some new threading was added to a downstream application. We decided to wrap calls documented here https://github.com/libusb/hidapi/issues/45#issuecomment-1546803030 into a single thread, which seems to have fixed the issue. Since we're actually downstream of downstream, we submitted the patch to the library we depend upon that wraps `hidapi` functionality. I'm not sure if the patch will be accepted, but I wanted to offer gratitude for documenting this.
marciopamplona commented 2024-07-19 07:53:39 -05:00 (Migrated from github.com)

Hello, reverting this change seems to fix the issue. Any drawbacks on doing this ?

image
Hello, reverting this change seems to fix the issue. Any drawbacks on doing this ? <img width="1537" alt="image" src="https://github.com/user-attachments/assets/99ea2306-56c7-4058-aaf2-946faf0a5ee6">
Youw commented 2024-07-21 10:56:31 -05:00 (Migrated from github.com)

If will break other scenarios, not similar to yours.

If will break other scenarios, not similar to yours.
xobs commented 2025-09-03 05:48:27 -05:00 (Migrated from github.com)

This has bitten me today. A problem arises when you first call init_hid_manager() from an ephemeral thread. It will bind itself to that thread. If that thread exits, then subsequent calls will crash. See https://github.com/ruabmbua/hidapi-rs/issues/175 for the downstream bug.

Applying the fix mentioned by @marciopamplona does work, as does calling init_hid_manager() first from the main thread.

The problem is that it's a global, but one that's owned by the current thread. If that thread goes away, then things explode.

What scenarios will break if it's run on the main loop rather than the current runloop?

This has bitten me today. A problem arises when you first call `init_hid_manager()` from an ephemeral thread. It will bind itself to that thread. If that thread exits, then subsequent calls will crash. See https://github.com/ruabmbua/hidapi-rs/issues/175 for the downstream bug. Applying the fix mentioned by @marciopamplona does work, as does calling `init_hid_manager()` first from the main thread. The problem is that it's a global, but one that's owned by the current thread. If that thread goes away, then things explode. What scenarios will break if it's run on the main loop rather than the current runloop?
Youw commented 2025-09-04 09:32:05 -05:00 (Migrated from github.com)

What scenarios will break if it's run on the main loop rather than the current runloop?

Many that work today w/o any issues, like when hid_init/hid_exit is performed in the same, but other than the main thread (I personally have a few such projects of my own). Sometime you simply do not have access to the main thread (e.g. when run from a dynamic library).

UPD: OK, maybe not many different scenarios, but one scenario used by different projects.

> What scenarios will break if it's run on the main loop rather than the current runloop? Many that work today w/o any issues, like when hid_init/hid_exit is performed in the same, but other than the main thread (I personally have a few such projects of my own). Sometime you simply do not have access to the main thread (e.g. when run from a dynamic library). UPD: OK, maybe not many different scenarios, but one scenario used by different projects.
xobs commented 2025-09-04 10:40:22 -05:00 (Migrated from github.com)

Thanks. I appreciate that input.

Is there a way around it? The issue I see right now is that you need to know what thread first called into hidapi and then deallocate threadapi if that thread quits.

  • Is there a problem with initializing hidapi multiple times? Or is it a singleton?
  • Is there a way that you know of to fire off a runloop in another thread on init? It might be better to waste a thread rather than have mysterious crashes...
  • What are the implications of calling hid_exit() while other threads are still using it?

I'm trying to come up with a way to track usage across multiple threads in the Rust library, and I'm not really coming up with anything foolproof other than documentation saying to always call into hidapi first from the main thread, which doesn't seem ideal...

Thanks. I appreciate that input. Is there a way around it? The issue I see right now is that you need to know what thread first called into hidapi and then deallocate threadapi if that thread quits. * Is there a problem with initializing hidapi multiple times? Or is it a singleton? * Is there a way that you know of to fire off a runloop in another thread on init? It might be better to waste a thread rather than have mysterious crashes... * What are the implications of calling `hid_exit()` while other threads are still using it? I'm trying to come up with a way to track usage across multiple threads in the Rust library, and I'm not really coming up with anything foolproof other than documentation saying to always call into hidapi first from the main thread, which doesn't seem ideal...
Youw commented 2025-09-04 10:50:15 -05:00 (Migrated from github.com)

Is there a problem with initializing hidapi multiple times? Or is it a singleton?

Global initialisation, it is a singletone.

Is there a way that you know of to fire off a runloop in another thread on init? It might be better to waste a thread rather than have mysterious crashes...

https://github.com/libusb/hidapi/issues/666#issuecomment-2041026698

What are the implications of calling hid_exit() while other threads are still using it?

This is undefined behavior. (Depends on a platform, but most likely it will crash at some point, maybe immediately)


See also: https://github.com/libusb/hidapi/wiki/Multi%E2%80%90threading-Notes

Is there a way around it?

The "ideal workaround" (sounds funny, right?) is to create a dedicated thread, and call hid_init/hid_exit (maybe even all other "global" functions) from that thread. As I mentioned in my other comment - probably best if that thread is created by hidapi itself, but I don't find that to be trivial being done in HIDAPI source right now in C.
Besides, we have a single global state right now, and a thread per-device. I'd like that to be changed for v1, anyway.

> Is there a problem with initializing hidapi multiple times? Or is it a singleton? Global initialisation, it is a singletone. > Is there a way that you know of to fire off a runloop in another thread on init? It might be better to waste a thread rather than have mysterious crashes... https://github.com/libusb/hidapi/issues/666#issuecomment-2041026698 > What are the implications of calling hid_exit() while other threads are still using it? This is undefined behavior. (Depends on a platform, but most likely it will crash at some point, maybe immediately) --- See also: https://github.com/libusb/hidapi/wiki/Multi%E2%80%90threading-Notes > Is there a way around it? The "ideal workaround" (sounds funny, right?) is to create a dedicated thread, and call hid_init/hid_exit (maybe even all other "global" functions) from that thread. As I mentioned in my other comment - probably best if that thread is created by hidapi itself, but I don't find that to be trivial being done in HIDAPI source right now in C. Besides, we have a single global state right now, and a thread per-device. I'd like that to be changed for v1, anyway.
Sign in to join this conversation.
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#666
No description provided.