hidapi icon indicating copy to clipboard operation
hidapi copied to clipboard

Proposal: Callback-based API for Input Reports

Open noah-nuebling opened this issue 9 months ago • 6 comments

Problem

Sending feature reports or output reports and receiving responses is currently well-supported by hidapi. However, to handle input reports from a device, e.g. to track scroll-wheel-movement, the user has to create a dedicated thread and then poll the device using hid_read().

This is bad in particular on macOS because it already provides a callback-based interface that notifies the user whenever the device issues an input report – IOHIDDeviceRegisterInputReportCallback(). In fact, the macOS implementation of hid_read() is built on top of IOHIDDeviceRegisterInputReportCallback().

That means, when building a callback-system on top of hidapi, on macOS, the control-flow will end up looking something like this:

USB interrupt 
  -> CFRunLoop 
    -> IOHIDDeviceRegisterInputReportCallback() callback 
      -> input_reports queue 
        -> hid_read() 
          -> User's polling loop 
            -> User's callback

We're essentially turning an asynchronous API into a synchronous one, and then turning it back into an asynchronous one through polling. It seems suboptimal.

Proposal

Therefore I propose a dedicated API for receiving input reports via a callback:

int hid_register_input_report_callback(
    hid_device *dev, 
    void (*callback)(unsigned char *data, size_t length, void *user_data),
    void *user_data
);
int hid_unregister_input_report_callback(hid_device *dev);

To control which thread the callbacks arrive on, you'd need additional interfaces. For macOS, it could look like this:

void hid_darwin_set_run_loop(hid_device *dev, CFRunLoopRef run_loop);

If the thread is left unspecified, the API could spawn its own internal thread to monitor the device and deliver callbacks.

Benefits

With this API, the control-flow on macOS would be much simplified:

Instead of:

USB interrupt 
  -> CFRunLoop 
    -> IOHIDDeviceRegisterInputReportCallback() callback 
      -> input_reports queue 
        -> hid_read() 
          -> User's polling loop 
            -> User's callback

It would be:

USB interrupt 
  -> CFRunLoop 
    -> IOHIDDeviceRegisterInputReportCallback() callback 
      -> hid_register_input_report_callback() callback

Therefore, it should be more efficient.

It would also allow users on macOS to easily have the input reports be delivered on their program's main thread (by simply setting the runLoop to CFRunLoopGetMain()), thereby preventing the need to create and coordinate multiple threads, and potentially making their program less error-prone.

The existing synchronous APIs such as hid_read() could stay exactly as they are.

What about Linux and Windows?

My experience is with macOS, but I think on Linux and Windows, a callback-based API like this would also make hidapi more easy-to-use and efficient for the use cases we discussed, such as tracking scroll-wheel-movement.

As I said, I don't know much about Linux and Windows development, so the specific APIs might have to be adjusted.


Thank you for your time and for maintaining hidapi. It really seems like an exceptional library, and I hope to adopt it in my projects.

noah-nuebling avatar Mar 07 '25 13:03 noah-nuebling

Related to: #202. The difference is in the implementation, but the idea is the same.

In any case I don't think this is something that will be developed befor v1 of the API which is faaar from be done.

Just to be clear - I agree that an interface/API to get input reports asyncronously is a good idea, but if you want to adopt the library for your project soon (and not maintain a fork with suggested changes), make sure to check #721 and Multi‐threading Notes.


My thoughts on this as per your description:

Therefore, it should be more efficient.

When it comes to efficiency, I'm always very sceptical. If you cannot measure the difference - it is not an efficiency problem, so it is likely not worth "fixing" at all. While what you sugges is really less operations "on paper", but I highly doubt you will be able to get a measurable difference in performance on any of the supported platforms. The bottleneck always will be a USB bus, and not an overhead on multithreading or syncronisation.

With this API, the control-flow on macOS would be much simplified

USB interrupt 
  -> CFRunLoop 
    -> IOHIDDeviceRegisterInputReportCallback() callback 
      -> hid_register_input_report_callback() callback

And there is a a potential problem with this aproach: when running a user-code directly in the device callback, the library looses control over timings of the callback implementation, i.e. - user code might (unintentionally) perform some long-running operation on the report, which might disrupt the behaviorr of the device depending on the platform. It will make much harder to have identical behavior across platforms (i.e. - what happens when a new report is available while user-code still being executed in the callback).

So for a library developer it is always beneficial to have separate queue/thread for device callback and user-callback. And the difference in overhead will not be noticable at all.

On that NOTE, on macOS we always have a thread per-device for device-callbacks, and I think for v1 it should be enought to have a single thread/RunLoop for a context (yet to be introduced) shared across all devices. And only an additional callback thread per-device. That way it will be simplest way to ensure device stability and similar behavior across platforms.

Windows/Linux(hidraw)

