2011年12月7日水曜日

CocoaPods に対応していないライブラリを集めた自分用リポジトリを作る方法

この記事はiOS Advent Calendar 2011の7日目の記事になります。ということでもうすぐクリスマスですね。クリスマスプレゼントの準備はお済みですか?まだの方はちょっとオシャレに、今年のプレゼントをCocoaPodsでご用意してみてはいかがでしょうか?


■ご存じ、ないのですか!?

さて念のためCocoaPodsについておさらい。要するにiOS/OS X用のmavenです。以上。細かい点については以下の記事が詳しいのでそちらをご参照ください。っていうかMac Dev JP Advent CalendarとネタがもろかぶりこのCocoaPodsを使うと今まで大変面倒くさかったライブラリの管理が嘘のように簡単になります。たとえば、新しいプロジェクトを始めるときに、
  • 通信したいからASIHTTPRequestを使おう
  • APIのレスポンスがJSONだからJSONKitも必要だな
  • DBにはCore Dataを採用したいから、MagicalRecordも欲しいな
  • Blocksバリバリ使うからBlocksKitは常識だよね
と思ったら、さくっと以下のような設定ファイル(Podfile, mavenで言うところのpom.xml)を書いてやれば、あとはCocoaPodsが指定されたライブラリを取ってきてビルド設定までやってくれるわけです。
platform :ios

dependency 'ASIHTTPRequest' ,'~> 1.8'
dependency 'JSONKit'        ,'~> 1.4'
dependency 'BlocksKit'
dependency 'MagicalRecord'
ARCあり・なしのライブラリを混ぜても全く問題ありません。素晴らしい!!


■公開なんて、あるわけない

とまぁ実に素晴らしいツールなのですが、問題もあります。
  • つい最近できたばかりのツールなので、対応しているライブラリが少ない
  • 対応しているライブラリでも、もともと依存関係処理をするという文化があまりなかったせいか、一部のライブラリ(Reachabilityとか)が内包された状態で出回っていたり、バージョンタグが一つや二つしか付いていないので上手くバージョン管理が出来ない(しかも極端に古かったりバグがあったり、さんざん)
  • CocoaPods自体が開発途中ということもあり、機能がどんどん追加されているようなのだがドキュメントが追いついていない
例を挙げると以下の画像のような感じで、バージョンが一つしかなかったり、あるんだけれど飛んでいたりなどなど。要するに自分の使いたいコードがCocoaPodsの中央リポジトリで管理されていないということがままあります。



■俺達は、自分たちでpodspecを用意することを......強いられているんだ!

ありがたいことに、CocoaPodsには中央リポジトリ以外の任意のリポジトリをライブラリ管理用のリポジトリとして追加する機能があります。この機能を使って、自分で対応していないライブラリのpodspecファイルを書いて、CocoaPodsで使えるようにすることができます。またCocoaPods 0.3.0以降であれば、設定ファイルに直接自分の好きなライブラリのpodspecを書くこともできるみたいです。

まず最初のステップはpodspecを書くことです。今回は例としてMKNetworkKitというライブラリのバージョンv0.8a用のpodspecを書いてみることにします。

# まずは対象のリポジトリをcloneしてくる
# ここでは相手のリポジトリを直接使ってますが、github上でforkして、そっちを使うようにしてもいいです。forkしたほうが自分で自由にコードに改変を加えたりtagを打ったりできますのでよいかも。
git clone https://github.com/MugunthKumar/MKNetworkKit.git
# 移動
cd MKNetworkKit
# 対象のバージョンにHEADを移動します
git reset --hard v0.8a
# podspecファイルのひな形を出力します
pod spec create MKNetworkKit
これでMKNetworkKit.podspecファイルが出力されますので、今度はこのファイルを書き換えます。先ほどcloneしてきたライブラリのソースコードとプロジェクト設定を見ながら、必要なソースコード、必要なリソース、不要なファイル、ライブラリやフレームワークなどのビルド設定を考えて、適切な設定を用意しなければなりません。
今回はこんな感じで書きました:
Pod::Spec.new do |s|
  s.name     = 'MKNetworkKit'
  s.version  = '0.8a'
  s.license  = 'MIT'
  s.summary  = 'Full ARC based Networking Kit for iOS 4+ devices'
  s.homepage = 'https://github.com/MugunthKumar/MKNetworkKit'
  s.author   = { 'MugunthKumar' => 'mknetworkkit@mk.sg' }
  s.source   = { :git => 'https://github.com/MugunthKumar/MKNetworkKit.git', :tag => 'v0.8a' }

  s.source_files = 'MKNetworkKit/*.{h,m}', 'MKNetworkKit/Categories/*.{h,m}'
  s.clean_paths  = 'MKNetworkKitDemo', '*.xcodeproj', 'sample.JPG'
  s.frameworks   = 'CFNetwork'
  s.requires_arc = true

  s.dependency 'Reachability', '~> 2.0'
