CでRubyを実行する

この例はtagという単純で小さなゲームです。このゲームには2升あります。青升はキーボード上の矢印キーを使って操作しますが赤升はRubyのスクリプトで操作します。動かすためにC APIを使ってスクリプトがアクセスできる小さなRuby APIを定義し、全てのフレーム毎にRubyスクリプトで定義されているメソッドを呼び出し2升用のデータをカプセル化するオブジェクトを渡します。

Rubyスクリプトは以下のような見た目をしています。

def think ai, player
  # 位置を得る
  ax, ay = ai.pos

  # プレーヤーが動いた方向を得る
  dx, dy = player.dir

  # ……動作の仕組み……
  x = dy
  y = -dx

  # この方向に動く
  ai.move x, y
end

CのコードはグラフィックにSDL2と入力を使い、ファイルの変更に合わせてAIスクリプトが即座に再読み込みされるようstat()(あまり可搬性は高くなさそうですが)を使います。以下はtag.cです。

#include <stdio.h>
#include <stdbool.h>
#include <math.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>

#include <SDL2/SDL.h>
#include <ruby.h>

/* 定数 */
const unsigned int win_width = 1024;
const unsigned int win_height = 768;
const unsigned int actor_size = 30;

/* 位置と方向用 */
struct vec2
{
	float x;
	float y;
};

/* プレーヤーと敵用 */
struct actor
{
	struct vec2 pos;
	struct vec2 dir;
	float speed; // ピクセル毎ミリ秒での最高速度
	SDL_Color color;
};

/* AIのアクターにAIのスクリプトのメタデータを渡すための梱包 */
struct ai_actor
{
	char* script;
	bool loaded;
	bool error;
	time_t load_time;
	struct actor* actor;
};

/* AIのエラー状態を設定し、例外を印字する可能性があります */
void ai_error(struct ai_actor* ai)
{
	ai->error = true;

	ai->actor->dir.x = 0.f;
	ai->actor->dir.y = 0.f;
	ai->actor->color.a = 127;

	/* 例外を印字 */
	VALUE exception = rb_errinfo();
	rb_set_errinfo(Qnil);

	if (RTEST(exception)) rb_warn("AI script error: %"PRIsVALUE"", rb_funcall(exception, rb_intern("full_message"), 0));
}

/* AIのエラー状態を消去 */
void ai_reset(struct ai_actor* ai)
{
	ai->error = false;
	ai->actor->color.a = 255;
}

/* (再)読み込みしたAIスクリプトを試す */
void ai_load(struct ai_actor* ai)
{
	/* スクリプトの変更日時を得る */
	struct stat script_stat;
	if (stat(ai->script, &script_stat))
	{
		if (ai->loaded)
			fprintf(stderr, "Can't stat AI script\n");
		ai->loaded = false;
		ai_error(ai);
		return;
	}

	/* 既にスクリプトを読み込んでおり更新されていなければ、することはありません */
	if (ai->loaded && ai->load_time == script_stat.st_mtime) return;

	if (ai->loaded)
		fprintf(stderr, "Reloading AI...\n");
	else
		fprintf(stderr, "Loading AI...\n");

	ai->loaded = true;
	ai->load_time = script_stat.st_mtime;

	ai_reset(ai);

	int state;
	rb_load_protect(rb_str_new_cstr(ai->script), 0, &state);

	if (state) ai_error(ai);
}

/* AIのスクリプト中で例外をrescueするためのもの */
VALUE think_wrapper(VALUE actors)
{
	rb_funcall(rb_mKernel, rb_intern("think"), 2, rb_ary_entry(actors, 0), rb_ary_entry(actors, 1));

	return Qundef;
}

/* 可能であればAIのスクリプトを走らせる */
void ai_think(struct ai_actor* ai, VALUE ai_v, VALUE player_v)
{
	if (!ai->loaded || ai->error) return;

	int state;
	rb_protect(think_wrapper, rb_ary_new_from_args(2, ai_v, player_v), &state);

	if (state) ai_error(ai);
}

/* msだけ時間が経ったらアクターを動かす */
void step_actor(struct actor* act, unsigned int ms)
{
	float norm = sqrtf(act->dir.x * act->dir.x + act->dir.y * act->dir.y);

	/* 動きなし */
	if (norm == 0.f) return;

	/* アクターがspeedに比べて遅くはあっても速くならない程度に動けるようにします */
	if (norm < 1.f) norm = 1.f;

	act->pos.x += (act->dir.x * act->speed * (float)ms) / norm;
	act->pos.y += (act->dir.y * act->speed * (float)ms) / norm;

	/* 位置が画面内に来るよう切り詰めます */
	if (act->pos.x < 0.f)
		act->pos.x = 0.f;
	else if (act->pos.x > win_width - actor_size)
		act->pos.x = win_width - actor_size;
	if (act->pos.y < 0.f)
		act->pos.y = 0.f;
	else if (act->pos.y > win_height - actor_size)
		act->pos.y = win_height - actor_size;
}

