Пишем расширение на Си для Ruby, ч. 1
февраля 5, 2011 | Published in Ruby, Основы, Расширения, Си и C++ | 2 Comments
Пишем расширение на Си для Ruby, ч. 1
Пишем расширение на Си для Ruby, ч. 2
Вчера на почтовый ящик мне пришло письмо от читателя блога RubyDev , к которому был приложен перевод понравившейся ему статьи авторства небезызвестного в Ruby сообществе (известного также под псевдонимом tenderlove). Большое спасибо Василию за приложенные усилия и за помощь!
А теперь собственно статья:
Мне нравится писать расширения для Ruby на C. В этом цикле статей мы разберемся, как это делать. Я раскрою такие темы, как создание рабочего окружения для написания расширения, TDD, отладку, взаимодействие со сборщиком мусора, кросс-компиляцию для windows и т.д.
К концу серии постов мы должны написать расширение, которое будет оберткой для libstree. libstree - это реализация суффиксного дерева на C.
В этой части мы подготовим все, что нам нужно для разработки, изучим шаблон обычного расширения на C и реализуем наш первый метод на C. Естественно, все это мы будем делать используя TDD и autotest.
Необходимые gem’ы
Для начала нам надо установить несколько джемов, которые сделают разработку расширения проще.
$ sudo gem install ZenTest hoe rake-compiler
ZenTest
ZenTest содержит в себе autotest, который мы будем использовать для автоматического запуска тестов при разработке.
hoe
Hoe используется для создания gemspec и предоставляет несколько rake задач, которые упрощают разработку.
rake-compiler
Этот gem предоставляет rake задачи для компиляции, тем самым упрощая сборку gem’ов. Работа с ним будет рассмотрена в следующих частях.
Создание проекта
Наш gem будет называться stree. Для его создания необходимо выполнить в консоли следующую команду:
$ sow stree
После этого мы должны удалить директорию «bin», т.к. нам для этого примера она не понадобится. Я переименовал все мои файлы с доками в rdoc, но делать это вовсе необязательно.
Пишем первый тест
Первое, что нам нужно сделать, это написать первый тест для пока несуществующего функционала, который естественно при запуске сообщит нам о неудаче тестирования. Для этого открываем файл test/test_stree.rb и делаем его таким:
require "test/unit" require "stree" class TestStree < Test::Unit::TestCase def test_hello_world assert_equal 'hello world', Stree.hello_world end end
Это очень просто тест, проверяющий работу метода hello_world, который будет реализован на C. Сейчас вы должны выполнить команду rake и увидеть сообщение, которое вывел тест.
Шаблон расширения
Шаблоны расширений очень похожи на шаблоны библотек, написанных на Ruby. В случае расширения, мы просто добавим директорию ext. В ней мы создадим еще дну директорию, которую назовем так же, как наш gem — stree. Директори ext/stree — это место, где мы будем хранить весь код на Си. В итоге должна получиться следующая структура директорий:
$ tree . |-- CHANGELOG.rdoc |-- Manifest.txt |-- README.rdoc |-- Rakefile |-- ext | `-- stree |-- lib | `-- stree.rb `-- test `-- test_stree.rb
Измененяем Rakefile
Следующий шаг — изменение файла Rakefile. После наших модификаций у нашего Rakefile появится Rake задача rake compile, которая будет выполнять сборку расширения.
Rakefile:
require 'rubygems' require 'hoe' Hoe.spec 'stree' do developer('Aaron Patterson', 'aaron@tenderlovemaking.com') self.readme_file = 'README.rdoc' self.history_file = 'CHANGELOG.rdoc' self.extra_rdoc_files = FileList['*.rdoc'] self.extra_dev_deps << ['rake-compiler', '>= 0'] self.spec_extras = { :extensions => ["ext/stree/extconf.rb"] } Rake::ExtensionTask.new('stree', spec) do |ext| ext.lib_dir = File.join('lib', 'stree') end end Rake::Task[:test].prerequisites << :compile
Я изменил секции readme и history файлов для того, чтобы использовать свои имена файлов. Важными частями этого Rakefile являются строки: spec_extras, Rake::Task, Rake::ExtensionTask.
Параметр spec_extra используется для модификации gemspec’a. Когда кто-нибудь устанавливает наш gem, эта строка указывает на то, что надо выполнить файл ext/stree/extconf.rb Мы поговорим о файле extconf.rb немного позже.
Rake::ExtensionTask — это строка, где мы получаем задачу rake compile. Rake::ExtensionTask находится в gem’е rake-compiler. Этот блок также настраивает путь сохранения скомпилированного расширения.
Последняя строка сообщает Rake’у, что перед запуском тестов нужно скомпилировать расширение. Некоторые разработчики возможно не захотят это использовать, но лично мне нравится компилировать расширения перед запуском тестов.
Настраиваем autotest
Autotest не использует обычные rake задачи, при выполнении ваших тестов. Это значит, что мы должны научить autotest компилировать наше расширение перед запуском тестов. Для этого мы перехватываем run_command в autotest и собираем расширение перед запуском тестов.
Также мы научим autotest запускать тесты после любой модификации .c файлов.
Откройте файл .autotest и впишите туда следующий код:
require 'autotest/restart' Autotest.add_hook :initialize do |at| at.add_mapping(/.*\.c/) do |f, _| at.files_matching(/test_.*rb$/) end end Autotest.add_hook :run_command do |at| system "rake clean compile" end
Запустите autotest и пусть он работает в фоне. К концу этого поста autotest должен будет показать один успешный тест, ну а сейчас мы должны увидеть следующее сообщение:
rake aborted!
Don’t know how to build task ‘ext/stree/extconf.rb’
Давайте разберемься с этой ошибкой.
extconf.rb
extconf.rb отвечает за генерацию makefile, который будет использоваться для сборки расширения. Также мы должны научить extconf.rb тому, как выбирать целевую систему, что бы быть уверенными, что библиотека libstree установлена.
Сейчас нам не надо делать каких-либо проверок системы. Мы всего лишь хотим создать Makefile. Для сборки нашего Makefile мы будем использовать mkmf. Открываем файл ext/stree/extconf.rb и делаем его таким:
require 'mkmf' create_makefile('stree/stree')
Это минимальный код, который необходим для генерации нашего Makefile.
Как я заметил ранее, extconf.rb выполняется системой RubyGems, когда устанавливается наш gem. Пока мы разрабатываем gem, rake-compiler позаботится о запуске этого файла.
Наш первый код на C
Великолепно! Теперь наше окружение знает, как компилировать, однако у нас все еще нету того, что нужно скомпилировать! Пришло время написато код на Си.
Создадим файл ext/stree/stree.c. Имя файла должно быть именно таким. Это завист от строки «create_makefile«, которая находится в файле extconf.rb. После сборки расширения у нас появится файл lib/stree/stree.dylib (расширение файла также может быть .so, в зависимости от используемой операционной системы). Это очень важное соглашение, но я расскажу об этом в следующих постах.
Когда Ruby загружает динамическую библиотеку, происходит сборка. Когда stree.dylib загружена, Ruby автоматически пытается вызвать функцию Init_stree. Вторая часть имени функции совпадает с именем файла, который загружен. В функции Init_stree происходит инициализация нашего расширения.
stree.c должен выглядеть следующим образом:
#include <ruby.h>
void Init_stree()
{
VALUE mStree = rb_define_module("Stree");
rb_define_singleton_method(mStree, "hello_world", hello_world, 0);
}
Эта функция делает две вещи: определяет модуль "Stree" и метод "hello_world".
Первая строка определяет модуль, а вторая говорит руби определить singleton-метод "hello_world". И когда этот метод вызывается, то вызывается указательно на си функцию "hello_world". Параметр 0 определяет количество аргументов этой функции.
Давайте добавим функцию hello_world на С:
static VALUE hello_world(VALUE mod) { return rb_str_new2("hello world"); }
Мы определили функцию как статическую, т.к она не будет вызываться за пределами этого файла. Все методы Ruby должны возвращать значение VALUE. Первый аргумент функции на Си - это всегда получатель сообщения, в нашем случае модуль Stree.
Функцией rb_str_new2() мы создаем новую строку и возвращаем ее.
Конечная вид файла stree.c:
#include <ruby.h> static VALUE hello_world(VALUE klass) { return rb_str_new2("hello world"); } void Init_stree() { VALUE mStree = rb_define_module("Stree"); rb_define_singleton_method(mStree, "hello_world", hello_world, 0); }
Замечание по типам
Когда мы пишем на Ruby, то абсолютно все в этом языке является объектом. Когда мы пишем расширение для Ruby на Си, то все является VALUE. Я расскажу больше о VALUE в следующих статьях этого цикла статей о написании расширений на Си.
Заключение
Мы написали код на Си. Теперь все должно компилироваться и копироваться в нужное место. Но наши тесты все равно падают. Почему?
Мы должны сделать еще одну маленькую правку. Нам необходимо явно подключить динамическую библиотеку, которую мы собрали. Открываем файл lib/stree.rb и пишем:
require 'stree/stree' module Stree VERSION = '1.0.0' end
Тем самым мы говорим руби загружать динамическую библиотеку. И теперь наши тесты проходят успешно. Поздравляю! Вы успешно объединили код на C с кодом на Ruby!
В итоге дерево проекта должно выглядеть следующим образом:
$ tree -I tmp . |-- CHANGELOG.rdoc |-- Manifest.txt |-- README.rdoc |-- Rakefile |-- ext | `-- stree | |-- extconf.rb | `-- stree.c |-- lib | |-- stree | | `-- stree.bundle | `-- stree.rb `-- test `-- test_stree.rb
tmp - это директория, где rake-compiler хранит .o файлы, когда компилирует расширение.
Оригинал статьи на английском можно найти здесь:
Лучшая блогадарность автору блога - ваши комментарии!
февраля 6, 2011 at 01:28 (#)
Вот что бывает, когда нужная статья попалась в нужное время — как раз этим вопросом к диплому решил поинтересоваться в связи с парой идей. В итоге сон отложился до 5 утра
Из полезного — мега-утилита tree, почему я раньше ее не знал?! :)
Ну и плюс в статье написано гладно, а у меня все пошло несколько через задницу — я не знаю, виноват ли в этом RVM :) Сначала sow глючил, не мог почему-то скопировать в .hoe_extensions стандартную папку, пришлось руками делать, а потом в Rakefile указать require ‘rake/extensiontask’
Но за ценное указание спасибо :)
февраля 6, 2011 at 11:41 (#)
Да, проблема с темплейтами была. В версии hoe 2.9.1 ее исправили.