SSブログ

CocoaのDistributed Objects [プログラミング]

Mac OS XのCocoaフレームワークにはDistributed Object(分散オブジェクト)というしくみががあって、これを使うとクライアント・サーバ型のアプリが簡単にできる。クライアント・サーバアプリというと僕はまっさきにX Windowシステムを思い浮かべるんだけど、普通の人にとってはWEBサーバなんかが典型的だろう。

Cocoaを使うと、こんなんでいいの、と言うくらい簡単にできてしまう。

昨日、仕事で必要があってちょっといじっていたら、すごく面白いことに気がついた。2時間ほどこれで遊んでしまった。今日はこの話をメモする。Cocoaでの分散オブジェクトシステムのちょっとコアな話。

2  CocoaのDistributed Object

Cocoaでクライアント・サーバ型アプリを作るにはDistributed Objectというしくみを使うと非常に簡単になる。いくつかのクラスからできているけど、一番簡単な方法ではひとつのクラスNSConnectionを知っていればできる。

サーバ側のプロセスで、実際の作業をするオブジェクトが
    connection = [NSConnection serviceConnectionWithName:@"DOServer"
                                              rootObject:self];
としてコネクションの名前と作業オブジェクトを指定してNSConnectionのインスタンスを作る。

もしこれがヘッドレスアプリ(App Kitを使っていない)ならこのあと
    [[NSRunLoop currentRunLoop] run];
として実行ループをまわす。実行ループがないとNSConnectionは動作しない。
そしてクライアント側は
    id              serverProxy;
    NSConnection    *connection;

    connection = [NSConnection connectionWithRegisteredName:@"DOServer"
                                                       host:nil];   // (1)
    serverProxy = [[theConnection rootProxy] retain];
    [serverProxy setProtocolForProxy:@protocol(ServerProtocol)];    // (2)
として、作業オブジェクトの代理を受け取る。これだけでいい。代理に対してメソッドを発行すると、それがサーバ側の作業オブジェクトに伝えられて実行される。

どのサーバに接続するかは名前だけで決まる。上の例では(1)のhostはnilなので同じマシンの上にあるNSConnectionが探されるが、インターネットドメインネーム(developper.apple.comみたいなの)を指定すればそのホストが探される。(2)では作業オブジェクトが持っているメソッドをプロトコルにしておいて、それを指定している。そうするとクライアントからメソッドを発行するとき、作業オブジェクトがそのメソッドを実行できるかどうかを確認する通信を省略できるので、パフォーマンスが上がる。

代理オブジェクトへは同じプロセスのオブジェクトに対するのとまったく同じでいい。

3  具体的な動作例

実際に動かしてみる。かんたんなもので、
  1. クライアントはサーバに時間を指定する
  2. サーバはそれを受け取ってその間スリープする
  3. スリープから起きたとき実際にスリープしていた時間をクライアントに報告する
  4. クライアントはそれを受け取って表示する
というもの。
サーバの作業オブジェクトは
@protocol DOProtocol
- (NSTimeInterval)waitForInterval:(NSTimeInterval)interval;
@end

@interface DOServer : NSObject <DOProtocol>
@end
というようなプロトコルを持つもの。この実装を
- (NSTimeInterval)waitForInterval:(NSTimeInterval)interval
{
    NSDate  *startDate = [NSDate date];
    usleep((useconds_t)(interval * 1000000));
    return -[startDate timeIntervalSinceNow];
}
とする。単にほんとにスリープしてその間の時間を返している。usleep()はμsec単位でブロックする(最大で1時間ちょっとになる)。この例ではNSTimeInterval(doubleにtypedef)を返しているけど、なんでもいい。NSDateのようなCocoaのオブジェクトを返すこともできる。

これだけを持つヘッドレスアプリを作る。そのmain関数は
#import <Foundation/Foundation.h>
#import "DOServer.h"

int main(int argc, char *argv[])
{
    NSAutoreleasePool   *pool = [[NSAutoreleasePool alloc] init];
    DOServer            *doServer = [[DOServer alloc] init];
    [[NSRunLoop currentRunLoop] run];
    [doServer release];
    [pool drain];
    return 0;
}
みたいなの。

クライアント側はこれを呼ぶ。

