OSSチャレンジPart5 (glib実装)

目的

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

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

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

やりたいこと

RubyでArrowのMonth, Day, Nano Interval Typeを使えるようにする。

RubyC++のライブラリを使う方法

詳しくはこちら!

github.com

簡単に書くと、いくつか方法があって、RedArrowでは GObject Introspection を用いています。

C・C++で実装されたライブラリーの機能をRubyから使えるようにしたRubyのライブラリーのことを「バインディング」と呼びます。

と上のページに書いてあるように、今回はバインディングと呼ばれるRubyのライブラリーを作ります。

そのバインディングは、すでに作成されているので、今回はこちらを修正していきます。

arrow/c_glib/arrow-glib at master · apache/arrow · GitHub

作業した流れ

TemporalData型クラスを追加

cppの実装部分をRubyからの利用できるようにしたい。

この IntervalType は TemporalType を継承しているのだが、まだ TemporalType を使えるようになってなかったので、まず追加した。

ARROW-14935: [GLib] Add GArrowTemporalDataType by okadakk · Pull Request #11809 · apache/arrow · GitHub

この型は基底クラスで、特にメソッドを持っているわけではないので、定義だけ追加すればOK!

IntervalDataクラスを追加

次にIntervalData型クラスを追加する。

ARROW-15134: [GLib] Add GArrow{Month,DayTime,MonthDayNano}IntervalDataType by okadakk · Pull Request #11975 · apache/arrow · GitHub

cppでの定義はこちら

class ARROW_EXPORT IntervalType : public TemporalType, public ParametricType {
 public:
  enum type { MONTHS, DAY_TIME, MONTH_DAY_NANO };

  virtual type interval_type() const = 0;

 protected:
  explicit IntervalType(Type::type subtype) : TemporalType(subtype) {}
  std::string ComputeFingerprint() const override;
};

バインディングを作る必要があるメソッドを決める

まず、こちらのバインディングを作る際にどのメソッドを使えるようにするべきかを考える。

public:
  enum type { MONTHS, DAY_TIME, MONTH_DAY_NANO };

ここのenumは、MONTHS = 0, DAY_TIME = 1 みたいな定義をしてるだけなので、type.hに同じように追加すれば、OK!!

 protected:
  explicit IntervalType(Type::type subtype) : TemporalType(subtype) {}

ここは、ruby でいう new だが、protectedということは、外から使えないので今回は実装しなくてOK!

protected:
  std::string ComputeFingerprint() const override;

こちらも同じく外から使えないので、実装しなくてOKなのと、overrideしてるということは、継承先の方で既にバインディングを作成するべきなので、ここでは作らない。

public:
  virtual type interval_type() const = 0;

このメソッドだけは実装する必要がある。

メソッドのバインディングを作る

glibの世界とC++の世界に分けて考えるとわかりやすい。

まず、引数のGArrowIntervalDataTypeは、glibで定義した型 = Rubyから渡されたもの。

これを、まずC++の世界で利用できる形に変換する。

そして、C++で作られたメソッドを叩く。

最後に、C++のメソッドの戻り値をglib(=Ruby)の世界で利用できる形に変換する。

// GArrowIntervalType = cpp での arrow::IntervalType::type 相当
GArrowIntervalType
garrow_interval_data_type_get_interval_type(GArrowIntervalDataType *data_type) {
  // 最初の変換処理(glib -> C++)
  // GARROW_DATA_TYPE(data_type)) は、親クラスにcastしてる。
  //   - 実行時に、本当に子クラスなのかのvalidationとかもしてる
  //   - イメージはこんな感じ。(GArrowDataType*) data_type
  // garrow_data_type_get_raw で、arrow::BaseType 的な型にcastしてる。
  // std::static_pointer_cast<arrow::IntervalType> で、arrow::IntervalTypeに、castしてあげることで、C++で扱える形に変換完了!
  auto arrow_data_type =
    std::static_pointer_cast<arrow::IntervalType>(
      garrow_data_type_get_raw(GARROW_DATA_TYPE(data_type)));
  
  // C++の関数を呼ぶだけ。結果はC++で扱える形(arrow::IntervalType::type)の状態
  auto arrow_interval_type = arrow_data_type->interval_type();
  
  // 最後の変換処理(C++ -> glib)
  // arrow::IntervalType::type を GArrowIntervalTypeに変換する
  return std::static_cast<GArrowIntervalType>(arrow_interval_type);
}

子クラスを作る

ここで問題として、IntervalDataクラスは new できないので、テストができない。

そのため、テストできるように子クラスを追加して、そこに new メソッドを追加する。

GArrowIntervalMonthsDataType *
garrow_interval_months_data_type_new(void)
{
  auto arrow_data_type = arrow::month_interval();

  GArrowIntervalMonthsDataType *data_type =
    GARROW_INTERVAL_MONTHS_DATA_TYPE(g_object_new(GARROW_TYPE_INTERVAL_MONTHS_DATA_TYPE,
                                                  "data-type", &arrow_data_type,
                                                  NULL));
  return data_type;
}

