Array#both_endの使いどころを考える

はじめに

Array#minimaxというメソッドがRuby 2.4から導入されていて、そのメソッドの実装を読んでいた時に思いついたArray#both_endメソッドの実用例についてつらつらと書いてみた記事。

なので、実際にRubyにそういうメソッドがあるわけではない。

実装

/*
 *  call-seq:
 *     ary.both_end     ->  new_ary
 *     ary.both_end(n)  ->  new_ary
 *
 *  Returns the last element(s) of +self+. If the array is empty,
 *  the first form returns +nil+.
 *
 *  See also Array#first for the opposite effect.
 *
 *     a = [ "w", "x", "y", "z" ]
 *     a.both_end     #=> ["w", "z"]
 *     a.both_end(2)  #=> [["w", "x"], ["y", "z"]
 *     [].both_end    #=> [nil, nil]
 */

static VALUE
rb_ary_both_end(int argc, VALUE *argv, VALUE ary)
{
    VALUE first, last;

    rb_check_arity(argc, 0, 1);

    if (RARRAY_LEN(ary) == 0)
        return rb_assoc_new(Qnil, Qnil);

    first = rb_ary_first(argc, argv, ary);
    last = rb_ary_last(argc, argv, ary);

    return rb_assoc_new(first, last);
}

実装はこんな感じで、内部のコードではArray#firstArray#lastを呼び出してる。最後にrb_assoc_newで新しくArrayを作って返している。

空のArrayの場合は[nil, nil]が返えるようになっている。

現状の使いどころ

Arrayの両端の値を指定の数だけとってきたい時に使う感じ

ary = [1, 2, 3, 4, 5]

result = ary.both_end(2)

puts result.first == [1, 2]
# => true

puts result.last == [4, 5]
# => true

あとはこんな感じでArrayの両端を使った繰り返し書くときとか?

ary = [1, 2, 3, 4, 5]

result = ary.both_end(2)

result[0].map{|a1|
  result[1].map{|a2|
    puts a1 == a2
  }
}

Railsとかで使う場合は、最新と最古のデータとってきたい時に一行で書けるかもしれない?

@result = Topic.both_end(3)
# => 最新と最古のデータを三件づつとってくるイメージ

現状の問題点?

内部の実装でArray#firstArray#lastを使っているのでこういうコードはエラーにならない。

ary = [1, 2, 3, 4, 5]

result = ary.both_end(10)
# => [[1, 2, 3, 4, 5], [1, 2, 3, 4, 5]]

エラーになった方が自然なのかどうなのかは判断できなかったので、そのままにしてるけどエラーのほうが良いのだろうか? まあ、要素が同じArrayを複数持つことができるのでエラーにならない方が恩恵ありそうかも。

ary = [1, 2, 3, 4, 5]

result = ary.both_end(10)
# => [[1, 2, 3, 4, 5], [1, 2, 3, 4, 5]]

a1 = result.first
a2 = result.last
# => [1, 2, 3, 4, 5]がそれぞれに入る

まとめ

とりあえず、最新と最古のデータとってきたりするときには使えそうかなぁという感じ。 今後も仕様とか検討してみるけど、提案するかはまだ保留。

もし、記事を読んだ方で興味のある方はコメントとかいただけると助かります

Rubyにパッチを投げてきたアレコレ

はじめに

この記事は俺GW頑張ったというGWアドベントカレンダーの5月1日の記事です。

プログラミング言語Rubyにテストの修正やbuiltin対応などでパッチを投げてきた時の知見などを振りかえってみたかったので書いてみた。

やったこと

テスト修正関連

そもそもRubyのパッチとかを書き始めたのは、ruby-jpでコミッターの方が「テストの修正をしたいんですが、手伝っていただける人いませんか?」と投げられてたのがきっかけ。

具体的に修正したテストはこれら

remove warning & support multi-run test for test/psych/visitors/test_… #2881

remove warning & support multi-run test for test/psych_test_yaml.rb #2887

Fix remove warning & support multi-run test for test/psych/visitors/test_to_ruby.rb #2893

support multi-run test for test/did_you_mean/test_verbose_formatter.rb #2894

support multi-run test for test/webrick/test_filehandler.rb #2896

support multi-run test for test_readline.rb #2914

support multi-run test for test/did_you_mean/spell_checking/test_class_name_check.rb #2880

Rubyではmake test-allというすべてのテストを行うコマンドがあり、それをmake test-all TESTS=--repeat-count=2のように繰り返して実行するといくつかのテストが落ちてしまうという問題が起きていました。

原因としては、変数などの初期化がされていないので二回目以降に落ちるということが多かったですね。 とはいえ、原因の判別は結構難しかった……。

テストの修正を行うにあたってまずは落ちているテストのリストアップを行いました。 この時、注意したのは自分の環境要因で落ちているものもあった点ですね

たとえばWSL(Ubuntu 18.04)を使用した環境ではネットワーク関係やIO周りのテストで落ちることがありましたが、VM(Ubuntu 18.04)上では落ちることがありませんでした。 このように環境によってテストの結果が変わるので、そこを分離していくことから始めました。

結果的にWSL上でのテスト環境はネットワークやIO周りの問題が解決しなかったので、おとなしくVMを使って作業することに。

そこから落ちているテストをピックアップしていき、落ちている原因らしきものをチェックしていきました。

チェックにあたっては、最小の構成でテストが落ちるコードを書いて動きを確認。その後に、原因らしき箇所を洗うという流れで進めました おおよその場合は同じテストファイルの中で初期化が済んでいないということが原因であり、それらは比較的対処が楽でしたね。

辛かったのは、別のテストで値が変更されておりテストが落ちるケースでした……。 こういうテストが落ちる箇所は再現コードを作るのが難しく、なんどもRubyリファレンスマニュアルを読みふけり、Rubyの実装を読んでました。

こんな感じでテスト関係は修正を進め、無事テストが通るようになりました。

builtin対応

builtinというRubyとCを使ってRubyのメソッドを実装できる仕組みがあります。 builtinを使うことでキーワード引数を使ったメソッドはHashを作らないため高速に動作するようになります。

詳しくはこちらを参考に

Ruby(とC)でRubyを実装してみた(builtinで遊んでみた)

そのbuiltin対応をパッチとして投げました。

support builtin for Kernel#clone #2954

support builtin for Kernel#Float #3048

