Подтвердить что ты не робот

Расширение ruby ​​на C - как указать значения аргументов по умолчанию для работы?

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

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

Вы можете инициализировать его с помощью mc = MyClass.new("blah"), а возрастной параметр будет установлен внутри. Как это сделать в 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, но это было не слишком ясно. Есть идеи? Спасибо.

4b9b3361

Ответ 1

Вы правы - вы можете сделать это, используя 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;
}

Ответ 2

Вам нужно использовать 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:

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

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