桌面環境做了好幾年,對 X 和 wayland 比較感興趣,但是一直沒有機會深入去看。正好我要離職了,在現在這個單位,北京上海加起來有將近十年了,最年輕的十年,也收獲了不少。最近工作交接完沒啥事,就抓著 wayland 代碼看了看,整體概念大概了解一點,先總結一下吧,免得又忘了。這篇文章主要目的是梳理 server 端和 client 端的函數調用是怎么實現的?并且拿 display 和 registry 為例做簡單地說明。
wayland 核心協議定義了許多全局對象,包括:display、registry、compositor、output、seat、surface、buffer 等等。客戶端連接到服務端后,會創建一個客戶端的可用對象,一般叫 proxy,服務端對應會創建一個 resource,這倆是對應的,實現 client 端對 server 端的資源綁定。
client 和 server 一方面各自在消息循環上等待數據(socket 上的數據),拿到數據經過反序列化后生成本地的函數掉用;另一方面,將本地的調用請求封裝序列化后,通過 socket 發出去。
wayland 協議
核心協議通過 xml 文件來定義,wayland-scanner 程序掃描并生成一個 wayland-protocol.c 和兩個頭文件,分別給服務端程序和客戶端程序使用。
以 display 和 registry 為例,在生成的 wayland-protocol.c 中,display 的簽名:
static const struct wl_message wl_display_requests[] = {
{ "sync", "n", wayland_types + 8 }, // wl_callback_interface
{ "get_registry", "n", wayland_types + 9 }, //wl_registry_interface
};
static const struct wl_message wl_display_events[] = {
{ "error", "ous", wayland_types + 0 },
{ "delete_id", "u", wayland_types + 0 },
};
WL_EXPORT const struct wl_interface wl_display_interface = {
"wl_display", 1,
2, wl_display_requests,
2, wl_display_events,
};
wl_message 的結構:
struct wl_message {
/** Message name */
const char *name;
/** Message signature */
const char *signature;
/** Object argument interfaces */
const struct wl_interface **types;
};
{ "get_registry", "n", wayland_types + 9 } 的這個 n 表示 new_id,也就是說客戶端在發出 get_registry 請求的時候會傳遞一個 wl_interface * 的參數,對應為這個參數創建一個新的對象。比如 wl_display_get_registry 返回 wl_registry 對象,這個對象就是 registry 在客戶端的代理對象,這樣創建出來的。registry 對象的相關簽名如下:
static const struct wl_message wl_registry_requests[] = {
{ "bind", "usun", wayland_types + 0 },
};
static const struct wl_message wl_registry_events[] = {
{ "global", "usu", wayland_types + 0 },
{ "global_remove", "u", wayland_types + 0 },
};
WL_EXPORT const struct wl_interface wl_registry_interface = {
"wl_registry", 1,
1, wl_registry_requests,
2, wl_registry_events,
};
簽名類型簡單羅列如下:
* * `i`: int
* * `u`: uint
* * `f`: fixed
* * `s`: string
* * `o`: object
* * `n`: new_id
* * `a`: array
* * `h`: fd
* * `?`: 問號表示后面接的參數可空
wl_message 有個成員 types,在 demarshal 消息的過程中,如果傳過來的參數中有 object 或者 new_id,那么我們設置的 implementation 必須要知道對象的類型是什么,這個 types 就是用來做這個的。
wayland_types 是個指針數組,數組里面的成員是 wl_interface *,比如 global 對應 wayland_types + 0 指向 NULL,get_registry 對應的 wayland_types + 9 指向 wl_registry_interface。
對于 display,客戶端先創建一個 proxy,將 request 發給服務端,服務端收到消息后創建對應的 display_resource,這個 display_resource 與客戶端的 display_proxy 是對應的,客戶端給服務端發送 request 消息,服務端給客戶端發送 event 消息。display 的 request 有兩個: sync 和 get_registry,在服務端實現,event 有兩個: error 和 delete_id,在客戶端實現。
服務端
1、wayland server 啟動時會創建一個 socket(_wl_display_add_socket),并將這個 socket fd 加入 epoll 中,這樣如果有客戶端程序連接,epoll 就會通知服務端,從而執行回調函數 socket_data。
2、當有客戶端程序連接過來,在 socket_data 函數中調用 accept 創建新的 socket fd,緊接著創建 wl_client,并把它加入合成器的 client list;在創建 wl_client 的時候,wl_client_create (display, fd) 將這個 socket fd 加入 epoll,這樣從客戶端發過來的請求,也可以通過 epoll 通知服務端,回調函數是 wl_client_connect_data。
3、等創建了 wl_client,接著執行函數 bind_display,創建 wl_display 在 server 端的 resource 對象 client->display_resource,設置它的 implementation,也就是 wl_display_requests 的倆函數:
static const struct wl_display_interface display_interface = {
display_sync,
display_get_registry
};
客戶端
1、客戶端程序在運行時先調用 wl_display_connect 連接到服務端,這個函數返回的 wl_display 就是 display 對象在 client 端的代理[上面第 3 步中說了 server 端是 display_resource,這倆配合工作,實現互相調用函數]。
在 wl_display_connect 中為代理對象 set_implementation,也就是 wl_display_events
static const struct wl_display_listener display_listener = {
display_handle_error,
display_handle_delete_id
};
這時第一個資源對象 display 就在 server 和client 端都設置好了。
2、然后調用 wl_display_get_registry 來獲取 server 端所有可用的 global 對象,并執行 wl_registry_add_listener 為新的 registry 對象注冊事件:
static const struct wl_registry_listener registry_listener = {
registry_handle_global,
registry_handle_global_remove
};
函數 wl_display_get_registry,它會調用下面這個函數
wl_proxy_marshal_constructor((struct wl_proxy *) wl_display, WL_DISPLAY_GET_REGISTRY, &wl_registry_interface, NULL);
上面的 &wl_registry_interface 就是 get_registry 的簽名 "n"。一方面它會返回 registry 在 客戶端的 proxy,也就是 display->registry,另一方面把請求發出去。
簡單分析這兩個函數,首先介紹
wl_registry_add_listener(display->registry, ®istry_listener, display);
第一個參數是 registry 的客戶端 proxy,第二個參數是 implementation,這個函數的作用就是設置 proxy event 的 implementation
proxy->object.implementation = implementation;
proxy->user_data = data;
接著再分析 wl_proxy_marshal_constructor,在 wayland-client.c 中層層調用到了 wl_proxy_marshal_array_constructor_versioned:
message = &proxy->object.interface->methods[opcode];
if (interface) {
new_proxy = create_outgoing_proxy(proxy, message,
args, interface,
version);
if (new_proxy == NULL)
goto err_unlock;
}
解析 message 這里需要先看 wl_interface 的結構定義,如下:
struct wl_interface {
/** Interface name */
const char *name;
/** Interface version */
int version;
/** Number of methods (requests) */
int method_count;
/** Method (request) signatures */
const struct wl_message *methods;
/** Number of events */
int event_count;
/** Event signatures */
const struct wl_message *events;
};
其中 methods 是 request 的簽名,這里的 object.interface 應該是 &wl_display_interface,在函數 wl_display_connect 設置它的值:
display->proxy.object.interface = &wl_display_interface;
這個接口的 methods 有兩個,method_count 就是2; opcode 是 WL_DISPLAY_GET_REGISTRY,拿到的 message 也就是 “{ "get_registry", "n", wayland_types + 9 }”。
傳遞過來的 interface 為 wl_registry_interface,在 create_outgoing_proxy 得到 “n”,就會創建新的 proxy 對象,設置這個對象的 interface 為 wl_registry_interface,并把這個新的 proxy 對象加入映射表中,得到 object.id 填充到 args 中 。然后,再把 message,opcode,args 等組好 wl_closure ,序列化完成后,通過 socket 發給服務端。
再回到服務端,收到客戶端消息后,進入回調函數 wl_client_connection_data,反序列化消息,得到 wl_closure,找到目標對象對應的接口函數,利用 libffi 執行 server 端的函數: display_get_registry,這個函數會在 server 端創建對應的 wl_resource 也就是 registry_resource,將它 set_implementation,也就是 registry_bind。然后再遍歷可用的 global 對象發信號 global。
wl_list_for_each(global, &display->global_list, link)
if (wl_global_is_visible(client, global) && !global->removed)
wl_resource_post_event(registry_resource,
WL_REGISTRY_GLOBAL,
global->name,
global->interface->name,
global->version);
這樣,第二個對象 registry 在服務端和客戶端的 request 和 event 也都設置好了。
進程間的函數調用
當服務端發出 global 信號時,客戶端程序就可以 wl_registry_bind 這些對象,生成本地的可用對象 wl_proxy。[參考 weston 的客戶程序代碼 window.c]
但是信號機制一般是對同一個進程來說的,我們可以監聽某個對象的某個信號,當收到信號時執行對應的回調函數;而這里其實是兩個程序,服務端和客戶端,這種跨進程的信號不是簡單地 connect 就可以的,而是需要通過 socket 來傳遞。
要讓兩個進程通過 socket 進行函數調用,首先需要將調用抽象成數據流的形式,這些信息通過 wl_closure_marshal 寫入 wl_closure 結構,再由 serialize_closure 變成數據流;等到了目標進程,從數據流中通過 wl_connection_demarshal 轉回 wl_closure 結構。
如果參數中不止整型字符串等簡單類型,還存在對象的話,就需要 wl_map 來做這個對象在 server 和 client 端的映射了。
上面所謂的發出 global 信號,其實也就是把函數調用的請求序列化,然后再發給客戶端程序,客戶端收到消息,反序列化,解析出object id, 從 wl_map 中找到本地可用對象,拿到 opcode,再解析本地對象的 implementation 對應 opcode 的函數,也就是執行 registry_handle_global。
if (strcmp(interface, "wl_compositor") == 0) {
d->compositor = wl_registry_bind(registry, id,
&wl_compositor_interface, 3);
} else if (strcmp(interface, "wl_output") == 0) {
display_add_output(d, id);
} else if (strcmp(interface, "wl_seat") == 0) {
display_add_input(d, id, version);
}
同樣的,這個 wl_registry_bind 也是把客戶端的請求消息序列化后,發給服務端;服務端收到后反序列化,執行對應的 bind,也就是 wayland-server.c 中定義的 registry_bind
整個流程就是 client 端通過 wl_proxy 調用 server 端的 wl_resource 的 request,server 端通過 wl_resource 調用 client 端的 wl_proxy 的 event。
再簡單說明一下客戶端收到消息的處理流程,wl_display_dispatch -> wl_display_read_events -> read_events -> queue_event,讀取數據,將解析出來的 event 加入 event_list 中。
在 dispatch_queue 中 dispatch_event,也就是執行 wl_closure_invoke。具體的下次再說吧……