3

I'm writing a C extension in Ruby, and I'm running into a bit of a problem. I have a couple of C structs, here's a simplified version:

typedef struct
{
  int score;
} Player;

typedef struct
{
  int numPlayers;
  Player *players;
} Game;

And I have these very nicely wrapped up in a C extension. I've set up methods for each struct, and here's the player ones since that's what I'm interested in:

static VALUE players(VALUE self)
{
  Game *g;
  Data_Get_Struct(self, Game, g);
  VALUE players = rb_ary_new();
  for (int i = 0; i<g->numPlayers; i++)
  {
    //cPlayer is the ruby class I defined in my Init method
    VALUE player = Data_Wrap_Struct(cPlayer, NULL, NULL, &g->players[i]);
    rb_ary_push(players, player);
  }
  return players;
}

static VALUE set_players(VALUE self, VALUE players)
{
  Game *g;
  Data_Get_Struct(self, Game, g);
  free(g->players);
  g->players = malloc(sizeof(Player)*RARRAY_LEN(players));
  for (int i = 0; i<RARRAY_LEN(players); i++)
  {
    VALUE iValue = INT2NUM(i);
    VALUE player = rb_ary_aref(1, &iValue, players);
    Player *cPlayer;
    Data_Struct_Get(player, Player, cPlayer);
    memcpy(&g->players[i], cPlayer, sizeof(Player));
  }
  return Qnil;
}

And then, in my Ruby code, I could do something like this:

g = Game.new
g.players = [Player.new, Player.new, Player.new]
g.players.each {|player| player.score = 5}

This works fine. However, I don't know how to do this:

g.players << Player.new
 => [<Player0>, <Player1>, <Player2>, <Player3>]
g.players
 => [<Player0>, <Player1>, <Player2>]
g.players[0] = Player.new
 => [<Player4>, <Player1>, <Player2>]
g.players
 => [<Player0>, <Player1>, <Player2>]

Obviously, the problem is that when I access the array, it computes a new array each time. So, when I add a player or append one, that array changes, but the underlying player array stays the same. I feel like I have to have two arrays, one in Ruby, one in C. Whenever I add some players to the C array, say in an initialize_game function, I would have to update the Ruby array, and whenever the Ruby array gets updated, there would have to be some sort of callback to update the C array. But, I'm not sure exactly how to do that.

2
  • I actually just had a flash of inspiration and realized that this design pattern is probably the real issue, and that instead of wrapping the C structs, I should be creating pure Ruby objects. Then, in my extension, the method I need a performance boost should create new structs from the current Ruby array, then run the speedier C function using those newly created structs as input. Seems much easier and more maintainable than what I've been doing. Anyone else agree that this is the way to go?
    – Devin
    Commented Apr 23, 2015 at 18:05
  • That depends on how expensive it is to derive the structs you need on-demand and write the results back to the Ruby objects. You should either be "native Ruby, convert to/from in C" or "native C, provide Ruby access". The call g.players[0] = Player.new is perhaps not a great approach anyway, because your caller is interfering too deep into the Game object to do that - it needs to work with knowledge that Game should be encapsulating Commented Apr 23, 2015 at 21:59

1 Answer 1

0

You can store a Ruby VALUE reference in your struct:

typedef struct
{
  int numPlayers;
  VALUE players;
} Game;

You will need to initialise it separately (I usually implement an init function and call it after the alloc routine).

In addition you must mark the value using rb_gc_mark when asked - you need to fill in those NULL references in Data_Wrap_Struct to play nice with Ruby's GC.

You really should already be providing a destroy function, else you will have memory leaks:

E.g.

Data_Wrap_Struct( cGame, game_gc_mark, game_destroy, new_game );

game_gc_mark should look like this:

void game_gc_mark( Game *g ) {
  rb_gc_mark( g->players );
  return;
}

game_destroy should look like this:

void game_destroy( Game *g ) {
  xfree( g );
  return;
}

Note there is no need to handle memory management for the Array or the Player objects inside the array if you do this - Ruby's GC will handle that for you, provided you supply similar methods for destroying Player objects.

The advantage of doing this is all objects are accessible just like normal Ruby, whilst behind the scenes the data is stored in the structs ready to use efficiently for any method you want to implement in C. No converting to/fro, no spawning new Ruby objects to follow relations, just speed when you want it and have time to write the C.

The disadvantage is you will need to code defensively against manipulations of the players array by Ruby code, otherwise you risk segfaults if you assume you have Player objects, but in fact a bug has resulted in them being some other class.

3
  • I'm worried about the speed of a ruby array though. The eventual application is a fairly intensive Monte Carlo sim, which I first wrote in Ruby (which, yeah, not Ruby's strength), then rewrote in C for an immediate two order of magnitude speedup. I just wrote a quick test to loop through an int array and a Ruby array filled with 30 integers, which is something it does a lot, and it showed the same two order of magnitude difference. I just need the Ruby part to hook up with my Rails app to display on a webpage.
    – Devin
    Commented Apr 23, 2015 at 23:47
  • In that case why are you concerned about behaviour of g.players[0] = Player.new at all? So what if g.players is not directly mutable? Commented Apr 24, 2015 at 5:56
  • Also, you don't need to have every array in your C accessible like this to Ruby . . . just the main relational ones that you want to manipulate in Ruby. The arrays you need to iterate quickly can remain native C arrays of ints etc. If you really need fast in C and mutable in Ruby for numeric arrays, look into NArray gem. My gem convolver uses NArray, and it's a tiny single-purpose gem, so you could take a look how that works (note it returns new Narrays, but you can store them as VALUEs as per my answer if you need them in the structs). Commented Apr 24, 2015 at 6:10

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.