Introduction
Apple provides documentation for using IOKit, but it’s not very clear how to do a number of things. Descriptions and relationships of functions often isn’t clear. While there are examples, they tend to be lacking. The generic types IOKit uses doesn’t help either because you could have multiple different types of hardware sharing the same device object type.
I was working on a project where I needed to enumerate USB devices on macOS. I needed to see all USB devices so I needed USB enumeration and not something like HID enumeration. HID has HID specific objects and function which make things very clear. Sadly, USB is not as nicely designed as the HID interface.
With USB you get a generic object, from which you pull out a USB object. The USB object is partly data but mainly provides function callbacks. It’s very object oriented but somewhat confusing.
The Notification Callback
Apple provides an example,
for enumerating USB devices which I found to be very helpful. That said, It
uses a notification callback IOServiceAddMatchingNotification
for listing the
devices which I don’t like.
Notifications are great if you’re waiting for a device to be attached or if you want to do something like cache attached devices. However, this isn’t want I needed. I needed something to show me specific information and if it is the device I want, I will use it.
The project I was working on wasn’t a long running process that needed to wait for a device. It was either connected or not and if not my program must return an error.
Looking around online, it seems like pretty much everyone used this notification setup. There are a few things I don’t like about using a callback here and thankfully you can enumerate without it.
The main reason I see people using the notification callback when they don’t
want or use notifications is to get the io_iterator_t
for attached USB
devices. It’s unnecessary to use the notification callback to query this is
object. We can get it by querying the USB plane directly.
Enumeration Without Notification Callback
If you use the notification callback method you need to attached to an event loop. Conversely, querying the USB plane directly does not need an event loop. Making this advantageous in some situations.
The notification callback does allow you to filter for specific devices based on given criteria. However, you can still filter because all the metadata is accessible without opening the device. When looping though all attached devices, you can ignore any that do not have certain attributes you are looking for.
Core enumeration loop
The steps we need to take are:
- Get the IO registry which has the system information for connected hardware.
- Get an iterator for the USB plane.
- Walk the iterator.
- Pull out IO Plugins for each iterator.
- Get an IOUSBDeviceInterface for each USB device from the IO Plugin object.
void usb_enum(void)
{
io_registry_entry_t entry = 0;
io_iterator_t iter = 0;
io_service_t service = 0;
kern_return_t kret;
/* 1. Get the IO registry which has the system information for connected hardware. */
entry = IORegistryGetRootEntry(kIOMasterPortDefault);
if (entry == 0)
return;
/* 2. Get an iterator for the USB plane. */
kret = IORegistryEntryCreateIterator(entry, kIOUSBPlane, kIORegistryIterateRecursively, &iter);
if (kret != KERN_SUCCESS || iter == 0)
return;
/* 3. Walk the iterator. */
while ((service = IOIteratorNext(iter))) {
IOCFPlugInInterface **plug = NULL;
IOUSBDeviceInterface **dev = NULL;
io_string_t path;
SInt32 score = 0;
IOReturn ioret;
/* 4. Pull out IO Plugins for each iterator. */
kret = IOCreatePlugInInterfaceForService(service, kIOUSBDeviceUserClientTypeID, kIOCFPlugInInterfaceID, &plug, &score);
IOObjectRelease(service);
if (kret != KERN_SUCCESS || plug == NULL) {
continue;
}
/* 5. Get an IOUSBDeviceInterface for each USB device from the IO Plugin object. */
ioret = (*plug)->QueryInterface(plug, CFUUIDGetUUIDBytes(kIOUSBDeviceInterfaceID), (void *)&dev);
(*plug)->Release(plug);
if (ioret != kIOReturnSuccess || dev == NULL) {
continue;
}
/* Print out the path in the IO Plane the device is at. */
if (IORegistryEntryGetPath(service, kIOServicePlane, path) != KERN_SUCCESS) {
(*dev)->Release(dev);
continue;
}
printf("Found device at '%s'\n", path);
/* Print device metadata. */
print_dev_info(dev);
/* All done with this device. */
(*dev)->Release(dev);
}
IOObjectRelease(iter);
}
Since we’re only enumerating we are not going to open every device. Most
metadata is available without opening the device. Opening a device this
way can be problematic on macOS because the kernel will have attached
a system driver to the device based on the profile the device reports
itself as. For example, a Serial over USB device will have a /dev
entry created and opening using IOUSB
will fail. In these case you need
to access the device using other methods. Serial functions for serial presented
devices, HID for HID devices, etc.
Printing device information
The enumeration loop allows us to get a IOUSBDeviceInterface
object for each
attached USB device. We want to print out (or capture) some metadata about the
device.
The IOUSBDeviceInterface
has a number of internal functions that
provide various information about the device. Following are the
ones to access the most interesting and important information.
void print_dev_info(IOUSBDeviceInterface **dev)
{
char *str;
UInt8 si;
UInt16 u16v;
if ((*dev)->GetDeviceVendor(dev, &u16v) == kIOReturnSuccess)
printf("\tVendor ID: %u\n", u16v);
if ((*dev)->GetDeviceProduct(dev, &u16v) == kIOReturnSuccess)
printf("\tProduct ID: %u\n", u16v);
if ((*dev)->USBGetManufacturerStringIndex(dev, &si) == kIOReturnSuccess) {
get_string_from_descriptor_idx(dev, si, &str);
printf("\tManufacturer: %s\n", str);
free(str);
}
if ((*dev)->USBGetProductStringIndex(dev, &si) == kIOReturnSuccess) {
get_string_from_descriptor_idx(dev, si, &str);
printf("\tProduct: %s\n", str);
free(str);
}
if ((*dev)->USBGetSerialNumberStringIndex(dev, &si) == kIOReturnSuccess) {
get_string_from_descriptor_idx(dev, si, &str);
printf("\tSerial: %s\n", str);
free(str);
}
if ((*dev)->GetDeviceSpeed(dev, &si) == kIOReturnSuccess) {
printf("\tSpeed: ");
switch (si) {
case kUSBDeviceSpeedLow:
printf("Low\n");
break;
case kUSBDeviceSpeedFull:
printf("Full\n");
break;
case kUSBDeviceSpeedHigh:
printf("High\n");
break;
case kUSBDeviceSpeedSuper:
printf("Super\n");
break;
case kUSBDeviceSpeedSuperPlus:
printf("Super Plus\n");
break;
case kUSBDeviceSpeedSuperPlusBy2:
printf("Super Plus 2\n");
break;
}
}
if ((*dev)->GetConfiguration(dev, &si) == kIOReturnSuccess)
printf("\tCurrent Config: %u\n", si);
}
One thing to keep in mind for getting string based information is the
IOUSBDeviceInterface
only provides an offset to the string when
querying the device for more information. It does not return a string
itself. We need query and handle any conversions ourself.
Converting string index to string
To convert the string index, for something like reading the manufacturer, we
need to send a control transfer to the device to request this information.
Thankfully, the IOUSBDeviceInterface
does not have to be open for this
to happen.
bool get_string_from_descriptor_idx(IOUSBDeviceInterface **dev, UInt8 idx, char **str)
{
IOUSBDevRequest request;
IOReturn ioret;
char buffer[4086] = { 0 };
CFStringRef cfstr;
CFIndex len;
if (str != NULL)
*str = NULL;
request.bmRequestType = USBmakebmRequestType(kUSBIn, kUSBStandard, kUSBDevice);
request.bRequest = kUSBRqGetDescriptor;
request.wValue = (kUSBStringDesc << 8) | idx;
request.wIndex = 0x409;
request.wLength = sizeof(buffer);
request.pData = buffer;
ioret = (*dev)->DeviceRequest(dev, &request);
if (ioret != kIOReturnSuccess)
return false;
if (str == NULL || request.wLenDone <= 2)
return true;
/* Now we need to parse out the actual data.
* Byte 1 - Length of packet (same as request.wLenDone)
* Byte 2 - Type
* Byte 3+ - Data
*
* Data is a little endian UTF16 string which we need to convert to a utf-8 string. */
cfstr = CFStringCreateWithBytes(NULL, (const UInt8 *)buffer+2, request.wLenDone-2, kCFStringEncodingUTF16LE, 0);
len = CFStringGetMaximumSizeForEncoding(CFStringGetLength(cfstr), kCFStringEncodingUTF8) + 1;
if (len < 0) {
CFRelease(cfstr);
return true;
}
*str = calloc(1, (size_t)len);
CFStringGetCString(cfstr, *str, len, kCFStringEncodingUTF8);
str_trim(*str);
CFRelease(cfstr);
return true;
}
A few things to note here. We need to create our control transfer using a
IOUSBDevRequest
. This is a raw USB request we have to structure ourselves.
The string data we get back need a little manipulation due to the character
encoding. Since this is C and we’re going to return a C string, we want
it to be UTF-8 encoded. We’ll use one of the Core Foundation functions to
take care of this for us. If we wanted to mix in some Objective-C we could
use the NSString
class to do this.
The USB request returns a string with padding. We use a string trimming function to remove any white space.
String trimming helper
Here is a basic, in place, string trimming function. There are number of ways to write a function like this, and this one is included here for completeness of working code.
Realize, if you use a different function, this is an in place operation and moves the data in the string. Some functions return a pointer to the first non-white space character instead of moving the data to the start.
The trimmed string is an allocated string that’s being used as an out
parameter. We need to set the out parameter to the pointer that was allocated and
not the start of non-white space data. Otherwise, free
will be unhappy. Make
any needed alterations to the code if using a trim function that operates
differently.
void str_trim(char *str)
{
size_t len;
size_t i;
size_t offset;
if (str == NULL)
return;
len = strlen(str);
if (len == 0)
return;
/* Trim white space from the end. */
offset = 0;
for (i = len; i-- > 0; ) {
if (!isspace(str[i])) {
break;
}
offset++;
}
if (offset > 0)
str[len-offset] = '\0';
/* Trim white space from the beginning. */
len = strlen(str);
offset = 0;
for (i = 0; i < len; i++) {
if (!isspace(str[i])) {
break;
}
offset++;
}
if (offset > 0)
memmove(str, str+offset, len-offset+1);
}
Conclusion
While tedious enumerating USB devices it turns out it isn’t very difficult. The
main hangup is lack of clear documentation. I didn’t see anything from Apple
about querying the IO Plane for USB devices as an alternative to using the
IOServiceAddMatchingNotification
function.