2019年3月9日土曜日

C の配列を Ruby に変換する方法

概要

前回 RubyInline で Struct の使い方を紹介しました
今回は配列を主に扱いたいと思います
Ruby で定義した配列を C で扱う方法と逆に C で定義した配列を Ruby で扱えるようにする方法も紹介したいと思います
また環境は macOS になるので Linux などで実行する場合は、若干 C の命令が異なるので注意してください

環境

  • macOS 10.14.3
  • Ruby 2.5.1p57

サンプルコード

手軽に試せる inline を使っています

require 'inline'

class MyTest
  inline do |builder|
    builder.add_compile_flags '-x c++', '-std=c++11'
    builder.c_raw '
      VALUE new_c_ary() {
        VALUE result = rb_ary_new2(10);
        for (int i = 0; i < 10; i++) {
          rb_ary_store(result, i, rb_int_new(i + 1));
        }
        return result;
      }
    '
  end
end

p = MyTest.new
c_ary = p.new_c_ary
puts c_ary.class
puts c_ary

まず Ruby に返せるように rb_ary_new2 で配列を作成し、それに対して rb_ary_store で値を格納しています
格納する際にも明示的な型の指定が必要で rb_int_new を使って int を配列に格納しています
これで return すれば C 側で生成した int の配列を Ruby 側で配列を扱うことができます

uint8_t 型のポインタ配列を変換する

今度はポインタ配列のケースを考えます
基本的には先程と同じでポイントの値を Ruby で扱えるデータに変換して配列に格納し直します

require 'inline'

class MyTest
  inline do |builder|
    builder.add_compile_flags '-x c++', '-std=c++11'
    builder.include '<malloc/malloc.h>'
    builder.include '<stdio.h>'
    builder.c_raw %q[
      VALUE new_uint8t_ary() {
        VALUE result = rb_ary_new2(16);
        uint8_t *buf = (uint8_t *)malloc(16 * sizeof(uint8_t));
        for (unsigned long i = 0; i < malloc_size(buf); i++) {
          buf[i] = '@'; // 40 (hex)
          rb_ary_store(result, i, CHR2FIX(buf[i]));
          printf("%ld: %02x\n", i, buf[i]);
        }
        free(buf);
        // VALUE ret = ULONG2NUM(malloc_size(buf));
        return result;
      }
    ]
  end
end

p = MyTest.new
ret = p.new_uint8t_ary
ret.each_with_index { |r, i|
  puts sprintf("%d: %02x", i, r)
}

uint8_t は C99 から追加された unsigned int の 8 ビット型です
16 個の領域を持つポインタを malloc で定義します
uint8_t *buf = (uint8_t *)malloc(16 * sizeof(uint8_t));
確保された領域のサイズは malloc_size(buf) で取得できます
macOS の場合 <malloc/malloc.h> を使いますが Linux 環境の場合は <malloc.h> で参照できます

確保された領域分ループしポインタのアドレスを 1 つずつずらし値を格納していきます
今回は @ を追加しています
アスキーコードで 64 で 16 進数表示で 40 になります
Ruby 用の配列に値を格納するときは CHR2FIX を使います
CHR2FIX は char 型の整数 buf[i] を Ruby の Fixnum に変換してくれるマクロです

あとは配列を return すれば OK です
Ruby 側で参照しても同じ結果になることが確認できると思います

逆に C 側で Ruby の配列を参照するには

Ruby 側で生成した Array クラスのオブジェクトを今度は C 側の uint8_t 型のポインタに格納してみます

require 'inline'

class MyTest
  inline do |builder|
    builder.add_compile_flags '-x c++', '-std=c++11'
    builder.include '<stdio.h>'
    builder.c %q[
      void show_ruby_ary(VALUE ary) {
        // long len = RARRAY_LEN(ary);
        struct RArray *a = RARRAY(ary);
        long len = a->as.heap.len;
        VALUE *p = RARRAY_PTR(ary);
        uint8_t *buf = (uint8_t *)malloc(len * sizeof(uint8_t));
        for (long i = 0; i < len; i++) {
          buf[i] = NUM2CHR(p[i]);
          printf("%ld: %02x\n", i, buf[i]);
        }
        free(buf);
      }
    ]
  end
