ラベル NativeExtensions の投稿を表示しています。 すべての投稿を表示
ラベル NativeExtensions の投稿を表示しています。 すべての投稿を表示

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 かなと思います

参考サイト

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 に対して同じことをしても良いかなと思います

参考サイト

2019年3月31日日曜日

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

概要

NativeExtensions でインスタンスメソッドを定義する場合大抵は FIX2INT や rb_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);
}

ポイント

基本的な alloc や free 、 rb_define_class や rb_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 でそれを実現するためには必須の方法かなと思います
もしかするとこれがベストプラクティスではなく、これ以外にも方法があるかもしれません

2019年3月30日土曜日

NativeExtensions で method chain 的な使い方をする方法

概要

NativeExtensions で作ったライブラリでも Ruby 側で使うときは method chain 的にドットを使ってメンバなどを参照したいです
そんな場合に使えるサンプルを紹介します

環境

  • macOS 10.14.3
  • Ruby 2.5.1p57

サンプルコード

ポイントは wrap_chain 関数が self を return している点です
こうすることで別のメソッドをドットで繋げてコールすることができます

static VALUE wrap_chain(VALUE self, VALUE _name) {
  MyStruct *s;
  Data_Get_Struct(self, MyStruct, s);
  char *name = StringValuePtr(_name);
  s->name1 = name;
  return self;
}

static VALUE wrap_name(VALUE self) {
  MyStruct *s;
  Data_Get_Struct(self, MyStruct, s);
  return rb_str_new2(s->name1);
}

ちなみに MyStruct は以下のような感じです

typedef struct {
  char *name1;
} MyStruct;

使う

こんな感じで chanin.name という感じで参照できます
chain が Qnil を返すような関数にすると再度 cli から参照する必要があります

cli = FirstExt.new
puts cli.chain("hawksnowlog").name

2019年3月29日金曜日

Ruby の NativeExtensions で boolean を引数に取る方法

概要

C 側で Ruby の Boolean を扱う方法です

環境

  • macOS 10.14.3
  • Ruby 2.5.1p57

サンプル

C 側のコードは以下の通りです

static VALUE wrap_bool_test(VALUE self, VALUE _bool) {
  int bool = RTEST(_bool);
  if (bool == 1) {
    return Qtrue;
  } else {
    return Qfalse;
  }
}

ポイントは RTEST で C 側で扱える int に変換している点です
RTEST は Qfalse or Qnil が Ruby から渡ってきた場合に真 (1) を返すマクロです

テストコード

Ruby 側でコールする方法です

RSpec.describe FirstExt do
  it "create a instance" do
    cli = FirstExt.new
    puts cli.bool_test(true)
    puts cli.bool_test(false)
    puts cli.bool_test(1)
    puts cli.bool_test('a')
  end
end

結果は以下の通りです

true
false
true
true

結果を見るとわかりますが完全に Ruby の Bool と C 側の変換が完全互換でできているわけでありません
false を渡しているときだけ false が返ってきていますが true 以外を渡しても true が返ってきてしまっています

最後に

NativeExtensions で Boolean を扱う方法を紹介しました
おそらくこれが一番簡単な方法だと思います

参考サイト

2019年3月22日金曜日

rb_raise で指定可能なエラーの一覧

概要

rb_raise は ruby の C API で C 側から Ruby のエラーを発生させることができます
第一引数でエラーの種類を指定することができるのですが情報が見つからないので自分で出してみました
デフォルトで用意されているエラーの一覧になります

環境

  • macOS 10.14.3
  • Ruby 2.5.1p57

一覧

  • rb_eArgError -> ArgError
  • rb_eEOFError -> EOFError
  • rb_eFloatDomainError -> FloatDomainError
  • rb_eIOError -> IOError
  • rb_eIndexError -> IndexError
  • rb_eLoadError -> LoadError
  • rb_eNameError -> NameError
  • rb_eNoMemError -> NoMemError
  • rb_eNotImpError -> NotImpError
  • rb_eRangeError -> RangeError
  • rb_eRuntimeError -> RuntimeError
  • rb_eScriptError -> ScriptError
  • rb_eSecurityError -> SecurityError
  • rb_eStandardError -> StandardError
  • rb_eSyntaxError -> SyntaxError
  • rb_eSystemCallError -> SystemCallError
  • rb_eTypeError -> TypeError
  • rb_eZeroDivError -> ZeroDivError

どうやって出したの