/* アクターを色付きの箱として描画します */
void draw_actor(SDL_Renderer* renderer, struct actor* act)
{
	SDL_SetRenderDrawColor(renderer, act->color.r, act->color.g, act->color.b, act->color.a);
	SDL_Rect rectangle = { .x = act->pos.x, .y = act->pos.y, .w = actor_size, .h = actor_size };
	SDL_RenderFillRect(renderer, &rectangle);
}

/* AIのスクリプト用に定義する、API用のメソッド */
/* time - 合計の消費時間をミリ秒で返します */
VALUE m_time(VALUE self)
{
	return UINT2NUM(SDL_GetTicks());
}

/* mark/free/GCなどは何ら必要ありません。クラスを定義するところにある後述のコメントを参照 */
static const rb_data_type_t actor_type = { .wrap_struct_name = "actor" };

/* Actor#pos - 画面の位置x, yをピクセルで返します */
VALUE actor_m_pos(VALUE self)
{
	struct actor* data;
	TypedData_Get_Struct(self, struct actor, &actor_type, data);

	return rb_ary_new_from_args(2, DBL2NUM(data->pos.x), DBL2NUM(data->pos.y));
}

/* Actor#dir - 直近の動く方向x, yを返します。それぞれ (-1..1) の範囲内です */
VALUE actor_m_dir(VALUE self)
{
	struct actor* data;
	TypedData_Get_Struct(self, struct actor, &actor_type, data);

	return rb_ary_new_from_args(2, DBL2NUM(data->dir.x), DBL2NUM(data->dir.y));
}

/* Actor#move - 次の動く方向を設定します。x, yはActor#posと同様です */
VALUE actor_m_move(VALUE self, VALUE x, VALUE y)
{
	float nx = NUM2DBL(x);
	float ny = NUM2DBL(y);

	struct actor* data;
	TypedData_Get_Struct(self, struct actor, &actor_type, data);

	data->dir.x = nx;
	data->dir.y = ny;

	return Qnil;
}

