treeland/qwlroots/doc/ai/qwlroots-wrapping-patterns-...

19 KiB
Raw Permalink Blame History

qwlroots 封装模式最佳实践指南

概述

qwlroots 是对 wlroots C 接口的 Qt 风格封装库,通过宏系统提供了多种封装模式来适应不同类型的 wlroots 结构体。本文档总结了项目中使用的几种主要封装模式,为添加新的类封装提供指导。

核心设计理念

  1. Qt 风格接口:遵循 Qt 规范的接口风格
  2. 信号槽机制:将 wl_signal 封装为 Qt 信号
  3. 内存管理:自动处理对象生命周期和所有权
  4. 类型安全:利用 C++ 模板和概念确保类型安全
  5. 零开销抽象:在 Release 模式下提供零开销的封装

封装模式分类

1. QW_CLASS_OBJECT 模式 - 基于 QObject 的完整封装

适用场景

  • wlroots 结构体包含信号events 字段)
  • 需要完整的对象生命周期管理
  • 需要 Qt 信号槽机制

特点

  • 继承自 qw_object<Handle, Derive>,间接继承 QObject
  • 自动连接 wlroots 的 destroy 信号
  • 支持信号绑定和转发
  • 提供完整的内存管理和所有权控制

使用示例

// 头文件声明
class QW_CLASS_OBJECT(compositor)
{
    QW_OBJECT           // 必需的宏,提供基础功能
    Q_OBJECT           // Qt 元对象系统

    // 信号绑定 - 将 wlroots 信号转换为 Qt 信号
    QW_SIGNAL(new_surface, wlr_surface*)

public:
    // 静态函数绑定 - create 函数自动返回 qw_compositor*
    QW_FUNC_STATIC(compositor, create, qw_compositor *, wl_display *display, uint32_t version, wlr_renderer *renderer)
};

class QW_CLASS_OBJECT(surface)
{
    QW_OBJECT
    Q_OBJECT

    // 多个信号绑定
    QW_SIGNAL(client_commit)
    QW_SIGNAL(commit)
    QW_SIGNAL(new_subsurface, wlr_subsurface*)
    QW_SIGNAL(map)
    QW_SIGNAL(unmap)

public:
    // 静态函数 - from/get 函数
    QW_FUNC_STATIC(surface, from_resource, qw_surface *, wl_resource *resource)

    // 成员函数绑定
    QW_FUNC_MEMBER(surface, for_each_surface, void, wlr_surface_iterator_func_t iterator, void *user_data)
    QW_FUNC_MEMBER(surface, has_buffer, bool)
    QW_FUNC_MEMBER(surface, send_frame_done, void, const timespec *when)
    QW_FUNC_MEMBER(surface, map, void)
    QW_FUNC_MEMBER(surface, unmap, void)
};

关键宏说明

  • QW_OBJECT:提供基础的对象功能,必须在 protected 区域
  • QW_SIGNAL(name, ...):绑定 wlroots 信号到 Qt 信号
  • QW_FUNC_STATIC绑定静态函数create/from/get 函数自动处理返回类型
  • QW_FUNC_MEMBER:绑定成员函数

2. QW_CLASS_REINTERPRET_CAST 模式 - 轻量级类型转换

适用场景

  • wlroots 结构体不包含信号
  • 只需要函数封装,不需要对象生命周期管理
  • 追求最小内存开销

特点

  • 基于 reinterpret_cast 的零开销封装
  • 不分配额外内存
  • 不支持信号机制
  • 主要用于工具类和简单结构体

使用示例

class QW_CLASS_REINTERPRET_CAST(client_buffer)
{
public:
    QW_FUNC_STATIC(client_buffer, get, qw_client_buffer *, wlr_buffer *buffer)
};

3. QW_CLASS_BOX 模式 - 值类型封装

适用场景

  • 简单的数据结构
  • 需要与 Qt 类型互操作
  • 值语义的对象

特点

  • 包含实际的数据成员
  • 支持与 Qt 类型的转换
  • 值语义,支持拷贝和赋值

使用示例

class qw_box
{
public:
    explicit qw_box() = default;
    explicit qw_box(const wlr_box &box);
    explicit qw_box(const QRect &box);

    QRect toQRect();
    operator wlr_box*();

private:
    wlr_box m_handle;
};

