Создание расширений Ruby на C++ с использованием интерфейса Rice

Добавьте новые расширения программирования в Ruby

Научитесь расширять язык программирования Ruby с использованием интерфейса Rice в дополнении к собственному C API Ruby. Используя этот объектно-ориентированный интерфейс, вы сможете создавать объекты Ruby в коде C++, преобразовывать объекты данных между Ruby и C++ и выполнять множество других операций.

Арпан Сен, технический директор, Synapti Computer Aided Design Pvt Ltd

Арпан Сен (Arpan Sen) – ведущий инженер, работающий над разработкой программного обеспечения в области автоматизации электронного проектирования. На протяжении нескольких лет он работал над некоторыми функциями UNIX, в том числе Solaris, SunOS, HP-UX и IRIX, а также Linux и Microsoft Windows. Он проявляет живой интерес к методикам оптимизации производительности программного обеспечения, теории графов и параллельным вычислениям. Арпан является аспирантов в области программных систем.



28.05.2013

Одной из самых замечательных особенностей языка Ruby является возможность его расширения с использованием интерфейсов прикладного программирования (API), определенных на C/C++. Ruby предоставляет заголовок C ruby.h, который включает ряд функций для создания классов Ruby, модулей и многого другого. Помимо этого предоставляемого языком Ruby заголовка, существует несколько других высокоуровневых абстракций для расширения Ruby, основывающихся на собственном заголовке данного языка ruby.h. Одной из таких абстракций является рассматриваемый в настоящей статье интерфейс Ruby для расширений C++, илиRice.

Создание расширения Ruby

Прежде чем переходить к представлению API C языка Ruby и расширений Rice, я хотел бы четко описать стандартный процесс создания расширения:

  1. У вас есть один или несколько файлов исходного кода C/C++, из которых вы создаете совместно используемую библиотеку.
  2. Если вы создаете расширение с использованием Rice, вам необходимо связать код как с libruby.a, так и с librice.a.
  3. Скопируйте совместно используемую библиотеку в какую-либо папку и укажите эту папку в переменной среды RUBYLIB.
  4. Используйте обычную загрузку на основе require в приглашении Interactive Ruby (irb)/сценарии Ruby. Если совместно используемая библиотека называется rubytest.so, то для ее загрузки достаточно простого ввода команды require 'rubytest'.

Предположим, заголовок ruby.h находится в папке /usr/lib/ruby/1.8/include, заголовки Rice — в папке /usr/local/include/rice/include, а код расширения — в файле rubytest.cpp. В листинге 1 показано, каким образом можно скомпилировать и загрузить код.

Листинг 1. Компиляция и загрузка расширения Ruby
bash# g++ -c rubytest.cpp –g –Wall -I/usr/lib/ruby/1.8/include  \
    -I/usr/local/include/rice/include
bash# g++ -shared –o rubytest.so rubytest.o -L/usr/lib/ruby/1.8/lib \
    -L/usr/local/lib/rice/lib  -lruby –lrice –ldl  -lpthread
bash# cp rubytest.so /opt/test
bash# export RUBYLIB=$RUBYLIB:/opt/test
bash# irb
irb> require 'rubytest'
=> true

Программа Hello World

Теперь вы готовы к созданию своей первой программы Hello World с использованием Rice. Используя API Rice, вы создаете класс Test с методом hello, который выводит строку «Hello, World!» При загрузке расширения интерпретатор Ruby вызывает функцию Init_<имя совместно используемой библиотеки>. Для расширения rubytest из листинга 1 данный вызов подразумевает, что rubytest.cpp имеет определенную функцию Init_rubytest. Rice позволяет вам создать свой собственный класс с использованием API define_class. Соответствующий код показан в листинге 2.

Листинг 2. Создание класса с использованием API Rice
#include "rice/Class.hpp"
extern "C"
void Init_rubytest( ) { 
  Class tmp_ = define_class("Test");
}