RDoc に管理してる定数があったのでそこから引っ張ってきています
RDoc で管理されているおそらく正解だと思います、、

require 'rdoc'

RDoc::KNOWN_CLASSES.each { |k, v| puts "* #{k} -> #{v}" if k.include?('Error') }

2019年3月18日月曜日

NativeExtensions な gem に rdoc のコメントを入れてドキュメントを生成する方法

概要

c のソースファイル上で普通に rdoc のタグが使えます
今回はいろいろ試して実際にドキュメントを作成してみました
またプロジェクトなどは前回までのものを流用しています

環境

  • macOS 10.14.3
  • Ruby 2.5.1p57

とりあえずコメントを入れてみる

例えば NativeExtensions として定義した C の関数に以下のようにコメントを入れてみます

  • vim ext/firstext/firstext.c
/*
 * Set data to struct members.
 */
static VALUE wrap_set(VALUE self, VALUE _index) {
  int index = FIX2INT(_index);
  MyStruct *s;
  Data_Get_Struct(self, MyStruct, s);
  char *name = (char *)"hawk1";
  if (index == 0) {
    s->name1 = name;
    s->age = 10;
  } else if (index == 1) {
    s->name2 = name;
    s->age = 20;
  } else {
    s->name3 = name;
    s->age = 30;
  }
  return Qnil;
}

ドキュメントを作成する

rdoc コマンドが使えるのでそれで生成します

  • bundle exec bundle rdoc ext/ lib/

ext/ と lib/ 配下を対象に rdoc を作成します
これを指定しないと Gemfile や README.md やらいろいろと含まれてしまうので対象のディレクトリを指定しています
今回は YARD ではなく rdoc の Darkfish を使います

成功すると doc/ ディレクトリができているのでその配下の index.html を開きましょう

  • open doc/index.html

こんな感じで関数に設定したコメントがドキュメントにも反映されていると思います
右側に「toggle a source」もあるので選択すると C のソースファイルを確認することもできます

クラスにコメントを入れる

NativeExtensions でクラスを追加 (rb_define_class) した場合に、そのクラスにコメントを入れる方法です

  • vim ext/firstext/firstext.c
/*
 * My first native extensions.
 */
void Init_firstext(void) {
  VALUE fe = rb_define_class("FirstExt", rb_cObject);
  rb_define_alloc_func(fe, my_struct_alloc);
  rb_define_method(fe, "set", wrap_set, 1);
  rb_define_method(fe, "show", wrap_show, 1);
  Init_secondext();
}

こんな感じで Init_ から始まる関数の前にコメントを入れれば OK です
rdoc の parser が rb_define_class で定義しているクラス名から Init_ が付くメソッドを自動で探してコメントを解析しているようです

ちなみに Init_ がない場合には Document-class: name という命令を使って好きな箇所にコメントを入れることでクラスの説明を追加することもできます

引数の説明や返り値の説明を入れる

NativeExtensions で作成した関数の場合、引数などの表示が p1 みたいな感じでポインタを表す文字だけになってしまうようです
これだとよくわからないので関数の引数や返り値に対して説明を追記してあげましょう
例えば以下のような感じです

/*
 * Debug struct members.
 *
 * call-seq:
 *   show(index) -> nil
 *
 * ===== Parameters
 * - index - Member's index.
 *
 * ===== Return
 * - nil
 */
static VALUE wrap_show(VALUE self, VALUE _index) {
  // ...
}

結論から言うと rdoc には引数や返り値を説明するための命令はありません
代わりに markup が用意されておりそれらを駆使してそれっぽい説明に見せる感じです
例えば上記の場合だと ===== は見出しようの markup です
- はリストを表示することができる markup でこれで各引数の説明を記載しています
これらはあくまでも自分のやり方になるので、こうじゃなくても OK です
再度説明しますが YARD のような @param や @return のような命令が rdoc にはないので markup を駆使する感じになります

ただ rdoc にも call-seq: という命令がありこれで「関数をどう呼び出すか」を定義することができます
なのでデフォルトで引数が p1 となっていた部分を適切な引数名に変えることができるようになります
上記で生成したドキュメントは以下のようになります

Constants と VERSION を設定する

まず Constants ですがこれは定数がそもそもないと表示されません
rb_define_const で定義した定数に対してコメントを入れれば rdoc でも表示されます

/*
 * My first native extensions.
 */
