如何扩展 wayland 协议
为了能够扩展 wayland 协议,首先需要理解 wayland 协议,并且知道怎么样在server和client端实现协议中定义的接口。看了一堆文档,试着按照自己的理解来整理文档,并动手写简单的代码来加深理解。【希望一个月之后再读这篇文章不会觉得是一坨shit】
wayland 协议是什么
wayland核心协议是一个 xml 文件,如果我们安装了 wayland 开发包,这个文件在一般在系统的 /usr/share/wayland/wayland.xml。核心协议的内容有限,不满足我们平常对窗口的一些操作,所以为了实现一些窗口管理的功能,还有很多扩展的协议,比如 xdg-shell 就是为了实现桌面窗口而扩展的协议。协议有稳定版本和不稳定版本,在这篇文档中我们主要看 /usr/share/wayland-protocols/stable/xdg-shell/xdg-shell.xml,这是一个稳定的版本,它的 xdg_surface 对象有一个 request 接口是 set_window_geometry,从注释来看,如果实现这个接口,应该能满足我们的需求。
可以通过 wayland-scanner 工具解析这个 xml 生成Server、Client的头文件以及 glue code一个 C 文件。
wayland-scanner server-header < xdg-shell.xml > xdg_shell_server_header.h
wayland-scanner client-header < xdg-shell.xml > xdg_shell_client_header.h
wayland-scanner private-code < xdg-shell.xml > xdg_shell_protocol.c
上面这张图是网上找到的,开发的时候基本上就是这么个流程。写一个server.c 文件,用到生成的 server-protocol.h 和 protocol.c ,编译命令:
gcc -o server server.c xdg_shell_protocol.c -lwayland-server
编译Client程序:
gcc -o xdg_client xdg_client.c xdg_shell_protocol.c -lwayland-client
理解 wayland 协议
wayland 协议其实就是我们预先定义好一个 object 的接口,包括它的 request 和 event,Server实现 request 接口,Client实现对 event 的监听和响应。当Client把对这个对象的 request 封装成消息发给Server,Server收到消息后,根据对象id的和操作码执行对应的响应函数;event 也是类似的流程,Server把对这个对象的 event 发到了Client,Client会作出响应。
在Server和Client之间,对象是一一对应的,互相知道这个对象的状态。Client是 wl_proxy,与之对应的,在Server就会有一个 wl_resource。Server程序需要知道这个 resource 属于哪个Client程序。这个对象之间映射就是 wayland 的 wl_map 来维护的。
struct wl_map {
struct wl_array client_entries;
struct wl_array server_entries;
uint32_t side;
uint32_t free_list;
};
side 分WL_MAP_CLIENT_SIDE 和 WL_MAP_SERVER_SIDE两种,表明当前map保存的是Client还是 Server对象。
wayland Server运行之后,一般会有多个 wayland Client程序连接。每当Client程序调用 wl_display_connect 连接到wayland, 对应就会为它维护一个 wl_map结构:display->objects[代码wayland-client.c ];如果Server监听到Client连接,在 wl_client_create 的时候,也会为之创建一个 wl_map:client->objects[代码wayland-server.c]。
在映射表中,Client proxy 对象从 0 开始,Server resource 对象从 0xff000000 开始存放,display->objects只用了 client_entries,client->objects 只用了 server_entries。wl_map在Client和Server端各有一个,它们分别存了wl_proxy和wl_resource的数组,且是一一对应的。这些对象在这个数组中的索引作为它们的id。这样,参数中的对象只要传id,这个id被传到目的地后会通过查找这个wl_map表来得到本地相应的对象。
映射表创建后,就可以插入数据了。比如我们客户端创建wl_proxy,设置interface等信息,然后将该wl_proxy插入到display->objects的wl_map中,返回值为id,其实就是在wl_map中数组中的索引值。这个值是会被发到Server端的,这样Server端就可以创建 wl_resource,并把它插入到Server端的wl_map数组的相同索引值的位置。这样逻辑上,就创建了wl_proxy和wl_resource的映射关系。以后,Client和Server间要相互引用对象只要传这个id就可以了。
上面这张图被多次引用,它很清楚的描述了Server和Client之间的对象映射和事件调用。
object
为了Server和Client之间的通信,第一步就是创建对象,wayland中默认第一个对象就是 wl_display,object id 为1,这个对象之外的所有其他对象,都是需要通信来创建的。
在 wayland 中,wl_display 是第一个对象,wl_registry 是第二个对象,因为有了 wl_registry 之后,我们才能注册绑定其他所有的 global object。在运行Server程序前,开启 WAYLAND_DEBUG=1,可以看到Client连接后,Server的 debug 如下:
[1008582.564] wl_display@1.get_registry(new id wl_registry@2)
[1008582.581] -> wl_registry@2.global(1, "wl_output", 1)
[1008582.654] wl_registry@2.bind(1, "wl_output", 1, new id [unknown]@4)
request 和 event
在生成的 glue code代码中,主要定义了一些对象,以及这写对象的request和event接口。Server头文件中定义 request 的接口结构,因为Server需要响应Client请求,所以我们在开发Server程序时需要实现 request接口;在Client头文件中定义 event 的 listener,因为Client需要监听Server传过来的事件,执行对应的回调。所以在开发Client程序时,需要去 add_listener,实现收到事件之后要做的工作。
以 xdg_surface 为例,它在生成的xdg_shell_protocol.c 文件中定义如下:
WL_PRIVATE const struct wl_interface xdg_surface_interface = {
"xdg_surface", 3,
5, xdg_surface_requests,
1, xdg_surface_events,
};
这个 xdg_surface_interface 是一个全局变量,数据类型是 wl_interface。这个结构中成员组成:name、version、request个数、request签名、event个数以及event签名。也就是说 xdg_surface 这个对象有 5 个请求和 1 个事件。
request 和 event 签名是 wl_message 结构的,不管是 request 和 event 都会被封装成 MESSAGE 在 server 和 client 之间传递。
static const struct wl_message xdg_surface_requests[] = {
{ "destroy", "", xdg_shell_types + 0 },
{ "get_toplevel", "n", xdg_shell_types + 7 },
{ "get_popup", "n?oo", xdg_shell_types + 8 },
{ "**set_window_geometry**", "iiii", xdg_shell_types + 0 },
{ "ack_configure", "u", xdg_shell_types + 0 },
};
message
当Client程序拿到一个对象了,就可以给Server发对这个对象的request,Server收到这个请求去执行对应的工作。比如Client的窗口中有个entry,我们输入文字时,窗口内容需要更新,那么就可以在Client调用 wl_surface_damage 告知Server这块区域无效需要重新绘制了,其实 wl_surface_damage 这个函数其实就是把就是wl_surface 提供的一个 request“damage”封装成一条操作消息,通过 socket从Client发到Server,Server收到了这条消息,解析message,找到操作码去执行对应的操作;同样Server也会给Client发event,Client监听到event去执行对应的回调。
对于一条 message,我们需要了解它的基本结构:
object id + messeage size + opcode + 其他各个参数组成
其中 opcode 就是我们请求或者事件的操作码,这个码其实是由协议文件 xml 中它的出现顺序决定的(从0开始计数),比如 damage 在 wl_surface_request 中是第三个,它的 opcode 就是 2,下面这张图来自 wayland protocol book:
其实我想看 set_window_geometry 的,但是不知道怎么样打印出上面的数据。xdg_surface 的 set_window_geometry 是第4个,所以它的 opcode 应该是 3,在生成的客户端头文件中定义:#define XDG_SURFACE_SET_WINDOW_GEOMETRY 3
协议消息打包
Client请求
接下来以我们最关心的xdg-shell 扩展协议中的 xdg_surface_set_window_geometry 为例来介绍消息是如何从Client发到Server的。这个函数是 wayland-scanner 扫描协议文件xdg-shell-unstable-v5.xml,为xdg_surface 的请求 set_window_geometry 自动生成的供Client使用的函数,函数里面只是简单地执行了下面这个语句:
wl_proxy_marshal((struct wl_proxy *) xdg_surface, XDG_SURFACE_SET_WINDOW_GEOMETRY, x, y, width, height);
wl_proxy_marshal 在我们这几个生成文件中找不到定义,它是 wayland 库提供的,需要在 wayland 代码中去找,src/wayland-client.c 文件中,通过层层调用,在函数 wl_proxy_marshal_array_constructor_versioned 中开始构建 message 并发送:
构建 wl_closure 消息:
closure = wl_closure_marshal(&proxy->object, opcode, args, message);
发送消息:
wl_closure_send(closure, proxy->display->connection)
Server和Client的通信通过 socket 来实现,由于这部分跟我们扩展协议暂时没什么关系,所以没有深入去看,感兴趣可以自己去看代码。附录简单介绍了通信机制。
Server事件
流程其实差不多,只不过Server的函数是 post_event。比如创建对象时发出的global信号:
wl_resource_post_event(resource,
WL_REGISTRY_GLOBAL,
global->name,
global->interface->name,
global->version);
协议消息解包
消息解包就是把上面的 marshal 过程再 demarshal,因为在打包消息的时候知道 object id,interface的接口签名以及 opcode,这样就能根据这几个信息从 interface 中解析得到参数格式,从而把一整条消息解析出来。比如 set_window_geometry,从接口定义中找到它的 wl_message 格式:
{ "set_window_geometry", "iiii", xdg_shell_unstable_v5_types + 0 },
这是一条 wl_message 结构的数据:
struct wl_message {
/** Message name */
const char *name;
/** Message signature */
const char *signature;
/** Object argument interfaces */
const struct wl_interface **types;
};
其中消息签名是”iiii”表示这个 request 的参数是四个整形数据。第三个成员 types,需要从数组 xdg_shell_unstable_v5_types 中去找,这里加 0 表示数组第一条数据。这个请求不需要创建新的对象,所以签名为空。如果需要创建新对象,消息签名中会有“n”,表示 new id,types 就是这个新对象的接口定义。比如 wl_display_get_registry 需要返回 wl_registry 对象,Server和Client需要为这个新对象达成共识,好为后面的request、event传递打下基础,所以需要提前定好 interface,它的这个 types 就是 &wl_registry_interface。// { "get_registry", "n", wayland_types + 9 },
回到 set_window_geometry,协议中定义了这个请求,Client把这个请求组成 message 发送给Server。Server socket 监听机制监听到这条消息后,就会执行 socket 的回调函数 wl_client_connection_data[见附录说明]。这个函数里面反序列化消息,得到 wl_closure,找到目标对象对应的接口函数,利用 libffi 执行 server 端的 implementation 函数。
如何找到目标对象的接口函数?这就需要 server 端来实现了。wayland server 和 client 之间的对象是一一对应的,对于每个 object,它的 interface 定义,有几个 request,有几个 event,以及各自的参数是什么,它们相互之间都很清楚。所以每当Client申请 bind 一个对象,Server需要创建一个资源与之对应,如果这个对象有 request 需要实现,Server就需要去实现这些函数接口并且 set_implementation。
Server代码简述
为了响应Client的请求,Server需要创建对应的对象。首先我们需要去查协议中对象的接口定义,再去实现对象接口中定义的request。代码片段1:
display = wl_display_create ();
wl_display_add_socket_auto (display);
wl_global_create (display, &wl_compositor_interface, 3, NULL, &compositor_bind);
wl_global_create (display, &wl_shell_interface, 1, NULL, &shell_bind);
wl_global_create (display, &xdg_wm_base_interface, 1, NULL, &xdg_wm_base_bind);
wl_global_create (display, &wl_seat_interface, 1, NULL, &seat_bind);
每当Server调用 wl_global_create 创建一个对象,就会将 global 事件打包发给Client,Client收到 global 事件,在回调函数中需要 bind 这个对象。上面创建 wl_compositor 对象时,传递了 &compositor_bind 函数指针,也就是说,如果Client执行 wl_registry_bind 绑定 wl_compositor对象,Server就会执行这个 compositor_bind。在绑定的时候,Server创建与Client对象相对应的资源,并且设置它的实现函数。比如 wl_compositor,它的 request 有两个:
static const struct wl_message wl_compositor_requests[] = {
{ "create_surface", "n", wayland_types + 10 },
{ "create_region", "n", wayland_types + 11 },
};
那么Server代码中就需要定义这两个请求对应的处理函数:
static struct wl_compositor_interface compositor_implementation =
{
&compositor_create_surface,
&compositor_create_region
};
在 compositor_bind 中,主要做这两步工作:
struct wl_resource *resource = wl_resource_create (client, &wl_compositor_interface, 1, id);
wl_resource_set_implementation (resource, &compositor_implementation, NULL, NULL);
如果我们定义的 compositor_create_surface没有问题,Client调用 wl_compositor_create_surface 函数的时候,Server就能执行到 compositor_create_surface,从而打通从Client到Server的请求流程。
但是,由于 wl_compositor,wl_surface 这些对象要实现的接口太太多了,所以在测试代码中,这些对象的 request 我就只定义了空的函数体,保证Client请求能拿到对象,但是没有实际工作。代码片段如下:
static const struct **wl_output_interface** wl_output_implementation = {
.release = wl_output_handle_release,
};
在绑定对象时,设置 request 接口的实现
struct wl_resource *resource = wl_resource_create (client, &wl_output_implementation, wl_output_interface.version, id);
wl_resource_set_implementation(resource, &wl_output_implementation, client_output, wl_output_handle_resource_destroy);
wayland 中有两个 wl_output_interface,千万不要混淆了。其中一个是wl_interface 类型的变量,指明了 name version request 和 event,在生成的glue code文件中定义:
WL_PRIVATE const struct wl_interface ***wl_output_interface*** = {
"wl_output", 3,
1, wl_output_requests,
4, wl_output_events,
};
另一个是结构体,成员是待实现的函数指针,在生成的server 头文件中定义:
struct ***wl_output_interface*** {
void (*release)(struct wl_client *client,
struct wl_resource *resource);
};
每个对象都会有这两个让人迷糊的 wl_xxx_interface,一定要注意区分。结构体 wl_output_interface 里面的函数指针就对应是这个对象需要实现的 request。
实现request
qtwayland 无法设置 Client 窗口的坐标,理论上来说,可能是下面两种原因:
1、客户端程序的 set_window_geometry 没有给 wayland Server 发请求
2、虽然客户端发了请求,但是 wayland Server 端实际上没有实现它。
很多现有的合成器都没有实现 set_window_geometry 接口,因为 wayland 设计理念就是这样。它明确表示不希望客户端程序自己设置坐标,而是觉得客户端的坐标应该由合成器去做决定。
我找到了一个非常非常简单的开源合成器代码,它没有实现 xdg-shell 的 set_window_geometry 请求。一开始我在运行 weston-flower 客户程序时,不管怎么调用 xdg_surface_set_window_geometry尝试设置坐标都无效,一直都显示在位置0,0.
补充:开源代码地址:https://github.com/eyelash/tutorials.git
通过修改合成器代码,实现 xdg-shell 的协议来达到设置客户端窗口的目标。
首先我们需要维护资源的状态,可以定义一个结构体,维护当前 xdg_surface,当前坐标 x,y 等。
第一步:在创建对象时,绑定我们的实现接口:
struct surface *surface = wl_resource_get_user_data (_surface);
surface->xdg_surface = wl_resource_create (client, &xdg_surface_interface, 1, id);
wl_resource_set_implementation (surface->xdg_surface, &xdg_surface_implementation, surface, NULL);
这个surface 就是自定义的结构体,把它设置为服务端资源的user_data:resource->data = surface,当接收到客户端请求的时候,这个数据会作为 wl_resource 的参数一起传递过来,用于为维护处于不断变化中的资源的状态。
第二步:定义各个 request 实现:
static struct xdg_surface_interface xdg_surface_implementation = {
&xdg_surface_destroy,
&xdg_surface_get_toplevel,
&xdg_surface_get_popup,
&xdg_surface_set_window_geometry,
&xdg_surface_ack_configure
};
第三步:实际的实现接口:
static void xdg_surface_set_window_geometry (struct wl_client *client, struct wl_resource *resource,
int32_t x, int32_t y, int32_t width, int32_t height)
{
struct surface *surface = wl_resource_get_user_data (resource);
surface->x = x;
surface->y = y;
redraw_needed = 1;
}
由于我们想要更新服务端 resource 的位置,首先需要拿到服务端维护的xdg_surface,在绑定实现接口时,传入的 surface 参数,可以取出来。redraw_needed 是为了触发下一帧画面时重新渲染客户端窗口,要不然不会更新。
附录
wayland通信
在开发 wayland Client程序时,第一步工作就是连接到wayland Server拿到资源对象,一般是wl_display_connect 再wl_display_get_registry,拿到 wl_registry 对象后,就会监听 global 信号,通过调用 wl_registry_add_listener 来监听,注册信号回调函数。
Server会把可用对象挨个发出 global 信号,Client程序在 global 信号回调函数中就可以 wl_registry_bind 这些对象,生成Client的可用对象。[参考 weston 的客户程序代码 window.c] 但是信号机制一般是对同一个进程来说的,我们可以监听某个对象的某个信号,当收到信号时执行对应的回调函数;而这里其实是两个程序,Server和Client,这种跨进程的信号不是简单地 connect 就可以的,而是需要通过 socket 来传递。 要让两个进程通过 socket 进行函数调用,首先需要将调用抽象成数据流的形式,这些信息通过 wl_closure_marshal 写入 wl_closure 结构,再由 serialize_closure 变成数据流;等到了目标进程,从数据流中通过 wl_connection_demarshal 转回 wl_closure 结构。
1、wayland server 启动时会创建一个 socket(_wl_display_add_socket),并将这个 socket fd 加入 epoll 中,这样一旦有有Client程序连接,epoll 就会通知Server,从而执行回调函数 socket_data。Client可能会有很多,用 epoll 会比较有效率。
2、当有Client程序通过调用wl_display_connect 连接到 server, 在回调函数 socket_data 中会调用 accept 创建新的 socket fd,紧接着创建 wl_client并在创建 wl_client 的时候,wl_client_create (display, fd) 将这个 socket fd 加入 epoll,继续监听新的 socket,为的是响应从Client发过来的请求,回调函数是 wl_client_connect_data。
一个 socket 负责监听Client连接,对于每个Client还有一个socket负责监听Client的请求。