Objective-C で AUTOLOAD (あるいは method_missing )
forwardInvocation
とかを使えば出来るみたい。
#import <Foundation/Foundation.h>
@interface Foo : NSObject;
-(void)call:(NSString*)sel;
@end
@implementation Foo
-(void)call:(NSString*)sel {
NSLog(@"call: %@", sel);
}
-(void)forwardInvocation:(NSInvocation *)invocation {
[self call:NSStringFromSelector([invocation selector])];
}
-(NSMethodSignature*)methodSignatureForSelector:(SEL)sel {
NSMethodSignature* sig = [super methodSignatureForSelector:sel];
if (sig) return sig;
return [[self class] instanceMethodSignatureForSelector:@selector(call:)];
}
@end
int main() {
NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
Foo* foo = [[Foo alloc] init];
[foo bar];
[foo buzz];
[foo release];
[pool drain];
return 0;
}
// gcc -framework Foundation foo.m
詳細は Objective-C Runtime Programming Guide にある。
libuv を iOS 対応した件
だいぶ前から iPhone アプリ作成でネットワーク系の機能を作るときには libev を愛用してるのだが、今日 node をいじっていたら libuv がなかなかよさそうに感じた。
libuv は libev が Windows で動かないからっていう理由で始まったプロジェクトだとおもうけど、Linux などの環境においても libev をラップしつつ、より便利な機能が追加されていていわば C でネットワークプログラミングするためのフレームワークといえるくらいになってる、みたい。(まだ詳しく見たわけじゃないけど)
libev は基本的に io 監視と timer のみで、socket の生成などは基本的に自分で syscall 呼んでつくる必要があるけど、libuv は uv_tcp_*
とか uv_udp_*
といったAPI郡をもってて便利そう。 あと、非同期 DNS ルックアップがサポートされてるのはうれしい。これ、 libev になくて不便だった。
そういうとこまでラップしないと同じコードを Windows でも動かすってのは無理だからそうなったんだとおもうけど、結果として大変便利なライブラリになっているのではないか。
あと、テストケースが充実しているので、(たぶん)すべての機能のサンプルコードがテストを見ればOKっていうのもうれしい。
っていうわけで iOS 対応のパッチを書きました。
#243: Added experimental iOS support by typester for joyent/libuv - Pull Request - GitHub
iOS で動かない理由はハードウェア時間をnano秒で取得してる関数が iOS にはない CoreServices.framework に依存しちゃってるからってことだけなので、それを使わないように修正をした。
現状のパッチは iOS 用にビルドするときのみ CoreServices 非依存のコードを使うようにしているけど、これに依存してることで uv.a を組み込むだけじゃなくて、 CoreServices.framework に別途リンクする必要があってめんどいからパフォーマンスや精度が問題なければ非依存のコードの方に統一してしまった方がいいと思う。っていうようなことは pull req に返信が来たら伝えようとは思っている。
とりあえず、これで使えるようになったから。さっそく今作ってるアプリに組み込んでにようと思う。
node.js を iPhone アプリから動かす
結論から言うと、 jailbreak してないと動きません。詳細はこの辺:
Issue 1312 - v8 - It's time to get iOS supported! - V8 JavaScript Engine - Google Project Hosting
というわけでターゲットがデベロッパー用に絞られてしまいますがやってみましょう!
Node ダウンロード
今回はリリースされたばかりの 0.6.0 を使いました。
からダウンロードして展開しましょう
libuv にパッチを当てる
libev はなにもしないでもそのまま iOS で動きますが、 libuv はそうではないようです。
$ cd deps/uv
$ wget --no-check-certificate https://raw.github.com/gist/1354552/cfb4e9a544185bdbda1a8374aaf1cd5cc812c070/libuv-ios.patch
$ patch -p1 < libuv-ios.patch
なお、このパッチは本家に pull req 送っておきました。取り込まれると良いですね。
V8 にパッチを当てる
V8 自体は ARM に対応しているようですが、iOS SDK でクロスコンパイルしようとすると
ARM EABI support is required.
とか言われてしまいます。iPhone の ARM が EABI をサポートしてないためでしょうか。
ただ、最初にリンクした Goole Code の issue 内で iOS 対応のパッチを投稿している人がいて、そのパッチを当てることでビルド可能になる模様。
$ cd deps/v8/src
$ patch -p0 < ~/Downloads/v8-ios.patch
Node にパッチを当てる
プロセスリストにでるプロセス名を設定する機能が Carbon をつかっているので関数殺しちゃいます。iOS でプロセス名かえられても誰得ですよね。具体的には
process.title = "hoge";
が動かなくなります。
$ wget --no-check-certificate https://raw.github.com/gist/1354570/0c28584c07f0410c5e5608d4e2e9ea68e6d5dbc4/node-ios.patch
$ patch -p1 < node-ios.patch
ビルド
まずもろもろ環境変数をセットし:
export CC=/Developer/Platforms/iPhoneOS.platform/Developer/usr/bin/gcc
export CFLAGS="-arch armv7 -isysroot /Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS5.0.sdk -I$HOME/dev/iphone/lib/openssl/include"
export CXX=/Developer/Platforms/iPhoneOS.platform/Developer/usr/bin/g++
export CXXFLAGS=$CFLAGS
export CPP=/Developer/Platforms/iPhoneOS.platform/Developer/usr/bin/cpp
export AR=/Developer/Platforms/iPhoneOS.platform/Developer/usr/bin/ar
export LINKFLAGS="-arch armv7 -isysroot /Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS5.0.sdk -L$HOME/dev/iphone/lib/openssl/lib"
ビルド:
./tools/waf-light configure build --product-type=cstaticlib --dest-cpu=arm --without-snapshot
ひっそり、前もってビルド済みの openssl を参照していますが、ない場合は --without-ssl
を追加すればいけるはずです。
成功すると
./out/Release/deps/uv/uv.a
./out/Release/libnode.a
./out/Release/libv8.a
./out/Release/libv8preparser.a
といった静的ライブラリが出来てると思うんで、これらをアプリに埋め込んであげればいいわけですね。
armv6, armv7, i386 用それぞれビルドし、lipo
でユニバーサルバイナリに仕上げるのが iOS 用静的ライブラリ作成のセオリーですが、ここではめんどいので省略します。
アプリ
だいぶ適当ですが、サンプル作ってみました。
lib ディレクトリにさっきビルドした .a ファイルを突っ込んであげる必要があります。 また node.h
の参照元を nave でインストールした node のパスを設定してあるので、違う環境の人は Header Search Path を環境に合わせて書き換える必要もあります。
こんな感じの画面が出るんで、適当にコード書いて Run おせば動かすことが出来ます。
注意
コードみてもらえばわかりますがだいぶ適当につくっておりまして、node の標準出力などをうけとっておりません。また、二回目実行しようとするとアプリが落ちますw
直していただける方お待ちしております!
iPhone4S とか iOS5 とか Xcode 4.2 とか
iPhone4S は予約しようと思ってたけど、予約開始日がちょうど F1 日本 GP とかぶっていたので初日予約はあきらめ、暇なときに予約しに行こうと思っていて今に至る…。
iOS5 は開発者用のものを前から使っていたけど、通知で邪魔されないようになったのがすごい大きい。 いままではカーナビとして使っている最中に Push 通知が来てしまうと通知を消すまでカーナビとしての役割をはたさないようになってしまっていたからね。
で、こういうApple製品リリースの中で一番ライフチェインジングだったのが Xcode 4.2! ARC (Automatic Reference Counting) と Storyboard が便利すぎて iOS アプリの開発速度いままでの何倍にもなります。ただ Storyboard つかっちゃうと iOS5 以降限定になっちゃうってのが…。
ARC も zeroing weak 変数(破棄されたらnilになってるって保証されてるweak変数)使おうとすると iOS5 限定になっちゃうみたいだし。 zeroing ってなんて訳せば良いんだろ。
そんなこんなで、次のアプリから iOS5 のみで良いかなとか考えてる最中。
最近の活動その1 Reengo
気づいたら3ヶ月も更新途絶えてましたが、その間ずっとiPhoneアプリを開発してました。
今年一番時間をかけ、いま現在もつきっきりで開発しているのが Reengo というアプリ。
いわゆる VoIP アプリケーションですが、Facebookアカウント連動で友達がアプリさえインストールしていればすぐに通話できるというのが特徴です。
この辺の投稿なんかはこれの伏線だったわけです。
クライアント実装は通信周りのロジックは C で書いていて、この部分は近日リリース予定の Android 版と共有して使いまわしてます。 サーバー実装は Perl と Node を適材適所使い分けております。Node の使用用途としてはいままで Perl の AnyEvent を使ってやっていた部分を Node に置き換えたというイメージで、個人的にはこれはなかなかいい選択肢なんじゃないかなーとおもってます。 技術的なところの詳細はまたどこかで別途書きたいと思ってます。
まだまだ課題があってそれを徐々につぶしているという状態ですが、ぜひ使ってみてください!
iOS の socket(2) は 0 を返すことがある
iOS 4.2.1。
-1 チェックしかしていなくて、まさかここだとは思わずごちゃごちゃ迷走してしまった。。 あり得ないと思うことは表明(assert)しとくっていう基本をちゃんとしたい。
0 のときは無視して単純にもう一度 socket(2) を呼んであげるとちゃんと返ってくると言う謎。 その実装で fd も増えていかないのでとりあえずしのぐ。
iPhoneアプリケーション多言語化をpoファイルを使ってやる方法
iPhoneアプリケーションを他言語対応したいとしたら、Apple側がすでに用意してくれている方法を使うのが自然かと思う。 僕も最初はその方法を試した。ドキュメント的には以下がエントリポイントだろうか:
iOS Reference Library - Internationalization Programming Topics
しかしこの方法にはいろいろな問題があり、大きなアプリケーションを作ろうとするとストレスがかかってくるので現在は使用していない。(小さなプロジェクトでは使うこともある)
その問題とは以下のようなものだ:
- 言語ファイル(Localizable.strings)が独自形式なため、既存の翻訳ツールを使うことが出来ない
- 一つ目と少しかぶるが翻訳抜けや言語一覧などをチェックする機能がない
- ibtool のマイグレーション機能がくそで、バージョンアップのたびにリソースファイル内の文字列を頑張って置換する作業が必要になる
など。これらを解決するために現在は以下のような手法でアプリの多言語化を行っている:
- 言語ファイルには一般的なファイルである GNU gettext のカタログファイル形式(.po)を使う
- ソースファイルから
NSLocalizedString
系の呼び出しを抽出して .po ファイルの雛型を作成してくれるスクリプト(xgettext.pl)を作成 - .po ファイルを Localizable.string に変換するスクリプト(po2string.pl)を作成
- .nib ファイル内の多言語化が必要な文字情報はすべてインタフェースビルダーでは設定せず
.m
.mm
ファイルからセット(上のツール群をそのまま使うため)
と言う感じでやっている。その二つのスクリプトはこんな感じ。
Locale::Maketext::Extract
に依存している。
$ ./xgettext.pl -o resources/ja.po src/*.m
みたいな感じでまず言語カタログを作成。このファイルは Emacs の po-mode や Poedit などのツールで編集することができる。 個人的には Emacs を使い編集しているが、翻訳者のために良いツールを探しているのでおすすめがある方は教えていただきたい。
このスクリプトのポイントは何回実行しても.poファイル内のすでに翻訳済みのエントリは変更しないという点だ。 アプリケーションを更新したらスクリプトを実行し、.poファイル内の未翻訳のところのみ訳す、というサイクルを繰り返すことができる。.poファイルエディタは未翻訳エントリだけを簡単に編集できるというところも便利ポイントかな。
.po ファイルが翻訳されたら
$ ./po2strings.pl --po=resources/ja.po --strings=resources/Japanese.lproj/Localizable.strings src/*.m
などのようにしてiPhoneアプリから使える言語ファイルに書き出してやって完了。 ソースファイルを渡す必要があるのはスクリプトソースを見てもらえばわかるがただの手抜きである。
あ、ちなみにわざわざ NSLocalizedString(...)
とか書くのは面倒なので、
#define L(s) NSLocalizedString(s, nil)
とかを定義しておいて L(@"Hello")
とかでいけるようにしている。(もちろんxgettext.plはその書式にも対応している)
bREADER の PDF リフロー表示機能が素晴らしい件
BB2C というiPhone用の2chビューワアプリがあるのですが、このアプリは2chビューワとしての評価が高いだけではなく、その凄まじく軽快な動作からiPhoneアプリ開発者で知らない人はモグリだと言われるくらいの良アプリなのです。 最近その作者のブログ に電子書籍の話題が出ていて気になっていたのですが、どうやら次は電子書籍リーダーアプリを開発していたらしい。それが bREADER。
iPad を買って以来書籍はPDFにして読むのがメインになっていて、大体はi文庫HDで、検索が必要なときはiBooksかMac上で読んでます。
この環境におおむね満足しているのだけど、電車の中で座れなかったときに本を読もうと思っても iPad を出すのはきびしいし、かといって iPhone だと PDF を等倍表示ではきびしいものがある。(Retinaならよめないことはないけど…) 拡大したら拡大したで、1ページを読むのにページ内をぐるぐるスクロールしつつ読まなくてはいけなくなり大変苦痛である。 文字を拡大したぶんだけページがずれていってくれるePubや青空文庫みたいな形式でなければiPhoneではきびしいなーと思っていました。bREADERをしるまでは!
この問題はがんばって紙の書籍をスキャンしてPDF化している現状が問題であって、出版社がePubみたいなので最初から電子出版してくれれば解決なんだからそれまで待とう…。 と普通の人は考えます。bREADERの作者はそういう発想はないらしく、力技でこの問題を解決しようとしているようです。
詳しくは先ほどリンクしたブログを参照していただくとして、手持ちのPDFで試してみた結果を紹介します。
まずこれがPDFをそのまま表示した時。(iPhoneの画面だとわかりにくいのでわざとコントロールを表示した画面にしています)
iPhoneの普通のPDFリーダーはみんなこんな感じに表示されてしまいます。Retinaディスプレイを搭載したiPhone4でやっとよめるというくらいですが、それでも小さくてつらいです。 つぎに bREADER がうまいことやってくれたあとの画面を見てみましょう
このようにまるでePubかと思うような拡大処理をしてくれてものすごく読みやすくなります。 そしてこのアプリの動作自体も BB2C を彷彿とさせる軽快さで、サクサク読み進められます。これはPDF化をがんばってる人でiPhoneのような小さいデバイスで本を読みたいと思っていた人には夢のようなアプリですね!
ただし現状ではPDFがそのまま自動で上のようなリフロー表示(というらしい)になってくれるわけではありません。ちょっとごにょっと作業をして上げる必要があります。
bREADER で PDF をリフロー表示に対応させる手順
リフロー表示を行うにはあらかじめMacかPCで書籍の文字情報を解析してメタ情報ファイルを作ってあげる必要があります。 作者がサイト上でそのためのbrcというツールを配布してくださっています。 が、そのツールはPDFに対応していませんのでまずはPDFをPNGとかなんか画像ファイルに変換してあげるという作業が必要になります。
- PDFを連番のPNG画像に変換
- brcでメタ情報ファイルを作成
- zipでかためてbREADERに転送
ってなかんじになります。
PDFを連番のPNG画像に変換
これをどうやろうかとおもいまして、最初に Preview.app を試してみたのですがどうも1ページずつしかPNGに保存できないっぽくあきらめ、OSX の機能をつかった pdf2png を適当にでっちあげた。
こいつを
$ gcc -framework Cocoa pdf2png.m -o pdf2png
とかでビルドしてあげて、
$ ./pdf2png foobar.pdf ./foobar
みたいにすると foobar.pdf を 0001.png みたいな連番の画像にして foobar ディレクトリに保存してくれると言う感じ。出力先のディレクトリはあらかじめ存在している必要があります。 PDFのスケーリングがよくわからなかったので適当に設定してあり、そこは誰か良い感じに書き直してくれればと…! あと CGBitmapContextCreate
の第一引数にNULLをわたしていてそこが OS X 10.6 依存です。それ以前のでうごかしたいときは malloc(w*h*4)
したの食わせとけばいいです。
brcでメタ情報ファイルを作成
先ほど生成した連番PNGが入っているディレクトリを brc コマンドに食わせる
$ brc ./foobar
これでそのディレクトリに d.brd というファイルが生成されていれば成功。
zipでかためてbREADERに転送
$ zip -0 foobar.zip *.png d.brd
みたいにして png と d.brd がふくまれた zip ファイルを作り、あとはそれを iTunes で bREADER のデーターフォルダに転送してあげればOK。簡単ですね。
まとめ
と、ここまで絶賛してきましたが現状では、ページ上部に常に出ている章の名前とか欄外に脚注なんかがあったりすると表示がおかしくなったりします。 とはいえそれでもiPhone上ではこの表示モードを使おうと言うくらいのレベルに既になっていると思います。これはほんとにすごい。
今後、bREADERもbrcも便利になっていくだろうし、brcはいらなくなる可能性もあるし、非常に楽しみですね!
bREADER 現バージョンの不満点をあげるとすればアプリ内に時計表示がどこにもなくアプリを落とさない限り時間がわからない、ということくらいでしょうか。 自炊派も、業者派も、iPhoneでPDF(書籍)を読む人すべてにおすすめなアプリです!
iPhoneバックグラウンドでのソケット監視のサンプル
iOS4 からアプリケーションのバックグラウンド動作がサポートされているが、それらはだいぶ制限されたものとなっていて、通常のアプリケーションはバックグラウンドでは最大10分程度しか生存することが出来ない。
それを回避するために Info.plist
に特殊なフラグをたてることで例外的に制限を解除することができるようになっている。そのフラグの種類は
- audio
- location
- voip
であり、それぞれバックグラウンド音声再生、バックグラウンド位置情報取得、バックグラウンドでのソケット監視(voipの着信監視用)に対応している。 そのうち voip だけ今まで扱ったことがなかったので挙動確認のためやってみた、というのが今回のお話。
まず書いてみたコード:
ソケットのハンドリングに libev を使っているので、ビルドを通すには $(HOME)/dev/iphone/lib/libev
に libev がインストールされている必要がある。 また、AppDelegate.m の #define HOST inet_addr("127.0.0.1")
という部分を自分のサーバーのIPになおしてあげるというのも必要。
んで、付属の server.pl が適当に作ったサーバーで、STDIN を読み込んで、入力があったらつながっているiPhoneにその内容を送信するというものになっている。 iPhoneアプリ側はバックグラウンド時にそのパケットを受け取ると受け取った内容をLocalNotificationで表示っていう感じ。
NSInputStream にたいして
[stream setProperty:NSStreamNetworkServiceTypeVoIP
forKey:NSStreamNetworkServiceType];
とかしてあげとくだけで、アプリがサスペンドしたときには OS 側がソケットの監視を代理でやってくれる。 で、そのソケットにたいして何かイベントが起こるとアプリを起こして処理を戻す。アプリ側は通常のソケット操作だけ実装しておけばいい。
このアプリのようにネットワーク周りはCで処理している場合、ファイルディスクリプタを NSStream にアップグレードする必要があってそこは若干めんどう。 Cocoa::EventLoop でもおなじことをしているのでその XS 部分からコピペしてきた。
Skype のバッテリー消費が半端ないのでこの機能にたいしてあまり良い印象はなかったのだが、実際にやってみるとバッテリー消費は思ったより気にならない(いまのところ。あまり検証はしていない)。 おそらく Skype は頻繁にパケットを送受信しているから、アプリが頻繁に起こされ、結果ものすごい勢いでバッテリーが減る、ということなのだろうか。
とにかく、なかなかおもしろい機能。VoIP アプリ以外でもつかえればなー。
kAudioSessionCategory_PlayAndRecord のときの AudioRoute 文字列メモ
ドキュメントに載ってなくて、実際うごかしてみて文字列を知るという状況。 なので持ってない機種とかデバイスでどうなるのかわからないという問題があり不安。
ReceiverAndMicrophone
PlayAndRecord 時は iPhone ではデフォルトでこれになる模様。 通常のスピーカーではなく電話の受話口から音が出る。
HeadphonesAndMicrophone
ヘッドフォンさしたとき。
SpeakerAndMicrophone
iPad とか(たぶん最新以外のiPod Touchも)のように受話口がない端末ではこれがデフォルトっぽい。
デフォルト以外のルートに設定する
スピーカーに強制的に変更するには AudioSessionSetProperty
でできる。
UInt32 audioRouteOverride = kAudioSessionOverrideAudioRoute_Speaker;
AudioSessionSetProperty(
kAudioSessionProperty_OverrideAudioRoute,
sizeof (audioRouteOverride),
&audioRouteOverride
);
他のルートに変更する方法は undocumented。
ただこのうわがきはすでに SpeakerAndMicrophone なところで実行するとおかしくなっちゃう。 AudioSessionSetProperty
するまえに AudioSessionGetProperty
で現在の値を確認するようにすること。
あと、AudioRoute の変更は AudioSessionAddPropertyListener
で kAudioSessionProperty_AudioRouteChange
を登録しておけば監視できる。