"Black Square" 是 Kazimir Malevich 的作品:
Black Square by Kazimir Malevich 最后,我们会有一个 这样可以运行的黑盒子(其实你得不到,会直接𐳐的一下退出,后面在说,作为事例可以了):
Screenshot of a solid black square on top of some code 设置好环境
虽然存在独立的Wayland实现如Skylane 和Sudbury ,但是libwayland-client是参考实现,目前来说最流行的。并且还有很多它的语言绑定如Python,C++,Rust,Haskell甚至是Objective-C。
但是在这个小册,我们只会用纯C的实现。
Wayland本身不局限于Linux平台,但是为了简便还是以Linux为示例平台。
然后你要根据自己的发行版安装wayland协议实现的开发库,一般就叫wayland。这些就自行查阅相关的wiki了。安装了之后你应该有一个wayland-scanner之类名字的命令。
你需要wayland-client.h这个头文件以及其它相关的头文件
要运行一个wayland程序还需要一个wayland环境,一个wayland作为backend的窗口管理器就行。gnome,sway或者weston。装完了直接从tty下面敲就行了,具体的配置去对应程序官网下面找就行。
第一步
创建一个main.c
文件,写如下内容:
Copy #include <stdio.h>
#include <wayland-client.h>
int main(void)
{
struct wl_display *display = wl_display_connect(NULL);
if (display) {
printf("Connected!\n");
} else {
printf("Error connecting ;(\n");
return 1;
}
wl_display_disconnect(display);
return 0;
}
用下面的命令编译:
Copy $ gcc main.c -l wayland-client -o runme
$ ./runme
Connected!
恭喜,你完成了基本的连接操作!
Wayland的基本原理
Wayland是客户端服务器架构的协议。一个客户端想要在屏幕上显示点什么,首先就需要和服务器建立连接。那个服务器就是常说的窗口管理器,负责给一个客户端分配窗口,渲染窗口标题,管理窗口的组合方式等。
管理器也许会对窗口的变换做一些特效,比如渐隐渐入,纸片效果或放大镜效果等。窗口之间有磁铁吸引的感觉等都是由窗口管理器决定的,客户端本身不知道这些的存在。
Wayland也分层。Wire Format 决定了数据是如何序列化,传输以及反序列化的。这些琐碎的东西不需要我们自己管理,有wayland库来帮我们建立这一层抽象。
Wire Format指定使用unix domain socket来进行交流。这个socket文件一般位于linux系统的某个位置,也是程序运行的起始点。这里一般是由XDG_RUNTIME_DIR
指定所在目录,文件名一般是waylnad-0
。可以使用WAYLAND_DISPLAY
来显示文件。
这个socket是由服务器启动时创建的,同时还有WAYLAND_DISPLAY
这个环境变量。
接着,wayland指定了对象模型 ,功能的调用是通过协议调用这些对象的方法 完成的。注意,这些对象只是一个抽象概念,并没有真的内存来进行存放或执行。接下来你会看到一些扩展协议就是围绕这些对象完成的。添加一个对象,进行方法调用,完事。一个协议对应一个对象。
方法调用有两个说法,事件 和请求 。其实是一个东西,就是socket发的一个包,包的内容是method call,只是从客户端发到服务器的是请求,反之是事件。本质上就是个有格式的数据。客户端的那些方法其实就是一个这个数据格式的封装罢了,之后会看到。
wayland就是个协议的规范,记住这一点。它允许实现建立一个个具体的协议,比如xdg-shell。通过将一个个协议抽象成server的上的对象来表示这个server所支持的协议(所有的功能)。然后协议的具体内容通过协议对象的”方法“来进行封装,客户端根据协议的内容准备好资源,然后发个包注册或者通知对方,对方再做动作。wayland不管资源,它只是一个规范罢了。怎么实现,也就是协议规范的内容具体怎么做由我们自已安排。
比如wl_pointer
这个对象有set_cursor
调用来改变光标的形状以及motion
的事件。这个现在不要纠结,继续看下去就知道了,待会儿再回来看这个就明白了。
所有的方法都没有返回值,这就对了,它们只是个数据封装而已,同时这也是为了支持wayland的异步特性。下面就是为啥:
wayland是基于消息的协议,方法调用是以消息的形式再客户端和服务器之间来进行传输的(消息的方向由请求或事件这两个消息的角色决定)。一个消息包含对象的ID,方法的识别码和参数组成。一个消息的内容结构是由wire format指定的,wayland官网上有的。
这里面重要的是消息的发送不会阻塞客户端(服务器)。它们可以自由的继续做该做的事情,好像它们之前啥都没做一样。换句话说,这种通信之间是没以延时的。wayland就是特地这样设计的,高效而且快。
结果就是wayland是异步的。你不需要阻塞在那里等待一个结果,也没有什么值可以返回给你。如果一个方法调用逻辑上需要返回值,它通长是通过另一个带着参数的方法调用实现的。例如一个客户端需要通过wl_shell_surface.set_fullscreen
指定全屏的请求。服务器就通过wl_shell_surface.configure
事件,通过这个方法的参数来返回新窗口的长宽。这种一来一回的策略显然是要有延时的,而且使用一个带有返回值的函数似乎也不会有啥好处。
这里需要明白的是,客户端和服务器实现的逻辑也应该是异步的。set_fullscreen
和configure
这两个函数调用之间的延时可能不仅是由CPU处理时间引起的(或者有相当大的网络延时如果这是通过网络来交互的话)。服务器可能询问用户是否希望允许他的窗口变全屏并且指只允许在用户同意后发起configure
事件。通常来说,窗口应当把set_fullscreen
请求当做一个它会在不久后全屏的提示。而且它应当继续做正在做的事,就好像什么都没发生一样。它还需要以同样的方式在任何时候都能对configure
事件作出反映(比如改变大小),无论这个事件是谁发起的。
另一个通用的模式就是一个函数初始化并且返回一个新的对象。这可以同样的通过请求/事件方法来实现。但是既然wayland对象本身不携带任何的数据,客户端和服务端唯一需要协商的就是新的对象的ID。简单来说,服务端需要把ID传给客户端。这个操作需要一个来回的延时(你需要发个请求再等服务端回你ID)。因此,在wayland里面,是客户端将这个新的对象ID作为一个逻辑上创建那个对象请求的参数传给服务端。使用这种方式就不需要阻塞或者等待了,而且客户端可以立即继续,包括继续创建新的对象。
尽管用的很少,同样的技术也被用在其它的地方。有的时候会有这样一个事件/请求对(比如wl_shell_surface.ping
and wl_shell_surface.pong
),当服务端创建了一个新的对象的时候,它会把对象的ID以事件参数的形式发给客户端。
最后,wayland明确了对象的具体类型,叫做interface
。我们可以调用这些对象上面的方法(method),包括上面提到的。那些常用的就是 Wayland core protocol 。举个例子,wl_surface
接口有wl_surface.attach
请求。
不像我们之前讨论的,核心协议被设计成有可扩展性。也就是说可能之后有新的接口加到里面。现在就有多个扩展存在,最出名的就是xdg-shell
。我们会在接下来的一节讨论这个。
Wayland-client 库
创建一个wayland客户端最简单的方法就是使用wayland-client库。基本上来说,它将wire格式都抽象了一层。
添加下面的头文件来使用这个库
Copy #include <wayland-client.h>
并且通过以下命令链接这个库
Copy $ gcc main.c -l wayland-client
wayland-client通过存储一些对象的元数据来跟踪这些对象。元数据存储在对应的不透明结构中。你永远都可以通过指针使用这些结构并且不需要关心它里面的样子。你需要把这些结构的实例就当做存在wayland服务端上的那个来操作。
具体的说,发送一个请求就等于调用一个函数,传入对应的对象指针。请求的参数就是函数的参数。
Copy struct wl_shell_surface *shell_surface = ...;
wl_shell_surface_set_title(shell_surface, "Hello World!");
这里,我们构建一个关于wl_shell_surface
的wl_shell_surface.set_title
请求。这个请求只有一个参数也就是新的UTF-8编码的标题。
为了响应服务端发起的事件,我们需要设置好事件处理函数。这里的API很直白。
Copy void surface_enter_handler(void *data, struct wl_surface *surface, struct wl_output *output)
{
printf("enter\n");
}
void surface_leave_handler(void *data, struct wl_surface *surface, struct wl_output *output)
{
printf("leave\n");
}
...
struct wl_surface *surface = ...;
struct wl_surface_listener listener = {
.enter = surface_enter_handler,
.leave = surface_leave_handler
};
wl_surface_add_listener(surface, &listener, NULL);
这里我们为wl_surface
对象创建了事件处理函数。两个,enter
和leave
。两个都只有一个参数,类型是wl_output
。我们需要创建一个监听结构,这个结构需要有指向这两个函数的指针。并且通过方法add_listener
来添加这个监听器。第一个就是wl_surface
指针,第二个就是那个监听结构的指针,第三个我们这里传NULL 。第三个参数是传给两个响应函数的void *data
,也就是函数的第一个参数。对于这样的对象,wl_surface_listener
可以复用。
当我们发起的请求的一个参数有一个新的ID(就是这个请求会创建一个新的对象),这个对应的函数会返回一个指向这个新对象的指针用作表示这个新的对象。如下例:
Copy struct wl_shm *shm = ...;
struct wl_shm_pool *pool = wl_shm_create_pool(shm, fd, size);
这里,我们通过wl_shm.create_pool
请求创建一个新的wl_shm_pool
对象。这个函数有new_id
,fd
,和size
参数。我们会得到一个新的对象指针而不是传递这个新的ID。
全局对象
就像我之前提到的,wayland是面向对象的,意味着它所有的东西都是关于对象的。有一些对象(interface
)会有多个实例,就像 wl_buffer
(它可以根据客户程序的需要而有多个实例)。其它的一些只有一个实例(这个模式叫做 singleton )。比如,wl_compositor
只能有一个。也有一些接口存在两者之间。比如,一般都只有固定数量的display (由wl_output
表示)连接server。
这就引入了一个新的概念全局对象 (global objects )。全局对象显示它所在环境和compositor
的属性。大多数全局对象都是对应API集的入口点。接下来的几节会逐一探索。
把下面的代码放入main.c
文件中:
Copy #include <stdio.h>
#include <wayland-client.h>
void registry_global_handler
(
void *data,
struct wl_registry *registry,
uint32_t name,
const char *interface,
uint32_t version
) {
printf("interface: '%s', version: %u, name: %u\n", interface, version, name);
}
void registry_global_remove_handler
(
void *data,
struct wl_registry *registry,
uint32_t name
) {
printf("removed: %u\n", name);
}
int main(void)
{
struct wl_display *display = wl_display_connect(NULL);
struct wl_registry *registry = wl_display_get_registry(display);
struct wl_registry_listener registry_listener = {
.global = registry_global_handler,
.global_remove = registry_global_remove_handler
};
wl_registry_add_listener(registry, ®istry_listener, NULL);
while (1) {
wl_display_dispatch(display);
}
}
编译并运行:
Copy $ gcc main.c -l wayland-client -o runme
$ ./runme
interface: 'wl_drm', version: 2, name: 1
interface: 'wl_compositor', version: 3, name: 2
interface: 'wl_shm', version: 1, name: 3
interface: 'wl_output', version: 2, name: 4
interface: 'wl_output', version: 2, name: 5
interface: 'wl_data_device_manager', version: 3, name: 6
interface: 'gtk_primary_selection_device_manager', version: 1, name: 7
interface: 'zxdg_shell_v6', version: 1, name: 8
interface: 'wl_shell', version: 1, name: 9
interface: 'gtk_shell1', version: 1, name: 10
interface: 'wl_subcompositor', version: 1, name: 11
interface: 'zwp_pointer_gestures_v1', version: 1, name: 12
interface: 'wl_seat', version: 5, name: 13
interface: 'zwp_relative_pointer_manager_v1', version: 1, name: 14
interface: 'zwp_pointer_constraints_v1', version: 1, name: 15
^C
我们刚刚就写了一个简化版的 weston-info
命令。由于没有写退出代码,所以你可能需要用Ctrl-C
来中断这个程序。
接下来我们会逐步分析刚刚发生了什么。首先,wl_display
是一个全局的单实例对象,代表了整个与server的连接。它有很多特殊的地方。这是唯一一个你不需要创建的对象:当你成功建立连接的时候你就有了这个对象。wayland-client 库使用 wl_display_connect()
函数返回它。尽管它的名义有个wl_display,但是这个函数并不是wl_display
对象的方法。同样的,wl_display_dispatch()
也不是wl_diplay.dispatch
方法,它只是一个 wayland-client
里面的函数。
另一方面, wl_display.get_registry
更像是Wayland的一个请求。它使用 new_id
机制,然后我们拿到wl_registry
对象。
wl_registry
是另一个全局单实例对象,它的功能是告知所有其它全局对象的存在。一旦有什么东西改变了或者是客户程序初始化完成(比如新的display接入了),Wayland是通过registry API告知客户程序它所在的环境而并非通过一个API调用查询服务器的状态和Client所在的Wayland环境。通过这种方式,Wayland是完全”热插拔“的。这也是客户程序查询Wayland API可用版本和Wayland支持插件的方式。
registry 通过 wl_registry.global
事件告知新的全局对象以及通过 wl_registry.global_remove
事件告知它们的移除。.
现在,随着我们的深入,所有这些信息都会非常有用,但是目前,我们通过一些魔法只获取当前必要的对象:
Copy #include <stdio.h>
#include <string.h>
#include <wayland-client.h>
struct wl_compositor *compositor;
struct wl_shm *shm;
struct wl_shell *shell;
void registry_global_handler
(
void *data,
struct wl_registry *registry,
uint32_t name,
const char *interface,
uint32_t version
) {
if (strcmp(interface, "wl_compositor") == 0) {
compositor = wl_registry_bind(registry, name,
&wl_compositor_interface, 3);
} else if (strcmp(interface, "wl_shm") == 0) {
shm = wl_registry_bind(registry, name,
&wl_shm_interface, 1);
} else if (strcmp(interface, "wl_shell") == 0) {
shell = wl_registry_bind(registry, name,
&wl_shell_interface, 1);
}
}
基本上,wl_registry.global
传递的“名字”还不是真正的对象ID,我们需要使用wl_registry_bind()
去设置好它。因bind创建静态未知类型的新对象的特殊方法,在wayland-client里的函数同 wl_registry.bind
请求下的函数在函数签名上有点不一样。
我们同样的将 registry_listener
对象的定义放在main()
的外边:
Copy void registry_global_remove_handler
(
void *data,
struct wl_registry *registry,
uint32_t name
) {}
const struct wl_registry_listener registry_listener = {
.global = registry_global_handler,
.global_remove = registry_global_remove_handler
};
下一步,在main()
中,我们等待global
事件的第一次出现。该事件保证包含所有当前可用的全局对象:
Copy int main(void)
{
struct wl_display *display = wl_display_connect(NULL);
struct wl_registry *registry = wl_display_get_registry(display);
wl_registry_add_listener(registry, ®istry_listener, NULL);
// wait for the "initial" set of globals to appear
wl_display_roundtrip(display);
// all our objects should be ready!
if (compositor && shm && shell) {
printf("Got them all!\n");
} else {
printf("Some required globals unavailable\n");
return 1;
}
while (1) {
wl_display_dispatch(display);
}
}
这里,wl_display_roundtrip
(同样的,它同样不是 wl_display
的方法,而是一个特殊的底层使用wl_display.sync
请求的wayland-client
函数。)用于清空消息队列(事件和请求)。他会一直阻塞客户程序,直到所有的事件或请求都被接收或发送。最后等待所有的事件监听器都被执行。
继续编译运行:
Copy $ gcc main.c -l wayland-client -o runme
$ ./runme
Got them all!
^C
搞定!
surface和buffer
Wayland协议中没有"window"这个词,相反的它使用"surface "。如果你稍作思考的话,它的确有其目的:那些在屏幕上的长方形的东西和墙上的可以让你从里面看到外面的洞有什么相同的地方呢?你可以非常容易的将"windows"想象成平面,一层层纸一样的东西,飘在桌面壁纸上面。
但是surface(平面)不仅仅是桌面窗口。还有其它“漂浮的东西”也是surface。比如菜单,鼠标指针(cursor),你通过鼠标指针拖放的玩意儿,等等。一个surface是一个窗口,一个指针还是一个拖放的图标取决于它的role ,角色。 我们稍后讲解role。但是无论什么角色,它们都有许多共同的功能:它们显示并更新它们的内容,当然还有改变大小。
还有一点需要注意,就是你不会直接在一个surface做渲染工作比如画一个图标,加载一个图片这些。你还需要一个buffer 。你需要创建一个buffer,并将需要渲染的东西写入这个buffer中,再将这个buffer分配(attach) 给一个surface。如果你需要更新或者重新渲染一些东西,你将新的内容写入到另一个 buffer(当然也可以用同一个buffer,buffer就是一块可读写的内存块,重用buffer的做法也很常见),然后在将这个buffer分配给surface。最后一直重复这些步骤去渲染每一个你想要显示的帧。这个和直接渲染到surface的方法相比有诸多优势:比如,你可以提前或并行的渲染每一帧然后在恰当的时候显示它们。另一个好处是每一帧都是“完美”的。你只会将渲染完毕的buffer分配给surface(一个surface,多个buffer服务)。结果就是避免了任何的画面撕裂(screen tearing )。
要创建一个surface,使用wl_compositor.create_surface
请求。并且使用wl_surface.attach
来分配一个buffer。最后记得wl_surface.commit
:
Copy struct wl_compositor *compositor = ...;
struct wl_surface *surface = wl_compositor_create_surface(compositor);
struct wl_buffer *buffer = ...;
wl_surface_attach(surface, buffer, 0, 0);
wl_surface_commit(surface);
我们这里使用从registry得到的compositor对象。下一节讨论如何创建一个buffer。但是现在我想指出单单创建surface并不足以让它显示在屏幕上,你还需要给它分配一个role 。说完buffer再来讨论role。
分配一个buffer内存
Wayland被特意设计成能支持不同格式以及不同特性的buffer。 只用核心协议创建的buffer只能是共享内存池(shared memory pool),但是扩展允许添加新的buffer种类。比如在GPU中分配的内存作为buffer。
所以,我们先用内存池的方法创建buffer。背后的想法是,与其将整个buffer的内容通过socket发给服务器,我们设置好一个在客户程序和服务器之间共享的内存池,然后只需要将buffer所在的地址发过去就可以了。避免大的数据开销。
具体要做的如下:
第一件事就是将一些内存空间映射到客户程序的地址空间中。原作者使用memfd_create
系统调用,代码如下,我之后会展示Linux上的shm调用。
Copy #include <syscall.h>
#include <unistd.h>
#include <sys/mman.h>
int size = 200 * 200 * 4; // bytes, explained below
// open an anonymous file and write some zero bytes to it
int fd = syscall(SYS_memfd_create, "buffer", 0);
ftruncate(fd, size);
// map it to the memory
unsigned char *data = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
下一步我们希望知会窗口管理器我们创建好的地址空间的地址,并让它也将这段地址映射到它的内存中。我们使用wl_shm.create_pool
请求来完成这件事情:
Copy struct wl_shm *shm = ...;
struct wl_shm_pool *pool = wl_shm_create_pool(shm, fd, size);
那个文件描述符以及内存大小通过socket发送给管理器。管理器通过mmap
调用来访问那段地址。
被分享的内存池本身还不是一个buffer。我们还需要使用wl_shm_pool.create_buffer
请求来从池中创建一个buffer。本质上就是你创建一大块的空间,然后给它分成一段一段的buffer。这么做是因为如果buffer的大小需要改变的话,可以直接通过在那个池中重新分配就行了。可能就是一个结构的改变,不会涉及到真真的内存变动。
Copy int width = 200;
int height = 200;
int stride = width * 4;
int size = stride * height; // bytes
struct wl_shm_pool *pool = ...;
struct wl_buffer *buffer = wl_shm_pool_create_buffer(pool,
0, width, height, stride, WL_SHM_FORMAT_XRGB8888);
在我们这个简单的例子中,我们将创建一个覆盖整个池的buffer。我们将offset设为0,并且将buffer的大小设置成池的大小。在真实的程序中,你可能需要使用到一些高级的内存分配策略来根据需要创建多个buffer,从而不需要重复的map了unmap文件改变内存的大小。
这里是完整的分配buffer的代码:
Copy #include <syscall.h>
#include <unistd.h>
#include <sys/mman.h>
int width = 200;
int height = 200;
int stride = width * 4;
int size = stride * height; // bytes
// open an anonymous file and write some zero bytes to it
int fd = syscall(SYS_memfd_create, "buffer", 0);
ftruncate(fd, size);
// map it to the memory
unsigned char *data = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
// turn it into a shared memory pool
struct wl_shm *shm = ...;
struct wl_shm_pool *pool = wl_shm_create_pool(shm, fd, size);
// allocate the buffer in that pool
struct wl_buffer *buffer = wl_shm_pool_create_buffer(pool,
0, width, height, stride, WL_SHM_FORMAT_XRGB8888);
如果我们要在现在渲染什么东西,我们就可以把东西放到已经分配好的内存中去了。因为我们直接是将整个池都映射为一个buffer,并且我们选择的是XRGB8888格式,全0就已经是一个黑色的状态了。我们不需要做任何额外的操作就已经是一个黑色的东东了。
Shell Surface
只用shell surface不会给你一个能跑起来的程序,也就是说你不会得到一个黑框框。你需要到下一章的xdg-shell一节中才能有一个跑起来的黑框框。所以这里就先熟悉一下整个的创建流程,之后在使用xdg-shell的时候会到这里来做参考。
我们现在已经给surface分配了buffer,为了让它显示在屏幕上,我们还需要给它分配一个role 。Wayland里面没有一个通用的方法给surface分配role,每一个role都有它自己的分配方法。例如,我们需要通过wl_pointer.set_cursor
请求来给surface分配一个cursor角色。
其它角色的分配就显得复杂多了,因为这些role在被分配前有额外的功能需要实现。我们现在即将使用的角色shell surface ,我们可以通过它来将surface显示在一个桌面风格的窗口。
对于shell surface ,可以将它想象成surface的子类,拥有额外的功能。子类是通过一个巧妙的方法实现的:你通过调用一个新的 wl_shell_surface
对象的额外方法。 通过wl_shell.get_shell_surface
请求创建那个对象并同时给了那个surface一个新的shell surface role。
我们也可以将surface(有shell surface的role)和shell surface想象成两个完全不同的对象,但是互相关联的实体。在这个例子中,shell surface代表一个窗口而surface负责窗口的内容。
wl_shell_surface
有很多的功能,比如处理窗口的大小改变,但是这里我们不会去深究这个。那是因为wl_shell
和 wl_shell_surface
都被废弃了并且被xdg_sell
扩展协议取代。现在使用wl_shell_surface
的理由只是我们暂时只想使用Wayland核心协议。
把下面的代码放在给surface分配buffer的代码之前。
Copy struct wl_shell_surface *shell_surface = wl_shell_get_shell_surface(shell, surface);
wl_shell_surface_set_toplevel(shell_surface);
完整的代码
下面的就是完整的显示一个黑框的代码:
Copy #include <stdio.h>
#include <string.h>
#include <syscall.h>
#include <unistd.h>
#include <sys/mman.h>
#include <wayland-client.h>
struct wl_compositor *compositor;
struct wl_shm *shm;
struct wl_shell *shell;
void registry_global_handler
(
void *data,
struct wl_registry *registry,
uint32_t name,
const char *interface,
uint32_t version
) {
if (strcmp(interface, "wl_compositor") == 0) {
compositor = wl_registry_bind(registry, name,
&wl_compositor_interface, 3);
} else if (strcmp(interface, "wl_shm") == 0) {
shm = wl_registry_bind(registry, name,
&wl_shm_interface, 1);
} else if (strcmp(interface, "wl_shell") == 0) {
shell = wl_registry_bind(registry, name,
&wl_shell_interface, 1);
}
}
void registry_global_remove_handler
(
void *data,
struct wl_registry *registry,
uint32_t name
) {}
const struct wl_registry_listener registry_listener = {
.global = registry_global_handler,
.global_remove = registry_global_remove_handler
};
int main(void)
{
struct wl_display *display = wl_display_connect(NULL);
struct wl_registry *registry = wl_display_get_registry(display);
wl_registry_add_listener(registry, ®istry_listener, NULL);
// wait for the "initial" set of globals to appear
wl_display_roundtrip(display);
struct wl_surface *surface = wl_compositor_create_surface(compositor);
struct wl_shell_surface *shell_surface = wl_shell_get_shell_surface(shell, surface);
wl_shell_surface_set_toplevel(shell_surface);
int width = 200;
int height = 200;
int stride = width * 4;
int size = stride * height; // bytes
// open an anonymous file and write some zero bytes to it
int fd = syscall(SYS_memfd_create, "buffer", 0);
ftruncate(fd, size);
// map it to the memory
unsigned char *data = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
// turn it into a shared memory pool
struct wl_shm_pool *pool = wl_shm_create_pool(shm, fd, size);
// allocate the buffer in that pool
struct wl_buffer *buffer = wl_shm_pool_create_buffer(pool,
0, width, height, stride, WL_SHM_FORMAT_XRGB8888);
wl_surface_attach(surface, buffer, 0, 0);
wl_surface_commit(surface);
while (1) {
wl_display_dispatch(display);
}
}
继续编译运行:
Copy $ gcc main.c -l wayland-client -o runme
$ ./runme
你会看到一个黑色的框框显示在屏幕上:
由于它没有window frame,所以现在不好拖动它。不过你的Wayland窗口管理器可能提供额外的可以移动窗口的方法。除此之外,它和一般的窗口没什么不同。比如如果你正在使用gnome,那么它会在你做窗口切换的时候出现,而且也应该是一个黑方块。
我们现在还没有实现关闭窗口的功能也没有实现回应ping事件的代码,所以你的窗口管理器可能会认为你的窗口处在“不响应”的状态中,同时提供一个方法让你“强制退出”。当然,你可以直接在shell里面敲下Ctrl-C来终止这个程序。
PostScript:不需要那么多的往返开销
我们的代码好像有很多的往返开销在组合器和我们的进程之间。我们获取全局对象,创建了一个内存池,在里面分配了一个buffer,创建了不同的对象,比如Isurface
和shell_surface
,并且调用了它们的方法。如果每一个动作都要求一个往返开销来完成,那在我们看到那个黑框框之前,我们总共需要花费掉12个往返的开销(而且那还是管理器只告知我们需要的对象的情况,没把额外的暂时对我们没用的对象算进去)。想象一下这会多么的慢。如果我们使用的是X.Org的话,你直接都能感受到了。Wayland可以做的更好。
让我们看看我们的程序中到底发生了什么:
Wayland-client库有一个内置的可供使用的历史记录功能。只需要将WAYLAND_DEBUG
环境变量设置成 1
:
Copy $ WAYLAND_DEBUG=1 ./runme
[1610518.311] -> wl_display@1.get_registry(new id wl_registry@2)
[1610518.358] -> wl_display@1.sync(new id wl_callback@3)
[1610518.488] wl_display@1.delete_id(3)
[1610518.502] wl_registry@2.global(1, "wl_drm", 2)
[1610518.511] wl_registry@2.global(2, "wl_compositor", 3)
[1610518.521] -> wl_registry@2.bind(2, "wl_compositor", 3, new id [unknown]@4)
[1610518.536] wl_registry@2.global(3, "wl_shm", 1)
[1610518.545] -> wl_registry@2.bind(3, "wl_shm", 1, new id [unknown]@5)
[1610518.555] wl_registry@2.global(4, "wl_output", 2)
[1610518.563] wl_registry@2.global(5, "wl_output", 2)
[1610518.570] wl_registry@2.global(6, "wl_data_device_manager", 3)
[1610518.586] wl_registry@2.global(7, "gtk_primary_selection_device_manager", 1)
[1610518.604] wl_registry@2.global(8, "zxdg_shell_v6", 1)
[1610518.612] wl_registry@2.global(9, "wl_shell", 1)
[1610518.631] -> wl_registry@2.bind(9, "wl_shell", 1, new id [unknown]@6)
[1610518.652] wl_registry@2.global(10, "gtk_shell1", 1)
[1610518.658] wl_registry@2.global(11, "wl_subcompositor", 1)
[1610518.676] wl_registry@2.global(12, "zwp_pointer_gestures_v1", 1)
[1610518.694] wl_registry@2.global(13, "zwp_tablet_manager_v2", 1)
[1610518.718] wl_registry@2.global(14, "wl_seat", 5)
[1610518.728] wl_registry@2.global(15, "zwp_relative_pointer_manager_v1", 1)
[1610518.737] wl_registry@2.global(16, "zwp_pointer_constraints_v1", 1)
[1610518.745] wl_registry@2.global(17, "zxdg_exporter_v1", 1)
[1610518.755] wl_registry@2.global(18, "zxdg_importer_v1", 1)
[1610518.763] wl_callback@3.done(16657)
[1610518.769] -> wl_compositor@4.create_surface(new id wl_surface@3)
[1610518.775] -> wl_shell@6.get_shell_surface(new id wl_shell_surface@7, wl_surface@3)
[1610518.784] -> wl_shell_surface@7.set_toplevel()
[1610518.805] -> wl_shm@5.create_pool(new id wl_shm_pool@8, fd 5, 160000)
[1610518.818] -> wl_shm_pool@8.create_buffer(new id wl_buffer@9, 0, 200, 200, 800, 1)
[1610518.835] -> wl_surface@3.attach(wl_buffer@9, 0, 0)
[1610518.844] -> wl_surface@3.commit()
^C
这里的请求前面有个 ->
箭头记号。这些就是实际的Wayland请求和事件,所以你不会在这里见到wayland-client的额外的东西比如wl_display_connect
以及 wl_display_roundtrip
。(后面一个实际上封装了wl_display.sync
和 wl_callback.done
)
回忆一下,无论是请求还是事件都需要一个往返开销。例如,所有从wl_compositor.create_surface
开始的请求都在底下快速的完成了一个会话,并且没有等待管理器的回应。
注意一下,当通过wayland-client的记录计算往返开销的时候,wayland-client记录调用会在它们创建的的时候立即调用,而不是当它们发送出去的时候。因为Wayland异步的机制允许暂存许多方法调用然后在我们完成所有的调用后等待回应的时候一次性的将请求一起发出去。
想要换种方法知道到底发生了什么,我们使用strace来跟踪系统的socket调用:
Copy $ strace -e trace=network ./runme
socket(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC, 0) = 3
connect(3, {sa_family=AF_UNIX, sun_path="/run/user/1000/wayland-0"}, 27) = 0
sendmsg(3, {msg_name=NULL, msg_namelen=0, msg_iov=[{iov_base="\1\0\0\0\1\0\f\0\2\0\0\0\1\0\0\0\0\0\f\0\3\0\0\0", iov_len=24}], msg_iovlen=1, msg_controllen=0, msg_flags=0}, MSG_DONTWAIT|MSG_NOSIGNAL) = 24
recvmsg(3, {msg_name=NULL, msg_namelen=0, msg_iov=[{iov_base="\2\0\0\0\0\0\34\0\1\0\0\0\7\0\0\0wl_drm\0\0\2\0\0\0\2\0\0\0"..., iov_len=4096}], msg_iovlen=1, msg_controllen=0, msg_flags=MSG_CMSG_CLOEXEC}, MSG_DONTWAIT|MSG_CMSG_CLOEXEC) = 720
sendmsg(3, {msg_name=NULL, msg_namelen=0, msg_iov=[{iov_base="\2\0\0\0\0\0(\0\2\0\0\0\16\0\0\0wl_compositor\0\0\0"..., iov_len=220}], msg_iovlen=1, msg_control=[{cmsg_len=20, cmsg_level=SOL_SOCKET, cmsg_type=SCM_RIGHTS, cmsg_data=[5]}], msg_controllen=20, msg_flags=0}, MSG_DONTWAIT|MSG_NOSIGNAL) = 220
recvmsg(3, {msg_name=NULL, msg_namelen=0, msg_iov=[{iov_base="\5\0\0\0\0\0\f\0\0\0\0\0\5\0\0\0\0\0\f\0\1\0\0\0\t\0\0\0\0\0\10\0", iov_len=3376}, {iov_base="", iov_len=720}], msg_iovlen=2, msg_controllen=0, msg_flags=MSG_CMSG_CLOEXEC}, MSG_DONTWAIT|MSG_CMSG_CLOEXEC) = 32
recvmsg(3, {msg_name=NULL, msg_namelen=0, msg_iov=[{iov_base="\7\0\0\0\1\0\24\0\0\0\0\0\310\0\0\0\310\0\0\0", iov_len=3344}, {iov_base="", iov_len=752}], msg_iovlen=2, msg_controllen=0, msg_flags=MSG_CMSG_CLOEXEC}, MSG_DONTWAIT|MSG_CMSG_CLOEXEC) = 20
^C
我们可以看到我们的程序创建了一个新的Unix domain socket,连接到 /run/user/1000/wayland-0
,然后发送初始消息(该消息编码了 wl_display.get_registry
和 wl_display.sync
请求)。然后它等待回应(因为我们用了wl_display_roundtrip()
调用)。管理器用wl_registry.global
, wl_display.delete_id
和 wl_callback.done
事件回应,全部都在一个消息中。这是我们第一个往返开销。
之后我们的程序发送一个编码了所有剩余请求的消息,三次wl_registry.bind
调用。
上面的就是所有的内容了。足够让那个黑盒子显示在屏幕上。创建完窗口之后,还有两个从管理器发过来的消息没有显示出来,那是因为我们没有设置相应的监听器。
所以实际上要显示一个Wayland窗口只需要一个半的往返开销 。