Ruby – это замечательный язык программирования, приобретающий всю большую популярность в последнее время. Наверное, вы знакомы с Ruby, раз принялись читать эту статью. В противном случае вам сначала лучше познакомиться с информацией, представленной на официальном сайте www.ruby-lang.org, прежде чем приступать к чтению.
Одна из привлекательных черт Ruby – это возможность создавать свои расширения как на самом Ruby, так и на других языках. Например, у вас есть участок кода, критичный к производительности, то вам лучше реализовать его на C, и потом работать с ним из Ruby. Или же если Ruby не поддерживает вашу любимую библиотеку, а вы хотите «научить» Ruby с ней работать (сперва загляните на www.rubyforge.org, наверняка расширение Ruby для этой библиотеки уже реализовано). А может, вы хотите использовать какие-то специфические возможности операционной системы? Так или иначе, вы всегда можете взять в руки клавиатуру, опуститься на нижний уровень, и запрограммировать всё необходимое на C. Демонстрацией того, как это можно сделать, мы и займёмся в данной статье.
Инструментарий
Инструментарий зависит от ОС в которой мы будем работать. В любом случае, нам понадобится дистрибутив Ruby, последнюю версию которого вы можете загрузить с http://www.ruby-lang.org. В данной статье мы строим расширения Ruby для Windows. Сборка в среде Linux выполняется аналогично с использованием gcc и make.
Нам понадобится Microsoft Visual Studio. Загрузить Visual Studio Express Visual C++ вы можете совершенно бесплатно отсюда: http://microsoft.com/express/.
Простое расширение
Сейчас мы напишем самое простое расширение. Оно будет содержать две полезные функции: одна выводит на консоль ”Hello World!”, а вторая вычисляет квадрат переданного числа.
Запустим командную строку, в которой будем работать, и создадим для нашего расширения отдельную директорию:
>mkdir MyTest >cd MyTest
Пишем исходный текст
Теперь создадим файл mytest.c, в котором будет располагаться код нашего расширения:
#include "ruby.h" void Init_mytest(); VALUE method_sqr(VALUE, VALUE); VALUE method_sayhello(VALUE); VALUE mytest = Qnil; void Init_mytest() { mytest = rb_define_module("MyTest"); rb_define_method(mytest, "sqr", method_sqr, 1); rb_define_method(mytest, "sayhello", method_sayhello, 0); } VALUE method_sayhello(VALUE self) { puts("Hello World!"); return Qnil; } VALUE method_sqr(VALUE self, VALUE x) { int y = NUM2INT(x); y *= y; return INT2NUM(y); }
Генерируем Makefile
В той же директории создайте файл extconf.rb, со следующим содержимым:
# mkmf используется для создания мейкфайла расширения require 'mkmf' extension_name = 'mytest' dir_config(extension_name) create_makefile(extension_name)
Запускаем этот скрипт, и он создаст нам Makefile для нашего расширения. Благо нам не нужно составлять его вручную, Ruby побеспокоился об этом за нас:
>ruby extconf.rb creating Makefile
Теперь один важный момент: если путь до каталога с Ruby содержит пробелы, вам наверняка нужно будет внести изменения в Makefile, иначе расширение скомпилируется неправильно. Откройте Makefile и убедитесь, что значения переменных topdir и ruby не содержат пробелов, либо заключены в кавычки.
Устанавливаем переменные окружения
Если всё верно, самое время скомпилировать расширение.
Сначала необходимо загрузить переменные окружения Visual С++, чтобы компилятор и линковщик могли найти то, что им нужно. Для этого из командной строки, в которой вы работаете, перейдите в каталог \Каталог_Вижуал_Студии\VC\bin\ и запустите vcvars32.bat.
Правим config.h
Ещё одна вещь, которую нужно сделать перед сборкой проекта, это отредактировать файл /Каталог_Ruby/lib/ruby/1.8/i386-mswin32/config.h
Необходимо закомментировать, или удалить следующие строчки в начале файла, чтобы Ruby не ругался, когда мы начнём компилировать расширение:
#if _MSC_VER != 1200 #error MSC version unmatch #endif
Собираем проект
Теперь вернитесь в каталог с нашим расширением и запустите nmake:
>nmake Microsoft (R) Program Maintenance Utility Version 9.00.20209 Copyright (C) Microsoft Corporation. All rights reserved. cl -nologo -I. -I"E:/Program Files/ruby/lib/ruby/1.8/i386-mswin32" -I"E:/Program Files/ruby/lib/ruby/1.8/i386-mswin32" -I. -MD -Zi -O2b2xg- -G6 -c -Tcmytest.c mytest.c cl -nologo -LD -Femytest.so mytest.obj msvcrt-ruby18.lib oldnames.lib user32.lib advapi32.lib ws2_32.lib -link -incremental:no -debug -opt:ref -opt:icf -dll -libpath:"E:/Program Files/ruby/lib" -def:mytest-i386-mswin32.def -implib:mytest-i386-mswin32.lib -pdb:mytest-i386-mswin32.pdb Creating library mytest-i386-mswin32.lib and object mytest-i386-mswin32.exp
Добавляем манифест в библиотеку
Расширение скомпилировано. Теперь нужно добавить в него манифест, чтобы библиотека правильно загружалась. Манифест – это некое представление всего, что находится в библиотеке.
Так, выполним команду:
>mt.exe -manifest mytest.so.manifest -outputresource:mytest.so;2 Microsoft (R) Manifest Tool version 5.2.3790.2014 Copyright (c) Microsoft Corporation 2005. All rights reserved.
Подробнее про манифесты вы можете прочесть в MSDN по следующему адресу: http://msdn2.microsoft.com/en-us/library/Aa375365.aspx
Проверяем расширение
Расширение готово. Теперь протестируем его с помощью irb, запустив его из той же директории:
>irb irb(main):001:0> require "mytest" => true irb(main):002:0> include MyTest => Object irb(main):003:0> sayhello() Hello World! => false irb(main):004:0> sqr(5) => 25 irb(main):005:0> quit
Поздравляю! Первое расширение работает. Это файл mytest.so. Для того, чтобы его можно было использовать в ваших программах на Ruby, его нужно скопировать в \Каталог_Ruby\lib\ruby\site_ruby\1.8\i386-msvcrt\. Либо выполнить nmake install.
Стоит заметить, что для того, чтобы это расширение работало на других компьютерах, необходимо, чтобы на них был установлен Visual C++ Redistributable Package, который можно загрузить с сайта Microsoft.
На данном этапе предполагается, что процесс сборки расширения Ruby был описан, и больше возвращаться к этой теме мы не будем, а заострим внимание непосредственно на программировании.
Так что же мы запрограммировали?
Разберемся с тем, что представляет собой исходный код нашего модуля. Мы подключаем файл ruby.h, в котором содержатся все объявления, которые нам необходимы.
Основная функция нашего расширения – это Init_mytest(), её вызывает интерпретатор Ruby, когда вы пытаетесь загрузить расширения из своего скрипта. В ней обычно при помощи функций rb_define_module, rb_define_method, rb_define_class и других, регистрируется содержимое расширения. Любое расширение Ruby должно определить глобальную функцию Init_name, где name – имя расширения.
В ruby.h объявлен основной тип объектов Ruby – VALUE. VALUE представляет собой указатель на область памяти, в которой располагается какой-то объект Ruby (на самом деле, VALUE не всегда указатель, но он этом чуть позже). Параметры функций, вызываемых из Ruby имеют тип VALUE, ровно как и каждая функция, которая может быть вызвана из Ruby должна возвращать VALUE. Даже если функции не нужно ничего возвращать, она обязана вернуть Qnil. Qnil представляет собой NULL-значение указателя любого объекта Ruby.
Так, в нашем расширении определяется модуль, содержащий две функции. Указатель на модуль представляет следующая переменная:
VALUE mytest = Qnil;
Самое интересное начинается, когда интерпретатор Ruby вызывает Init_mytest() тогда, когда встречает команду require “mytest” в Ruby-программе:
void Init_mytest() { mytest = rb_define_module("MyTest"); rb_define_method(mytest, "sqr", method_sqr, 1); rb_define_method(mytest, "sayhello", method_sayhello, 0); }
В первой строке функции мы объявляем модуль Ruby с именем MyTest. Это то имя, которое мы использовали в проверочном скрипте когда писали include MyTest. В следующих двух строках мы объявляем метод уровня модуля, соответственно, функция rb_define_method принимает четыре параметра: указатель на сущность, в которой определяется модуль (может быть модуль, может быть класс), имя метода, ссылка на функцию и количество параметров. Функции библиотеки Ruby обычно имеют префикс rb_.
На самом деле, мы программируем на Ruby из C: при нас остаются все чудесные возможности Ruby, вдобавок мы получаем контроль над ресурсами, который нам может предложить C.
Документацию по Ruby API можно найти по следующему адресу: http://www.ruby-doc.org/doxygen/current/
Тип VALUE
Переменные типа VALUE как непосредственные значения
Наш первый метод method_sayhello довольно примитивен – он лишь печатает строку. А вот о втором, method_sqr, нужно сказать несколько слов. Не так давно мы отметили, что VALUE это указатель на какой то объект. Однако, из соображений производительности, для некоторых простых типов Ruby, значения располагаются прямо в переменной. Получается, переменные типа VALUE могут быть и указателями, и непосредственными значениями. Ruby использует магические манипуляции с битами, чтобы определить, является ли значение переменной указателем на объект, либо непосредственным значением. Следующие типы Ruby хранятся непосредственно в переменной типа VALUE: Fixnum, Symbol, true, false, nil.
В нашей функции method_sqr в переменной x передаётся число, а не указатель. В нашем случае, мы приводим VALUE к int с помощью макроса NUM2INT, далее выполняем возведение в квадрат, и затем результат преобразуем обратно с помощью INT2NUM.
Переменные типа VALUE как строки
В C строка представляется последовательностью байтов, завершаемой нулевым символом ». Строки Ruby представляются типом RString и содержат в себе длину строки, и ссылку на саму строку. Для создания Ruby-строки из C-строки используется следующая функция:
rb_str_new2(char*)
Если мы имеем строку Ruby, мы можем получить доступ к её «внутренностям» с помощью макроса:
RSTRING(str)->len // длина строки RSTRING(str)->ptr // указатель на C-строку
Переменные типа VALUE как другие объекты
Переменные типа VALUE, как было сказано, могут быть указателями на объекты Ruby. Среди таких объектов: массивы, хеш-таблицы, строки, и многие другие типы. Все они определены в ruby.h и имеют имена, начинающиеся с R: RArray, RHash и т.д. Для проверки соответствия типа нашим ожиданиям, мы можем использовать следующий макрос:
Check_Type(VALUE value, int type)
Пример работы с VALUE: массивы
Мы немного изменим наше расширение, добавив в него ещё одну функцию, чтобы увидеть объектное «лицо» типа VALUE на примере массива. Мы реализуем функцию, которая вычисляет сумму всех элементов числового массива.
Определим прототип:
VALUE method_sum(VALUE, VALUE);
Объявим метод в функции Init_mytest:
rb_define_method(mytest, "sum", method_sum, 1);
Поставленную задачу можно решить двумя способами. В стиле Ruby, и в стиле C. Сначала посмотрим на способ в стиле Ruby.
// Функция, представляющая «итерационный блок» VALUE iter_sum (VALUE c, int *psum) { *psum += NUM2INT(c); return Qnil; } // Возвращает сумму элементов массива а VALUE method_sum(VALUE self, VALUE a) { int sum = 0; Check_Type(a, T_ARRAY); rb_iterate(rb_each, a, iter_sum, (VALUE)&sum); return INT2NUM(sum); }
iter_sum – вспомогательная функция. Она представляет собой содержимое «блока» each. В method_sum мы сначала с помощью Check_Type требуем, чтобы наша функция вызывалась только с аргументом-массивом. Далее с помощью rb_iterate запускаем итерацию по массиву.
Наш «блок» iter_sum вызывается для каждого элемента массива. Результаты суммирования сохраняются в переменной sum, указатель на которую мы тоже передаём в iter_sum. В результате работы rb_iterate у нас в переменной sum находится сумма всех элементов массива, мы преобразуем это число в Fixnum и возвращаем из метода.
Теперь мы можем из Ruby вызывать этот метод:
> require “mytest” => true > include MyTest => Object > sum([1,2,3]) => 6 > sum("hello") TypeError: wrong argument type String (expected Array)
Рассмотрим второй способ.
VALUE method_sum(VALUE self, VALUE a) { int i = 0; int sum = 0; Check_Type(a, T_ARRAY); for(i = 0; i < RARRAY(a)->len; i++) sum += NUM2INT(RARRAY(a)->ptr[i]); return INT2NUM(sum); }
Вы видите, здесь мы общаемся с «внутренностями» массива Ruby. Мы пользуемся знанием того, как устроен массив и пробегаем по всем его элементам при помощи указателей. Результат работы аналогичен предыдущему.
Классы
До сих пор мы рассматривали расширения, в которых был объявлен модуль, а в нём обычные методы. В этом разделе мы рассмотрим классы: как объявлять классы Ruby в C-коде, и как работать с ними.
Сначала мы рассмотрим простой класс в Ruby, а затем создадим эквивалентный на C.
class Person < Object def initialize(name) @name = name end def say print "My name is #{@name}" end end
Результаты работы:
>person = Person.new("Vasya"); >person.say My name is Vasya
Теперь посмотрим, как создать аналогичный класс в C. Наш класс наследует от Object, имеет конструктор с параметром, который сохраняется в переменной уровня экземпляра. Затем метод say выводит дружественную строку.
#include "ruby.h" void Init_mytest(); VALUE initialize(VALUE, VALUE); VALUE say(VALUE); VALUE personclass = Qnil; // Наш класс // Инициализация расширения void Init_mytest() { personclass = rb_define_class("Person", rb_cObject); rb_define_method(personclass, "initialize", initialize, 1); rb_define_method(personclass, "say", say, 0); } // Конструктор VALUE initialize(VALUE self, VALUE name) { Check_Type(name, T_STRING); rb_iv_set(self, "@name", name); return self; } // Метод say VALUE say(VALUE self) { VALUE name = rb_iv_get(self, "@name"); printf("My name is %s", RSTRING(name)->ptr); return Qnil; }
Результаты работы:
>require “mytest” >person = Person.new("Vasya"); >person.say My name is Vasya
Рассмотрим подробнее, что теперь представляет собой наше расширение.
В функции инициализации расширения мы делаем следующее:
personclass = rb_define_class("Person", rb_cObject);
Объявляем класс с именем Person. Второй параметр функции – это класс, от которого мы наследуем. В Ruby API такие классы представлены переменными с именами rb_cName, где Name – имя класса. Далее объявляются конструктор и ещё один метод класса Person.
В конструкторе мы проверяем, является ли параметр строкой, и при помощи функции rb_iv_set устанавливаем переменную уровня экземпляра с именем ”@name”. В методе say при помощи «обратной» функции rb_iv_get мы получаем значение переменной @name и выводим текст на консоль.
В этом примере мы не определяли модуль. Если мы хотим определить модуль, а в него поместить класс, то следует использовать функцию rb_define_class_under, например:
mytest = rb_define_module("MyTest"); personclass = rb_define_class_under(mytest, "Person", rb_cObject);
В таком случае, из Ruby к нашему классу нужно будет обращаться так (либо использовать include):
person = MyTest::Person.new("Vasya")
Заключение
В данной статье была описана общая идея, введение в создание расширений для Ruby на языке C. Вы могли видеть, это не так уж и сложно. Возможность написания расширений для скриптового языков – несомненное преимущество для него. Желаю успешного программирования!
Есть прикольная статья — Использование C и Ruby, в которой описанно 4 метода скрещивания С и Ruby. Вообще, эту задачу хорошо решает swig
Использую SWIG в качестве связки между ruby и C++. Пока все гладко. Имеет ли SWIG подводные камни?