end
大事なのはsource, source_files, frameworks, requires_arc, dependencyぐらいです。あとは自分しか使わないならでたらめでかまいません。
sourceは:tagの指定の代わりに:commitでコミットのハッシュ値を指定することもできるみたいです。
このpodspecファイルの記法、やたらとたくさんある上にドキュメントがあまりないので、私は結局公式リポジトリのpodspecを探して見よう見まねで書きました。以下、参考にした物を列挙します。
https://github.com/CocoaPods/Specs
https://github.com/CocoaPods/Specs/blob/master/ASIHTTPRequest/1.8.1/ASIHTTPRequest.podspec
https://github.com/CocoaPods/Specs/blob/master/ASIWebPageRequest/1.8.1/ASIWebPageRequest.podspec
https://github.com/CocoaPods/Specs/blob/master/BlocksKit/0.5.0/BlocksKit.podspec
https://github.com/CocoaPods/Specs/blob/master/BlocksKit/0.9.0/BlocksKit.podspec
https://github.com/CocoaPods/Specs/blob/master/SSToolkit/0.1.2/SSToolkit.podspec
https://github.com/CocoaPods/Specs/blob/master/Kiwi/1.0.0/Kiwi.podspec
https://github.com/CocoaPods/Specs/blob/master/MGSplitViewController/1.0.0/MGSplitViewController.podspec
使用するリソースファイルも指定出来るみたいです。
https://github.com/CocoaPods/Specs/blob/master/SVProgressHUD/0.5/SVProgressHUD.podspec
巨大なのになるとこんなのも書けるみたいです。
https://github.com/CocoaPods/Specs/blob/master/RestKit/0.9.3/RestKit.podspec
https://github.com/CocoaPods/Specs/blob/master/Nimbus/0.9.0/Nimbus.podspec

書き終わったら、書いたpodspecファイルに問題がないかをチェックします。
pod spec lint MKNetowrkKit.podspec
何か問題があれば何かエラーが出ます。修正しましょう。何も無ければ何も出ません。
問題が無くなったらひとまずpodspecファイルについては完成です。次はこのpodspecファイルを置くリポジトリをgithubを使って用意します。github上に適当な名前でリポジトリを作りましょう。私は今回 https://github.com/akisute/Specs というリポジトリを作りました。
リポジトリを作ったら、先ほど作ったpodspecファイルを、以下の命名規則に従ってリポジトリの中に配置します:
/podspecのs.name/podspecのs.version/先ほど作ったpodspecファイル
たとえば今回の例では:
/MKNetworkKit/0.8a/MKNetworkKit.podspec
という名前で配置する必要があります。私が試した際は、間違ってると正しくpodspecファイルを認識してくれませんでした。ファイルを配置したらこのリポジトリをgithubにpushします。

さてこれでpodspec用のリポジトリが出来ましたので、今度はCocoaPods側の設定を行います。以下のコマンドを実行します:

pod repo add myrepo リポジトリのURL
これでmyrepoという名前でリポジトリが登録されます。 ~/.cocoapods/ 以下を覗いてみると、確かに myrepo という名前のリポジトリが追加されているはずです。

あとは普通にCocoaPodsを使うのと同じ要領で、Podfileを書いて、pod installすればうまくいくはずです。・・・といいたいところなのですが、一発でうまくいくことはまれで、たいていpodspecファイルの書き方に問題があったりとか、pod化したい対象のライブラリのコードに問題があってビルドが通らないのが普通です。そこで以下のようなワークフローになります。
  1. コードに問題があるなら、コードをforkして自分の思うように書き換えてpush
  2. podspecファイルを修正して自分のpodspec用リポジトリにpush
  3. 組み込みたいプロジェクトのPodsディレクトリ、Podfile.lockファイル、生成されたxcworkspaceを削除。
  4. 再度 pod install MyProject.xcodeproj を実行。
  5. ビルド。
  6. 問題があれば1. に戻る。
うん、これは素人にはお勧めできない。

