2019年3月31日日曜日

NativeExteions で別のクラスの Struct を return する関数を考える

概要

NativeExtensions でインスタンスメソッドを定義する場合大抵は FIX2INTrb_str_new2 を使って値を返します
ただケースとしてインスタンスメソッドが Struct (VALUE) を返したい場合があると思います
かつ self であれば簡単に返せますが self ではない Struct を返したいケースもあると思います
今回はその方法を紹介します

環境

  • macOS 10.14.3
  • Ruby 2.5.1p57

サンプルコード

FirstExt というクラスと FirstExt という Struct 、SecondExt と SecondExt の Struct が紐付いています
FirstExt のインスタンスメソッドに second というメソッドがありこれが SecondExt のインスタンスを返却しています
詳細は後述しています

#include <ruby.h>

VALUE se;

typedef struct {
  char *name;
  int age;
} FirstExt;

typedef struct {
  char *address;
  char *phone_number;
} SecondExt;

static void first_ext_free(FirstExt *s) {
  free(s);
}

static VALUE first_ext_alloc(VALUE self) {
  FirstExt *s = ALLOC(FirstExt);
  return Data_Wrap_Struct(self, 0, first_ext_free, s);
}

static void second_ext_free(SecondExt *s) {
  free(s);
}

static VALUE second_ext_alloc(VALUE self) {
  SecondExt *s = ALLOC(SecondExt);
  return Data_Wrap_Struct(self, 0, second_ext_free, s);
}

static VALUE wrap_second(VALUE self, VALUE _address, VALUE _phone_number) {
  char *address = StringValuePtr(_address);
  char *phone_number = StringValuePtr(_phone_number);
  SecondExt *s;
  Data_Get_Struct(self, SecondExt, s);
  s->address = address;
  s->phone_number = phone_number;
  return Data_Wrap_Struct(se, 0, 0, s);
}

static VALUE wrap_show(VALUE self, VALUE _address, VALUE _phone_number) {
  SecondExt *s;
  Data_Get_Struct(self, SecondExt, s);
  printf("%s\n", s->address);
  printf("%s\n", s->phone_number);
  return Qnil;
}

void Init_firstext(void) {
  VALUE fe = rb_define_class("FirstExt", rb_cObject);
  se = rb_define_class("SecondExt", rb_cObject);
  rb_define_alloc_func(fe, first_ext_alloc);
  rb_define_alloc_func(se, second_ext_alloc);
  rb_define_method(fe, "second", wrap_second, 2);
  rb_define_method(se, "show", wrap_show, 0);
}

ポイント

基本的な allocfreerb_define_classrb_define_method の説明は省略します
最大のポイントは wrap_second メソッドです

static VALUE wrap_second(VALUE self, VALUE _address, VALUE _phone_number) {
  char *address = StringValuePtr(_address);
  char *phone_number = StringValuePtr(_phone_number);
  SecondExt *s;
  Data_Get_Struct(self, SecondExt, s);
  s->address = address;
  s->phone_number = phone_number;
  return Data_Wrap_Struct(se, 0, 0, s);
}

一度 Data_Get_Struct で構造体情報を取得し各フィールドに値をセットします
そして事前に rb_define_class で定義した VALUE se クラスを元に Data_Wrap_Struct に構造体のポインタ (s) と返り値として返したいクラス (se) の情報を渡すことで Ruby で扱えるクラスのオブジェクトに変換し return しています
この際に second_ext_free の関数は指定しません
なぜならばここで作成したオブジェクトが参照されなくなった際には second_ext_free が自動でコールされます
そこに更に second_ext_free をコールするように指定してしまうと double free というメモリ参照エラーを起こしてしまうからです

テスト

テストしてみます

RSpec.describe FirstExt do
  it "create a instance" do
    cli = FirstExt.new
    cli.second('123-4567', '080-1234-5678').show
  end
end

こんな感じでメソッドチェイン的にコールできます
second メソッドの返り値が SecondExt クラスのインスタンスなのでそのまま show が呼べるようになっています

最後に

NativeExtensions の関数で self ではない別のクラスのオブジェクトを返却する方法を紹介しました
Ruby だとあるメソッドの返り値がクラスになっていて、そのフィールドを参照するという使い方はよくあるので、NativeExtensions でそれを実現するためには必須の方法かなと思います
もしかするとこれがベストプラクティスではなく、これ以外にも方法があるかもしれません

0 件のコメント:

コメントを投稿