C言語からRubyやPHPなどのスクリプト言語を呼び出して、実行結果をC言語に渡す方法

最近、自作httpサーバーをC言語で書いているが、「C言語側からRubyやPHPを呼び出して、そのRubyの実行結果をC言語側に渡す」という方法が分からなかった。

いろいろ調べた所、「これでOK」という方法が見つかったので、備忘録としてまとめていく。

開発環境

  • Mac 10.14.6
  • Ruby 2.3.7
  • Cコンパイラ Apple LLVM version 10.0.1

今回は上記の環境で試しているが、Unixの中心的な機能のみを使った実装にしているので、Linuxでもgccでも同じように動くと思う。

今回はRubyのコードを呼び出すという事をしているが、別にPHPだろうがPerlだろうが変わりはない。

C言語からRubyのプログラムを呼び出す

まずは単純にC言語からRubyのプログラムを呼び出す、というコードを書いていく。

これを実現するために必要なのが、execveという関数。第1引数に実行するプログラム、第2引数に実行するプログラムに与える引数、第3引数には環境変数を入れる。

#include <unistd.h>
int execve(const char *filename, char *const argv[],
char *const envp[]);

参考:Man page of EXECVE

サンプルとして、hello.rbというファイルを用意して、実際にコードを書くと、以下のようになる。(結果は、hello worldと出力される)

# hello.rb
puts "hello world"
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int main() {
        char *argv[3] = {"/usr/bin/ruby", "hello.rb", NULL};

        if(execve(argv[0], argv, NULL) == -1) {
                printf("ERROR! LINE: %d", __LINE__);
                exit(1);
        }
}

ポイントは、char *argv[3] = {"/usr/bin/ruby", "hello.rb", NULL};のところ。"/usr/bin/ruby"にはrubyの実行ファイルを示しており、"hello.rb"は実行ファイルの引数として与える。あと、コマンドライン引数の最後には、NULLを入れておく事。

上記のコードは、要はRubyを実行するときにやっている/usr/bin/ruby hello.rbとかruby hello.rbをC言語で行なっているだけに過ぎない。

子プロセス上でRubyを実行して、実行結果を親プロセスに渡す

上記のコードの場合だと「普通にrubyで実行しろよ」となるが、問題は「C言語側からRubyを呼び出して、Rubyの実行結果をC言語に渡したい」という事。

これを実現するためには、OSの以下の3つの機能を使えば良い。

  • fork()でプロセスを複製する
  • pipe()で複数のプロセス間でやりとりができるようにする。
  • dup2()でプロセス間のやり取りを、stdout,stdinで行うようにする

プロセスとは?

プロセスとは、プログラム毎に用意されるプログラムを実行するための空間のこと。1つのプロセスの中には、そのプログラムを実行するための機械語のコードや変数を記憶するためのメモリ、スタックなどが用意されている。

今回はfork()を使うことで、現在実行しているプロセスの複製を作る。複製したプロセスのことを「子プロセス」と呼び、複製元のプロセスを「親プロセス」と呼ぶ。

上記の図では親プロセスと子プロセスにint i = 10が作られているが、親プロセスの変数iと子プロセスの変数iは全く別のものとして扱われる。つまり、親プロセスの変数iを変更しても、子プロセスの変数iには影響を与えない。

パイプ(pipe)とは?

場合によっては、違うプロセス同士を繋いでお互いが通信を行えるようにしたい時がある。(C言語からRubyの結果をもらいたい場合とか)

その場合にpipe()を使ってプロセス間通しの通信を行えるようにすれば良い。

例えば、以下のようにpipe()を使う。

int child2parent[2];
int child = pipe(child2parent);

pipe()を実行してパイプを作成した時に、OSはこのパイプでやり取りをするための一時バッファを用意する。(バッファとは、データを一時的に貯めてくれるもの)

パイプを作成した時に、そのパイプにアクセスするためのファイル・ディスクリプター (descripter) を用意する。(FILE *fp的なやつ)

上記ではint child2parent[2]をディスクプリターとして用意しているが、child2parent[1]は書き込み専用のディスクプリターの参照(ポインタ的なやつ)が与えられて、こいつを使ってパイプのバッファに書き込む。

そしてchild2parent[0]は読み込み専用ディスクプリターの参照が与えられて、child2parent[1]を使ってバッファに書き込まれた内容を読み込む時に使う。

参考:Man page of PIPE

dup2とは?

上記のpipe()を使った後にwrite(child2parent[1], buf, buf_size)と言う処理を書けば、パイプのバッファに書き込むことができるが、write()ではなく、標準出力を使ってパイプのバッファに書き込みたい時がある。

