Работа с USB устройствами в программе на C в MacOS X

в 22:26, , рубрики: mac os x, usb, Программирование, метки: ,

Приветствую уважаемых читателей.
В этой небольшой статье я бы хотел рассмотреть вопрос взаимодействия с оборудованием (в данном случае с USB устройствами) в операционной системе MacOS X.
Будет рассмотрены очень интересный и полезный фреймворк IOKit, способ получения уведомлений о добавлении/удалении обордования, а так же получения полной информации об устройствах. Конечно данный материал не претендует на какую либо уникальность, т.к. каждый может разобраться с этими вопросами самостоятельно, изучив документацию Apple, а так же покурив разнообразные исходники на opensource.apple.com
Моя статья — попытка восполнить пробелл в русскоязычном материале подобного рода и описать некоторые грабли, с которыми могут столкнуться новичики.
Все кто заинтересовался — добро пожаловать под кат.

IOKit.
В ядре MacOS X существует замечательная вещь — фреймворк IOKit. Это объектно-ориентированный С++ фреймворк, разработанный специально для поддержки инфраструктуры драйверов оборудования. Правда С++ там несколько урезанный, например в нем отсутствуют исключения, RTTI, шаблоны. Остальная же часть ядра написана преемущественно на С.
Сам IOKit можно разделить на две части: собственно kernel framework, на котором пишутся драйвера и юзерспейсный IOKit.framework, предназначенный для простого доступа к kernel модулям (и соответственно оборудованию) из пользовательских приложений.
Объектно-ориентированная природа IOKit позволяет удобно отображать реальную физическую модель оборудования: USB устройство подключено, через USB порт подключено к хабу, хаб подключен соответствующему контроллеру в компьютере, этот контроллер, через шину PCI подключен к остальному чипсету материнской платы. Каждый IOKit драйвер является в этой модели либо конечным узлом (например USB модем) либо же связующим модулем (например контроллер USB порта), в терминологии Apple, последний назыается nub.
В комлект поставки XCode входит графическая утилита IORegExplorer, позволяющая просмотреть всю структуру IOKit модулей, прочитать разнообразные параметры и техническую информацию.
Работа с USB устройствами в программе на C в MacOS X
Сущетствует и консольный аналог утилиты — ioreg.

В этой статье будет рассмотрена работа только с юзерспейсным IOKit.framework. Если кого-то из читателей заинтересовал kernel framework — можно прочитать статью habrahabr.ru/post/36875/, а так же замечательную книгу OS X and iOS Kernel Programming.

Получение уведомлений USB
Как уже говорилось выше, работать мы будем с USB, поэтому нам понадобится всего один заголовочный файл

#include <IOKit/usb/IOUSBLib.h>

Начнем с того, что подпишемся на получение уведомлений о добавлении/удалении USB устройств. В этом нам поможет специальный объект типа IONotificationPortRef.
Создадим и проинициализируем экземпляр IONotificationPortRef
IONotificationPortRef notificationPort = IONotificationPortCreate(kIOMasterPortDefault);
Мы создали своего рода виртуальный порт, для прослушивания событий из IOKit (пока что не определено, каких именно). kIOMasterPortDefault — это константа, определяющая некий порт «по умолчанию» для получения сообщений из IOKit, всегда используйте это значение.

Теперь определим, какие именно сообщения мы хотим получать. Для этого необходимо создать так называемый словарь, воспользовавшись соответствующим контейнером базового фреймворка MacOS X.
CFMutableDictionaryRef matchDict = (CFMutableDictionaryRef) CFRetain(IOServiceMatching(kIOUSBDeviceClassName));
Ключевым методом тут является IOServiceMatching, создающий словарь устройств класса USB, думаю, что это очевидно (существуют и другие константы, например для FireWire и т.п., позволяющие получать соответствующие уведомления). Далее мы «забираем» владения этим объектом, с помощью CFRetain. Это нужно для того, что бы не возникли проблемы с возможным двойным освобождением объекта в методах фреймворка IOKit. Собственно нам самим освобождать этот объект не нужно, в других же случаях никогда не забывайте вызывать CFRelease для аллоцированных в куче объектов (не полученных каким-либо Get* методом). Кроме того, всегда проверяйте, свежесозданные/свежеполученный *Ref объекты базового фрейморвка, на NULL, т.к. это, по сути, обычные указатели и работать с ними нужно соответствующе.

Далее «навесим» этот словарь на ранее созданный порт, как matching notification. Делается это методом IOServiceAddMatchingNotification.
Рассмотрим его внимательно

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

