コンパイルする

Rubyを組込むには、ruby.hヘッダが要ります。 このファイルには、プラットフォーム固有のruby/config.hヘッダが含まれます。 恐らく、これらのヘッダのincludeパスをコンパイラに伝える必要があるでしょう。 Rubyライブラリともリンクする必要があります。 私の環境では、最小限のコンパイラオプションは次の通りです。

$ gcc foo.c -I/usr/include/ruby-3.0.0 -I/usr/include/ruby-3.0.0/x86_64-linux -lruby

しかしもし可能であれば、pkg-configを使うと良いでしょう。 自分のOSに適したオプションが得られます。

$ pkg-config --cflags --libs ruby-3.0

こうしたやり方は、Rubyがマシン上の標準的でない場所にインストールされていたり、OSが標準的なヘッダないしライブラリを提供していないときは、うまくいかないかもしれません。 より頑健なやり方は、どこにあるのかRuby自体に問い合わせて、実行時ライブラリパスを準備することです。

#!/usr/bin/env ruby
require 'shellwords'

# ruby.hの場所
hdrdir = Shellwords.escape RbConfig::CONFIG["rubyhdrdir"]
# ruby/config.hの場所
archhdrdir = Shellwords.escape RbConfig::CONFIG["rubyarchhdrdir"]
# librubyの場所
libdir = Shellwords.escape RbConfig::CONFIG["libdir"]

# GCCの引数
puts "-I#{hdrdir} -I#{archhdrdir} -L#{libdir} -Wl,-rpath,#{libdir}"

Windows

Windowsでは、RubyInstallerDevkit付きで使うことを強くお勧めします。 このツールにより、WindowsのコマンドラインでGCCを使えるようになり、ビルドフラグの取得にpkgconfも使えます。

$ ridk enable # devkitを有効にします
$ pkgconf.exe --cflags --libs C:\Ruby33-x64\lib\pkgconfig\ruby-3.3.pc # GCCの引数を表示
$ gcc foo.c ...

なお、GCCの引数はPowerShellでは正しく解析されないことがあります。 そのため、cmdを代わりに使ってください。 また、Ruby 3.3のインストーラーは何らかの理由でpkgconfの出力からライブラリの場所が欠けています。 そのため、手動で-Lオプションを加える必要があります。 前述のように、RbConfig::CONFIG["libdir"]で配置できます。 筆者の場合、インストーラーにより、C:\Ruby33-x64\libに置かれました。

最後に、Windowsにはrpathはありません。 そのため、ビルドされた実行するプログラムが実行できるようにするため、リンクされたDLLをそのそばに複製する必要があるでしょう。 これには、Ruby自体に必要なDLLも全て含まれます。 筆者の場合、インストーラはこれらをC:\Ruby33-x64\bin\ruby_builtin_dllsに置いています。

起動と終了

RubyインタプリタをCやC++のプログラムに含めることはとても簡単です。 ヘッダーを含めて、 APIを使用する前にインタプリタを立ち上げるための関数を main で呼び、 そして完了後に後片付けをする関数を呼べばよいのです。

#include <ruby.h>

int main(int argc, char* argv[])
{
	/* VMを構築 */
	ruby_init();

	/* Rubyはここに入る */

	/* VMを解体 */
	return ruby_cleanup(0);
}

ruby_init() の最中にVMが実行に失敗したら、エラーを表示してプログラムが終了してしまいます!もっと柔軟にエラーを出したいときは、代わりに失敗したときにゼロではない値を返すruby_setup() を使いましょう(残念ながら、この場合のエラーメッセージの出しかたはよくわかっていません1)。

rb_cleanup() の最中にエラーが発生したときは、ゼロではない値を返します。 もしエラーが発生しなければ、渡した引数が返ります。 この仕様により、後片付けに失敗したときのエラーステータスを返す部分が少し短く書けます(先の例で実演したように)。

技術的には mainruby_initruby_setup を呼ぶ必要はありません。 しかし、RubyのVMは以降全てのRubyのコードが同じかこれより低層のスタックフレームから 実行されることを仮定しています(ガベージコレクションのためです)。 他の方法でも動くにせよ、 このことを確証する最も簡単な方法がプログラムのトップレベルで立ち上げを行うことなのです。 例えば深く入れ子になった関数でRubyを初期化して、沢山のスタックフレームを立ち上げ、 そして沢山のRubyのコードを実行するようなことはよくありません。