しかしながらこのCocoaPodに対応するライブラリが増えていけば、iOSの開発はずいぶんと楽になるはずです。ということで積極的に使っていきたいと思います!

2011年11月27日日曜日

Cocoa Framework に用意されていないロックを Objective-C で実装する


Cocoaフレームワークは非同期処理時のロックを取るために、NSLockingというプロトコルと、NSLock, NSRecurrsiveLock, そしてNSConditionalLockという3種類のロックの実装を提供しています。が、残念ながらちょっとまともな非同期コードを書こうと思うとこれでは全然足りません。っていうか、NSConditionalLockがロック抜けるときにしか条件値を書き換えられない実装なのが正直いけてないと思います。これじゃCounting Lock(最初に決めた数だけ同時にロックできるロック。Counting Semaphoreともいう)にもRead/Write Lock(書き込みロックと読み込みロックの二種類が用意され、書き込みロックが取られていない限りは、何個でも同時に読み込みロックが取れる、効率のいいロック)にも使えません。というわけで、Objective-Cで書かれたCounting LockとRead/Write Lockを見つけましたのでご紹介いたします。

http://cocoaheads.byu.edu/wiki/locks

中身はpthread.hのpthread_mutexを使って実装しているようです。一見危なそうですが、Cocoaフレームワークが使用するスレッドは全て内部実装がpthreadなので全く問題ありません。

2011年11月23日水曜日

静的ライブラリ中のシグネチャが衝突してビルドできないときに再ビルドしないでシグネチャを書き換える

皆さんも以下のようなビルドエラーを見たことが一度はあると思います。

これはビルド時に同一プロジェクト内に同じ名前のシグネチャの関数やクラスが存在するためリンクができなくて失敗しているというエラーです。特に以下のようなケースでよく発生します。
  • 自分が作ったクラスや関数の名前と、外部から持ってきたライブラリが使っているクラスや関数の名前が衝突している
  • 外部から持ってきたライブラリ同士でクラスや関数の名前が衝突している
  • 外部ライブラリをインストールする際に、-all_loadしたり-ObjCしたりている
そういうわけで、外部からライブラリをたくさん導入すると、base64やMD5など、プログラム上でよく使われるのに標準で用意されていないライブラリがよく衝突してしまうわけです。大抵の場合はぶつかっているシグネチャの名前をソースコード上でちょっと書き換えて再度ビルドすることで回避ができるのですが、極稀にソースコードを書き換えることができないケースが存在します。以下にそんな場合の対処方法をまとめます。


■具体例
AdMobのSDK(libGoogleAdMob.a)とopenssl(libcrypto.a)を同時に一つのプロジェクトにインストールした時、冒頭の画像のようにMD5というシグネチャがビルド時に衝突してしまうのです。AdMobのはプロプロエタリなので当然書き換えられませんし、opensslのように巨大で複雑なコードに手を入れて再ビルドするのも非常に危険です。そもそもopensslはビルド自体が難しいのです。

このような場合は、ソースコード自体を書き換えるのではなくビルド済みの静的ライブラリ側のオブジェクトファイルを書き換えることで対処を行うことが可能です。
コンパイル時のリンカの設定を変更すれば対処できそうな気もするんですが、GNU ldにはそのようなオプションが見当たりませんでした。なんかSun Solarisのldだと対処できるみたいです。
参考: http://stackoverflow.com/questions/6940384/how-to-deal-with-symbol-collisions-between-statically-linked-libraries
参考: http://stackoverflow.com/questions/393980/restricting-symbols-in-a-linux-static-library

注意: 以下の手順は失敗すると静的ライブラリ自体が完全に破損したり、実行時に深刻な問題が発生する可能性があります!! 以下の解説を見てもちんぷんかんぷんな人は適用しないことを強くおすすめいたします。この手順を適用したことによって発生したいかなる損害についても私は責任をとりかねます。


■静的ライブラリ内部のシグネチャを書き換える方法
静的ライブラリ内部のシグネチャを書き換えるには、以下のようなツールを使用します。

lipo
以前にもご紹介した、Apple純正のユニバーサルバイナリ/ユニバーサルライブラリ(fat binaryともいいます)を作ったりバラしたりするツールです。iOSのライブラリはほぼすべてがi386, armv6, armv7の三種類に対応するfat binaryになっており、基本的にApple純正でないツールはそういったfat binaryに対して歯が立たないので、まずこのツールを使って普通のライブラリに戻した上で、以下のツールを使うわけです。

