MacOS XのCプログラムでUSBデバイスを操作する

親愛なる読者への挨拶。

この短い記事では、MacOS Xオペレーティングシステムの機器(この場合はUSBデバイス)との相互作用の問題を検討したいと思います。

非常に興味深く有用なIOKitフレームワークが検討されます。これは、機器の追加/削除、およびデバイスに関する完全な情報の取得に関する通知を受信する方法です。 もちろん、この資料は独自性を主張していません。 Appleのドキュメントを勉強し、opensource.apple.comでさまざまなソースを吸ったことで、誰もがこれらの問題に自分で対処できます。

私の記事は、この種のロシア語資料のギャップを埋め、新人が遭遇する可能性のあるレーキについて説明する試みです。

興味のある方-猫へようこそ。





IOKit。

MacOS Xカーネルには素晴らしいものがあります-IOKitフレームワークです。 これは、ハードウェアドライバーインフラストラクチャをサポートするために特別に設計されたオブジェクト指向C ++フレームワークです。 確かに、C ++はそこでは多少切り捨てられています。たとえば、例外、RTTI、テンプレートはありません。 カーネルの残りの部分は主にCで書かれています。

IOKit自体は、ドライバーが記述されるカーネルフレームワーク自体と、ユーザーアプリケーションからカーネルモジュール(およびハードウェア)に簡単にアクセスできるように設計されたユーザー固有のIOKit.frameworkの2つの部分に分けることができます。

IOKitのオブジェクト指向の性質により、機器の実際の物理モデルを便利に表示できます:USBデバイスが接続され、ハブに接続されたUSBポートを介して、ハブはコンピューターの対応するコントローラーに接続され、このコントローラーはPCIバスを介してマザーボードチップセットの残りの部分に接続されます。 このモデルの各IOKitドライバーは、エンドノード(USBモデムなど)または接続モジュール(USBポートコントローラーなど)のいずれかです。Appleの用語では、後者はnubと呼ばれます。

Xcodeデリバリキットには、グラフィカルユーティリティIORegExplorerが含まれています。このユーティリティを使用すると、IOKitモジュールの構造全体を表示し、さまざまなパラメーターと技術情報を読み取ることができます。



ユーティリティのコンソールアナログ-ioregもあります。



この記事では、ユーザー固有のIOKit.frameworkのみを扱います。 読者のいずれかがカーネルフレームワークに興味を持っている場合は、記事habrahabr.ru/post/36875と、素晴らしい本OS XおよびiOSカーネルプログラミングを読むことができます。



USB通知を受け取る

上記のように、USBで作業するため、必要なヘッダーファイルは1つだけです。

#include <IOKit/usb/IOUSBLib.h>
      
      





まず、USBデバイスの追加/削除に関する通知を受け取るように登録します。 IONotificationPortRefのような特別なオブジェクトがこれに役立ちます。

IONotificationPortRefのインスタンスを作成して初期化します

 IONotificationPortRef notificationPort = IONotificationPortCreate(kIOMasterPortDefault);
      
      





IOKitからのイベントをリッスンするための一種の仮想ポートを作成しました(どのポートがまだ決定されていないか)。 kIOMasterPortDefaultは、IOKitからメッセージを受信するための特定のデフォルトポートを定義する定数で、常にこの値を使用します。



次に、受信するメッセージを決定します。 これを行うには、MacOS Xベースフレームワークの適切なコンテナを使用して、いわゆる辞書を作成します。

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





ここで重要なメソッドは、USBクラスデバイスのディクショナリを作成するIOServiceMatchingです。これは明らかだと思います(たとえば、FireWireなど、通知の受信を許可する他の定数があります)。 次に、CFRetainを使用してこのオブジェクトの所有権を「取得」します。 これは、IOKitフレームワークメソッドでオブジェクトが二重にリリースされても問題がないようにするために必要です。 実際、このオブジェクトを自分で解放する必要はありません。他の場合は、ヒープに割り当てられたオブジェクト(Get *メソッドで受信されていない)に対してCFReleaseを呼び出すことを忘れないでください。 また、常にチェックして、新しく作成された/新しく受信された*基礎となるフレームワークのRefオブジェクト。 実際、これらは通常のポインターであり、それに応じて作業する必要があります。



次に、一致する通知として、この辞書を以前に作成したポートに「ハング」させます。 これはIOServiceAddMatchingNotificationメソッドによって行われます。

慎重に検討してください



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







notifyPortの最初のパラメーターは、以前に作成した実際のポートです。

2番目のnotificationTypeパラメーターは、このディクショナリの通知のタイプ( kIOTerminatedNotificationkIOMatchedNotificationkIOFirstMatchNotificationなど)です。 これらの定数の名前から、それらが何を意味するのかは非常に明確だと思います。

3番目に一致するパラメーターは辞書自体です。 ご覧のとおり、オブジェクトのタイプはCFDictionaryRefです。 これは問題ではありません CFMutableDictionaryRefは、CFDictionaryRefに簡単にキャストできます。

4番目のコールバックメソッドが最も興味深いです。 これは、対応するイベントが到着したときに呼び出されるコールバックメソッドへのポインタです。 彼についてもう少し低い。

5番目のパラメーターrefConはコールバックメソッドのコンテキストです。ここでは、この関数にデータを転送できます。

最後の通知パラメーターは、デバイスコレクションを反復処理するように設計されたイテレーターです。この引数はIOServiceAddMatchingNotificationに初期化され、コレクション内の最初のデバイスを示すコールバックメソッドにも渡されます。