Kernel#cloneKernel#Floatの実装をbuiltin対応させて処理を高速化できたのは結構うれしかったですね。 ただ、ベンチマークを追加し忘れていたりしたので、その点は反省点かも。

ちなみに、今もいくつかのメソッドのbuiltin対応を進めていますね(いくつかベンチマーク忘れてたりしたけど)。

support builtin for Numeric#clone #2968

support builtin for Dir class singleton method #3007

support builtin for String#lines & String#each_line #3022

support builtin for Exception#full_message #3038

support builtin for Kernel#Integer #3054

その他

ドキュメント周りやCIの修正、不要になった関数宣言の削除とかやったりしましたね。

Add array.rb to .document #2904

Fix AppVeyor CI sometimes fail #2979

Fix AppVeyor pull request CI #2983

remove unused rb_str_clear define #3059

この辺は、あんまり複雑なことはしていないですね。 ソースコード読んだりしてて気になったので修正投げた感じです。

CIは落ちてたので修正投げたんですが、挙動が良くわからないところもあり良い感じの修正にならなかったのが残念ですね。

今現在も、二つほどCのコードを変更したものを投げたりしてますね。

Add rb_ary_max_or_min func #3060

Move rb_big_isqrt defination to internal/bignum.h #3065

おわりに

振り返ってみると意外とアレコレ修正パッチ書いてるなぁという感じ。

今後も何かしら修正できそうなもの見つけたらパッチを投げていきたいと改めて思ったね。

RubyのFloat(arg, exception: true)をbuiltinで再実装してみた

はじめに

この記事は以前書いた以下の記事で紹介したbuiltinを使ってFloat(arg, exception: true)というメソッドを再定義してみた記事になります。

gamelinks007.hatenablog.com

やっていることは前回の記事とおおよそ同じです。前回と異なる点は、特定のクラスのメソッドではなく、どこでも呼び出すことができるメソッドをbuiltinで再定義してみたという点です。

前回の記事を書いた後、raiseFloatなどのメソッドをbuiltin対応ができるのか気になり試してみた感じですね。

結論から言うと、そういったグローバルなメソッドもbuiltinで対応することができるようです(意図した挙動かは確認とっていないので、推奨されたものではないかもしれませんが……)

builtinって?

builtinとはRuby(とC)でRuby自体を実装するというものです。詳しくは前回の記事を読んでいただければと思います。

gamelinks007.hatenablog.com

環境構築

前回の記事の環境をそのまま使用しています。特に大きな変更点などもありません。

gamelinks007.hatenablog.com

やってみた

Floatメソッドの実装場所を探す

まずはFloatメソッドが実装されているソースを探します。Rubyのメソッドは大体こんなかんじで定義されています。

rb_define_method(rb_mKernel, "to_s", rb_any_to_s, 0);

第一引数がメソッドが定義されているクラス/モジュールの変数になります。大体、rb_cArrayなどのようにクラス名やモジュール名で作成されています。第二引数の"to_s"Ruby側で呼び出されるメソッド名になっています。第三引数はメソッドが呼び出された際に実行するCの関数で、最後の引数がメソッドが受け取る引数の数を指定しています。

今回、再実装したいのはFloatメソッドですのでRubyソースコード内からgit grep \"Float\"などで検索してみましょう。 するとobject.cで以下のように実装されているようです。

rb_define_global_function("Float", rb_f_float, -1);

それでは再実装していきます。

Floatメソッドの再実装

前回の記事ではHash#deleteを再実装していました。またbuiltin対応のため、common.mkなどの修正していました。 ですが、現在のRubyのmasterのコードではobject.cで実装されていたKernel#cloneをbuiltinに対応させているので、それらの修正は必要ありません。

修正が必要なソースはobject.ckernel.rbの二つになります。

object.cの修正

object.cではまずFloatメソッドを定義している以下のコードを削除します。

void
InitVM_Object(void)
{
    Init_class_hierarchy();

    // 省略

-    rb_define_global_function("Float", rb_f_float, -1);

    // 省略
}

次に、Floatメソッドの処理を定義しているrb_f_floatを以下のように修正します。

- /*  
-  *  call-seq:    
-  *     Float(arg, exception: true)    -> float or nil 
-  *   
-  *  Returns <i>arg</i> converted to a float. Numeric types are   
-  *  converted directly, and with exception to String and 
-  *  <code>nil</code> the rest are converted using    
-  *  <i>arg</i><code>.to_f</code>.  Converting a String with invalid  
-  *  characters will result in a ArgumentError.  Converting   
-  *  <code>nil</code> generates a TypeError.  Exceptions can be   
-  *  suppressed by passing <code>exception: false</code>. 
-  *   
-  *     Float(1)                 #=> 1.0   
-  *     Float("123.456")         #=> 123.456 
-  *     Float("123.0_badstring") #=> ArgumentError: invalid value for Float(): "123.0_badstring"   
-  *     Float(nil)               #=> TypeError: can't convert nil into Float   
-  *     Float("123.0_badstring", exception: false)  #=> nil  
-  */  

static VALUE    static VALUE
- rb_f_float(int argc, VALUE *argv, VALUE obj)
+ rb_f_float(rb_execution_context_t *ec, VALUE main, VALUE arg, VALUE opts)
{
-    VALUE arg = Qnil, opts = Qnil;    
-
-    rb_scan_args(argc, argv, "1:", &arg, &opts);    
-    return rb_convert_to_float(arg, opts_exception_p(opts));      
+   return rb_convert_to_float(arg, opts_exception_p(opts));
}

builtinを使い、キーワード引数を受け取るのでint argc, VALUE *argvではなくVALUE main, VALUE arg, VALUE optsとしています。またVALUE mainはグローバルなメソッドが暗黙の裡に受け取っている引数を代わりに受け取るために追加しています。

これでobject.cでの修正は完了です!

kernel.rbの修正

Rubyでは、どこからでも呼び出すことができるグローバルなメソッドはKernelモジュールにて再定義することができます。

たとえば、putsメソッドは以下のようにモンキーパッチすることができます。

module Kernel
   def puts *args
      p "hoge" 
   end
end


puts :ho
#=> "hoge" と表示される

これを使い、kernel.rb内に以下のようにFloatメソッドを追加します。

