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 をまぜるともりもりメモリ食うようになるから気をつけよう!
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
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
これは使えるなぁ。
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 に返信が来たら伝えようとは思っている。
とりあえず、これで使えるようになったから。さっそく今作ってるアプリに組み込んでにようと思う。