1. 程式人生 > >GLib 建立自定義事件源

GLib 建立自定義事件源

GLib 實現了一個功能強大的事件迴圈分發處理機制,被抽象成為 GMainLoop,用於迴圈處理事件源上的事件。每個 GMainLoop 都工作在指定的 GMainContext 上。事件源在 GLib 中則被抽象成了 GSource。在 GMainContext 中有一個 GSource 列表。GLib 內部定義實現了三種類型的事件源,分別是 Idle, Timeout 和 I/O。同時也支援建立自定義的事件源。

自定義事件源的基本作用
自定義的事件源可以用來將外部訊號(事件)掛到程式中的指定主迴圈上,從而在 g_main_loop_run 中可以響應這些事件。

如何建立自定義事件源
GLib 提供了一系列的介面用於建立自定義的事件源,下面我們先講解一下建立事件源的基本函式和資料結構,最後給出一些例項。

自定義的事件源是一個繼承 GSource 的結構體,即自定義事件源的結構體 的第一個成員是 GSource 結構體, 其後便可放置程式所需資料, 例如:

typedef struct _MySource MySource;
 
struct _MySource
{
    GSource _source;
    gchar text[256];
}

實現了事件源資料結構的定義之後,還需要實現事件源所規定的介面,主要為 prepare, check, dispatch, finalize 等事件處理函式(回撥函式),它們包含於 GSourceFuncs 結構體中。將 GSourceFuncs 結構以及事件源結構的儲存空間寬度作為引數傳給 g_source_new 便可構造一個新的事件源,繼而可使用 g_source_attach 函式將新的事件源新增到主迴圈上下文中。下面這個示例可建立一個只會講“Hello world!”的事件源,並將其新增到主事件迴圈預設的 GMainContext 中。

#include <glib.h>
#include <glib/gprintf.h>
 
typedef struct _MySource MySource;
struct _MySource
{
    GSource source;
    gchar text[256];
};
 
static gboolean prepare(GSource *source, gint *timeout)
{
    *timeout = 0;
 
    return TRUE;
}
 
static gboolean check(GSource *source)
{
    return
TRUE; }   static gboolean dispatch(GSource *source, GSourceFunc callback, gpointer user_data) { MySource *mysource = (MySource *)source;   g_print("%s\n", mysource->text);   return TRUE; }   int main(void) { GMainLoop *loop = g_main_loop_new(NULL, TRUE); GMainContext *context = g_main_loop_get_context(loop); GSourceFuncs source_funcs = {prepare, check, dispatch, NULL}; GSource *source = g_source_new(&source_funcs, sizeof(MySource));   g_sprintf(((MySource *)source)->text, "Hello world!"); g_source_attach(source, context); g_source_unref(source);   g_main_loop_run(loop);   g_main_context_unref(context); g_main_loop_unref(loop);   return 0; }

上述程式的 g_main_loop_run 函式執行時,會迭代訪問 GMainContext 的事件源列表,步驟大致如下:
a. g_main_loop_run 通過呼叫事件源的 prepare 介面並判斷其返回值以確定各事件源是否作好準備。如果各事件源的 prepare 介面的返回值為 TRUE,即表示該事件源已經作好準備,否則表示尚未做好準備。顯然,上述程式所定義的事件源是已經作好了準備。
b. 若某事件源尚未作好準備 ,那麼 g_main_loop 會在處理完那些已經準備好的事件後再次詢問該事件源是否作好準備 ,這一過程是通過呼叫事件源的 check 介面而實現的,如果事件源依然未作好準備,即 check 介面的返回 FALSE,那麼 g_main_loop_run 會讓主事件迴圈進入睡眠狀態。主事件迴圈的睡眠時間是步驟 a 中遍歷時間源時所統計的最小時間間隔 ,例如在 prepare 介面中可以像下面這樣設定時間間隔。到達一定時間後, g_main_loop_run 會喚醒主事件迴圈,再次詢問。如此周而復始,直至事件源的 prepare 介面返回值為 TRUE。

static gboolean prepare(GSource *source, gint *timeout)
{
    *timeout = 1000; /* set time interval one second */
 
    return TRUE;
}

c. 若事件源 prepare 與 check 函式返回值均為 TRUE,則 g_main_loop_run 會呼叫事件源的 dispatch 介面,由該介面呼叫事件源的響應函式。事件源的響應函式是回撥函式,可使用 g_source_set_callback 函式進行設定。在上例中, 我們沒有為自定義的事件源提供響應函式。

上文自定義的事件源實際是 Idle 型別的,此類事件源,是指那些只有在主事件迴圈無其他事件源處理時才會被處理的事件源。GLib 提供了預定義的空閒事件源型別,其用法見下面的示例。

#include <glib.h>
 
static gboolean idle_func(gpointer data)
{
    g_print("%s\n", (gchar *)data);
 
    return TRUE;
}
 
int main(void)
{
    GMainLoop *loop = g_main_loop_new(NULL, TRUE);
    GMainContext *context = g_main_loop_get_context(loop);
 
    g_idle_add(idle_func, "Hello world!");
 
    g_main_loop_run(loop);
 
    g_main_context_unref(context);
    g_main_loop_unref(loop);
 
    return 0;
}

上述示例中,idle_func 是 idle 事件源的響應函式,如果該函式返回值為 TRUE,那麼它會在主事件迴圈空閒時重複被執行;如果 idle_func 的返回值為 FALSE,那麼該函式在執行一次後,便被主事件迴圈從事件源中移除。g_idle_add 函式內部定義了一個空閒事件源,並將使用者定義的回撥函式設為空閒事件源的響應函式, 然後將該事件源掛到主迴圈上下文。

Timeout 類事件源,GLib 也提供了預定義的定時器事件源,其用法與 GLib 預定義的空閒事件源類似。例如:

#include <glib.h>
 
static gboolean timeout_func(gpointer data)
{
    static guint i = 0;
 
    i += 2;
    g_print ("%d\n", i);
 
    return TRUE;
}
 
int main(void)
{
    GMainLoop *loop = g_main_loop_new(NULL, TRUE);
    GMainContext *context = g_main_loop_get_context(loop);
 
    g_timeout_add(2000, timeout_func, loop);
 
    g_main_loop_run(loop);
 
    g_main_context_unref(context);
    g_main_loop_unref(loop);
 
    return 0;
}

如果要自定義定時器型別的事件源,只需讓事件源的 prepare 與 check 介面在時間超過所設定的時間間隔時返回 TRUE, 否則返回 FALSE。

I/O 型別的事件源要稍微難理解一些,因為涉及到了作業系統層面的 poll 機制。所謂 poll 機制,就是作業系統提供的對檔案描述符所關聯的 I/O 的狀態監視功能 ,例如向檔案中寫入資料 ,那麼 I/O 的狀態可以表示為 POLLOUT, 而從檔案中讀取資料,那麼 I/O 的狀態就變為 POLLIN。GLib 為 Unix 系統與Windows 系統的 poll 機制進行了封裝,並且可以將檔案與主事件迴圈的事件源建立關聯,在主迴圈的過程中, g_main_loop_run 會輪詢各個關聯到檔案的事件源,並處理相應的事件響應。I/O 型別的事件源, prepare,其 check, dispatch 等介面的執行次序如下:

a. 主事件迴圈會首先呼叫 check 介面, 詢問事件源是否準備好。因為此時, g_main_loop_run 尚未輪詢那些與 I/O 相關聯的事件源, 所以 I/O 型別的事件源, check 介面的返回值應該是 FALSE。其主事件迴圈呼叫 g_main_context_iteration 輪詢各事件源,探尋是否有 I/O 型別事件源的狀態發生變化,並記錄變化結果。
b. 主迴圈呼叫 check 介面, 詢問事件是否準備好。此時, 如果 I/O 型別事件源的狀態變化符合要求,那麼就返回 TRUEE,否則返回 FALSE。
c. 如果 prepare 與 check介面的返回值均為 TRUE, 那麼此時主事件迴圈會呼叫 dispatch 介面分發訊息。

下面的示例展示了一個自定義的 I/O 型別事件源的基本用法。該示例所產生的程式接受使用者在終端中輸入的字串,並統計輸入的字元數量。

#include <glib.h>
 
typedef struct _MySource MySource;
 
struct _MySource
{
    GSource _source;
    GIOChannel *channel;
    GPollFD fd;
};
 
static gboolean watch(GIOChannel *channel)
{
    gsize len = 0;
    gchar *buffer = NULL;
 
    g_io_channel_read_line(channel, &buffer, &len, NULL, NULL);
    if(len > 0)
      g_print("%d\n", len);
    g_free(buffer);
 
    return TRUE;
}
 
static gboolean prepare(GSource *source, gint *timeout)
{
    *timeout = -1;
    return FALSE;
}
 
static gboolean check(GSource *source)
{
    MySource *mysource = (MySource *)source;
 
    if(mysource->fd.revents != mysource->fd.events)
      return FALSE;
 
    return TRUE;
}
 
static gboolean dispatch(GSource *source, GSourceFunc callback, gpointer user_data)
{
    MySource *mysource = (MySource *)source;
 
    if(callback)
      callback(mysource->channel);
 
    return TRUE;
}
 
static void finalize(GSource *source)
{
    MySource *mysource = (MySource *)source;
 
    if(mysource->channel)
      g_io_channel_unref(mysource->channel);
}
 
int main(int argc, char* argv[])
{
    GMainLoop *loop = g_main_loop_new(NULL, FALSE);
    GSourceFuncs funcs = {prepare, check, dispatch, finalize};
    GSource *source = g_source_new(&funcs, sizeof(MySource));
    MySource *mysource = (MySource *)source;
 
    mysource->channel = g_io_channel_new_file("test", "r", NULL);
    mysource->fd.fd = g_io_channel_unix_get_fd(mysource->channel);
    mysource->fd.events = G_IO_IN;
    g_source_add_poll(source, &mysource->fd);
    g_source_set_callback(source, (GSourceFunc)watch, NULL, NULL);
    g_source_set_priority(source, G_PRIORITY_DEFAULT_IDLE);
    g_source_attach(source, NULL);
    g_source_unref(source);
 
    g_main_loop_run(loop);
 
    g_main_context_unref(g_main_loop_get_context(loop));
    g_main_loop_unref(loop);
 
    return 0;
}

像 Idle 型別與 Timeout 型別事件源那樣,GLib 也提供了預定義的 I/O 型別事件源,使用它可以將上例簡化為:

#include <glib.h>
 
gboolean io_watch(GIOChannel *channel, GIOCondition condition, gpointer data)
{
    gsize len = 0;
    gchar *buffer = NULL;
 
    g_io_channel_read_line(channel, &buffer, &len, NULL, NULL);
    if(len > 0)
      g_print("%d\n", len);
    g_free(buffer);
 
    return TRUE;
}
 
int main(int argc, char* argv[])
{
    GMainLoop *loop = g_main_loop_new(NULL, FALSE);
    GIOChannel* channel = g_io_channel_unix_new(1);
 
    if(channel)
    {
        g_io_add_watch(channel, G_IO_IN, io_watch, NULL);
        g_io_channel_unref(channel);
    }
 
    g_main_loop_run(loop);
 
    g_main_context_unref(g_main_loop_get_context(loop));
    g_main_loop_unref(loop);
 
    return 0;
}

Over!