LoginSignup
1
1

More than 3 years have passed since last update.

RubyでC拡張を作りたいのでmagroプロジェクトをながめる

Posted at

はじめに

この記事は、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.extensionsextconf.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_moduleimgrw.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)などについてもっと詳しく学ぶ。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1