テストを書く

compileして、installした後に、こんな感じに書いたら動いた

class TestIntervalMonthsDataType < Test::Unit::TestCase
  def test_type
    data_type = Arrow::IntervalMonthsDataType.new
    assert_equal(Arrow::Type::INTERVAL_MONTHS, data_type.id)
  end

  def test_interval_type
    data_type = Arrow::IntervalMonthsDataType.new
    assert_equal(Arrow::IntervalType::MONTHS, data_type.interval_type)
  end

  def test_name
    data_type = Arrow::IntervalMonthsDataType.new
    assert_equal("month_interval", data_type.name)
  end

  def test_to_s
    data_type = Arrow::IntervalMonthsDataType.new
    assert_equal("month_interval", data_type.to_s)
  end
end

ScalarClass, ArrayClassを作る

ARROW-15462: [GLib] Add GArrow{Month,DayTime,MonthDayNano}Interval{Scalar,Array,ArrayBuilder} by okadakk · Pull Request #12269 · apache/arrow · GitHub

書き方はルール的なのがあるので、他のコードや、下の説明を真似して書く。

github.com

ScalarClass

「new」,「value」を実装すればよい。

ScalarClassはC++の実装では、型定義が必要だから、Scalar型を定義する必要があるが、Rubyから利用されることはあんまりない。

Scalar型がIntとかBooleanなどそれぞれに定義することで、処理が抽象化できるので便利!

scalar.h に定義を書く

GARROW_AVAILABLE_IN_8_0
GArrowMonthIntervalScalar *
garrow_month_interval_scalar_new(gint32 value);
GARROW_AVAILABLE_IN_8_0
gint32
garrow_month_interval_scalar_get_value(GArrowMonthIntervalScalar *scalar);

scalar.c に実装を書く

GArrowMonthIntervalScalar *
garrow_month_interval_scalar_new(gint32 value)
{
  # 引数の値を、glibからC++で利用できる形に変換
  auto arrow_scalar =
    std::static_pointer_cast<arrow::Scalar>(
      std::make_shared<arrow::MonthIntervalScalar>(value));
  # C++のarrow::Scalarのtypeを見て、glibで利用できるクラスを生成
  return GARROW_MONTH_INTERVAL_SCALAR(garrow_scalar_new_raw(&arrow_scalar));
}
gint32
garrow_month_interval_scalar_get_value(GArrowMonthIntervalScalar *scalar)
{
  # glibからC++の形に変換
  const auto arrow_scalar =
    std::static_pointer_cast<arrow::MonthIntervalScalar>(
      garrow_scalar_get_raw(GARROW_SCALAR(scalar)));
  # C++のArrow実装のvalueを呼ぶ。MonthIntervalの値はただの数字なので、C++のint32が返ってくる。
  # gint32と互換性があるので変換しなくてOK!!
  return arrow_scalar->value; 
}

ArrayClass

同様に「new」,「get_value」,「get_values」を実装すればよい。

実装を一応抜粋するが、共通化がされてるので、他のところの真似をしただけでできた。

basic-array.h

GARROW_AVAILABLE_IN_8_0
GArrowMonthIntervalArray *
garrow_month_interval_array_new(gint64 length,
                                GArrowBuffer *data,
                                GArrowBuffer *null_bitmap,
                                gint64 n_nulls);
GARROW_AVAILABLE_IN_8_0
gint32
garrow_month_interval_array_get_value(GArrowMonthIntervalArray *array,
                                      gint64 i);
GARROW_AVAILABLE_IN_8_0
const gint32 *
garrow_month_interval_array_get_values(GArrowMonthIntervalArray *array,
                                       gint64 *length);

basic-array.c

GArrowMonthIntervalArray *
garrow_month_interval_array_new(gint64 length,
                                GArrowBuffer *data,
                                GArrowBuffer *null_bitmap,
                                gint64 n_nulls)
{
  auto array = garrow_primitive_array_new<arrow::MonthIntervalType>(length,
                                                                    data,
                                                                    null_bitmap,
                                                                    n_nulls);
  return GARROW_MONTH_INTERVAL_ARRAY(array);
}
gint32
garrow_month_interval_array_get_value(GArrowMonthIntervalArray *array,
                                      gint64 i)
{
  auto arrow_array = garrow_array_get_raw(GARROW_ARRAY(array));
  return static_cast<arrow::MonthIntervalArray *>(arrow_array.get())->Value(i);
}
const gint32 *
garrow_month_interval_array_get_values(GArrowMonthIntervalArray *array,
                                       gint64 *length)
{
  auto arrow_array = garrow_array_get_raw(GARROW_ARRAY(array));
  return garrow_array_get_values_raw<arrow::MonthIntervalType>(
    arrow_array, length);
}

ArrayBuilderClass

同様に「new」,「append_value」,「append_values」を実装すればよい。

