前回はなぜ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対応ライブラリー)では次のように定義しています。
#define GARROW_ERROR garrow_error_quark()
GQuark garrow_error_quark(void);
GARROW_ERROR
というマクロを用意しているのはそういう習慣(#{名前空間}_#{ドメイン名}
という命名規則)だからです。直接garrow_error_quark()
を呼ぶAPIとしてもよいですが、習慣に乗ったほうが使う人が使いやすくなるのでマクロを定義することをオススメします。
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
に対応させています。
/**
* 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でのエラーの扱いについて説明しました。次回は戻り値のオブジェクトの寿命について説明します。