module Kernel
+ 
+   #
+   #  call-seq:
+   #     Float(arg, exception: true)    -> float or nil
+   #
+   #  Returns <i>arg</i> converted to a float. Numeric types are
+   #  converted directly, and with exception to String and
+   #  <code>nil</code> the rest are converted using
+   #  <i>arg</i><code>.to_f</code>.  Converting a String with invalid
+   #  characters will result in a ArgumentError.  Converting
+   #  <code>nil</code> generates a TypeError.  Exceptions can be
+   #  suppressed by passing <code>exception: false</code>.
+   #
+   #     Float(1)                 #=> 1.0
+   #     Float("123.456")         #=> 123.456
+   #     Float("123.0_badstring") #=> ArgumentError: invalid value for Float(): "123.0_badstring"
+   #     Float(nil)               #=> TypeError: can't convert nil into Float
+   #     Float("123.0_badstring", exception: false)  #=> nil
+   #
+   def Float(arg, exception: true)
+     __builtin_rb_f_float(arg, exception)
+   end
+ 
  #
  #  call-seq:
  #     obj.clone(freeze: nil) -> an_object
  #
  #  Produces a shallow copy of <i>obj</i>---the instance variables of
  #  <i>obj</i> are copied, but not the objects they reference.
  #  #clone copies the frozen value state of <i>obj</i>, unless the
  #  +:freeze+ keyword argument is given with a false or true value.
  #  See also the discussion under Object#dup.
  #
  #     class Klass
  #        attr_accessor :str
  #     end
  #     s1 = Klass.new      #=> #<Klass:0x401b3a38>
  #     s1.str = "Hello"    #=> "Hello"
  #     s2 = s1.clone       #=> #<Klass:0x401b3998 @str="Hello">
  #     s2.str[1,4] = "i"   #=> "i"
  #     s1.inspect          #=> "#<Klass:0x401b3a38 @str=\"Hi\">"
  #     s2.inspect          #=> "#<Klass:0x401b3998 @str=\"Hi\">"
  #
  #  This method may have class-specific behavior.  If so, that
  #  behavior will be documented under the #+initialize_copy+ method of
  #  the class.
  #
  def clone(freeze: nil)
    __builtin_rb_obj_clone2(freeze)
  end
end

先ほどbuiltin対応させたrb_f_float__builtin_rb_f_floatで呼び出しています。これでkernel.rbでの修正はOKです。

最後に

あとはmakemake install を実行してビルドできればbuiltinでの実装は完了です。

おわりに

こんな感じでFloatIntegerなどのメソッドをbuiltin対応できそうです。パッチとして送るかは別として、結構応用できそうだったのが面白かったですね。

ちなみに、以下のようなベンチマークを試してみたところパフォーマンス向上は期待できそうにないかなという感じでした。

benchmark:
  float: "Float(42)"
  float_true: "Float(42, exception: true)"
  float_false: "Float(42, exception: false)"
loop_count: 10000

以下結果

Calculating -------------------------------------
                     compare-ruby  built-ruby
               float      37.495M     15.878M i/s -     10.000k times in 0.000267s 0.000630s
          float_true       1.109M      1.211M i/s -     10.000k times in 0.009015s 0.008261s
         float_false       1.227M      1.191M i/s -     10.000k times in 0.008150s 0.008395s

Comparison:
                            float
        compare-ruby:  37495314.0 i/s
          built-ruby:  15878056.0 i/s - 2.36x  slower

                       float_true
          built-ruby:   1210507.2 i/s
        compare-ruby:   1109250.0 i/s - 1.09x  slower

                      float_false
        compare-ruby:   1226948.7 i/s
          built-ruby:   1191128.5 i/s - 1.03x  slower

仮にFloatなどのメソッドをbuiltin対応する場合はパフォーマンスよりもコードのメンテナンス性などを重視することになるのかなと思いますね。

Ruby(とC)でRubyを実装してみた(builtinで遊んでみた)

はじめに

タイトルにもあるようにRubyのbuiltin(正式名称を知らないので呼び出し方法から拝借)というものを使ってRuby自体をRuby(とC)で実装してみた話です。

内容としてはRuby自体の実装に興味のある方向けの話になります。

builtinって?

builtinとはRuby(とC)でRuby自体を実装するというものです(正式な名前は今のところないみたい?)。以下のように__builtin_<Cで定義した関数名>Rubyのコードから呼び出すことでRubyとCを使い、より簡単にRubyの実装を行うことができます。

たとえば、Hash#deleteはCで以下のように実装されています。

static VALUE
rb_hash_delete_m(VALUE hash, VALUE key)
{
    VALUE val;

    rb_hash_modify_check(hash);
    val = rb_hash_delete_entry(hash, key);

    if (val != Qundef) {
    return val;
    }
    else {
    if (rb_block_given_p()) {
        return rb_yield(key);
    }
    else {
        return Qnil;
    }
    }
}

第一引数のhashはハッシュ自体を引数に受け取り、第二引数のkeyHash#deleteで渡しているキーを受け取っています。ちなみに、Ruby側の変数などの値はVALUE型で受け取り、Cの関数で処理されています。

    rb_hash_modify_check(hash);

rb_hash_modify_check関数は内部でrb_check_frozen関数を実行し、ハッシュが凍結されているかを確認しています。

static void
rb_hash_modify_check(VALUE hash)
{
    rb_check_frozen(hash); // オブジェクトが凍結されているか確認
}

val = rb_hash_delete_entry(hash, key);では引数に受け取ったキーをもとに削除する値を取得し、同時に削除を行っています。キーと対になる値がない場合はQundefというCで使用する未定義の値が入ります。

    if (val != Qundef) {
    return val;
    }
    else {
    if (rb_block_given_p()) {
        return rb_yield(key);
    }
    else {
        return Qnil;
    }
    }

valの値で処理を分岐させ、Qundefではない場合(つまりキーを使って値が取れ、削除できた場合)は削除された値を返します。 Qundefだった場合はQnil(Rubyでのnil)を返します。ブロックが渡されている場合はrb_yield(key)を実行し、その結果を返しています。

このように皆さんが普段使っているRubyは、Cを使って実装されています。

builtin機能を使うことで先ほどのコードが以下のようになります。

class Hash
    def delete(key)
        value = __builtin_rb_hash_delete_m(key)

        if value.nil?
            if block_given?
                yield key
            else
                nil
            end
        else
            value
        end
    end
