だいぶ実装がアレな SkypeLogger の解説
Mac OS X のアプリケーションを作る方法が Objective-C だけとは、限りません。
今回はそういった意味ではだいぶ実装がアレな拙作 SkypeLogger の実装を見ながら、こんな作り方もあるよねということを紹介したいと思います。
SkypeLogger の紹介
まず、SkypeLogger ですが、何をするものかというと、Skype のチャットログを人間が読める形で普通のファイルに書きだしてくれるツールです。Skype.app 自体にログを残す機能はあるのですが、人間が読めないし、機械でも読めない独自の形式1で書き出されるので不便なのです。 そこで、Skype API を使ってチャットの発言の送信、受信イベントをもとにチャットの発言を逐一ファイルに書き込んでいくだけの簡単なお仕事をしていただく、というツールです。
しかしこの Skype API が曲者で、API とは名ばかりで Skype.app
と通信するインターフェイスが提供されるだけで、実際に Skype.app
が起動していないと使えません。今回のような用途に限ってはそれでも問題ないのですが、ボットを作る、とかだとちょっと面倒2ですね。
SkypeLogger の概要
さて以上を踏まえてこの SkypeLogger の実装についてお話しましょう。仕組みはこうです。
まず Skype.app
の起動を検出して自身も同時に起動します。これには SIMBL を使っています。SIMBL とは、Mac OS X の既存の(ほとんど、どんな)アプリケーションでも拡張できるプラグインを作るためのプラグインです。具体的には、Reederを Growl に対応させたりとか、そういうツールを作るのに使います。
動作原理についてはまた後述するとして、全体像を把握するために、ここでは SIMBL は他のアプリの起動と同時にアプリと一緒にプラグインを読み込んでくれるツールと考えてください。
SkypeLogger は、この SIMBL を使って薄い Objective-C で書かれたコードを Skype に読み込ませます。次に、そのコードは ruby
をつかって Ruby で書かれた Skype API を使ってログを取るツールを内部的に起動します。これには RubyCocoa を使っています。そしてそのツールが Skype.app
と通信をしてログをLogger
を使って所定のファイルに書き出していきます。
あとは Objective-C で書かれた部分で Skype.app
の終了通知を監視して、Skypeが終了したら、内部的に起動したツールも一緒に終了させます。
と、まあこういう仕組みで動いているのですが、つまり、このツールというかアプリ、Objective-C と、Ruby (と RubyCocoa) の2つを使って書かれています。このあたり、各言語ごとにブリッジが存在している Objective-C ならではですね。
ここまで見るとわかると思うのですが、Ruby で書かれた部分は SIMBL を使う Objective-C で書かれた部分と完全に切り離されていることに気がつくと思います。実際、この Ruby 部分だけでロガーとして別に手動で起動することも可能です。これには諸事情3あってこのような実装になっています4。
SIMBL の仕組み
では、まず起点となる SIMBL から見て行きましょう。SIMBL はほとんどのMac OS Xのアプリにプラグイン機能を提供してくれます。つまり、ちょっーっと足らないあと一歩の機能を自分で追加したりすることができます。
さて、その仕組みなのですが、Mac OS X のバージョンアップを重ねるにつれて複雑化してきています。その昔は Mac OS Xのアプリが Library/InputManagers
にあるプラグインを自動的に読み込む機能を経由して独自のプラグインを読みこませることをしていましたが、現在は /Library/ScriptingAdditions
とデーモンによる複雑な手法になっています5。
まず、対象のアプリケーションが起動すると /Library/ScriptingAdditions
にあるスクリプト拡張プラグインを読み込みます。が、実行はされません。SIMBL はここにスクリプト拡張プラグインを置いて、対象のアプリケーション内部にスクリプティング用のエンドポイントを提供します。
次にバックグラウンドで動いているデーモン SIMBL Agent
がアプリケーションの起動監視をしていて、アプリケーションが起動され次第、事前にスクリプト拡張で作っておいたエンドポイントをスクリプティング経由で叩きます。
その後、対象のアプリケーション内部で Library/Application Support/SIMBL/Plugins
から読み込むべきプラグインを探して +[NSBundle bundleWithPath:]
で対象のアプリケーションに読み込みます。
詳しい挙動はSIMBLのソースを確認してください。
なんだか面倒な仕組みですが、このような努力の結果、Library/Application Support/SIMBL/Plugins
にプラグインを置いておけば、対象のアプリが起動したときに自動で読み込んで実行してくれます。
SIMBL プラグインを作る
ここからは SkypeLoggerのソース を片手にお読みください。
SIMBL プラグインを作るには、Xcode で Bundle を作り、Info.plist
に対象とするアプリケーションの Bundle identifier
とバージョンを記載しておきます。Skype.app
の場合はこんな感じ。
そして、SIMBL がこの Bundle を読み込んだ際に Principal class の +load
が呼ばれるのでそこにやりたいことを書きます。
大抵は対象のアプリケーションの一部のメソッドを書き換えたり奪ったりしてやりたいことを追加したりします。 書き換えには Objetive-C のランタイムの機能を使ったり、NSObject
のカテゴリを使ったりする手法がありましたが、最近は method_exchangeImplementations
を使うと楽です。
しかし、SkypeLogger
はアレなのでそういうことはせずに ruby
を内部的に起動します。つまり、fork
します。SkypeLoggerLoader.m
あたりをご覧ください。
とても普通に fork
ですね。親プロセス(つまり Skype.app)側では fork
後、アプリケーションの終了通知 NSApplicationWillTerminateNotification
を待っておきます。
子プロセス(つまり、Ruby スクリプト)側は Skype API の使用準備を始めます。
親プロセスは、つまり Skype.app
の終了通知が来たら、子プロセスに、つまり fork
した ruby
にシグナルを送って死んでもらいます。
以上、ここまでは Objective-C で書かれています。
Skype API を使う
ここからは Ruby の世界になります。/lib
以下をご覧ください。
Mac OS X 標準添付の Ruby では RubyCocoa が提供されていて6、require "osx/cocoa"
することで Objective-C のクラスとRubyのクラスを行ったり来たりすることができます。
Skype API を使うには Skype.framework
を読み込んで +[SkypeAPI setSkypeDelegate:]
でコールバックされるクラスを提供して +[SkypeAPI sendSkypeCommand:]
でコマンドを送ります。
これを RubyCocoa を使って Ruby で書くとこうなります。
簡単ですねー。あんまり深いことは考えなくても動くのでいろいろ楽ですが、ちょっとミスると簡単にプロセスが死ぬので怖いです。
Skype API のバグ? 仕様変更?
Skype API は基本的に同期呼び出しが可能だったはずなのですが、あるバージョンから突然同期呼び出しができなくなりました7。つまり SkypeAPI.sendSkypeCommand()
を読んでも結果はすぐに返らずに非同期で SkypeAPI.setSkypeDelegate()
で登録したクラスの skypeNotificationReceived()
が呼ばれます。
というわけで SkypeLogger では同期呼び出しが出来るか出来ないかを最初のメッセージを受け取った時に判断して以降のメッセージハンドラを差し替えるという キモい かっこいいことをしています。メッセージを受け取るたびに if
文が走るなんてことはありません。
まとめ
Mac OS X のアプリケーションを作るには様々な方法があります。アプリケーションの形態も様々で単体のアプリケーションからこういうようなプラグイン形式のものなどいろいろあります。
AppStore 全盛期にアプリケーションはこれこれこういう形でこう作るんだよ、みたいな風潮がありますが、もっと本当は自由なんですよーってことでまとまったようなのでオシマイ!
感想とか、こうしたほうがいいとか、これはマズいとかは @niw にツイートしてもらえると嬉しいです。
-
確か、けっこう頑張って誰かが解析してたはず。 ↩︎
-
Linux用のバイナリを使ってコンソールで使えるようにしていた記事もあったはず。 ↩︎
-
実は最初は内部的に別プロセスの Ruby を起動するようにはせず、直接 RubyCocoa で書かれたコードをSIMBLから呼ぶようにしていました。しかし、どうやら
Skype.app
は同じプロセスがAPIを使うことを想定していないようで、このような実装だと毎回APIアクセスの許可を求めるダイアログが出てしまう面倒くさい挙動になってしまったので、別プロセスを起動するようにしました。 ↩︎ -
さらに補足すれば、実際のところ、SIMBLをつかって同時起動しようと思ったのはずいぶん後で、昔は手動で Ruby の部分だけ起動していました。 ↩︎
-
年々厳しくなります。 10.8 ではどうなることやら。 ↩︎
-
実はこっそり MacRuby も標準搭載されているのですが、PrivateFramework で簡単に使えません。 ↩︎
-
5.6 頃から? ↩︎