2012年9月28日 星期五

探究如何整合 GLib Main Event Loop 和 Node.js 的 libuv

Standard
在普通情況下,整合 GLib Main Event Loop 和 libuv 不是件平常人會做的事,因為,一般人使用著 GTK+、Clutter、DBus 等等函式庫(Library)時,永遠只會使用 GLib 而不會使用到第二套事件引擎。但是,在 Node.js 中,其事件引擎並不是 GLib,而是使用自己的『libuv』,想同時運行兩套事件引擎是不可能的,所以這將注定我們無法以 Node.js 去引入 GTK+、Clutter、DBus 等函式庫來使用。不過,天下文章一大抄,世界上所有事件引擎的設計差異不大,在理解 GLib Context 的運作後,我們還是可以嘗試將兩者整合在一起,協同運行。

簡單理解一下事件引擎,其說白了就是一個跑到天荒地老的無窮迴圈,不停的去檢查是否有事件被喚醒。所以,由此可知,兩套事件引擎不能被同時跑起來,因為任何一個事件處理的無窮迴圈,都將導致另一個事件處理的迴圈無法正常運作。所以,首要解決的課題,就是讓兩個無窮迴圈可以同時運作。

為了解決這樣的問題,你可能會想到去使用 Thread (執行緒/線程),只要在建立兩個 Thread,然後各自跑不同的事件引擎就可以了。但是這樣做,問題馬上就會出現。由於程式正在運作的過程中,事件數量相當多,事件被安插和喚醒的次數非常頻繁,這將導致兩個 Thread 之間很難維持穩定的資料交換,你得實作 Mutex Lock 等機制來達成 Thread-safe,更還要想辦法解決兩邊各類大小資料交換的需求。種種原因,都會造成兩個事件引擎大量的彼此等待,而效能不彰。此外,也不易同時控制兩邊的事件觸發順序,以及事件被喚醒時間的精確度,如計時器(Timer)。

既然使用 Thread 是不好的做法,是否有其他方式可以解決我們一開始的課題?解決方法是有的,從事件引擎的運作細節,我們可以發現一些端倪。

一般來說,大部份的事件引擎都包括了幾個部份:


從 Initial 出發, Event Loop 的迴圈每運行一次,都經歷了『prepared』、『Polling』和『Dispatching』三種狀態,若用白話來說,他們所代表的意義就是『已準備好事件清單』、『監控事件』、『喚醒、分配並處理事件』。

如果你去參考事件引擎程式碼,會發現在三個狀態之間,分別有『prepare()』、『query()』、『check()』和『dispatch()』等四個動作,而這四個動作的行為簡單來說是『準備事件清單』、『取得需要被監控的事件』、『確認是否事件被觸發』、『喚醒並處理被觸發的事件』。

理解了事件引擎的運作流程後,該如何讓兩個引擎協同運作便有了答案。就如同本文一開頭所說,其實所有的事件引擎都大同小異,所以 GLib 和 libuv 也有差不多的行為,同樣有『prepare()』、『check()』等動作。那麼,我們只需要將 GLib 的事件處理機制,原封不動的一一搬到 libuv 上功能對應的地方即可。

理論上,移植 GLib 的工作並不難,我們只要參考 GLib Source Code,把 g_main_loop_run() 和 g_main_context_iterate() 內的工作拆解出來,放到 libuv 的 prepare() 和 check() 就能達到目的。

首先,在註冊兩個 libuv 的事件(用於 prepare 和 check):
/* Prepare */
uv_prepare_init(uv_default_loop(), &prepare_handle);
uv_prepare_start(&prepare_handle, prepare_cb);

/* Check */
uv_check_init(uv_default_loop(), &check_handle);
uv_check_start(&check_handle, check_cb);

