Extending Ruby with C

Garrett Rooney

CollabNet

Why extend Ruby?

The Big Secret

Places to Go For Info

How Do I Build It?

Hello World extconf.rb

require 'mkmf'

dir_config("hello")

create_makefile("hello")

Let's Try It Out

$ ruby extconf.rb
creating Makefile
$ make
gcc -fPIC -Wall -g -O2  -fPIC  -I. -I/usr/lib/ruby/1.8/i486-linux -I/usr/lib/ruby/1.8/i486-linux -I.   -c hello.c
gcc -shared  -L"/usr/lib" -o hello.so hello.o  -lruby1.8  -lpthread -ldl -lcrypt -lm   -lc
$ ls
extconf.rb  hello.c  hello.o  hello.so*  Makefile
$ irb
irb(main):001:0> require 'hello'
LoadError: Failed to lookup Init function ./hello.so
        from (irb):1:in `require'
        from (irb):1
irb(main):002:0>

What Went Wrong?

The Minimal Extension

#include "ruby.h"

void
Init_hello()
{
  /* nothing here yet */
}

Another Shot

$ ruby extconf.rb
creating Makefile
$ make
gcc -fPIC -Wall -g -O2  -fPIC  -I. -I/usr/lib/ruby/1.8/i486-linux -I/usr/lib/ruby/1.8/i486-linux -I.   -c hello.c
gcc -shared  -L"/usr/lib" -o hello.so hello.o  -lruby1.8  -lpthread -ldl -lcrypt -lm   -lc
$ ls
extconf.rb  hello.c  hello.o  hello.so*  Makefile
$ irb
irb(main):001:0> require 'hello'
=> true
irb(main):002:0>

Digression: Useful mkmf Functions

Creating a Class

An Example With Class

#include "ruby.h"

static VALUE rb_mHello;
static VALUE rb_cHelloWorld;

void
Init_hello()
{
  rb_mHello = rb_define_module("Hello");

  rb_cHelloWorld = rb_define_class_under(rb_mHello,
                                         "World",
                                         rb_cObject);

  /* Hold off on rb_define_alloc_func... */
}

Example: Hello::World.new

$ irb
irb(main):001:0> require 'hello'
=> true
irb(main):002:0> h = Hello::World.new
=> #<Hello::World:0xb7cf2b50>
irb(main):003:0>

Digression: What's a Value?

Shortcuts

Classes That Do Stuff

Digression: Garbage Collection

Using Data_Wrap_Struct 1

static void
file_mark(FILE *f)
{
  /* Nothing needed */
}

static void
file_free(FILE *f)
{
  if (f)
    fclose(f);
}

static VALUE
file_alloc(VALUE klass)
{
  FILE *f = NULL;

  return Data_Wrap_Struct(klass, file_mark, file_free, f);
}

Using Data_Wrap_Struct 2

static VALUE
file_init(VALUE obj, VALUE path)
{
  DATA_PTR(obj) = fopen(RSTRING(path)->ptr, "r");

  return Qnil;
}

void
Init_hello()
{
  rb_mHello = rb_define_module("Hello");

  rb_cMyFile = rb_define_class_under(rb_mHello,
                                     "MyFile",
                                     rb_cObject);

  rb_define_alloc_func(rb_cMyFile, file_alloc);
  rb_define_method(rb_cMyFile, "initialize", file_init, 1);
}

Lets Try It

$ irb
irb(main):001:0> require 'hello'
=> true
irb(main):002:0> h = Hello::MyFile.new("hello.c")
=> #<Hello::MyFile:0xb7cf2b50>
irb(main):003:0>

Writing Methods

The World's Stupidest Method

static VALUE
print_string(VALUE self, VALUE string)
{
  Check_Type(string, T_STRING);

  printf("%s", RSTRING(string)->ptr);

  return Qnil;
}

void
Init_hello()
{
  rb_mHello = rb_define_module("Hello");

  rb_cWorld = rb_define_class_under(rb_mHello,
                                    "World",
                                    rb_cObject);

  rb_define_method(rb_cWorld, "print_string", print_string, 1);
}

Dealing with Strings

Dealing With Arrays

Dealing With Hashes

Calling Ruby Methods

Method Call Example

static VALUE
write_to_file(VALUE self, VALUE file)
{
  ID method = rb_intern("<<");

  if (! rb_respond_to(file, rb_intern("<<")))
    rb_raise(rb_eRuntimeError, "target must respond to '<<'");

  rb_funcall(file, method, 1, self);

  return Qtrue;
}

Exception Handling

Exceptions Example

static VALUE handle_exception(VALUE unused) {
  return Qfalse;
}

static VALUE call_write(VALUE ary) {
  rb_funcall(rb_ary_entry(ary, 0), rb_intern("<<"), 1, rb_ary_entry(ary, 1));
  return Qtrue;
}

static genxStatus writer_send(void *baton, constUtf8 s) {
  VALUE ary, file = (VALUE) baton;

  if (! rb_respond_to(file, rb_intern("<<")))
    rb_raise(rb_eRuntimeError, "target must respond to '<<'");

  ary = rb_ary_new2(2);

  rb_ary_store(ary, 0, file);
  rb_ary_store(ary, 1, rb_str_new2((const char *) s));

  if (rb_rescue(call_write, ary, handle_exception, Qnil) == Qfalse)
    return GENX_IO_ERROR;
  else
    return GENX_SUCCESS;
}

Extensions and Gems

Fun with SWIG

Example: SWIG Interface

%module libc

int fread(void *, size_t, size_t, FILE *);
int fwrite(void *, size_t, size_t, FILE *);

void *malloc(size_t);

Example: Building SWIG Code

$ swig -ruby libc.i
# produces libc_wrap.c
$ ruby extconf.rb
# produces a Makefile
$ make
# produces the extension, libc.so

Example: Using SWIG modules

require 'libc'

f1 = Libc.fopen("source.txt", "r")
f2 = Libc.fopen("dest.txt", "w+")

buffer = Libc.malloc(1024)

nread = Libc.fread(buffer, 1, 1024, f1)

while nread > 0
  Libc.fwrite(buffer, 1, nread, f2)
  nread = Libc.fread(buffer, 1, 1024, f1)
end

# dest.txt now contains the contents of source.txt

Fun with RubyInline

Example: RubyInline

require 'inline'

class Copier
  inline do |builder|
    builder.c <<EOF
      void copy_file(const char *source, const char *dest) {
        FILE *source_f = fopen(source, "r");
        FILE *dest_f = fopen(dest, "w+");
        char buffer[1024];
        int nread = fread(buffer, 1, 1024, source_f);
        while (nread > 0) {
          fwrite(buffer, 1, nread, dest_f);
          nread = fread(buffer, 1, 1024, source_f);
        }
      }
EOF
  end
end

c = Copier.new
c.copy_file("source.txt", "dest.txt")
# the contents of source.txt are copied to dest.txt

Wrap Up