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:

  1. Get the IO registry which has the system information for connected hardware.
  2. Get an iterator for the USB plane.
  3. Walk the iterator.
  4. Pull out IO Plugins for each iterator.
  5. 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 = 0;
    bool   start  = true;

    if (str == NULL)
        return;

    len = strlen(str);
    for (len = 0; i < len; i++) {
        if (isspace(str[i])) {
            if (start) {
                offset++;
            } else {
                str[i] = '\0';
                break;
            }
        }
    }

    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.