darakeLog

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

2019-09-15 10:01:04 C言語 Linux

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

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

開発環境

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

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

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

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

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

<pre class="code lang-c" data-lang="c" data-unlink=""><span class="synPreProc">#include </span><span class="synConstant"><unistd.h></span>
<span class="synType">int</span> execve(<span class="synType">const</span> <span class="synType">char</span> *filename, <span class="synType">char</span> *<span class="synType">const</span> argv[],
<span class="synType">char</span> *<span class="synType">const</span> envp[]);

参考:Man page of EXECVE

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

# hello.rb
puts "hello world"
<pre class="code lang-c" data-lang="c" data-unlink=""><span class="synPreProc">#include </span><span class="synConstant"><stdio.h></span>
<span class="synPreProc">#include </span><span class="synConstant"><unistd.h></span>
<span class="synPreProc">#include </span><span class="synConstant"><stdlib.h></span>
<span class="synType">int</span> main() {
<span class="synType">char</span> *argv[<span class="synConstant">3</span>] = {<span class="synConstant">"/usr/bin/ruby"</span>, <span class="synConstant">"hello.rb"</span>, <span class="synConstant">NULL</span>};
<span class="synStatement">if</span>(execve(argv[<span class="synConstant">0</span>], argv, <span class="synConstant">NULL</span>) == -<span class="synConstant">1</span>) {
printf(<span class="synConstant">"ERROR! LINE: </span><span class="synSpecial">%d</span><span class="synConstant">"</span>, <span class="synConstant">__LINE__</span>);
exit(<span class="synConstant">1</span>);
}
}

ポイントは、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つの機能を使えば良い。

プロセスとは?

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

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

f:id:toumasuxp:20190915192250j:plain

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

パイプ(pipe)とは?

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

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

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

<pre class="code lang-c" data-lang="c" data-unlink=""> <span class="synType">int</span> child2parent[<span class="synConstant">2</span>];
<span class="synType">int</span> child = pipe(child2parent);

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

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

f:id:toumasuxp:20190915193149j:plain

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

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

参考:Man page of PIPE

dup2とは?

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

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

<pre class="code lang-c" data-lang="c" data-unlink="">dup(child2parent[<span class="synConstant">1</span>], <span class="synConstant">1</span>);

参考:Man page of DUP

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

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

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

pipeを作成する

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

<pre class="code lang-c" data-lang="c" data-unlink="">    <span class="synType">int</span> child2parent[<span class="synConstant">2</span>];
<span class="synType">int</span> parent2child[<span class="synConstant">2</span>];
<span class="synType">int</span> child = pipe(child2parent);
<span class="synType">int</span> parent = pipe(parent2child);

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

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

<pre class="code lang-c" data-lang="c" data-unlink="">    pid_t pid = fork();
<span class="synStatement">if</span>(pid == -<span class="synConstant">1</span>) {
<span class="synStatement">else</span> <span class="synStatement">if</span>(pid == <span class="synConstant">0</span>) {
} <span class="synStatement">else</span> {
}

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

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

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

<pre class="code lang-c" data-lang="c" data-unlink="">        printf(<span class="synConstant">"child call!!</span><span class="synSpecial">\n</span><span class="synConstant">"</span>);
close(parent2child[<span class="synConstant">1</span>]);
close(child2parent[<span class="synConstant">0</span>]);
dup2(child2parent[<span class="synConstant">1</span>], <span class="synConstant">1</span>);
dup2(parent2child[<span class="synConstant">0</span>], <span class="synConstant">0</span>);
close(child2parent[<span class="synConstant">1</span>]);
close(parent2child[<span class="synConstant">0</span>]);
<span class="synType">char</span> *argv[<span class="synConstant">3</span>] = {<span class="synConstant">"/usr/bin/ruby"</span>, <span class="synConstant">"hello.rb"</span>, <span class="synConstant">NULL</span>};
<span class="synStatement">if</span>(execve(argv[<span class="synConstant">0</span>], argv, <span class="synConstant">NULL</span>) == -<span class="synConstant">1</span>) {
printf(<span class="synConstant">"ERROR! LINE: </span><span class="synSpecial">%d\n</span><span class="synConstant">"</span>, <span class="synConstant">__LINE__</span>);
exit(<span class="synConstant">1</span>);
}

二行目では

<pre class="code lang-c" data-lang="c" data-unlink="">        close(parent2child[<span class="synConstant">1</span>]);
close(child2parent[<span class="synConstant">0</span>]);

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

<pre class="code lang-c" data-lang="c" data-unlink="">        dup2(child2parent[<span class="synConstant">1</span>], <span class="synConstant">1</span>);
dup2(parent2child[<span class="synConstant">0</span>], <span class="synConstant">0</span>);

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

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

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

<pre class="code lang-c" data-lang="c" data-unlink="">        printf(<span class="synConstant">"parent call!!</span><span class="synSpecial">\n</span><span class="synConstant">"</span>);
close(parent2child[<span class="synConstant">0</span>]);
close(child2parent[<span class="synConstant">1</span>]);
<span class="synType">char</span> buf[<span class="synConstant">20</span>];
<span class="synStatement">if</span>(read(child2parent[<span class="synConstant">0</span>], buf, <span class="synConstant">20</span>) == -<span class="synConstant">1</span>) {
printf(<span class="synConstant">"ERROR! LINE: </span><span class="synSpecial">%d</span><span class="synConstant">"</span>, <span class="synConstant">__LINE__</span>);
exit(<span class="synConstant">1</span>);
};
printf(<span class="synConstant">"read call!!</span><span class="synSpecial">\n</span><span class="synConstant">"</span>);
write(STDOUT_FILENO, buf, <span class="synConstant">20</span>);

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

<pre class="code lang-c" data-lang="c" data-unlink="">        <span class="synStatement">if</span>(read(child2parent[<span class="synConstant">0</span>], buf, <span class="synConstant">20</span>) == -<span class="synConstant">1</span>) {
printf(<span class="synConstant">"ERROR! LINE: </span><span class="synSpecial">%d</span><span class="synConstant">"</span>, <span class="synConstant">__LINE__</span>);
exit(<span class="synConstant">1</span>);
};

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

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

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

<pre class="code" data-lang="" data-unlink="">parent call!!
child call!!
read call!!
hello world