Расширение ruby ​​в C - как указать значения аргументов по умолчанию для функции? - PullRequest
16 голосов
/ 02 октября 2011

Я пытаюсь написать расширение C для ruby, которое сгенерирует класс. Я смотрю на то, как определить некоторые аргументы по умолчанию для класса. Например, если у меня есть этот класс decleration в ruby:

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

Вы можете инициализировать его с помощью mc = MyClass.new("blah"), и параметр age будет установлен внутри. Как мне сделать это в C? Пока я получил это, но это заставляет ввести другой аргумент:

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);
}

Я думал о проверке значения age против Qnil или использовании if ( TYPE(age) == T_UNDEF ), но я просто получаю оттуда segfaults. Прочтение README.EXT наводит меня на мысль, что я могу сделать это через rb_define_method, используя значение argc, но это было не слишком ясно. Есть идеи? Спасибо.

Ответы [ 2 ]

29 голосов
/ 02 октября 2011

Вы правы - вы можете сделать это, используя rb_define_method и отрицательное значение для argc.

Обычно argc указывает количество аргументов, которые принимает ваш метод, но использование отрицательного значения указывает, что метод принимает переменное количество аргументов, которые Ruby передает в виде массива.

Есть две возможности. Во-первых, используйте -1, если вы хотите, чтобы аргументы передавались вашему методу в массиве C. Ваш метод будет иметь подпись, такую ​​как VALUE func(int argc, VALUE *argv, VALUE obj), где argc - это число аргументов, argv - указатель на сами аргументы, а obj - это принимающий объект, т.е. self. Затем вы можете манипулировать этим массивом, если вам нужно имитировать аргументы по умолчанию или все, что вам нужно, в вашем случае это может выглядеть примерно так:

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;
}

Альтернативой является передача массива Ruby в ваш метод, который вы указываете с помощью -2 при вызове rb_define_method. В этом случае ваш метод должен иметь сигнатуру типа VALUE func(VALUE obj, VALUE args), где obj - это принимающий объект (self), а args - это массив Ruby, содержащий аргументы. В вашем случае это может выглядеть примерно так:

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;
}
8 голосов
/ 24 ноября 2013

Вам нужно использовать argc из rb_define_method.Вы должны передать -1 как argc в rb_define_method и использовать rb_scan_args для обработки необязательных аргументов.Например, пример Мэтта можно упростить до следующего:

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;
}

Использование

Получено из Прагматической книжной полки :

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.

Пример:

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

Кроме того, в Ruby 2 также есть флаг :, который используется для именованных аргументов и хэша опций.Однако мне еще предстоит выяснить, как это работает.

Почему?

Существует множество преимуществ использования rb_scan_args:

  1. Он обрабатывает необязательные аргументы, присваивая их nil (Qnil в C).Это имеет побочный эффект предотвращения странного поведения вашего расширения, если кто-то передает nil одному из необязательных аргументов, что делает .
  2. Он использует rb_error_arity для поднятия ArgumentError в стандартном формате (например, wrong number of arguments (2 for 1)).
  3. Обычно оно короче.

Преимущества rb_scan_argsуточняется здесь: http://www.oreillynet.com/ruby/blog/2007/04/c_extension_authors_use_rb_sca_1.html

...