4. QW_CLASS_INTERFACE 模式 - 接口实现封装

适用场景

  • 需要实现 wlroots 接口
  • 提供回调函数实现
  • 自定义行为扩展

特点

  • 基于模板的接口绑定
  • 自动处理函数指针绑定
  • 支持虚函数重写

使用示例

class QW_CLASS_INTERFACE(output)
{
    QW_INTERFACE_INIT(output)

    // 接口函数实现
    QW_INTERFACE(commit, bool, wlr_output_state *state)
    QW_INTERFACE(set_cursor, bool, wlr_buffer *buffer, int hotspot_x, int hotspot_y)

protected:
    // 实现具体的接口函数
    bool commit(wlr_output_state *state);
    bool set_cursor(wlr_buffer *buffer, int hotspot_x, int hotspot_y);
};

目录结构和文件命名规律

qwlroots 的目录结构和文件命名完全遵循 wlroots 的组织方式,这确保了项目结构的一致性和可维护性。

目录结构对应关系

qwlroots 的 src/ 目录结构与 wlroots 的 include/wlr/ 目录结构完全对应:

wlroots/include/wlr/          qwlroots/src/
├── types/                 ←→ ├── types/
├── util/                  ←→ ├── util/
├── render/                ←→ ├── render/
├── interfaces/            ←→ ├── interfaces/
├── backend/               ←→ ├── (backend相关文件直接在src/)
└── xwayland/              ←→ └── (xwayland相关文件在types/)

文件命名规律

  1. 头文件命名

    • wlroots: wlr_compositor.h → qwlroots: qwcompositor.h
    • wlroots: wlr_output.h → qwlroots: qwoutput.h
    • wlroots: wlr_buffer.h → qwlroots: qwbuffer.h
  2. 类名命名

    • wlroots: struct wlr_compositor → qwlroots: class qw_compositor
    • wlroots: struct wlr_surface → qwlroots: class qw_surface
  3. 目录分类规律

    • types/: 主要的 Wayland 协议类型和核心对象
    • util/: 工具类和辅助函数
    • render/: 渲染相关的类型
    • interfaces/: 接口实现相关的类型

查找对应关系的方法

当需要为新的 wlroots 类型创建封装时:

  1. 定位源文件:在 wlroots/include/wlr/ 中找到对应的头文件
  2. 确定目录:根据 wlroots 的目录结构确定在 qwlroots 中的位置
  3. 命名转换:按照 wlr_xxx.hqwxxx.h 的规律命名

示例

# wlroots 中的文件
wlroots/include/wlr/types/wlr_xdg_shell.h

# 对应的 qwlroots 文件应该是
qwlroots/src/types/qwxdgshell.h

新增封装的完整操作流程

当用户请求"新增xxx类"或"新增xxxx函数"时,应按照以下流程操作:

步骤 0更新 wlroots 子模块

在开始任何封装工作之前,首先确保 wlroots 子模块是最新的:

# 进入 wlroots 子模块目录
cd wlroots

# 获取最新的标签和提交
git fetch --tags

# 查看最新的版本标签
git tag --sort=-version:refname | head -5

# 切换到最新的稳定版本(例如 0.19.0
git checkout 0.19.0

# 返回项目根目录
cd ..

# 提交子模块更新
git add wlroots
git commit -m "submodule: update wlroots to latest version"

步骤 1在 wlroots 子模块中查找目标类型

使用以下方法在 wlroots 子模块中定位要封装的类型:

# 查找特定的头文件
find wlroots/include/wlr -name "*目标名称*" -type f

# 搜索结构体定义
grep -r "struct wlr_目标名称" wlroots/include/wlr/

# 查看具体文件内容
cat wlroots/include/wlr/types/wlr_目标名称.h

步骤 2确定版本信息

查找该类型是在哪个版本中引入的:

# 在 wlroots 目录中查看文件的提交历史
cd wlroots
git log --oneline --follow include/wlr/types/wlr_目标名称.h

# 查看特定提交的详细信息
git show 提交哈希

# 查找包含该提交的最早标签
git tag --contains 提交哈希 --sort=version:refname | head -1

这个信息将用于在 qwlroots 中添加版本兼容性宏。