int main(int argc, char** argv)
{
	/* Rubyを開始 TODO これは余計だろうか */
	if (ruby_setup())
	{
		fprintf(stderr, "Failed to init Ruby VM\n");
		return 1;
	}
	/* <main>よりもいい名前を設定 */
	ruby_script("ruby");

	/* AIスクリプトで使う小振りの独自APIを定義 */
	rb_define_global_function("time", m_time, 0);

	/* ActorはRubyに渡す上で構造体actorを梱包します */
	VALUE cActor = rb_define_class("Actor", rb_cObject);
	rb_define_method(cActor, "pos", actor_m_pos, 0);
	rb_define_method(cActor, "dir", actor_m_dir, 0);
	rb_define_method(cActor, "move", actor_m_move, 2);

	/*
	   ActorがCのデータを梱包していたとしても、割り当てや解放関数を定義しなかった点に注目です。
	   これは全てのアクターをCで作り、Rubyに露出させるつもりだからです。
	   しかしRubyが新しいActorを作れてしまうと不当なデータポインタを含んでしまうので、
	   Rubyがそうできないようにすべきです。
	 */
	rb_undef_method(rb_singleton_class(cActor), "new");

	/* SDLを開始 */
	SDL_Init(SDL_INIT_VIDEO);

	/* ウィンドウを作成 */
	SDL_Window* window = SDL_CreateWindow(
		"Tag",
		SDL_WINDOWPOS_UNDEFINED,
		SDL_WINDOWPOS_UNDEFINED,
		win_width,
		win_height,
		0
	);
	if (window == NULL)
	{
		fprintf(stderr, "SDL_CreateWindow failed: %s\n", SDL_GetError());
		return 1;
	}

	/* 描画子を作成 */
	SDL_Renderer* renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);
	if (renderer == NULL)
	{
		fprintf(stderr, "SDL_CreateRenderer failed: %s\n", SDL_GetError());
		return 1;
	}
	SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);

	/* アクターを作成 */
	struct actor player = {
		.pos = { .x = win_width / 2.f + 100.f - actor_size / 2.f, .y = win_height / 2.f - actor_size / 2.f },
		.dir = { .x = 0.f, .y = 0.f },
		.speed = 0.5f,
		.color = { .r = 0, .g = 0, .b = 255, .a = 255 }
	};
	struct actor ai_act = {
		.pos = { .x = win_width / 2.f - 100.f - actor_size / 2.f, .y = win_height / 2.f - actor_size / 2.f },
		.dir = { .x = 0.f, .y = 0.f },
		.speed = 0.55f,
		.color = { .r = 255, .g = 0, .b = 0, .a = 255 }
	};

	struct ai_actor ai = {
		.script = "./ai.rb",
		.loaded = false,
		.error = false,
		.actor = &ai_act
	};

	/* アクター用のRubyオブジェクトを作成 */
	/* データがスタックにあるので解放関数にはNULLが使えます */
	VALUE player_v = TypedData_Wrap_Struct(cActor, &actor_type, &player);
	VALUE ai_v = TypedData_Wrap_Struct(cActor, &actor_type, &ai_act);

	/* プレーヤーがAIスクリプトを介して動くことがないようにします */
	rb_undef_method(rb_singleton_class(player_v), "move");

	/* タイミングを取る */
	unsigned int ai_step = 33; /* 30fpsでAIを走らせる */
	unsigned int last_time = SDL_GetTicks();
	unsigned int now;
	unsigned int frame_time;
	unsigned int ai_time;

	/* AIを起動 */
	ai_load(&ai);
	ai_think(&ai, ai_v, player_v);

	/* プレーヤーの入力用 */
	const Uint8* keyboard = SDL_GetKeyboardState(NULL);

	/* 本体の繰り返し */
	SDL_Event event;
	bool running = true;
	while (running)
	{
		/* タイマーを更新 */
		now = SDL_GetTicks();
		frame_time = now - last_time;
		ai_time += frame_time;
		last_time = now;

		/* イベントの制御 */
		while (SDL_PollEvent(&event))
		{
			switch (event.type)
			{
				case SDL_QUIT:
					running = false;
					break;
				case SDL_KEYDOWN:
					if (event.key.keysym.sym == SDLK_ESCAPE)
					{
						running = false;
						break;
					}
			}
		}

		/* プレーヤーの動作 */
		player.dir.x = 0.f;
		player.dir.y = 0.f;

		if (keyboard[SDL_SCANCODE_UP])
			player.dir.y -= 1.f;
		if (keyboard[SDL_SCANCODE_DOWN])
			player.dir.y += 1.f;
		if (keyboard[SDL_SCANCODE_LEFT])
			player.dir.x -= 1.f;
		if (keyboard[SDL_SCANCODE_RIGHT])
			player.dir.x += 1.f;

		/* AIの動作 */
		if (ai_time >= ai_step)
		{
			ai_load(&ai);
			ai_think(&ai, ai_v, player_v);

			ai_time %= ai_step;
		}

		/* ゲームを停止 */
		step_actor(&ai_act, frame_time);
		step_actor(&player, frame_time);

		/* 描画 */
		SDL_SetRenderDrawColor(renderer, 255, 255, 255, 255);
		SDL_RenderClear(renderer);

		draw_actor(renderer, &ai_act);
		draw_actor(renderer, &player);

		SDL_RenderPresent(renderer);

		/* CPUを休止 */
		SDL_Delay(1);
	}

	/* 整頓 */
	SDL_DestroyRenderer(renderer);
	SDL_DestroyWindow(window);

	/* SDLを停止 */
	SDL_Quit();

	/* Rubyを停止 */
	return ruby_cleanup(0);
}

Makefileは何ということはありません。

# これは本手引きでのRubyのバージョンを得るための単なるハックです
# 単にハードコードできます 例:RUBY=2.4
RUBY=$(shell grep rbversion ../../_config.yml | cut -d' ' -f2)

CFLAGS=-std=gnu11 -Wall $(shell pkg-config --cflags ruby-$(RUBY) sdl2)
LDLIBS=$(shell pkg-config --libs ruby-$(RUBY) sdl2)

all: tag

clean:
	rm -f tag *.o

RubyでCを実行する

この例は任意精度計算用のGMP Cライブラリを包むRubyのC拡張です。これは完全な例からはほど遠いものです。整数関数のみを包み、ライブラリの基本的な機能のみを実装しています。またRubyの既存の数値型といい感じに統合することに心を砕いてはいません。もし 本当の 完全な例が欲しければ GMP gemをご確認ください。

全てはGMP::Integerクラスを定義しているgmp.cにあります。

#include <ruby.h>
#include <gmp.h>
#include <string.h>

