2019年4月3日水曜日

NativeExtensions でブロックを引数に取ってみる

概要

NativeExtensions の開発でブロックを渡せるメソッドを作成してみました

環境

  • macOS 10.14.3
  • Ruby 2.5.1p57

とりあえずブロックを文字列として扱う

まずは動かしてみましょう
ブロックの引数として受け取った引数に「hello」という文字列をセットするサンプルです

#include "ruby.h"

static VALUE wrap_yield_test(VALUE self) {
  VALUE result;
  if (!rb_block_given_p()) {
    rb_raise(rb_eArgError, "Expected block");
  }
  result = rb_yield(rb_str_new2("hello"));
  return result;
}

void Init_firstext() {
  VALUE fe = rb_define_class("FirstExt", rb_cObject);
  rb_define_method(fe, "yield_test", wrap_yield_test, 0);
}

呼び出す場合は以下のよういになります
ちゃんと文字列クラスのオブジェクトが返ってくるのでそのまま upcase メソッドなどが呼べます

RSpec.describe FirstExt do
  it "create a instance" do
    fe = FirstExt.new
    fe.yield_test { |c|
      puts c.upcase
    }
    # fe.yield_test("a")
  end
end

rb_block_given_p は引数がブロックとして渡されているかチェックできます
ブロックでない場合は引数エラーを返しています

独自のクラスのオブジェクトを扱う

ブロック内で独自のクラスのオブジェクトを扱いケースは多いと思います
そのようなケースで使えるサンプルになります
急にコード量が増えてしまいます、、、

#include "ruby.h"
#include "stdio.h"

VALUE fe;

typedef struct {
  char *name;
} 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_yield_test(VALUE self) {
  VALUE result;
  if (!rb_block_given_p()) {
    rb_raise(rb_eArgError, "Expected block");
  }
  FirstExt *p;
  Data_Get_Struct(self, FirstExt, p);
  result = rb_yield(Data_Wrap_Struct(fe, 0, 0, p));
  return result;
}

static VALUE wrap_set_name(VALUE self, VALUE _name) {
  FirstExt *p;
  Data_Get_Struct(self, FirstExt, p);
  p->name = StringValuePtr(_name);
  return Qnil;
}

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

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

まず、クラスに紐づく Struct が必要になるので定義します
そして (おなじみの) alloc と free 関数を準備し rb_define_alloc_func で登録します

ポイントは rb_yield(Data_Wrap_Struct(fe, 0, 0, p)); で FirstExt Struct を Ruby のオブジェクトに変換し rb_yield に渡してる点です
これでブロックに渡された引数にクラスのオブジェクトが設定されます

使い方は以下の通りです

RSpec.describe FirstExt do
  it "create a instance" do
    fe = FirstExt.new
    fe.yield_test { |c|
      c.name = "hawk"
    }
    puts fe.name
  end
end

ブロック内でオブジェクトの初期化をして、それを別の場所で参照するみたいな使い方ができるようになります

最後に

NativeExtensions でブロック引数を扱う方法を紹介しました
ポイントは rb_yield かなと思います

参考サイト

0 件のコメント:

コメントを投稿