end
static VALUE
rb_hash_delete_m(rb_execution_context_t *ec, VALUE hash, VALUE key)
{
    VALUE val;

    rb_hash_modify_check(hash);
    val = rb_hash_delete_entry(hash, key);

    if (val != Qundef) {
        return val;
    }
    else {
        return Qnil;
    }
}

Ruby側でブロックの実行などを処理させているため。Cでの実装はよりシンプルで読みやすくなったと思います。 また

このようにbuiltin機能を使うことでRubyと少しのCのコードでRubyを実装することができます。

またCで実装するよりもRubyで実装した場合、パフォーマンスが向上するケースもあるようです。 より具体的な話は笹田さんがRubyKaigi 2019にて話されていますのでそちらを参照して頂ければと思います。

Write a Ruby interpreter in Ruby for Ruby 3

やってみた

builtinを使うことでRubyのコードを使い、メソッドを実装できることがわかったので実際にやってみました。

開発環境構築

まずはRubyの開発環境を作成するところからはじめました。僕の環境としてはWSL+Ubuntu 18.04を使い、開発環境を構築しました。 基本的な手順としてはRuby Hack Challengeの(2) MRI ソースコードの構造を参考に進めました。

まずは使用するライブラリなどをインストールしていきます。

sudo apt install git ruby autoconf bison gcc make zlib1g-dev libffi-dev libreadline-dev libgdbm-dev libssl-dev

次に、作業用のディレクトリを作成し、そこへ移動します。。

mkdir workdir
cd workdir

作業用のディレクトリに移動後、Rubyソースコードをcloneします。結構時間がかかるのでこの間にコーヒーでも入れておくといいでしょう。

git clone https://github.com/ruby/ruby.git

ソースコードのcloneが終わったら、rubyディレクトリへと移動し、autoconfを実行します。あとで実行するconfigureスクリプトを生成するためですね。実行後、workdirまで戻ります。

cd ruby
autoconf
cd ..

次に、ビルド用のディレクトリを作成し、そこへ移動します。

mkdir build
cd build

../ruby/configure --prefix=$PWD/../install --enable-sharedを実行してビルドするためのMakefileを作成します。また--prefix=$PWD/../installではRubyをインストールする先を指定しています

../ruby/configure --prefix=$PWD/../install --enable-shared

その後、make -jを実行してビルドします。-jは並列にコンパイルを実行するためのオプションです。特に急ぐわけでもない場合はmakeだけでも良いでしょう。

make -j

最後にmake installを実行するとworkdirディレクトリ内にinstallディレクトリが作成され、Rubyがインストールされます。

make install

これで最新のRubyworkdir/installにインストールされています。

ちなみに、本当にインストールされているか気になる方は../install/bin/ruby -vを実行してみましょう。ruby 2.8.0devRubyのバージョンが表示されていればRubyは正しくインストールされています。

builtinでメソッドを再定義してみる

開発環境が整ったのでbuiltinを使い、メソッドを再定義していきます。先ほど例にも挙げたHash#deleteを再実装していきます。

common.mkの修正

まずは、ビルドの際にRubyソースコードを使用するための諸設定をcommon.mkに追加します。 common.mkの1000行目辺りに、BUILTIN_RB_SRCSという記述があります。このBUILTIN_RB_SRCSで読み込むRubyのコードが記述されているファイルを追加します。

BUILTIN_RB_SRCS = \
        $(srcdir)/ast.rb \
        $(srcdir)/gc.rb \
        $(srcdir)/io.rb \
        $(srcdir)/pack.rb \
        $(srcdir)/trace_point.rb \
        $(srcdir)/warning.rb \
        $(srcdir)/array.rb \
        $(srcdir)/prelude.rb \
        $(srcdir)/gem_prelude.rb \
        $(empty)
BUILTIN_RB_INCS = $(BUILTIN_RB_SRCS:.rb=.rbinc)

今回は、Hashの実装を行うためhash.rbを以下のように追加します。

BUILTIN_RB_SRCS = \
        $(srcdir)/ast.rb \
        $(srcdir)/gc.rb \
        $(srcdir)/io.rb \
        $(srcdir)/pack.rb \
        $(srcdir)/trace_point.rb \
        $(srcdir)/warning.rb \
        $(srcdir)/array.rb \
        $(srcdir)/prelude.rb \
        $(srcdir)/gem_prelude.rb \
+       $(srcdir)/hash.rb \
        $(empty)
BUILTIN_RB_INCS = $(BUILTIN_RB_SRCS:.rb=.rbinc)

次に、2520行目辺りにあるHashのビルドで読み込むファイルを指定している部分を修正します。 このようにhash.cなど読み込むファイルが指定されています。

hash.$(OBJEXT): {$(VPATH)}hash.c
hash.$(OBJEXT): {$(VPATH)}id.h
hash.$(OBJEXT): {$(VPATH)}id_table.h
hash.$(OBJEXT): {$(VPATH)}intern.h
hash.$(OBJEXT): {$(VPATH)}internal.h
hash.$(OBJEXT): {$(VPATH)}missing.h

ここに、hash.rbincbuiltin.hを追加します。

hash.$(OBJEXT): {$(VPATH)}hash.c
+hash.$(OBJEXT): {$(VPATH)}hash.rbinc
+hash.$(OBJEXT): {$(VPATH)}builtin.h
hash.$(OBJEXT): {$(VPATH)}id.h
hash.$(OBJEXT): {$(VPATH)}id_table.h
hash.$(OBJEXT): {$(VPATH)}intern.h
hash.$(OBJEXT): {$(VPATH)}internal.h
hash.$(OBJEXT): {$(VPATH)}missing.h

hash.rbincmake実行時に自動的に生成されるファイルで、hash.rb内の__builtin_<呼び出すCの関数名>をチェックした内容をもとに生成されています。またbuiltin.hはbuiltinを使うための実装などがかかれたヘッダーファイルです。

これでcommon.mkでの修正は完了です。

inits.cの修正

次に、inits.cを修正します。といっても非常に修正は簡単なものです。

#define BUILTIN(n) CALL(builtin_##n)
    BUILTIN(gc);
    BUILTIN(io);
    BUILTIN(ast);
    BUILTIN(trace_point);
    BUILTIN(pack);
    BUILTIN(warning);
    BUILTIN(array);
    Init_builtin_prelude();
}

inits.cでは上記のようにbuiltinを使用しているRubyのソースファイルを追加しています。ここに同じようにBUILTIN(hash);を追加します。

