📜 ⬆️ ⬇️

Working with USB devices in a C program on MacOS X

Greetings dear readers.
In this small article I would like to consider the issue of interaction with hardware (in this case with USB devices) in the MacOS X operating system.
It will be considered a very interesting and useful IOKit framework, a way to get notifications about adding / removing obordovaniya, as well as getting complete information about the devices. Of course, this material does not pretend to any uniqueness, because Everyone can deal with these issues on their own, having studied the Apple documentation, as well as having smoked various sources on opensource.apple.com
My article is an attempt to fill the gap in the Russian-language material of this kind and to describe some of the rakes that novices may encounter.
All who became interested - welcome under cat.

There is a wonderful thing in the MacOS X core - the IOKit framework. This is an object-oriented C ++ framework designed specifically to support the hardware driver infrastructure. True C ++ there is somewhat curtailed, for example, there are no exceptions, RTTI, patterns. The rest of the kernel is written primarily in C.
IOKit itself can be divided into two parts: the actual kernel framework on which the drivers are written and the user-specific IOKit.framework, designed for easy access to the kernel modules (and equipment, respectively) from user applications.
The object-oriented nature of IOKit allows you to conveniently display the real physical model of the equipment: a USB device is connected, a USB port is connected to the hub, a hub is connected to the appropriate controller in the computer, this controller, is connected via PCI bus to the rest of the motherboard chipset. Each IOKit driver is either an end node (for example, a USB modem) or a connecting module (for example, a USB port controller) in this model, in Apple's terminology, the latter is called nub.
The Xcode delivery package includes the IORegExplorer graphical utility that allows you to view the entire structure of IOKit modules, read various parameters and technical information.

There is also a console analog utility - ioreg.

This article will cover the work only with the user-specific IOKit.framework. If any readers are interested in the kernel framework, you can read the habrahabr.ru/post/36875 article, as well as the wonderful book OS X and iOS Kernel Programming.

Receive USB Notifications
As mentioned above, we will work with USB, so we need only one header file
#include <IOKit/usb/IOUSBLib.h> 

Let's start by subscribing to receive notifications about adding / removing USB devices. A special object of type IONotificationPortRef will help us with this.
Create and initialize an instance of IONotificationPortRef
 IONotificationPortRef notificationPort = IONotificationPortCreate(kIOMasterPortDefault); 

We have created a kind of virtual port for listening to events from IOKit (it has not yet been determined which ones). kIOMasterPortDefault is a constant that defines a certain “default” port to receive messages from IOKit, always use this value.

Now we will determine which messages we want to receive. To do this, you need to create a so-called dictionary, using the corresponding container of the base framework MacOS X.
 CFMutableDictionaryRef matchDict = (CFMutableDictionaryRef) CFRetain(IOServiceMatching(kIOUSBDeviceClassName)); 

The key method here is IOServiceMatching, which creates a dictionary of USB class devices, I think that this is obvious (there are other constants, for example, for FireWire, etc., that allow you to receive corresponding notifications). Next, we “take” ownership of this object using CFRetain. This is to ensure that there are no problems with the possible double release of an object in the methods of the IOKit framework. Actually, we ourselves do not need to free this object, in other cases, never forget to call CFRelease for objects allocated in a heap (not received by any Get * method). In addition, always check, freshly created / freshly obtained * Ref objects of the basic framework, for NULL, since these are, in fact, ordinary pointers and you need to work with them accordingly.

Then we “hang” this dictionary on the previously created port, as matching notification. This is done using the IOServiceAddMatchingNotification method.
Consider it carefully

 kern_return_t IOServiceAddMatchingNotification( IONotificationPortRef notifyPort, const io_name_t notificationType, CFDictionaryRef matching, IOServiceMatchingCallback callback, void *refCon, io_iterator_t *notification ); 

