概要
前回 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 件のコメント:
コメントを投稿