2021-01-20 wayland 协议的实现

如何扩展 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

wayland 协议生成 gluecode.png

上面这张图是网上找到的,开发的时候基本上就是这么个流程。写一个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就可以了。


wayland.png

上面这张图被多次引用,它很清楚的描述了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:

2021-01-16_13-25.png

其实我想看 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的请求。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,905评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,140评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,791评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,483评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,476评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,516评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,905评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,560评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,778评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,557评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,635评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,338评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,925评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,898评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,142评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,818评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,347评论 2 342

推荐阅读更多精彩内容