実装を一応抜粋するが、こちらも共通化がされてるので、他のところの真似をしただけでできた。

ArrayBuilderはなんのためにあるのか質問したところ、以下と教えてもらった。

Arrayにappendしたい時に、メモリ上どう配置するかを実装者が考えないといけない。その時に、ArrayBuilderを使うとarrowのメモリ配置ルールに沿って、データをメモリに割り当ててくれる。

また、C++レベルでは、arrowのArray.new()が受け取るのは、binrayのデータであるので、stringの配列とかを渡してarrowのArrayを作ることはできない。

なので、stringの配列から、arrowのArrayを作りたいときは、ArrayBuilderを用いる必要がある。

array-builder.h

GARROW_AVAILABLE_IN_8_0
GArrowMonthIntervalArrayBuilder *
garrow_month_interval_array_builder_new(void);

GARROW_AVAILABLE_IN_8_0
gboolean
garrow_month_interval_array_builder_append_value(
  GArrowMonthIntervalArrayBuilder *builder,
  gint32 value,
  GError **error);
GARROW_AVAILABLE_IN_8_0
gboolean
garrow_month_interval_array_builder_append_values(
  GArrowMonthIntervalArrayBuilder *builder,
  const gint32 *values,
  gint64 values_length,
  const gboolean *is_valids,
  gint64 is_valids_length,
  GError **error);

array-builder.cpp

GArrowMonthIntervalArrayBuilder *
garrow_month_interval_array_builder_new(void)
{
  auto builder = garrow_array_builder_new(arrow::month_interval(),
                                          NULL,
                                          "[month-interval-array-builder][new]");
  return GARROW_MONTH_INTERVAL_ARRAY_BUILDER(builder);
}
gboolean
garrow_month_interval_array_builder_append_value(
  GArrowMonthIntervalArrayBuilder *builder,
  gint32 value,
  GError **error)
{
  return garrow_array_builder_append_value<arrow::MonthIntervalBuilder *>
    (GARROW_ARRAY_BUILDER(builder),
     value,
     error,
     "[month-interval-array-builder][append-value]");
}
gboolean
garrow_month_interval_array_builder_append_values(
  GArrowMonthIntervalArrayBuilder *builder,
  const gint32 *values,
  gint64 values_length,
  const gboolean *is_valids,
  gint64 is_valids_length,
  GError **error)
{
  return garrow_array_builder_append_values<arrow::MonthIntervalBuilder *>
    (GARROW_ARRAY_BUILDER(builder),
     values,
     values_length,
     is_valids,
     is_valids_length,
     error,
     "[month-interval-array-builder][append-values]");
}

DayTimeInterval, MonthDayNanoIntervalの実装

MonthIntervalは月の差分はただの数値で表現される。(例えば、3月と5月だったら差分は「2」)

だが、DayTimeInterval や MonthDayNanoInterval は、違ってて、以下のようにCのstructが返ってくる。

struct DayMilliseconds {
    int32_t days = 0;
    int32_t milliseconds = 0;
}

だから、難しかった...

まず、Cのstruct相当のものをglibで実装する。

arrow/interval.cpp at e402be253a5e5c99790f03783604bc1e9a139a88 · okadakk/arrow · GitHub

それと、glibで作ったそれを、Cのstructに相互変換するメソッドを実装した。

GArrowDayMillisecond *
garrow_day_millisecond_new_raw(
  arrow::DayTimeIntervalType::DayMilliseconds *arrow_day_millisecond)
{
  auto day_millisecond =
    g_object_new(garrow_day_millisecond_get_type(),
                 "day", arrow_day_millisecond->days,
                 "millisecond", arrow_day_millisecond->milliseconds,
                 NULL);
  return GARROW_DAY_MILLISECOND(day_millisecond);
}

arrow::DayTimeIntervalType::DayMilliseconds *
garrow_day_millisecond_get_raw(GArrowDayMillisecond *day_millisecond)
{
  auto priv = GARROW_DAY_MILLISECOND_GET_PRIVATE(day_millisecond);
  return &priv->day_millisecond;
}

あとは、MonthIntervalと同じように実装。(詰まったけど教えてもらってなんとか完成した)

その他学んだこと

  • GlistはLinkedList, CのListとは構造が違う。
    • 素数があらかじめわかる場合はListを使った方がいいが、arrow-glibは、Glistを使ってるところが多いので、基本Glistを使う。
  • glibは、コメントによって、所有権をわたす(= 呼び出した人がメモリ解放する)かどうかを記載する。  - このコメントを見て、glibがいい感じにメモリ解放とかしてくれる。コメント大事!!

感想

めっちゃ難しかった...

基本的には、glib(Cで実装されてる)のコードを、C++(arrow本体はC++で実装されてる)に変換するコードを書くだけだから、他のところを真似すれば、そんなに難しくないし、理解もしやすかった。

ただ、真似できないところになるとどうやればいいか分からなかった...

まだ全然理解できてる気はしないけど、雰囲気は掴めたので今後も触ってみたい!