OSSチャレンジ Part7 (ruby-duckdb編)

目的

学んだことを忘れないようにメモ!

間違えたことを書いている可能性あるので、注意してください!

あと、全部教えてもらいながらやったことを書いてるだけなので、自分の力ではないです。

やりたいこと

ruby-duckdb の gemに、Result#columnsを実装したい!

github.com

実装環境構築

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

RubyGCについて教えてもらったこと

(理解間違えてたらごめんなさい)

RubyGCは、「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;

感想

CのAPI実装を介して、Rubyのことをもっとよく知れた。 よかった!