うさぎ駆動開発

UWP, Xamarin.Macを中心によしなしごとを書いていきます。

Xamarin バインディングされていないAPIを使う

この記事は Xamarin(その2) Advent Calendar 2016 - Qiita の25日目の記事です。

みなさん Xamarin のカバーする範囲で満足してませんか。たしかにその範囲に収めるように開発すれば非常に生産性が高く,気持ちよく書いていくことができます。しかしながら需要や実装コストの兼ね合いからXamarinでバインディングされていないAPIというのが結構あります。これを使って行くための取っかかりを提供することで,ラスボスとしての責務を果たしましょう。

なお Xamarin.Mac のみがターゲットです。他でもおおむね同じような手法を取ることができるはずですが,カバーされてないAPI量はMacの方が圧倒的なので。

アミューズ: ドキュメントにはあるが,バインディングされていないもの

サンドボックス環境下(= AppStoreビルド)においてアプリケーションが ユーザーの選択を待つことなく 自分のデータを保持できるのは ~/Library/Containers/{app identifier}/Data です。これを取得するAPI NSHomeDirectory() が存在していますが,バインディングされていません。

NSHomeDirectory() - Foundation | Apple Developer Documentation

これはこのように展開できます。

基本的には IntPtr で受けて,NSObject に変換してやればいいです。NSObject にさえなってしまえば,DebugDescription プロパティを読むことでおおよそ何者かがわかります。この時点で NSObject に変換できなかった場合は問答無用でアプリケーションが落ちます。おおむね,そのままポインタで扱わなければならないことを意味します。

前菜: ドキュメントにないので,バインディングされていないもの

非公開APIというものが世の中にはあって,これがぐっとくる機能を提供していたりします。OSバージョンアップに伴っていつの間にか消えていたりするのは玉に瑕ですが,どうしてもやりたいことができないときの最終手段としては有用です。

調べ方と使ったときに考えなくてはならないリスク,AppStore審査への注意については別の機会に譲るとして,ここでは初心者にもわかりやすい NSTouchDevice というクラスを触ってみることにしましょう。これはシステムに接続されているタッチ操作を受け入れるデバイスを列挙できます。

NSTouchDevice がまずは存在しているかを確認します。これは定番の書き方ですので覚えましょう。

IntPtr handle = ObjCRuntime.Class.GetHandle("NSTouchDevice");

APIバージョンが古く利用不可能な場合は IntPtr.Zero が返ります。未検証ですが,10.10 Yosemiteから使えるはずです。うまく取得できたら,静的クラスとして呼び出してみます。その上で,感圧タッチ対応デバイスがあるかどうか調べてみましょう。

NSObject deviceRoot = ObjCRuntime.Runtime.GetNSObject(handle);
NSObject ret = deviceRoot.ValueForKey(new NSString("_hasForceCapableTouchDevice"));                

感圧タッチ対応デバイスの存在は _hasForceCapableTouchDevice の boolValue を調べることでわかります。値を調べるには NSObject.ValueForKey を使い,直接キーを指定します。なおキーが間違っていた場合は, NSUnknownKeyException 例外が送出されます。

ではさらに接続されているデバイスの列挙を取得しましょう。NSArrayで受けます。

NSArray devices = (NSArray)deviceRoot.ValueForKey(new NSString("touchDevices"));

これも先ほどと同じ手法で可能でした。簡単です。では列挙できたデバイスが触覚フィードバック対応か調べて,対応だった場合はさらに軽くコツンとフィードバックさせてみましょう。TouchBar 搭載のMac では二つのデバイスが列挙され,一つはフィードバック非対応であることがわかります。今までのコードと組み合わせて,だいたいこんな感じに書きます。

混沌としていますが,挿入したコメント通りです。最終的には objc_msgSendセレクタと適切な引数を渡してやれば,呼べないものはありません。このコードを実行すると,感圧トラックパッドに指を載せていればコツンとほんとうに小さなフィードバックがあります。

フィードバックするだけなら NSHapticFeedbackPerformer が Xamarin バインディングされてるのでその方がお手軽です。というかそうすべきです。わたしがやりたかったのは自由な強さと時間でフィードバックさせることだったのですが,これを可能にする + (void)actuateWithPattern:(long long)arg1 forEvent:(id)arg2; は10.11で削除されてしまっていたようです。無念。

メインディッシュ: IOKit と仲良くなる

さて,macOSではIOKitというそこそこ大きめのフレームワークが用意されています。がしかし,

github.com

f:id:ailen0ada:20161224210555p:plain

まあ未実装なんです。というのも結構巨大で手間がかかる割に嬉しいのが少数だという,実にいつものXamarin.Mac。Bugzillaにも。

https://bugzilla.xamarin.com/show_bug.cgi?id=28503

ということで,グロサミで買ってきたBlynclightMacアプリから光らせてみようというのがメインです(ここまで長かった)。

f:id:ailen0ada:20161224212437j:plain

必要なAPIを調べる

だいたい以下のような流れで行けるのではないかと想定しました。

  1. Blynclightデバイスを探す
  2. バイスハンドルを取得
  3. バイスを開く
  4. コントロールメッセージを送信
  5. バイスを閉じる
  6. その他お片付け

ここに TouchBar とか絡ませていけば超クールに光ってクリスマス要素バッチリじゃん!という目論見です。だいたいこんな感じで NativeMethods.cs を作りました。

それぞれエントリポイントを合わせてあるので,何者か知りたい方はググってくださいませ。

バイスを開いてみる

ではこれらを組み合わせて,デバイスを開いて閉じるところまでやってみます。

おお,なんかできてるっぽい。そして開くのも閉じるのもきちんと0が返っているので成功しています。ではあと,メッセージを送るだけです。

戻り値 3758116864 とな…! これは IOReturn なので,次のように分解します。

var retVal = 3758116864;
var sys = (retVal>>26)&0x3f; // sys_iokit 0x38
var sub = (retVal>>14)&0xfff; // sub_iokit_usb 0x01
var code = retVal&0x3fff; // 0x1(000)

// #define kIOReturnInvalid         iokit_common_err(0x1)   // should never be seen

えー…。

要するに送信するメッセージが間違っていると。いうことのようです。うーむ。おっかしいなあ。

まとめ

メインディッシュが志半ばにして挫折してしまいました。ぐぬぬ

もちろんObjectiveCなりSwiftなりで目的に合ったAPIラッパーを書いてあげて,それを呼び出すというアプローチもありますし,その方が実戦では使うことは多いかもしれません。

しかしXamarinをせっかく使っているので,C#だけで解決するアプローチも覚えておいて損はありません。ちょいちょいっとAPI叩きたいときにまあ結構何とかなることもあるので,飛び道具的にお勧めです。

いつか光ったら続きを書きます。みなさんよいお年をお迎えください。