Both of these platforms do not use the internal thread for input reports, since both of these use OS-specific polling mechanisms inside of the hid_read_timeout implementation.

And I will have to double-check with OS-specific documentation, but I believe to implement an input report callback on each of these platforms an additional device-specific thread would need to be introduced anyway, so practically it will make no difference at all.

tracking scroll-wheel-movement

I really need to document it somewhere that Mouse/Keyboard is not a "regular" HID device, and HIDAPI should not be considered as a first option to be used with these, unless you really know what you're doing. And the reason is rather simple - OS driver holds Mouse/Keyboard exclusively to itself, so just any application cannot simply "sniff" user's input.

Youw avatar Mar 07 '25 15:03 Youw

Thank you for your detailed response.

if you want to adopt the library for your project soon [...] make sure to check https://github.com/libusb/hidapi/pull/721 and Multi‐threading Notes.

I won't adopt it very soon – It will likely take more than a year. Thank you for the links. I did plan to use hidapi from a single thread, so the thread safety concerns shouldn't affect me.

When it comes to efficiency, I'm always very sceptical. [...]

Point taken. I agree with you. This is most likely just an 'on paper' issue.

And there is a a potential problem with this aproach: when running a user-code directly in the device callback, the library looses control over timings of the callback implementation. [...] (i.e. - what happens when a new report is available while user-code still being executed in the callback).

I don't see this point. It seems to me that the interaction model that hidapi provides for devices is simple enough that the library doesn't need to have specific "control over timings" of the interactions. It just has to guarantee that everything is executed as soon as possible, and that timeouts work correctly.

If the user asks hidapi to deliver callbacks on a runloop, and there are delays in callback execution because the runloop is busy with other work, that's expected behavior and the user's responsibility to deal with.

However, I agree that direct runloop integration might be unnecessary:

[...] callback thread per-device

This sounds great.

I think I've changed my mind since the original proposal: Not bothering with runloops and having hidapi control the report-callback-thread would keep the API simple and platform-agnostic. On macOS, users could still easily dispatch from hidapi's callback thread to their runLoop using CFRunLoopPerformBlock().

The only reasons I can think of now to directly integrate hidapi with runloops are:

  • If it's very hard to dispatch from hidapi's callback to a runloop on Linux or Windows.
  • If there are performance concerns. Perhaps this matters when monitoring very high-frequency sensors like touchpads (though this might be another 'on paper' issue).

I really need to document it somewhere that Mouse/Keyboard is not a "regular" HID device, and HIDAPI should not be considered as a first option to be used with these, unless you really know what you're doing. And the reason is rather simple - OS driver holds Mouse/Keyboard exclusively to itself, so just any application cannot simply "sniff" user's input.

That is interesting. On macOS, I've been able to read from mice alongside the OS Driver in the past (Except when using hid_darwin_set_open_exclusive()).

I'm interested in writing a (multiplatform) c library for handling vendor-specfic features (like the touch sensor on the MX Master's horizontal scrollwheel). Would these device access limitations affect this usecase? And do they vary by platform? This would indeed be valuable to document.

noah-nuebling avatar Mar 08 '25 09:03 noah-nuebling

I've been able to read from mice alongside the OS Driver in the past

With or without giving your application explicit "Accessibility" permission? Maybe only Keyboards are not available as HID devices on macOS. Its been a while since I checked last time.

Would these device access limitations affect this usecase? And do they vary by platform?

At least on Windows there is no access to any report that is under HID_USAGE_PAGE_GENERIC/HID_USAGE_GENERIC_MOUSE/HID_USAGE_GENERIC_KEYBOARD (and a few others).

Youw avatar Mar 08 '25 10:03 Youw

Interesting. Thank you for the information.

The last time I tested this I was using Accessibility permissions, but I'm not sure whether they are necessary. I only tested mice, not keyboards.

noah-nuebling avatar Mar 08 '25 10:03 noah-nuebling

I did some quick testing regarding HID device access on macOS (M1 MacBook Air, macOS 15.3):

Starting with a fresh macOS app project in Xcode, to monitor mouse input reports with hidapi, you need to:

  1. Disable app sandboxing
  2. Enable Input Monitoring permissions (Accessibility permissions should also work)

To monitor keyboards, you must do the same 2 steps as for mice, plus you must call hid_darwin_set_open_exclusive(false) before opening the device. Otherwise there's a privilege violation error.

I tested the internal trackpad and keyboard of my MacBook, plus an external USB mouse and keyboard. I didn't notice a difference between internal and external devices.

noah-nuebling avatar Mar 08 '25 12:03 noah-nuebling

Enable Input Monitoring permissions (Accessibility permissions should also work)

I guess that's what Darwin platform allows. On Windows I believe there is no such permission anywhere.

Youw avatar Mar 08 '25 20:03 Youw