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

## はじめに


この記事は、magroという、Rubyのライブラリのソースコードを眺めて、RubyのC拡張をどのように作っていけばいいのか学ぶ記事です。

## おことわり
　この記事は、C言語もC拡張もよくわかっていない人間が適当に書いた記事です。
　自分の勉強を最優先に書かれており非常に低クオリティです。
　正確な情報が知りたい人は色々な資料を当たってくださいな！

## magroとは

magroとは、Rubyでpng画像やjpeg画像を行列にできるライブラリです。

* https://github.com/yoshoku/magro

Ruby向け総合機械学習ライブラリRumaleの作者であるyoxhokuさんが開発しています。

### magroの基本的な使い方

```ruby
require 'magro'
image = Magro::IO.imread('foo.png')
# => Numo::UInt8#shape=[512,512,3]
```

## ながめてみる

まずはgemspecを見ていきます。

## gemspec

```ruby
  spec.extensions    = ['ext/magro/extconf.rb']
```

おそらく、ポイントになるのは、ここだけ。拡張ライブラリを作成する場合は
`spec.extensions` で `extconf.rb` を指定してやればよいのでしょう。

## Rakefile

おそらく、bundlerによって自動生成されたものだと思われますが、いくつか注目すべきところはあります。
`Rake::ExtensionTask` を生成してブロックで、ディレクトリのパスを指定するようです。

```ruby
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英和辞典によると

https://ejje.weblio.jp/content/clobber
> 容赦なく(幾度も)打つ、殴り倒す、圧倒的に打ち負かす、大打撃を与える、(…を)痛めつける、ひどくしかる、酷評する

という意味だそうです。

## ext/magro

次に、本体の拡張ライブラリが入っているディレクトリを見ていきます。

### extconf.rb

```ruby
require 'mkmf'
```

mkmfを読み込んでいます。
`have_header` というメソッドがたくさん並んでいます。これでヘッダーファイルを検索できるようですね。

```ruby
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` を探している部分

```ruby
$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はるりまサーチで探してもあんまり出てこない気がする、ひょっとするともっと良い方法があるのかも知れません。）

* https://yoshoku.hatenablog.com/entry/2019/04/14/080000

拡張ライブラリを拡張する拡張ライブラリ…

```ruby
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と他のプラットフォームの差異を吸収しているものと思われます。

```ruby
create_makefile('magro/magro')
```

これがちょっと曲者で、`magro/mgaro` はファイルパスではないようです。

* https://docs.ruby-lang.org/ja/latest/method/Kernel/i/create_makefile.html

を見ると、/の手前が、ディレクトリ名で、/の後が Init_magro のmagroを意味しているようです。

`tree`

```
.
├── extconf.rb
├── imgrw.c
├── imgrw.h
├── magro.c
└── magro.h

```

### magro.h

```c
#ifndef MAGRO_H
#define MAGRO_H 1

#include <ruby.h>

#include "imgrw.h"

#endif /* MAGRO_H */
```

ここは MAGRO_H という定数を指定しているようですが、恐らく、2回同じファイルを読み込むことを防ぐためのおまじないだと思われます。

### magro.c

```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

```c
#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初心者には少しむずかしくなってきました。まず最初の、

```c
#ifndef MAGRO_IO_H
#define MAGRO_IO_H 1

#endif /* MAGRO_IO_H */
```

は2回読み込まないためのおまじないでしょう。次に、

```c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
```

は、標準ライブラリのヘッダファイルを読み込んでいると思われます。

```c
#include <setjmp.h>
#include <png.h>
#include <jpeglib.h>
```

はmagroで使うライブラリのヘッダファイルを読み込んでいます。

```c
#include <ruby.h>
#include <numo/narray.h>
#include <numo/template.h>
```

ここはRuby言語に関するヘッダファイルを読み込んでいます。

```h
void init_io_module();
```

ここは関数のプロトタイプ宣言でしょう。

### imgrw.c

ここは読みこなすのが難しいので、いくつかの関数を拾っていくことにします。

```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)などについてもっと詳しく学ぶ。