#define BUILTIN(n) CALL(builtin_##n)
    BUILTIN(gc);
    BUILTIN(io);
    BUILTIN(ast);
    BUILTIN(trace_point);
    BUILTIN(pack);
    BUILTIN(warning);
    BUILTIN(array);
+    BUILTIN(hash);
    Init_builtin_prelude();

inits.cの修正はこれでOKです。

hash.cの修正

いよいよ、hash.cのコードを修正していきます。

builtin.hの読み込み

まずは、40行目辺りのヘッダー読み込み部分に#include "builtin.h"を追加します。

  #include "ruby/st.h"
  #include "ruby/util.h"
  #include "ruby_assert.h"
  #include "symbol.h"
  #include "transient_heap.h"
+ #include "builtin.h"

これでbuiltinに必要な構造体などをhash.cで使用することができます。

Hash#deleteの定義を削除

次に、Hash#deleteを定義している部分を取り除きます。

hash.cの下部にInit_Hash(void)という関数が定義されていると思います。

void
Init_Hash(void)
{
 /// Hashの実装コードなどが記述されています。
}

Rubyの各クラスのメソッドはこの関数内で以下のように定義されています。

rb_define_method(rb_cHash, "delete", rb_hash_delete_m, 1);

rb_define_methodRubyでいうところのメソッドの定義と同じと考えてください。第一引数にメソッドを定義するクラスのVALUEを渡し、第二引数がメソッド名となっています。  第三引数がCで定義された関数(メソッドで実行される処理)で、第四引数がメソッドが受け取る引数の数となっています。

builtinでRubyのメソッドを定義する場合はこの定義部分を削除する必要があります。今回はHash#deleteを再実装しますので、deleteが定義されている部分を削除します。

    rb_define_method(rb_cHash, "shift", rb_hash_shift, 0);
-   rb_define_method(rb_cHash, "delete", rb_hash_delete_m, 1);
    rb_define_method(rb_cHash, "delete_if", rb_hash_delete_if, 0);

rb_hash_delete_mをbuiltinから使用できるように修正

先ほど削除したrb_define_method(rb_cHash, "delete", rb_hash_delete_m, 1);で呼び出されているrb_hash_delete_mをbuiltinで使用できるように修正します。

2380行辺りにrb_hash_delete_mの実装があります。

static VALUE
rb_hash_delete_m(VALUE hash, VALUE key)
{
    VALUE val;

    rb_hash_modify_check(hash);
    val = rb_hash_delete_entry(hash, key);

    if (val != Qundef) {
    return val;
    }
    else {
    if (rb_block_given_p()) {
        return rb_yield(key);
    }
    else {
        return Qnil;
    }
    }
}

これを以下のように修正します。

static VALUE
rb_hash_delete_m(rb_execution_context_t *ec, VALUE hash, VALUE key)
{
    VALUE val;

    rb_hash_modify_check(hash);
    val = rb_hash_delete_entry(hash, key);

    if (val != Qundef)
    {
        return val;
    }
    else
    {
        return Qnil;
    }
}

builtin対応のために第一借り引数にrb_execution_context_t *ecを渡しているところが実装の肝ですね。

これでRubyからCで定義した関数を呼び出すことができるようになります。

hash.rbincの読み込み

最後に、自動生成されるhash.rbincを読み込むようにします。 #include "hash.rbinc"hash.cの一番下に追加します。

#include "hash.rbinc"

これでCのコード側での修正は完了しました。

hash.rbの作成

それではRubyHash#deleteを実装してみましょう。hash.cと同じ階層にhash.rbを作成します。 作成後、以下のようにコードを追加します。

class Hash
    def delete(key)
        puts "impl by Ruby(& C)!"
        value = __builtin_rb_hash_delete_m(key)

        if value.nil?
            if block_given?
                yield key
            else
                nil
            end
        else
            value
        end
    end
end

先ほどbuiltinで呼び出せるようにした__builtin_rb_hash_delete_mに受け取ってきた引数を渡し、その結果をvalueに代入しています。

あとは、valueの値がnilか同課で処理を分岐させています。nilの場合かるブロックが渡されている場合はkeyを引数にブロックを実行しています。

puts "impl by Ruby(& C)!"は実際に試す際に確認するためのメッセージになりますね。

これでbuiltinでの実装はすべて完了しました!

ビルドしてみる

それでは開発環境を構築した時同様にビルドしてみましょう。

make -j && make install

ビルドが成功すればOKです!もしビルドが失敗した場合はtypoなどが無いか確認してみましょう。

実際にirbで試してみる

それではirbを使ってbuiltinで実装したHash#deleteを試してみましょう!

../install/bin/irb

後は以下のコードを貼り付けてみましょう!

hash = {:key => "value"}
hash.delete(:k)
hash.delete(:key)

以下のように結果が表示されていればbuiltinでの実装は完了です!

irb(main):001:0> hash = {:key => "value"}
irb(main):002:0> hash.delete(:k)
impl by Ruby(& C)!
=> nil
irb(main):003:0> hash.delete(:key)
impl by Ruby(& C)!
=> "value"
irb(main):004:0>

impl by Ruby(& C)!と表示されているのでRubyで定義したHash#deleteが実行されていることが分かりますね。

これでRuby(とC)でRubyを実装できました!

終わりに

このようにbuiltinを使うことでRubyと(少しのC)のコードを使ってRuby自体を実装することができます。 そのため普段Rubyを書いている人でも気軽にメソッドの修正などのパッチを送ることができるようになるのではないかと思います。

あとやってみてRuby側で処理が書けたりするので意外と書きやすいというのはうれしいですね。

個人的にはExtensionなどでも使用できるようになるとC/C++でのRuby拡張がより書きやすくなるのではないかと思うので、今後の展望が非常に楽しみですね。

参考

Write a Ruby interpreter in Ruby for Ruby 3

Rubyソースコード完全解説

Rubyのしくみ

一年間で作ってきたリポジトリをさかのぼる

はじめに

この記事は、俺今年頑張ったAdvent Calendar 2019 の13日目の記事です。

 

タイトル通り、一年間で作ってきたリポジトリを振り返っていこうと思います。なお、GitHubで公開しているもののみに限定しています。

 

リポジトリ

今年一年で作ったリポジトリの数は、ちょうど200個!(2019年12月13日現在)意外と作ってるなぁという印象。

 

