ククログ

株式会社クリアコード > ククログ > 名古屋Ruby会議03:Apache ArrowのRubyバインディング(4) #nagoyark03

名古屋Ruby会議03:Apache ArrowのRubyバインディング(4) #nagoyark03

前回はなぜApache ArrowのバインディングをGObject Introspectionで作るとよさそうかについて説明しました。今回からはGObject Introspectionを使ったバインディングの作り方について説明します。実際に動くバインディングはkou/arrow-glibにあります。

基本的な作り方はGObject Introspection対応ライブラリーの作り方を参照してください。今回からはもう少し突っ込んだところを説明します。

今回はエラーの扱いについて説明します。

Apache Arrowのエラー:arrow::Status

Apache ArrowはC++で実装されていますが、エラーの通知は例外ではなくarrow::Statusというオブジェクトを返すことで実現しています。たとえば、arrow::io::FileOutputStream::Open()は次のようになっています。パスにあるファイルを開けなかったらarrow::Statusで理由を返します。

namespace arrow {
  namespace io {
    class FileOutputSTream {
      // When opening a new file, any existing file with the indicated path is
      // truncated to 0 bytes, deleting any existing memory
      static Status Open(const std::string& path, std::shared_ptr<FileOutputStream>* file);
    };
  }
}

GObject Introspectionのエラー:GError

GObject Introspectionを使ってエラーを扱うにはGErrorを使います。

まず、一般的なGErrorの使い方を説明します。

GErrorはエラー情報を表現するオブジェクトで、次の情報を保持します。

  • エラーのグループ(ドメインと呼んでいる)

  • エラーコード

  • エラーメッセージ

エラーのグループはGQuarkで表現します。これはRubyやSchemeで言えばシンボルに相当します。ようは名前が紐付いているIDです。

arrow-glib(現在開発しているApache ArrowのGObject Introspection対応ライブラリー)では次のように定義しています。

arrow-glib/error.h:

#define GARROW_ERROR garrow_error_quark()

GQuark garrow_error_quark(void);

GARROW_ERRORというマクロを用意しているのはそういう習慣(#{名前空間}_#{ドメイン名}という命名規則)だからです。直接garrow_error_quark()を呼ぶAPIとしてもよいですが、習慣に乗ったほうが使う人が使いやすくなるのでマクロを定義することをオススメします。

arrow-glib/error.cpp:

G_DEFINE_QUARK(garrow-error-quark, garrow_error)

G_DEFINE_QUARK()の呼び出しでgarrow_error_quark()を定義しています。ざっくり言うと、g_quark_from_static_string("garrow-error-quark");を実行する関数として定義してくれます。

これでGErrorに設定するエラーのグループを使えるようになりました。

次はエラーコードを用意します。具体的にはenumを用意します。enumの中身はarrow::StatusCodeに対応させています。

arrow-glib/error.h:

/**
 * GArrowError:
 * @GARROW_ERROR_OUT_OF_MEMORY: Out of memory error.
 * @GARROW_ERROR_KEY: Key error.
 * @GARROW_ERROR_TYPE: Type error.
 * @GARROW_ERROR_INVALID: Invalid value error.
 * @GARROW_ERROR_IO: IO error.
 * @GARROW_ERROR_UNKNOWN: Unknown error.
 * @GARROW_ERROR_NOT_IMPLEMENTED: The feature is not implemented.
 *
 * The error code used by all arrow-glib functions.
 */
typedef enum {
  GARROW_ERROR_OUT_OF_MEMORY = 1,
  GARROW_ERROR_KEY,
  GARROW_ERROR_TYPE,
  GARROW_ERROR_INVALID,
  GARROW_ERROR_IO,
  GARROW_ERROR_UNKNOWN = 9,
  GARROW_ERROR_NOT_IMPLEMENTED = 10
} GArrowError;

このenum定義から実行時にenumの名前・値を取得できるようにする情報を自動生成する必要があるのですが、ここでの説明は省略します。

これで以下の情報が揃ったのでGErrorを使うための事前準備は完了です。

  • エラーのグループ(ドメインと呼んでいる)

  • エラーコード

残りの以下は実際にGErrorを使うときに個別に設定します。

  • エラーメッセージ

エラーのグループとエラーコードは次のように使います。エラーメッセージはprintf()のように動的にフォーマットできることを示すためにムダに%dを使っています。

static void
fail_function(GError **error)
{
  g_set_error(error,
              GARROW_ERROR,
              GARROW_ERROR_INVALID,
              "Wrong number of argument: required %d argument",
              1);
}

g_set_error()は他にもいくつか亜種があるので必要に応じて使い分けます。

arrow-glibでの実装

実際の実装では次のようにg_set_error()をラップした関数を使っています。

void
garrow_error_set(GError **error,
                 const arrow::Status &status,
                 const char *context)
{
  if (status.ok()) {
    return;
  }

  g_set_error(error,
              GARROW_ERROR,
              garrow_error_code(status),
              "%s: %s",
              context,
              status.ToString().c_str());
}

次のように使います。

/**
 * garrow_io_file_output_stream_open:
 * @path: The path of the file output stream.
 * @append: Whether the path is opened as append mode or recreate mode.
 * @error: (nullable): Return location for a #GError or %NULL.
 *
 * Returns: (nullable) (transfer full): A newly opened
 *   #GArrayIOFileOutputStream or %NULL on error.
 */
GArrowIOFileOutputStream *
garrow_io_file_output_stream_open(const gchar *path,
                                  gboolean append,
                                  GError **error)
{
  std::shared_ptr<arrow::io::FileOutputStream> arrow_file_output_stream;
  auto status =
    arrow::io::FileOutputStream::Open(std::string(path),
                                      append,
                                      &arrow_file_output_stream);
  if (status.ok()) {
    return garrow_io_file_output_stream_new_raw(&arrow_file_output_stream);
  } else {
    std::string context("[io][file-output-stream][open]: <");
    context += path;
    context += ">";
    garrow_error_set(error, status, context.c_str());
    return NULL;
  }
}

このようにエラーをGErrorで表現しておくと、あとはバインディングレベルでいい感じにしてくれます。RubyならGErrorが設定されたら例外にします。

require "gi"

Arrow = GI.load("Arrow")
ArrowIO = GI.load("ArrowIO")

ArrowIO::FileOutputStream.open("/tmp/nonexistent/xxx", false)
# -> gobject-introspection/loader.rb:110:in `invoke': [io][file-output-stream][open]: </tmp/nonexistent/xxx>: IOError: Failed to open file: /tmp/nonexistent/xxx (Arrow::Error::Io)
#    	from gobject-introspection/loader.rb:110:in `block in define_singleton_method'
#    	from /tmp/a.rb:6:in `<main>'

まとめ

GObject Introspectionでのエラーの扱いについて説明しました。次回は戻り値のオブジェクトの寿命について説明します。