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", a开发者_如何学编程ge);
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.
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;
}
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
:
- 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 passesnil
to one of the optional arguments, which does happen. - It uses
rb_error_arity
to raise an ArgumentError in the standard format (ex.wrong number of arguments (2 for 1)
). - 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
精彩评论