Skip to content

refactor: replace private access hacks with accessor pattern#625

Open
zccrs wants to merge 1 commit into
linuxdeepin:masterfrom
zccrs:master
Open

refactor: replace private access hacks with accessor pattern#625
zccrs wants to merge 1 commit into
linuxdeepin:masterfrom
zccrs:master

Conversation

@zccrs
Copy link
Copy Markdown
Member

@zccrs zccrs commented May 25, 2026

Remove all '#define private/protected public' hacks and replace them with a proper C++ template-based private accessor pattern using explicit template instantiation (friend injection trick), mirroring the approach used in linuxdeepin/treeland#875.

Add src/private/dprivateaccessor_p.h with Accessor/AccessorImpl templates and D_DECLARE_PRIVATE_MEMBER, D_DECLARE_PRIVATE_METHOD, D_DECLARE_PRIVATE_CONST_METHOD, D_PRIVATE_MEMBER, D_PRIVATE_CALL macros. All helpers are in global namespace so ADL correctly finds the friend-injected get() function.

Copy link
Copy Markdown

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @zccrs, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@zccrs zccrs requested a review from Copilot May 25, 2026 05:13
@deepin-ci-robot
Copy link
Copy Markdown
Contributor

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: zccrs

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR removes preprocessor-based private/protected access hacks and replaces them with a template-based private accessor pattern for reaching Qt private members, while also adding new CI workflows to build the project on Deepin (crimson) and Arch Linux.

Changes:

  • Added a new header implementing an explicit-template-instantiation-based private member accessor and convenience macros.
  • Refactored DQMLGlobalObject to access QSGNode private fields via the new accessor macros instead of #define private public.
  • Updated scenegraph rendering logic in dbackdropnode.cpp and introduced new GitHub Actions build workflows for Deepin and Arch Linux.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
src/util/dprivateaccessor_p.h Adds the template-based private accessor pattern + macros.
src/private/dqmlglobalobject.cpp Replaces #define private public usage with accessor tags/macros for QSGNode internals.
src/private/dbackdropnode.cpp Removes access-hack macros and updates renderer invocation flow.
.github/workflows/dtkdeclarative-deepin-build.yml Adds Deepin (crimson) container build workflow that builds dependencies from source and produces .deb artifacts.
.github/workflows/dtkdeclarative-archlinux-build.yml Adds Arch Linux container build workflow producing install artifacts.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/util/dprivateaccessor_p.h
Comment thread src/util/dprivateaccessor_p.h
Comment thread src/util/dprivateaccessor_p.h
Comment thread .github/workflows/dtkdeclarative-deepin-build.yml Outdated
Comment thread .github/workflows/dtkdeclarative-deepin-build.yml Outdated
Comment thread .github/workflows/dtkdeclarative-archlinux-build.yml Outdated
@zccrs zccrs force-pushed the master branch 7 times, most recently from 5579d87 to a262b4e Compare May 26, 2026 10:37

#include <algorithm>

#ifndef QT_NO_OPENGL
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这个宏判断不需要

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已处理,这个条件编译已经去掉,当前直接保留 static_assert(sizeof(QSGRenderer) == 432),并已随最新 force-push 一起更新。

@zccrs
Copy link
Copy Markdown
Member Author

zccrs commented Jun 1, 2026

已更新这个 PR:\n\n1. 代码已经 rebase 到 upstream 最新,并去掉了混入的 fork master 提交;\n2. dbackdropnode.cpp 已按 Treeland 674e6cfde9925fa2f6b4cb0e49625bee05b8d941 的等价做法改掉;\n3. 刚刚已 force-push 更新分支。

Remove all '#define private/protected public' hacks and replace them with a
proper C++ template-based private accessor pattern using explicit template
instantiation (friend injection trick), mirroring the approach used in
linuxdeepin/treeland#875.

Adds src/util/dprivateaccessor_p.h with Accessor/AccessorImpl templates and
D_DECLARE_PRIVATE_MEMBER, D_PRIVATE_MEMBER macros. All helpers live at global
scope so ADL correctly resolves the friend-injected get() function.
@zccrs
Copy link
Copy Markdown
Member Author

zccrs commented Jun 1, 2026

