桌面环境做了好几年,对 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。具体的下次再说吧……