アクトインディ開発者ブログ

子供とお出かけ情報「いこーよ」を運営する、アクトインディ株式会社の開発者ブログです

社内エンジニア勉強会 ruby拡張ライブラリ

こんにちは、akiyamaです。

社内勉強会担当が回ってきました。

弊社で使用されている言語はrubyが主流なので、 今回はruby拡張ライブラリの書き方について発表しました。 ついでなのでcrystalで書きました。

サンプルとして竹内関数を拡張ライブラリ化しました。

(発表時点でcrsytalのバージョンは0.16.0です。 ここに書かれていることは、将来のバージョンでは使用できなくなる可能性があります)

以下、コードと解説です。

def tarai(x, y, z)
  if x <= y
    y
  else
    tarai(tarai(x - 1, y, z),
          tarai(y - 1, z, x),
          tarai(z - 1, x, y))
  end
end

lib Ruby
  $rb_cObject: Void*
  fun rb_define_class(name: LibC::Char*, value: Void*): Void*
  fun rb_define_method(klass: Void*,
                       name: LibC::Char*,
                       func: LibC::Int, LibC::ULong*, Void* -> LibC::ULong,
                       argc: LibC::Int)
  fun rb_fix2int(value: LibC::ULong) : LibC::Long
end

fun ext_tarai(argc: LibC::Int, args: LibC::ULong*, rb_self: Void*) : LibC::ULong
  x = Ruby.rb_fix2int(args[0])
  y = Ruby.rb_fix2int(args[1])
  z = Ruby.rb_fix2int(args[2])
  n = tarai(x, y, z).to_u64
  n << 1 | 0x01
end

fun init = Init_rubyext : Void
  GC.init
  LibCrystalMain.__crystal_main(0, Pointer(Pointer(UInt8)).null)
  klass = Ruby.rb_define_class("RubyExtCrystal", Ruby.rb_cObject)
  Ruby.rb_define_method(klass, "tarai", ->ext_tarai, -1)
end

lib宣言

呼びたいCの関数やグローバル変数をここで宣言する

fun rb_fix2int(value: LibC::ULong) : LibC::Long

は、Cで言うと

extern long rb_fix2int(unsigned long);

と同じ

ext_tarai

ruby -> cブリッジ部分

  • rubyから渡ってきた引数(VALUE)を整数値へと変換する
    • VALUERubyの世界で値を意味する型(ruby/ruby.h)
    • typedef uintptr_t VALUE; マシンワードサイズ
    • rubyはsizeof(void*) == sizeof(long) or sizeof(LONG_LONG)の環境でしか動かない
    • crystal内ではULong,Void*として取り扱う
  • tarai関数を呼び出す
    • 次のリターン用に型変換
  • 結果をFIXNUMに変換してリターン
    • INT2NUMなどのCマクロが使用できないので手動変換
    • rubyのFIXNUMは数値を1bit左シフトして最下位ビットを1にしたもの
    • いわゆるタグ付き
    • C:0b0011 (3) -> ruby:0b0111 (7)

Init_rubyext

rubyからload時に呼ばれる初期化関数

  • crystalのGCを初期化してcrystal初期化関数を呼ぶ(呼ばないとSEGV)
  • rb_define_classrubyの世界にRubyExtCrystalクラスを定義する
  • RubyExtCrystalにtaraiメソッドを定義する
    • ext_taraiをProc化してコールバック関数として登録する
    • 可変長関数を扱うのが面倒なのでarity -1指定して自分で展開する
      • argc, argsに引数が入る

build

crystal build --release --single-module --link-flags="-dynamic -bundle -lruby" -o rubyext.bundle rubyext.cr

rubyから呼び出してベンチマーク

require 'benchmark'
require_relative './rubyext'
ext = RubyExtCrystal.new
def tarai(x, y, z)
  if x <= y
    y
  else
    tarai(tarai(x - 1, y, z),
          tarai(y - 1, z, x),
          tarai(z - 1, x, y))
  end
end
x,y,z = ARGV.map(&:to_i)
Benchmark.bm 10 do |b|
  b.report 'ruby' do
    tarai(x, y, z)
  end
  b.report 'crystal' do
    ext.tarai(x, y, z)
  end
end
$ ruby tarai.rb 18 10 5
                 user     system      total        real
ruby         3.990000   0.010000   4.000000 (  3.999338)
crystal      0.130000   0.000000   0.130000 (  0.132147)

同等のコードをC言語で書く

#include <stdio.h>
#include <stdlib.h>
int tarai(int x, int y, int z)
{
  if (x <= y) {
    return y;
  } else {
    return tarai(
        tarai(x - 1, y, z),
        tarai(y - 1, z, x),
        tarai(z - 1, x, y));
  }
}
int main(int argc, char** argv)
{
  int x, y, z;
  x = atoi(argv[1]);
  y = atoi(argv[2]);
  z = atoi(argv[3]);
  printf("%d %d %d\n", x, y, z);
  printf("%d\n", tarai(x, y, z));
  retturn 0;
}
$ clang -O2 tarai.c
$ time ./a.out 18 10 5
18 10 5
18
./a.out 18 10 5  0.14s user 0.00s system 96% cpu 0.146 total

ほぼ、おんなじでした