|
| 1 | ++++ |
| 2 | +date = '2025-10-27T17:41:01+08:00' |
| 3 | +draft = true |
| 4 | +title = 'Wayland Compatibility' |
| 5 | ++++ |
| 6 | + |
| 7 | +# 从mpv闪退引出的wayland协议兼容性思考 |
| 8 | + |
| 9 | +在UOS的最新kwin wayland环境下,mpv播放器在反复全屏和还原过程中会出现闪退的问题,笔者经过一番排查,最终确定根因是kwin_wayland向mpv发送了一条mpv无法处理的高版本版本的wl_pointer事件,导致客户端在libwayland中abort了。借此机会,笔者仔细研究了出现该问题的条件和libwayland的机制,本文记录一些笔者对于系统中wayland协议兼容性的思考。 |
| 10 | + |
| 11 | +传统上,我们将兼容性分级为api兼容性和abi兼容性。没有保证api兼容的库,应用将无法在新版本上编译,库应首先保证api兼容性,以确保新版本推出时,适配旧版本的应用可以进行重构建。仅仅没有保证abi兼容性的库,但保证了api兼容的库,应用可以在新版本的库中编译,但是在运行时会产生问题,比如无法访问某一个数据成员,库实现方往往会采用一些隐藏数据成员的方法来实现abi兼容,如C语言编程中的结构体数据成员隐藏和Qt编程中的d指针都是此种方法。api兼容是abi兼容的基础,首先要保证api兼容性,才有可能谈abi兼容性。对于没有破坏api兼容性但是破坏了abi兼容性的库,应用程序也可以通过重编的方法重新保证abi一致(只是保持单次一致,兼容性问题仍然存在)。上述分类是针对于动态库的,静态库因为其每次都需要和应用程序一起重新编译,不存在所谓的abi兼容性。 |
| 12 | + |
| 13 | +笔者在本文要讨论的wayland协议,即wayland.xml,它作为文本文件,在各个项目中共享,经过wayland-scanner扫描后生成代码,和应用一起编译,可以看作是一种标准的静态库分发过程。那么对于wayland协议,我们应该如何讨论它的兼容性呢?笔者认为,上述问题应分解为:我们应该在何种情况下讨论wayland协议的何种兼容性。考虑到实际情况和wayland实际处理,应该考虑分发渠道和应用重构建状态的组合。应用可能通过自维护和系统共享两种分发方式获取到wayland协议,在系统wayland协议升级之后,它也会存在已重构建和未重构建这两种状态。即应用存在自分发已重构建、自分发未重构建、系统共享已重构建和系统共享未重构建这四种情况。对于兼容性层面,我们应该考虑api兼容性,以及客户端和合成器采用不同版本协议的通信兼容性。本文只讨论由libwayland提供的wayland层面保证的兼容性,即应用对兼容性无感知,它只会对自己有感知的版本进行实现,并假定对端也是使用相同版本。笔者认为,该情况对于客户端是很常见的情形,对于合成器这种责任重大的应用则见仁见智。 |
| 14 | + |
| 15 | +综上,本文讨论的情况可总结为如下表格: |
| 16 | + |
| 17 | +| 分发形式 / 重构建状态 | API / 源码兼容性 | 协议通信兼容性 (客户端 vs. 合成器) | |
| 18 | +| :--------------------------------- | :--------------: | :--------------------------------: | |
| 19 | +| **自分发wayland.xml (已重构建)** | ✅ | ✅ | |
| 20 | +| **自分发wayland.xml (未重构建)** | ✅ | ✅ | |
| 21 | +| **系统共享wayland.xml (已重构建)** | ✅ | ❌ | |
| 22 | +| **系统共享wayland.xml (未重构建)** | ✅ | ✅ | |
| 23 | + |
| 24 | +从表格中可以看到,wayland保证了大部分情况的两种兼容性,唯独没办法保证系统共享wayland.xml在已重构建情况的兼容性。 |
| 25 | + |
| 26 | +wayland.xml中添加新协议的方式,保证了wayland.xml在所有情况下的源码兼容性。每次往wayland.xml中添加一个event或者一个新的enum entry的时候,审核者要求开发者将请求顺序添加在interface的最后面或者enum定义的后面,这样通过wayland scanner生成的代码中的数据结构的前半部分会兼容旧版本,同一含义的枚举值也保持不变,该行为在一定程度上也保证了通信兼容性。对于修改原有协议和删除原有请求或事件,这种行为是被禁止的,需要通过另外开启一个新版本协议的方式进行,这也能解释为什么wayland实现同一个功能的协议版本这么多,当你觉得原版本的功能不满足你的需求的时候,就会新开一个版本,例如text input现在就已经迭代到v4了。 |
| 27 | + |
| 28 | +对于协议通信兼容性,wayland.xml中要求对每一个新加的事件或者请求添加一个since字段,说明事件或者请求添加的最低版本。但事实上,since字段起到的作用十分有限,它只能防止客户端调用某一个合成器不支持的请求,但是并不能有效防止合成器向客户端发送一个高版本的事件消息。但是libwayland并不是完全没有对该情况进行检查,从libwayland的代码中可以看到: |
| 29 | + |
| 30 | +```C |
| 31 | +static int |
| 32 | +queue_event(struct wl_display *display, int len) |
| 33 | +{ |
| 34 | + ...... |
| 35 | + |
| 36 | + if (opcode >= proxy->object.interface->event_count) { |
| 37 | + wl_log("interface '%s' has no event %u\n", |
| 38 | + proxy->object.interface->name, opcode); |
| 39 | + return -1; |
| 40 | + } |
| 41 | + |
| 42 | + ...... |
| 43 | + return size; |
| 44 | +} |
| 45 | +``` |
| 46 | +
|
| 47 | +libwayland会在queue_event,也就是读取到消息将消息添加到队列中的时候,检查opcode和interface的event count的大小关系,从而达到防止接收到高版本事件的目的。但是笔者为什么说该防护是有限的呢?因为interface的event_count可能并不是客户端预期的值。 |
| 48 | +
|
| 49 | +讨论上表中唯一没有保证兼容性的情况,应用使用系统共享的wayland.xml,在v1版本的时候,应用实现了v1版本定义的所有事件,合成器实现的也是v1版本,此时双方可以正常通信。当wayland.xml升级到v2版本,添加了一个事件,合成器跟进升级了v2版本,实现了这一个事件。客户端因为没有升级到v2版本的需求,仍然只实现v1版本,在系统提供的wayland.xml升级之后它进行了重构建,因为事件添加的特性,此次从v1升级到v2并不会破坏客户端的构建,客户端能够正常地构建,不过一旦运行,合成器往客户端发送v2版本的事件的时候,事件就会突破上述检查直至处理的时候找到null的handler而引发abort,因为客户端在重构建后使用的v2版本的wayland.xml,event_count是通过xml生成,自然也是v2版本的event_count。对于另外三种情况,应用使用的都是应用已感知版本的wayland.xml协议构建,event_count是正确的,也就可以在上述代码中被拦截,保证了协议通信兼容性。 |
| 50 | +
|
| 51 | +细心的读者可以发现,系统共享wayland.xml并且已重构建的情况,相当于应用使用某一版本的wayland.xml协议但是并没有将所有handler实现(哪怕handler不处理任何逻辑)。根据开发经验,这在wayland开发里面显然是一种错误的做法,当合成器发送客户端未实现的事件时,客户端就会直接终止。虽然这是一个在开发者看来明显缺乏审查和被忽视的场景,但对于整个系统维护来说,这是一个再正常不过的场景(系统中wayland.xml升级,应用在无感知的情况下重新构建),比如本文的引子mpv播放器。它是一个三方应用,系统开发者在升级wayland.xml的时候显然不会帮mpv升级wayland的实现,而只会确认它是否能在新版本的wayland.xml协议上编译通过并基本正常运行。或者退一步,系统管理员发现了mpv存在兼容性问题,那如何去对它进行修改也是一个问题。如若mpv上游社区是一个活跃的社区,那更新版本或许是一个轻松的解决方案,但mpv版本和libmpv版本绑定,深度影院依赖libmpv,升级mpv意味着深度影院要重新适配新版的libmpv。为了一个用户可能不常使用到的三方应用而重新适配新版libmpv,只能说单就这个问题本身而言代价太大了,不易推动。 |
| 52 | +
|
| 53 | +要解决上述问题,笔者认为目前有三种办法: |
| 54 | +
|
| 55 | +1. 改进libwayland,让其支持运行时动态检测协议通信兼容性,客户端在bind的时候已经指定了想要使用的协议版本,但是libwayland并没有充分利用这个版本号。 |
| 56 | +2. 每次libwayland升级时,所有应用均升级到相同的协议版本。这显然工作量很大,可以通过添加空handler减少工作量,但同样为后期维护带来了相当大的复杂度。 |
| 57 | +3. 每一个使用wayland.xml生成代码的应用,都自己维护一个版本的xml,也就是表格中的自分发方式。Qt项目实际上就是使用这种方式。系统中直接接触到wayland.xml的应用并不多,可能只有GTK和Qt这种底层组件需要做这种工作,但是这种做法加剧了wayland生态的碎片化,从工程领域也不推荐。 |
0 commit comments