然後在 uv_prepare_t 的 Callback Function(check_cb),取得 GLib main context 的事件清單並監聽:
void prepare_cb(uv_prepare_t *handle, int status)
{
 gint i;
 gint timeout;
 struct gcontext *ctx = &g_context;

 g_main_context_prepare(ctx->gc, &ctx->max_priority);

 /* Getting all sources from GLib main context */
 while(ctx->allocated_nfds < (ctx->nfds = g_main_context_query(ctx->gc,
   ctx->max_priority,
   &timeout,
   ctx->fds,
   ctx->allocated_nfds))) { 

  g_free(ctx->fds);
  g_free(ctx->poll_handles);

  ctx->allocated_nfds = ctx->nfds;

  ctx->fds = g_new(GPollFD, ctx->allocated_nfds);
  ctx->poll_handles = g_new(uv_poll_t, ctx->allocated_nfds);
 }

 /* Polling */
 for (i = 0; i < ctx->nfds; ++i) {
  GPollFD *pfd = ctx->fds + i;
  uv_poll_t *pt = ctx->poll_handles + i;

  struct gcontext_pollfd *data = new gcontext_pollfd;
  data->pfd = pfd;
  pt->data = data;

  pfd->revents = 0;

  uv_poll_init(uv_default_loop(), pt, pfd->fd);
  uv_poll_start(pt, UV_READABLE | UV_WRITABLE, poll_cb);
 }
}
註:Polling 這段在實作 GLib 內部函數 g_main_context_poll() 的工作。

監聽的部份,若有事件被觸發則呼叫 poll_cb(),所以我們也定義:
void poll_cb(uv_poll_t *handle, int status, int events)
{
 struct gcontext_pollfd *_pfd = (struct gcontext_pollfd *)handle->data;

 GPollFD *pfd = _pfd->pfd;

 pfd->revents |= pfd->events &
  ((events & UV_READABLE ? G_IO_IN : 0) |
  (events & UV_WRITABLE ? G_IO_OUT : 0));

 uv_poll_stop(handle);
 uv_close((uv_handle_t *)handle, NULL);
 delete _pfd;
}
註:revents 在監聽前會被我們歸零,在事件觸發時被我們設值,而其用途是告知 GLib main context 該事件被什麼行為(讀或寫)觸發。

最後是 check_cb(),我們要停止所有對事件的 Polling,然後讓 GLib main context 去檢查並處理事件:
void check_cb(uv_check_t *handle, int status)
{
 gint i;
 struct gcontext *ctx = &g_context;

 /* Release all polling events which aren't finished yet. */
 for (i = 0; i < ctx->nfds; ++i) {
  GPollFD *pfd = ctx->fds + i;
  uv_poll_t *pt = ctx->poll_handles + i;

  if (uv_is_active((uv_handle_t *)pt)) {
   uv_poll_stop(pt);
   uv_close((uv_handle_t *)pt, NULL);
   delete (struct gcontext_pollfd *)pt->data;
  }
 }

 g_main_context_check(ctx->gc, ctx->max_priority, ctx->fds, ctx->nfds);
 g_main_context_dispatch(ctx->gc);
}

到目前為止,GLib 的事件引擎已經可以良好的跑在 libuv 上。

此外,由於筆者本身開發了許多 Node.js Module 都有用到 GLib Main Event Loop,如:『dbus』、『jsdx-toolkit』等,常有這樣的需求,所以直接將這個部份寫成 C++ Class,以便日後其他專案使用。如果你有需要,可以參考兩支檔案:


將 Header 引入你的程式碼後,就可以直接宣告並使用它:
Context *context = new Context;
context->Init();


後記

libuv 的歷史和說明,本文就不多提,欲知更多細節,可參閱舊文『Node.js v0.8 大變革?!Native Module 開發者的福音!』。而接下來,你只需要知道,日後『libuv』將是 Node.js 內統一的事件處理機制就可以,這對撰寫 Node.js C/C++ Add-on 的人來說尤為重要,因為無論是在哪一個作業系統,都可以直接使用 libuv API 來操作事件。