中にはサンプルとして組んでいるものなども結構あるので、意外とあれこれやってきたんだなあと実感。

 

言語別だと一番作っているのはRubyで、次点でPHPC++という感じですね。RubyC++が多いのは個人的な趣味で良く書くことが多いからだと思うけど、PHPが意外と多かったのには驚いた。

今年は、仕事でPHPを書くことが多かったのでそのせいかな……?

 

個人的な抱負としては来年もC++Rubyをガンガン書いていきたい。で、時点でRustとかGoをやっていきたい。まあ、その余裕があるかはわかんないけどね。

 

特に面白そうなもの 

AutoNSFWChecker

github.com

MastodonのLTLを監視して、卑猥な画像などを自動判定してくれるBotですね。Google Cloud Vision APIを使っているので比較的シンプルに実装しています。

ただ、LTLが活発だと結構コストがかかるのでこれをそのまま導入するのはお勧めできない感じ

RubyGoogle Cloud Vision を使ってみた方はこれを参考にしてみるといいかも

 

MastodonDestroyButton

github.com

ネタで作ったMastodonサーバーの爆破ボタン。AmazonDashボタンを使って、押された際に自分のMastodonサーバーをスナック感覚で爆破できる代物です。とはいえAmazonDashボタン自体が販売中止だそうなので、今後使う場面はなさそう

 

ARcpp

github.com

C++/Vue.jsで作ったAR読込のサンプル。結構シンプルに作ってあるのでAR関係のサンプルを作るときにたまに見返してたりする

 

MyFirstRails

github.com

Railsチュートリアル集として作ったリポジトリ。色々と作っているのでたまに自分用のリファレンスみたく使ったり。最近メンテナンスできていないので、今年の年越しはメンテしたい。

 

face_under_construction

github.com

顔面工事というハッシュタグをブーストするBotですね。実装にあたってAWS Lambdaとか使ったりしていい勉強になりました。あと今年の分散SNS萬本やRubyistMagazineでも作った際のあれこれを寄稿させてもらったりといいきっかけになったやつですね。

 

arukas

github.com

ArukasというDockerイメージを元に気軽にデプロイできるサービスをRubyで実行できるようにしたものですね。とはいえ、Arukas自体が来年一月にサービスが終了するのでもう使うことはなさそう……。

 

conpass-shimane-line

github.com

Connpassというイベント管理サービスのAPIを使って、LINEに直近のイベントをシェアしてくれるBotですね。実装はAWS lambdaを使っているので先ほどの顔面工事Botと同じ感じですね。意外と重宝しているので今後もメンテナンスしていきそう。

 

sol

github.com

C++/Svelte/Stimulusを使って自作したWebフレームワーク。Railsライクなコマンドでアプリのひな型とか作れるので重宝してる。ちょっとしたWebアプリケーションとか作って納品しようかなってときには楽で助かってる。

 

bootstrap_plugin_card_slide

github.com

WordPressプラグインをスライド風に表示して紹介できるプラグイン。実装が結構重いので今後修正していきたい。

 

tatara

github.com

Ruby拡張機能として型を使えるようにしてみたもの。おそらく今年一番コミットしたりしてたリポジトリ。Riceを使ってたんだけど、最近C++のみの実装に移行した。tataraは今後もじっくりと開発を続けていきたいところ。

 

おわりに

意外とアレコレ作ってきたなぁという感想。あとジャンルが結構違ったりするので雑食だなぁとも思った。

とりあえず、来年も頑張るかな?

C++で作るRuby拡張

はじめに

この記事は「Ruby Advent Calendar 2019」の8日目の記事です。

C++でのRuby拡張実装について、つらつらと書いている記事になります。

内容としてはTataraというRubyで型を使えるライブラリを作ってみたで紹介した自作Ruby拡張を作るにあたって得たC++でのRuby拡張実装知見の記事になります。

Ruby拡張って?

皆さんが普段使っているRuby(ここではCRubyのことです)はCによって実装されています。ですので、Cを使ってRuby拡張機能を作成することもできます。

つまり、Cで既に作成されているライブラリなどをRuby拡張として作成することができるというメリットがあります。CでRuby拡張を実装した場合、Rubyで実装するよりも高速に処理できるケースもあるようです。

実際にCで拡張機能が実装されているgemとしてはsqlite3mysql2などがあります。

またRustやC++Ruby拡張機能を作成するケースもあります。

例えば最近面白いなぁと思ったのはRustでのRuby拡張を実装できるHelixですね。HelixRustを使うことでCやC++よりも安全にRuby拡張を書くことができます。

また実装コード自体もかなり読みやすく以下のようなコードでクラスとメソッドを実装できます(※ HelixのREADMEより引用)。

ruby! {
    class Console {
        def log(string: String) {
            println!("LOG: {}", string);
        }
    }
}

ただHelix公式のチュートリアルではRails向けに拡張機能を実装する内容になっています。そのためRuby向けの拡張を作成する際のドキュメントがあまりなく、少し辛いところがあります。

実際にHelixでRuby拡張を作成しているものとしては以下の記事などがあります。

ref: Rustでgemを書く際のハマりどころ in 2017

ref: Writing Ruby gems with Rust and Helix

またC++ではRiceExt++などのRuby拡張を実装できるライブラリも存在しています。

RubyKaigi 2017ではImprove extension API: C++ as better language for extensionにてC++でのRuby拡張実装について紹介されています。

興味のある方はそちらも確認してみると良いでしょう。

今回はC++でのRuby拡張の実装方法について解説します。具体的にはRiceExt++C++のみでの実装方法などを解説していきます。

つくるもの

今回は、Helloというクラスを作成し、Hello Ruby Extension!と画面に表示するsayというメソッドを実装します。

具体的には以下のようなコードが実行できるRuby拡張を実装していきます。

require 'hello'

Hello.new.say
# => "Hello Ruby Extension!"

今回はRiceExt++C++でそれぞれ実装していきます。

今回の記事作成にあたって各ライブラリでの実装サンプルをGitHubに上げておきました。興味のある方はこちらも見ると良いかも。

S-H-GAMELINKS/RubyAdventCalendarExtensionSample

実装

Riceでの実装

Riceとは?

Riceとは、C++を使ってRuby拡張を簡単に作成できるライブラリになります。

RiceはgemとしてRubyGemsからインストールすることができます。

gem install rice

これでRiceが使えるようになります!

