Пишем расширение на Си для Ruby, ч. 2

февраля 8, 2011  |  Published in Ruby, Расширения, Си и C++

ruby

Это вторая статья — перевод из двух оригинальных статей авторства , которые Василий любезно решил пожертвовать в архив RubyDev.

Пишем расширение на Си для Ruby, ч. 1
Пишем расширение на Си для Ruby, ч. 2

Во второй части мы изменим наш extconf.rb для того, что бы он мог найти необходимые файлы в libstree. Затем создадим класс на Ruby, который будет оберткой над структурой реализованной на Си.

Готовое расширение можно скачать на .

Использование mkmf для поиска библиотек

Как говорилось в предыдущей части, extconf.rb используется для поиска библиотек, заголовочных файлов и проверки сведений о целевой системе перед установкой. Сейчас мы научим extconf.rb находить библиотеку libstree вместе с ее заголовочными файлами. Также мы дадим пользователям возможность указывать gem’у, где находится libstree.

Настройка mkmf с помощью dir_config

Первое, что нам следует сделать, это указать mkmf, где следует изначально искать libstree. Для этого мы будем использовать метод dir_config, который принимает 3 аргумента:

* произвольная строка (обычно это имя библиотеки, в нашем случае «stree»)
* список путей, по которым надо искать заголовочные файлы
* список путей, по которым надо искать файлы библиотеки

Метод dir_config также позволяет пользователям, устанавливающим gem, указывать mkmf, где искать различные необходимые файлы. Давайте посмотрим на наш вызов dir_config(extconf.rb):

LIBDIR = Config::CONFIG['libdir']
INCLUDEDIR  = Config::CONFIG['includedir']

  HEADER_DIRS = [
  # First search /opt/local for macports
  '/opt/local/include',

  # Then search /usr/local for people that installed from source
  '/usr/local/include',

  # Check the ruby install locations
  INCLUDEDIR,

  # Finally fall back to /usr
  '/usr/include',
]

LIB_DIRS = [
  # First search /opt/local for macports
  '/opt/local/lib',

  # Then search /usr/local for people that installed from source
  '/usr/local/lib',

  # Check the ruby install locations
  LIBDIR,

  # Finally fall back to /usr
  '/usr/lib',
]

dir_config('stree', HEADER_DIRS, LIB_DIRS)

HEADER_FILES и LIB_DIRS содержат списки путей, по которым надо искать заголовочные файлы и файлы библиотеки. Эти настройки будут полезными для будущих пользователей, так как если у них libstree установлена в /opt/local/ или в /usr/local/, то gem установится без каких либо настроек пользователем.

Затем, мы вызываем dir_config, в который передаем строку «stree» и два списка. Этот вызов dir_config всего лишь настривает mkmf для поиска директорий. На самом деле, поиск мы еще не сделали. Вызов dir_config также позволяет пользователю настривать установку gem с помощью следующих параметров:

—with-stree-dir
—with-stree-include
—with-stree-lib

Поиск заголовочных файлов и библиотек

После настройки mkmf для поиска необходимых файлов, мы должны, собственно, искать заголовочные файлы и библиотеки. Делать мы это будем с помощью функций find_header и find_library.

Нам надо найти заголовочный файл stree/lst_string.h. Метод find_header будет выглядеть следующим образом:

unless find_header('stree/lst_string.h')
  abort "libstree is missing.  please install libstree"
end

Этот код указывает mkmf, что надо искать заголовочные файлы, которые нам нужны. Если они не находятся, то find_header возвращает false и мы можем прервать установку и указать каки-нибудь флаги. Если find_header выполняется успешно, то директория, где находятся заголовочные файлы, будет добавлена к флагу -I, который передается в компилятор.

Затем, нам надо найти библиотеку libstree. Для этого мы будем использовать функцию find_call:

unless find_library('stree', 'lst_stree_free')
  abort "libstree is missing.  please install libstree"
end

Эта функция принимает два параметра. Первый из них — это библиотека, которую мы должны слинковать. Эта строка будет передаваться -L флагу компилятора. Второй параметр — это то, что мы должны найти в библиотеке.

В этом примере, mkmf создает тестовую программу на C, которая пытается слинковать stree и найти там функцию lst_stree_free. Если линковка выполняется успешно, то путь добавляется к флагу -L, который передается компилятору. Если завершается с ошибкой, то мы прерываем установку и выводим сообщение об ошибке.

