From f8fa07a2aa69584053bab66ef08f8b47cc6decfd Mon Sep 17 00:00:00 2001 From: Lu YaNing Date: Wed, 7 Jan 2026 10:58:10 +0800 Subject: [PATCH] fix: Refactor DConfig wrapper class generation for thread safety and lifecycle management Problem: - Single-threaded design with weak state machine (Invalid -> Succeed/Failed) - No proper handling of object destruction during initialization - Signal emissions in worker thread context (incorrect thread context) - Fragile destructor unable to handle all cleanup scenarios Solution: 1. Introduce Data layer separation (TreelandUserConfigData + TreelandUserConfig) - Clear separation between internal data management and public API - Enables safer object lifecycle management 2. Enhance state machine (3-state -> 5-state model) - Add Initializing and Destroyed states - Use atomic CAS operations for thread-safe state transitions - States: Invalid -> Initializing -> (Succeed | Failed | Destroyed) 3. Improve async initialization and cleanup - Use QPointer for safe backref checks (prevent use-after-free) - Support 4 destruction paths: normal/failed/quick/mid-initialization - Atomic state transitions with proper signal emission guards 4. Separate thread responsibilities - updateValue(): Worker thread reads config values - updateProperty(): Main thread updates properties and emits signals - Use QMetaObject::invokeMethod for correct thread context Improvements: - Thread safety: Complete atomic operations coverage - Memory safety: QPointer guards prevent dangling pointers - Code clarity: Layered architecture with clear responsibilities - Backward compatibility: API unchanged --- tools/dconfig2cpp/main.cpp | 585 ++++++++++++++++++++----------------- 1 file changed, 321 insertions(+), 264 deletions(-) diff --git a/tools/dconfig2cpp/main.cpp b/tools/dconfig2cpp/main.cpp index 85d92a60..82361e72 100644 --- a/tools/dconfig2cpp/main.cpp +++ b/tools/dconfig2cpp/main.cpp @@ -11,6 +11,7 @@ #include #include #include +#include static QString toUnicodeEscape(const QString& input) { QString result; @@ -49,7 +50,6 @@ static QString jsonValueToCppCode(const QJsonValue &value){ } else if (variantValue.userType() == QVariant(static_cast(1)).userType()) { return QString::number(variantValue.toLongLong()); } - return QString::number(value.toDouble()); } else if (value.isString()) { const auto string = value.toString(); @@ -71,8 +71,7 @@ static QString jsonValueToCppCode(const QJsonValue &value){ QJsonObject obj = value.toObject(); for (auto it = obj.begin(); it != obj.end(); ++it) { elements << QString("{QStringLiteral(u\"%1\"), QVariant(%2)}") - .arg(toUnicodeEscape(it.key()), - jsonValueToCppCode(it.value())); + .arg(toUnicodeEscape(it.key()), jsonValueToCppCode(it.value())); } return "QVariantMap{" + elements.join(", ") + "}"; } else { @@ -157,18 +156,16 @@ int main(int argc, char *argv[]) { QString generationTime = QDateTime::currentDateTime().toString(Qt::ISODate); if (!parser.isSet(noComment)) { - QString headerComment = QString( - "/**\n" - " * This file is generated by dconfig2cpp.\n" - " * Command line arguments: %1\n" - " * Generation time: %2\n" - " * JSON file version: %3\n" - " *\n" - " * WARNING: DO NOT MODIFY THIS FILE MANUALLY.\n" - " * If you need to change the content, please modify the dconfig2cpp tool.\n" - " */\n\n" - ).arg(commandLineArgs, generationTime, version); - + QString headerComment = QString("/**\n" + " * This file is generated by dconfig2cpp.\n" + " * Command line arguments: %1\n" + " * Generation time: %2\n" + " * JSON file version: %3\n" + " *\n" + " * WARNING: DO NOT MODIFY THIS FILE MANUALLY.\n" + " * If you need to change the content, please modify the dconfig2cpp tool.\n" + " */\n\n") + .arg(commandLineArgs, generationTime, version); headerStream << headerComment; } @@ -189,8 +186,6 @@ int main(int argc, char *argv[]) { headerStream << "#include \n"; headerStream << "#include \n\n"; - headerStream << "class " << className << " : public QObject {\n"; - headerStream << " Q_OBJECT\n\n"; struct Property { QString typeName; @@ -215,18 +210,20 @@ int main(int argc, char *argv[]) { "isDefaultValue", "m_config", "m_status", + "m_data", }; for (int i = 0; i <= (contents.size()) / 32; ++i) { usedKeywords << QLatin1String("m_propertySetStatus") + QString::number(i); } - // Iterate over JSON contents to extract properties + // Extract properties from JSON for (auto it = contents.begin(); it != contents.end(); ++it) { QJsonObject obj = it.value().toObject(); QString propertyName = it.key(); QString typeName; const auto value = obj[QLatin1String("value")]; + if (value.isBool()) { typeName = "bool"; } else if (value.isArray()) { @@ -257,39 +254,120 @@ int main(int argc, char *argv[]) { } propertyNames << propertyName; - properties.append(Property({ - typeName, - propertyName, - capitalizedPropertyName, - "QStringLiteral(\"" + propertyName + "\")", - obj[QLatin1String("value")] - })); + properties.append(Property({typeName, + propertyName, + capitalizedPropertyName, + "QStringLiteral(\"" + propertyName + "\")", + obj[QLatin1String("value")]})); propertyNameStrings << properties.last().propertyNameString; + } + + // ==================== Forward Declarations ==================== + headerStream << "class " << className << ";\n\n"; + + // ==================== TreelandUserConfigData definition ==================== + headerStream << "class " << className << "Data : public QObject {\n" + << "public:\n" + << " enum class Status {\n" + << " Invalid = 0,\n" + << " Initializing = 1,\n" + << " Succeed = 2,\n" + << " Failed = 3,\n" + << " Destroyed = 4\n" + << " };\n" + << "\n" + << " explicit " << className << "Data(QObject *parent = nullptr)\n" + << " : QObject(parent) {}\n" + << "\n" + << " void initializeInConfigThread(DTK_CORE_NAMESPACE::DConfig *config);\n" + << " void updateValue(const QString &key, const QVariant &fallback = QVariant());\n" + << " void updateProperty(const QString &key, const QVariant &value);\n" + << "\n" + << " inline void markPropertySet(const int index, bool on = true) {\n"; + + for (int i = 0; i <= (properties.size()) / 32; ++i) { + headerStream << " if (index < " << (i + 1) * 32 << ") {\n" + << " if (on)\n" + << " m_propertySetStatus" << i << ".fetchAndOrOrdered(1 << (index - " << i * 32 << "));\n" + << " else\n" + << " m_propertySetStatus" << i << ".fetchAndAndOrdered(~(1 << (index - " << i * 32 << ")));\n" + << " return;\n" + << " }\n"; + } + headerStream << " Q_UNREACHABLE();\n" + << " }\n" + << "\n" + << " inline bool testPropertySet(const int index) const {\n"; + + for (int i = 0; i <= (properties.size()) / 32; ++i) { + headerStream << " if (index < " << (i + 1) * 32 << ") {\n" + << " return (m_propertySetStatus" << i << ".loadRelaxed() & (1 << (index - " << i * 32 << ")));\n" + << " }\n"; + } + headerStream << " Q_UNREACHABLE();\n" + << " }\n\n" + << " QAtomicPointer m_config = nullptr;\n" + << " QAtomicInteger m_status = static_cast(Status::Invalid);\n" + << " QPointer<" << className << "> m_userConfig = nullptr;\n"; - const QString readFunction = usedKeywords.contains(propertyName) ? QLatin1String(" READ get") + capitalizedPropertyName - : QLatin1String(" READ ") + propertyName; - headerStream << " Q_PROPERTY(" << typeName << " " << propertyName << readFunction - << " WRITE set" << capitalizedPropertyName << " NOTIFY " << propertyName << "Changed" - << " RESET reset" << capitalizedPropertyName << ")\n"; + for (int i = 0; i <= (properties.size()) / 32; ++i) { + headerStream << " QAtomicInteger m_propertySetStatus" << i << " = 0;\n"; } - headerStream << " Q_CLASSINFO(\"DConfigKeyList\", \"" << propertyNames.join(";") <<"\")\n" - << " Q_CLASSINFO(\"DConfigFileName\", \"" << QString(jsonFileName).replace("\n", "\\n").replace("\r", "\\r") <<"\")\n" - << " Q_CLASSINFO(\"DConfigFileVersion\", \"" << version <<"\")\n\n" + headerStream << "\n // Property storage\n"; + for (const Property &property : properties) { + if (property.typeName == QLatin1String("QString")) { + headerStream << " // Default value: \"" + << property.defaultValue.toString().replace("\n", "\\n").replace("\r", "\\r") << "\"\n"; + } + headerStream << " " << property.typeName << " p_" << property.propertyName << " { " + << jsonValueToCppCode(property.defaultValue) << " };\n"; + } + headerStream << "};\n\n"; + + // ==================== TreelandUserConfig definition ==================== + headerStream << "class " << className << " : public QObject {\n" + << " Q_OBJECT\n" + << "public:\n" + << " using Data = " << className << "Data;\n\n"; + + // Generate Q_PROPERTY declarations + for (const Property &property : properties) { + const QString readFunction = usedKeywords.contains(property.propertyName) ? + QLatin1String(" READ get") + property.capitalizedPropertyName : + QLatin1String(" READ ") + property.propertyName; + headerStream << " Q_PROPERTY(" << property.typeName << " " << property.propertyName << readFunction << " WRITE set" + << property.capitalizedPropertyName << " NOTIFY " << property.propertyName << "Changed" + << " RESET reset" << property.capitalizedPropertyName << ")\n"; + } + + headerStream << " Q_CLASSINFO(\"DConfigKeyList\", \"" << propertyNames.join(";") << "\")\n" + << " Q_CLASSINFO(\"DConfigFileName\", \"" << QString(jsonFileName).replace("\n", "\\n").replace("\r", "\\r") + << "\")\n" + << " Q_CLASSINFO(\"DConfigFileVersion\", \"" << version << "\")\n\n" << "public:\n" - << " explicit " << className - << R"((QThread *thread, DTK_CORE_NAMESPACE::DConfigBackend *backend, + << " explicit " << className << R"((QThread *thread, DTK_CORE_NAMESPACE::DConfigBackend *backend, const QString &name, const QString &appId, const QString &subpath, bool isGeneric, QObject *parent) - : QObject(nullptr) { + : QObject(parent), m_data(new Data) { + m_data->m_userConfig = this; + if (!thread->isRunning()) { qWarning() << QLatin1String("Warning: The provided thread is not running."); } Q_ASSERT(QThread::currentThread() != thread); auto worker = new QObject(); worker->moveToThread(thread); - QPointer watcher(parent); - QMetaObject::invokeMethod(worker, [=, this]() { + auto data = m_data; + + QMetaObject::invokeMethod(worker, [=]() { + // Atomically transition from Invalid to Initializing + if (!data->m_status.testAndSetOrdered(static_cast(Data::Status::Invalid), + static_cast(Data::Status::Initializing))) { + worker->deleteLater(); + return; + } + DTK_CORE_NAMESPACE::DConfig *config = nullptr; if (isGeneric) { if (backend) { @@ -314,268 +392,247 @@ int main(int argc, char *argv[]) { } } } - if (!config) { + + if (!config || !config->isValid()) { qWarning() << QLatin1String("Failed to create DConfig instance."); + + if (data->m_status.testAndSetOrdered(static_cast(Data::Status::Initializing), + static_cast(Data::Status::Failed))) { + // Successfully transitioned to Failed - notify main thread + QMetaObject::invokeMethod(data, [data]() { + if (data->m_userConfig) + Q_EMIT data->m_userConfig->configInitializeFailed(); + }); + } + worker->deleteLater(); + if (config) + delete config; + return; } + config->moveToThread(QThread::currentThread()); - initializeInConfigThread(config); - if (watcher != parent) { - // delete this if watcher is changed to nullptr. - deleteLater(); - } else if (!this->parent() && parent) { - // !parent() means that parent is not changed. - this->setParent(watcher); - } + + // Initialize through Data class + data->initializeInConfigThread(config); + + QObject::connect(config, &DTK_CORE_NAMESPACE::DConfig::valueChanged, data, [data](const QString &key) { data->updateValue(key); }, Qt::DirectConnection); + QObject::connect(config, &QObject::destroyed, data, &QObject::deleteLater); + QMetaObject::invokeMethod(data, [data, config]() { + if (data->m_userConfig) + Q_EMIT data->m_userConfig->configInitializeSucceed(config); + }); worker->deleteLater(); }); } )"; const QString jsonFileString = "QStringLiteral(u\"" + toUnicodeEscape(jsonFileName) + "\")"; - // Generate constructors - if (parser.isSet(forceRequestThread)) - headerStream << " static " << className << "* create(QThread *thread, const QString &appId = {}, const QString &subpath = {}, QObject *parent = nullptr)\n"; - else - headerStream << " static " << className << "* create(const QString &appId = {}, const QString &subpath = {}, QObject *parent = nullptr, QThread *thread = DTK_CORE_NAMESPACE::DConfig::globalThread())\n"; - headerStream << " { return new " << className << "(thread, nullptr, " << jsonFileString << ", appId, subpath, false, parent); }\n"; - if (parser.isSet(forceRequestThread)) - headerStream << " static " << className << "* create(QThread *thread, DTK_CORE_NAMESPACE::DConfigBackend *backend, const QString &appId = {}, const QString &subpath = {}, QObject *parent = nullptr)\n"; - else - headerStream << " static " << className << "* create(DTK_CORE_NAMESPACE::DConfigBackend *backend, const QString &appId = {}, const QString &subpath = {}, QObject *parent = nullptr, QThread *thread = DTK_CORE_NAMESPACE::DConfig::globalThread())\n"; - headerStream << " { return new " << className << "(thread, backend, " << jsonFileString << ", appId, subpath, false, parent); }\n"; - if (parser.isSet(forceRequestThread)) - headerStream << " static " << className << "* createByName(QThread *thread, const QString &name, const QString &appId = {}, const QString &subpath = {}, QObject *parent = nullptr)\n"; - else - headerStream << " static " << className << "* createByName(const QString &name, const QString &appId = {}, const QString &subpath = {}, QObject *parent = nullptr, QThread *thread = DTK_CORE_NAMESPACE::DConfig::globalThread())\n"; - headerStream << " { return new " << className << "(thread, nullptr, name, appId, subpath, false, parent); }\n"; - if (parser.isSet(forceRequestThread)) - headerStream << " static " << className << "* createByName(QThread *thread, DTK_CORE_NAMESPACE::DConfigBackend *backend, const QString &name, const QString &appId = {}, const QString &subpath = {}, QObject *parent = nullptr)\n"; - else - headerStream << " static " << className << "* createByName(DTK_CORE_NAMESPACE::DConfigBackend *backend, const QString &name, const QString &appId = {}, const QString &subpath = {}, QObject *parent = nullptr, QThread *thread = DTK_CORE_NAMESPACE::DConfig::globalThread())\n"; - headerStream << " { return new " << className << "(thread, backend, name, appId, subpath, false, parent); }\n"; - - if (parser.isSet(forceRequestThread)) - headerStream << " static " << className << "* createGeneric(QThread *thread, const QString &subpath = {}, QObject *parent = nullptr)\n"; - else - headerStream << " static " << className << "* createGeneric(const QString &subpath = {}, QObject *parent = nullptr, QThread *thread = DTK_CORE_NAMESPACE::DConfig::globalThread())\n"; - headerStream << " { return new " << className << "(thread, nullptr, " << jsonFileString << ", {}, subpath, true, parent); }\n"; - if (parser.isSet(forceRequestThread)) - headerStream << " static " << className << "* create(QThread *thread, DTK_CORE_NAMESPACE::DConfigBackend *backend, const QString &subpath = {}, QObject *parent = nullptr)\n"; - else - headerStream << " static " << className << "* create(DTK_CORE_NAMESPACE::DConfigBackend *backend, const QString &subpath = {}, QObject *parent = nullptr, QThread *thread = DTK_CORE_NAMESPACE::DConfig::globalThread())\n"; - headerStream << " { return new " << className << "(thread, backend, " << jsonFileString << ", {}, subpath, true, parent); }\n"; - if (parser.isSet(forceRequestThread)) - headerStream << " static " << className << "* createGenericByName(QThread *thread, const QString &name, const QString &subpath = {}, QObject *parent = nullptr)\n"; - else - headerStream << " static " << className << "* createGenericByName(const QString &name, const QString &subpath = {}, QObject *parent = nullptr, QThread *thread = DTK_CORE_NAMESPACE::DConfig::globalThread())\n"; - headerStream << " { return new " << className << "(thread, nullptr, name, {}, subpath, true, parent); }\n"; - if (parser.isSet(forceRequestThread)) - headerStream << " static " << className << "* createGenericByName(QThread *thread, DTK_CORE_NAMESPACE::DConfigBackend *backend, const QString &name, const QString &subpath = {}, QObject *parent = nullptr)\n"; - else - headerStream << " static " << className << "* createGenericByName(DTK_CORE_NAMESPACE::DConfigBackend *backend, const QString &name, const QString &subpath = {}, QObject *parent = nullptr, QThread *thread = DTK_CORE_NAMESPACE::DConfig::globalThread())\n"; - headerStream << " { return new " << className << "(thread, backend, name, {}, subpath, true, parent); }\n"; - // Destructor - headerStream << " ~" << className << R"(() { - if (m_config.loadRelaxed()) { - m_config.loadRelaxed()->deleteLater(); - m_config.storeRelaxed(nullptr); - } - } - - Q_INVOKABLE DTK_CORE_NAMESPACE::DConfig *config() const { - return m_config.loadRelaxed(); + // Create factory methods + if (parser.isSet(forceRequestThread)) { + headerStream + << " static " << className + << "* create(QThread *thread, const QString &appId = {}, const QString &subpath = {}, QObject *parent = nullptr)\n"; + headerStream << " { return new " << className << "(thread, nullptr, " << jsonFileString + << ", appId, subpath, false, parent); }\n"; + headerStream << " static " << className + << "* create(QThread *thread, DTK_CORE_NAMESPACE::DConfigBackend *backend, const QString &appId = {}, const " + "QString &subpath = {}, QObject *parent = nullptr)\n"; + headerStream << " { return new " << className << "(thread, backend, " << jsonFileString + << ", appId, subpath, false, parent); }\n"; + headerStream << " static " << className + << "* createByName(QThread *thread, const QString &name, const QString &appId = {}, const QString &subpath " + "= {}, QObject *parent = nullptr)\n"; + headerStream << " { return new " << className << "(thread, nullptr, name, appId, subpath, false, parent); }\n"; + headerStream << " static " << className + << "* createByName(QThread *thread, DTK_CORE_NAMESPACE::DConfigBackend *backend, const QString &name, const " + "QString &appId = {}, const QString &subpath = {}, QObject *parent = nullptr)\n"; + headerStream << " { return new " << className << "(thread, backend, name, appId, subpath, false, parent); }\n"; + headerStream << " static " << className + << "* createGeneric(QThread *thread, const QString &subpath = {}, QObject *parent = nullptr)\n"; + headerStream << " { return new " << className << "(thread, nullptr, " << jsonFileString + << ", {}, subpath, true, parent); }\n"; + headerStream << " static " << className + << "* create(QThread *thread, DTK_CORE_NAMESPACE::DConfigBackend *backend, const QString &subpath = {}, " + "QObject *parent = nullptr)\n"; + headerStream << " { return new " << className << "(thread, backend, " << jsonFileString + << ", {}, subpath, true, parent); }\n"; + headerStream << " static " << className + << "* createGenericByName(QThread *thread, const QString &name, const QString &subpath = {}, QObject " + "*parent = nullptr)\n"; + headerStream << " { return new " << className << "(thread, nullptr, name, {}, subpath, true, parent); }\n"; + headerStream << " static " << className + << "* createGenericByName(QThread *thread, DTK_CORE_NAMESPACE::DConfigBackend *backend, const QString " + "&name, const QString &subpath = {}, QObject *parent = nullptr)\n"; + headerStream << " { return new " << className << "(thread, backend, name, {}, subpath, true, parent); }\n"; + } else { + headerStream << " static " << className + << "* create(const QString &appId = {}, const QString &subpath = {}, QObject *parent = nullptr, QThread " + "*thread = DTK_CORE_NAMESPACE::DConfig::globalThread())\n"; + headerStream << " { return new " << className << "(thread, nullptr, " << jsonFileString + << ", appId, subpath, false, parent); }\n"; + headerStream << " static " << className + << "* create(DTK_CORE_NAMESPACE::DConfigBackend *backend, const QString &appId = {}, const QString &subpath " + "= {}, QObject *parent = nullptr, QThread *thread = DTK_CORE_NAMESPACE::DConfig::globalThread())\n"; + headerStream << " { return new " << className << "(thread, backend, " << jsonFileString + << ", appId, subpath, false, parent); }\n"; + headerStream << " static " << className + << "* createByName(const QString &name, const QString &appId = {}, const QString &subpath = {}, QObject " + "*parent = nullptr, QThread *thread = DTK_CORE_NAMESPACE::DConfig::globalThread())\n"; + headerStream << " { return new " << className << "(thread, nullptr, name, appId, subpath, false, parent); }\n"; + headerStream << " static " << className + << "* createByName(DTK_CORE_NAMESPACE::DConfigBackend *backend, const QString &name, const QString &appId = " + "{}, const QString &subpath = {}, QObject *parent = nullptr, QThread *thread = " + "DTK_CORE_NAMESPACE::DConfig::globalThread())\n"; + headerStream << " { return new " << className << "(thread, backend, name, appId, subpath, false, parent); }\n"; + headerStream << " static " << className + << "* createGeneric(const QString &subpath = {}, QObject *parent = nullptr, QThread *thread = " + "DTK_CORE_NAMESPACE::DConfig::globalThread())\n"; + headerStream << " { return new " << className << "(thread, nullptr, " << jsonFileString + << ", {}, subpath, true, parent); }\n"; + headerStream << " static " << className + << "* create(DTK_CORE_NAMESPACE::DConfigBackend *backend, const QString &subpath = {}, QObject *parent = " + "nullptr, QThread *thread = DTK_CORE_NAMESPACE::DConfig::globalThread())\n"; + headerStream << " { return new " << className << "(thread, backend, " << jsonFileString + << ", {}, subpath, true, parent); }\n"; + headerStream << " static " << className + << "* createGenericByName(const QString &name, const QString &subpath = {}, QObject *parent = nullptr, " + "QThread *thread = DTK_CORE_NAMESPACE::DConfig::globalThread())\n"; + headerStream << " { return new " << className << "(thread, nullptr, name, {}, subpath, true, parent); }\n"; + headerStream + << " static " << className + << "* createGenericByName(DTK_CORE_NAMESPACE::DConfigBackend *backend, const QString &name, const QString &subpath = " + "{}, QObject *parent = nullptr, QThread *thread = DTK_CORE_NAMESPACE::DConfig::globalThread())\n"; + headerStream << " { return new " << className << "(thread, backend, name, {}, subpath, true, parent); }\n"; } - Q_INVOKABLE bool isInitializeSucceed() const { - return m_status.loadRelaxed() == static_cast(Status::Succeed); + // Destructor + headerStream << "\n ~" << className << R"(() { + int oldStatus = m_data->m_status.fetchAndStoreOrdered(static_cast(Data::Status::Destroyed)); + m_data->m_userConfig = nullptr; + + if (oldStatus == static_cast(Data::Status::Succeed)) { + if (auto config = m_data->m_config.loadRelaxed()) { + config->deleteLater(); + // m_data will be deleted by config->destroyed connection + } else { + m_data->deleteLater(); + } + } else if (oldStatus == static_cast(Data::Status::Failed) || oldStatus == static_cast(Data::Status::Invalid)) { + m_data->deleteLater(); + } + // If Initializing, worker thread handles m_data deletion when it sees Destroyed status } - Q_INVOKABLE bool isInitializeFailed() const { - return m_status.loadRelaxed() == static_cast(Status::Failed); - } + Q_INVOKABLE DTK_CORE_NAMESPACE::DConfig *config() const { return m_data->m_config.loadRelaxed(); } + Q_INVOKABLE bool isInitializeSucceed() const { return m_data->m_status.loadRelaxed() == static_cast(Data::Status::Succeed); } + Q_INVOKABLE bool isInitializeFailed() const { return m_data->m_status.loadRelaxed() == static_cast(Data::Status::Failed); } + Q_INVOKABLE bool isInitializing() const { return m_data->m_status.loadRelaxed() == static_cast(Data::Status::Initializing); } - Q_INVOKABLE bool isInitializing() const { - return m_status.loadRelaxed() == static_cast(Status::Invalid); - } + Q_INVOKABLE QStringList keyList() const { return { )" + << propertyNameStrings.join(", ") << R"( }; } + Q_INVOKABLE bool isDefaultValue(const QString &key) const { )"; - - headerStream << " Q_INVOKABLE QStringList keyList() const {\n" - << " return { " << propertyNameStrings.join(",\n ") << "};\n" - << " }\n\n"; - - headerStream << " Q_INVOKABLE bool isDefaultValue(const QString &key) const {\n"; - for (int i = 0; i < properties.size(); ++i) { - headerStream << " if (key == " << properties.at(i).propertyNameString << ")\n" - << " return " << properties.at(i).propertyName << "IsDefaultValue();\n"; + for (const auto &p : properties) { + headerStream << " if (key == " << p.propertyNameString << ") return " << p.propertyName << "IsDefaultValue();\n"; } - headerStream << " return false;\n" - << " }\n\n"; + headerStream << " return false;\n }\n\n"; - // Generate property getter and setter methods + // Property getters/setters/signals for (int i = 0; i < properties.size(); ++i) { - const Property &property = properties[i]; - const QString readFunction = usedKeywords.contains(property.propertyName) - ? "get" + property.capitalizedPropertyName - : property.propertyName; - assert(!usedKeywords.contains(readFunction)); - - headerStream << " " << property.typeName << " " << readFunction << "() const {\n" - << " return p_" << property.propertyName << ";\n }\n"; - headerStream << " void set" << property.capitalizedPropertyName << "(const " << property.typeName << " &value) {\n" - << " auto oldValue = p_" << property.propertyName << ";\n" - << " p_" << property.propertyName << " = value;\n" - << " markPropertySet(" << i << ");\n" - << " if (auto config = m_config.loadRelaxed()) {\n" - << " QMetaObject::invokeMethod(config, [this, value]() {\n" - << " m_config.loadRelaxed()->setValue(" << property.propertyNameString << ", value);\n" - << " });\n" - << " }\n" - << " if (p_" << property.propertyName << " != oldValue) {\n" - << " Q_EMIT " << property.propertyName << "Changed();\n" - << " Q_EMIT valueChanged(" << property.propertyNameString << ", value);\n" + const auto &p = properties[i]; + const QString read = usedKeywords.contains(p.propertyName) ? "get" + p.capitalizedPropertyName : p.propertyName; + headerStream << " " << p.typeName << " " << read << "() const { return m_data->p_" << p.propertyName << "; }\n" + << " void set" << p.capitalizedPropertyName << "(const " << p.typeName << " &v) {\n" + << " if (m_data->p_" << p.propertyName << " == v && m_data->testPropertySet(" << i << ")) return;\n" + << " m_data->p_" << p.propertyName << " = v;\n" + << " m_data->markPropertySet(" << i << ");\n" + << " if (auto config = m_data->m_config.loadRelaxed()) {\n" + << " QMetaObject::invokeMethod(config, [config, v]() { config->setValue(" << p.propertyNameString + << ", v); });\n" << " }\n" + << " Q_EMIT " << p.propertyName << "Changed();\n" + << " Q_EMIT valueChanged(" << p.propertyNameString << ", v);\n" << " }\n" - << " void reset" << property.capitalizedPropertyName << "() {\n" - << " if (auto config = m_config.loadRelaxed()) {\n" - << " QMetaObject::invokeMethod(config, [this]() {\n" - << " m_config.loadRelaxed()->reset(" << property.propertyNameString << ");\n" - << " });\n" + << " void reset" << p.capitalizedPropertyName << "() {\n" + << " if (auto config = m_data->m_config.loadRelaxed()) {\n" + << " QMetaObject::invokeMethod(config, [config]() { config->reset(" << p.propertyNameString + << "); });\n" << " }\n" - << " }\n"; - headerStream << "#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)\n"; - headerStream << " QBindable<" << property.typeName << "> bindable" << property.capitalizedPropertyName << "() {\n" - << " return QBindable<" << property.typeName << ">(this, \"" << property.propertyName << "\");\n" - << " }\n"; - headerStream << "#endif\n"; - - headerStream << " Q_INVOKABLE bool " << property.propertyName << "IsDefaultValue() const {\n" - << " return !testPropertySet(" << i << ");\n" - << " }\n"; + << " }\n" + << " Q_INVOKABLE bool " << p.propertyName << "IsDefaultValue() const { return !m_data->testPropertySet(" + << i << "); }\n"; } - // Generate signals for property changes - headerStream << "Q_SIGNALS:\n" - << " void configInitializeFailed(DTK_CORE_NAMESPACE::DConfig *config);\n" + headerStream << "\nQ_SIGNALS:\n" + << " void configInitializeFailed();\n" << " void configInitializeSucceed(DTK_CORE_NAMESPACE::DConfig *config);\n" - << " void valueChanged(const QString &key, const QVariant &value);\n\n"; - for (const Property &property : std::as_const(properties)) { - headerStream << " void " << property.propertyName << "Changed();\n"; - } + << " void valueChanged(const QString &key, const QVariant &value);\n"; + for (const auto &p : properties) + headerStream << " void " << p.propertyName << "Changed();\n"; - // Generate private methods and members - headerStream << "private:\n"; - - headerStream << " void initializeInConfigThread(DTK_CORE_NAMESPACE::DConfig *config) {\n" - << " Q_ASSERT(!m_config.loadRelaxed());\n" - << " m_config.storeRelaxed(config);\n" - << " if (!config->isValid()) {\n" - << " m_status.storeRelaxed(static_cast(Status::Failed));\n" - << " Q_EMIT configInitializeFailed(config);\n" - << " return;\n" - << " }\n\n"; - for (int i = 0; i < properties.size(); ++i) { - const Property &property = properties[i]; - headerStream << " if (testPropertySet(" << i << ")) {\n"; - headerStream << " config->setValue(" << property.propertyNameString << ", QVariant::fromValue(p_" << property.propertyName << "));\n"; - headerStream << " } else {\n"; - headerStream << " updateValue(" << property.propertyNameString << ", QVariant::fromValue(p_" << property.propertyName << "));\n"; - headerStream << " }\n"; - } - headerStream << R"( - if (!m_config.loadRelaxed()) - return; - connect(config, &DTK_CORE_NAMESPACE::DConfig::valueChanged, this, [this](const QString &key) { - updateValue(key); - }, Qt::DirectConnection); - - m_status.storeRelaxed(static_cast(Status::Succeed)); - Q_EMIT configInitializeSucceed(config); - } - void updateValue(const QString &key, const QVariant &fallback = QVariant()) { - if (!m_config.loadRelaxed()) - return; - Q_ASSERT(QThread::currentThread() == m_config.loadRelaxed()->thread()); - const QVariant &value = m_config.loadRelaxed()->value(key, fallback); -)"; - for (int i = 0; i < properties.size(); ++i) { - const Property &property = properties.at(i); - headerStream << " if (key == " << property.propertyNameString << ") {\n"; - - headerStream << " markPropertySet(" << i << ", !m_config.loadRelaxed()->isDefaultValue(key));\n"; - - headerStream << " auto newValue = qvariant_cast<" << property.typeName << ">(value);\n" - << " QMetaObject::invokeMethod(this, [this, newValue, key, value]() {\n" - << " Q_ASSERT(QThread::currentThread() == this->thread());\n" - << " if (p_" << property.propertyName << " != newValue) {\n" - << " p_" << property.propertyName << " = newValue;\n" - << " Q_EMIT " << property.propertyName << "Changed();\n" - << " Q_EMIT valueChanged(key, value);\n" - << " }\n" - << " });\n" - << " return;\n" - << " }\n"; - } - headerStream << " }\n"; + headerStream << "\nprivate:\n Data *m_data;\n};\n\n"; - // Mark property as set - headerStream << " inline void markPropertySet(const int index, bool on = true) {\n"; - for (int i = 0; i <= (properties.size()) / 32; ++i) { - headerStream << " if (index < " << (i + 1) * 32 << ") {\n" - << " if (on)\n" - << " m_propertySetStatus" << QString::number(i) << ".fetchAndOrOrdered(1 << (index - " << i * 32 << "));\n" - << " else\n" - << " m_propertySetStatus" << QString::number(i) << ".fetchAndAndOrdered(~(1 << (index - " << i * 32 << ")));\n" - << " return;\n" - << " }\n"; - } - headerStream << " Q_UNREACHABLE();\n }\n"; + // ==================== TreelandUserConfigData Implementation ==================== + headerStream + << "inline void " << className << "Data::initializeInConfigThread(DTK_CORE_NAMESPACE::DConfig *config) {\n" + << " Q_ASSERT(!m_config.loadRelaxed());\n" + << " m_config.storeRelaxed(config);\n"; - // Test if property is set - headerStream << " inline bool testPropertySet(const int index) const {\n"; - for (int i = 0; i <= (properties.size()) / 32; ++i) { - headerStream << " if (index < " << (i + 1) * 32 << ") {\n"; - headerStream << " return (m_propertySetStatus" << QString::number(i) << ".loadRelaxed() & (1 << (index - " << i * 32 << ")));\n"; - headerStream << " }\n"; + for (int i = 0; i < properties.size(); ++i) { + const auto &p = properties[i]; + headerStream << " if (testPropertySet(" << i << ")) config->setValue(" << p.propertyNameString + << ", QVariant::fromValue(p_" << p.propertyName << "));\n" + << " else updateValue(" << p.propertyNameString << ", QVariant::fromValue(p_" << p.propertyName + << "));\n"; } - headerStream << " Q_UNREACHABLE();\n" - << " }\n"; - - // Member variables - headerStream << R"( - QAtomicPointer m_config = nullptr; -public: - enum class Status { - Invalid = 0, - Succeed = 1, - Failed = 2 - }; -private: - QAtomicInteger m_status = static_cast(Status::Invalid); - -)"; - - // Property variables - for (const Property &property : std::as_const(properties)) { - if (property.typeName == QLatin1String("int") || property.typeName == QLatin1String("qint64")) { - headerStream << " // Note: If you expect a double type, use XXX.0\n"; - } else if (property.typeName == QLatin1String("QString")) { - headerStream << " // Default value: \"" << property.defaultValue.toString().replace("\n", "\\n").replace("\r", "\\r") << "\"\n"; + headerStream << R"( // Transition from Initializing to Succeed + if (!m_status.testAndSetOrdered(static_cast(Status::Initializing), static_cast(Status::Succeed))) { + if (m_status.loadRelaxed() == static_cast(Status::Destroyed)) { + config->deleteLater(); + deleteLater(); } - headerStream << " " << property.typeName << " p_" << property.propertyName << " { "; - headerStream << jsonValueToCppCode(property.defaultValue) << " };\n"; + return; + }} + +inline void )" << className + << R"(Data::updateValue(const QString &key, const QVariant &fallback) { + auto config = m_config.loadRelaxed(); + if (!config) return; + const QVariant &v = config->value(key, fallback); +)"; + for (int i = 0; i < properties.size(); ++i) { + const auto &p = properties[i]; + headerStream << " if (key == " << p.propertyNameString << ") {\n" + << " markPropertySet(" << i << ", !config->isDefaultValue(key));\n" + << " // Safe capture using QPointer to handle object destruction\n" + << " QPointer<" << className << "Data> safeThis(this);\n" + << " QMetaObject::invokeMethod(this, [safeThis, key, v]() {\n" + << " if (safeThis) safeThis->updateProperty(key, v);\n" + << " });\n" + << " return;\n }\n"; } - - // Property set status variables - for (int i = 0; i <= (properties.size()) / 32; ++i) { - headerStream << " QAtomicInteger m_propertySetStatus" << QString::number(i) << " = 0;\n"; + headerStream << "}\n\ninline void " << className << "Data::updateProperty(const QString &key, const QVariant &v) {\n"; + for (const auto &p : properties) { + headerStream << " if (key == " << p.propertyNameString << ") {\n" + << " " << p.typeName << " nv = qvariant_cast<" << p.typeName << ">(v);\n" + << " if (p_" << p.propertyName << " != nv) {\n" + << " p_" << p.propertyName << " = nv;\n" + << " if (m_userConfig) {\n" + << " auto uc = m_userConfig;\n" + << " if (uc) {\n" + << " Q_EMIT uc.data()->" << p.propertyName << "Changed();\n" + << " Q_EMIT uc.data()->valueChanged(key, v);\n" + << " }\n" + << " }\n" + << " }\n" + << " return;\n }\n"; } - headerStream << "};\n\n"; - headerStream << "#endif // " << className.toUpper() << "_H\n"; + headerStream << "}\n\n#endif\n"; return 0; }