При компиляции и загрузке кода из листинга 2 в irb вы должны получить результат, показанный в листинге 3.

Листинг 3. Тестирование класса, созданного с помощью Rice
irb> require ‘rubytest’
=> true
irb> a = Test.new
=> #<Test:0x1084a3928>
irb> a.methods
=> ["inspect", "tap", "clone", "public_methods", "__send__", 
      "instance_variable_defined?", "equal?", "freeze", …]

Обратите внимание на доступность нескольких предварительно определенных методов класса, таких как inspect. Это объясняется тем, что определенный вами класс Test неявным образом выведен из класса Object(каждый класс Ruby является производным от Object; в сущности, все содержимое Ruby, включая числа, представляет собой объекты, базовым классом которых является Object).

Теперь добавьте в класс Test какой-либо метод. Соответствующий код показан в листинге 4.

Листинг 4. Добавление метода в класс Test
void hello() {
   std::cout << "Hello World!";
}
extern "C"
 void Init_rubytest() {
      Class test_ = define_class("Test")
         .define_method("hello", &hello);
}

В листинге 4 для добавления метода в класс Test используется API define_method. Обратите внимание, что define_class— это функция, возвращающая объект типа Class; define_method— это функция-член класса Module_Impl, который является базовым классом для Class. В данном случае тест Ruby проверяет, что все действительно корректно:

irb> require ‘rubytest’
=> true
irb> Test.new.hello
Hello, World!
=> nil

Передача аргументов из Ruby в код C/C++

Теперь, когда наша программа Hello World работает, попробуйте передать какой-либо аргумент из Ruby в функцию hello и дать этой функции команду на вывод переданного аргумента на стандартное устройство вывода (sdtout). Проще всего это можно сделать, добавив строковой аргумент в функцию hello:

void hello(std::string args) {
   std::cout << args << std::endl;
}
extern "C"
 void Init_rubytest() {
      Class test_ = define_class("Test")
         .define_method("hello", &hello);
}

В мире Ruby такая функция hello вызывалась бы следующим образом:

irb> a = Test.new
<Test:0x0145e42112>
irb> a.hello "Hello World in Ruby"
Hello World in Ruby
=> nil

Самое примечательное при использовании Rice то, что вам не нужно делать ничего особенного для преобразования строки Ruby в std::string.

Теперь попробуйте использовать какой-либо массив строк в функции hello, а затем попробовать передать информацию из Ruby в код C++. Проще всего сделать это с помощью типа данных Array, который предоставляет Rice. Использование Rice::Array, определяемого в заголовке rice/Array.hpp, подобно использованию контейнера стандартной библиотеки шаблонов (Standard Template Library, сокращенно STL). Обычные итераторы в стиле STL и другие определяются в качестве составной части интерфейса Array. В листинге 5 показана подпрограмма count, которая принимает Array Rice в качестве аргумента.

Листинг 5. Вывод массива Ruby
#include "rice/Array.hpp"

void Array_Print (Array a)   {
      Array::iterator aI = a.begin();
      Array::iterator aE = a.end();
      while (aI != aE) {
        std::cout << "Array has " << *aI << std::endl;
        ++aI;
      }
  }

Теперь расскажем о том, в чем состоит прелесть данного решения: предположим, у вас есть std::vector<std::string> в качестве аргумента Array_Print. Ruby выдает следующую ошибку:

>> t = Test.new
=> #<Test:0x100494688>
>> t.Array_Print ["g", "ggh1", "hh1"]
ArgumentError: Unable to convert Array to std::vector<std::string, 
    std::allocator<std::string> >
	from (irb):3:in `hello'
	from (irb):3

Однако благодаря показанной здесь подпрограмме Array_Print Rice заботится о преобразовании массива Ruby в тип Array C++. Ниже приводится образец выполнения:

>> t = Test.new
=> #<Test:0x100494688>
>>  t.Array_Print ["hello", "world", "ruby"]
Array has hello
Array has world
Array has ruby
=> nil

Теперь попробуйте сделать обратное, передав массив из C++ в мир Ruby. Обратите внимание, что в Ruby элементы массива не обязательно относятся к одному типу. Соответствующий код показан в листинге 6.

Листинг 6. Передача массива из C++ в Ruby
#include "rice/String.hpp"
#include "rice/Array.hpp"
using namespace rice; 

Array return_array (Array a)  {
      Array tmp_;
      tmp_.push(1);
      tmp_.push(2.3);
      tmp_.push(String("hello"));
      return tmp_;
 }

В листинге 6 четко показано, что вы можете создать массив Ruby с элементами разных типов непосредственно внутри C++. Ниже приводится код теста в Ruby:

>> x = t.return_array
=> [1, 2.3, "hello"]
>> x[0].class
=> Fixnum
>> x[1].class
=> Float
>> x[2].class
=> String

Что если у меня нет возможности изменять список аргументов C++?

Зачастую вы будете сталкиваться с тем, что от интерфейса Ruby требуется трансляция данных для функций C++, сигнатуру которых вы не можете изменять. Для примера рассмотрим случай, когда вам необходимо передать какой-либо массив строк из Ruby в C++. Сигнатура функции C++ выглядит следующим образом:

void print_array(std::vector<std::string> args)

В сущности, все, что вам нужно в данной ситуации, — это какая-либо функция from_ruby, которая принимает массив Ruby и преобразовывает его в std::vector<std::string>. Именно это и предоставляет Rice–функцию from_ruby со следующей сигнатурой:

template <typename T>
T from_ruby(Object );

Для каждого типа данных Ruby, который требуется преобразовывать в тип данных C++, вам нужно специализировать шаблон подпрограммы from_ruby. Например, в листинге 7 показано, как следует определить функцию from_ruby при передаче массива Ruby в приведенную выше функцию обработки.

Листинг 7. Преобразование массива Ruby в std::vector<std::string>
template<>
std::vector<std::string> from_ruby< std::vector<std::string> > (Object o)   {
    Array a(o);
    std::vector<std::string> v;
    for(Array::iterator aI = a.begin(); aI != a.end(); ++aI)
        v.push_back(((String)*aI).str());
    return v;
    }

Обратите внимание, что функцию from_ruby не требуется вызывать явным образом. Когда из мира Ruby в качестве аргумента функции передается какой-либо массив string, функция from_ruby преобразует этот массив в std::vector<std::string>. Тем не менее код в листинге 7 несовершенен, и вы уже видели, что массивы в Ruby могут иметь элементы разных типов. Напротив, вы выполнили вызов ((String)*aI).str() для получения std::string из Rice::String. ( str— это метод Rice::String:. Для получения дополнительной информации обратитесь к String.hpp). В листинге 8 показано, как выглядел бы код, если бы вам пришлось иметь дело с наиболее общим случаем.

Листинг 8. Преобразование массива Ruby в std::vector<std::string> (общий случай)
template<>
std::vector<std::string> from_ruby< std::vector<std::string> > (Object o)   {
    Array a(o);
    std::vector<std::string> v;
    for(Array::iterator aI = a.begin(); aI != a.end(); ++aI)
        v.push_back(from_ruby<std::string> (*aI));
    return v;
    }

Поскольку каждый элемент массива Ruby также является объектом Ruby типа String, и вы полагаетесь на то, что Rice имеет определенный метод from_ruby для преобразования данного типа в std::string, от вас больше не требуется никаких действий. В противном случае вам необходимо предоставить для преобразования свой метод from_ruby. Ниже приводится метод from_ruby из to_from_ruby.ipp в исходниках Rice:

template<>
inline std::string from_ruby<std::string>(Rice::Object x) {
  return Rice::String(x).str();
}

Протестируйте этот код из мира Ruby. Начните с передачи массива всех строк, как показано в листинге 9.

Листинг 9. Проверка функционала from_ruby
>> t = Test.new
=> #<Test:0x10e71c5c8>
>> t.print_array ["aa", "bb"]
aa bb
=> nil
>> t.print_array ["aa", "bb", 111]
TypeError: wrong argument type Fixnum (expected String)
	from (irb):4:in `print_array'
	from (irb):4

