togo

= ひとりごと to go

zumuya の人による机の上系情報サイト

2 つの Swift ライブラリを公開 - ZMAX、ZMIOUSB

  • Apple
  • 開発
  • 配布物

先日初参加した try! Swift で、ある人に「オープンソースやらないんですか?」「Mac 関係のコード見たい!」と言ってもらえた。これはいい機会。ちょうど誰かの役に立つかもしれないコードがいくつかあったので、熱のあるうちにライブラリとして GitHub で公開することに。ちょっと豪華な二本立て!

古くからある、特に macOS 限定の API に関してはまだ C 関数が多くて Swift からは触りづらかったりする。Swift 3 で Core Graphics 関係のオブジェクトが Swift っぽく触れるように改善されたのをヒントに、これらを触りやすくしてみようという試みである。

CGContextMoveToPoint(context, p1)
CGContextAddLineToPoint(context, p2)
CGContextStrokePath(context)
Swift 2
context.move(to: p1)
context.addLine(to: p2)
context.stroke()
Swift 3

両ライブラリの内容は名前そのままで、ZMAX は AX(アクセシビリティ)、ZMIOUSB は IOKit の USB 通信のこと。

使用前後のコードを「こんなにすっきりした」と見せても元を触ったことのない人はまったくピンとこないと思うけど、IOKit USB なんて OS バージョンごとの互換性を保つために「構造体のポインタのポインタ」を触らなければいけない設計になっていて、それを Swift で扱うだけでも非常に頭が痛くなる話なのだ。これまではそこだけ Objective-C を混ぜて対処していた。でもやっぱり Swift で書きたい...!

ラッパークラスを作るのではなく、元の型を extension で拡張

「低レベル API なんて ObjC や Swift、C# のラッパークラスに内包させれば美しいよね」という考えで作られたライブラリが多い。確かにメモリ管理は楽になるし一時的に必要なオブジェクトなどは隠蔽して必要な部分だけ見えているので一見扱いやすいのだけど、低レベルの API を使うときはどうしても細かい制御をする必要が出てくるから結局元のオブジェクトを取り出してオリジナルの API を呼び出すコードで溢れてしまうことがよくある。特に IOKit なんて巨大すぎてとてもすべての機能をカバーしきれない。

今回作ったライブラリは基本的に元の型を extension で拡張しているだけなので、細かい制御が必要な部分ではいつでもキャストなしの自然な形でオリジナルの関数を呼び出す処理を挟むことができる。むしろ細かくゴリゴリと書きながら楽をしたい部分だけすっきりさせるような使い方にも対応できる。あくまでも Swift との橋渡しに徹するというコンセプト。元の複雑な設計にもそれなりに意味があったりするし。

let deviceInterface = try USBDeviceInterface.create(vendorIdentifier: VENDOR_ID, productIdentifier: PRODUCT_ID)
defer { deviceInterface.release() }

let interfaceInterface = try deviceInterface.createInterfaceInterface()
defer { interfaceInterface.release() }

try interfaceInterface.openAndPerform {
    try interfaceInterface.write([0x01, 0x02, 0x03], pipe: PIPE_WRITE)
    let result = try interfaceInterface.readBytes(minCount: 16, pipe: PIPE_READ)
}
Swift 風にすっきり書きながら
let ioReturn = interfaceInterface.pointee?.pointee.ResetPipe(interfaceInterface, 1)
オリジナルの関数もそのまま使える

元のエラーをそのまま throw

macOS の低レベルな C 言語の API ではエラーコードを返す関数が多い。その扱いがまた面倒でいつも悩みの種だった。実行してみて success でなければそれに対応した NSError を生成して抜けるというコードだらけに...

IOReturn ioReturn;
ioReturn = (*interfaceInterface)->ResetPipe(interfaceInterface, 0);
if (ioReturn == kIOReturnSuccess) {
    ioReturn = ...
    if (ioReturn == kIOReturnSuccess) {
        ...
    } else ...
} else {
    if (errorPtr) *errorPtr = [NSError errorWithDomain: ... code: ioReturn userInfo: ...];
    return NO;
}

Swift では enumstructLocalizedError というプロトコルを実装すればエラーとして使用したり表示するメッセージを用意したりできる。

今回のライブラリを作っていて一番感動したことなのだけど、大昔から IOKit で使われている IOReturn というエラーコードの型(enum ですらなく大量の #define で各コードが定義されている)が、extensionLocalizedError に適合させる後付け作業だけでそのまま Swift のエラーとして扱えるようになったのだ。普通に throw とか catch ができてしまう。

typedef kern_return_t       IOReturn;

...

#define kIOReturnSuccess         KERN_SUCCESS            // OK
#define kIOReturnError           iokit_common_err(0x2bc) // general error   
#define kIOReturnNoMemory        iokit_common_err(0x2bd) // can't allocate memory 
...
IOReturn の定義
extension IOReturn: LocalizedError
{
    ...
}
extension で拡張するだけで
let ioReturn = interfaceInterface.pointee?.pointee.ResetPipe(interfaceInterface, 1)
if (ioReturn != .success) {
    throw ioReturn
}
そのまま投げられるように!
do {
    try interfaceInterface.clearPipeStall(pipe: READ_PIPE)
} catch let ioReturn as IOReturn {
    switch ioReturn {
    case .timeout, .usbTransactionTimeout:
        print("timeout!")
    default:
        break
    }
    self.presentError(ioReturn)
} ...
catch もできる

さらに、throwIfNotSuccess() というメソッドも用意。これを使えば古い関数が try をつけて呼べるようになるのだ。

try interfaceInterface.pointee?.pointee.ResetPipe(interfaceInterface, 1).throwIfNotSuccess()

extension だけでここまでできるなんて、Objective-C 時代の手間を考えるととにかく Swift の自由度に感動してしまう。(Objective-C もこれはこれで別の変態的な方向に自由度が高いので嫌いではないのだが。むしろ愛してる♡

ちなみに errorDescriptionrecoverySuggestion は最低限しか用意せず、アプリケーションによるカスタマイズ前提にしてある。

IOReturn.errorDescriptionHandlers.append { error in
    NSLocalizedString(String(format: "IOReturn_description_%i", error.rawValue), comment: "")
}
IOReturn.recoverySuggestionHandlers.append { error in
    NSLocalizedString(String(format: "IOReturn_recoverySuggestion_%i", error.rawValue), comment: "")
}

ライブラリ側ですべてのローカライズを揃えるなんて不可能であるし用途によって伝えたいポイントも異なるためだ。開発者にしかわからないような内容のメッセージをユーザに表示してもしかたがないし。

USB 通信周りは macOS、Windows ともに色々なライブラリを触ってみて痒いところに手が届かない気持ちをいつも感じていたのだけど、だいぶ改善できたのではないかと思う。

Share

リンクも共有もお気軽に。記事を書くモチベーションの向上に役立てます。