步骤 3分析 wlroots 结构体

  1. 检查是否有 events 字段

    • 有 events 字段 → 使用 QW_CLASS_OBJECT
    • 无 events 字段 → 使用 QW_CLASS_REINTERPRET_CAST
  2. 检查是否需要接口实现

    • 需要实现回调 → 使用 QW_CLASS_INTERFACE
  3. 检查是否为简单数据结构

    • 简单数据结构 → 考虑自定义封装或 QW_CLASS_BOX

步骤 2确定文件位置和命名

根据 wlroots 源文件的位置确定对应的 qwlroots 文件位置:

# 1. 找到 wlroots 中的源文件
find /path/to/wlroots/include/wlr -name "wlr_example.h"
# 例如wlroots/include/wlr/types/wlr_example.h

# 2. 确定 qwlroots 中的对应位置
# src/types/qwexample.h

步骤 3创建头文件

// 文件src/types/qwexample.h
#pragma once

#include <qwobject.h>

extern "C" {
#include <wlr/types/wlr_example.h>
}

QW_BEGIN_NAMESPACE

class QW_CLASS_OBJECT(example)
{
    QW_OBJECT
    Q_OBJECT

    // 绑定所有相关信号
    QW_SIGNAL(signal_name, param_type)

public:
    // 绑定静态函数
    QW_FUNC_STATIC(example, create, qw_example *, param_type param)
    QW_FUNC_STATIC(example, from_resource, qw_example *, wl_resource *resource)

    // 绑定成员函数
    QW_FUNC_MEMBER(example, method_name, return_type, param_type param)

    // 如果有 destroy 方法,析构函数应该是 protected
protected:
    QW_FUNC_MEMBER(example, destroy, void)
};

QW_END_NAMESPACE

步骤 4处理版本兼容性

// 处理不同 wlroots 版本的 API 差异
#if WLR_VERSION_MINOR < 18
    QW_SIGNAL(old_signal, param_type)
#else
    QW_SIGNAL(new_signal, param_type)
#endif

#if WLR_VERSION_MINOR < 19
    QW_FUNC_MEMBER(example, old_method, void, param_type)
#else
    QW_FUNC_MEMBER(example, new_method, void, param_type)
#endif

步骤 5添加到构建系统

src/CMakeLists.txt 中添加新的头文件:

set(HEADERS
    # ... 其他头文件
    types/qwexample.h
)

最佳实践和注意事项

1. 命名规范

  • 类名:qw_ + wlroots 类型名(去掉 wlr_ 前缀)
  • 信号名:notify_ + 原始信号名
  • 函数名:保持与 wlroots 函数名一致(去掉类型前缀)

2. 信号处理

// 正确的信号绑定
QW_SIGNAL(destroy)           // 自动处理,无需手动绑定
QW_SIGNAL(custom_event, param_type)  // 自定义信号

// 信号连接示例
connect(surface, &qw_surface::notify_commit, this, [](){
    // 处理提交事件
});

3. 内存管理

// 创建对象(拥有所有权)
auto *compositor = qw_compositor::create(display, version, renderer);

// 从现有句柄获取对象(不拥有所有权)
auto *surface = qw_surface::from(wlr_surface);

// 检查对象是否存在
auto *existing = qw_surface::get(wlr_surface);  // 可能返回 nullptr

4. 函数绑定原则

4.1 确定函数所有权和放置位置

在封装 wlroots 函数时,确定函数应该是静态函数还是成员函数,以及它属于哪个类是至关重要的。遵循以下系统性方法:

函数命名分析模式: wlroots 函数遵循模式:wlr_<类型>_<函数名>

步骤 1解析函数名

  • wlr_ 后按下划线分割函数名
  • 识别类型部分和函数名部分
  • 类型部分应该对应一个现有的 wlroots 类型

示例分析:

// 函数wlr_surface_get_image_description_v1_data
// 解析wlr_ + surface + _ + get_image_description_v1_data
// 类型wlr_surface
// 函数名get_image_description_v1_data

// 为什么不是 wlr_surface_get
// 因为 wlr_surface_get 不是一个有效的 wlroots 类型
// get_image_description_v1_data 是一个更合理的函数名

步骤 2验证类型存在性

  • 检查 wlr_<类型> 是否作为结构体存在于 wlroots 头文件中
  • 如果类型存在,这确认了解析是正确的