void Init_firstext(void) {
  VALUE fe = rb_define_class("FirstExt", rb_cObject);
  rb_define_alloc_func(fe, my_struct_alloc);
  rb_define_method(fe, "set", wrap_set, 1);
  rb_define_method(fe, "show", wrap_show, 1);
  /*
   * Set max score with 100.
   */
  rb_define_const(fe, "SCORE", INT2FIX(100));
  Init_secondext();
}

こんな感じです
次に VERSION です
VERSION は lib/firstext/version.rb ですでに定義されている定数です
versions.rb でコメントしても OK ですが今回は C 側でコメントする方法を紹介します
先程の SCORE 同様 VERSION 定数を rb_define_const で定義してあげれば OK です

/*
 * My first native extensions.
 */
void Init_firstext(void) {
  VALUE fe = rb_define_class("FirstExt", rb_cObject);
  rb_define_alloc_func(fe, my_struct_alloc);
  rb_define_method(fe, "set", wrap_set, 1);
  rb_define_method(fe, "show", wrap_show, 1);
  /*
   * Set max score with 100.
   */
  rb_define_const(fe, "SCORE", INT2FIX(100));
  /* 
   * 1.0.0: The version of this package
   */
  rb_define_const(fe, "VERSION", rb_str_new2("1.0.0"));
  Init_secondext();
}

ちなみに値は version.rb が優先されます
これで以下のように表示されます

Pages と Table of Contents

よく Pages と Table of Contents が表示されている rdoc があると思います
これらは .rdoc ファイルや .md ファイルが直接表示されています
例えば rdoc 実行時に以下のように README.md を指定しましょう

  • bundle exec rdoc ext/ lib/ README.md

すると左ペインに Pages が追加されて README.md が項目にあると思います
それを選択すると Markdown がパースされて Table of Contents が自動的に生成されるというようになっています

最後に

NativeExtensions として開発した gem にコメントを入れてみました
今回は rdoc 形式なのでご注意ください
最近だと YARD 形式しか見ない気もします、、

rdoc を使う場合には参考にしてみてください

参考サイト

2019年3月17日日曜日

複数の .c ファイルを作成して NativeExtensions を開発する方法

概要

NativeExtensions でクラスやモジュールを複数定義した場合があると思います
構造体も異なる場合は別の .c ファイルを作成したほうが良い場合があると思います
今回はその方法を紹介します

環境

  • macOS 10.14.3
  • Ruby 2.5.1p57

.c ファイルを追加する

まずは別のクラスを管理する C のコードを追加しましょう

  • touch ext/firstext/secondext.c
  • vim ext/firstext/secondext.c
#include <ruby.h>

void Init_secondext(void) {
  VALUE se = rb_define_class("SecondExt", rb_cObject);
}

とりあえずクラスだけ定義しています
中身は空です

エントリーポイントとなる .c ファイルから参照する

NativeExtensions は Makefile 内で TARGET_ENTRY を指定しているのですが 1 つしか指定しません
なので、そのエントリーポイントから追加した secondext.c を参照します

  • vim ext/firstext/firstext.c
#... ↑ 省略

void Init_firstext(void) {
  VALUE fe = rb_define_class("FirstExt", rb_cObject);
  rb_define_alloc_func(fe, my_struct_alloc);
  rb_define_method(fe, "set", wrap_set, 0);
  rb_define_method(fe, "show", wrap_show, 0);
  Init_secondext();
}

最後の Init_secondext(); を 1 行追加しただけです
今回はヘッダファイル (.h) は作成しておらずソースファイル (.c) だけです
同一階層にソースファイルを追加した場合は include しなくても別ソースファイルの関数を参照できました

コンパイル&テスト

  • bundle exec rake clean
  • bundle exec rake compile

でいつも通り lib/firstext.bundle が作成されます
ちゃんと SecondExt が使えるかテストしましょう

RSpec.describe FirstExt do
  it "has a version number" do
    expect(FirstExt::VERSION).not_to be nil
  end

  it "create a instance" do
    cli = FirstExt.new
    cli.set
    cli.show
    cli = SecondExt.new
  end
end
  • bundle exec rake spec

でちゃんと SecondExt クラスが new できるようになっていると思います
クラスが見つからないというエラーが出る場合は再度コンパイルし直してみてください

最後に

NativeExtensions の開発時に複数のソースファイルに分けて開発する際のポイントを紹介しました
基本的にはクラスやモジュール単位でソースファイルも分かると良いかなと思います

