Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Extending ruby in C - how to specify default argument values to function?

I'm trying to write a C extension to ruby that'll generate a class. I'm looking on how to define some default arguments to a class. For example, if I have this class decleration in ruby:

class MyClass
  def initialize(name, age=10)
    @name = name
    @age  = age
  end
end

You can initialize it with mc = MyClass.new("blah"), and the age parameter will be set internally. How do I do this in C? So far I got this, but this forces entering the other argument:

require "ruby.h"

static VALUE my_init(VALUE self, VALUE name, VALUE age)
{
    rb_iv_set(self, "@name", name);
    rb_iv_set(self, "@age", age);

    return self;
}

VALUE cMyClass;

void Init_MyClass() 
{
    // create a ruby class instance
    cMyClass = rb_define_class("MyClass", rb_cObject);

    // connect the instance methods to the object
    rb_define_method(cMyClass, "initialize", my_init, 2);
}

I thought about checking the value of age against Qnil or using if ( TYPE(age) == T_UNDEF ), but I just get segfaults from there. Reading through README.EXT leads me to believe I can accomplish this through rb_define_method using the value of argc, but this wasn't too clear. Any ideas? Thanks.

like image 424
sa125 Avatar asked Oct 02 '11 13:10

sa125


2 Answers

You do need to use the argc of rb_define_method. You should pass -1 as the argc to rb_define_method and use rb_scan_args to handle optional arguments. For example, matt's example could be simplified to the following:

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

    VALUE name, age;
    rb_scan_args(argc, argv, "11", &name, &age);    // informs ruby that the method takes 1 mandatory and 1 optional argument, 
                                                    // the values of which are stored in name and age.

    if (NIL_P(age))         // if no age was given...
        age = INT2NUM(10);  // use the default value

    rb_iv_set(self, "@age",  age);
    rb_iv_set(self, "@name", name);

    return self;
}

Usage

Derived from the Pragmatic Bookshelf:

int rb_scan_args (int argcount, VALUE *argv, char *fmt, ...

Scans the argument list and assigns to variables similar to scanf:

fmt A string containing zero, one, or two digits followed by some flag characters. 
        The first digit indicates the count of mandatory arguments; the second is the count of optional arguments. 
    A * means to pack the rest of the arguments into a Ruby array. 
    A & means that an attached code block will be taken and assigned to the given variable 
        (if no code block was given, Qnil will be assigned).

After the fmt string, pointers to VALUE are given (as with scanf) to which the arguments are assigned.

Example:

VALUE name, one, two, rest;
rb_scan_args(argc, argv, "12", &name, &one, &two);
rb_scan_args(argc, argv, "1*", &name, &rest);

Furthermore, in Ruby 2, there is also a : flag that is used for named arguments and the options hash. However, I have yet to figure out how it works.

Why?

There are many advantages of using rb_scan_args:

  1. It handles optional arguments by assigning them nil (Qnil in C). This has the side effect of preventing odd behaviour from your extension if someone passes nil to one of the optional arguments, which does happen.
  2. It uses rb_error_arity to raise an ArgumentError in the standard format (ex. wrong number of arguments (2 for 1)).
  3. It's usually shorter.

The advantages of rb_scan_args are further elaborated here: http://www.oreillynet.com/ruby/blog/2007/04/c_extension_authors_use_rb_sca_1.html

like image 37
tophat Avatar answered Oct 10 '22 14:10

tophat


You're right - you can do this using rb_define_method and a negative value for argc.

Normally argc specifies the number of arguments your method accepts, but using a negative value specifies that the method accepts a variable number of arguments, which Ruby will pass in as an array.

There are two possibilities. First, use -1 if you want the arguments passed in to your method in a C array. Your method will have a signature like VALUE func(int argc, VALUE *argv, VALUE obj) where argc is the number of arguments, argv is a pointer to the arguments themselves, and obj is the receiving object, i.e. self. You can then manipulate this array as you need to mimic default arguments or whatever you need, in your case it might look something like this:

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

    VALUE age;

    if (argc > 2 || argc == 0) {  // there should only be 1 or 2 arguments
        rb_raise(rb_eArgError, "wrong number of arguments");
    }

    rb_iv_set(self, "@name", argv[0]);

    if (argc == 2) {        // if age has been included in the call...
        age = argv[1];      // then use the value passed in...
    } else {                // otherwise...
        age = INT2NUM(10);  // use the default value
    }

    rb_iv_set(self, "@age", age);

    return self;
}

The alternative is to have a Ruby array passed into your method, which you specify by using -2 in your call to rb_define_method. In this case, your method should have a signature like VALUE func(VALUE obj, VALUE args), where obj is the receiving object (self), and args is a Ruby array containing the arguments. In your case this might look something like this:

static VALUE my_init(VALUE self, VALUE args) {

    VALUE age;

    long len = RARRAY_LEN(args);

    if (len > 2 || len == 0) {
        rb_raise(rb_eArgError, "wrong number of arguments");
    }

    rb_iv_set(self, "@name", rb_ary_entry(args, 0));

    if (len == 2) {
        age = rb_ary_entry(args, 1);
    } else {
        age = INT2NUM(10);
    }

    rb_iv_set(self, "@age", age);

    return self;
}
like image 152
matt Avatar answered Oct 10 '22 15:10

matt