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)
context.move(to: p1)
context.addLine(to: p2)
context.stroke()
両ライブラリの内容は名前そのままで、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)
}
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 では enum
や struct
に LocalizedError
というプロトコルを実装すればエラーとして使用したり表示するメッセージを用意したりできる。
今回のライブラリを作っていて一番感動したことなのだけど、大昔から IOKit で使われている IOReturn
というエラーコードの型(enum
ですらなく大量の #define
で各コードが定義されている)が、extension
で LocalizedError
に適合させる後付け作業だけでそのまま 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
...
extension IOReturn: LocalizedError
{
...
}
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)
} ...
さらに、throwIfNotSuccess()
というメソッドも用意。これを使えば古い関数が try
をつけて呼べるようになるのだ。
try interfaceInterface.pointee?.pointee.ResetPipe(interfaceInterface, 1).throwIfNotSuccess()
extension
だけでここまでできるなんて、Objective-C 時代の手間を考えるととにかく Swift の自由度に感動してしまう。(Objective-C もこれはこれで別の変態的な方向に自由度が高いので嫌いではないのだが。むしろ愛してる♡)
ちなみに errorDescription
と recoverySuggestion
は最低限しか用意せず、アプリケーションによるカスタマイズ前提にしてある。
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 ともに色々なライブラリを触ってみて痒いところに手が届かない気持ちをいつも感じていたのだけど、だいぶ改善できたのではないかと思う。