Первый параметр notifyPort — собственно порт, созданный нами ранее.
Второй параметр notificationType — тип уведомлений для данного словаря, такие как kIOTerminatedNotification, kIOMatchedNotification, kIOFirstMatchNotification. Думаю, что из названий этих констант вполне понятно, что они означают.
Третрий параметр matching — собственно словарь. Тип объекта, как можно заметить, — CFDictionaryRef. Это не проблема, т.к. CFMutableDictionaryRef без проблем приводится в тип CFDictionaryRef.
Четвертый метод callback — самый интересный. Это указатель на callback метод, вызываемый при поступлении соответствующего события. О нем чуть ниже.
Пятый параметр refCon — контекст коллбэк метода, тут можно передать свои данные этой функции.
Последний параметр notification — итератор, предназаченный для перебора коллекции устройств, этот аргумент инициализуруется в IOServiceAddMatchingNotification и так же передается callback методу, указывая на самое первое устройство в коллекции.

В случае успеха IOServiceAddMatchingNotification возвращает нуль, иначе — положительное число, код ошибки.

Callback функция определена таким образом:
typedef void (*IOServiceMatchingCallback)(void *refcon, io_iterator_t iterator);
Как видно, функция принимает те самые последние два аргумента из IOServiceAddMatchingNotification.

Нам необходимо объявить две такие функции — на добавление и на удаление устройств

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

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

А теперь, наконец, настроим уведомления.

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);

Никакой дополнительной инициализации методов не требуется и обязательно проверяйте result код после каждого вызова IOServiceAddMatchingNotification.

Теперь нам необходимо настроить поток с циклом, в котором будет крутится наш прослушиватель. В MacOS X для данных целей предусмотрен соответствующий объект — CFRunLoop. Для нормальной работы данному объекту необходимо указатать так называемый источник и запустить в контексте текущего либо же какого-то другого потока, а после окончания работы — остановить.
Сделаем это так:

CFRunLoopAddSource(CFRunLoopGetCurrent(), IONotificationPortGetRunLoopSource(notificationPort), kCFRunLoopDefaultMode);

В данном случае мы добавляем источник уведомлений с помощью специального метода IONotificationPortGetRunLoopSource, использую наш порт уведомлений, кроме того, мы указываем идентификатор необходимого поток — в данном случае текущий поток CFRunLoopGetCurrent()

Далее этот поток можно стартовать и начинать получать уведомления. Но есть один нюанс, что бы все заработало — коллбэк методы нужно единожды вызвать вручную, как бы «протерировав» коллекцию. Если этого не сделать — уведомления приходить не будут.

Стар потока выполняется методом CFRunLoopRun(); Все, в этой точке выполнение кода не будет идти дальше. Остановить этот цикл можно только с помощью CFRunLoopStop, аргументом, этому методу, передается идентификатор того потока, где был запущен CFRunLoopRun, собственно то значение, что вернуло выше CFRunLoopGetCurrent. В многопоточной среде рекомендую сохранять это значение, что бы иметь возможность остановить RunLoop из контекста другого потока, просто указав корректный, сохраненный идентиификатор.

После остановки потока необходимо немного прибрать за собой:

CFRunLoopRemoveSource(CFRunLoopGetCurrent(), IONotificationPortGetRunLoopSource(notificationPort), kCFRunLoopDefaultMode);
IONotificationPortDestroy(notificationPort);

Получение информации о USB устройствах
Итак, мы научились получать уведомления о добавлении/удалении USB устройств, теперь посмотрим как можно получить всю информацию об устройствах, непосредственно в callback методах.
По хорошему, оба методы должны сводится к вызову одного метода iterate_usb_devices

void iterate_usb_devices(io_iterator_t iterator)
{
	io_service_t usbDevice;

	while ((usbDevice = IOIteratorNext(iterator))) {
		....
		IOObjectRelease(usbDevice);
	}
}

Здесь мы осуществляем проход по коллекции устройств, с помощью переданного итератора и IOIteratorNext, после использования объект необходимо освобождать в IOObjectRelease.
Каждый полученный объект usbDevice является источником всей необходимой информации. Простейший пример:

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

В случае успеха будет выведено читабельное имя устройства, например «Novatel wireless modem». io_name_t является по сути обычной char массивом.

Другой пример, получение полного пути к устройству, в иерархии IOKit:

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

Это самые простейшие случаи, т.к. с объектом usbDevice связан ещё целый словарь разнообразнейших параметров в виде пары ключ-значение. По соответствующим ключам можно получать соответствующие значения параметров, например, получим VendorID устройства.

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

