Wayland协议是由几层抽象构建的。它从基本的线路协议格式开始,这是一种可解码的消息流,使用事先商定的接口。然后我们有更高级的程序来列举接口,创建符合这些接口的资源,并交换有关它们的消息-Wayland协议及其扩展。在此之上,我们有一些更广泛的模式,这些模式在Wayland协议设计中经常使用。我们将在本章中介绍所有这些。让我们从下到上开始工作吧。
2.1 连通协议基础
注意:如果您只想使用libwayland,本章是可选的-请跳到第2.2章。
连通协议是一个32位值的流,以主机的字节顺序(例如,在x86系列CPU上的小端)进行编码。这些值代表以下基本类型:
- int,uint:32位有符号或无符号整数。
- fixed:24.8位有符号定点数。
- object:32位对象ID。
- new_id:32位对象ID,在接收时分配该对象。
除了这些基本类型之外,还使用了以下其他类型:
- string:以32位整数为前缀的字符串,指定其长度(以字节为单位),后跟字符串内容和一个空字符终止符,用未定义数据填充到32位。编码未指定,但实践中使用UTF-8。
- array:任意数据的Blob,以32位整数为前缀,指定其长度(以字节为单位),然后是数组的逐字内容,用未定义数据填充到32位。
- fd:在主要传输中值为0,但使用Unix域套接字消息(msg_control)中的辅助数据将文件描述符传输到另一端。
- enum:已知常量的枚举中的一个值(或位图),编码为32位整数。
消息
连通协议是使用这些基本类型构建的消息流。每条消息都是作用于对象的事件(在服务器向客户端发送消息的情况下)或请求(客户端向服务器)。
消息头有两个字段。第一个字段是受影响对象的ID。第二个是两个16位值;上16位是消息的大小(包括头本身),下16位是事件或请求操作码。消息参数根据双方事先商定的消息名称而定。接收方查找对象的ID接口及其操作码定义的事件或请求,以确定消息的名称和性质。
要理解消息,客户端和服务器必须首先确定对象。对象ID 1是预先分配的Wayland显示单例,可用于引导其他对象。我们将在第4章讨论这个问题。下一章将介绍接口是什么,以及请求和事件如何工作,假设您已经协商了对象ID。说到这个……
对象ID
当一条消息带有new_id参数时,发送方会为它分配一个对象ID - 用于该对象的接口是通过附加参数建立的,或预先为该请求/事件商定。这个对象ID可以在未来的消息中使用,既可以作为头部的第一个单词,也可以作为object_id参数。客户端在[1, 0xFEFFFFFF]范围内分配ID,服务器在[0xFF000000, 0xFFFFFFFF]范围内分配ID。ID从该范围的低端开始,每个新对象分配时递增。
对象ID为0表示空对象;即,不存在的对象或明确缺少的对象。
传输
迄今为止,所有已知的Wayland实现都通过Unix域套接字工作。这特别用于一个原因:文件描述符消息。Unix套接字是最实用的传输方式,能够在进程之间传输文件描述符,这对于大型数据传输(键盘映射、像素缓冲区和剪贴板内容是主要用例)是必要的。理论上,可以使用不同的传输(例如TCP),但有人需要找出一种替代传输大量数据的方法。
要找到要连接的Unix套接字,大多数实现都像libwayland一样:
- 如果设置了WAYLAND_SOCKET,将其解释为已经建立连接的文件描述符编号,假设父进程为我们配置了连接。
- 如果设置了WAYLAND_DISPLAY,将其与XDG_RUNTIME_DIR连接以形成Unix套接字的路径。
- 假设套接字名称为wayland-0,将其与XDG_RUNTIME_DIR连接以形成Unix套接字的路径。
- 放弃。
2.2 接口、请求和事件
Wayland协议通过发出对对象起作用的请求和事件来工作。每个对象都有一个接口,该接口定义了可能的请求和事件,以及每个请求和事件的签名。让我们考虑一个示例接口:wl_surface。
请求
表面是一个可以在屏幕上显示的像素框。它是我们构建应用程序窗口等事物的原始组件之一。它的一个请求是从客户端发送到服务器的“damage”,客户端使用它来指示表面的某些部分已经更改并需要重新绘制。下面是有注释的“damage”消息的示例(以十六进制表示):
0000000A Object ID (10)
00180002 Message length (24) and request opcode (2)
00000000 X coordinate (int): 0
00000000 Y coordinate (int): 0
00000100 Width (int): 256
00000100 Height (int): 256
这是一个会话的片段-表面早些时候被分配并分配了一个ID号为10。当服务器收到此消息时,它会查找ID号为10的对象,发现它是一个wl_surface实例。知道这一点后,它会查找请求的签名,该请求的操作码为2。然后它知道期望四个整数作为参数,并且可以解码消息并将其分派到内部进行处理。
事件
服务器也可以向客户端发送消息,即事件。服务器可以发送的一个关于wl_surface的事件是“enter”,当该表面正在特定输出上显示时,它会发送该事件(客户端可能会对此做出响应,例如,通过调整其用于HiDPI显示器的缩放因子)。以下是此类消息的示例:
0000000A Object ID (10)
000C0000 Message length (12) and event opcode (0)
00000005 Output (object ID): 5
此消息通过其ID引用另一个对象:该表面正在显示的wl_output对象。客户端收到此消息后,会以与服务器相同的节奏进行操作。它会查找对象10,将其与wl_surface接口相关联,并查找与操作码0相对应的事件的签名。它会相应地解码其余的消息,查找ID为5的wl_output,然后将其分派到内部进行处理。
接口
定义请求和事件列表的接口、与每个请求和事件相关联的操作码以及可以用来解码消息的签名都是事先商定的。我相信你一定想知道这是如何实现的——只需翻到下一页,悬念就会结束。
2.3 高层协议
在1.3章中,我提到wayland.xml可能已经与您系统上的Wayland软件包一起安装了。现在,在您最喜欢的文本编辑器中查找并打开该文件。正是通过此文件以及类似的其他文件,我们定义了Wayland客户端或服务器支持的接口。
每个接口都与它的请求和事件及其各自的签名一起在文件中定义。我们使用XML,每个人最喜欢的文件格式,来实现这一目的。让我们看一下我们在前一章讨论的关于wl_surface的示例。这是一个样本
<interface name="wl_surface" version="4">
<request name="damage">
<arg name="x" type="int" />
<arg name="y" type="int" />
<arg name="width" type="int" />
<arg name="height" type="int" />
</request>
<event name="enter">
<arg name="output" type="object" interface="wl_output" />
</event>
</interface>
注意:为了简洁起见,我已经修剪了这段摘录,但如果你面前有wayland.xml文件,请找出此接口并自行检查——包括额外的文档,解释每个请求和事件的目的和精确语义。
在处理此XML文件时,我们按照它们出现的顺序为每个请求和事件分配一个操作码,从零开始编号并独立递增。结合参数列表,当通过线路接收到请求或事件时,您可以解码它,并根据XML文件中提供的文档决定如何编写您的软件以相应地运行。这通常以代码生成的形式出现——我们将在第3章讨论libwayland如何实现这一点。
从第4章开始,本书的其余部分大部分都致力于解释此文件以及一些补充协议扩展。
2.4 协议设计模式
在大多数Wayland协议的设计中,有一些关键概念被应用在其中,我们在此应该简短地涵盖它们。这些模式在Wayland高层协议和协议扩展中普遍存在(好吧,至少在Wayland协议中是这样)。如果您正在编写自己的协议扩展,明智的做法是应用这些模式:
原子性
Wayland协议设计模式中最重要的模式是原子性。Wayland的一个既定目标是“每一帧都是完美的”。为此,大多数接口允许您使用多个请求以事务方式更新它们,以构建其状态的新表示,然后一次性提交它们。例如,wl_surface上可以配置多个属性:
- 已附加的像素缓冲区
- 需要重绘的损坏区域
- 为优化而定义的不可见区域
- 可以接受输入事件的区域
- 旋转90度等变换
- 用于HiDPI的缓冲区缩放
接口包括用于配置每个属性的单独请求,但这些请求应用于挂起状态。只有当提交请求发送时,挂起状态才会合并到当前状态,允许您在单个帧内原子地更新所有这些属性。与其他几个关键的设计决策相结合,这使得Wayland组合器能够在每一帧中完美呈现所有内容-没有撕裂或部分更新的窗口,每个像素都在其位置上,每个位置都在其像素中。
资源生命周期
另一个重要的设计模式是避免服务器或客户端发送与无效对象相关的事件或请求的情况。因此,定义具有有限生命周期的资源的接口通常包括请求和事件,通过这些请求和事件,客户端或服务器可以声明他们不再为该对象发送请求或事件的意图。只有当双方都同意这一点时,才会异步销毁他们为该对象分配的资源。
Wayland是一个完全异步协议。消息保证按发送顺序到达,但仅相对于一个发送者而言。例如,当客户端决定销毁其键盘设备时,服务器可能有几个输入事件排队等待。在服务器赶上之前,客户端必须正确处理它不再需要的事物的相关事件。同样,如果客户端在销毁一个对象之前为其排队等待一些请求,那么它必须以正确的顺序发送这些请求,以便在客户端同意销毁该对象后不再使用该对象。
版本控制
Wayland协议中使用了两种版本控制模型:不稳定版本和稳定版本。这两种模型只允许向后兼容的更改,但当协议从不稳定版本过渡到稳定版本时,最后一次破坏性更改是允许的。这为协议提供了一个孵化期,在此期间我们可以对其进行实践测试,然后应用我们的见解进行最后一次大规模的破坏性更改,以制定一个经得起时间考验的协议2。
要进行向后兼容的更改,您只能将新事件或请求添加到接口的末尾,或将新成员添加到枚举的末尾。此外,每个实现必须限制自己只使用连接的另一端支持的消息。我们将在第5章讨论如何确定每个接口的哪些版本被各方使用。
- 1 除了那个接口。你看,至少我们尝试了,对吗?
- 2 请注意,在撰写本文时,许多有用的协议仍处于不稳定状态。它们可能有点笨拙,但仍然被广泛使用,这就是为什么向后兼容性很重要的原因。当将协议从不稳定版本升级到稳定版本时,它是通过允许软件同时支持不稳定版本和稳定版本的方式完成的,从而实现更平滑的过渡。
网友评论