今回はヘッダファイルを作成していません
お作法的にはちゃんとヘッダファイルを書くべきです
他に気になるワードとしては depend という機能がありこれに依存関係を書いておくと依存関係のあるファイルを自動でコンパイルしてくれます
rake-compiler を使っている場合はもしかしたら不要っぽかったので今回は使っていません

参考サイト

2019年3月16日土曜日

NativeExtensions を gem ファイルにパッケージする方法

概要

通常の gem を作成する方法とほぼ変わりませんが rake compile したり gemspec ファイルの編集が必要になります
プロジェクトの構成や rake-compiler については前回の記事を確認してください

環境

  • macOS 10.14.3
  • Ruby 2.5.1p57

.gemspec の編集

spec.extensions を追記します

Gem::Specification.new do |spec|
  spec.name          = "firstext"
  spec.version       = FirstExt::VERSION
  spec.extensions    = ["ext/firstext/extconf.rb"]
  # ...
end

途中は長いので省略しています
好きな箇所に追記してもらえれば OK です

コンパイル

最新の .so or .bundle ファイルを作成します

  • bundle exec rake compile

git commit

ビルドする前に gem に含めるファイルをコミットしなければいけません

  • git add .
  • git commit -m "add resources"

ビルド

gem ファイルを作成します
通常の gem を作成する方法と同じコマンドで作成できます

  • bundle exec rake build

これで pkg/firstext-0.1.0.gem が作成されます

ローカルインストール

ローカルでテストしたい場合は実施します

  • cd pkg
  • gem install -l firstext-0.1.0.gem

gem の作り直し

  • bundle exec rake clobber
  • bundle exec rake build
  • gem uninstall firstext
  • gem install -l firstext-0.1.0.gem