end

ary = 16.times.map { |i| '@'.ord }
ary.each_with_index { |r, i|
  puts sprintf("%d: %02x", i, r)
}
p = MyTest.new
p.show_ruby_ary(ary)

inline を使っている場合ですが builder.c_raw ではなく builder.c を使います
c_raw は記載した C のコードをそのまま使うのですが c は inline 側で必要な型変換などを自動で行ってくれます
もし c_raw で定義したい場合は VALUE show_ruby_ary(int argc, VALUE *argv, VALUE self) のような感じで関数を定義する必要があります

ポイントは struct RArray *a = RARRAY(ary); で C で使える構造体 RArray に変換している点です
こうすることで配列の長さや値を管理するポインタを参照できるようになります
今回の場合 @ の文字コードを Ruby 側で代入しているので同じように文字コードとして認識されるために NUM2CHR を使ってコンバートしています

実行してみると同じような結果が表示されると思います

Struct を使って共有するには

だいぶ長くなりますが Struct を使って配列のデータを Ruby と C 間で共有する方法も紹介します
この方法を使えば C 側でも Struct がメモリ空間に保存されるので別の関数からでも Struct に保存した配列データを参照することができます

require 'inline'

class MyTest
  inline do |builder|
    builder.add_compile_flags '-x c++', '-std=c++11'
    builder.include '<stdio.h>'
    builder.include '<malloc/malloc.h>'
    builder.prefix '
      typedef struct {
        uint8_t *buf;
      } Data;
    '
    builder.c_singleton %q[
      VALUE allocate() {
        Data *p = ALLOC(Data);
        return Data_Wrap_Struct(self, NULL, free, p);
      }
    ]
    builder.c %q[
      void data2struct() {
        Data *p;
        Data_Get_Struct(self, Data, p);
        uint8_t *buf = (uint8_t *)malloc(16 * sizeof(uint8_t));
        for (unsigned long i = 0; i < malloc_size(buf); i++) {
          buf[i] = '@';
        }
        p->buf = buf;
      }
    ]
    builder.c_raw %q[
      static VALUE buf(int argc, VALUE *argv, VALUE self) {
        Data *p;
        Data_Get_Struct(self, Data, p);
        VALUE result = rb_ary_new2(malloc_size(p->buf));
        for (unsigned long i = 0; i < malloc_size(p->buf); i++) {
          rb_ary_store(result, i, CHR2FIX(p->buf[i]));
        }
        return result;
      }
    ]
  end
end

p = MyTest.new
p.data2struct
p.buf.each { |b|
  puts b.chr
}

data2struct で C 側で配列データを作成し struct Data のフィールド buf に値を保存します
配列のサイズは 16 でそれぞれに @ を格納しています

次に buf メソッドを作成します
本来は builder.accessor 'buf', 'uint8_t *' としてアクセサで定義したいのですが inline の場合できません
inline ないで @type_map があらかじめ定義されておりそこに uint8_t * がないためです
なので、Struct に保存した buf を取得することができる関数を自分で作成している感じです

buf は c_raw で定義しています
やっていることは単純で rb_ary_store を使って Ruby に返す用の配列に Struct に保存した buf の値を 1 つ 1 つ格納しているだけです
これで Ruby 側で buf の内容を表示すると C 側で格納した @ が 16 個表示されると思います

最後に

Ruby と C 間で配列データをやり取りする方法をいろいろと考えてみました
ポイントは必ず Ruby および C で扱える型に変換してあげる必要があることです
今回のサンプルでは配列に含める型は完全に決め打ちですが、Check_Type というマクロが用意されているのでそれに応じて挙動を変更することも可能です
この辺りの C とのやりとりは ruby.h に使われているマクロや関数を使っています
NativeExtensions を開発するときも同じような技術が必要になるので、ruby.h を使えるようになるのがてっとり早いと思います

今回のサンプルは inine を使っているので NativeExtensions ではそのまま使えませんが使える箇所は多いと思います

参考サイト

0 件のコメント:

コメントを投稿