Данный метод ищет соответствующий параметр по строковому ключу, в данном случае «idVendor» (CFSTR — это метод фреймворка MacOS X, выполняющий быстрое преобразование C-строки во внутренний объект типа CFStringRef)

IORegistryEntrySearchCFProperty возвращает NULL, если поиск не увенчался успехом, либо же указатель на объект искомого типа, в данном случае — CFNumberRef. Что бы «выудить» нормальное числовое значение из CFNumberRef, необходимо воспользоваться следующим:

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

Получение «ProductID» выполняется точно таким же образом, только в качестве ключа следует использовать строку «idProduct»

Разумеется, что список параметров и соответствующих ключей можно сильно варьироваться от устройства к устройству. У читателя может возникнут соответствующий вопрос — как узнать, какие параметры и по каким ключам нужно искать у данного устройства и как отобразить сразу все значения?
Делается это довольно просто:

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

В случае успешного выполнения данного метода мы получим в свое распоряжение словарь, заполненный всеми ключами/значениями устройства.
Самый простой способ, теперь, для печати этого словаря на экран — фреймворковый CFShow. Пару слов о нем, CFShow — это универсальный метод для задампливания любых объекто CoreFoundation Framework в MacOS X — CFMutableDictionaryRef, CFStringRef, CFNumberRef и т.д. Вывод выполняется в stderr.
Итак, выводим содержимое словаря на экран:

CFShow(properties);

Теперь у нас есть список всех ключей и всех параметров, для данного устройтсва и мы можем использовать это знание, для получение конкретных значений в виде конкретных объектов (либо через IORegistryEntrySearchCFProperty, либо непосредственно из словаря, воспользовавшись методами для работы со словарями)

Рабочее приложение.
Теперь я бы хотел привести пример небольшого приложения, содержащего все, что выше описано. Кроме всего прочего, я решил добавить в этот пример обработку сигналов, что бы уметь корректно завершать приложение по Сtrl-С.

#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 ---> Okn");
}

void configure_and_start_notifier()
{
	printf("Starting notifier...nn");

	CFMutableDictionaryRef matchDict = (CFMutableDictionaryRef) CFRetain(IOServiceMatching(kIOUSBDeviceClassName));

	if (!matchDict) {
		fprintf(stderr, "Failed to create matching dictionary for kIOUSBDeviceClassNamen");
		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 kIOMatchedNotificationn");
		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 kIOTerminatedNotificationn");
		return;
	}

	usb_device_removed(NULL, deviceRemovedIter);

	CFRunLoopRun();
}

void deinit_notifier()
{
	CFRunLoopRemoveSource(CFRunLoopGetCurrent(), IONotificationPortGetRunLoopSource(notificationPort), kCFRunLoopDefaultMode);
	IONotificationPortDestroy(notificationPort);
	printf("deinit_notifier ---> Okn");
}

void signal_handler(int signum)
{
	printf("ngot signal, signnum=%i  stopping current RunLoopn", 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 %sn", prefix, cVal);
	}

	free(cVal);
}

void print_cfnumberref(const char* prefix, CFNumberRef cfVal)
{
	int result;
	if (CFNumberGetValue(cfVal, kCFNumberSInt32Type, &result)) {
		printf("%s %in", 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: %sn", newdev ? "FOUND" : "REMOVED", devicename);

	if (IORegistryEntryGetPath(device, kIOServicePlane, entrypath) == KERN_SUCCESS) {
		printf("tDevice entry path: %sn", entrypath);
	}

	if (IOObjectGetClass(device, classname) == KERN_SUCCESS) {
		printf("tDevice class name: %sn", 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);
}

Компиляция приложения выполняется командой
gcc usbnotify.c -framework IOKit -framework Foundation -o notifier
или же
clang usbnotify.c -framework IOKit -framework Foundation -o notifier
Результат выполнения обеих команд будет идентичен.

Ниже можно увидеть скриншоты работающего приложения с выводом информации о подключаемых и отключаемых устройствах

Работа с USB устройствами в программе на C в MacOS X

Работа с USB устройствами в программе на C в MacOS X

Работа с USB устройствами в программе на C в MacOS X

Работа с USB устройствами в программе на C в MacOS X

Работа с USB устройствами в программе на C в MacOS X

Список литературы:
goo.gl/LRyIr
goo.gl/O1Oyk
goo.gl/NQUtL
goo.gl/daEiS
goo.gl/7jkUs
goo.gl/yeJre
www.amazon.com/OS-X-iOS-Kernel-Programming/dp/1430235365
habrahabr.ru/post/36875/

Спасибо за внимание.

Автор: elenbert

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js