补充说明一下刚才的更新:\n\n1. 这个 PR 的 head 用的是 zccrs:master,所以我在清理 PR 提交链并 force-push 后,原先混在 master 里的 GitHub Action 提交也一起从该分支历史里拿掉了;\n2. 我已经把原来那条历史备份到了 backup/master-before-pr-cleanup 分支,原有 GitHub Action 改动还在;\n3. 按 Treeland 的做法补了 QSGRenderer bitfield accessor 单测,并新增独立目标 test_qsgrenderer_accessor,本地已通过。

@deepin-ci-robot
Copy link
Copy Markdown
Contributor

deepin pr auto review

这份代码的核心改进是将访问 Qt 私有类成员的机制从 #define private public(属于未定义行为且极其粗暴)替换为基于 C++ 显式模板实例化漏洞的访问器模式。这是一个非常专业且符合现代 C++ 实践的改进。

然而,在实现细节、跨平台兼容性以及代码安全性上,仍有一些需要严重关注的问题。以下是详细的审查意见:

一、 代码安全与未定义行为

1. 严重的内存对齐与类型双关问题
dprivateaccessor_p.hD_DECLARE_PRIVATE_BITFIELD 宏中:

struct TagName##_bf_layout_mirror { PrevMemberType prev; BfStorageType storage; };
static constexpr std::size_t kOffset = 
    __builtin_offsetof(TagName##_bf_layout_mirror, storage) - 
    __builtin_offsetof(TagName##_bf_layout_mirror, prev);

这里试图通过镜像结构体来计算位域的偏移量,但这是不可靠且危险的

  • 对齐填充:C++ 编译器会在 prevstorage 之间插入未定义的字节进行对齐。例如,如果 PrevMemberTypeQSet<QSGNode*>(通常 8 字节对齐),而 BfStorageTypeuint(4 字节),编译器可能会在两者之间插入 4 字节的 padding。这会导致计算出的 kOffset 与 Qt 源码中实际的位域偏移不符。
  • 严格别名违例:在 storagePtr 中,使用 reinterpret_cast<BfStorageType*>(reinterpret_cast<char*>(&prev_ref) + kOffset) 绕过了严格别名规则,虽然在底层编程中常见,但仍属于 UB。

改进建议:对于位域的访问,最安全的方式是定义一个与原类完全匹配的镜像类(包含相同的位域定义),或者使用编译时静态断言配合手动指定的偏移量。如果必须使用当前方案,必须加上严格的 static_assert 验证偏移量。

2. 危险的 static_assert(sizeof(QSGRenderer) == 432)
dbackdropnode.cpp 和测试代码中:

static_assert(sizeof(QSGRenderer) == 432, ...);

类的尺寸高度依赖于编译器、平台(x86 vs ARM)、以及 Qt 的编译选项。硬编码 432 会导致在其他平台或编译器上直接编译失败(而非运行时错误)。

改进建议:将其改为范围检查或取消此限制。如果是为了防止 Qt 升级导致布局变化,可以只在 Debug模式下触发 Q_ASSERT,而不是 static_assert,否则会严重破坏跨平台编译。

二、 语法与逻辑

1. 宏定义中缺少对变参的包裹
D_DECLARE_PRIVATE_METHOD 宏中:

#define D_DECLARE_PRIVATE_METHOD(TagName, ClassName, MethodName, RetType, ...) \
    struct TagName { \
        using MemberPtr = RetType (ClassName::*)(__VA_ARGS__); \
        // ...

当声明无参方法时,__VA_ARGS__ 为空,展开后变为 RetType (ClassName::*)(),这是合法的。但如果用户传入的参数带有逗号(如 std::map<int, int>),且没有用额外括号包裹,宏会将其解析为多个参数。

改进建议:保持当前实现即可,但在文档或注释中提醒使用者,对于含有逗号的复杂类型,需使用别名或括号包裹。

2. 测试用例中的未定义行为
ut_qsgrenderer_accessor.cpp 中:

alignas(QSGRenderer) unsigned char buffer[sizeof(QSGRenderer)];
// ...
renderer = reinterpret_cast<QSGRenderer *>(buffer);
// ...
renderer->m_is_rendering = true; // 严重 UB

直接在未构造对象的裸内存上调用非静态成员赋值(即使是通过直接访问)在 C++ 标准中是严重的未定义行为。

改进建议:使用 QSGRenderer 的合法构造函数创建对象,或者如果构造函数不可用,至少应使用 std::memset 初始化后,仅测试通过 Accessor(宏)进行的读写操作,而不应直接使用 renderer->m_is_rendering 进行验证,除非测试本身也使用了 -fno-access-control 编译(虽然当前测试确实加了,但这使得测试依赖于破坏标准的编译器选项,掩盖了潜在的布局错误)。

三、 代码质量与性能

1. constexpr 函数的 ODR 使用问题
dprivateaccessor_p.h 中:

friend constexpr MemberPtr get(TagName) noexcept { return Ptr; }

get 被声明为 constexpr。在 C++14/17 中,如果 get 在多个翻译单元中被 ODR 使用,由于 constexpr 函数默认是内联的,这通常是安全的。但某些老旧编译器或特定的链接阶段可能会导致符号冲突。

改进建议:确保所有使用该头文件的翻译单元使用相同的编译器标准设置,或直接将其声明为 inline constexpr(C++17 起默认内联,但显式写出更清晰)。

2. 测试对编译器扩展的依赖
测试中使用了 -fno-access-control,这是 GCC/Clang 的专有扩展。

改进建议:既然测试的目的是验证“通过 Accessor 访问的位域与 Qt 内部真实的位域一致”,更好的测试方法是:在测试中构造一个与 Qt 内部完全相同的镜像 Struct,通过 Accessor 写入,然后通过镜像 Struct 读取,反之亦然。这样就不需要依赖破坏访问控制的编译器选项。

四、 改进后的代码示例

针对 dprivateaccessor_p.h 中最危险的位域部分,建议进行如下加固:

// 改进后的 D_DECLARE_PRIVATE_BITFIELD 宏
#define D_DECLARE_PRIVATE_BITFIELD(TagName, ClassName, PrevMember, PrevMemberType, BfStorageType, ExpectedOffset) \
    struct TagName { \
        using MemberPtr = PrevMemberType ClassName::*; \
        friend constexpr MemberPtr get(TagName) noexcept; \
    }; \
    template struct DtkDeclarativePrivateAccessorImpl<TagName, &ClassName::PrevMember>; \
    struct TagName##_Bits { \
        /* 使用用户提供的预期偏移量,并在编译期进行强校验 */ \
        static constexpr std::size_t kOffset = ExpectedOffset; \
        /* 注意:移除了不可靠的 __builtin_offsetof 镜像推算 */ \
        static BfStorageType *storagePtr(ClassName *obj) noexcept { \
            PrevMemberType &prev_ref = (*obj).*get(TagName{}); \
            return reinterpret_cast<BfStorageType*>( \
                reinterpret_cast<char *>(&prev_ref) + kOffset); \
        } \
        static const BfStorageType *storagePtr(const ClassName *obj) noexcept { \
            return storagePtr(const_cast<ClassName *>(obj)); \
        } \
        static bool getBit(const ClassName *obj, unsigned bit) noexcept { \
            return (*storagePtr(obj) >> bit) & BfStorageType{1}; \
        } \
        static void setBit(ClassName *obj, unsigned bit, bool val) noexcept { \
            BfStorageType *p = storagePtr(obj); \
            if (val) *p |= (BfStorageType{1} << bit); \
            else     *p &= ~(BfStorageType{1} << bit); \
        } \
    }

在使用处,明确指定偏移量:

// 假设通过分析 qsgrenderer_p.h 得知 m_is_rendering 在 m_nodes_dont_preprocess 之后的 0 字节处
D_DECLARE_PRIVATE_BITFIELD(QSGRenderer_m_nodes_dont_preprocess_tag,
                           QSGRenderer, m_nodes_dont_preprocess,
                           QSet<QSGNode *>, uint, 0);

总结

整体架构改进方向非常值得肯定,消除了 #define private public 带来的全局命名空间污染和 ODR 违背问题。但位域偏移的计算逻辑是目前代码中最大的定时炸弹,强烈建议放弃通过镜像结构体自动推算偏移的做法,改为人工核定偏移量并辅以运行时断言校验,以确保在不同编译器和平台上的内存安全。

@zccrs
Copy link
Copy Markdown
Member Author

zccrs commented Jun 1, 2026

补充一下 GitHub Action 那条提交的处理:当前 PR 仍保持只包含本次修复,不再把 workflow 提交带回 master;为了保留并方便单独处理,我已经把 workflow 提交单独推到了 ci-workflows-only 分支(另外旧链路也还在 backup/master-before-pr-cleanup)。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants