19 KiB
qwlroots 封装模式最佳实践指南
概述
qwlroots 是对 wlroots C 接口的 Qt 风格封装库,通过宏系统提供了多种封装模式来适应不同类型的 wlroots 结构体。本文档总结了项目中使用的几种主要封装模式,为添加新的类封装提供指导。
核心设计理念
- Qt 风格接口:遵循 Qt 规范的接口风格
- 信号槽机制:将 wl_signal 封装为 Qt 信号
- 内存管理:自动处理对象生命周期和所有权
- 类型安全:利用 C++ 模板和概念确保类型安全
- 零开销抽象:在 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/)
文件命名规律
-
头文件命名:
- wlroots:
wlr_compositor.h→ qwlroots:qwcompositor.h - wlroots:
wlr_output.h→ qwlroots:qwoutput.h - wlroots:
wlr_buffer.h→ qwlroots:qwbuffer.h
- wlroots:
-
类名命名:
- wlroots:
struct wlr_compositor→ qwlroots:class qw_compositor - wlroots:
struct wlr_surface→ qwlroots:class qw_surface
- wlroots:
-
目录分类规律:
types/: 主要的 Wayland 协议类型和核心对象util/: 工具类和辅助函数render/: 渲染相关的类型interfaces/: 接口实现相关的类型
查找对应关系的方法
当需要为新的 wlroots 类型创建封装时:
- 定位源文件:在
wlroots/include/wlr/中找到对应的头文件 - 确定目录:根据 wlroots 的目录结构确定在 qwlroots 中的位置
- 命名转换:按照
wlr_xxx.h→qwxxx.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 结构体
-
检查是否有 events 字段:
- 有 events 字段 → 使用
QW_CLASS_OBJECT - 无 events 字段 → 使用
QW_CLASS_REINTERPRET_CAST
- 有 events 字段 → 使用
-
检查是否需要接口实现:
- 需要实现回调 → 使用
QW_CLASS_INTERFACE
- 需要实现回调 → 使用
-
检查是否为简单数据结构:
- 简单数据结构 → 考虑自定义封装或
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_STATIC 或 QW_FUNC_MEMBER 时编译失败。
解决方案:
- 检查 wlroots 版本兼容性
- 确认函数名拼写正确
- 使用条件编译处理版本差异
2. 信号连接失败
问题:Qt 信号无法正常触发。
解决方案:
- 确保使用了
QW_OBJECT和Q_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++ 封装。在添加新的类封装时,应该:
- 仔细分析 wlroots 结构体的特性
- 选择合适的封装模式
- 遵循项目的命名和代码规范
- 处理版本兼容性问题
- 编写相应的测试代码
通过遵循这些最佳实践,可以确保新添加的封装类与项目整体架构保持一致,并提供良好的开发体验。