Wayland協議解析 一 Wayland中的資料結構解析
為了更好的學習wayland,我們可以先學習wayland中定義的一些資料結構.因為貫穿wayland的所有東西都是基於這些資料結構.
- 首先介紹wl_array
struct wl_array {
/** Array size */
size_t size;
/** Allocated space */
size_t alloc;
/** Array data */
void *data;
};
Wayland定義的陣列資料結構. 其中data儲存實際的資料,size儲存實際資料的大小,alloc儲存當前data總共分配的大小(malloc/realloc分配的大小)。
注意,alloc總是大於size,因為空間總要比儲存的資料大才行,另外當往數組裡面插入資料的時候,alloc不夠大了,
那麼就會以當前alloc大小翻倍的大小重新分配記憶體。因此在進行網路傳輸的時候,只需要把data裡面的資料傳送出去即可.
- 然後介紹wl_map
struct wl_map {
struct wl_array client_entries;
struct wl_array server_entries;
uint32_t side;
uint32_t free_list;
};
struct wl_array {
/** Array size */
size_t size;
/** Allocated space */
size_t alloc;
/** Array data */
void *data;
};
union map_entry {
uintptr_t next;
void *data;
};
這個資料結構是wayland的核心,用來儲存程序間通訊的實際物件指標,並得到對於指標的ID,用於程序間傳遞。
wl_map結構體存放wayland客戶端和伺服器端對應的物件。其中:
client_entries: 用wl_array陣列儲存客戶端的物件。(這種情況server_entries不使用)
server_entries:用wl_array陣列儲存伺服器端的物件。(這種情況client_entries不使用)
side:表明當前map儲存的是客戶端還是伺服器端物件(通過這個變數,確定client_entries/server_entries裡面儲存有物件,並且這個變數只在客戶端初始化為WL_MAP_CLIENT_SIDE,在伺服器端初始化為WL_MAP_SERVER_SIDE)
free_list:這個變數用來記錄當前已經被刪除了的物件的存放位置,但是對這個位置做了個處理。((i << 1) | 1 : i代表下標, 也就是指標的最後一位置為1),
然後,這個下標所指的位置的map_entry.next變數記錄著下一個被刪除的位置(直到為0,free_list初始值為0),形成連結串列
提示: map的節點是map_entry結構。data儲存實際的物件的地址。但是這個地址做了處理(原理上data是指標,編譯器為了4位元組對齊,最後兩位都是0,map利用了這兩位。把倒數第二位儲存flags的值(0/1),最後一位表示當前物件是否已經被刪除了(1表示刪除,0表示沒刪除)),map_entry是個聯合體,data成員儲存實際的物件指標,而next是在物件被刪除的時候,用來儲存下一個被刪除的物件的下標。
資料儲存關係: wl_map儲存兩個wl_array陣列, wl_array儲存map_entry聯合體的節點元素。
flags: entry->next |= (flags & 0x1) << 1;
deleted: map->free_list = (i << 1) | 1;
wayland協議實現基本決定了,建立物件都是客戶端發起的,流程是客戶端請求建立物件,通過wl_map_insert_new函式插入物件,並返回物件的ID(其實是下標),然後把ID傳遞到伺服器端,在伺服器端通過wl_map_insert_at函式把建立的物件插入到指定的下標(ID),這樣就建立了兩個物件的對應關係。只有一個物件是特殊的,就是wl_display.這個物件是寫死的下標(為1),因為這個物件肯定是第一個建立的。
- wl_list 連結串列
struct wl_list {
/** Previous list element */
struct wl_list *prev;
/** Next list element */
struct wl_list *next;
};
wayland實現的這種連結串列在很多優秀的程式碼裡面都有過,這種連結串列非常優秀,它可以儲存任何型別的元素。為什麼可以儲存任何型別的元素呢? 我舉個例子,你一定很好理解。
在中國有很多很多的家庭,如果想要把這些家庭給串聯起來,可以怎麼做呢?有個非常好的辦法就是,我給每一個家庭一個電話簿,電話簿裡面儲存兩個電話號碼,一個是這個家庭前面一家的號碼,另一個是後面一個家庭的號碼。所有家庭都是這樣,是不是就把他們的關係給串聯起來了?任何一個家庭都可以通過他家裡的電話簿聯絡到所有家庭。
怎麼樣? Wayland裡面的連結串列就是這個電話簿一樣的角色。
但是這裡有個問題,就是連結串列裡面儲存的都是電話簿,要怎麼把這個電話簿轉換成家庭呢? 這個就是C語言的語言特性。如果知道一個結構體成員的地址,就可以反推到這個結構體的地址。
#define wl_container_of(ptr, sample, member) \
(__typeof__(sample))((char *)(ptr) - \
offsetof(__typeof__(*sample), member))
offsetof 這個是C語言標準裡面提供的獲取成員偏移量的巨集,整個巨集就是提供成員變數的地址獲取到整個結構體的地址。有興趣的讀者可以多翻閱資料,也可以留言諮詢。
Wayland原始碼裡面大量使用了這個結構體,來儲存各種不同的結構體物件的連結串列。並且wayland提供了介面來更好的操作這種連結串列。
- 客戶端真正的物件結構體
struct wl_proxy {
struct wl_object object;
struct wl_display *display;
struct wl_event_queue *queue;
uint32_t flags;
int refcount;
void *user_data;
wl_dispatcher_func_t dispatcher;
uint32_t version;
};
在這裡我要告訴大家一個事實:
Wayland協議裡面的那些interface的物件在客戶端其實真正的結構體是wl_proxy,而那些結構體都是不存在的,只是一個宣告而已,根本不存在。如果讀者有看過wayland的原始碼,會發現一個問題,就是wayland協議裡面的那些interface物件,從來都不是程式設計師自己創建出來的,而是通過wayland的一些介面返回回來的。全部都是,無一例外。讀者如果不相信可以自己去找找,並且可以自己嘗試建立那些物件,肯定是會報錯的,因為那些結構體都是不存在的,建立會編譯出錯。
- 伺服器端真正的物件結構體
struct wl_resource {
struct wl_object object;
wl_resource_destroy_func_t destroy;
struct wl_list link;
struct wl_signal destroy_signal;
struct wl_client *client;
void *data;
};
和客戶端一樣,伺服器端所有的interface物件全部都是wl_resource結構體物件。
接下來我要總結一下這個結構體裡面都儲存了些什麼東西。看下圖:
從上圖中可知,所有的物件都有個wl_object成員來記錄它是屬於哪個interface的,並用implementation成員來儲存真正需要呼叫的函式指標,這個id就是用來進行客戶端和伺服器端物件對映的關鍵。
然後wl_interface就是wayland協議裡面的interface,一個inerface有名字屬性,還有版本號,版本號是非常有用的東西。而interface裡面的request和event就是由wl_message結構體儲存,表示一個函式。wl_message結構體包含函式名,以及函式的引數(所有的引數都儲存在signature變數裡面,通過字元的方式表示),方法如下:(wayland原始碼裡面的解釋)
/**
* Protocol message signature
*
* A wl_message describes the signature of an actual protocol message, such as a
* request or event, that adheres to the Wayland protocol wire format. The
* protocol implementation uses a wl_message within its demarshal machinery for
* decoding messages between a compositor and its clients. In a sense, a
* wl_message is to a protocol message like a class is to an object.
*
* The `name` of a wl_message is the name of the corresponding protocol message.
*
* The `signature` is an ordered list of symbols representing the data types
* of message arguments and, optionally, a protocol version and indicators for
* nullability. A leading integer in the `signature` indicates the _since_
* version of the protocol message. A `?` preceding a data type symbol indicates
* that the following argument type is nullable. While it is a protocol violation
* to send messages with non-nullable arguments set to `NULL`, event handlers in
* clients might still get called with non-nullable object arguments set to
* `NULL`. This can happen when the client destroyed the object being used as
* argument on its side and an event referencing that object was sent before the
* server knew about its destruction. As this race cannot be prevented, clients
* should - as a general rule - program their event handlers such that they can
* handle object arguments declared non-nullable being `NULL` gracefully.
*
* When no arguments accompany a message, `signature` is an empty string.
*
* Symbols:
*
* * `i`: int
* * `u`: uint
* * `f`: fixed
* * `s`: string
* * `o`: object
* * `n`: new_id
* * `a`: array
* * `h`: fd
* * `?`: following argument is nullable
*
* While demarshaling primitive arguments is straightforward, when demarshaling
* messages containing `object` or `new_id` arguments, the protocol
* implementation often must determine the type of the object. The `types` of a
* wl_message is an array of wl_interface references that correspond to `o` and
* `n` arguments in `signature`, with `NULL` placeholders for arguments with
* non-object types.
*
* Consider the protocol event wl_display `delete_id` that has a single `uint`
* argument. The wl_message is:
*
* \code
* { "delete_id", "u", [NULL] }
* \endcode
*
* Here, the message `name` is `"delete_id"`, the `signature` is `"u"`, and the
* argument `types` is `[NULL]`, indicating that the `uint` argument has no
* corresponding wl_interface since it is a primitive argument.
*
* In contrast, consider a `wl_foo` interface supporting protocol request `bar`
* that has existed since version 2, and has two arguments: a `uint` and an
* object of type `wl_baz_interface` that may be `NULL`. Such a `wl_message`
* might be:
*
* \code
* { "bar", "2u?o", [NULL, &wl_baz_interface] }
* \endcode
*
* Here, the message `name` is `"bar"`, and the `signature` is `"2u?o"`. Notice
* how the `2` indicates the protocol version, the `u` indicates the first
* argument type is `uint`, and the `?o` indicates that the second argument
* is an object that may be `NULL`. Lastly, the argument `types` array indicates
* that no wl_interface corresponds to the first argument, while the type
* `wl_baz_interface` corresponds to the second argument.
**/
我簡單解釋一下:wayland引數不是就那幾種嗎?這個地方就把這幾個引數再縮短標記,用一個字元來表示,但是有個特殊的‘?’,它表示後面這個引數可以為空。在引數的最前面有一個數字,這個數字代表著版本號。但是呢,引數中有可能是interface的物件,那麼就必須指明到底是哪個interface的物件,因此wl_message的最後一個成員types,就用一個數組的方式記錄這個引數是哪個interface的物件,如果是非interface的引數就為空。
最後,我們來得出,wayland解析協議檔案產生了些什麼,其實,看上面的圖片就能知道很多東西。看下面的檔案:
- 產生一個協議原始檔, 裡面儲存了協議檔案裡面所有的interface轉換而成的wl_interface結構體變數,包括wl_message結構體記錄的request和event函式。
- 產生一個客戶端使用的標頭檔案,裡面封裝了wl_proxy轉換成指定interface假宣告的結構體操作的一些介面函式。以及request函式的實現,但是這個實現只是把請求傳送到伺服器端而已,實際呼叫在伺服器端進行。最後,檔案裡面還封裝了一個回撥函式的結構體,成員就是所有的event函式指標,需要客戶端去實現,並設定到interface的物件裡面,該檔案生成了這個設定的介面,實際就是填充到wl_object結構體的implementation變數中。
- 產生一個伺服器端標頭檔案,裡面基本和客戶端一樣的組成。只是結構體是wl_resource,函式結構體的成員是所有request的函式指標。以及所有的event的實現。
也就是說,客戶端需要程式設計師自己實現事件(event),伺服器端需要程式設計師實現請求(request)。
好了,wayland協議的解析差不多都說完了,有什麼不清楚的,可以留言諮詢。接下來就開始講述wayland協議解析之後的工作原理。
對了,感興趣的讀者可以去看看QtWayland裡面解析wayland協議的工具原始碼,它把wayland協議再做了一層封裝成了C++面向物件,看起來更容易。