Как и ожидалось, первый вызов print_array прошел успешно. Поскольку метод from_ruby для преобразования Fixnum в std::string отсутствует, второй вызов приводит к тому, что интерпретатор Ruby выдает TypeError. Существует несколько способов исправления этой ошибки — например, во время вызова Ruby можно передать в качестве части массива только строки (например, t.print_array["aa", "bb", 111.to_s]) или выполнить вызов к Object.to_s внутри кода C++. Метод to_s является частью интерфейса Rice::Object и возвращает Rice::String, которая имеет предварительно определенный метод str, возвращающий std::string. Такой подход с применением C++ применяется в листинге 10.

Листинг 10. Использование Object.to_s для заполнения вектора строк
template<>
std::vector<std::string> from_ruby< std::vector<std::string> > (Object o)   {
    Array a(o);
    std::vector<std::string> v;
    for(Array::iterator aI = a.begin(); aI != a.end(); ++aI)
        v.push_back(aI->to_s().str());
    return v;
    }

В общем случае код в листинге 10 будет сложнее, поскольку вам придется иметь дело с нестандартными представлениями строк для пользовательских классов.


Создание полного класса с переменными с использованием C++

Вы уже видели, как можно создать класс Ruby и соответствующие функции внутри кода C++. Для более общих классов вам требуется какой-либо способ определения объектных переменных и обеспечения поддержки метода initialize. Для установления и получения значений привязочных переменных объектов Ruby используются методы Rice::Object::iv_set и Rice::Object::iv_get соответственно. Код показан в листинге 11.

Листинг 11. Определение метода initialize в C++
void init(Object self) {
      self.iv_set("@intvar", 121);
      self.iv_set("@stringvar", String("testing"));
 }
Class cTest = define_class("Test").
                          define_method("initialize", &init);

Когда какая-либо функция C++ объявляется в качестве метода класса Ruby с использованием API define_method, вы имеете возможность объявить первый аргумент данной функции C++ как Object, при этом Ruby заполнит этот Object ссылкой на вызывающий экземпляр. Затем вы вызываете iv_set для Object, чтобы установить объектные переменные. Вот как выглядит данный интерфейс в мире Ruby:

>> require 'rubytest'
=> true
>> t = Test.new
=> #<Test:0x1010fe400 @stringvar="testing", @intvar=121>

Аналогично, для возврата какой-либо объектной переменной возвращающая функция должна принять Object, который ссылается на объект в Ruby, и вызвать для него iv_get. Соответствующий фрагмент кода показан в листинге 12.

Листинг 12. Выборка значений из объекта Ruby
void init(Object self) {
      self.iv_set("@intvar", 121);
      self.iv_set("@stringvar", String("testing"));
 }
int getvalue(Object self) { 
    return self.iv_get("@intvar");
}
Class cTest = define_class("Test").
                          define_method("initialize", &init).
                          define_method("getint", &getvalue);

Трансформация класса C++ в тип Ruby

До сих пор вы обертывали в качестве методов классов Ruby свободные функции (т. е. методы, не принадлежащие ни к какому классу). Вы передавали ссылки на объект Ruby путем объявления функций C с первым аргументом Object. Такой подход работает, но он недостаточно хорош для обертывания класса C++ в качестве объекта Ruby. Чтобы обернуть какой-либо класс C++, вы по-прежнему будете использовать метод define_class, но теперь создадите из него шаблон для конкретного типа класса C++. В коде, представленном в листинге 13, класс C++ обертывается в качестве типа Ruby.