The first parameter notifyPort is the port we created earlier.
The second parameter of the notificationType is the type of notifications for this dictionary, such as kIOTerminatedNotification , kIOMatchedNotification , kIOFirstMatchNotification . I think that from the names of these constants it is quite clear what they mean.
The third parameter matching is the dictionary itself. The object type, as you can see, is CFDictionaryRef. This is not a problem, because CFMutableDictionaryRef is no problem cast to type CFDictionaryRef.
The fourth callback method is the most interesting. This is a pointer to a callback method called when the corresponding event arrives. About him a little lower.
The fifth parameter refCon is the context of the callback method, here you can transfer your data to this function.
The last parameter of the notification is an iterator intended to iterate over the collection of devices, this argument is initialized in IOServiceAddMatchingNotification and also is passed to the callback method, indicating the very first device in the collection.

If successful, IOServiceAddMatchingNotification returns zero, otherwise a positive number, an error code.

The callback function is defined as follows:
typedef void (*IOServiceMatchingCallback)(void *refcon, io_iterator_t iterator);
As you can see, the function takes the very last two arguments from IOServiceAddMatchingNotification.

We need to declare two such functions - add and delete devices.

 void usb_device_added(void* refcon, io_iterator_t iterator) { } void usb_device_removed(void* refcon, io_iterator_t iterator) { } 

And now, finally, we set up notifications.

 kern_return_t result; io_iterator_t deviceAddedIter; io_iterator_t deviceRemovedIter; result = IOServiceAddMatchingNotification(notificationPort, kIOMatchedNotification, matchDict, usb_device_added, NULL, &deviceAddedIter); ... result = IOServiceAddMatchingNotification(notificationPort, kIOTerminatedNotification, matchDict, usb_device_removed, NULL, &deviceRemovedIter); 

No additional initialization of the methods is required and be sure to check the result code after each call to IOServiceAddMatchingNotification.

Now we need to set up a stream with a cycle in which our listener will spin. In MacOS X for these purposes, the corresponding object is provided - CFRunLoop. For normal operation, this object must indicate the so-called source and run in the context of the current or some other stream, and after the end of the work, stop.
Let's do it like this:
 CFRunLoopAddSource(CFRunLoopGetCurrent(), IONotificationPortGetRunLoopSource(notificationPort), kCFRunLoopDefaultMode); 

In this case, we add the source of the notifications using the special IONotificationPortGetRunLoopSource method, using our notification port, and we also specify the identifier of the required stream — in this case, the current CFRunLoopGetCurrent ()

Then this thread can start and start receiving notifications. But there is one caveat that it would all work - callback methods must be called manually once, as if “rubbing” the collection. If you do not do this - notifications will not come.

Starter stream is executed by the CFRunLoopRun () method; Everything, at this point, the execution of the code will not go further. This cycle can only be stopped using CFRunLoopStop, the argument, this method, is passed the identifier of the thread where CFRunLoopRun was run, the actual value that returned above CFRunLoopGetCurrent. In a multithreaded environment, I recommend saving this value so that you can stop RunLoop from the context of another thread, simply by specifying the correct, saved identifier.

After stopping the flow, you need to tidy up a little behind you
 CFRunLoopRemoveSource(CFRunLoopGetCurrent(), IONotificationPortGetRunLoopSource(notificationPort), kCFRunLoopDefaultMode); IONotificationPortDestroy(notificationPort); 

Getting information about USB devices
So, we learned how to receive notifications about adding / removing USB devices, now let's see how you can get all the information about devices directly in callback methods.
On good, both methods should be reduced to calling one method iterate_usb_devices

 void iterate_usb_devices(io_iterator_t iterator) { io_service_t usbDevice; while ((usbDevice = IOIteratorNext(iterator))) { .... IOObjectRelease(usbDevice); } } 

Here we make a walk through the device collection, using the passed iterator and IOIteratorNext, after using the object must be freed in IOObjectRelease.
Each received usbDevice object is the source of all the necessary information. The simplest example is:

 io_name_t devicename; if (IORegistryEntryGetName(usbDevice, devicename) != KERN_SUCCESS) == KERN_SUCCESS) { printf("Device name: %s\n", devicename); } 

If successful, a readable device name will be displayed, for example, “Novatel wireless modem”. io_name_t is essentially an ordinary char array.

