はじめに
この記事は、magroという、Rubyのライブラリのソースコードを眺めて、RubyのC拡張をどのように作っていけばいいのか学ぶ記事です。
おことわり
この記事は、C言語もC拡張もよくわかっていない人間が適当に書いた記事です。
自分の勉強を最優先に書かれており非常に低クオリティです。
正確な情報が知りたい人は色々な資料を当たってくださいな!
magroとは
magroとは、Rubyでpng画像やjpeg画像を行列にできるライブラリです。
Ruby向け総合機械学習ライブラリRumaleの作者であるyoxhokuさんが開発しています。
magroの基本的な使い方
require 'magro'
image = Magro::IO.imread('foo.png')
# => Numo::UInt8#shape=[512,512,3]
ながめてみる
まずはgemspecを見ていきます。
gemspec
spec.extensions = ['ext/magro/extconf.rb']
おそらく、ポイントになるのは、ここだけ。拡張ライブラリを作成する場合は
spec.extensions
で extconf.rb
を指定してやればよいのでしょう。
Rakefile
おそらく、bundlerによって自動生成されたものだと思われますが、いくつか注目すべきところはあります。
Rake::ExtensionTask
を生成してブロックで、ディレクトリのパスを指定するようです。
require 'rake/extensiontask'
task build: :compile
Rake::ExtensionTask.new('magro') do |ext|
ext.lib_dir = 'lib/magro'
end
rake -T
した時の様子
rake clobber # Remove any generated files
rake compile # Compile all the extensions
rake compile:magro # Compile magro
ちなみにclobberという聞き慣れない単語は、Weblio英和辞典によると
容赦なく(幾度も)打つ、殴り倒す、圧倒的に打ち負かす、大打撃を与える、(…を)痛めつける、ひどくしかる、酷評する
という意味だそうです。
ext/magro
次に、本体の拡張ライブラリが入っているディレクトリを見ていきます。
extconf.rb
require 'mkmf'
mkmfを読み込んでいます。
have_header
というメソッドがたくさん並んでいます。これでヘッダーファイルを検索できるようですね。
unless have_header('setjmp.h')
puts 'setjmp.h not found.'
exit(1)
end
unless have_header('png.h')
puts 'png.h not found.'
exit(1)
end
unless have_header('jpeglib.h')
puts 'jpeglib.h not found.'
exit(1)
end
unless have_library('png')
puts 'libpng not found.'
exit(1)
end
unless have_library('jpeg')
puts 'libjpeg not found.'
exit(1)
end
narray.h
を探している部分
$LOAD_PATH.each do |lp|
if File.exist?(File.join(lp, 'numo/numo/narray.h'))
$INCFLAGS = "-I#{lp}/numo #{$INCFLAGS}"
break
end
end
unless have_header('numo/narray.h')
puts 'numo/narray.h not found.'
exit(1)
end
ここは、ちょっと面倒くさいことをしている気がします。INCFLAGS
はインクルーディングフラグを意味していると思われます。Numo::NArrayのヘッダーファイルをincludeパスに含めるためにこのようなことをしているようです。(INCFLAGSはるりまサーチで探してもあんまり出てこない気がする、ひょっとするともっと良い方法があるのかも知れません。)
拡張ライブラリを拡張する拡張ライブラリ…
if RUBY_PLATFORM =~ /mswin|cygwin|mingw/
$LOAD_PATH.each do |lp|
if File.exist?(File.join(lp, 'numo/libnarray.a'))
$LDFLAGS = "-L#{lp}/numo #{$LDFLAGS}"
break
end
end
unless have_library('narray', 'nary_new')
puts 'libnarray.a not found.'
exit(1)
end
end
この部分はWindowsと他のプラットフォームの差異を吸収しているものと思われます。
create_makefile('magro/magro')
これがちょっと曲者で、magro/mgaro
はファイルパスではないようです。
を見ると、/の手前が、ディレクトリ名で、/の後が Init_magro のmagroを意味しているようです。
tree
.
├── extconf.rb
├── imgrw.c
├── imgrw.h
├── magro.c
└── magro.h
magro.h
#ifndef MAGRO_H
#define MAGRO_H 1
#include <ruby.h>
#include "imgrw.h"
#endif /* MAGRO_H */
ここは MAGRO_H という定数を指定しているようですが、恐らく、2回同じファイルを読み込むことを防ぐためのおまじないだと思われます。
magro.c
#include "magro.h"
VALUE mMagro;
void Init_magro()
{
mMagro = rb_define_module("Magro");
init_io_module();
}
ここでは、Magro
というモジュールをトップレベルに定義していると思われます。init_io_module
は imgrw.h
で定義されている関数です。
imgrw.h
#ifndef MAGRO_IO_H
#define MAGRO_IO_H 1
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <setjmp.h>
#include <png.h>
#include <jpeglib.h>
#include <ruby.h>
#include <numo/narray.h>
#include <numo/template.h>
void init_io_module();
#endif /* MAGRO_IO_H */
C初心者には少しむずかしくなってきました。まず最初の、
#ifndef MAGRO_IO_H
#define MAGRO_IO_H 1
#endif /* MAGRO_IO_H */
は2回読み込まないためのおまじないでしょう。次に、
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
は、標準ライブラリのヘッダファイルを読み込んでいると思われます。
#include <setjmp.h>
#include <png.h>
#include <jpeglib.h>
はmagroで使うライブラリのヘッダファイルを読み込んでいます。
#include <ruby.h>
#include <numo/narray.h>
#include <numo/template.h>
ここはRuby言語に関するヘッダファイルを読み込んでいます。
void init_io_module();
ここは関数のプロトタイプ宣言でしょう。
imgrw.c
ここは読みこなすのが難しいので、いくつかの関数を拾っていくことにします。
void init_io_module()
{
VALUE mIO = rb_define_module_under(mMagro, "IO");
rb_define_module_function(mIO, "read_png", magro_io_read_png, 1);
rb_define_module_function(mIO, "save_png", magro_io_save_png, 2);
rb_define_module_function(mIO, "read_jpg", magro_io_read_jpg, 1);
rb_define_module_function(mIO, "save_jpg", magro_io_save_jpg, -1);
}
ここでは、恐らくMagroモジュール以下にIOモジュールを作成して、そこに各種のメソッドを登録し、対応する関数を指定していると思われます。最後の数値ですが、
- 引数の数が固定されている場合、引数の数( レシーバー)
- C配列内の可変数の引数の場合、 -1
- Ruby配列内の可変数の引数の場合、 -2
らしいですが、まだよくわかっていません。
RUBY_EXTERN VALUE mMagro;
このRUBY_EXTERNもよくわかっていません。
- rb_raise 例外を発生するようです
- rb_narray_new これは恐らく NArray.new のようなCの関数でしょう
- RB_GC_GUARD これは何でしょうね、GCから守る関数でしょうか
- rb_funcall
- rb_intern
- rb_scan_args
このあたりはnarrayに関する関数でしょうかね…
- GetNArray
- NA_NDIM
- NA_SHAPE
ここらはReturnに使われています。
- Qtrue
- Qfalse
- Qnil
まとめ
GemspecやRakefile、extconf.rbなどのプロジェクトに必要なファイルの構成を眺めた。
次回は、C拡張に必要なRubyの関数(C API)などについてもっと詳しく学ぶ。