nm
こいつもApple純正です。バイナリの中に入っているシグネチャの名前を一覧表示することができます。Apple純正なのでfat binaryに対しても使えて超便利です。

objdump
GNU objdumpというツールがありまして、こいつを使うとバイナリの詳細な中身を覗き見ることができます。nmよりも表示される情報が詳細です。Apple純正ではないので、以下で紹介されているようにしてMacPorts経由でインストールするのをお勧めします。
http://d.hatena.ne.jp/amachang/20080401/1207027290

objcopy
GNU objcopyです。ライブラリ内部のシグネチャを書き換える事ができるツールで、objdumpとセットでついてくるのですが、残念ながらiOS向けのバイナリに対しては全く使えません。話すと長くなるのですがバージョンを変えようがarm向けのセットをインストールしようがarでライブラリからオブジェクトファイルを取り出して試みてみようが何やっても一切無駄です。使えませんので諦めてください。
参考: http://www.mail-archive.com/bug-binutils@gnu.org/msg02829.html
参考: http://stackoverflow.com/questions/2231698/how-can-i-easily-install-arm-elf-gcc-on-os-x

objconv
で、使えないobjcopyに代わって、今回の英雄です。こいつを使って実際にライブラリ内部のシグネチャを自由自在に書き換えることができます。メンテもしっかり行われているようで動作も安定しています。以下のサイトからダウンロードできます。
サイト: http://www.agner.org/optimize/#objconv
マニュアル: http://www.agner.org/optimize/objconv-instructions.pdf
残念ながらsource配布のみしか無いため、自分でビルドしてやる必要があります。と言ってもそこそこ簡単で、以下のようにするだけです。
  • ソースコードをダウンロードする
  • zipを解凍する
  • source.zipを解凍する
  • build.shを実行する。ただしこのシェルスクリプトは1行目だけがひどくバグってるので、自分で中を見てビルドコマンドを叩いてやるようにしてください。ね?簡単でしょう?
さて、これで実際にシグネチャの書き換えを行う準備が整いました。


■実践:全く同一のライブラリのシグネチャだった場合
base64なんかでよく発生します。この場合は片方をweakシンボルにします。
weakシンボルとは: http://d.hatena.ne.jp/syohex/20100610/1276180481 がわかりやすいです。

早速やってみましょう。以下の例ではlibTest.a中のbase64_encodeシグネチャを書き換えます。
まずは以下のコマンドで対象のライブラリのfat binaryを通常のバイナリに戻して:
lipo -thin armv6 libTest.a -output libTest_armv6.a
lipo -thin armv7 libTest.a -output libTest_armv7.a
lipo -thin i386 libTest.a -output libTest_i386.a
objconvを実行:
objconv -fmacho -nw:base64_encode libTest_armv6.a
objconv -fmacho -nw:base64_encode libTest_armv7.a
objconv -fmacho -nw:base64_encode libTest_i386.a
lipoで元通りに戻します:
lipo -create libTest_i386.a libTest_armv6.a libTest_armv7.a -output libTest.a
これでビルドが通るはずです。


■実践:同じ名前の違うライブラリのシグネチャだった場合
冒頭のMD5のケースがこれです。名前が同じなのに実装がまるで違うので、weakシンボルにすると深刻なバグが発生します。こういう時は慎重に見定めた上で、使われていないと思われる方のシグネチャをhiddenシンボル(ローカルシンボル)にして、外部ファイルからリンクできないようにしてしまいます。これなら実装は存在しますがリンクされないようになるだけなので、対象のシンボルが外部から使われていないのであればこれだけでいけます。

今度はlibcrypto.a中の_MD5シグネチャを書き換えてみましょう。
こちらもまずはlipoを使って通常のバイナリに戻して:
lipo -thin armv6 libcrypto.a -output libcrypto_armv6.a
lipo -thin armv7 libcrypto.a -output libcrypto_armv7.a
lipo -thin i386 libcrypto.a -output libcrypto_i386.a
objconvを実行:
objconv -fmacho -nl:_MD5 libcrypto_armv6.a
objconv -fmacho -nl:_MD5 libcrypto_armv7.a
objconv -fmacho -nl:_MD5 libcrypto_i386.a
lipoで元通りにして完成:
lipo -create libcrypto_i386.a libcrypto_armv6.a libcrypto_armv7.a -output libcrypto.a
これで無事ビルドが通りました。


■実践:同じ名前の違うライブラリのシグネチャで、かつ外からバリバリ呼ばれていた場合