Листинг 13. Обертывание класса C++ в качестве типа Ruby
class cppType {
    public:
      void print(String args) {
        std::cout << args.str() << endl;
      }
};
Class rb_cTest =
        define_class<cppType>("Test")
         .define_method("print", &cppType::print);

Обратите внимание на создание шаблона из define_class, о чем было сказано выше. Тем не менее с этим классом не все в порядке. Ниже представлена запись журнала интерпретатора Ruby при попытке создать экземпляр объекта типа Test:

>> t = Test.new
TypeError: allocator undefined for Test
	from (irb):3:in `new'
	from (irb):3

Что же произошло? Дело в том, что вам необходимо явным образом связать конструктор с каким-либо типом Ruby. (Это одна из «причуд» Rice). Rice предоставляет в ваше распоряжение метод define_constructor для связывания конструктора с типом C++. Кроме того, вам необходимо включить заголовок Constructor.hpp. Обратите внимание, что вы должны делать это, даже если у вас в коде нет явно заданного конструктора. Образец кода представлен в листинге 14.

Листинг 14. Связывание конструктора C++ с типом Ruby
#include "rice/Constructor.hpp"
#include "rice/String.hpp"
class cppType {
    public:
    void print(String args) {
        std::cout << args.str() << endl;
      }
    };

Class rb_cTest =
        define_class<cppType>("Test")
         .define_constructor(Constructor<cppType>())
        .define_method("print", &cppType::print);

Также можно связать конструктор с каким-либо списком аргументов с помощью метода define_constructor. Rice предусматривает для этого добавление типов аргументов в список шаблонов. Например, если cppType имеет конструктор, принимающий какое-либо целое число, вы должны вызвать define_constructor как define_constructor(Constructor<cppType, int>()). Здесь требуется сделать пояснение: типы Ruby не могут иметь несколько конструкторов. Таким образом, если у вас есть какой-либо тип C++ с несколькими конструкторами, и вы связываете их с помощью define_constructor, вы можете создать из мира Ruby экземпляр типа с аргументами (или без таковых), определяемого последним define_constructor в исходном коде. Все, что было рассмотрено выше, поясняется в листинге 15.

Листинг 15. Связывание конструкторов с аргументами
class cppType {
    public:
      cppType(int m) {
        std::cout << m << std::endl;
      }
      cppType(Array a) {
        std::cout << a.size() << std::endl;
      }
      void print(String args) {
        std::cout << args.str() << endl;
      }
    };
Class rb_cTest =
        define_class<cppType>("Test")
         .define_constructor(Constructor<cppType, int>())
         .define_constructor(Constructor<cppType, Array>())
         .define_method("print", &cppType::print);

Ниже представлена запись журнала на стороне Ruby. Обратите внимание, что Ruby «понимает» только конструктор, связанный последним:

>> t = Test.new 2
TypeError: wrong argument type Fixnum (expected Array)
	from (irb):2:in `initialize'
	from (irb):2:in `new'
	from (irb):2
>> t = Test.new [1, 2]
2
=> #<Test:0x10d52cf48>

Определение нового типа Ruby в качестве части модуля

Определение нового модуля Ruby из C++ сводится к выполнению вызова к define_module. Чтобы определить какой-либо класс, доступный только в качестве составной части указанного модуля, используйте define_class_under вместо обычного метода define_class. Первым аргументом для define_class_under является объект модуля. Если бы из листинга 14 вам потребовалось определить cppType в качестве части модуля Ruby с именем types, это можно было бы сделать так, как показано в листинге 16.

Листинг 16. Определение типа в качестве части модуля
#include "rice/Constructor.hpp"
#include "rice/String.hpp"
class cppType {
    public:
    void print(String args) {
        std::cout << args.str() << endl;
      }
    };

Module rb_cModule = define_module("Types");
Class rb_cTest =
        define_class_under<cppType>(rb_cModule, "Test")
         .define_constructor(Constructor<cppType>())
        .define_method("print", &cppType::print);

А вот как вы делаете то же самое в Ruby:

>> include Types
=> Object
>> y = Types::Test.new [1, 1, 1]
3
=> #<Types::Test:0x1058efbd8>

Обратите внимание на то, что в Ruby имена модулей и классов должны начинаться с прописной буквы. Rice не выдаст ошибку, если, предположим, вы назовете модуль types вместо Types.


Создание структуры Ruby с использованием кода C++

Для быстрого создания стандартных классов Ruby используется конструкция struct. В листинге 17 показан типовой для Ruby способ создания нового класса типа NewClass с тремя переменными a, ab и aab.

Листинг 17. Использование структуры Ruby для создания нового класса
>> NewClass = Struct.new(:a, :ab, :aab)
=> NewClass
>> NewClass.class
=> Class
>> a = NewClass.new
=> #<struct NewClass a=nil, ab=nil, aab=nil>
>> a.a = 1
=> 1
>> a.ab = "test"
=> "test"
>> a.aab = 2.33
=> 2.33
>> a
=> #<struct NewClass a=1, ab="test", aab=2.33>
>> a.a.class
=> Fixnum
>> a.ab.class
=> String
>> a.aab.class
=> Float

Чтобы написать на C++ код, эквивалентный представленному в листинге 17, вам необходимо использовать API define_struct( ), объявляемый в заголовке rice/Struct.hpp. Этот API возвращает Rice::Struct. Вы связываете класс Ruby, создаваемый данной конструкцией struct, и модуль, частью которого будет данный класс. Вот для чего нужен метод initialize. Отдельные члены класса определяются с использованием вызова функции define_member. Обратите внимание, что вы создали новый тип Ruby, но не связали с ним типы или функции C++. Ниже представлен код для создания класса NewClass:

#include "rice/Struct.hpp"
…
Module rb1 = define_module("Types");
define_struct().
        define_member("a").
        define_member("ab").
        define_member("aab").
        initialize(rb1, "NewClass");

Заключение

В статье рассматривается широкий круг вопросов — создание объектов Ruby в коде C++, связывание функций в стиле C в качестве методов объектов Ruby, преобразование типов данных между Ruby и C++, создание объектных переменных и обертывание класса C++ в качестве типа Ruby. Все эти задачи можно выполнить с помощью заголовка ruby.h и libruby, но для того, чтобы все это заработало, вам пришлось бы написать очень много стандартного кода. Использование Rice облегчает выполнение этой работы. Желаю вам получить удовольствие от написания новых расширений на C++ для мира Ruby!

Ресурсы

Комментарии

developerWorks: Войти

Обязательные поля отмечены звездочкой (*).


Нужен IBM ID?
Забыли Ваш IBM ID?


Забыли Ваш пароль?
Изменить пароль

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Профиль создается, когда вы первый раз заходите в developerWorks. Информация в вашем профиле (имя, страна / регион, название компании) отображается для всех пользователей и будет сопровождать любой опубликованный вами контент пока вы специально не укажите скрыть название вашей компании. Вы можете обновить ваш IBM аккаунт в любое время.

Вся введенная информация защищена.

Выберите имя, которое будет отображаться на экране



При первом входе в developerWorks для Вас будет создан профиль и Вам нужно будет выбрать Отображаемое имя. Оно будет выводиться рядом с контентом, опубликованным Вами в developerWorks.

Отображаемое имя должно иметь длину от 3 символов до 31 символа. Ваше Имя в системе должно быть уникальным. В качестве имени по соображениям приватности нельзя использовать контактный e-mail.

Обязательные поля отмечены звездочкой (*).

(Отображаемое имя должно иметь длину от 3 символов до 31 символа.)

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Вся введенная информация защищена.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Open source
ArticleID=931596
ArticleTitle=Создание расширений Ruby на C++ с использованием интерфейса Rice
publish-date=05282013