IOHIDDeviceRegisterInputReportCallback is called after "Close->Open" device cycle

My application does the following cycle:

  • Collect devices from IOHIDManagerRef using the IOHIDManagerRegisterDeviceMatchingCallback
  • IOHIDDeviceOpen for the received IOHIDDeviceRef
  • IOHIDDeviceRegisterInputReportCallback for the received IOHIDDeviceRef
  • IOHIDDeviceClose for the received IOHIDDeviceRef
  • IOHIDDeviceOpen for the received IOHIDDeviceRef
  • IOHIDDeviceRegisterInputReportCallback for the received IOHIDDeviceRef

I delete the "context" that I pass to the IOHIDDeviceRegisterInputReportCallback after the device closing.

In this situation I observe that the registered call back is called on the delete callback.

Does anyone have clue why the call back remains registered even after the device closing?

I also tried to "unrgister" the callback manually using this trick: IOHIDDeviceRegisterInputReportCallback(dev, orig_buff_ptr, orig_buff_size, nullptr, orig_ctx);

This doesn't help as well. I checked https://github.com/aosm/IOKitUser/blob/master/hid.subproj/IOHIDDevice.c already and it seems the IOHIDDeviceClose call should be enough to rid of the staling call back records.

Thank you in advance!

Does anyone have a clue why the call back remains registered even after the device closing?

Off the top of my head, there are two possibilities. The first and simple one is race conditions between the kernel and user space. Calling "close" tells the kernel to stop sending messages to you, but it won't stop delivery of messages it's already set. Keep in mind that these messages are going to be queued up for delivery by the runloop, so if you're closing on the same thread as the runloop (which you probably should be), it's possible that multiple messages can be queued up while you've blocked the thread. This is what makes them appear "after" your close— they were sent before you're close, but you couldn't receive them until later.

The second issue is that IOHIDLib comes from a very particular point in time, which, unfortunately, makes its architecture pretty darn weird. In rough summary:

  1. There's a user client implemented by the IOHIDFamily.

  2. The IOHIDFamily implements an Objective-C class to wrap that user client, because directly calling user clients is "miserable".

  3. There's a CFPlugin/COM interface implemented by the IOHIDFamily.

  4. IOHIDLib implements a CFType and function collection to hide the CFPlugin interface, because directly calling COM is REALLY "miserable".

Adding to the fun, the actual implementation is actually spread across those layers, particularly #2 and #4. So, for example, direct messages (like report changes) move through all layers, but things like removal are directly handled using IOService APIs in #4. All of this creates lots of extra opportunities for odd behavior. The initial race condition is almost certainly the direct cause, but the extra complexity the architecture above introduces is why we never changed/fixed it.

NOW, if you're wondering what amazing benefit this provided over just using #2 and throwing away layers 3 & 4... the answer is "none”. Note that our two most widely used user client libraries have both been directly replaced with new frameworks which both work by simply stopping at step #2 above.

Returning to the "why", early in macOS X's development (prior to the release of 10.0), there was significant pushback from the Mac development community[1] over all the change macOS X was creating and, in particular, Cocoa/Objective-C. That pushback led to the creation of Carbon, IOKit, and a general avoidance of Objective-C in our more low-level APIs. In the case of our IOKit user client libraries, that led to the use of CFPlugin/COM creating the mess above. None of this was really driven by engineering requirements.

...all of which leads me to ask, have you considered using CoreHID? Even if you prefer Objective-C over Swift, I think you'll find the benefits of a more straightforward API outweigh any language issues.

[1] Twenty-six years ago, I was one of those external developers, so let me formally apologize for my tiny role in encouraging this.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

Thanks for the reply, Kevin, I will try to make the minimal example today, but from what I have in the real application it's unlikely the race condition, since the device doesn't send input reports without prior output report.

The use of CoreHID is possible, but our app is implemented is C++ and it will cause additional efforts for creating the bridge. Anyway I will consider this option, thank you!

Here is the minimal example. I forced 12 second delay between the closing the device and opening it again, that proves that it barely can be race condition. (If you have the FIDO device on your end, you can test it)

IOHIDDeviceRegisterInputReportCallback is called after "Close->Open" device cycle
 
 
Q