/*
   Cのデータを得るためにVALUEを開封することがしょっちゅうになるでしょう。
   そこまで大変ではないですが、面倒になってきます。
   これがあるとVALUEから奥底にあるデータに直通できるようにするものです。
 */
#define UNWRAP(val, data) \
	mpz_t* data;\
	TypedData_Get_Struct(val, mpz_t, &mpz_type, data);

/*
   またGMP::Integer型のオブジェクトのみ受け付けるように大変厳密にしていきます。
   なのでこれを頻繁に検査します。
 */
#define CHECK_MPZ(val) \
	if (CLASS_OF(val) != cInteger)\
		rb_raise(rb_eTypeError, "%+"PRIsVALUE" is not a %"PRIsVALUE, val, cInteger);

/* メソッドで簡単にアクセスできるので、これらを大域的にしているのはいいことです */
VALUE mGMP;
VALUE cInteger;

/* GMP::Integerに梱包されたデータを解放する関数 */
void integer_free(void* data)
{
	/* GMPにより割り当てられたメモリを解放 */
	mpz_clear(*(mpz_t*)data);

	free(data);
}

static const rb_data_type_t mpz_type = {
	.wrap_struct_name = "gmp_mpz",
	.function = {
		.dfree = integer_free,
		/* 恐らく.dsizeを設定すべきなのでしょうがどうmpz_tに書き込めばいいか分かりません…… */
	},
	.flags = RUBY_TYPED_FREE_IMMEDIATELY,
};

/* GMP::Integer.allocate */
VALUE integer_c_alloc(VALUE self)
{
	mpz_t* data = malloc(sizeof(mpz_t));
	/* GMPの初期化 */
	mpz_init(*data);

	return TypedData_Wrap_Struct(self, &mpz_type, data);
}

/* GMP::Integer#initialize
  
   最初の引数を使って内部のmpz_tを設定します
  
   最初の引数が文字列なら2つ目のFixnum引数を文字列を解釈するための基数として与えられます。
   既定の0の基数は基数がStringの前置詞から決定することを意味しています。
 */
VALUE integer_m_initialize(int argc, VALUE* argv, VALUE self)
{
	int base = 0;

	/* 基数のオプション引数を確認 */
	VALUE val;
	VALUE rbase;
	if (rb_scan_args(argc, argv, "11", &val, &rbase) == 2)
	{
		/* 基数は文字列のみが認識されます */
		Check_Type(val, T_STRING);
		Check_Type(rbase, T_FIXNUM);

		base = FIX2INT(rbase);

		/* GMPは特定の基数のみを受け付けます */
		if (!(base >= 2 && base <= 62) && base != 0)
			rb_raise(rb_eRangeError, "base must be 0 or in (2..62)");
	}

	UNWRAP(self, data);

	VALUE str;

	switch (TYPE(val))
	{
		case T_FIXNUM:
			/* 簡単な場合 */
			mpz_set_si(*data, FIX2LONG(val));
			return self;
		case T_BIGNUM:
			/* これが安全に変換する上で一番簡単な方法です */
			str = rb_funcall(val, rb_intern("to_s"), 0);
			base = 10;
			break;
		case T_STRING:
			str = val;
			break;
		case T_DATA:
			/* 別のGMP::Integerを複製 */
			if (CLASS_OF(val) == cInteger)
			{
				UNWRAP(val, other);

				mpz_set(*data, *other);

				return self;
			}
			/* breakは意図的に省かれています */
		default:
			rb_raise(rb_eTypeError, "%+"PRIsVALUE" is not an integer type", val);
			break;
	}

	/* 代入 */
	char* cstr = StringValueCStr(str);
	if (mpz_set_str(*data, cstr, base))
	{
		if (base == 0)
			rb_raise(rb_eArgError, "invalid number: %"PRIsVALUE, val);
		else
			rb_raise(rb_eArgError, "invalid base %d number: %"PRIsVALUE, base, val);
	}

	return self;
}

/* GMP::Integer#to_s
  
   Stringの基数(既定は10)のためにオプションのFixnum引数を受け付けます
 */
VALUE integer_m_to_s(int argc, VALUE* argv, VALUE self)
{
	int base = 10;

	/* 基数のオプション引数を確認 */
	VALUE rbase;
	if (rb_scan_args(argc, argv, "01", &rbase) == 1)
	{
		Check_Type(rbase, T_FIXNUM);

		base = FIX2INT(rbase);

		/* GMPは特定の基数のみを受け付けます */
		if (!(base >= -36 && base <= -2) && !(base >= 2 && base <= 62))
			rb_raise(rb_eRangeError, "base must be in (-36..-2) or (2..62)");
	}

	UNWRAP(self, data);

	/* GMPからCの文字列を得る */
	char* cstr = malloc(mpz_sizeinbase(*data, base) + 2);
	mpz_get_str(cstr, base, *data);

	/* Rubyの文字列を作成 */
	VALUE str = rb_str_new_cstr(cstr);

	/* メモリを解放 */
	free(cstr);

	return str;
}

