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!
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