Статьи

Code Safari: расширение Ruby с помощью C

GitHub недавно выпустил новую библиотеку Ruby для анализа Markdown: Redcarpet . Это обертка вокруг библиотеки C Upskirt , которая заставила меня задуматься: как ты вообще пишешь расширение C для Ruby? Для тех из нас, кто привык к плюшевому комфорту земли Руби, спуск к С — пугающая перспектива. Призрак сегфоутов преследует границу, заставляя неподготовленных путешественников повернуть назад при первом взгляде на точку с запятой.

Что если это не так сложно? Возможно, немного мужества, немного сердца и мозгов мы сможем открыть для себя возможность написания расширения на Си.

Первые шаги

Мой первый следственный ход всегда один и тот же: клонировать репозиторий и запустить rake умолчанию.

 $ git clone git://github.com/tanoku/redcarpet.git $ cd redcarpet $ rake 

В хорошо сделанном геме (как этот) он запустит полный набор тестов. В этом случае мы также видим в выводе, что некоторый C-код компилируется ранее. Мы можем покопаться в Rakefile чтобы узнать больше.

 # excerpt from redcarpet/Rakefile DLEXT = Config::MAKEFILE_CONFIG['DLEXT'] RUBYDIGEST = Digest::MD5.hexdigest(`#{RUBY} --version`)   file "ext/ruby-#{RUBYDIGEST}" do |f| rm_f FileList["ext/ruby-*"] touch f.name end CLEAN.include "ext/ruby-*"   file 'ext/Makefile' => FileList['ext/*.{c,h,rb}', "ext/ruby-#{RUBYDIGEST}"] do chdir('ext') { ruby 'extconf.rb' } end CLEAN.include 'ext/Makefile', 'ext/mkmf.log'   file "ext/redcarpet.#{DLEXT}" => FileList["ext/Makefile"] do |f| sh 'cd ext && make clean && make && rm -rf conftest.dSYM' end CLEAN.include 'ext/*.{o,bundle,so,dll}'   file "lib/redcarpet.#{DLEXT}" => "ext/redcarpet.#{DLEXT}" do |f| cp f.prerequisites, "lib/", :preserve => true end   desc 'Build the redcarpet extension' task :build => "lib/redcarpet.#{DLEXT}" 

При использовании rake только для запуска задач с зависимостями легко забыть, что изначально он был задуман как версия make для Ruby. Как таковой, он предоставляет методы, отличные от task которые пригодятся для строительных проектов. file является одним из таких методов, создавая задачу, которая генерирует именованный файл.

 $ rake ext/Makefile $ ls ext/Makefile ext/Makefile # it exists! 

Следуя цепочке зависимостей в приведенном выше коде, мы можем извлечь основные шаги, необходимые для компиляции расширения C:

 cd ext ruby extconf.rb make 

make является стандартным для сборки практически любого кода на C, но extconf.rb является новым.

 # redcloth/ext/extconf.rb require 'mkmf'   dir_config('redcarpet') create_makefile('redcarpet') 

Документация для mkmf подтверждает, что мы ожидаем, что этот код будет делать:

модуль для создания Makefile для модулей расширения

Предположительно это компилирует файл с таким же именем redcarpet.c , так что давайте посмотрим на это, и у нас должны быть redcarpet.c для создания нашего собственного расширения.

 // excerpt from redcarpet/ext/redcarpet.c #include <stdio.h> #include "ruby.h" &nbsp; static VALUE rb_cRedcarpet; &nbsp; // ... &nbsp; static VALUE rb_redcarpet_toc(int argc, VALUE *argv, VALUE self) { // ... } &nbsp; static VALUE rb_redcarpet_to_html(int argc, VALUE *argv, VALUE self) { // ... } &nbsp; void Init_redcarpet() { rb_cRedcarpet = rb_define_class("Redcarpet", rb_cObject); rb_define_method(rb_cRedcarpet, "to_html", rb_redcarpet_to_html, -1); rb_define_method(rb_cRedcarpet, "toc_content", rb_redcarpet_toc, -1); } 

Это не выглядит слишком страшно. Метод Init_redcarpet устанавливает класс, а затем добавляет к нему несколько методов, которые отображаются на другие функции, определенные в файле. Мы можем сделать это. На самом деле, давайте!

Расширение нашего собственного

В духе выполнения самой простой вещи, давайте напишем расширение C, которое просто складывает числа вместе. Функционально, возможно, бесполезно, но обучение полезно ! Из нашего исследования Redcarpet мы знаем, что для создания нашего расширения требуется всего три шага:

  1. Написать функцию Init в C-коде.
  2. Создайте файл extconf.rb который может создать Makefile для компиляции этого C-кода.
  3. Запустите Makefile, чтобы скомпилировать расширение.

