Running Ruby in C

This example is a simple little game of tag. In the game there are two squares: the blue square is controlled using the arrow keys on the keyboard while the red square is controlled by a Ruby script. To make this work, we’ll use the C API to define a little Ruby API that the script can access, and every couple of frames we’ll call a method defined in the Ruby script and pass objects encapsulating the data for the two squares.

The Ruby script can look something like this:

def think ai, player
  # get my position
  ax, ay = ai.pos

  # get direction that player moved
  dx, dy = player.dir

  # ... movement logic ...
  x = dy
  y = -dx

  # move in this direction
  ai.move x, y
end

The C code uses SDL2 for graphics and input and uses stat() (which may not be very portable) to hot-reload the AI script whenever the file is changed . Here’s 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>

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

/* for position and direction */
struct vec2
{
	float x;
	float y;
};

/* for the player and their opponent */
struct actor
{
	struct vec2 pos;
	struct vec2 dir;
	float speed; // top speed in pixels/millisecond
	SDL_Color color;
};

/* wrapper for passing AI actor with AI script metadata */
struct ai_actor
{
	char* script;
	bool loaded;
	bool error;
	time_t load_time;
	struct actor* actor;
};

/* set AI error state and possibly print exception */
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;

	/* print exception */
	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));
}

/* clear AI error state */
void ai_reset(struct ai_actor* ai)
{
	ai->error = false;
	ai->actor->color.a = 255;
}

/* try to (re)load AI script */
void ai_load(struct ai_actor* ai)
{
	/* get script modification time */
	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;
	}

	/* nothing to do if we've already loaded the script and it hasn't been updated */
	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);
}

/* for rescuing exceptions in the AI script */
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;
}

/* run the AI script if possible */
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);
}

/* move actor after ms time has elapsed */
void step_actor(struct actor* act, unsigned int ms)
{
	float norm = sqrtf(act->dir.x * act->dir.x + act->dir.y * act->dir.y);

	/* no movement */
	if (norm == 0.f) return;

	/* allow actor to move slower than speed, but not faster */
	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;

	/* clamp position to screen */
	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;
}

/* draw an actor as a colored box */
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);
}

/* methods for the API we're defining for the AI script */
/* time - returns total elapsed time in milliseconds */
VALUE m_time(VALUE self)
{
	return UINT2NUM(SDL_GetTicks());
}

/* we don't need any mark/free/GC/etc. see later comment when we define the class */
static const rb_data_type_t actor_type = { .wrap_struct_name = "actor" };

/* Actor#pos - returns screen position x, y in pixels */
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 - returns last movement direction x, y. each is in the range (-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 - set next movement direction. x, y as in 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)
{
	/* start Ruby TODO is this redundant? */
	if (ruby_setup())
	{
		fprintf(stderr, "Failed to init Ruby VM\n");
		return 1;
	}
	/* set a nicer script name than <main> */
	ruby_script("ruby");

	/* define our own little API for use in the AI script */
	rb_define_global_function("time", m_time, 0);

	/* Actor will wrap struct actor for passing to Ruby */
	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);

	/*
	 * Notice that even though Actor wraps C data, we didn't define an
	 * allocation or free function. That's because we're going to create all
	 * the actors in C and expose them to Ruby. However we should make sure
	 * that Ruby can't create new Actors, because they'll contain invalid data
	 * pointers
	 */
	rb_undef_method(rb_singleton_class(cActor), "new");

	/* start SDL */
	SDL_Init(SDL_INIT_VIDEO);

	/* create window */
	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;
	}

	/* create renderer */
	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);

	/* create actors */
	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
	};

	/* create Ruby objects for actors */
	/* we can use NULL for the free function because the data are on the stack */
	VALUE player_v = TypedData_Wrap_Struct(cActor, &actor_type, &player);
	VALUE ai_v = TypedData_Wrap_Struct(cActor, &actor_type, &ai_act);

	/* don't allow the player to be moved via the AI script */
	rb_undef_method(rb_singleton_class(player_v), "move");

	/* set up timing */
	unsigned int ai_step = 33; /* run AI at 30fps */
	unsigned int last_time = SDL_GetTicks();
	unsigned int now;
	unsigned int frame_time;
	unsigned int ai_time;

	/* start up AI */
	ai_load(&ai);
	ai_think(&ai, ai_v, player_v);

	/* for player input */
	const Uint8* keyboard = SDL_GetKeyboardState(NULL);

	/* main loop */
	SDL_Event event;
	bool running = true;
	while (running)
	{
		/* update timers */
		now = SDL_GetTicks();
		frame_time = now - last_time;
		ai_time += frame_time;
		last_time = now;

		/* event handling */
		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 movement */
		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 movement */
		if (ai_time >= ai_step)
		{
			ai_load(&ai);
			ai_think(&ai, ai_v, player_v);

			ai_time %= ai_step;
		}

		/* game step */
		step_actor(&ai_act, frame_time);
		step_actor(&player, frame_time);

		/* render */
		SDL_SetRenderDrawColor(renderer, 255, 255, 255, 255);
		SDL_RenderClear(renderer);

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

		SDL_RenderPresent(renderer);

		/* let CPU rest */
		SDL_Delay(1);
	}

	/* clean up */
	SDL_DestroyRenderer(renderer);
	SDL_DestroyWindow(window);

	/* stop SDL */
	SDL_Quit();

	/* stop Ruby */
	return ruby_cleanup(0);
}

