Ruby и C. Часть 2.

    В прошлой части мы рассмотрели библиотеку RubyInline, которая позваляет делать вставки C кода прямо в Ruby методы. Ее очень удобно использовать если нам нужно ускорить несколько критичных методов. Но если нам нужно реализовать C библиотеку и использовать в Ruby, или сделать обертку для уже существующей, то нам на помощь придет C API для создания расширений.


    Ruby C API предоставляет нам средства написания Ruby кода с помощью C. Звучит немного странно, но давайте рассмотримм пример.
    Ruby код:
        class Test
            def test
                #реализация метода
            end
        end
    

    Аналогичный C код:
        VALUE test_method(VALUE self) {
            //реализация метода
        }
    
        Init_test() {
            VALUE cTest = rb_define_class("Test", rb_cObject); //создание класса Test
            rb_define_method("test", cTest, test_method, 0); //создание метода test в классе Test
        }
    
    

    Тривиальный пример — создание класса с одним методом. Теперь рассмотрим подробнее C код.
    С помощью функции rb_define_class мы создаем класс Test наследник класса Object. Затем с помощью функции rb_define_method мы создаем метод test класса Test, и в качестве реализации этого метода передаем функцию test_method. Последний параметр означает что наш метод не имеет аргументов.

    В примере везде используется тип данных VALUE. Любой Ruby объект, будь то строка, число, класс, модуль и т.п., в C имеет тип VALUE.
    Ruby C API предоставляет набор макросов и функции для конвертации типов данных Ruby в C и наоборот.
    Вот некоторые из них(из Ruby в С):
        int NUM2INT(VALUE)
        int FIX2INT(VALUE)
        double NUM2DBL(VALUE)
        char *StringValuePtr(VALUE)
    Также есть макрос STR2CSTR, но его лучше не использовать, цитата из документации:
    "because STR2CSTR() has a risk of a dangling pointer problem in the to_str() impliclit conversion."

    Из С в Ruby:
           VALUE INT2NUM(int)
    	VALUE INT2FIX(int)
    	VALUE rb_str_new2(char *)
    	VALUE rb_float_new(double) 	
    


    Модули создаются аналогично классам:
    VALUE rb_define_module(const char *name)

    Теперь о методах.
    Есть несколько функций для создания методов. Вот некоторые из них:
    void rb_define_method(VALUE klass, const char *name, VALUE (*func)(), int argc)
    void rb_define_private_method(VALUE klass, const char *name, VALUE (*func)(), int argc)
    
    Аргументами являются класс, имя метода, ссылка на функцию с реализацией метода, и количество аргументов.
    Функция с реализацией метода должна быть следующего вида:
    VALUE test(VALUE self, VALUE arg1, VALUE arg2, ...)
    self, это указатель на объект, он присутствует всегда, даже если у метода нет аргументов.
    В зависимости от значения аргумента argc, сигнатура может меняться. Отрицательные значения argc используются для определения метода с переменным числом аргументов.
    Если argc равен -1:
    VALUE test(int argc, VALUE *argv, VALUE self)
    argc — количество аргументов, *argv — массив аргументов.
    Если argc равен -1:
    VALUE test(VALUE args, VALUE self)
    args — Ruby массив с аргументами.

    Функция Init_test() вызывается при инициализации нашего расширения, в ней мы как раз создаем классы и модули.

    Теперь, после рассмотрения основных возможностей Ruby C API, давайте попробуем создать собственное расширение. Я решил не придумывать искусственных примеров и проиллюстрировать создание расширения с помощью обертки для какой-нибудь существующей C библиотеки. Посли долгих поисков(почти для всех популярных библиотек уже есть обертки) выбор пал на libdecodeqr, библиотеку для распознавания QR кодов. Простой пример для работы с ней в С можно посмотреть тут qrtest.c

    Наше расширение будет очень простым, класс QRDecoder со статическим методом decode.
    Метод decode в качестве аргумента берет имя файла с QR кодом, и возвращает строку там закодированную.
    Пример использования (сравните с кодом в С:)):
        require "decodeqr"
        puts QRDecoder.decode "test.png"
    

    Задача ясна, приступим к реализации.
    Что нам потребуется:
    1. пакет ruby1.8-dev для сборки расширений.
    sudo apt-get install ruby1.8-dev
    2. библиотека libdecodeqr.
    sudo apt-get install libdecodeqr libdecodeqr-dev

    Начнем с инициализации расширения:
        Init_decodeqr() {
            VALUE cQRDecoder = rb_define_class("QRDecoder", rb_cObject);
            rb_define_singleton_method(cQRDecoder, "decode", decode, 1);
        }
    

    Все просто, создаем класс QRDecoder и статический метод decode.
    Теперь реализация функции decode:
        VALUE decode(VALUE self, VALUE file) {
            char *file_name = StringValuePtr(file); //конвертируем Ruby строку в С
            //работа с библиотекой libdecodeqr, более подробно можно посмотреть в С примере
            QrDecoderHandle qr = qr_decoder_open();
            IplImage *src = cvLoadImage(file_name, 0);
            qr_decoder_decode_image(qr, src, DEFAULT_ADAPTIVE_TH_SIZE, DEFAULT_ADAPTIVE_TH_DELTA);
            QrCodeHeader *qrh = calloc(sizeof(QrCodeHeader), 1);
            qr_decoder_get_header(qr, qrh);
            char *buf = calloc(qrh->byte_size+1, 1);
            qr_decoder_get_body(qr, (unsigned char*)buf, qrh->byte_size+1);
            qr_decoder_close(qr);
            return rb_str_new2(buf); //возвращаем Ruby строку
        }
    

    Наше расширение готово! Код целиком можно посмотреть тут decodqr.c
    Теперь нам нужно собрать наше расширение. Для этого мы используем стандартную Ruby библиотеку mkmf, с помощью которой мы сгенерируем Make файл.
    Пример, файл extconf.rb:
        require 'mkmf'
    
        if have_library("cv") and #если есть библиотека "cv"
        have_library("decodeqr") and #и библиотека "decodeqr"
        have_library("highgui") and #а также "highgui"
        find_header("highgui.h", "/usr/include/opencv/") then # и нашли заголовочный файл "highgui.h"
            create_makefile("decodeqr") #тогда создаем Make файл
        else
            puts "oops..." #а иначе не создаем
        end
    

    И результат его выполнения:
    $ ruby extconf.rb
        checking for main() in -lcv... yes
        checking for main() in -ldecodeqr... yes
        checking for main() in -lhighgui... yes
        checking for highgui.h in /usr/include/opencv/... yes
        creating Makefile
    

    А теперь стандартные:
        make
        sudo make install
    

    и наше расширение готово к использованию.

    Таким образом, если у вас есть С код, или хотите использовать С библиотеку, или вы хотите ускорить Ruby, вы можете просто сделать расширение и использовать его в своих Ruby программах.

    Чего я не коснулся в этой статье:
    1. Работа со строками и массивами
    2. Сборщик мусора
    3. Создание оберток для С структур

    Об этом вы можете почитать в приведенных ссылках:
    1. Файл README.EXT, который поставляеться с Ruby дистрибутивом.
    2. Глава Extending Ruby в знаменитом pickaxe
    3. Этот замечательный блог maxidoors.ru

    Для заинтересовавшихся, библиотека libdecodeqr: trac.koka-in.org/libdecodeqr
    Осторожно! Там все по японски:) Документацию можно посмотреть прямо в заголовочном файле:
    trac.koka-in.org/libdecodeqr/browser/tags/release-0.9.3/src/libdecodeqr/decodeqr.h

    В следующей части я расскажу про использование Ruby в качестве скриптового языка в C/C++ приложениях.
    Поделиться публикацией
    Похожие публикации
    Ой, у вас баннер убежал!

    Ну. И что?
    Реклама
    Комментарии 11
    • 0
      Помню читал про Rubinius — у них там был FFI-подход, который вроде бы и в JRuby используется, да и MRI/YARV поддерживает.
      • –1
        Да, FFI тоже можно использовать, и из Ruby 1.8 тоже.
        kenai.com/projects/ruby-ffi
        • НЛО прилетело и опубликовало эту надпись здесь
      • 0
        Кто реально писал свои расширения?
        • НЛО прилетело и опубликовало эту надпись здесь
        • +1
          Когда-то тоже писал на эту тему.
          Кому интересно, в дополнение: Введение в расширения Ruby на C
          • +1
            Спасибо за вторую часть! Всё супер. :-) Ждём продолжения по этой теме, а также по другим. ;-)
            • +1
              Я хоть и не Рубист, но статья отличная! ^_^ Успехов автору…
              P.S. Всмё больше начинаю заинтерисовываться Руби…
              • 0
                А как вернуть рубевское булево значение?
                • +1
                  Нужно использовать константы Qtrue и Qfalse (для nil — Qnil).
                  Например:

                  VALUE test(VALUE self) {
                  return Qfalse;
                  }

                  этот метод возвращает false.

                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                Самое читаемое