步骤 3确定静态函数 vs 成员函数

  • 成员函数:第一个参数类型与解析出的类型匹配

    // wlr_surface_get_image_description_v1_data(struct wlr_surface *surface)
    // 第一个参数是 wlr_surface* → qw_surface 的成员函数
    QW_FUNC_MEMBER(surface, get_image_description_v1_data, const wlr_image_description_v1_data *)
    
  • 静态函数:第一个参数类型与解析出的类型不匹配

    // wlr_compositor_create(wl_display *display, ...)
    // 第一个参数是 wl_display*,不是 wlr_compositor* → 静态函数
    QW_FUNC_STATIC(compositor, create, qw_compositor *, wl_display *display, ...)
    

步骤 4处理跨文件函数 函数可能定义在与其目标类型不同的头文件中。这在 wlroots 中很常见:

// wlr_surface_get_image_description_v1_data 定义在 wlr_color_management_v1.h 中
// 但操作 wlr_surface所以它属于 qw_surface 类

// 在 qwcompositor.h 中:
#if WLR_VERSION_MINOR >= 19
#include <wlr/types/wlr_color_management_v1.h>
#endif

class QW_CLASS_OBJECT(surface) {
    // ...
#if WLR_VERSION_MINOR >= 19
    QW_FUNC_MEMBER(surface, get_image_description_v1_data, const wlr_image_description_v1_data *)
#endif
};

常见模式:

// 创建函数 - 总是静态的,返回该类型
QW_FUNC_STATIC(compositor, create, qw_compositor *, ...)

// From/Get 函数 - 通常是静态的,返回该类型
QW_FUNC_STATIC(surface, from_resource, qw_surface *, wl_resource *resource)

// 实例方法 - 成员函数,第一个参数匹配类型
QW_FUNC_MEMBER(surface, has_buffer, bool)
QW_FUNC_MEMBER(surface, send_frame_done, void, const timespec *when)

// 工具函数 - 检查第一个参数确定放置位置
// 如果第一个参数是 wlr_surface* → qw_surface 的成员
// 如果第一个参数是其他类型 → 静态函数或属于其他类

4.2 函数绑定语法

// 静态函数绑定
QW_FUNC_STATIC(type, create, qw_type *, ...)       // 创建函数,返回同类型用 qw_*
QW_FUNC_STATIC(type, from, qw_type *, ...)         // 转换函数,返回同类型用 qw_*
QW_FUNC_STATIC(type, get, qw_type *, ...)          // 获取函数,返回同类型用 qw_*
QW_FUNC_STATIC(type, get_other, wlr_other *, ...) // 获取其他类型,保持 wlr_*
QW_FUNC_STATIC(type, utility, return_type, ...)   // 工具函数,按实际返回类型

// 成员函数绑定
QW_FUNC_MEMBER(type, method, return_type, ...)     // 实例方法,返回类型按实际情况

5. 错误处理

// 检查对象有效性
if (!surface->is_valid()) {
    return;
}

// 安全的句柄访问
if (auto *handle = surface->handle()) {
    // 使用 handle
}

6. 版本兼容性处理

  • 使用条件编译处理 API 变化
  • 优先支持最新版本的 API
  • 为旧版本提供兼容性宏

7. 测试

为新的封装类创建测试:

// tests/test_qwexample.cpp
#include "qwexample.h"
#include <QtTest>

class TestQwExample : public QObject
{
    Q_OBJECT

private Q_SLOTS:
    void testCreate();
    void testSignals();
    void testMethods();
};

常见问题和解决方案

1. 编译错误:找不到 wlroots 函数

问题:使用 QW_FUNC_STATICQW_FUNC_MEMBER 时编译失败。

解决方案

  • 检查 wlroots 版本兼容性
  • 确认函数名拼写正确
  • 使用条件编译处理版本差异

2. 信号连接失败

问题Qt 信号无法正常触发。

解决方案

  • 确保使用了 QW_OBJECTQ_OBJECT
  • 检查信号参数类型是否正确
  • 验证 wlroots 结构体确实有对应的信号

3. 内存泄漏或崩溃

问题:对象销毁时出现问题。

解决方案

  • 正确处理对象所有权
  • 对于有 destroy 方法的类,将析构函数设为 protected
  • 使用智能指针管理生命周期