Another example is getting the full path to a device in the IOKit hierarchy:

 io_name_t entrypath; if (IORegistryEntryGetPath(usbDevice, kIOServicePlane, entrypath) == KERN_SUCCESS) { printf("\tDevice entry path: %s\n", entrypath); } 

These are the simplest cases, because A whole dictionary of various parameters in the form of a key-value pair is associated with the usbDevice object. By the corresponding keys, you can get the corresponding parameter values, for example, we will get the VendorID of the device.

 CFNumberRef vendorId = (CFNumberRef) IORegistryEntrySearchCFProperty(usbDevice , kIOServicePlane , CFSTR("idVendor") , NULL , kIORegistryIterateRecursively | kIORegistryIterateParents); 

This method searches for the corresponding parameter using a string key, in this case “idVendor” (CFSTR is a MacOS X framework method that performs a quick conversion of a C string to an internal object of type CFStringRef)

IORegistryEntrySearchCFProperty returns NULL if the search was unsuccessful, or a pointer to the object of the desired type, in this case CFNumberRef. To "catch" the normal numeric value from CFNumberRef, you need to use the following:

 int result; if (CFNumberGetValue(vendorId, kCFNumberSInt32Type, &result)) { printf("VendorID: %i\n", result); } 

Getting the “ProductID” is performed in exactly the same way, only the key “idProduct” should be used as the key

Of course, the list of parameters and the corresponding keys can vary greatly from device to device. The reader may have a corresponding question - how to find out what parameters and by what keys you need to look for this device and how to display all the values ​​at once?
This is done quite simply:

 CFMutableDictionaryRef properties; IORegistryEntryCreateCFProperties(usbDevice, &properties, kCFAllocatorDefault, 0); 

In case of successful implementation of this method, we will have at our disposal a dictionary filled with all the keys / values ​​of the device.
The easiest way, now, to print this dictionary to the screen is the CFShow framework. A few words about it, CFShow is a universal method for crafting any CoreFoundation Framework objects in MacOS X - CFMutableDictionaryRef, CFStringRef, CFNumberRef, etc. Output is done in stderr.
So, we display the contents of the dictionary on the screen:


Now we have a list of all keys and all parameters for this device and we can use this knowledge to get specific values ​​in the form of specific objects (either through IORegistryEntrySearchCFProperty, or directly from the dictionary, using the methods for working with dictionaries)

