Examples
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