ちなみに、実際にRiceを使ったサンプルコードは以下のようになります。

#include <iostream>
#include <rice/Data_Type.hpp>
#include <rice/Constructor.hpp>

using namespace Rice;

class Hello {
    public:
        Hello() {};
        void say() { std::cout << "Hello Ruby Extension!" << std::endl; };
};

extern "C" {
    void Init_hello() {
        Data_Type<Hello> rb_cHello = define_class<Hello>("Hello")
            .define_constructor(Constructor<Hello>())
            .define_method("say", &Hello::say);
    }
}

このようにRiceを使う場合、非常に簡単にRuby拡張を作ることができます。

またC++のテンプレートなどを使って以下のようなコードを書くこともできます。

template <class T>
class CppArray {
    public:
        CppArray<T>() {};
};

Data_Type<CppArray<int>> rb_cIntArray = define_class<CppArray<int>>("IntArray")
    .define_constructor(Constructor<CppArray<int>>());

Riceを使うメリットととしては、非常に簡単にC++でのRuby拡張を作ることができる点ですね。C++のライブラリなどをRubyで使えるようにするラッパーなどは、Riceを使って実装するといいかもしれません。

デメリットとしては、日本語のドキュメントもあまりないことと、開発自体があまり活発でない印象があることですね。

日本語で書かれた記事はあまり(Rice以外での実装とかはあったりする)なく、IBMRice を使用して Ruby の拡張機能を C++ で作成するが日本語で唯一詳しく書かれたRiceのチュートリアルになりそうです、

英語が読める方であれば、こちらのドキュメントを読み解けばよいかと思います。

GitHubリポジトリでのコミットログなどを見るた印象ではあまり開発が活発な印象はないです。最近、いくつかPull Requestが取り込まれてはいるようですが……。 そのため、Rice側の開発が打ち切られると辛いことになりそうな気配がありますね……。

とはいえ、大きな変更が入る可能性は少ないのでとりあえずC++でのRuby拡張を作る分には良いライブラリだと思います。

実装

それでは、Riceを使ってRuby拡張を実装してみましょう。

なにはともあれ、Riceをインストールしましょう。

gem install rice

インストールが無事終了した後は、extconf.rbというファイルを作成します。これはC++のコードをビルドするMakefileを自動生成するためのファイルになります。CでRuby拡張を作る場合も同様にextconf.rbを作成します。

require 'mkmf-rice'

create_makefile('hello')

mkmf-riceはRiceを使ってかかれたC++のソースをもとにMakefileを作成するためのライブラリになります。ちなみに、Cで拡張機能を実装する場合はmkmfというライブラリを読み込んでMakefileを自動生成していますね。

またcreate_makefileに渡している文字列がビルドされた拡張ライブラリの名前になります。

次に、hello.cppextconf.rbと同じ階層に作成します。

#include <iostream>
#include <rice/Data_Type.hpp>
#include <rice/Constructor.hpp>

using namespace Rice;

class Hello {
    public:
        Hello() {};
        void say() { std::cout << "Hello Ruby Extension!" << std::endl; };
};

extern "C" {
    void Init_hello() {
        Data_Type<Hello> rb_cHello = define_class<Hello>("Hello")
            .define_constructor(Constructor<Hello>())
            .define_method("say", &Hello::say);
    }
}

軽くコードの解説をすると、以下の二行でRiceのヘッダーを読み込んでいます。

#include <rice/Data_Type.hpp>
#include <rice/Constructor.hpp>

RiceではData_Typeを使い、既存のクラスをもとにRuby向けにコンバートしています。

Data_Type<Hello> rb_cHello = define_class<Hello>("Hello")

上記のコードではC++で定義したHelloクラスをRubyで呼び出すHelloというクラスに変換しています。

.define_constructor(Constructor<Hello>())

.define_constructor(Constructor<Hello>())ではC++で定義したHelloクラスのコンストラクタ(Rubyでいうところのinitializeのようなもの)を使って、RubyHelloクラスのインスタンスを作成できるようにしています。 つまり、Rubyのinitializeを実装しています。

最後に.define_method("say", &Hello::say);sayというメソッドをHelloクラスに追加しています。

.define_method("say", &Hello::say);

これでC++側での実装は完了です。

次に、extconf.rbを実行してMakefileを生成します。

ruby extconf.rb
# => Makefileを自動生成

あとは、makeコマンドでビルドすればhello.ohello.soが生成されていると思います。

make
# => hello.o と hello.so が生成される

最後に、作成したRuby拡張を実際に動かしてみましょう。hello.rbを以下のように作成して実行してみましょう。

require './hello.so'

Hello.new.say
ruby hello.rb
# => Hello Ruby Extension!

Hello Ruby Extension!と表示されていればOKです!

Ext++での実装

Ext++とは?

Ext++はRice同様にC++を使って、Ruby拡張を作成できるライブラリです。

Ext++もRubyGemsで配布されているのでgemとしてインストールできます。

gem install extpp

Ext++での実装は以下のようになります。

#include <iostream>
#include <ruby.hpp>

RB_BEGIN_DECLS

void Init_hello() {
    rb::Class klass("Hello");

    klass.define_method("initialize", [](VALUE rb_self, int argc, VALUE *argv) {
        return Qnil;
    });

    klass.define_method("say", [](VALUE rb_self) {
        std::cout << "Hello Ruby Extension!" << std::endl;
        return Qnil;
    });
}

RB_END_DECLS

Ext++ではC++ラムダ式を引数に渡して実装することができる点が特徴的です。そのためラムダ式をうまく使うことでRubyのメソッドとC++の実装を一度に書くことができます。

また、Ext++ではruby.hppをインクルードするだけで良いところも便利です。Riceの場合、必要なヘッダーを個別に読み込まなければならず

Riceではラムダ式を使ってメソッドの定義などはできないため、ラムダ式でメソッドを定義したい人はExt++を使うと良いかもしれません

Ext++を使うメリットとしては、実装が一か所で済む点かなと思います。また開発者が日本の方(というか @kou さん)ですので開発者本人にあって話が聴けるという点もメリットかもしれません。

デメリットとしては、サンプルのコードが一つしかなく、人によってはどのように実装を進めていけばいいのかが分かりにくい時がある点でしょうか?その点に関しては今後Pull Requestなどでサンプルコードを投げれたらと思っていますね。 また開発バージョンであり、今後のバージョンアップでは大きな変更も入る可能性もありそうです。

