Rubyの拡張ライブラリをCrystalで書く

# はじめに

CrystalはRubyによく似た文法でありながら静的型付けコンパイラ言語であり、高速に動作します。そうなるとRubyの拡張ライブラリをCrystalでかけたら良いのになと思ったりします。

[前回の記事](https://qiita.com/theanine/items/5b9e98f9091153a0f170)ではCrystalとC++の連携を解説しました。C++（C言語)とCrystalの関数・メソッドは相互にコールできます。一方でRubyの拡張ライブラリはC言語でかきます。このことから、C言語を介してRubyとCrystalの連携ができそうだなと想像されます。

この記事は、Rubyの拡張ライブラリをCrystalで書く例を解説します。

検証した環境

OS: MacOS
Crystal 1.6
Ruby 3.1

**[12/11追記]大規模開発では検証しておらず問題が発生する可能性もあります。最後の参考も合わせてみてください。**

# Rubyの拡張ライブラリの書き方

## Crystal側のコード

そもそもRubyの拡張ライブラリはC言語を前提としています。以下の公式ドキュメントなどを参照してある程度は仕組みを理解しておく必要がありますが、ここではサンプルを先に上げて、理解に必要なポイントをまとめていきます。

https://docs.ruby-lang.org/en/3.0/extension_ja_rdoc.html

```rubyext.cr
lib Ruby
  # RubyのVALUE
  type VALUE = Void*

  $rb_cObject : VALUE

  # グローバルメソッド定義
  fun rb_define_global_function(name : LibC::Char*, func : VALUE, VALUE -> VALUE, argc : Int32)
  # 整数の変換
  fun rb_num2int(value : VALUE) : Int32
  fun rb_int2inum(value : Int32) : VALUE
end

# Rubyから呼び出されるメソッド
# valueが引数
def fib_cr(selfobj : Ruby::VALUE, value : Ruby::VALUE)
  # Ruby整数 --> CrystalのInt32へ変換
  ini = Ruby.rb_num2int(value)
  # Crystal の Int32をRubyの整数に変換して戻す
  Ruby.rb_int2inum(fibonacci(ini))
end

# 実際にフィボナッチ数列を求める
def fibonacci(n)
  return n if n <= 1
  fibonacci(n - 1) + fibonacci(n - 2)
end

fun init = Init_rubyext : Void
  # GC初期化 
  GC.init
  # トップレベルコード実行
  LibCrystalMain.__crystal_main(0, Pointer(Pointer(UInt8)).null)
  # 引数が1個のメソッド"fib"を定義する
  Ruby.rb_define_global_function("fib", ->fib_cr, 1)
end
```

## Crystalから呼ぶC言語の関数を宣言
`lib Ruby`...の部分はCrystalから呼び出すC言語の関数を書きます。Ruby拡張はC言語で書くので、ここで必要なC言語の関数を記述します。この`lib`の記述については、
https://ja.crystal-lang.org/reference/syntax_and_semantics/c_bindings/index.html
や
https://qiita.com/theanine/items/5b9e98f9091153a0f170
などをごらんください。

まず
```crystal
  # RubyのVALUE
  type VALUE = Void*
```

RubyのオブジェクトであるVALUEはCrystalではVoid*なので、このような別名をつけています(Cでいうところのtypedef）。Rubyは動的言語なので、型は実行時に決まるため、すべてのデータの型はVALUE１つです。よってCrystal側でRubyから渡ってくるデータはすべてVALUEです。VALUEに型の情報などが含まれています。Crystal側で扱うにはRubyオブジェクトのCrystlaのオブジェクトに変換が必要です。

C言語でRuby拡張を書く場合は、C言語からは`rb_????`といった関数を使うことでRubyと連携します。以下のページにさまざまな関数の説明があります。
https://docs.ruby-lang.org/en/3.0/extension_ja_rdoc.html
使う関数をここに書いておく必要があります。今回使う３つの関数は
```c
void rb_define_global_function(const char *name, VALUE (*func)(ANYARGS), int argc)
long rb_num2int(VALUE)
VALUE rb_int2inum(long)
```
です。１つめはRubyのトップレベルメソッドを定義するための関数です。残り２つの関数はデータ変換用の関数です。Rubyの数値変換はマクロNUM2INTなどを使うようです。しかし、CrystalからはC言語のマクロは使えないため、マクロの定義を見に行って実際に呼び出している元の関数を探す必要があります。rubyのソースを見に行って探す必要がありました。

これらの型の情報をもとに以下のように関数を定義します。
```crystal
  # グローバルメソッド定義
  fun rb_define_global_function(name : LibC::Char*, func : VALUE, VALUE -> VALUE, argc : Int32)
  # 整数の変換
  fun rb_num2int(value : VALUE) : Int32
  fun rb_int2inum(value : Int32) : VALUE
```
今回はfuncの型は、定義するメソッドの引数に依存します。今回定義したいメソッドは引数１個なので`VALUE -> VALUE`な気がしますが、１個増やして`VALUE, VALUE -> VALUE`です。これはselfが渡ってくるためです。これは後で説明します。

https://docs.ruby-lang.org/ja/latest/function/NUM2INT.html
https://docs.ruby-lang.org/ja/latest/function/INT2NUM.html

## 初期化用関数「Init_ライブラリ名」を定義する

Rubyがライブラリを読み込んだときに呼び出される関数で、この中でメソッド定義やクラス定義などを行います。`rubyext`という名前にしたので`Init_rubyext`です。

Crystalの方の必要な初期化も兼ねています。[前回の記事](https://qiita.com/theanine/items/5b9e98f9091153a0f170)でも述べられているようにCrystal側はGCの初期化とCrystalのmain関数を呼ぶことが必須です。したがって、
```crystal
  GC.init
  LibCrystalMain.__crystal_main(0, Pointer(Pointer(UInt8)).null)
```
は書いておく必要があります。

それ以降にRuby拡張としての初期化をしていきます。ここでは、Rubyのクラスやメソッドなどを定義していきます。ここでトップレベルメソッドを定義しています。
```crystal
 Ruby.rb_define_global_function("fib", ->fib_cr, 1)
```
"fib"という名前のメソッドを定義しました。実態はfib_crで、`->`でメソッドをProc化したものを呼び出します。最後の引数は正の数を与える場合は、引数の個数となります。詳しくは以下を参照してください

https://docs.ruby-lang.org/ja/latest/function/rb_define_method.html
https://rurema.clear-code.com/3.2.0/function/rb_define_global_function.html

## Crystal側でのライブラリの中身

最後にfib_crを用意します。

```crystal
# Rubyから呼び出されるメソッド
# valueが引数
def fib_cr(selfobj : Ruby::VALUE, value : Ruby::VALUE)
  # Ruby整数 --> CrystalのInt32へ変換
  ini = Ruby.rb_num2int(value)
  # Crystal の Int32をRubyの整数に変換して戻す
  Ruby.rb_int2inum(fibonacci(ini))
end

# 実際にフィボナッチ数列を求める
def fibonacci(n)
  return n if n <= 1
  fibonacci(n - 1) + fibonacci(n - 2)
end
```

`fib_cr`がトップレベルメソッド`fib`の実態です。引数は１個のメソッドですが、`fib_cr`は２個引数があり、１つ目の引数は`self`を受け取るようにするようです。２つ目の引数が実質１つ目の引数となります。

`value`はRubyのオブジェクトなので、Crystalで使うには変換が必要です。`rb_num2int`でCrystalのInt32型の整数に変換します。

これを使って、Crystalの`fibonacci`メソッドをコールします。ここからはCrystalの世界です。普通に再帰で求めています。`fibonacci`の戻り値はInt32です。

Rubyの世界に戻すには、CrystalのInt32をRubyのオブジェクトつまり`VALUE`に変換する必要があり、`rb_int2inum`をコールしています。

以上で、Crystal側の準備はできました。

## ビルド

`makefile`は以下の通りです。

```makefile
TARGET = rubyext.bundle

LIB=(libruby.3.1.dylibとかがあるパス)

rubyext.bundle: rubyext.cr 
	crystal build --release --single-module --link-flags="-dynamic -bundle -L$(LIB) -lruby" -o rubyext.bundle rubyext.cr

clean:
	rm -f $(TARGET)
```

パスについては各自の環境にあわせてかきましょう。

```
make
```
でrubyext.bundleができます

## 実行

```test.rb
require_relative 'rubyext'
puts fib(10)
```
これで
```
ruby test.rb
55
```
とでれば成功です

# パフォーマンス
Rubyとの比較をしてみましょう

```ruby
require_relative 'rubyext'
require "benchmark"
def rb_fib(n)
  return n if n <= 1
  rb_fib(n - 1) + rb_fib(n - 2)
end

Benchmark.bm do |x|
  x.report { rb_fib(40) }
  x.report { fib(40) }
end
```

結果は以下のようになりました。
```
           user     system      total        real
Ruby     9.264494   0.011057   9.275551 (  9.276154)
Crystal  0.394152   0.000243   0.394395 (  0.394432)
```
確かに高速化できたことが確認できました。

ちなみに、メモ化なしのフィボナッチ数列の再帰による計算はRubyでもJITが効きやすいようで、
```
                   user     system      total        real
Ruby(--jitあり)   2.073805   0.001984   2.075789 (  2.078277)
```
となりました。

# オブジェクトの相互変換の改善

上記でとりあえず動くのですが、やはりRubyとのオブジェクトのやりとりが面倒です。なので、Ruby用のC関数を繋ぐメソッドやマクロをCrystal側で用意するのが良いだろうと思います。今回はこういうものを作ってみました。

```crystal
struct Int32
  def to_ruby
    Ruby.rb_int2inum(self)
  end
  def self.from_ruby(value : Ruby::VALUE)
    Ruby.rb_num2int(value)
  end
end
# valueが引数
def fib_cr(selfobj : Ruby::VALUE, value : Ruby::VALUE)
  # Ruby整数 --> CrystalのInt32へ変換
  ini = Int32.from_ruby(value)
  # Crystal の Int32 --> Rubyの整数に変換
  fibonacci(ini).to_ruby
end
```
Rubyのオブジェクトからの変換用の`Int32.from_ruby`とRubyへのオブジェクトへの変換の`.to_ruby`を作りました。多少はCrystal的(Ruby的）な記述で可読性は上がったと思います。

RubyとCrystalで相互変換できそうなオブジェクトはたくさんありますので、こういう変換メソッドを用意していくと書くのが楽になるだろうなと思います。

# おわりに
今回はサンプルコードレベルでRubyからCrystalを使ってみました。しかし、RubyからCrystalを呼び出すインターフェイス部分を書くのはかなり面倒です。何らかのサポートする仕組みをCrystal側で用意する必要がありあそうです。今回はその１つの方法の例としてRuby,Crystalのオブジェクトの相互変換メソッドを作ってみました。メソッドの変換もサポートするマクロが作れそうな気がしていますが、まだできていません。

その点、[wasmを使う方法](https://qiita.com/kojix2/items/b233f1419b26f7fc0e1b)は、RubyオブジェクトとCrystalオブジェクト間の相互変換するところを面倒見なくてよいみたいなのでとても楽そうです。速度も結構速くて有望な方法に見えます。

以上で、この記事を終わります。

# 参考
本記事の執筆には以下の記事を参考にしました。

CrystalでRuby拡張を書く`proof of concept'。今回もっとも参考にしました。Rubyへのオブジェクト変換のメソッドなどもあって、この方針でCrystal側でライブラリを整備していくのが筋が良さそう。
https://github.com/manastech/crystal_ruby

CrystalをC++から使う
https://qiita.com/theanine/items/5b9e98f9091153a0f170

Ruby拡張をCrystalで書く例
https://tech.actindi.net/2016/05/12/rubyext.html
https://gist.github.com/notozeki/7159a9d9ab9707a22129

CrystalでRuby拡張の書き方解説
https://www.slideshare.net/AnnaKazakova/how-to-write-ruby-extensions-with-crystal
https://www.slideshare.net/5t111111/writing-ruby-extension-with-crystal

CrystalでRuby拡張を書くための支援。便利そうだが2022年12月現在、更新が止まっている。
https://github.com/phoffer/crystalized_ruby

CrystalでWebAssemblyに出力した関数をRubyから呼び出す
https://qiita.com/kojix2/items/b233f1419b26f7fc0e1b

[追記12/11]C#の場合のRubyとの連携。GCは独立して動いてしまう問題が述べられています。Crystalでも当てはまるかもしれません。
https://magazine.rubyist.net/articles/0021/0021-RubyWithCSharp.html