/* GMP::Integer#to_i */
VALUE integer_m_to_i(VALUE self)
{
	/* 最も安全かつ簡単に変換する方法はto_s.to_iを呼び出すことです */
	return rb_funcall(integer_m_to_s(0, NULL, self), rb_intern("to_i"), 0);
}

/* GMP::Integer#<=> */
VALUE integer_m_spaceship(VALUE self, VALUE x)
{
	CHECK_MPZ(x);

	UNWRAP(self, data);
	UNWRAP(x, other);

	/* 同値なオブジェクトのための早道 */
	if (data == other)
		return INT2FIX(0);

	return INT2FIX(mpz_cmp(*data, *other));
}

/* GMP::Integer#== */
VALUE integer_m_eq(VALUE self, VALUE x)
{
	/* GMP::Integerについては<=>を使用 */
	if (CLASS_OF(x) == cInteger)
		return integer_m_spaceship(self, x) == INT2FIX(0) ? Qtrue : Qfalse;

	return rb_call_super(1, &x);
}

/* GMP::Integer#+ */
VALUE integer_m_add(VALUE self, VALUE x)
{
	CHECK_MPZ(x);

	UNWRAP(self, data);
	UNWRAP(x, other);

	/*
	   結果を補完するために新しいGMP::Integerが必要ですが、
	   実際に`new`メソッドを使う必要は全くありません。
	 */
	VALUE result = integer_c_alloc(cInteger);
	UNWRAP(result, res);

	mpz_add(*res, *data, *other);

	return result;
}
/* 乗算と減算はほぼ同様に定義されるでしょう */

/* GMP::Integer#-@ */
VALUE integer_m_neg(VALUE self)
{
	UNWRAP(self, data);

	/* +メソッド中なので`new`を迂回 */
	VALUE result = integer_c_alloc(cInteger);
	UNWRAP(result, res);

	mpz_neg(*res, *data);

	return result;
}

/* 入口 */
void Init_gmp()
{
	mGMP = rb_define_module("GMP");

	/* GMP::Integerを定義 */
	cInteger = rb_define_class_under(mGMP, "Integer", rb_cObject);
	rb_define_alloc_func(cInteger, integer_c_alloc);
	rb_define_method(cInteger, "initialize", integer_m_initialize, -1);
	rb_define_method(cInteger, "to_s", integer_m_to_s, -1);
	rb_define_method(cInteger, "to_i", integer_m_to_i, 0);
	rb_define_method(cInteger, "<=>", integer_m_spaceship, 1);
	rb_define_method(cInteger, "==", integer_m_eq, 1);
	rb_define_method(cInteger, "+", integer_m_add, 1);
	rb_define_method(cInteger, "-@", integer_m_neg, 0);

	rb_define_alias(cInteger, "inspect", "to_s");
}

extconf.rbは本当に単純です。

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

raise "Can't find GMP lib"       unless have_library 'gmp'
raise "Can't find GMP header"    unless have_header  'gmp.h'
raise "Can't find string header" unless have_header  'string.h'

create_makefile 'gmp'

そしてこれで遂に基数62で自分の名前が何になるのかがわかります。

require './ext/gmp'

puts GMP::Integer.new('Maxwell', 62)
# 1283471748369

この例ではCにおける全てを試そうとしましたが、実際は必要ではないです(し望ましくもないです)。Cのメソッドの中に(例にあるto_i==のような)単に有象無象のAPI関数を呼ぶだけのものがあるなら、恐らくRubyでメソッドを実装することに比較して数CPUサイクル分しか稼いでいないでしょう。そしてもちろんCを書く時間が増えてRubyを書く時間が減ってしまうツケを払うことになります。😀

拡張を書くときの共通する慣習はCでは拡張の「肝」だけを実装し、残りを通常のRubyスクリプトでやってコンパイルされたライブラリに取り込むことです。 例えばgmp.rbスクリプトを書いて拡張を著しく単純にできます。

require './ext/gmp'

class GMP::Integer
  def to_i
    to_s.to_i
  end

  def == other
    return (self <=> other) == 0 if other.is_a? self.class
    super
  end

  alias :inspect :to_s
end