成功した場合、IOServiceAddMatchingNotificationはゼロを返し、そうでない場合は正の数、エラーコードを返します。



コールバック関数は次のように定義されます:

typedef void (*IOServiceMatchingCallback)(void *refcon, io_iterator_t iterator);





ご覧のとおり、この関数はIOServiceAddMatchingNotificationの最後の2つの引数を受け入れます。



このような関数を2つ宣言する必要があります-デバイスを追加および削除するため



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







追加のメソッドの初期化は必要ありません。IOServiceAddMatchingNotificationを呼び出すたびに結果コードを確認してください。



次に、リスナーがスピンするループを使用してストリームを構成する必要があります。 MacOS Xは、これらの目的に対応するオブジェクトCFRunLoopを提供します。 このオブジェクトの通常の操作では、いわゆるソースを指定し、現在のストリームまたは他のストリームのコンテキストで開始し、作業が完了したら停止する必要があります。

このようにしましょう:

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





この場合、特別なIONotificationPortGetRunLoopSourceメソッドを使用して通知ソースを追加し、通知ポートを使用します。さらに、必要なストリームの識別子を指定します。この場合、現在のストリームCFRunLoopGetCurrent()



その後、このストリームを開始して、通知の受信を開始できます。 しかし、すべてが機能するという1つの警告があります。コールバックメソッドは、コレクションを「テスト」するかのように、手動で1回呼び出す必要があります。 これが行われない場合、通知は届きません。



ストリームの開始はCFRunLoopRun()によって実行されます。 この時点で、コードの実行はそれ以上進みません。 このサイクルは、引数としてCFRunLoopStopを使用してのみ停止できます。このメソッドは、CFRunLoopRunが起動されたスレッドの識別子、実際にはCFRunLoopGetCurrentの上に返された値を渡します。 マルチスレッド環境では、正しい保存された識別子を指定するだけで、別のスレッドのコンテキストからRunLoopを停止できるように、この値を保持することをお勧めします。



フローを停止した後、少し整理する必要があります。

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







USBデバイスに関する情報を取得する

そこで、USBデバイスの追加/削除に関する通知を受け取る方法を学びました。次に、コールバックメソッドでデバイスに関するすべての情報を直接取得する方法を見てみましょう。

良い方法では、両方のメソッドを1つのメソッド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: %s\n", devicename); }
      
      







成功すると、「Novatel wireless modem」などの読み取り可能なデバイス名が表示されます。 io_name_tは基本的に通常のchar配列です。



IOKit階層でデバイスへのフルパスを取得する別の例:



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







これらは最も単純なケースです。なぜなら、 usbDeviceオブジェクトは、キーと値のペアの形式でさまざまなパラメーターの辞書全体に関連付けられています。 対応するキーを使用して、たとえばデバイスのVendorIDを取得するなど、対応するパラメーター値を取得できます。



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







このメソッドは、文字列キーで対応するパラメーターを検索します。この場合は「idVendor」です (CFSTRは、C文字列をCFStringRef型の内部オブジェクトにすばやく変換するMacOS Xフレームワークメソッドです)



IORegistryEntrySearchCFPropertyは、検索が失敗した場合、または探しているタイプのオブジェクトへのポインター(この場合はCFNumberRef)がNULLを返します。 CFNumberRefから通常の数値を「フェッチ」するには、次を使用する必要があります。



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







「ProductID」の取得はまったく同じ方法で行われ、キー「idProduct」のみを使用する必要があります



もちろん、パラメーターのリストと対応するキーは、デバイスごとに大きく異なる可能性があります。 読者は対応する質問を持っているかもしれません-このデバイスでどのパラメータとどのキーを検索するかを見つける方法と、すべての値を一度に表示する方法

これは非常に簡単に行われます:



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







このメソッドが正常に完了した場合、デバイスのすべてのキー/値で満たされた辞書を自由に使用できます。

この辞書を画面に印刷する最も簡単な方法は、CFShowフレームワークです。 CFShowは、MacOS XでCoreFoundationフレームワークオブジェクトをエミュレートするための普遍的な方法です-CFMutableDictionaryRef、CFStringRef、CFNumberRefなど。 出力はstderrで行われます。

そのため、画面に辞書の内容を表示します。



 CFShow(properties);
      
      







これで、このデバイスのすべてのキーとすべてのパラメーターのリストが得られ、この知識を使用して特定のオブジェクトの形式で特定の値を取得できます(IORegistryEntrySearchCFPropertyを使用するか、辞書を操作するメソッドを使用して辞書から直接)



動作中のアプリケーション。

次に、上記のすべてを含む小さなアプリケーションの例を示します。 特に、Ctrl-Cを使用してアプリケーションを正しく完了するために、この例に信号処理を追加することにしました。

注:実際のプロジェクトでは、シグナルハンドラーでこのようなアクションを実行しないでください。理由は、たとえばこのメッセージを読むことができます



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







アプリケーションのコンパイルは、次のコマンドによって行われます

gcc usbnotify.c -framework IOKit -framework Foundation -o notifier





または

clang usbnotify.c -framework IOKit -framework Foundation -o notifier





両方のコマンドを実行した結果は同じです。



以下に、実行中のアプリケーションのスクリーンショットと、接続および切断されたデバイスに関する情報を見ることができます























参照:

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



ご清聴ありがとうございました。



All Articles