OSSチャレンジ Part7 (ruby-duckdb編)
目的
学んだことを忘れないようにメモ!
間違えたことを書いている可能性あるので、注意してください!
あと、全部教えてもらいながらやったことを書いてるだけなので、自分の力ではないです。
やりたいこと
ruby-duckdb の gemに、Result#columnsを実装したい!
実装環境構築
m1 mac の場合は、以下コマンドを叩くと、bundle installができる。
gem install duckdb -- --with-duckdb-include=/opt/homebrew/include --with-duckdb-lib=/opt/homebrew/lib --with-duckdb-includeの代わりにCPATH、--with-duckdb-libの代わりにLIBRARY_PATH といった環境変数を使ってもいけた。
homebrewのバージョンとgemのバージョンをチェックしてズレてないか注意!(これにハマってテストが通らず困った...)
とりあえず一人で実装
[#168]Add DuckDB::Result#columns · okadakk/ruby-duckdb@bb31021 · GitHub
static VALUE duckdb_result_columns(VALUE oDuckDBResult) { rubyDuckDBResult *ctx; Data_Get_Struct(oDuckDBResult, rubyDuckDBResult, ctx); idx_t col_idx; # DuckDBのCのAPIを叩く。 # https://github.com/duckdb/duckdb/blob/33c0cbee75c134464984a2751e1b615477153d2a/src/include/duckdb.h#L375 idx_t column_count = duckdb_column_count(&(ctx->result)); # 空配列を作成 VALUE ary = rb_ary_new2(column_count); for(col_idx = 0; col_idx < column_count; col_idx++) { # https://github.com/duckdb/duckdb/blob/33c0cbee75c134464984a2751e1b615477153d2a/src/include/duckdb.h#L356 const char* column_name = duckdb_column_name(&(ctx->result), col_idx); # char* ということは文字列が帰ってきてるので、Rubyの文字列型に変換してarrayに値を追加。 rb_ary_store(ary, col_idx, rb_str_new2(column_name)); } return ary; }
アドバイスもらって実装
上のままだと、単なる文字列の配列しか返ってこないから不便ということで、クラスを返したほうがいいんではないか?とアドバイスをもらったので、自分でやってみた。
[#168]Add DuckDB::Column · okadakk/ruby-duckdb@8bd57b5 · GitHub
# 構造体を作って struct _rubyDuckDBColumn { duckdb_type _type; const char *_name; }; # 値を入れておいて VALUE create_column(VALUE oDuckDBResult, idx_t col) { VALUE obj; obj = allocate(cDuckDBColumn); rubyDuckDBColumn *ctx; Data_Get_Struct(obj, rubyDuckDBColumn, ctx); rubyDuckDBResult *ctxresult; Data_Get_Struct(oDuckDBResult, rubyDuckDBResult, ctxresult); ctx->_type = duckdb_column_type(&(ctxresult->result), col); ctx->_name = duckdb_column_name(&(ctxresult->result), col); return obj; } # アクセスできるようにした VALUE duckdb_column_name_value(VALUE oDuckDBColumn) { rubyDuckDBColumn *ctx; Data_Get_Struct(oDuckDBColumn, rubyDuckDBColumn, ctx); return rb_str_new2(ctx->_name); }
あってる気しないけど、取り急ぎ動いた!
レビューもらって実装
[#168]Add DuckDB::Column · okadakk/ruby-duckdb@38502cc · GitHub
# duckdb.h を見る限り、カラム名を取得したいときは、duckdb_result と index があればいいので、それをクラス変数として定義する。 struct _rubyDuckDBColumn { VALUE result; idx_t col; }; # Columnクラスを生成する。Column.new をしてる感じ。 # Class#new は Class#allocate でインスタンスを生成し、 Object#initialize で初期化を行っているので、ほぼ同じ。 VALUE create_column(VALUE oDuckDBResult, idx_t col) { VALUE obj; // allocateをして、cDuckDBColumnに必要なメモリを確保する。 obj = allocate(cDuckDBColumn); rubyDuckDBColumn *ctx; Data_Get_Struct(obj, rubyDuckDBColumn, ctx); // 変数に値を代入する。rb_ivar_setを使うと、Rubyからでもアクセス可能。 rb_ivar_set(obj, rb_intern("result"), oDuckDBResult); ctx->col = col; return obj; } # get_name を定義 VALUE duckdb_column_get_name(VALUE oDuckDBColumn) { rubyDuckDBColumn *ctx; Data_Get_Struct(oDuckDBColumn, rubyDuckDBColumn, ctx); # rb_ivar_setをしたら、rb_ivar_getをすると値を取得できる。 VALUE result = rb_ivar_get(oDuckDBColumn, rb_intern("result")); rubyDuckDBResult *ctxresult; Data_Get_Struct(result, rubyDuckDBResult, ctxresult); return rb_str_new2(duckdb_column_name(ctxresult->result, ctx->col)); } # get_typeを定義 VALUE duckdb_column_get_type(VALUE oDuckDBColumn) { rubyDuckDBColumn *ctx; Data_Get_Struct(oDuckDBColumn, rubyDuckDBColumn, ctx); VALUE result = rb_ivar_get(oDuckDBColumn, rb_intern("result")); rubyDuckDBResult *ctxresult; Data_Get_Struct(result, rubyDuckDBResult, ctxresult); duckdb_type type = duckdb_column_type(ctxresult->result, ctx->col); switch (type) { case DUCKDB_TYPE_BOOLEAN: return ID2SYM(rb_intern("boolean")); default: return ID2SYM(rb_intern("invalid")); } }
結果
マージしてもらった! github.com
RubyのGCについて教えてもらったこと
(理解間違えてたらごめんなさい)
RubyのGCは、「mark&sweep」というアルゴリズムで動いている。
簡単にいうと、mark phazeとsweep phazeでそれぞれ処理をしていて、
mark phazeは、root objectから参照があるobjectにのみどんどんマークをつけていく。
sweep phazeは、マークがついていないobjectのメモリを解放していく。
という感じ。
今回クラスを作るときに使った Data_Wrap_Struct は、第二引数にmark時に行う処理、第三引数にsweep時に行う処理を登録できる。
macro Data_Wrap_Struct (Ruby 3.1 リファレンスマニュアル)
今回は、第三引数にメモリ解放する関数を設定したので、sweep時に、正しくメモリが解放される。 これがないと、CのAPIで作ったクラスは、メモリが解放されない?
# 変数に代入する際に二つの方法があるが、以下のように異なる。 # こっちだと、Rubyが勝手にメモリ解放とかを管理してくれる rb_ivar_set(obj, rb_intern("result"), oDuckDBResult); # こっちだと、手動でメモリ解放を正しく管理しないといけない。(ruby-duckdbではdeallocateでメモリ解放してる) ctx->col = col;