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 . 5 f ,
. 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 . 55 f ,
. 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