Work application.
Now I would like to give an example of a small application containing everything described above. Among other things, I decided to add signal processing to this example in order to be able to correctly terminate the application using trl-.
Note: In real projects, never perform such actions in a signal handler, why, you can read, for example, in this message

 #include <IOKit/usb/IOUSBLib.h> #include <signal.h> #include <stdio.h> #include <stdlib.h> static IONotificationPortRef notificationPort; void usb_device_added(void* refcon, io_iterator_t iterator); void usb_device_removed(void* refcon, io_iterator_t iterator); void init_notifier() { notificationPort = IONotificationPortCreate(kIOMasterPortDefault); CFRunLoopAddSource(CFRunLoopGetCurrent(), IONotificationPortGetRunLoopSource(notificationPort), kCFRunLoopDefaultMode); printf("init_notifier ---> Ok\n"); } void configure_and_start_notifier() { printf("Starting notifier...\n\n"); CFMutableDictionaryRef matchDict = (CFMutableDictionaryRef) CFRetain(IOServiceMatching(kIOUSBDeviceClassName)); if (!matchDict) { fprintf(stderr, "Failed to create matching dictionary for kIOUSBDeviceClassName\n"); return; } kern_return_t addResult; io_iterator_t deviceAddedIter; addResult = IOServiceAddMatchingNotification(notificationPort, kIOMatchedNotification, matchDict, usb_device_added, NULL, &deviceAddedIter); if (addResult != KERN_SUCCESS) { fprintf(stderr, "IOServiceAddMatchingNotification failed for kIOMatchedNotification\n"); return; } usb_device_added(NULL, deviceAddedIter); io_iterator_t deviceRemovedIter; addResult = IOServiceAddMatchingNotification(notificationPort, kIOTerminatedNotification, matchDict, usb_device_removed, NULL, &deviceRemovedIter); if (addResult != KERN_SUCCESS) { fprintf(stderr, "IOServiceAddMatchingNotification failed for kIOTerminatedNotification\n"); return; } usb_device_removed(NULL, deviceRemovedIter); CFRunLoopRun(); } void deinit_notifier() { CFRunLoopRemoveSource(CFRunLoopGetCurrent(), IONotificationPortGetRunLoopSource(notificationPort), kCFRunLoopDefaultMode); IONotificationPortDestroy(notificationPort); printf("deinit_notifier ---> Ok\n"); } void signal_handler(int signum) { printf("\ngot signal, signnum=%i stopping current RunLoop\n", signum); CFRunLoopStop(CFRunLoopGetCurrent()); } void init_signal_handler() { signal(SIGINT, signal_handler); signal(SIGQUIT, signal_handler); signal(SIGTERM, signal_handler); } int main() { init_signal_handler(); init_notifier(); configure_and_start_notifier(); deinit_notifier(); return 0; } void print_cfstringref(const char* prefix, CFStringRef cfVal) { char* cVal = malloc(CFStringGetLength(cfVal) * sizeof(char)); if (!cVal) { return; } if (CFStringGetCString(cfVal, cVal, CFStringGetLength(cfVal) + 1, kCFStringEncodingASCII)) { printf("%s %s\n", prefix, cVal); } free(cVal); } void print_cfnumberref(const char* prefix, CFNumberRef cfVal) { int result; if (CFNumberGetValue(cfVal, kCFNumberSInt32Type, &result)) { printf("%s %i\n", prefix, result); } } void get_usb_device_info(io_service_t device, int newdev) { io_name_t devicename; io_name_t entrypath; io_name_t classname; if (IORegistryEntryGetName(device, devicename) != KERN_SUCCESS) { fprintf(stderr, "%s unknown device (unable to get device name)\n", newdev ? "Added " : " Removed"); return; } printf("USB device %s: %s\n", newdev ? "FOUND" : "REMOVED", devicename); if (IORegistryEntryGetPath(device, kIOServicePlane, entrypath) == KERN_SUCCESS) { printf("\tDevice entry path: %s\n", entrypath); } if (IOObjectGetClass(device, classname) == KERN_SUCCESS) { printf("\tDevice class name: %s\n", classname); } CFStringRef vendorname = (CFStringRef) IORegistryEntrySearchCFProperty(device , kIOServicePlane , CFSTR("USB Vendor Name") , NULL , kIORegistryIterateRecursively | kIORegistryIterateParents); if (vendorname) { print_cfstringref("\tDevice vendor name:", vendorname); } CFNumberRef vendorId = (CFNumberRef) IORegistryEntrySearchCFProperty(device , kIOServicePlane , CFSTR("idVendor") , NULL , kIORegistryIterateRecursively | kIORegistryIterateParents); if (vendorId) { print_cfnumberref("\tVendor id:", vendorId); } CFNumberRef productId = (CFNumberRef) IORegistryEntrySearchCFProperty(device , kIOServicePlane , CFSTR("idProduct") , NULL , kIORegistryIterateRecursively | kIORegistryIterateParents); if (productId) { print_cfnumberref("\tProduct id:", productId); } printf("\n"); } void iterate_usb_devices(io_iterator_t iterator, int newdev) { io_service_t usbDevice; while ((usbDevice = IOIteratorNext(iterator))) { get_usb_device_info(usbDevice, newdev); IOObjectRelease(usbDevice); } } void usb_device_added(void* refcon, io_iterator_t iterator) { iterate_usb_devices(iterator, 1); } void usb_device_removed(void* refcon, io_iterator_t iterator) { iterate_usb_devices(iterator, 0); } 

Compiling an application is done by the command
gcc usbnotify.c -framework IOKit -framework Foundation -o notifier
clang usbnotify.c -framework IOKit -framework Foundation -o notifier
The result of executing both commands will be identical.

Below you can see screenshots of the running application with information about the connected and disconnected devices.


Thanks for attention.

Source: https://habr.com/ru/post/145855/

All Articles