Part 2 - Adding Features

The next major thing we want to do is start adding some functionality to our SharedMemory class.

Opening POSIX shared memory is very similar to opening a file. So similar, in fact, that maybe it’s wise to try and make the object act like a File, or IO object.

Let’s modify what we have to accept a filename. Now, I want to be able to call our SharedMemory initializer with a filename as an argument:

#include <sys/mman.h>
#include <sys/fcntl.h>

static VALUE rb_shm_new(VALUE self,
  VALUE filename, VALUE arg)
{
  struct shared_memory_struct *sms;

  VALUE obj = Data_Make_Struct(self,
    struct shared_memory_struct, NULL,
    free, sms);

  int val = NUM2INT(arg);
  sms->size = val;

  int handle = shm_open(StringValuePtr(filename),
  O_RDWR | O_CREAT | O_EXCL, 644);
  if(handle == -1) {
    rb_sys_fail("shm_open(2)");
  }	

  return obj;
}

So, with we have an extra argument, filename that specifies a filename that we want to open.

We use the built in convenience function StringValuePtr to convert from a Ruby String to a C style string. Then we call shm_open and try and create the new file in a very similar manner to using the open(2) system call.

The function returns the handle to the shared memory file. If it returns a -1, this indicates an error and we use the rb_sys_fail method to handle the error.

We also need to modify our function definition to tell it we now accept two argument:

  rb_define_singleton_method(shm_class, "new",
    rb_shm_new, 2);

Let’s see how it works:

irb> require 'posixipc'
=> true
irb(main):002:0> a = PosixIPC::SharedMemory.new('blah', 10)
irb: symbol lookup error: ./posixipc.so: undefined symbol: shm_open

That didn’t work. It turns out, at least on Linux, that the shm_open function is within the librt library (real time library). Our application doesn’t link against it, so we get an unresolved symbol error. However, on OSX this works because this symbol is defined in the C library, so it works.

We can use extconf.rb to help us out:

require 'mkmf'

if ( have_library('c', 'shm_open') or
     have_library('rt', 'shm_open') )

  create_makefile ('posixipc')
end

What this does it checks if the shm_open symbol is located within libc, and if not, checks if it’s located with librt. When it finds the symbol, it adds that library to the library to compile against. Very nice. Now, after running ruby extconf.rb:

checking for shm_open() in -lc... no
checking for shm_open() in -lrt... yes
creating Makefile

And then running make to rebuild the Makefile, we can try this again:

irb> require 'posixipc'
=> true
irb> a = PosixIPC::SharedMemory.new("blah", 10)
=> #<PosixIPC::SharedMemory:0x227434>

irb> a = PosixIPC::SharedMemory.new("blah", 10)
Errno::EEXIST: File exists - shm_open(2)

Well, it looks like it works. It also looks like we can’t open a second instance of the same shared memory segment of the same name - something to expect because our open method specified the O_EXCL flag which causes an error of the shared memory segment already exists.

Maybe a good idea would be to make the SharedMemory.new method work very similiarly to the File.new method. According to what I’ve got, the possible File.new syntax is:

  • File.new(filename, mode=”r”) => file
  • File.new(filename [, mode [, perm]]) => file

This means we’ll need to accept 1, 2, or 3 arguments to our new method. That’s okay, Ruby makes it easy. In order to support a variable number of arguments to a Ruby method, we just define it like this:

  rb_define_singleton_method(shm_class, "new",
    rb_shm_new, -1);

This requires us to redesign our implementation of this method, as well:

static VALUE rb_shm_new(int argc,
  VALUE *argv, VALUE self)
{

In this case, what we now have is a method that can receive a variable number of arguments, C style like. The number of arguments passed to the method is stored in argc, and a C array of Ruby VALUEs of each of these objects is stored in argv. Then we can use the handy function rb_scan_args to parse out the arguments.

  VALUE fname, vmode, perm;

  rb_scan_args(argc, argv, "12", &fname,
    &vmode, &perm);

What this does is tells Ruby to scan argc and argv for 1 required argument and up to 2 optional arguments, and to put them into fname, vmode, and perm. Any more or any less arguments causes rb_scan_args to throw an ArgumentError.

Now, we can parse these arguments for our C style information:

  char *mode;
  int flags, fmode;

  if (FIXNUM_P(vmode)) {
    flags = FIX2INT(vmode);
  }
  else {
    mode = NIL_P(vmode) ?
      "r" : RSTRING(vmode)->ptr;

    flags = rb_io_mode_modenum(mode);
  }

  fmode = NIL_P(perm) ? 0666 : NUM2INT(perm);

Let’s go through this code. It says that if the vmode argument is a Fixnum, convert it to a C integer flags. Otherwise, if it’s nil, default to “r” mode, or whatever string mode was passed. Call the rb_io_mode_modenum function, (which is a function we’ll have to define shortly), which converts a string filemode into a numeric one. Finally, if no permissions were passed, use a default of 0666, otherwise use what was passed.

Now we have the C values of all of the arguments that have been passed. We can call shm_open just like before:

  int handle = shm_open(StringValuePtr(fname),
  flags, fmode);

Now we’ve got something that’s coming together. We’ve lost our size information, but that’s okay - we’ll figure out a way to play with it later.

At this point, I’m trying to think of the next step. I want to attempt to encapsulate some of the stuff from the IO Ruby class from within this object. I looked at the IO object, and noticed that IO.new took a file descriptor and a mode. I think this is what I want, because shm_open returns a file descriptor for use with normal system IO and file functions. But I’m also thinking File might be more than I’m wanting to deal with right now, so I think IO is what I want.

What I really want to do is, after initializing my information, somehow pass this information on to IO.new. That’s easy enough to do.

Let’s go back and redefine our new class as a subclass of IO instead of Object:

  VALUE shm_class =
    rb_define_class_under(posix_mod,
    "SharedMemory", rb_cIO);

Now, at the end of our rb_shm_new function, we need to pass this information on to IO.new. Since IO is our superclass, we can just call rb_call_super and have that happen for us. We need to set up the data we’re going to pass to make the function happy:

  VALUE derargs[2];
  derargs[0] = INT2NUM(handle);
  derargs[1] = INT2NUM(flags);

  return rb_call_super(2, derargs);

We simply make an array of the arguments to pass, one of which is the handle and the other is the flags defined earlier.

I think this is a good time to recap all of our code thus far. The following snippet also contains the rb_io_mode_modenum function that I mostly stole directly from the Ruby source code and modified a little bit for our needs.

#include "ruby.h"
#include <sys/mman.h>
#include <sys/fcntl.h>

struct shared_memory_struct {
  int size;
};

static int
rb_io_mode_modenum(const char *mode)
{
  int flags = 0;
  const char *m = mode;

  switch (*m++) {
    case 'r':
      flags |= O_RDONLY;
      break;
    case 'w':
      flags |= O_RDWR;
      break;
    default:
    error:
      rb_raise(rb_eArgError,
         "illegal access mode %s", mode);
  }

  while (*m) {
    switch (*m++) {
      case '+':
        flags =
          (flags & ~O_ACCMODE) | O_CREAT;
        break;
      default:
        goto error;
    }
  }

  return flags;
}

static VALUE rb_shm_new(int argc,
  VALUE *argv, VALUE self)
{
  struct shared_memory_struct *sms;

  VALUE fname, vmode, perm;
  char *mode;
  int flags, fmode;

  rb_scan_args(argc, argv, "12",
    &fname, &vmode, &perm);

  SafeStringValue(fname);

  if (FIXNUM_P(vmode)) {
    flags = FIX2INT(vmode);
  }
  else {
	 mode = NIL_P(vmode) ?
	   "r" : RSTRING(vmode)->ptr;

    flags = rb_io_mode_modenum(mode);
  }

  fmode = NIL_P(perm) ? 0666 : NUM2INT(perm);

  VALUE obj = Data_Make_Struct(self,
    struct shared_memory_struct, NULL,
    free, sms);

  //int val = NUM2INT(10);
  sms->size = 10;

  int handle = shm_open(StringValuePtr(fname),
  flags, fmode);

  if(handle == -1) {
    rb_sys_fail("shm_open(2)");
  }	

  VALUE derargs[2];
  derargs[0] = INT2NUM(handle);
  derargs[1] = INT2NUM(flags);

  return rb_call_super(2, derargs);
}

static VALUE rb_shm_size(VALUE self)
{
  struct shared_memory_struct *sms;

  Data_Get_Struct(self,
    struct shared_memory_struct, sms);

  return INT2NUM(sms->size);
}

void Init_posixipc() {
  VALUE posix_mod =
    rb_define_module("PosixIPC");

  VALUE shm_class =
    rb_define_class_under(posix_mod,
    "SharedMemory", rb_cIO);

  rb_define_singleton_method(shm_class, "new",
    rb_shm_new, -1);

  rb_define_method(shm_class, "size",
    rb_shm_size, 0);
}

At this point, we can fire up irb and verify that things still work like we expect (after rebuilding with make, of course). I’ll leave that as an exercise for the reader, but suffice to say, it all works here.

One extra check that we probably should add is to verify that the header files that we used towards the beginning of this project (sys/mman.h and sys/fcntl.h) exist on our particular platform. We can do this in the extconf.rb file:

require 'mkmf'

if ( ( have_library('c', 'shm_open') or
     have_library('rt', 'shm_open') ) and
     have_header('sys/mman.h') and
     have_header('sys/fcntl.h') )

  create_makefile ('posixipc')
end

Now when we run ruby extconf.rb:

user@host $ ruby extconf.rb
checking for sys/mman.h... yes
checking for sys/fcntl.h... yes
creating Makefile

Which works for us!