この場合にはdup2()を使えば良い。以下のように書くことで、標準出力がバッファの書き込みをしてくれるようになる。

dup(child2parent[1], 1);

参考:Man page of DUP

実際にコードを書いてみる。

理論的な話はこれまでにして、実際にコードを書いていく。まずはコードの全体像から。

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
int main() {
int child2parent[2];
int parent2child[2];
int child = pipe(child2parent);
int parent = pipe(parent2child);
pid_t pid = fork();
if(pid == -1) {
printf("ERROR! LINE: %d\n", __LINE__);
exit(1);
} else if(pid == 0) {
printf("child call!!\n");
close(parent2child[1]);
close(child2parent[0]);
dup2(child2parent[1], 1);
dup2(parent2child[0], 0);
close(child2parent[1]);
close(parent2child[0]);
char *argv[3] = {"/usr/bin/ruby", "hello.rb", NULL};
if(execve(argv[0], argv, NULL) == -1) {
printf("ERROR! LINE: %d\n", __LINE__);
exit(1);
}
} else {
printf("parent call!!\n");
close(parent2child[0]);
close(child2parent[1]);
char buf[20];
if(read(child2parent[0], buf, 20) == -1) {
printf("ERROR! LINE: %d", __LINE__);
exit(1);
};
printf("read call!!\n");
write(STDOUT_FILENO, buf, 20);
}
}

pipeを作成する

main関数の1行には以下のように書いているが、これはプロセス間の通信を行うためのパイプを作成している。子プロセスから親プロセスのchild2parent[2]、親プロセスから子プロセスのparent2child[2]のパイプを2種類を用意した。

int child2parent[2];
int parent2child[2];
int child = pipe(child2parent);
int parent = pipe(parent2child);

forkで子プロセスを作成する。

pid_t pid = fork();で親プロセスを複製して、子プロセスを作成する。fork()の戻り値はプロセス毎に異なっており、現在実行しているプロセスが子プロセスなら0、親プロセスなら子プロセスのプロセスID、エラーの場合は-1を返す。

    pid_t pid = fork();
if(pid == -1) {
else if(pid == 0) {
} else {
}

上記のコードではif(pid == 0)のように書くことで、子プロセスと親プロセスで行う処理を分けている。

子プロセスで行なっている処理

子プロセスでは、以下のような処理を行う。

        printf("child call!!\n");
close(parent2child[1]);
close(child2parent[0]);
dup2(child2parent[1], 1);
dup2(parent2child[0], 0);
close(child2parent[1]);
close(parent2child[0]);
char *argv[3] = {"/usr/bin/ruby", "hello.rb", NULL};
if(execve(argv[0], argv, NULL) == -1) {
printf("ERROR! LINE: %d\n", __LINE__);
exit(1);
}

二行目では

        close(parent2child[1]);
close(child2parent[0]);

として、使わないパイプへの参照を閉じている。今回は、「子プロセスでrubyを使ってパイプのバッファに書き込んで、そのバッファを親プロセスで読み込む」と言う事をしたいので、親プロセスから子プロセスへの書き込みなどの不要なパイプへの参照は閉じるようにした。

        dup2(child2parent[1], 1);
dup2(parent2child[0], 0);

上記のコードでは、dup2(child2parent[1], 1);とする事で、本来、子プロセスから親プロセスへ書き込みをする時に使うディスクプリターを標準出力に肩代わりさせるようにしている。

親プロセスで行なっている処理

親プロセスでは、以下の処理を行う。

        printf("parent call!!\n");
close(parent2child[0]);
close(child2parent[1]);
char buf[20];
if(read(child2parent[0], buf, 20) == -1) {
printf("ERROR! LINE: %d", __LINE__);
exit(1);
};
printf("read call!!\n");
write(STDOUT_FILENO, buf, 20);

注目すべきなのは、以下の部分。

        if(read(child2parent[0], buf, 20) == -1) {
printf("ERROR! LINE: %d", __LINE__);
exit(1);
};

基本的に親プロセスの方が早く実行されることが多いが、read()が呼び出された時には、まだchild2parent[0]が参照しているバッファには何も書き込まれていない。

その時にread()は処理をブロックして、プロセスが一時停止状態に入るので、子プロセスの方が実行されるようになる。そして、子プロセスでバッファに書き込まれるので、read()が実行されると言う流れになっている。

上記のコードの結果は、以下のようになる。

parent call!!
child call!!
read call!!
hello world