@uents blog

Code wins arguments.

Cocoaプログラミング:pipeでプロセス間通信を試す

最近MacBook Airを買ったのですが、せっかくMacを買ったのでCocoaをやらねばということで、Cocoaプログラミングをちまちまと勉強しています。手始めに詳解 Objective-C 2.0を読んだのですが、プロセス間通信のフレームワークの解説が少なかったので、少し勉強してみました。

pipeを試す

pipeを使うためのクラスであるNSPipeを試してみたいと思います。

まずは、Cでひとりpipeをread/writeするコードです。

#import <stdlib.h>
#import <stdio.h>
#import <string.h>
#import <unistd.h>

static void pipe_test(void)
{
    const char message[] = "hello";
    char buffer[1024];
    int pipes[2];

    if (pipe(pipes) != 0){
        goto err;
    }
    printf("read_fd = %d write_fd = %d\n", pipes[0], pipes[1]);

    // write data
    write(pipes[1], message, sizeof(message));

    // read data
    read(pipes[0], buffer, sizeof(message));
    printf("reslut => %s\n", buffer);

err:
    return;
}

Xcodeでの実行すると下記のようになります。

read_fd = 3 write_fd = 4
reslut => hello

pipe()によりread/write用のfdが割り当てられますが、それぞれ3,4となっています。UNIXの場合、標準入力/標準出力/標準エラー出力がそれぞれ0,1,2となるので、期待通りですね。

これをNSPipeクラスを使えば、以下のように書き替えることができます。

#import <Foundation/Foundation.h>
#import <stdlib.h>

static void nspipe_test(void)
{
    const char message[] = "hello";

    @autoreleasepool {
        NSPipe *pipe = [[NSPipe alloc] init];
        printf("read_fd = %d write_fd = %d\n",
               [[pipe fileHandleForReading] fileDescriptor],
               [[pipe fileHandleForWriting] fileDescriptor]);

        // write data
        NSData *inData = [[NSData alloc] init];
        inData = [inData initWithBytes:message length:sizeof(message)];
        [[pipe fileHandleForWriting] writeData:inData];

        // read data
        NSData *outData = [[pipe fileHandleForReading]
                           readDataOfLength:sizeof(message)];
        printf("result => %s\n",
               [[[NSString alloc]
                 initWithData:outData encoding:NSUTF8StringEncoding]
                UTF8String]);
    }
    return;
}

結果はCの例と同じです。

read_fd = 3 write_fd = 4
reslut => hello

NSPipeクラスには上記の通りデータ入出力用のメソッドが用意されていますので、NSPipeオブジェクトを操作するだけよいことが分かります。
また、読み出し側の実装は[pipe fileHandleForReading] readDataToEndOfFile]でもよいですが、その場合EOFを検出するまでブロッキングされます。具体的には、[pipe fileHandleForWriting] closeFile]のようにwrite側がクローズされるまでブロックされます。

pipeでプロセス間通信を行う

fork/exec()による実行結果をpipeでつなぐことで、2プロセス間のデータ通信を行うコードを書いてみます。

具体的には、write側のプロセス "ps -a" の結果を、read側のプロセス ”cat" で出力するコードです。

まずはCの例です。

static void pipe_test2(void)
{
    int pipes[2];

    if (pipe(pipes) != 0){
        goto err;
    }

    int ret = fork();
    // child process
    if (ret == 0){
        char* cmd[] = {"cat", 0}};
        printf("child process start\n");

        close(0);
        int new_fd = dup(pipes[0]);
        printf("#c read_fd = %d write_fd = %d new_fd = %d\n", pipes[0], pipes[1], new_fd);

        close(pipes[0]);
        close(pipes[1]);

        execvp(cmd[0], cmd);
    }
    // parent process
    else {
        char* cmd[] = {"ps", "-a" ,0};
        printf("parent process start\n");

        close(1);
        int new_fd = dup(pipes[1]);
        printf("#p read_fd = %d write_fd = %d new_fd = %d\n", pipes[0], pipes[1], new_fd);

        close(pipes[0]);
        close(pipes[1]);

        execvp(*cmd[0], cmd);
    }
err:
    return;
}

実行結果は以下の通りとなりました。

Xcodeで実行すると、write側のプロセス(親プロセス)の "ps -a" の結果が、read側のプロセス(子プロセス)に渡って出力されます。
ただし、なぜかGDBでbreakがかかるしオプション "-a" も伝わってないですが・・・(もし分かる方がいらっしゃれば教えて下さい><)

[Switching to process 918 thread 0x0]
#child read_fd = 3 write_fd = 4 new_fd = 0
#parent read_fd = 3 write_fd = 4 new_fd = 1
(gdb) c
Continuing.
  PID TTY           TIME CMD
  152 ttys000    0:00.00 (login)
  153 ttys000    0:00.00 -bash
Program ended with exit code: 0

これをNSPipe/NSTaskを使って書き直してみます。

static void nspipe_test3(void)
{
    @autoreleasepool {
        NSPipe *pipe = [[NSPipe alloc] init];

        NSTask *task1 = [[NSTask alloc] init];
        [task1 setLaunchPath:@"/bin/cat"];
        [task1 setStandardInput:pipe];
        [task1 launch];

        NSTask *task2 = [[NSTask alloc] init];
        [task2 setLaunchPath:@"/bin/ps"];
        [task2 setArguments:[NSArray arrayWithObjects:@"-a", nil]];
        [task2 setStandardOutput:pipe];
        [task2 launch];

        [task1 waitUntilExit];
        [task2 waitUntilExit];
    }
    return;
}

NSTaskオブジェクトがfork/execの実行を肩代わりしてくれるので、実装としてもすごく簡潔になりました(^^)

実行結果は下記の通りです。

[Switching to process 817 thread 0x0]
  PID TTY           TIME CMD
  152 ttys000    0:00.03 login -pfl uents /bin/bash -c exec -la bash /bin/bash
  153 ttys000    0:00.09 -bash
Program ended with exit code: 0

Cocoaで外部プログラムを実行する時は、forkするよりもNSTaskを使う方が、はるかに楽できそうです。