make install でインストールしてしまった場合は .so ファイルなどがすでに配置されてしまっており、そちらが優先されている可能性があります
macOS の場合は

  • rm -rf /usr/local/lib/ruby/site_ruby/2.5.0/x86_64-darwin17/*

あたりを実行してから gem install してください

その他

NativeExtensions の場合ヘッダファイルやライブラリファイルのインストールが事前に必要な場合があるので apt or yum でインストールしておきましょう
apt や yum でインストールできないライブラリもあるので、その場合は手動で適切なパスに配置してあげます
大抵は必要なヘッダファイルやライブラリは gem のドキュメントに記載されているはずです
ない場合は直接 extconf.rb などを見るしかないと思います

最後に

NativeExtensions を gem ファイルにする方法を紹介しました
あとは通常の rake release の手順で公開することができます

ハマりそうなのは git commit が必要な点かなと思います
未コミットのファイルは対象外になるのでファイルが見つからないなどのエラーが出る場合はちゃんとコミットされているか確認してみてください

2019年3月15日金曜日

NativeExtensions のテスト書く方法

概要

前回 NativeExtensions で Struct を使う方法を紹介しました
ライブラリを書けば当然テストしたくなります
NativeExtensions の場合少しクセがあるのでやり方の流れを紹介します

環境

  • CentOS 7.5.1804
  • Ruby 2.5.0p0

必要なファイル

spec ファイル関連のファイルが必要になります
bundle gem でプロジェクトを作成する際に -t オプションがあればテストに必要なファイルを一通り作成してくれます

.rspec
spec/firstext_spec.rb
spec/spec_helper.rb

rake-compiler のインストール

これを使ったほうが簡単 (というか公式のやり方) なのでこれを使います

  • vim firstext.gemspec
spec.add_development_dependency "rake-compiler"
  • bundle install --path vendor

Rakefile の編集

compile タスクを使えるようにします

  • vim Rakefile
require "bundler/gem_tasks"
require "rspec/core/rake_task"
require "rake/extensiontask"

Rake::ExtensionTask.new "firstext" do |ext|
  ext.lib_dir = "lib"
end

RSpec::Core::RakeTask.new(:spec)

task :default => :spec

ポイントは ext.lib_dir = "lib" の部分です
このパスに Makefile から作成される so ファイルを配置します
このパスに配置することで require 'firstext' できるようになります
ただ、このパスにはすでに lib/firstext.rb があります
どうやらこのファイルが優先されるようで、これがあるとせっかく C で実装したコードが参照されません
なので削除しておきます

  • rm lib/firstext.rb

rb ファイルを残したい場合は rake compile 時に吐き出されるファイル名を変更する方法もあります
が、その場合は require するのも吐き出されるファイル名になるのでご注意ください

lib/firstext/version.rb

module になっている部分を class に変更します
ここが module のままだと module が優先されて NativeExtensions 側のクラスが参照できなくなります

  • vim lib/firstext/version.rb
class FirstExt
  VERSION = "0.1.0"
end

spec/spec_helper.rb

特に何もしません

spec/firstext_spec.rb

ではテストを書きましょう
前回 NativeExtensions で実装した関数を呼び出しているだけです

  • vim spec/firstext_spec.rb
RSpec.describe FirstExt do
  it "has a version number" do
    expect(FirstExt::VERSION).not_to be nil
  end

  it "create a instance" do
    cli = FirstExt.new
    cli.set
    cli.show
  end
end

動作確認

  • bundle exec rake spec

でテストが動くはずです
うまく NativeExtensions で作成したクラスが参照できない場合は rake copile で so ファイルを再配置してみてください
もしくは so ファイル自体が指定のパスにあるか確認してください

また ext/firstext 配下に firstext.o や firstext.so などの成果物がある場合は削除しましょう
rake compile がエラーを吐きうまく動かないケースがあるようです

最後に

NativeExtensions で RSpec を使ってテストを書く方法を紹介しました
成果物の so ファイルを rb ファイルの代わりに配置するのがポイントです

NativeExtensions を変更した場合は再度 rake compile して成果物をデプロイする必要があります
あとは普通に spec テストを書いていけば OK です

参考サイト

2019年3月14日木曜日

NativeExtensions で Struct を使う方法

概要

Struct をメモリ空間に展開し、それに Ruby からアクセスする方法を紹介します

環境

  • CentOS 7.5.1804
  • Ruby 2.5.0p0

サンプルコード

NativeExtensions なのですべて C で書かれています
後述で説明します

#include <ruby.h>

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

static void my_struct_free(MyStruct *s) {
  free(s);
}

static VALUE my_struct_alloc(VALUE self) {
  MyStruct *s = ALLOC(MyStruct);
  return Data_Wrap_Struct(self, 0, my_struct_free, s);
}

static VALUE wrap_set(VALUE self) {
  MyStruct *s;
  Data_Get_Struct(self, MyStruct, s);
  s->name = (char *)"hawk";
  s->age = 10;
  return Qnil;
}

static VALUE wrap_show(VALUE self) {
  MyStruct *s;
  Data_Get_Struct(self, MyStruct, s);
  printf("%s\n", s->name);
  printf("%d\n", s->age);
  return Qnil;
}

void Init_firstext(void) {
  VALUE fe = rb_define_class("FirstExt", rb_cObject);
  rb_define_alloc_func(fe, my_struct_alloc);
  rb_define_method(fe, "set", wrap_set, 0);
  rb_define_method(fe, "show", wrap_show, 0);
}

説明

まずは今回の肝となる Struct を用意します
Struct 内に Struct は持たないようにしています

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

Ruby 側で FirstExt.new されたときに構造体の領域をメモリに確保する必要があります
そうしない状態で構造体にアクセスしようとすると Segmentation fault で落ちます
そのために rb_define_alloc_func(fe, my_struct_alloc); を定義します
これは new された際に my_struct_alloc メソットを呼び出し構造体のメモリ領域を確保します

my_struct_alloc は以下の通りです

static VALUE my_struct_alloc(VALUE self) {
  MyStruct *s = ALLOC(MyStruct);
  return Data_Wrap_Struct(self, 0, my_struct_free, s);
}

決まり文句のような感じです
Data_Wrap_Struct は C の構造体をラップして self クラスのインスタンスである Ruby オブジェクトを返します
これで構造体にアクセスできる準備はできました

Ruby から構造体にアクセスするためのメソッド set, show を用意します
そしてそれらは wrap_set, wrap_show を呼び出すように定義します

rb_define_method(fe, "set", wrap_set, 0);
rb_define_method(fe, "show", wrap_show, 0);

それぞれのメソッドを C 側で定義します
まずメモリ上に展開した構造体にアクセスするには Data_Get_Struct を使います
ポインタを準備することでそのポインタにアドレスを設定してくれます
あとはアロー演算子を使って構造体のメンバにアクセスすれば OK です
ここでポイントなのは構造体を参照する関数は必ず static VALUE wrap_show(VALUE self) というように引数と返り値に VALUE を使う必要があります
set は特に返り値はないですが、これを static void wrap_set(VALUE self) のように宣言してしまうといざ set をコールしたときに Segmentation fault が発生します

構造体自体をリターンするには

少し調べたのですが簡単にはいかなさそうです (というか無理そう?)
定義した構造体のメンバにアクセスするのが目的なのであれば各メンバを取得する Getter を定義してあげる感じになると思います
やり方がわかったら、また別の記事で招待したいと思います

最後に

NativeExtensions で Struct の基本的な扱い方を紹介しました
C 側にちゃんと領域を alloc してあげる必要があるのがポイントです
また構造体を参照する場合は Data_Get_Struct を使いますがこれを使う関数は VALUE な返り値と引数を必ず持つ点も重要です

2019年3月13日水曜日

NativeExtensions を開発するときに独自のヘッダファイルと so ファイルを使う方法

概要

apt や yum でインストールできるライブラリは特に気にする必要はありませんが場合によってはそれらを使ってライブラリをインストールできないことがあると思います
.h ファイルと .so ファイルが配布されておりそれらを使って NativeExtensions を開発したいことはあると思います
今回はそのような場合に .h と .so ファイルを参照する方法を紹介します

環境

  • CentOS 7.5.1804
  • Ruby 2.5.0p0

extconf.rb を編集する

extconf.rb は Makefile を作成するためのファイルです
事前に関数の存在チェックやヘッダファイルの存在チェックができます
ここで自分が作成したオリジナルのライブラリのチェックなどを行います

require "mkmf"

abort "missing malloc()" unless have_func "malloc"
abort "missing free()"   unless have_func "free"

dir_config('mylib', '/usr/lib/mylib/include', '/usr/lib/mylib/lib64')
if have_header("mylib.h") and have_library('mylib')
  create_makefile "mylib/mylib"
end

あとはヘッダファイルを /usr/lib/mylib/include/mylib.h に配置し so ファイルを /usr/lib/mylib/lib64/mylib.so に配置すれば OK です
もし配置していない場合は extconf.rb の実行中にエラーになるため NativeExtensions のインストールに失敗します

また上記のように書いておけば include と lib64 のパスを自分で指定することもできるようになります

  • ruby extconf.rb --with-mylib-include=/path/to/include --with-mylib-lib=/path/to/lib64

こんな感じです
これで Makefile が作成されるのであとは make すれば NativeExtensions が作成されます

参考サイト

2019年3月6日水曜日

Ruby で native extension な gem を作成する方法

概要

過去に gem を作成する方法を紹介しました
今回は C++ 言語を使った native extension を作成する方法を紹介します

環境

  • macOS 10.14.3
  • Ruby 2.5.1p57

プロジェクト作成

gem を開発するためのプロジェクトを作成しましょう

  • bundle gem firstext

ext ディレクトリの準備

C 言語で実装するための準備をします
決められたディレクトリとファイルを作成します
ポイントは extconf.rb と extconf.c です

  • cd firstext/
  • mkdir -p ext/firstext
  • vim ext/firstext/extconf.rb
require "mkmf"

abort "missing malloc()" unless have_func "malloc"
abort "missing free()"   unless have_func "free"

create_makefile "firstext/firstext"

malloc と free という関数が使えるかチェックします
使えない場合はエラーを返します
今回はどちらのメソッドも使いませんがサンプルなので入れています

  • touch ext/firstext/firstext.c
  • touch lib/firstext.rb

ライブラリ参照 (firstext.rb)

ビルドで作成される成果物を参照しておきます

  • vim lib/firstext.rb
require "firstext/version"
require "firstext/firstext"

extconf.rb で定義した create_makefile と同じパスを指定します

サンプルコード (firstext.c)

実際に C++ 言語でライブラリを作成します
今回は簡単な文字列を表示する関数を定義しています

  • vim ext/first/firstext.c
#include <ruby.h>

void func1(VALUE self, VALUE str) {
  char* s = StringValuePtr(str);
  printf("%s\n", s);
}

void Init_firstext(void) {
  VALUE fe = rb_define_class("FirstExt", rb_cObject);
  rb_define_method(fe, "func1", func1, 1);
}

ビルド

ビルドします
extconf.rb を実行して次に make します

  • cd ext/firstext
  • ruby extconf.rb
checking for malloc()... yes
checking for free()... yes
creating Makefile
  • make

動作確認

Mac の場合 firstext.bundle が作成されます
以下のように実行すると定義した関数をコールすることができます

  • cd ../../
  • ruby -Ilib:ext -r 'firstext' -e 'FirstExt.new.func1("hoge")'

最後に

とりあえず native extension を作ってみました
実際には C で実装するのでメモリの割り当てなどを考慮する必要があります

参考サイト