基于 PR #322 的常见封装错误

基于实际代码审查经验,以下是开发者在添加新封装时容易犯的错误:

4. interface 类型的错误封装

问题:错误地尝试封装 interface 相关的 init 函数。

解决方案

  • init 接口是给 interface 用的,不在普通类中封装
  • 应该封装对应的 interface 类型,如 qw_allocator_interface
  • interface 相关的封装通常需要单独处理

5. 静态函数返回类型错误

问题:静态函数返回类型与当前封装类同类型时,仍使用 wlr_* 类型。

错误示例

// 在 qw_color_transform 类中
QW_FUNC_STATIC(color_transform, create, wlr_color_transform *, ...)  // 错误:应该返回 qw_color_transform*

// 在 qw_abc 类中
QW_FUNC_STATIC(abc, create, wlr_abc *, ...)           // 错误:应该返回 qw_abc*
QW_FUNC_STATIC(abc, get_xxx_data, wlr_xxx *, ...)    // 正确:返回其他类型保持不变

正确格式

// 在 qw_color_transform 类中
QW_FUNC_STATIC(color_transform, create, qw_color_transform *, ...)   // 正确:返回同类型用 qw_*

// 在 qw_abc 类中
QW_FUNC_STATIC(abc, create, qw_abc *, ...)            // 正确:返回同类型用 qw_*
QW_FUNC_STATIC(abc, get_xxx_data, wlr_xxx *, ...)     // 正确:返回其他类型保持 wlr_*

解决方案

  • 只有当静态函数的返回类型与当前封装的类是同类型时,才将 wlr_* 改为 qw_*
  • 如果返回的是其他类型(如 wlr_xxx *),则保持原始的 wlr_* 类型
  • 这确保了封装的一致性:同类型返回封装类型,其他类型保持原始类型

6. 冗余的 destroy 信号声明

问题:在 QW_CLASS_OBJECT 类中手动声明 destroy 信号。

错误示例

class QW_CLASS_OBJECT(example)
{
    QW_OBJECT
    Q_OBJECT

    QW_SIGNAL(destroy)  // 冗余:基类已自动处理
};

正确格式

class QW_CLASS_OBJECT(example)
{
    QW_OBJECT
    Q_OBJECT

    // destroy 信号由基类自动处理,无需显式声明
    // 只声明特定的业务信号
    QW_SIGNAL(custom_event, param_type)
};

解决方案

  • destroy 信号是 QW_CLASS_OBJECT 里特殊处理的,无需手动声明
  • 只声明特定的业务信号即可

7. 类型混合封装错误

问题:在一个类中混合封装不同类型的函数。

错误示例

class QW_CLASS_REINTERPRET_CAST(linux_drm_syncobj_manager_v1)
{
public:
    QW_FUNC_STATIC(linux_drm_syncobj_manager_v1, create, ...)
    // 错误:在 manager 类中封装 syncobj_v1 类型的函数
    QW_FUNC_STATIC(linux_drm_syncobj_v1, get_surface_state, ...)
};

正确设计

// 分离不同的类型
class QW_CLASS_REINTERPRET_CAST(linux_drm_syncobj_manager_v1)
{
public:
    QW_FUNC_STATIC(linux_drm_syncobj_manager_v1, create, ...)
};

class QW_CLASS_REINTERPRET_CAST(linux_drm_syncobj_v1)
{
public:
    QW_FUNC_STATIC(linux_drm_syncobj_v1, get_surface_state, ...)
};

解决方案

  • qw_linux_drm_syncobj_manager_v1 里不应该封装 linux_drm_syncobj_v1 类型的东西
  • 每个类只封装属于自己类型的函数
  • 不同类型的函数应该分离到对应的类中

总结

qwlroots 的封装模式设计精巧,通过模板和宏系统提供了灵活而高效的 C++ 封装。在添加新的类封装时,应该:

  1. 仔细分析 wlroots 结构体的特性
  2. 选择合适的封装模式
  3. 遵循项目的命名和代码规范
  4. 处理版本兼容性问题
  5. 编写相应的测试代码

通过遵循这些最佳实践,可以确保新添加的封装类与项目整体架构保持一致,并提供良好的开发体验。