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!

Answered by DTS Engineer in 905479022

Here is the minimal example. I forced a 12-second delay between closing the device and opening it again, that proves that it barely can be a race condition.

That depends on what you mean by race...

So, the answer here is actually pretty simple. The configuration change you make to a given device is attached to the device object and they aren't actually destroyed until the object is destroyed. You're reusing the same IOHIDDeviceRef, so that object still has configuration attached to it. The 12s delay happens because you stop message delivery here:

~FidoHidDevice()
{
	if (m_open)
	{
	...
		IOHIDDeviceUnscheduleFromRunLoop(m_device, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode);
	...

But that resumes delivery here, BEFORE you register a new report callback:

explicit FidoHidDevice(IOHIDDeviceRef device)
	: m_device(device), m_inputBuffer(kReportSize)
{
...
	IOHIDDeviceScheduleWithRunLoop(m_device, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode);

	IOHIDDeviceRegisterInputReportCallback(m_device, m_inputBuffer.data(),
											static_cast(m_inputBuffer.size()),
											&FidoHidDevice::onInputReport, this);

However, I don't think getting a "fresh" object would actually help either. You should double-check this for yourself, but if you call "IOHIDManagerCopyDevices" twice, I think you'll find that you get exactly the same objects from both calls. IOHIDManagerCopyDevices is actually calling CFSetCreateCopy on its own data structure, so you're simply getting a copy of exactly the same objects. In practical terms, you need to think of IOHIDManager as managing a collection of singleton objects which you're also allowed to manipulate, NOT as a simple object getter/setter.

In terms of masking the immediate issue, I think calling IOHIDDeviceRegisterInputReportCallback before IOHIDDeviceScheduleWithRunLoop would "hide" the problem. Note that this is following our longstanding guidance of fully configuring whatever you're manipulating before turning it over to the run loop.

HOWEVER, that's not the solution I'd actually recommend. Assuming I'm right about IOHIDManagerCopyDevices, then the underlying problem here is that there are actions tied to IOHIDDeviceRef's destruction that you want to manage, but you don't actually control its destruction, IOHIDManager does.

My primary advice would be to switch to CoreHID. You'll note that while CoreHID has a similar API structure, with "HIDDeviceManager" and "HIDDeviceClient", it works by referencing objects using "HIDDeviceClient.DeviceReference". We haven't documented what exactly that value is, so I'll simply note that it is the same size as the value returned by IORegistryEntryGetRegistryEntryID and that a value like entryID would allow the manager to track objects without entangling itself with your HID object(s) the way IOHIDManager does.

However, if you really want to use IOHIDLib, then I think my advice would be to remove IOHIDManager entirely. Use standard IOService discovery to find your target services, then directly use IOHIDDeviceCreate with the io_service_t's it returns.

That leads me to my final point, which is that part of the problem here is that you're not actually using IOHIDManager the way it was intended to be used. IOHIDManager actually replaced the older "HID Manager" Carbon API, and part of that API’s design was that apps could describe the devices they wanted to monitor, and then APIs like IOHIDManagerRegisterInputReportCallback would return reports from "all" of those devices. That's also why IOHIDManagerCopyDevices returns "it's own" objects. The point of that API isn't to simply get a reference to a HID device; it's to be able to specifically manipulate the same device object IOHIDManager is using.

And, yes, in practice, this turns out to not be all that useful. Time and experience showed that most apps ended up directly interacting with specific devices or simply relied on the standard event system (so they didn't use the API at all), making the "aggregation" layer largely unused. More to the point, even apps that were aggregating were often doing it based on criteria that weren't trivially describable through the matching system, at which point they ended up using the direct device layer ANYWAY... which is why CoreHID doesn't have an aggregation layer API.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

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)

Accepted Answer

Here is the minimal example. I forced a 12-second delay between closing the device and opening it again, that proves that it barely can be a race condition.

That depends on what you mean by race...

So, the answer here is actually pretty simple. The configuration change you make to a given device is attached to the device object and they aren't actually destroyed until the object is destroyed. You're reusing the same IOHIDDeviceRef, so that object still has configuration attached to it. The 12s delay happens because you stop message delivery here:

~FidoHidDevice()
{
	if (m_open)
	{
	...
		IOHIDDeviceUnscheduleFromRunLoop(m_device, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode);
	...

But that resumes delivery here, BEFORE you register a new report callback:

explicit FidoHidDevice(IOHIDDeviceRef device)
	: m_device(device), m_inputBuffer(kReportSize)
{
...
	IOHIDDeviceScheduleWithRunLoop(m_device, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode);

	IOHIDDeviceRegisterInputReportCallback(m_device, m_inputBuffer.data(),
											static_cast(m_inputBuffer.size()),
											&FidoHidDevice::onInputReport, this);

However, I don't think getting a "fresh" object would actually help either. You should double-check this for yourself, but if you call "IOHIDManagerCopyDevices" twice, I think you'll find that you get exactly the same objects from both calls. IOHIDManagerCopyDevices is actually calling CFSetCreateCopy on its own data structure, so you're simply getting a copy of exactly the same objects. In practical terms, you need to think of IOHIDManager as managing a collection of singleton objects which you're also allowed to manipulate, NOT as a simple object getter/setter.

In terms of masking the immediate issue, I think calling IOHIDDeviceRegisterInputReportCallback before IOHIDDeviceScheduleWithRunLoop would "hide" the problem. Note that this is following our longstanding guidance of fully configuring whatever you're manipulating before turning it over to the run loop.

HOWEVER, that's not the solution I'd actually recommend. Assuming I'm right about IOHIDManagerCopyDevices, then the underlying problem here is that there are actions tied to IOHIDDeviceRef's destruction that you want to manage, but you don't actually control its destruction, IOHIDManager does.

My primary advice would be to switch to CoreHID. You'll note that while CoreHID has a similar API structure, with "HIDDeviceManager" and "HIDDeviceClient", it works by referencing objects using "HIDDeviceClient.DeviceReference". We haven't documented what exactly that value is, so I'll simply note that it is the same size as the value returned by IORegistryEntryGetRegistryEntryID and that a value like entryID would allow the manager to track objects without entangling itself with your HID object(s) the way IOHIDManager does.

However, if you really want to use IOHIDLib, then I think my advice would be to remove IOHIDManager entirely. Use standard IOService discovery to find your target services, then directly use IOHIDDeviceCreate with the io_service_t's it returns.

That leads me to my final point, which is that part of the problem here is that you're not actually using IOHIDManager the way it was intended to be used. IOHIDManager actually replaced the older "HID Manager" Carbon API, and part of that API’s design was that apps could describe the devices they wanted to monitor, and then APIs like IOHIDManagerRegisterInputReportCallback would return reports from "all" of those devices. That's also why IOHIDManagerCopyDevices returns "it's own" objects. The point of that API isn't to simply get a reference to a HID device; it's to be able to specifically manipulate the same device object IOHIDManager is using.

And, yes, in practice, this turns out to not be all that useful. Time and experience showed that most apps ended up directly interacting with specific devices or simply relied on the standard event system (so they didn't use the API at all), making the "aggregation" layer largely unused. More to the point, even apps that were aggregating were often doing it based on criteria that weren't trivially describable through the matching system, at which point they ended up using the direct device layer ANYWAY... which is why CoreHID doesn't have an aggregation layer API.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

Thanks a lot Kevin, I reimplemented the device lookup using the IOService discovery. We will probably switch to CoreHID API in the future, but currently all works perfectly!

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