2020-11-25 wayland 进程间函数调用

桌面环境做了好几年,对 X 和 wayland 比较感兴趣,但是一直没有机会深入去看。正好我要离职了,在现在这个单位,北京上海加起来有将近十年了,最年轻的十年,也收获了不少。最近工作交接完没啥事,就抓着 wayland 代码看了看,整体概念大概了解一点,先总结一下吧,免得又忘了。这篇文章主要目的是梳理 server 端和 client 端的函数调用是怎么实现的?并且拿 display 和 registry 为例做简单地说明。

wayland 核心协议定义了许多全局对象,包括:display、registry、compositor、output、seat、surface、buffer 等等。客户端连接到服务端后,会创建一个客户端的可用对象,一般叫 proxy,服务端对应会创建一个 resource,这俩是对应的,实现 client 端对 server 端的资源绑定。

client 和 server 一方面各自在消息循环上等待数据(socket 上的数据),拿到数据经过反序列化后生成本地的函数掉用;另一方面,将本地的调用请求封装序列化后,通过 socket 发出去。

wayland 协议

核心协议通过 xml 文件来定义,wayland-scanner 程序扫描并生成一个 wayland-protocol.c 和两个头文件,分别给服务端程序和客户端程序使用。
以 display 和 registry 为例,在生成的 wayland-protocol.c 中,display 的签名:

static const struct wl_message wl_display_requests[] = {
    { "sync", "n", wayland_types + 8 }, // wl_callback_interface
    { "get_registry", "n", wayland_types + 9 }, //wl_registry_interface
};

static const struct wl_message wl_display_events[] = {
    { "error", "ous", wayland_types + 0 },
    { "delete_id", "u", wayland_types + 0 },
};

WL_EXPORT const struct wl_interface wl_display_interface = {
    "wl_display", 1,
    2, wl_display_requests,
    2, wl_display_events,
};

wl_message 的结构:

 struct wl_message {
    /** Message name */
    const char *name;
    /** Message signature */
    const char *signature;
    /** Object argument interfaces */
    const struct wl_interface **types;
};

{ "get_registry", "n", wayland_types + 9 } 的这个 n 表示 new_id,也就是说客户端在发出 get_registry 请求的时候会传递一个 wl_interface * 的参数,对应为这个参数创建一个新的对象。比如 wl_display_get_registry 返回 wl_registry 对象,这个对象就是 registry 在客户端的代理对象,这样创建出来的。registry 对象的相关签名如下:

static const struct wl_message wl_registry_requests[] = {
    { "bind", "usun", wayland_types + 0 },
};

static const struct wl_message wl_registry_events[] = {
    { "global", "usu", wayland_types + 0 },
    { "global_remove", "u", wayland_types + 0 },
};

WL_EXPORT const struct wl_interface wl_registry_interface = {
    "wl_registry", 1,
    1, wl_registry_requests,
    2, wl_registry_events,
};

签名类型简单罗列如下:

 * * `i`: int
 * * `u`: uint
 * * `f`: fixed
 * * `s`: string
 * * `o`: object
 * * `n`: new_id
 * * `a`: array
 * * `h`: fd
 * * `?`: 问号表示后面接的参数可空

wl_message 有个成员 types,在 demarshal 消息的过程中,如果传过来的参数中有 object 或者 new_id,那么我们设置的 implementation 必须要知道对象的类型是什么,这个 types 就是用来做这个的。

wayland_types 是个指针数组,数组里面的成员是 wl_interface *,比如 global 对应 wayland_types + 0 指向 NULL,get_registry 对应的 wayland_types + 9 指向 wl_registry_interface。
对于 display,客户端先创建一个 proxy,将 request 发给服务端,服务端收到消息后创建对应的 display_resource,这个 display_resource 与客户端的 display_proxy 是对应的,客户端给服务端发送 request 消息,服务端给客户端发送 event 消息。display 的 request 有两个: sync 和 get_registry,在服务端实现,event 有两个: error 和 delete_id,在客户端实现。

服务端

1、wayland server 启动时会创建一个 socket(_wl_display_add_socket),并将这个 socket fd 加入 epoll 中,这样如果有客户端程序连接,epoll 就会通知服务端,从而执行回调函数 socket_data。

2、当有客户端程序连接过来,在 socket_data 函数中调用 accept 创建新的 socket fd,紧接着创建 wl_client,并把它加入合成器的 client list;在创建 wl_client 的时候,wl_client_create (display, fd) 将这个 socket fd 加入 epoll,这样从客户端发过来的请求,也可以通过 epoll 通知服务端,回调函数是 wl_client_connect_data。

3、等创建了 wl_client,接着执行函数 bind_display,创建 wl_display 在 server 端的 resource 对象 client->display_resource,设置它的 implementation,也就是 wl_display_requests 的俩函数:

static const struct wl_display_interface display_interface = {
    display_sync,
    display_get_registry
};

客户端

1、客户端程序在运行时先调用 wl_display_connect 连接到服务端,这个函数返回的 wl_display 就是 display 对象在 client 端的代理[上面第 3 步中说了 server 端是 display_resource,这俩配合工作,实现互相调用函数]。
在 wl_display_connect 中为代理对象 set_implementation,也就是 wl_display_events

static const struct wl_display_listener display_listener = {
    display_handle_error,
    display_handle_delete_id
};

这时第一个资源对象 display 就在 server 和client 端都设置好了。

2、然后调用 wl_display_get_registry 来获取 server 端所有可用的 global 对象,并执行 wl_registry_add_listener 为新的 registry 对象注册事件:

static const struct wl_registry_listener registry_listener = {
    registry_handle_global,
    registry_handle_global_remove
};

函数 wl_display_get_registry,它会调用下面这个函数
wl_proxy_marshal_constructor((struct wl_proxy *) wl_display, WL_DISPLAY_GET_REGISTRY, &wl_registry_interface, NULL);
上面的 &wl_registry_interface 就是 get_registry 的签名 "n"。一方面它会返回 registry 在 客户端的 proxy,也就是 display->registry,另一方面把请求发出去。

简单分析这两个函数,首先介绍
wl_registry_add_listener(display->registry, &registry_listener, display);

第一个参数是 registry 的客户端 proxy,第二个参数是 implementation,这个函数的作用就是设置 proxy event 的 implementation

    proxy->object.implementation = implementation;
    proxy->user_data = data;

接着再分析 wl_proxy_marshal_constructor,在 wayland-client.c 中层层调用到了 wl_proxy_marshal_array_constructor_versioned:

    message = &proxy->object.interface->methods[opcode];
    if (interface) {
        new_proxy = create_outgoing_proxy(proxy, message,
                          args, interface,
                          version);
        if (new_proxy == NULL)
            goto err_unlock;
    }

解析 message 这里需要先看 wl_interface 的结构定义,如下:

struct wl_interface {
    /** Interface name */
    const char *name;
    /** Interface version */
    int version;
    /** Number of methods (requests) */
    int method_count;
    /** Method (request) signatures */
    const struct wl_message *methods;
    /** Number of events */
    int event_count;
    /** Event signatures */
    const struct wl_message *events;
};

其中 methods 是 request 的签名,这里的 object.interface 应该是 &wl_display_interface,在函数 wl_display_connect 设置它的值:
display->proxy.object.interface = &wl_display_interface;
这个接口的 methods 有两个,method_count 就是2; opcode 是 WL_DISPLAY_GET_REGISTRY,拿到的 message 也就是 “{ "get_registry", "n", wayland_types + 9 }”。
传递过来的 interface 为 wl_registry_interface,在 create_outgoing_proxy 得到 “n”,就会创建新的 proxy 对象,设置这个对象的 interface 为 wl_registry_interface,并把这个新的 proxy 对象加入映射表中,得到 object.id 填充到 args 中 。然后,再把 message,opcode,args 等组好 wl_closure ,序列化完成后,通过 socket 发给服务端。

再回到服务端,收到客户端消息后,进入回调函数 wl_client_connection_data,反序列化消息,得到 wl_closure,找到目标对象对应的接口函数,利用 libffi 执行 server 端的函数: display_get_registry,这个函数会在 server 端创建对应的 wl_resource 也就是 registry_resource,将它 set_implementation,也就是 registry_bind。然后再遍历可用的 global 对象发信号 global。

    wl_list_for_each(global, &display->global_list, link)
        if (wl_global_is_visible(client, global) && !global->removed)
            wl_resource_post_event(registry_resource,
                           WL_REGISTRY_GLOBAL,
                           global->name,
                           global->interface->name,
                           global->version);

这样,第二个对象 registry 在服务端和客户端的 request 和 event 也都设置好了。

进程间的函数调用

当服务端发出 global 信号时,客户端程序就可以 wl_registry_bind 这些对象,生成本地的可用对象 wl_proxy。[参考 weston 的客户程序代码 window.c]
但是信号机制一般是对同一个进程来说的,我们可以监听某个对象的某个信号,当收到信号时执行对应的回调函数;而这里其实是两个程序,服务端和客户端,这种跨进程的信号不是简单地 connect 就可以的,而是需要通过 socket 来传递。
要让两个进程通过 socket 进行函数调用,首先需要将调用抽象成数据流的形式,这些信息通过 wl_closure_marshal 写入 wl_closure 结构,再由 serialize_closure 变成数据流;等到了目标进程,从数据流中通过 wl_connection_demarshal 转回 wl_closure 结构。
如果参数中不止整型字符串等简单类型,还存在对象的话,就需要 wl_map 来做这个对象在 server 和 client 端的映射了。
上面所谓的发出 global 信号,其实也就是把函数调用的请求序列化,然后再发给客户端程序,客户端收到消息,反序列化,解析出object id, 从 wl_map 中找到本地可用对象,拿到 opcode,再解析本地对象的 implementation 对应 opcode 的函数,也就是执行 registry_handle_global。

    if (strcmp(interface, "wl_compositor") == 0) {
        d->compositor = wl_registry_bind(registry, id,
                         &wl_compositor_interface, 3);
    } else if (strcmp(interface, "wl_output") == 0) {
        display_add_output(d, id);
    } else if (strcmp(interface, "wl_seat") == 0) {
        display_add_input(d, id, version);
    }

同样的,这个 wl_registry_bind 也是把客户端的请求消息序列化后,发给服务端;服务端收到后反序列化,执行对应的 bind,也就是 wayland-server.c 中定义的 registry_bind

整个流程就是 client 端通过 wl_proxy 调用 server 端的 wl_resource 的 request,server 端通过 wl_resource 调用 client 端的 wl_proxy 的 event。

再简单说明一下客户端收到消息的处理流程,wl_display_dispatch -> wl_display_read_events -> read_events -> queue_event,读取数据,将解析出来的 event 加入 event_list 中。
在 dispatch_queue 中 dispatch_event,也就是执行 wl_closure_invoke。具体的下次再说吧……

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

推荐阅读更多精彩内容