libuv 初窺--轉
過年了,人都走光了,結果一個人活也干不了。所以我便想找點東西玩玩。
今天想試一下 libev 寫點代碼。原本在我那臺 ubuntu 機器上一點問題都沒有,可在 windows 機上用 mingw 編譯出來的庫一個 backend 都沒有,基本不可用。然后網上就有同學推薦我試一下 libuv 。
libuv 是 node.js 作者做的一個封裝庫,在 unix 環境整合的 libev ,而在 windows 下用 IOCP 另實現了一套。看起來挺滿足我的玩兒的需求的。所以就試了一下。
這東西沒有文檔,暫時沒看出來作者有寫文檔的打算,恐怕他是自己用為主。我 google 了一下,就是 github 上有源代碼,.h 文件里有還算比較詳細的注釋。當然最主要是有些 test 程序,可以大概瀏覽其設計思路。
編譯倒挺順利,照著 test 寫點小東西也不復雜。所以我也就逐步開始了解這個東東了。
老實說,對于一個別人寫的庫,我愛用不愛用主要是考察其 API 設計。也就是該怎么用,設計的好不好,有沒有冗余設計。文檔什么的其實不太所謂,反正有代碼可以看嘛。
libuv 大體上我還算滿意,用 C 實現可以加很多分。不過有些小細節我覺得還是有點小遺憾的。(這個遺憾是指,如果我自己來設計,絕對不會像這個樣子)
最關鍵就是接口對 C 結構體布局的依賴性。這點我曾經因為 hiredis windows 版的緣故吐槽過一次了。以我自己的經驗,似乎大多數 Windows 出身的程序員都有一點這種壞習慣。好吧,我也不知道怎么把這點和 windows 聯系起來的,純粹感覺而已。因為我自己以前做設計也有這個習慣。
為什么我覺得這樣不好?
因為我覺得一個庫,若想被人當成黑盒子去使用,以后也作用黑盒子來維護,甚至可以用別的盒子去替代它。關鍵的一點就是接口簡單。這個簡單包括了使用最少的概念、需要最少的知識去理解它。
文檔通常是對接口無法自描述所需知識的一種補充。對一些例外的說明。而這些當然是越少越好。
我傾向于不在對外接口(對于 C 庫來說,就是 .h 文件)中定義所用數據結構的具體布局。通常只需要一個名字即可。這個名字是用來做強類型約束的。
過多的結構定義導致了過多的概念,增加了接口復雜度。
接口的最小知識表達就是用一致的 C 函數調用約定。有明確的輸入參數、輸出參數。對于接口函數,應該是無全局相關狀態的。這不僅僅是為了線程安全,而是可以保證沒有隱式約定(額外的知識)。
如果某些行為需要用戶設置或讀取某個結構體的一個特定域,我覺得就是在 C 函數調用這一方式外,增加了一種改變模塊行為的接口形式。或許這樣做的成本比 C 函數調用要來的低,但我以為得不償失。
尤其是、你的模塊如果相當依賴這種形式:直接對結構體的特定域賦值的形式來交換信息。這種依賴可能來至于你對性能的追求。那我覺得一般都是整個模塊的需求定義出了什么問題。一個獨立模塊需要解決的問題,通常對外界的信息交換應該是低頻的,它應該是可以獨立工作解決更復雜的問題的。而不應該是不斷的要求外部告知它新的狀態變換。
ps. 對于接口中的結構體定義問題。有另一種情況需要區分開。就是有大量的輸入參數或輸出參數需要一次性交換時,可以考慮定義一個結構體來做。這樣比在 C 函數調用前壓一大堆的數據去堆棧里要干凈的多。
寫了這么多,我是想說說我初步閱讀 libuv 代碼的感受。我碰到的第一個問題就是:libuv 用了大量 callback 機制來完成異步 IO 的問題。而這些 callback 函數通常都帶有一個參數 uv_stream_t
或 uv_req_t
等。這個數據表示這次 callback 綁定的數據 。
我們知道, C 語言是沒有原生 closure 支持的。若有的話,closure 應是 callback 機制最價解決方案。而 C 語言模擬 closure 的方法是用一個 C Function 并攜帶一個 void * ud 。此 ud 即原本應該在 closure 中綁定的數據塊。
這里,libuv 用的 uv_stream_t
大致上等同于這個 ud 。
問題出來了。用戶在用這類異步 IO 庫的時候,每次 IO 事件都需要綁定的行為需要的數據不僅僅是一個 stream 。還需要一些圍繞這個 stream 做的動作所需要的一些其它數據。
我在閱讀 test/echo-server.c
時看到這么一段:
static void after_write(uv_write_t* req, int status) { write_req_t* wr; if (status) { uv_err_t err = uv_last_error(loop); fprintf(stderr, "uv_write error: %s\n", uv_strerror(err)); ASSERT(0); } wr = (write_req_t*) req; /* Free the read/write buffer and the request */ free(wr->buf.base); free(wr); }
這里用了一次強制轉換,把 uv_write_t
轉換為 write_req_t
。為什么可以這樣干,是因為 write_req_t
被定義成:
typedef struct { uv_write_t req; uv_buf_t buf; } write_req_t;
這里根據 C 結構布局,req 是第一個域,所以排在最前面。
這樣做有點晦澀,我只能說感覺不太好。因為如果約定了 uv_write
接口傳遞的是一個 uv_write_t
類型的數據,這就明顯是利用 C 語言特性來夾帶私貨了。
如果這是作者推薦的慣用法的話,我則這樣理解:
libuv 其實在 API 上有個隱含約定。即回調函數的參數指向的地址偏移量為某個數值以后的數據是用戶數據。這個數值為類型的尺寸。這類似 c++ 的繼承。數據類型尺寸數值是編譯時通過編譯器來約定的。
而且,單就現在的用法,我認為更嚴謹的做法應該是類似 socket API ,顯式的把傳遞的結構尺寸在函數接口表達出來(參考 socket connect 的接口定義中的第三個參數 addrlen)。 這樣對庫的接口穩定有好處。庫可以知道用戶有可能擴展數據,長度信息提示了庫,傳入數據體的真實大小。
btw, C++ 在用繼承來完成類似設計時,則依靠了語言對 cast 的約定。C++ 語言的知識概念太多,很難完成簡潔的模塊接口約定。在我看來,這直接導致了 C++ 很難設計通用庫,而只能設計專有框架。
我著一些疑惑閱讀了不少 libuv 里的實現代碼,尤其是 uv.h 的細節。我發現這樣一個結構定義。
#define UV_HANDLE_FIELDS \ /* read-only */ \ uv_loop_t* loop; \ uv_handle_type type; \ /* public */ \ uv_close_cb close_cb; \ void* data; \ /* private */ \ UV_HANDLE_PRIVATE_FIELDS /* The abstract base class of all handles. */ struct uv_handle_s { UV_HANDLE_FIELDS };
注意這里有一個 data 域。從我的經驗判斷,這個域應該就是用來在一個 handle 上夾帶用戶數據的。由于沒有文檔確認,我只能從有限的代碼閱讀中來確認我的判斷。我很奇怪沒有定義一個明確的 api 出來綁定用戶數據。因為在庫的實現代碼中也確實庫自己用到過這個域,所以估計也不是庫的使用者可以自由使用的。
當然對應的還有幾處類似設計:
#define UV_REQ_FIELDS \ /* read-only */ \ uv_req_type type; \ /* public */ \ void* data; \ /* private */ \ UV_REQ_PRIVATE_FIELDS /* Abstract base class of all requests. */ struct uv_req_s { UV_REQ_FIELDS };
還有
struct uv_loop_s { UV_LOOP_PRIVATE_FIELDS /* list used for ares task handles */ uv_ares_task_t* uv_ares_handles_; /* Various thing for libeio. */ uv_async_t uv_eio_want_poll_notifier; uv_async_t uv_eio_done_poll_notifier; uv_idle_t uv_eio_poller; /* Diagnostic counters */ uv_counters_t counters; /* The last error */ uv_err_t last_err; /* User data - use this for whatever. */ void* data; };
這個 struct uv_loop_s
的 data 域倒是明確的注釋可以隨便使用了。
話說回來,這個綁定用戶數據的需求我在早年閱讀 Windows 的 MFC 實現時倒是見過另外一種解決方案。
Windows 的窗體有一個 SetWindowLong 的 API 可以讓用戶去設置一個用戶數據。這樣可以方便用戶在用 C++ 封裝的時候把一個 C++ 對象指針綁定在窗體 Handle 上。這樣在窗口消息回調函數中就可以取回這個對象指針。
MFC 封裝這些系統 API 時,可能是為了更通用,沒有占用這個內置域,而是自己建立了一個全局的映射表。每次窗體消息回調時,查表來找到對應的窗體對象。這種非侵入式的方案,也湊合用吧。就是對于用 C/C++ 編寫代碼的追求性能的同學來說,或許有些小小不爽。
這就是我初步閱讀 libuv 代碼的一些簡單看法。當然,我覺得 libuv 是個很不錯的東西,不然我也不會饒有興致的玩了一晚上。只是由于在這塊投入時間精力不多,錯誤難免。有行家看到,一笑了之吧。
RFID管理系統集成商 RFID中間件 條碼系統中間層 物聯網軟件集成