Конечно, есть и скрытый четвертый шаг: проверьте, все ли работает!

Прежде всего, код C:

 // fastadd/fastadd.c #include "ruby.h" &nbsp; static VALUE rb_cFastadd; &nbsp; void Init_fastadd() { rb_cFastadd = rb_define_class("Fastadd", rb_cObject); } 

Тогда немного extconf:

 # fastadd/extconf.rb require 'mkmf' &nbsp; dir_config('fastadd') create_makefile('fastadd') 

Затем соберите и протестируйте

 $ ruby extconf.rb Creating Makefile $ make gcc # ... $ ruby -r./fastadd -e 'puts Fastadd.new' #<Fastadd:0x0000010086dde0> 

Bonanza! Это настоящий класс Ruby, полностью определенный в C. Я начинаю волноваться. Давайте вернемся к коду Redcarpet, чтобы посмотреть, что еще мы можем сделать.

Снова красный ковер

Первая реальная логика, которая появится в redcarpet.c заключается в следующем в методе rb_redcarpet__render :

 VALUE text = rb_funcall(self, rb_intern("text"), 0); 

Обратите внимание, что «intern» — это имя в Ruby для «преобразовать в символ». В этом контексте приведенный выше код выглядит довольно просто: вызовите метод экземпляра с именем «text». Но текст не определен больше нигде в файле! Если вы попытаетесь запустить этот код в нашем расширении Fastadd, вы увидите, что он не предусмотрен по умолчанию (вы получите ошибку метода not found). Redcarpet, должно быть, делает некоторые другие махинации, и если его нет в этом файле, он должен быть где-то еще. Вернитесь в корневой каталог redcarpet и grep для «text». Игнорируя множество совпадений в каталоге test , он обнаруживает другое определение класса Redcarpet в lib/redcarpet.rb .

 class Redcarpet # Original Markdown formatted text. attr_reader :text &nbsp; def initialize(text, *extensions) @text = text extensions.each { |e| send("#{e}=", true) } end end &nbsp; &nbsp; require 'redcarpet.so' 

Как дерзко! Это совсем не чистое упражнение С. Redcarpet определяет класс в Ruby, затем переопределяет (или добавляет к нему) в C-земле. Открытые занятия на работе. Это действительно удобно: мы не только можем легко настроить большую часть нашего класса в Ruby, мы даже можем предоставить реализацию Ruby по умолчанию, а затем переопределить ее позже в нашем C-коде.

Давайте добавим это в наш класс Fastadd , так как ему нужно будет знать, к какому числу добавить.

 # fastadd/fastadd.rb class Fastadd attr_reader :n &nbsp; def initialize(n) @n = n end end &nbsp; require './fastadd.so' 

Реальная работа

Давайте сделаем так, чтобы наш класс Fastadd мог добавить один к числу. Молниеносно! Вы заметили обилие типов VALUE в коде Redcarpet C — они представляют объекты Ruby. Нам нужен способ конвертировать их в нативные типы Си, чтобы мы могли иметь дело с ними естественным образом на земле Си. Я не был уверен, как это сделать, но поиск «ruby c extension convert VALUE to int» привел меня к этой красивой шпаргалке, в которой перечислены некоторые макросы и функции для выполнения именно этого преобразования. NUM2INT выглядит особенно удобным для наших целей.

 // fastadd/fastadd.c #include "ruby.h" &nbsp; static VALUE rb_cFastadd; &nbsp; static VALUE rb_fastadd_add_one(int argc, VALUE *argv, VALUE self) { int n = NUM2INT(rb_funcall(self, rb_intern("n"), 0)); &nbsp; return INT2NUM(n + 1); } &nbsp; &nbsp; void Init_fastadd() { rb_cFastadd = rb_define_class("Fastadd", rb_cObject); rb_define_method(rb_cFastadd, "add_one", rb_fastadd_add_one, -1); } 

Опробовать это:

 $ make # gcc output $ ruby -r./fastadd -e 'puts Fastadd.new(3).add_one' 4 

Победитель.

Завершение

Конечно, это только начало того, что вы можете делать с расширениями Си. Вот некоторые дополнительные упражнения, которые вы можете выполнить, чтобы отточить свои навыки:

  • Мы обработали код Redcarpet и в итоге определили #add_one как метод с переменным числом аргументов. Правильно определите его как метод, который не принимает аргументов.
  • Доступ к переменной экземпляра @n осуществляется через метод reader. Вместо этого обращайтесь к нему напрямую.
  • Исследуйте, какую дополнительную работу Redcarpet должен выполнить в своей гемспецификации, чтобы включить расширение C в состав гема.

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