しかしながら、開発者本人に直接話を聞くことができそう(日本人からすると)なので採用するメリットはかなり大きいと思います。

またRiceと違い、Rubyの実装自体に近い実装コードを書くのでCRubyの実装を学んでみたいという人にもオススメかもしれませんね。

実装

それでは、Ext++を使ってRuby拡張を実装していきましょう。

まずはExt++をインストールします。

gem install extpp

インストール完了後、Riceでの実装の時と同じようにextconf.rbを作成します。

require 'extpp'

create_makefile('hello')

Riceの時とおおよそ同じコードですね。違う点としてはmkmf-riceではなく、extppを読み込んでいます。

次に、hello.cppextconf.rbと同じ階層に作成します。

#include <iostream>
#include <ruby.hpp>

RB_BEGIN_DECLS

void Init_hello() {
    rb::Class klass("Hello");

    klass.define_method("initialize", [](VALUE rb_self, int argc, VALUE *argv) {
        return Qnil;
    });

    klass.define_method("say", [](VALUE rb_self) {
        std::cout << "Hello Ruby Extension!" << std::endl;
        return Qnil;
    });
}

RB_END_DECLS

Ext++ではrb::Classで新しいクラスを作成します。また、作成したklassdefine_methodを使うことで必要なメソッドを新しく定義しています。

QnilRubyでのnilを返しています。CRubyのメソッドなどでnilが返ってきているメソッドでは、子のようにreturn Qnil;と書かれています。興味のある方はRuby Hack Challenge Holidayに参加したり、GitHubruby/rubyのコードを読んでみると良いかもしれません。

あとは。extconf.rbを実行し、Makefileを生成します。

ruby extconf.rb
# => Makefileが生成される

その後、makeで作成したRuby拡張をビルドします。

make
# => hello.o と hello.soが生成される

最後にhello.rbを以下のように作成し、実行してみましょう。

require './hello.so'

Hello.new.say
ruby hello.rb
# => Hello Ruby Extension!

Hello Ruby Extension!と表示されていればOKです!

C++での実装

実装

最後にC++でのみで作成するRuby拡張について紹介します。

まずはextconf.rbを作成します。

require "mkmf"

create_makefile("hello")

mkmfはCRubyに添付されているRuby拡張のためのMakefile作成ライブラリですね。

次に、hello.cppを以下のように作成します。

#include <ruby.h>
#include <iostream>

class Hello {
    public:
        Hello() {};
        ~Hello() {};
        void say() { std::cout << "Hello Ruby Extension!" << std::endl; };
};

static Hello* get_hello(VALUE self) {
    Hello *ptr;
    Data_Get_Struct(self, Hello, ptr);
    return ptr;
}

static void wrap_hello_free(Hello *ptr) {
    ptr->~Hello();
    ruby_xfree(ptr);
}

static VALUE wrap_hello_alloc(VALUE klass) {
    void *ptr = ruby_xmalloc(sizeof(Hello));
    ptr = std::move(new(Hello));
    return Data_Wrap_Struct(klass, NULL, wrap_hello_free, ptr);
}

static VALUE wrap_hello_init(VALUE self) {
    return Qnil;
}

static VALUE wrap_hello_say(VALUE self) {
    get_hello(self)->say();
    return Qnil;
}

extern "C" {
    void Init_hello() {
        VALUE rb_cHello = rb_define_class("Hello", rb_cObject);

        rb_define_alloc_func(rb_cHello, wrap_hello_alloc);
        rb_define_private_method(rb_cHello, "initialize", RUBY_METHOD_FUNC(wrap_hello_init), 0);
        rb_define_method(rb_cHello, "say", RUBY_METHOD_FUNC(wrap_hello_say), 0);
    }
}

ポイントとしては#include <ruby.h>Ruby拡張の実装で使用するマクロや関数などを呼び出している点ですね。これがないとRuby拡張を実装することができません。

またget_hello関数はRubyインスタンスを引数に受け取って、C++インスタンスのポインタを返しています。この関数を使うことでC++のクラスのメソッドをラップ関数から呼び出して使うことができるようになります。

wrap_hello_free関数はRubyGCが呼び出された際にメモリから解放する際の処理がかかれた関数になります。

wrap_hello_allocインスタンスを作成する際のアロケータになります。wrap_hello_initRubyでのinitializeになりますね。

あとは、extconf.rbを実行し、makeを実行してビルドしてみましょう

ruby extconf.rb
# => Makfileが生成される

make
# => hello.o と hello.so が生成される

最後に、hello.rbを以下のように作成して実行しましょう。

require './hello.so'

Hello.new.say
ruby hello.rb
# => Hello Ruby Extension!

Hello Ruby Extension!と表示されていればOKです!

おわりに

C++でのRuby拡張についてRice、Ext++、C++それぞれでのでの実装を紹介しました。意外と簡単そうと思っていただければ幸いです。

あと今回の記事ではC++をベースに紹介しましたが、もちろんCでの実装を行う方法もあります。むしろ、そちらのほうが参考になる記事が多いので、Ruby拡張を作る際にはCで作ると良いかもしれません。

あと、この記事でRubyの実装に興味を持たれた方はRuby Hack Challenge Holidayなどに参加してみると良いかもしれません。

意外と簡単にC++でもRuby拡張機能を作ることができるので、今後もC++の良さげなライブラリなどをRuby向けに実装していきたいと思います。

参考記事

ref: Rice ref: Ext++ ref: Improve extension API: C++ as better language for extension ref: Rice を使用して Ruby の拡張機能を C++ で作成する ref: Rice - Ruby Interface for C++ Extensions ref: ko1/rubyhackchallenge ref: ruby/ruby ref: C++言語で簡単なRuby拡張ライブラリを書いてみた ref: Rubyの拡張ライブラリの作り方 ref: Rubyソースコード完全解説 ref: TataraというRubyで型を使えるライブラリを作ってみた

Hanami向けにBlumaのラッパーgemを作った

HanamiというRubyのWebフレームワーク向けにBlumaってCSSライブラリのラッパーgemを作ってみた

 

github.com

 

 

使い方は簡単で、Gemfileにhenami-blumaを追加するだけ。

 

あとは、<%= stylesheet 'bluma' %>のように使いたいテンプレートで呼び出すだけ

 

実装にあたっては以下のラッパーgemを参考にさせて頂きました。

 

github.com