Конечный вариант extconf.rb можно найти .

Оборачиваем LST_String из libstree

libstree определяет структуру типа String. Мы напишем класс на Ruby, который обернет эту структуру. В итоге, мы сможем писать вот такой код:

string = STree::String.new 'foo'
assert_equal 3, string.length

Так как мы применяем концепцию TDD, то мы начнем работу с написания теста для метода length. Мы также напишем тест для проверки того, возвращают ли объекты, отличные от экземпляров класса String, исключение TypeError:

require 'stree'
require 'test/unit'

module STree
  class TestString < Test::Unit::TestCase
    def test_length
      string = STree::String.new 'foo'
      assert_equal 3, string.length
    end

    def test_type_error
      assert_raises(TypeError) do
        STree::String.new Object.new
      end
    end
  end
end

Структура файлов

В моих проектах на С я делаю под каждый класс свой файл. Так как нам нужна точка входа в программу, мы оставим файлы stree.c и stree.h из предыдущей части. Затем мы напишем stree_string.h и stree_string.c для нашего класса String.

Точка входа в библиотеку

Точка входа в нашу программу на С будет находиться в файле stree.c. Файл stree.c будет инициализировать класс String. Вот новый stree.h:

#ifndef RUBY_STREE
#define RUBY_STREE

#include <ruby.h>;
#include <stree/lst_string.h>;

#include <stree_string.h>;

extern VALUE mSTree;

#endif

Мы включили заголовочные файлы из libstree и заголовочный файл для класса String. Потом мы объявили глобальную переменную, в которой будет находиться ссылка на модуль STree, который реализован на Ruby.

Теперь код в файле stree.c выглядит так:

#include <stree.h>

VALUE mSTree;

void Init_stree()
{
  mSTree = rb_define_module("STree");

  Init_stree_string();
}

Когда подключается наша библиотека, то вызывается Init_stree. Затем, мы опишем модуль STree и инициализируем наш класс String. Сейчас мы должны определить Init_stree_string в stree_string.h и stree_string.c.

Создание класса String

Для начала мы создадим заголовочный файл для нашего класса String. Там у нас будет только одна public функция Init_stree_string:

#ifndef RUBY_STREE_STRING
#define RUBY_STREE_STRING

#include <stree.h>

void Init_stree_string();

#endif

Здесь мы подключили stree.h, потом определили функцию инициализации. Сейчас необходимо написать реализацию функции Init_stree_string в файле stree_string.c:


#include <stree_string.h>

void Init_stree_string()
{
  VALUE cSTreeString = rb_define_class_under(mSTree, "String", rb_cObject);
}

Функция rb_define_class_under будет определять класс String в модуле, на который указывает mSTree. Родителем модуля String будет являться класс Object. Вот эквивалент этого кода на Ruby:
[ruby]
module STree
class String
end
end
[ruby]

Сейчас мы должны скомпилировать проект и запустить тесты. Мы пока не определили ни один метод в рубишном классе STree::String, но наш проект все равно должен компилироваться.

Размещаем в памяти класс String

Первое, что мы должны сделать, это научить Ruby размещать наш класс. Ruby предоставляет возможность перехватить вызов метода allocate и разместить в памяти внешние структуры.

Для начала, мы модифицируем функцию инициализации (в файле stree_string.c):

void Init_stree_string()
{
  VALUE cSTreeString = rb_define_class_under(mSTree, "String", rb_cObject);

  rb_define_alloc_func(cSTreeString, allocate);
}

rb_define_alloc_func сообщает Ruby, что надо вызвать функцию allocate, когда происходит размещение этого класа. Определим эту самую функцию allocate (в файле stree_string.c):


static VALUE allocate(VALUE klass)
{
  LST_String * string = malloc(sizeof(LST_String));

  return Data_Wrap_Struct(klass, NULL, deallocate, string);
}

В этой функции мы выделяем столько памяти, сколько необхоимо для структуры LST_String. Потом мы вызываем функцию Data_Wrap_Struct, которая возвращает Ruby'новый объект. Эта функция принимает 4 аргумента:

* Класс реализаванный на Ruby с которым мы работаем
* Указатель на функцию, которая вызывается, когда объект помечается сборщиком мусора
* Указатель на функцию, которая вызывается, когда объект удаляется
* Указатель на данные, которые мы хотим обернуть

Определяем функцию dellocate, которая вызывается при уничтожении объекта:


static void deallocate(void * string)
{
    lst_string_free((LST_String *)string);
}

В эту функцию передается указатель на Data_Wrap_Struct, в нашем случает это указатель на LST_String. Затем мы используем функцию lst_string_free из libstree для освобождения этого указателя.

Определяем Stree::String#initialize

Сейчас мы должны определить метод initialize. Этот метод принимает один аргумент (строку). Также мы заполним структуру LST_String данными из Ruby'новой строки.

Перед определением метода initialize, мы вызываем метод rb_define_method(stree_string.c):


void Init_stree_string()
{
  VALUE cSTreeString = rb_define_class_under(mSTree, "String", rb_cObject);

  rb_define_alloc_func(cSTreeString, allocate);
  rb_define_method(cSTreeString, "initialize", initialize, 1);
}

rb_define_method принимает 4 аргумента:

* Класс, для которого мы определяем метод
* Имя метода
* Указатель на функцию, которая будет вызвана при вызове метода
* Количество параметров, которые будут передаваться в этот метод

Затем, мы определим функцию initialize(stree_string.c):


static VALUE initialize(VALUE self, VALUE rb_string)
{
  LST_String * string;
  void * data;

  Check_Type(rb_string, T_STRING);

  Data_Get_Struct(self, LST_String, string);

  data = calloc(RSTRING_LEN(rb_string), sizeof(char));
  memcpy(data, StringValuePtr(rb_string), RSTRING_LEN(rb_string));

  lst_string_init(
    string,
    data,
    sizeof(char),
    RSTRING_LEN(rb_string));

  return self;
}

Функция initialize принимает два параметра. Первый - экземпляр объекта STree::String, второй - параметр, передаваемый в наш метод.

После объявления переменных, мы проверяем их типы. Check_Type - это макрос, предоставляемый Ruby, который позволяет проверять типы объектов. Используем мы его для проверки того, что пользователь передал в качестве параметра Ruby'новую строку. Если это оказывается не строка, то макрос кидает исключение TypeError.

Затем, мы вызываем макрос Data_Get_Struct. Так как указатель LST_String сохранен внутри объекта VALUE, то макрос Data_Get_Struct занимается тем, что извлекает указатель из этого объекта. Мы передаем в Data_Get_Struct объект self, тип структуры, которую хотим извлечь (LST_String) и указатель, куда мы хотим ее поместить (string).

Затем, нам необходимо скопировать содержимое рубишной строки в буффер LST_String. Что бы это сделать, нужно:

* вызвать calloc для выделения памяти
* RSTRING_LEN возвращает длину нашей строки
* вызвать memcpy для копирования участка памяти
* вызвать StringValuePtr для получения указателя на рубишную строку

После этого, мы передаем данные в libstree с помощью вызова функции lst_string_init и потом возвращаем объект self.

На данном этапе у нас должен проходить один тест. Второй будет падать с ошибкой:


   1) Error:
test_length(STree::TestString):
NoMethodError: undefined method `length' for #
        ./test/test_stree_string.rb:8:in `test_length'

Далее, мы определим метод length.

Определяем STree::String#length

Тяжелая часть осталась позади. Определение метода length будет намного проще, чем определение метода initialize. Также, как и для метода инициализации, нам нужно вызвать rb_define_method:


void Init_stree_string()
{
  VALUE cSTreeString = rb_define_class_under(mSTree, "String", rb_cObject);

  rb_define_alloc_func(cSTreeString, allocate);
  rb_define_method(cSTreeString, "initialize", initialize, 1);
  rb_define_method(cSTreeString, "length", length, 0);
}

Мы определили метод length, который не принимает никаких аргументов, теперь опишем этот метод на C:


static VALUE length(VALUE self)
{
  LST_String * string;

  Data_Get_Struct(self, LST_String, string);

  return INT2NUM(lst_string_get_length(string));
}

Также, как и в функции initialize, мы объявляем переменные, в которые оборачивается наша структура. Для получения длины строки мы используем функцию lst_string_get_length из libstree, которая возвращает int. Для конвертации int в num, мы используем макрос INT2NUM.

Запустим тесты:


Loaded suite -e
Started
..
Finished in 0.000873 seconds.

2 tests, 2 assertions, 0 failures, 0 errors

Вот, собственно, и все. Ура!

Tags: , ,

Leave a Response

Для подсветки кода используйте BB - коды: [language]...[/language], где language может быть: ruby, javascript, css, html.