2019年3月7日木曜日

RubyInline で Ruby <-> C言語間でデータのやり取りをする方法

概要

過去に inline の基本的な使い方を紹介しました
今回は少し複雑な処理として Ruby と C言語間で Struct のやり取りを行ってみました
ポイントは NativeExtension 用の関数を使う点です

環境

  • macOS 10.14.3
  • Ruby 2.5.1p57
    • RubyInline 3.12.4

サンプルコード

Ruby <-> C 間で struct のやり取りをするサンプルコードの全体です

require 'inline'

class MyTest
  inline do |builder|
    # builder.add_compile_flags '-x c++', '-std=c++11'
    builder.prefix '
      typedef struct {
        char *name;
        int age;
      } Person;
    '
    builder.c_singleton '
      VALUE allocate() {
        Person *p = ALLOC(Person);
        return Data_Wrap_Struct(self, NULL, free, p);
      }
    '
    builder.struct_name = 'Person'
    builder.accessor 'name', 'char *'
    builder.accessor 'age', 'int'
    builder.c '
      void set() {
        Person *p;
        Data_Get_Struct(self, Person, p);
        p->name = (char*)"hawk";
        p->age = 10;
      }
    '
    builder.c '
      void show() {
        Person *p;
        Data_Get_Struct(self, Person, p);
        printf("%s\n", p->name);
        printf("%d\n", p->age);
      }
    '
  end
end

p = MyTest.new
p.set
p.show
p.name = "snowlog"
p.age = 20
p.show

少し長いですがこんな感じです
実行すると以下のようになります

hawk
10
snowlog
20

解説

簡単に上記サンプルコードについて説明します
自信がないので間違っていれば訂正をお願いしたいです

構造体の定義

まず C 側で使用する構造体を定義します
構造体の定義は builder.prefix で定義します (#define などもここです)
builder.prefix の定義だけでは C 側からしか参照できません

Ruby 側から構造体を参照する準備

Ruby 側から参照するために builder.struct_name, builder.accessor を定義します
struct_name は C 側で定義した構造体を指定します
この構造体を Ruby 側のクラスで使いますという定義になります
accessor は構造体のメンバーに Ruby 側からどうやってアクセスするかの定義をします
例えば builder.accessor 'name', 'char *' は name という char * 型のメンバーに対して name というアクセサ名でアクセスできるようにする定義になります
構造体に含まれるメンバー分定義しましょう

C 側に構造体のメモリ領域を確保する

そして 1 つ目のポイントの builder.c_singleton です
これは Ruby 側で構造体を作成した際に自動的にコールされます
自動的に構造体を格納するためのメモリ領域を確保してくれます
今回であれば、MyTest.new のタイミングで呼ばれています (たぶん)
定義しないと次の C 側で構造体を参照する際にエラーになります

C 側で構造体を参照する

set および show 関数でメモリ領域を確保した構造体にアクセスします
まず set ですが構造体のメンバーに値をセットしています
ポイントは Data_Get_Struct(self, Person, p); です
これでメモリ上に確保した構造体のポインタをセットします
最後の p に構造体のポインタが代入されるのであとは p を参照すれば OK です
p はポインタなのでアローで参照しましょう
同じように show をメモリ領域からポイントを取り出して参照しています
showprintf しているだけです

動作確認

今回はちゃんと Ruby <-> C 間のやり取りがうまくいっているか確認するために set -> show したあとに Ruby 側でメンバを参照して値を代入し直し再度 show しています
結果を見るとわかりますがちゃんと Ruby 側の反映がされて C 側で表示されていることがわかると思います

おまけ: struct in struct

構造体の中に更に構造体を入れた場合はあると思います
Ruby からの参照はできませんが、C 内だけであれば使えました

builder.prefix '
  typedef struct {
    char *type;
  } Job;
  typedef struct {
    char *name;
    int age;
    Job job;
  } Person;
'

という感じで構造体を定義して

builder.c '
  void set() {
    Person *p;
    Data_Get_Struct(self, Person, p);
    p->name = (char*)"hawk";
    p->age = 10;
    p->job.type = (char*)"se";
  }
'

こんな感じで参照すればメモリ上に格納されます
ただ jot.type を Ruby 側から参照する方法がわかりませんでした
builder.accessor 'job', 'Job' という感じで定義しても Job というタイプは存在しないと言われてエラーになります
VALUE を使えば何とかなりそうですがわかりませんでした、、

最後に

RubyInline で Ruby <-> C 言語間の構造体データのやり取りを行ってみました
int や char だけなら特に気にすることなく返り値と引数でやり取りすればいいのですが、構造体になるといきなりハードルが上がる感じがします

今回使った Data_Wrap_StructData_Get_Struct, VALUE は Ruby で NativeExtension を開発する際に必要になる関数です
RubyInline も内部的には NativeExtension 用の関数をコールしています
おそらくですが RubyInline はそのような関数を使わなくても Ruby から C 言語を簡単に参照できるライブラリかなと思います
なので、今回のように NativeExtension の関数を直接呼ぶような実装をするのであれば初めから NativeExtension として書いたほうが良いかもしれません
その辺りの感覚やベストプラクティスは開発経験を積まないと何とも言えないかなと思います

参考サイト

0 件のコメント:

コメントを投稿