クライアント側はこんなウィンドウを持たせる。
1027resource.png
上のテキストフィールドにユーザに入力してもらって、Callボタンを押すとメソッドが呼ばれるようにIBAction
- (IBAction)call:(id)sender
{
    NSTimeInterval  interval = [inputField doubleValue];
    NSTimeInterval  result;
    result = [serverProxy waitForInterval:interval];    // (3)
    [resultField setDoubleValue:result];
}
を設定する。(3)で代理オブジェクトに対してメソッドを発行している。

こうしておいて、サーバ側のアプリをターミナルから実行させておいて、クライアント側を立ち上げて、Callボタンを押すとこのようになる。
1027callresult.png
ちゃんと結果がかえってきている。usleep()は少なくとも引数μsec待つ、という関数なのでこの微妙に中途半端な結果は正しい。サーバ側が立ち上がってないと、NSConnectionのインスタンスではなくnilが返るので、クライアントは継続できない。

こんな簡単でええんかい、というほど簡単である。

3.1  重い作業をするサーバ

上の例ではサーバ側での処理(usleep())を待っている間、クライアント側はブロックする。つまりCallボタンは押された状態のままで、サーバから返ってくるまでなにもできない。

これは当然で、同じランタイム上のオブジェクトなら、
  1. クライアントオブジェクトは作業指示だけする
  2. サーバオブジェクトはそれを受け取って、すぐ返事をする
  3. サーバオブジェクトはそのあと作業に入る
  4. 作業が終わったらクライアントオブジェクトに報告する
ということをするのが普通で、これがいわゆる非同期(asynchronous)処理である。クライアントオブジェクトへの報告は「コールバック」と呼ばれて、Cocoa/Objective-CではInformal Protocol(NSObjectのカテゴリ)としてメソッドを定義しておいて、クライアント側が実装してサーバ側がそれを呼ぶ、ということするのが多い。もちろん、C/C++でやるようにクライアント側が呼んで欲しい関数をサーバに渡す(Objective-Cではセレクタを渡す)ということもできる。

CocoaのDistributed Objectのしくみでは、クライアント側の作業指示をもっと効率よくするためにonewayという予約語が与えられている。例えばサーバ側のプロトコルに
- (oneway void)waitForInterval:(NSTimeInterval)intervals;
と宣言すると、プロセス間通信のレベルで応答を求めないメソッドにすることができる。

ただし、これでは結果が得られない。そこで同じプロセス上にあるオブジェクトにするように
@protocol DOProtocol
- (oneway void)wait:(id)client forInterval:(NSTimeInterval)intervals;
@end

@interface NSObject (DOServer)
- (void)resultFrom:(id)server withValue:(NSTimeInterval)result;
@end
としよう。サーバ側を呼ぶときクライアント側のオブジェクトをclientとして渡す。そしてサーバ側の処理が終わったときにそのclientオブジェクトに下のInformal Protocolでコールバックする。

具体的にはサーバ側は
- (oneway void)wait:(id)client forInterval:(NSTimeInterval)intervals
{
    NSTimeInterval  result;
    NSDate  *startDate = [NSDate date];
    usleep(intervals * 1000000);
    result = - [startDate timeIntervalSinceNow];
    if ([client respondsToSelector:@selector(resultFrom:withValue:)])
        [client resultFrom:self withValue:result];
}
とする。一方のクライアント側は
- (IBAction)call:(id)sender
{        
    [serverProxy wait:self forInterval:interval];
}

- (void)resultFrom:(id)server withValue:(NSTimeInterval)resultValue
{
    [resultField setDoubleValue:resultValue];
    [resultField setNeedsDisplay:YES];
}
として受け取ったら表示するようにする。

これが同じプロセス(同じランタイム上)なら問題なく動く。

さて、これでさっきと同じように動かしてみると、Callボタンは押したらすぐ返ってきて、操作できるようになっている。そして待ち時間が過ぎると表示が更新される。

一見当たり前のようだけど、これはすごいんではないか?

だって、サーバ側が受け取ったclientオブジェクトはサーバ側のランタイムには定義さえ存在しない。なんでこれが動作するんだ????

3.2  どうやって実現されているのか?

デバガで動作を追ってみる。

