GIOChannelの使い方


こんにちは、稲垣@CEREVOです。今回はGTK+に関する (正確にはGLibに関することなのですが……) 話題で、GIOChannelの使い方を見てみたいと思います。

なぜGIOChannelを使うのか

GTK+でGUIアプリケーションを書くと、プログラムは基本的にイベントドリブンになります。つまり、メインループの中でイベントが起きるのを黙って待って、何か起きたらコールバックの中で処理します。こうしたフレームワークではファイルやソケットの読み書きでブロックされる(待ちが発生する) のは嬉しくありません。ブロックされている間は基本的に他の処理ができず、たとえば処理中のアニメーションが止まったりします。GIOchannelを使えば、ブロックされない状態になってから処理を開始することができます。

なお、今回はあまり関係ありませんが、テキストのエンコーディングを適当にUTF-8に変換してくれる機能もあります。

GIOChannelの使い方

  • g_io_channel_unix_newで生成 (unix系のシステムを想定しています……)
  • NONBLOCKに設定する (設定しないとG_IO_STATUS_AGAINが返るかわりにブロックされます)
  • g_io_add_watchでイベントソースをデフォルトメインループに追加
    (頻繁に掛け外しをする場合は、g_io_create_watchで作ったイベントソースを自分で扱った方がいいかも知れません)
  • コールバック
    • どんなイベントが起きたのか、GIOConditionを見て判断する
    • GIOChannelを読み書きしてG_IO_STATUS_NORMALやG_IO_STATUS_AGAINが返ってきたらTRUEを返す
      G_IO_STATUS_EOFやG_IO_STATUS_ERRORが返ってきたらFALSEを返してイベントソースを外す

読み込みサンプル

標準入力から読み込んでg_messageでメッセージを表示するサンプルです:


#include <glib.h>
static gboolean read_callback(GIOChannel *io, GIOCondition cond, gpointer user_data) {
  GMainLoop *loop = user_data;
  gboolean continue_to_watch = FALSE;
  if (cond & G_IO_IN) {
    GError *e = NULL;
    char *text;
    switch (g_io_channel_read_line(io, &text, NULL, NULL, &e)) {
    case G_IO_STATUS_NORMAL:
      g_message("%s: read line: %s", __func__, text);
      g_free(text);
      continue_to_watch = TRUE;
      break;
    case G_IO_STATUS_AGAIN:
      g_message("%s: AGAIN", __func__);
      continue_to_watch = TRUE;
      break;
    case G_IO_STATUS_ERROR:
      g_message("%s: error: %s", __func__, e->message);
      g_error_free(e);
      break;
    case G_IO_STATUS_EOF:
      g_message("%s: EOF", __func__);
      break;
    default:
      break;
    }
  }
  if (! continue_to_watch) {
    g_main_loop_quit(loop);
    g_main_loop_unref(loop);
  }
  return continue_to_watch;
}

static void sample_loop(int fd, GIOCondition cond, GIOFunc callback) {
  GMainLoop *loop = g_main_loop_new(NULL, FALSE);
  GIOChannel *io = g_io_channel_unix_new(fd);
  guint tag = g_io_add_watch(io, cond, callback, g_main_loop_ref(loop));
  g_io_channel_set_flags(io, G_IO_FLAG_NONBLOCK, NULL);
  g_io_channel_set_close_on_unref(io, TRUE);
  g_io_channel_set_encoding(io, NULL, NULL);
  g_main_loop_run(loop);
  g_io_channel_unref(io);
  g_main_loop_unref(loop);
}

int main(int argc, char *argv[]) {
  sample_loop(0, G_IO_IN, read_callback);
  return 0;
}

G_IO_INは読み込み可能を示すフラグです (読み込み専用のfdについてはこのフラグしか立たないようです――man poll参照)。ただし、読み込み可能と言っても、読んでみたらすぐにG_IO_STATUS_EOFが返ってくることもあります。EOFが返ってきたらもうチャンネルに用はないのでFALSEを返してイベントソースを外します。

書き込みサンプル

標準出力 (のバッファ) が書き込み可能になるのを待って書き込みまくるサンプルです。なおsample_loop関数は前節の関数をそのままコピーして使ってください。実行すると大量のメッセージが出力されますから、

./sample | read i

などとして、パイプが適当に閉じられるようにして実行してください:


#include <glib.h>
static gboolean write_callback(GIOChannel *io, GIOCondition cond, gpointer user_data) {
  GMainLoop *loop = user_data;
  gboolean continue_to_watch = FALSE;
  if (cond & (G_IO_ERR | G_IO_HUP))
    g_message("%s: channel is closed", __func__);
  else if (cond & G_IO_OUT) {
    GError *e = NULL;
    char *text = "BABEL\n";
    int len;
    switch (g_io_channel_write_chars(io, text, -1, &len, &e)) {
    case G_IO_STATUS_NORMAL:
      g_message("%s: wrote %d chars", __func__, len);
      continue_to_watch = TRUE;
      break;
    case G_IO_STATUS_AGAIN:
      g_message("%s: wrote %d chars, AGAIN", __func__, len);
      continue_to_watch = TRUE;
      break;
    case G_IO_STATUS_ERROR:
      g_message("%s: error: %s", __func__, e->message);
      g_error_free(e);
      break;
    case G_IO_STATUS_EOF:
      g_message("%s: EOF", __func__);
      break;
    default:
      break;
    }
  }
  if (! continue_to_watch) {
    g_main_loop_quit(loop);
    g_main_loop_unref(loop);
  }
  return continue_to_watch;
}

int main(int argc, char *argv[]) {
  sample_loop(1, G_IO_OUT | G_IO_ERR | G_IO_HUP, write_callback);
  return 0;
}

チャンネルが閉じられるとコンディションにG_IO_ERRのビットが立ちます。同時にG_IO_OUTビットが立つこともありますが、それは単にバッファに空きがあるというだけで、書き込んでも誰も見てくれないので無視するようにしました。

またg_io_channel_write_charsでは、本来は書き込まれたバイト数もチェックしなければならないのですが、長いデータを書き込まなければ途中で切れることはないようなので、今回はチェックしていません。

読み書き用の場合

ソケットは読み書き両用なので、必要なら読み書き両用のコールバックを書くことができます。そのときは上記のread_callback関数とwrite_callback関数を適当に結合させればいいでしょう。なお、ソケットの相手側が閉じられていても (コンディションにG_IO_ERRビットが立っていても)、バッファに読み込み可能なデータが残っていることがあります。プロトコルによりますが、必要ならG_IO_INビットをチェックしてG_IO_STATUS_EOFが返ってくるまで読み出してやることもできます。

おわり

GIOChannelを使えば、GLibのメインイベントループの中で、ブロックされることなく入出力を処理することができます。イベントドリブンなアプリケーションを書くために是非とも使い方を把握しておきたいものです。

Happy hacking!