The Makefile is nothing special:

# this is just a hack to get the Ruby version in this guide
# you can just hard-code it e.g. 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

Running C in Ruby

This example is a Ruby C extension that wraps the GMP C library for arbitrary precision arithmetic. This is far from a complete example: it only wraps the integer functions, implements only the basic functionality of the library, and doesn’t bother nicely integrating with Ruby’s existing numeric types. If you want a really full example, check out the GMP gem.

Everything is in gmp.c where we define our GMP::Integer class:

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

/*
 * we're going to be unwrapping VALUEs to get the C data A LOT.
 * It's not that hard, but it gets tedious. This lets us go
 * straight from a VALUE to the underlying data
 */
#define UNWRAP(val, data) \
	mpz_t* data;\
	TypedData_Get_Struct(val, mpz_t, &mpz_type, data);

/*
 * we're also going to be pretty strict about accepting only
 * objects of our GMP::Integer type, so this will be a frequent test
 */
#define CHECK_MPZ(val) \
	if (CLASS_OF(val) != cInteger)\
		rb_raise(rb_eTypeError, "%+"PRIsVALUE" is not a %"PRIsVALUE, val, cInteger);

/* it's nice to have these as globals for easy access in methods */
VALUE mGMP;
VALUE cInteger;

/* function to free data wrapped in GMP::Integer */
void integer_free(void* data)
{
	/* free memory allocated by 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,
		/* probably should set .dsize but I don't know how to write it for mpz_t... */
	},
	.flags = RUBY_TYPED_FREE_IMMEDIATELY,
};

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

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

/* GMP::Integer#initialize
 *
 * Sets internal mpz_t using first argument
 *
 * If the first argument is a String, you can supply a second Fixnum argument
 * as the base for interpreting the String. The default base of 0 means that
 * the base will be determined by the String's prefix.
 */
VALUE integer_m_initialize(int argc, VALUE* argv, VALUE self)
{
	int base = 0;

	/* check for optional base argument */
	VALUE val;
	VALUE rbase;
	if (rb_scan_args(argc, argv, "11", &val, &rbase) == 2)
	{
		/* base only makes sense with a string */
		Check_Type(val, T_STRING);
		Check_Type(rbase, T_FIXNUM);

		base = FIX2INT(rbase);

		/* GMP only accepts certain bases */
		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:
			/* easy case */
			mpz_set_si(*data, FIX2LONG(val));
			return self;
		case T_BIGNUM:
			/* this is the easiest way to safely convert */
			str = rb_funcall(val, rb_intern("to_s"), 0);
			base = 10;
			break;
		case T_STRING:
			str = val;
			break;
		case T_DATA:
			/* copy another GMP::Integer */
			if (CLASS_OF(val) == cInteger)
			{
				UNWRAP(val, other);

				mpz_set(*data, *other);

				return self;
			}
			/* break intentionally omitted */
		default:
			rb_raise(rb_eTypeError, "%+"PRIsVALUE" is not an integer type", val);
			break;
	}

	/* assign */
	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
 *
 * Accepts an optional Fixnum argument for the base of the String (default 10)
 */
VALUE integer_m_to_s(int argc, VALUE* argv, VALUE self)
{
	int base = 10;

	/* check for optional base argument */
	VALUE rbase;
	if (rb_scan_args(argc, argv, "01", &rbase) == 1)
	{
		Check_Type(rbase, T_FIXNUM);

		base = FIX2INT(rbase);

		/* GMP only accepts certain bases */
		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);

	/* get C string from GMP */
	char* cstr = malloc(mpz_sizeinbase(*data, base) + 2);
	mpz_get_str(cstr, base, *data);

	/* create Ruby String */
	VALUE str = rb_str_new_cstr(cstr);

	/* free memory */
	free(cstr);

	return str;
}

/* GMP::Integer#to_i */
VALUE integer_m_to_i(VALUE self)
{
	/* safest and easiest way to convert is to call 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);

	/* shortcut for identical objects */
	if (data == other)
		return INT2FIX(0);

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

/* GMP::Integer#== */
VALUE integer_m_eq(VALUE self, VALUE x)
{
	/* for GMP::Integers, use <=> */
	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);

	/*
	 * we need a new GMP::Integer to store the result, but there's no need
	 * to actually use the `new` method
	 */
	VALUE result = integer_c_alloc(cInteger);
	UNWRAP(result, res);

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

	return result;
}
/* multiplication and subtraction would be defined nearly identically */

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

	/* bypassing `new` as in the + method */
	VALUE result = integer_c_alloc(cInteger);
	UNWRAP(result, res);

	mpz_neg(*res, *data);

	return result;
}

/* entry point */
void Init_gmp()
{
	mGMP = rb_define_module("GMP");

	/* define 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");
}

The extconf.rb is really simple.

#!/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'

And now you can finally find out what your name means in base 62:

require './ext/gmp'

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

For this example I tried to do everything in C, but practically that isn’t necessary (or desirable). If one of your C methods just calls a bunch of API functions (like to_i and == in the example), you’re probably only saving yourself a couple CPU cycles compared to implementing the method in Ruby. And of course that comes at the cost of needing to spend more time writing C and less time writing Ruby. 😀

A common convention when writing extensions is to only implement the “meat” of the extension in C and to do everything else in a regular Ruby script that pulls in the compiled library. For example, we could have written a gmp.rb script to significantly simplify our extension:

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

Comments