実はNSConnectionが返す代理オブジェクトはNSDistantObjectのインスタンスで、作業オブジェクトそのものが返ってくるわけではない。もちろん作業オブジェクトに関してはクライアント側はプロトコルを知っているだけで、オブジェクト定義がなくてもいい。NSDistantObjectというのはNSProxyのサブクラスで、NSProxyというのはNSObject以外で唯一継承元を持たないことで有名なルートクラスである。

Appleのドキュメントによると、NSDistantObjectのインスタンスはメソッドが呼ばれたとき、
  1. NSDistantObjectはメソッド呼び出しをNSInvocationのオブジェクトに変換する
  2. NSConnectionがそれを受け取り、NSPortMessageのインスタンスに埋め込む
  3. NSConnectionはNSPortMessageをNSPortに渡す
  4. NSPortはNSPortMessageをNSPortCoderを使ってシリアライズする
  5. そのデータを通信チャンネルに流す
  6. サーバ側のNSPortが受け取って逆のプロセスでNSInvocationに戻す
  7. NSInvocationを実行する
  8. 結果(戻り値)は逆のプロセスでクライアントに戻される
というようなことをしているらしい。これで単に同じプロセス上のオブジェクトにメソッドを投げたのと同じように見えながら、実際は通信経路を通ってサーバに渡されて実行されている、ということになる。

上の例のコードのserverProxyはデバガのタイプには「NSDistantObject」と表示されるが、例えばdescriptionを呼ぶとこの経路でサーバに渡されて評価されるのでサーバ側のオブジェクトのクラス名(上の例ではDOServer)が返ることになる。

良くデバガで見てみると、ランタイム上にないクラスのインスタンスがやりとりされたとき、自動的にNSDistantObjectのインスタンスが自動的に追加されて、それへのメソッド呼び出しはサーバクライアントに関係なく常に通信路を通って反対側に渡されるている。

したがってさっきのコールバックではサーバ側には、実はclientオブジェクトに繋がったNSDistantObjectのインスタンスがわたってきている。その結果、clientオブジェクトへのメソッド呼び出しはちゃんと実行されることになる。

実行時バインドの特性を思う存分使ったObjective-Cらしいやりかたで、JavaやC++ではこんな簡単には実現不可能である(JavaやC++ではコンパイル時にオブジェクトの型が決まってなければメソッドを発行できない。サーバ側は自分には無関係にもかかわらず、クライアントオブジェクトの定義が必要になる)。非常に面白い。

細かい点、とくにretainCountなんかはどう処理されているのかよくわからない。このままではNSDistantObjectのインスタンスをretainするとそれも反対側のプロセスのオブジェクトをretainすることになってNSDistantObject本体のretainCountは変わらない。NSProxyがNSObjectを継承していないのはこう言う処理の違いを実現するためなのかもしれない。

3.3  一般のオブジェクト

クライアントとサーバでまったく同じ名前で同じ定義をしたクラスを持って、そのインスタンスをやりとりしたとしてもローカルなコピーではなくNSDistantObjectのインスタンスが作られるようになっている。

一方でFoundationフレームワークのオブジェクトをやりとりしたときは、NSDistantObjectのインスタンスではなくローカルなコピーが作られる。これはMutable/Immutableに関係なくつねにそうなるらしい。

そうすると、例えばサーバ側がMutableな文字列をインスタンス変数として持っていたとする。
@interface DOServer : NSObject <DOProtocol> {
    NSMutableString *str;
}
そしてそれをクライアント側に渡すようなメソッドを定義したとする。
- (NSMutableString *)stringInstance
{
    return str;
}
これはこのメソッドが呼ばれた時点でのインスタンス変数の値のコピーが返ってくる。この文字列をどう変更しようと、サーバ側のインスタンス変数には反映されない。

これは自前定義のクラスのオブジェクトを渡したときと動作が違ってくる、ということになる。

この違いは注意が必要だけど、そもそも他人のインスタンス変数を副作用的に書き換えるのは、ローカルなオブジェクトどうしであっても行儀のいいこととは言えない。
nice!(0)  コメント(0)  トラックバック(0) 

nice! 0

コメント 0

コメントを書く

お名前:
URL:
コメント:
画像認証:
下の画像に表示されている文字を入力してください。

トラックバック 0

献立10/27献立10/28 ブログトップ

この広告は前回の更新から一定期間経過したブログに表示されています。更新すると自動で解除されます。