後片付けをするときにも、VMはRubyのコードを評価するかもしれません(at_exit にブロックを渡したときなど)。 そしてそのときに例外が発生する可能性があります。 ruby_cleanup() はこのような例外が発生したときに、 ゼロではない値を返してエラーメッセージを表示することで制御します。 代わりに ruby_finalize() を呼ぶと、通常通り例外を発生させます。 (制御方法についてはExceptions節を参照)

別の例はこちらです。

#include <ruby.h>

int main(int argc, char* argv[])
{
	if (ruby_setup())
	{
		/* Rubyなしのコードを走らせる */
	}
	else
	{
		/* Rubyはここに入る */

		ruby_finalize(); /* XXX 絶対にここで例外をrescueすること */
	}

	return 0;
}

制約

上記のスタックフレームの警告以外にも制約があります。 1つのプロセスに1つだけRubyのVMを動かせます。 起動と終了の方法を見ると、何度でもVMの破壊と創造を繰り返せるような気がしてくるかもしれませんが、 ruby_cleanup はRubyのコードが全ての後片付けが完了したことだけを確認します。 VMの状態を再度初期化できるような状態までは後片付けしません。 もう一度 ruby_init を呼び出すと、実行に失敗してしまうでしょう。

何かかの理由があってプログラムで複数のRubyのVMが必要になったら、 この制約を回避するために複数のプロセスに小分けにしなければいけません。

VMを設定する

これでRubyのVMの実行の骨子を会得しました。 でも、Rubyのコードの実行に先駆けてもう少し設定したいことがあるかもしれません。 エラーメッセージとかのためにRubyのスクリプトの名前(例:$0)を設定したいときは、 以下のようにします。

ruby_script("new name")

gemが require で呼び出せるようにするためにロードパスを設定するには、 次のようにします。

ruby_init_loadpath()

VMにはコマンドラインで ruby するときと同じオプションを渡せます。 警告水準や冗長モード2の設定に手頃です。

#include <ruby.h>

int main(int argc, char* argv[])
{
	ruby_init();

	char* options[] = { "-v", "-eputs 'Hello, world!'" };
	void* node = ruby_options(2, options);

	int state;
	if (ruby_executable_node(node, &state))
	{
		state = ruby_exec_node(node);
	}

	if (state)
	{
		/* 例外に対処します。ないとは思いますが */
	}

	return ruby_cleanup(state);
}

ruby_optionsへの引数はmain関数と同じargcargvです。そして、rubyプログラムのmainと同じように、呼び出したときはVMは何らかのRubyのコードがあるものとしています。ロードするスクリプトのファイル名を与えていなかったり、-eで実行するコードがないときは、stdinから読み込もうとします。オプションを設定したいけれども、Rubyのコードを実行したく ない ときには、"-e "のように空行を渡せばよいです。

ruby_options() はコンパイルされたRubyのコードを表現する “node” を返します。 場合によっては(文法エラーとか)nodeが不正で実行すべきでないときがあります。 ruby_executable_node() はこのnodeを検査します。 nodeが妥当であれば、 ruby_exec_node() で実行できます。 ruby_executable_node() で(ポインタを介して)返る状態は、 コンパイルの最中やコードの実行時に例外が発生したら、ゼロではない値になります。 例外を自前で読むこともできますし、 ruby_cleanup()state を渡して適切なエラーメッセージを表示させることもできます。

Rubyは今のところコードの他のコンパイル・実行を別々にする方法を提供していません3

やったね

これでRubyとやりとりできました! C APIに戻りましょう。

脚註

  1. ruby_init()では、エラーメッセージを取得するためにerror_print() を使います。 しかし、この関数はAPIとして提供されていません。 これは普通に例外的なものなのでしょうか。 

  2. 筆者の実験では、-w-v といったフラグは得られませんでした。 ruby_prog_init() が関係するのかもしれません。 また、コマンドラインオプションを解析せず、これを実現できるはずです。 

  3. rb_load_file() 関数でこれができそうですが、動かせた試しがありません。