Part 1 - Getting Started
The first thing we need to do is create some actual meat code to work with. Let’s go with a barebones C extension (named myextension.c):
#include "ruby.h"
void Init_myextension() {
rb_define_class("MyClass", rb_cObject);
}
There are a few items of note here:
- We must define an Init__xxxx() function as an entry point for our module to load. The xxxx in this case refers to the name of our module.
- We need to include the ruby.h header
- Our module defines a new class, MyClass, and notes that it is a subclass of the built in ruby Object. The rb_cObject is just the C version of Ruby’s Object class.
Next, we need a way to build our code into a loadable module. This means we need a Makefile, and we need to tell it where ruby.h is located, how to link against the libruby.so shared library, amongst other things.
Luckily, Ruby provides an easy way to do this. We create a new Ruby file called extconf.rb with the following data:
require 'mkmf'
create_makefile("myextension")
Running ruby extconf.rb uses Ruby’s mkmf package to create the Makefile for us. As a result, we now have a Makefile, and can run make:
caleb@host ~/rubyext $ ruby extconf.rb creating Makefile caleb@host ~/rubyext $ make i686-pc-linux-gnu-gcc ... i686-pc-linux-gnu-gcc ... caleb@host ~/rubyext $
Now we have a myextension.so shared library which Ruby can load and use. Here’s an irb session highlighting it:
caleb@host ~/rubyext $ irb
irb(main):001:0> MyClass.nil?
NameError: uninitialized constant MyClass
irb(main):002:0> require 'myextension'
=> true
irb(main):003:0> MyClass.nil?
=> false
irb(main):004:0> MyClass.class
=> Class
irb(main):005:0> MyClass.superclass
=> Object
irb(main):005:0> quit
As seen, by loading our extension we’ve created a new class called MyClass, which is a subclass of the built in Ruby Object.
This is the basis for our extension work. Since the extension I’m going to be building is a module and classes based on Posix Interprocess Communication, I’m going to call my module posixipc from here on out. I’m going to make a slight modification to my file in order to support the structure I’m intending for:
#include "ruby.h"
void Init_posixipc() {
VALUE posix_mod =
rb_define_module("PosixIPC");
rb_define_class_under(posix_mod,
"SharedMemory", rb_cObject);
}
With this code, I’ve created a new Ruby module called PosixIPC and created a class underneath that module called SharedMemory. The SharedMemory class is a descendant of the Ruby Object class, just like before.
The new thing to notice here is the use of the VALUE type. In the Ruby extension world, VALUE is king. It’s used to encapsulate Ruby types, objects, and class information when passing the information around to various functions. We’ll see it used extensively.
At this point, you can run make again to build the extension. Don’t forget that since the name has changed to posixipc, to update the extconf.rb file to reflect our new module name.
After the successful build, you should be able to fire up irb again and verify that the posixipc module loads, and that you can do something with SharedMemory.
irb(main):001:0> require 'posixipc'
=> true
irb(main):002:0> PosixIPC
=> PosixIPC
irb(main):003:0> PosixIPC.class
=> Module
irb(main):004:0> PosixIPC::SharedMemory
=> PosixIPC::SharedMemory
irb(main):005:0> a = PosixIPC::SharedMemory.new
=> #<PosixIPC::SharedMemory:0xb7c38eb0>
Very good. All is well.
Our first major task is to create our own initializer for the SharedMemory object. Without it, all we really have is just another Ruby Object. To do this, we need to tell Ruby about this function, and create it as well. We accomplish that with this code:
#include "ruby.h"
static VALUE rb_shm_new(VALUE self)
{
return Qnil;
}
void Init_posixipc() {
VALUE posix_mod =
rb_define_module("PosixIPC");
VALUE shm_class =
rb_define_class_under(posix_mod,
"SharedMemory", rb_cObject);
rb_define_singleton_method(shm_class, "new",
rb_shm_new, 0);
}
So what have we done? We still have the same code as before within our Init_ function, but we’ve added a new call that tells Ruby we want to define a function that handles the call to Posix::SharedMemory.new. This function is called rb_shm_new, and it takes 0 arguments.
Since we did this, we also need to define this function. A few things to note about this:
- Our initializer function returns a VALUE. In fact, every C function that is called by Ruby must return a VALUE. In our case, we return a Qnil, which is equivalent to nil within Ruby.
- Our function takes a VALUE argument, representing the calling object. Even though we defined the function as taking 0 arguments, Ruby always passes the calling object as an argument to the function. More on this later.
Let’s see if it works (don’t forget to re-run make):
irb(main):001:0> require 'posixipc'
=> true
irb(main):002:0> PosixIPC::SharedMemory.new
=> nil
Returning nil when creating the new object doesn’t do us much good, so let’s just return the VALUE that’s passed t o the function - it’s the object we’re interested in anyway.
At this point, we need to start defining how our shared memory class is going to look. The first thing that comes to my mind is that it will have some defined size. Let’s get rid of this new method stuff for now, and instead define a method that returns the size of the shared memory.
#include "ruby.h"
static VALUE rb_shm_size(VALUE self)
{
return INT2NUM(0);
}
void Init_posixipc() {
VALUE posix_mod =
rb_define_module("PosixIPC");
VALUE shm_class =
rb_define_class_under(posix_mod,
"SharedMemory", rb_cObject);
rb_define_method(shm_class, "size",
rb_shm_size, 0);
}
That works great. We’ve created a size method for our class, and in this case it always return a 0. The INT2NUM macro is a convenient way of turning a C integer into a Ruby Numeric VALUE.
However, we can’t just always return a 0. We need a place to actually hold the size. Thus, we’ll need to create a C variable to hold it it:
#include "ruby.h"
static int size = 10;
static VALUE rb_shm_size(VALUE self)
{
return INT2NUM(size);
}
void Init_posixipc() {
VALUE posix_mod =
rb_define_module("PosixIPC");
VALUE shm_class =
rb_define_class_under(posix_mod,
"SharedMemory", rb_cObject);
rb_define_method(shm_class, "size",
rb_shm_size, 0);
}
irb(main):001:0> require 'posixipc'
=> true
irb(main):002:0> a = PosixIPC::SharedMemory.new
=> #<PosixIPC::SharedMemory:0xb7cc601c&bt;
irb(main):003:0> a.size
=> 10
Now all we need is a way to specify the size. The most convenient way is to do it when the object is created, ie, in it’s new function. So, we’re back to needing it again.
The first thing we need to do is create a structure that we’re going to hold the data in for our SharedMemory class. Right now, all we have is a size, so let’s just stick with that:
struct shared_memory_struct {
int size;
};
Now, we need to somehow envelop this information when our object is created. We need a smarter new function. Let’s try out this:
static VALUE rb_shm_new(VALUE self)
{
struct shared_memory_struct *sms;
VALUE obj = Data_Make_Struct(self,
struct shared_memory_struct, NULL,
free, sms);
return obj;
}
So, what’s going on here? What we’re doing here is making a Ruby object out of a C object. Let’s look at the arguments of this Data_Make_Struct call.
- The Ruby object we’re attempting to create. We just pass in the self parameter, since it’s a SharedMemory object and that’s what we’re aiming for.
- The C structure that we’re working with. Note that we want the actual data type and not an instance of the structure.
- A special mark function that we use for garbage collection. We’ll explore this later, but for our simple object it’s not needed so we can pass NULL.
- The function we use to free the resources acquired by this object. Since we’re not going anything fancy, we’ll just use the builtin C free function.
- A pointer to store the object data in.
When it’s done, Data_Make_Struct returns a Ruby instance of the object we’ve just created.
What this means is that we can now use this structure to store information about this object. For example, we can replace our size getting method with this:
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);
}
This function uses Ruby’s Data_Get_Struct to pull the structure information back out of the Ruby object. In this case, we grab sms->size, which is a C value, and convert it to a Ruby Numeric before returning it.
So our last step now is that we need to be able to define the size of the object at creation time. This means we want to be able to say PosixIPC::SharedMemory.new(1000) and use that as the size of our object.
First, we need to redefine our new method to take an argument:
rb_define_singleton_method(shm_class, "new",
rb_shm_new, 1);
The 1 here means that this function now accepts one argument.
We also need to redefine our implementation of this method:
static VALUE rb_shm_new(VALUE self, 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;
return obj;
}
We added an argument argto the function, which is our passed argument to new. And we use this to set the size value of the structure we are creating. Note that we have to change arg, which is a Ruby VALUE to a C integer before using it.
irb(main):001:0> require 'posixipc'
=> true
irb(main):002:0> PosixIPC::SharedMemory.new
ArgumentError: wrong number of arguments (0 for 1)
irb(main):003:0> a =
PosixIPC::SharedMemory.new(1000)
=> #<PosixIPC::SharedMemory:0xb7cd3a00>
irb(main):004:0> a.size
=> 1000
Hey look, we’ve done it!
For posterity, let’s look at the whole thing put together:
#include "ruby.h"
struct shared_memory_struct {
int size;
};
static VALUE rb_shm_new(VALUE self, 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;
return obj;
}
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_cObject);
rb_define_singleton_method(shm_class, "new",
rb_shm_new, 1);
rb_define_method(shm_class, "size",
rb_shm_size, 0);
}
I think that about wraps up everything we need to accomplish for this part.
When you’re ready, continue on over at part 2