2019年4月1日月曜日

NativeExtensions で accessor を作る

概要

NativeExtensions で accessor (所謂 getter/setter) を作ってみたいと思います
当然ですが attr_accessor のようなメソッドはないので accessor となるメソッドを準備する必要があります

環境

  • macOS 10.14.3
  • Ruby 2.5.1p57

サンプルコード

#include <ruby.h>

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

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 VALUE wrap_set_name(VALUE self, VALUE _name) {
  char *name = StringValuePtr(_name);
  FirstExt *s;
  Data_Get_Struct(self, FirstExt, s);
  s->name = name;
  return Qnil;
}

static VALUE wrap_get_name(VALUE self) {
  FirstExt *s;
  Data_Get_Struct(self, FirstExt, s);
  return rb_str_new2(s->name);
}

void Init_firstext(void) {
  VALUE fe = rb_define_class("FirstExt", rb_cObject);
  rb_define_alloc_func(fe, first_ext_alloc);
  rb_define_method(fe, "name=", wrap_set_name, 1);
  rb_define_method(fe, "name", wrap_get_name, 0);
}

テストコード

RSpec.describe FirstExt do
  it "create a instance" do
    cli = FirstExt.new
    cli.name = 'hawksnowlog'
    puts cli.name
  end
end

ポイント

setter の定義をするのに rb_define_method(fe, "name=", wrap_set_name, 1); としているのが最大のポイントだと思います
普通にオペレーションを含む関数名を定義することができるようです
これで Ruby の accessor のように構造体のメンバを参照することができます

ただこのままだとの実装だと 1 つ問題になる点があります
それは name に何もセットされていない状態で cli.name として参照すると Segmentation fault が発生してしまいます
なので getter となる wrap_get_name 関数は以下のように NULL チェックを入れることが望ましいです

static VALUE wrap_get_name(VALUE self) {
  FirstExt *s;
  Data_Get_Struct(self, FirstExt, s);
  if (RTEST(s->name)) {
    return rb_str_new2(""); // return Qnil;
  } else {
    return rb_str_new2(s->name);
  }
}

これで値が設定されていない場合は空の文字列が返るので Segmentation fault が発生しません
こんな感じのチェックは NativeExtensions の開発では日常茶飯事で出てくるので覚えておいて損はないテクニックだと思います

最後に

NativeExtensions で getter/setter を実装する方法を紹介しました
おそらくこれが一番単純で簡単なやり方かなと思います

今回はクラスに対応する Struct のメンバにアクセスする accessor を作りましたが単純に Struct に対して同じことをしても良いかなと思います

参考サイト

0 件のコメント:

コメントを投稿