Objective-C スレッドと libuv スレッドとのやりとり
libuv 専用スレッドと通常の Objective-C スレッドとのやりとりの仕方、ついでに書いておく。
tl;dr - libuv スレッドから Objective-C スレッドに対して何か送るときは Objective-C の作法がそのまま使える。逆は uv_async を使う
libuv スレッドから Objective-C スレッド
これは Objective-C の世界の作法がそのまま使える。
iOS4+ と OSX 10.6+ であれば GCD で、
dispatch_async(dispatch_get_main_queue(), ^{
// ここはメインスレッド
});
のようなのを書くだけでメインスレッドの動作を定義できるから、そこでメソッド呼ぶなり Notification を発行するなりすれば良いので楽ちん。
それ以下の環境だったら performSelector:onThread:withObject:waitUntilDone: 系のを使う。
Objective-C スレッドから libuv スレッド
libuv スレッドでは NSRunLoop が回ってないので上記の作法は使えない。代わりに uv_async を使う。
まず、libuv スレッドで async コールバックを設定:
static void async_cb(uv_async_t* handle, int status) {
}
uv_async_init(self->loop, &self->async, async_cb);
で、呼び出し側(Objective-Cスレッド)から
uv_async_send(&obj->async);
とすれば async_cb が libuv スレッドで発動するという寸法。
データを渡したいときは
@synchronized (obj.send_queue) {
[obj.send_queue addObject:@"foo"];
}
uv_async_send(&obj->async);
という感じにしておいて、取り出す側も
static void async_cb(uv_async_t* handle, int status) {
NSArray* queue;
@synchronized (self.send_queue) {
queue = [NSArray arrayWithArray:self.send_queue];
[self.send_queue removeAllObjects];
}
// 処理
}
みたいにすれば良い。
async コールバックは複数作ることができるから、用途に応じてコールバックを使い分けるのがよさそう。(データ送信用、スレッド終了用など)
libuv (libev) と Objective-C autorelease のはまりポイント
iOS や Mac アプリで HTTP 以外のネットワーク機能をつけたいといった場合に、libuv や libev を組み込んで使うというのを割とよくする。方法としては以下のような感じでその機能用のスレッドをつくる:
-(void)run {
NSThread* thread = [[NSThread alloc] initWithTarget:self
selector:@selector(loop)
object:nil];
self.thread = thread;
[thread release];
[thread start];
}
スレッドの中身は大体こんな感じ:
-(void)loop {
NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
uv_loop_t* loop = uv_loop_new();
// いろいろ初期化
// ...
// libuv イベントループ
uv_run(loop);
uv_loop_delete(loop);
[pool drain];
}
このスレッドは uv_run でブロックしてしまう。本来ここではCocoaのイベントループ(NSRunLoop)をまわす部分だが、かわりに libuv のイベントループを回している感じになっている。
したがってこのスレッドで Objective-C を混ぜる場合には autorelease がスレッド終了まで基本的にされなくなるから注意が必要。
これを解決する方法で最初に思いつくのは、uv_run (ev_run) のかわりに uv_run_once (ev_run(..., EVRUN_ONCE)) を使うことだ。
while (1) {
NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
uv_run_once(loop);
[pool drain];
}
これは一見簡単だけど while ループをとめるフラグを別途用意しないといけないし、uv_run とちがってループを抜けるときには各イベントハンドラが終了しているかを確かめる必要もありめんどくさい。
結果いまはこんな感じにしている:
static void idle_cb(uv_idle_t* handle, int status) {
uv_idle_stop(handle);
[(NSAutoreleasePool*)handle->data drain];
handle->data = NULL;
}
static void check_cb(uv_check_t* handle, int status) {
uv_idle_t* idle = (uv_idle_t*)handle->data;
if (NULL != idle->data) return;
NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
idle->data = (void*)pool;
uv_idle_start(idle, idle_cb);
}
-(void)loop {
uv_loop_t* loop = uv_loop_new();
uv_check_t check;
uv_check_init(loop, &check);
uv_check_start(&check, check_cb);
uv_idle_t idle;
uv_idle_init(loop, &idle);
check.data = (void*)&idle;
idle.data = NULL;
// いろいろ初期化
// ...
uv_run(loop);
uv_loop_delete(loop);
}
uv_check (ev_check) で NSAutoreleasePool をつくりつつ idle タイマーを作って、 その idle タイマー時に [pool drain] する。 これだとイベントが詰まっている場合は drain は呼ばれず、キリの良いときに呼んでくれるから run_once でいちいちやるよりは効率も良さそうな気がする。
このコードでは省略しているけど実際に使うときには check や idle も終了しないと uv_run から抜けてこないのでどこかのオブジェクトにまとめて突っ込んでおいたりして使うのが吉。
こういうのを何も考えずに Objective-C をまぜるともりもりメモリ食うようになるから気をつけよう!
skype-cli
昨日のコードを元に、skype-cli というコマンドラインツールを書いた。
Skype の Desktop API っていうのはマシン上で起動している Skype と通信することで、Skypeの機能にアクセスするAPIで、プラットフォーム毎に通信手段はことなるが、つながってしまったあとの通信内容はテキストベースのプロトコルとなっている。
なので、このテキストプロトコルをtelnet感覚で標準入出力でアクセスできるツールを作ってみたという感じ。
これを子プロセスで実行することで、nodeやEmacsなどからも簡単にSkype APIがたたける、はず。
なんだけど、実際にnodeで以下のようなコードを書いてみてもうまく動かない。
var child_process = require("child_process");
var skype = child_process.spawn("./skype-cli");
skype.stdout.setEncoding("utf8");
skype.stdout.on("data", function (data) {
console.log("stdout: %s", data);
});
skype.stderr.setEncoding("utf8");
skype.stderr.on("data", function (data) {
console.log("stderr: %s", data);
});
process.stdin.resume();
process.stdin.setEncoding("utf8");
process.stdin.pipe(skype.stdin, { end: false });
skype.on("exit", function (code) { process.exit(code) });
なーんでか。
Cocoa::Skype とか
Cocoa:: なんちゃらでほしいものを聞いていたときにも挙がっていた Cocoa::Skype だけど、その昔Skype4COMを使った記憶があって、SkypeAPI のバインディング書くのはめんどそうな印象だったのだが、 今日 Skype.framework のヘッダファイルみたら、
#import <Cocoa/Cocoa.h>
@protocol SkypeAPIDelegate;
@interface SkypeAPI : NSObject
{
}
+ (BOOL)isSkypeRunning;
+ (BOOL)isSkypeAvailable; // You can only connect and send commands when this method returns YES.
// For example, when Skype is running, but user is logged out, then it returns NO.
+ (void)setSkypeDelegate:(NSObject<SkypeAPIDelegate>*)aDelegate;
+ (NSObject<SkypeAPIDelegate>*)skypeDelegate;
+ (void)removeSkypeDelegate;
+ (void)connect;
+ (void)disconnect;
+ (void)sendSkypeCommand:(NSString*)aCommandString;
@end
// delegate protocol
@protocol SkypeAPIDelegate
- (NSString*)clientApplicationName;
@end
// delegate informal protocol
@interface NSObject (SkypeAPIDelegateInformalProtocol)
- (void)skypeNotificationReceived:(NSString*)aNotificationString;
- (void)skypeAttachResponse:(unsigned)aAttachResponseCode; // 0 - failed, 1 - success
- (void)skypeBecameAvailable:(NSNotification*)aNotification;
- (void)skypeBecameUnavailable:(NSNotification*)aNotification;
@end
と想像以上に短く、これならすぐバインディング書けそうだなーということで気分転換もかねてPerlバインディングをつくってみた。
使い方は付属の example を参照のこと。 Skype.framework の機能は全部つけてある。 Perl サイドのメソッド名とかはまだ変更するかもしれない。暇なときにドキュメントつけてリリースしよう。
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 の Perl バインディング
気分転換プロジェクトとして、libuvのPerlバインディングを書いている。
現状、timerとtcp周りを一通り実装したところ。
実装は愚直にlibuvの関数とperlの関数を1:1に(uv_tcp_init(...)をUV::tcp_init(...)というような形で)マッピングしている。 この実装はlow level APIと位置づけ、その上に使いやすい高レベルなインタフェースをPerl上で用意したいつもり。
このlow level APIをつかったTCP echo serverはこんな感じになる:
use strict;
use warnings;
use UV;
my $server = UV::tcp_init();
UV::tcp_bind($server, '0.0.0.0', 3000)
&& die 'bind error: ', UV::strerror(UV::last_error());
UV::listen($server, 10, sub {
my $client = UV::tcp_init();
UV::accept($server, $client) && die 'accept failed: ', UV::strerror(UV::last_error());
UV::read_start($client, sub {
my ($nread, $buf) = @_;
if ($nread < 0) {
my $err = UV::last_error();
if ($err != UV::EOF) {
warn 'client read error: ', UV::strerror($err);
}
UV::close($client);
}
elsif ($nread == 0) {
# nothing to read
}
else {
UV::write($client, $buf, sub {
my ($status) = @_;
if ($status) {
warn 'client write error: ', UV::strerror(UV::last_error());
UV::close($client);
}
});
}
});
}) && die 'listen error: ', UV::strerror(UV::last_error());
UV::run();
見てわかるようにperlモジュールとしての使いかってはわるいが、libuvを使い慣れた人なら迷いなく使うことが出来るようになっている。 これは僕にとっては発見だった。
また、このサンプルをみてもわかるようにlistenとかacceptとかまでラップしている関係上、libuvをAnyEventのバックエンドとしてつかうのは現状は無理そう。
libuv自体はWindowsでも動くはずだが、メインマシンをAirにして以来Windows環境を仮想環境でも持ち歩いていないため確認できていない。 Windows対応してくれる人募集!
開発を始めるためのステップ:
$ cpanm Module::Install
$ cpanm Module::Install::XSUtil
$ git clone git://github.com/typester/p5-UV.git
$ cd p5-UV
$ git submodule update --init
$ perl Makefile.PL
$ make
$ make test
Growl 1.3.1 と Skype
ひさしぶりにMacを再起動したらSkypeがGrowlを認識しなくなった。
そういえばGrowlを1.3.1にしてから再起動していなかった。原因はそれだろうとSupport Forumをのぞいてみると同様の問題を持った人がいっぱいいて、そこに解決法も書かれていた。
Skypeに内蔵されてるGrowl.frameworkが古いからそれをアップデートしてやればOKということらしい。
フレームワークのアップデートは自分でファイルを置き換えても良いが、Growl公式サイトでそれ用のツール、Growl Version Detectiveが配布されているのでそれを使用すると良い。
アプリはこんな感じで、
Skypeを選択してUpdate FWを押すだけで入れ替わる。
オレオレバッファ
C でなんか書くときに、 lighttpd の buffer.c をコピペ(&若干改変)したのをずっと使いまわしてきたけど、コピペして使い回すのがめんどくなってきたので submodule として使えるように github にアップした。
上げるついでにテスト書いたけど、全部テスト書くのが面倒だったので、使ってなかった関数郡はごっそり削除。 必要になったら追加する方針に。
こういうのってどっかに定番でみんな使うようなのあるんかなー。
ngx-queue.h
libuv のソースを見ていたら、ngx_queue_* という API が出てきてびっくり。どうやら nginx から ngx-queue.h っていうリンクドリストの実装を持ってきているようだ。
include/uv-private/ngx-queue.h at master from joyent/libuv - GitHub
なかなかおもしろい。これ、いろんなところで使えそうなので手元でも試してみた。
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
#include <string.h>
#include <stddef.h>
#include "ngx-queue.h"
typedef struct {
ngx_queue_t queue;
char* data;
} chunk_t;
chunk_t* chunk_init(const char* data, size_t len) {
chunk_t* c = malloc(sizeof(chunk_t));
assert(c);
ngx_queue_init(&c->queue);
c->data = malloc(len + 1);
assert(c->data);
memcpy(c->data, data, len);
c->data[len] = '\0';
return c;
}
void chunk_delete(chunk_t* c) {
ngx_queue_remove(&c->queue);
free(c->data);
free(c);
}
int main(int argc, char** argv) {
ngx_queue_t queue;
chunk_t* chunk;
ngx_queue_init(&queue);
chunk = chunk_init("foo", 3);
ngx_queue_insert_tail(&queue, &chunk->queue);
chunk = chunk_init("bar", 3);
ngx_queue_insert_tail(&queue, &chunk->queue);
chunk = chunk_init("buz", 3);
ngx_queue_insert_tail(&queue, &chunk->queue);
while (!ngx_queue_empty(&queue)) {
ngx_queue_t* q = ngx_queue_head(&queue);
chunk_t* c = ngx_queue_data(q, chunk_t, queue);
printf("data: %s\n", c->data);
chunk_delete(c);
}
chunk = chunk_init("foo", 3);
ngx_queue_insert_tail(&queue, &chunk->queue);
chunk = chunk_init("bar", 3);
ngx_queue_insert_tail(&queue, &chunk->queue);
chunk = chunk_init("buz", 3);
ngx_queue_insert_tail(&queue, &chunk->queue);
while (!ngx_queue_empty(&queue)) {
ngx_queue_t* q = ngx_queue_last(&queue);
chunk_t* c = ngx_queue_data(q, chunk_t, queue);
printf("data: %s\n", c->data);
chunk_delete(c);
}
return 0;
}
出力は
$ ./a.out
data: foo
data: bar
data: buz
data: buz
data: bar
data: foo
これは使えるなぁ。
テラスモール湘南に行ってきた
11月11日に辻堂駅前にオープンしたばかりの湘南地区最大級のショッピングモール、テラスモール湘南に行ってみた。
我らが鎌倉bowlsも出店しているからその様子見もかねて。
テラスモール湘南のデリカテッセンのお店。いつもの家庭の食卓にサプライズと笑顔をお届け!
個人的には、109シネマズ(もちろんIMAXシアター付き)が入っているというのが最大のポイント。いままでは川崎まで見に行っていたからね。
ただ、懸念点は交通事情。あのあたりってそんなに道も大きくないような気がしたし、テラスモール湘南自体は2500台収容の駐車場があるらしいけど周りの交通が麻痺してしまっていたら意味がない。
実際に行ってみるとそんなに心配することもなかった。到着したのはたぶん1番混んでると思われる14時台だったけど、テラスモール湘南の少し手前から駐車場に入る列が出来ている程度の渋滞しかなかった。 ただ駐車場は屋上以外はほとんど満車状態だったから、雨の日など屋根付きのところへ駐車したいという場合には早めか遅めの時間帯に行くようにした方がいいだろう。
鎌倉から向かうには海沿いをずっと行って浜見山交番前を右折、あとはずっとまっすぐ行くとテラスモールの真ん前に出るから1番楽だと思う。帰りはそのルートは混むので模索中…。
今回は映画を見るわけでもなく施設内をぶらぶらしただけだったけど、なかなか好印象。湘南地区の人たちは横浜や都内に出る機会がだいぶ減るんじゃないかな。ここで事足りるっていう意味で。 次回は映画を見に行こうと思う。
