#include "lidl_gen_client.h" #include "lidl_emit_common.h" #include #include #include #include #include #include #include // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- static bool isRefType(const QString& qt) { if (qt == "QString" || qt == "QStringList" || qt == "QJsonArray" || qt == "QVariantList" || qt == "QVariantMap" || qt == "QByteArray") return true; // A record is a struct: pass it by const& too. Anything that is not a known // Qt scalar/handle spelling is a generated record type. return !(qt == "bool" || qt == "int" || qt == "double" || qt == "float" || qt == "void" || qt == "qlonglong" || qt == "qulonglong" || qt == "QVariant" || qt == "LogosResult"); } static void emitParam(QTextStream& s, const QString& qtType, const std::string& name) { if (isRefType(qtType)) s << "const " << qtType << "& " << name; else s << qtType << " " << name; } static bool lidlIsRecord(const TypeExpr& te); static QString qtToVariantExpr(const TypeExpr& te, const QString& expr); static QString qtFromVariantExpr(const TypeExpr& te, const QString& expr); static QString returnConversionFor(const TypeExpr& te, const QString& qt); // A field's `?T` as a TypeExpr, whichever of the two spellings the author used // (`? name: T` sets the flag and leaves the type T; `name: ?T` makes the type // an Optional). Building one shape here is what makes the two emit identical // code — the rule fieldIsOptional()/fieldValueType() exist to enforce. static TypeExpr fieldOptionalType(const FieldDecl& f) { if (f.type.kind == TypeExpr::Optional) return f.type; TypeExpr o; o.kind = TypeExpr::Optional; o.elements.push_back(f.type); return o; } // Does this slot need a generator-emitted ELEMENT LOOP rather than a whole-value // QVariant hop? // // Two reasons a slot can need one, and they are now the same question: // * it mentions a RECORD — a struct with no Q_DECLARE_METATYPE, so // QVariant::fromValue of it is a blob nothing can read back; // * its Qt spelling is a TYPED container or a std::optional — // QList, QMap, std::optional — // which QVariant handles even worse than a record: qvariantToNlohmann // matches a CLOSED userType() set and answers null for it, and // qvariant_cast back yields an EMPTY container. Silently, both ways. // // QStringList, QVariantList and QVariantMap are NOT in this set: they are in // that closed set and cross whole, exactly as they always did. static bool holdsRecordType(const TypeExpr& te) { return lidlIsRecord(te) || (te.kind == TypeExpr::Array && te.elements.size() == 1 && lidlIsRecord(te.elements[0])) || (te.kind == TypeExpr::Map && te.elements.size() == 2 && lidlIsRecord(te.elements[1])); } static bool needsElementLoop(const TypeExpr& te) { return holdsRecordType(te) || lidlQtNeedsElementLoop(te); } // Does any slot in the contract materialise a std::optional on this surface? // Gates the generated `#include `, so a contract with no optional (or // one whose only optionals are `?any`) keeps its header byte-for-byte. static bool typeUsesStdOptional(const TypeExpr& te) { if (te.kind == TypeExpr::Optional) return lidlQtNeedsElementLoop(te) || (!te.elements.empty() && typeUsesStdOptional(optionalValueType(te))); for (const TypeExpr& e : te.elements) if (typeUsesStdOptional(e)) return true; return false; } static bool moduleUsesStdOptional(const ModuleDecl& m) { for (const TypeDecl& t : m.types) for (const FieldDecl& f : t.fields) { // The field's EFFECTIVE type: the optional wrapper when either // spelling makes it optional, the written type otherwise. const TypeExpr eff = fieldIsOptional(f) ? fieldOptionalType(f) : f.type; if (typeUsesStdOptional(eff)) return true; } for (const MethodDecl& md : m.methods) { if (typeUsesStdOptional(md.returnType)) return true; for (const ParamDecl& p : md.params) if (typeUsesStdOptional(p.type)) return true; } for (const EventDecl& ed : m.events) for (const ParamDecl& p : ed.params) if (typeUsesStdOptional(p.type)) return true; return false; } static QString returnConversion(const QString& qt) { if (qt == "bool") return "return _result.toBool();"; // 64-bit, matching lidlTypeToQt: toInt() truncated a LIDL int/uint, and for // uint it also read the value as signed. if (qt == "qlonglong") return "return _result.toLongLong();"; if (qt == "qulonglong") return "return _result.toULongLong();"; if (qt == "double") return "return _result.toDouble();"; if (qt == "float") return "return _result.toFloat();"; if (qt == "QString") return "return _result.toString();"; if (qt == "QStringList") return "return _result.toStringList();"; if (qt == "QJsonArray") return "return qvariant_cast(_result);"; if (qt == "QVariantList") return "return _result.toList();"; if (qt == "QVariantMap") return "return _result.toMap();"; if (qt == "LogosResult") return "return _result.value();"; return "return _result;"; } // Records, containers holding them, and every TYPED container / optional decode // through a generated element loop; everything else keeps the historical // QVariant accessor. `[tstr]` and `[any]` stay on the accessor: QStringList and // QVariantList are QVariant-native, so `.toStringList()` / `.toList()` is both // correct and what shipped. static QString returnConversionFor(const TypeExpr& te, const QString& qt) { if (needsElementLoop(te)) return "return " + qtFromVariantExpr(te, "_result") + ";"; return returnConversion(qt); } // The async twin of returnConversionFor: `v` is the wire QVariant. // // A record-bearing return MUST decode field by field here too. The wire carries // a QVariantMap and no Q_DECLARE_METATYPE is emitted for the struct, so // `qvariant_cast(v)` does not fail — it silently returns a // DEFAULT-CONSTRUCTED Status, and the caller sees empty fields with no // diagnostic. That is the worst failure mode available: the sync path is // correct, so the same call is right or wrong depending only on which overload // the caller reached for. static QString asyncReturnConversionFor(const TypeExpr& te, const QString& qt) { if (needsElementLoop(te)) return qtFromVariantExpr(te, "v"); return "qvariant_cast<" + qt + ">(v)"; } static QString asyncDefaultVal(const QString& qt) { if (qt == "bool") return "false"; if (qt == "int" || qt == "double" || qt == "float") return "0"; if (qt == "QString") return "QString()"; if (qt == "QStringList") return "QStringList()"; if (qt == "QJsonArray") return "QJsonArray()"; if (qt == "QVariantList") return "QVariantList()"; if (qt == "QVariantMap") return "QVariantMap()"; return qt + "{}"; } // --------------------------------------------------------------------------- // Records // // A `type Foo { … }` in the contract becomes a real C++ struct plus two inline // conversions, so a Qt consumer says `Status s = client.makeStatus();` instead // of digging fields out of a QVariantMap. One LIDL type, one type per language. // // bstr fields are QByteArray on purpose: logos-protocol's QVariant<->JSON // conversion already materialises the canonical {"_bytes": base64url} form as a // QByteArray and back (logos_json_convert.cpp), so the record conversions stay // pure field mapping and binary survives at any depth for free. // --------------------------------------------------------------------------- static bool lidlIsRecord(const TypeExpr& te) { return te.kind == TypeExpr::Named && !te.name.empty(); } // value expression of the Qt type -> QVariant // // The loop is emitted whenever the surface type is not QVariant-native — for a // record (a struct with no metatype) and now equally for every TYPED container // and optional. `QVariant::fromValue(QList)` is not a compile // error and not a runtime warning; it produces a QVariant that // qvariantToNlohmann answers `null` for, because that function matches a CLOSED // userType() set. So the whole value must never cross — only its elements, one // at a time, each of which IS in that set. static QString qtToVariantExpr(const TypeExpr& te, const QString& expr) { if (lidlIsRecord(te)) return qs(te.name) + "ToVariant(" + expr + ")"; // THE SOURCE IS A LAMBDA PARAMETER, never a local bound inside the body. // These loops nest — `[[uint]]` puts one inside another — and every level // wants the same short names, so a body-local (or a range-for over a name // the loop itself declares) would be self-referential: it compiles, and it // reads uninitialised memory. An ARGUMENT is evaluated in the ENCLOSING // scope, before the inner names exist. if (te.kind == TypeExpr::Array && te.elements.size() == 1 && needsElementLoop(te)) { return "[&](const auto& __c){ QVariantList __l; for (const auto& __e : __c) __l.append(" + qtToVariantExpr(te.elements[0], "__e") + "); return QVariant(__l); }(" + expr + ")"; } if (te.kind == TypeExpr::Map && te.elements.size() == 2 && needsElementLoop(te)) { return "[&](const auto& __c){ QVariantMap __m; for (auto __it = __c.begin(); " "__it != __c.end(); ++__it) __m.insert(__it.key(), " + qtToVariantExpr(te.elements[1], "__it.value()") + "); return QVariant(__m); }(" + expr + ")"; } // `?T` -> std::optional: EMPTY is the invalid QVariant, which is Qt's // single empty inhabitant and what the wire's `null` becomes. `?any` never // reaches here (it is still spelled QVariant, so needsElementLoop is false) // and rides the fromValue below unchanged. if (te.kind == TypeExpr::Optional && needsElementLoop(te)) { const TypeExpr& v = optionalValueType(te); return "[&](const auto& __c){ return __c.has_value() ? " + qtToVariantExpr(v, "*__c") + " : QVariant(); }(" + expr + ")"; } return "QVariant::fromValue(" + expr + ")"; } // QVariant expression -> value of the Qt type static QString qtFromVariantExpr(const TypeExpr& te, const QString& expr) { if (lidlIsRecord(te)) return qs(te.name) + "FromVariant(" + expr + ")"; if (te.kind == TypeExpr::Primitive) { const QString n = qs(te.name); if (n == "tstr") return expr + ".toString()"; if (n == "bstr") return expr + ".toByteArray()"; if (n == "int") return expr + ".toLongLong()"; if (n == "uint") return expr + ".toULongLong()"; if (n == "float64") return expr + ".toDouble()"; if (n == "bool") return expr + ".toBool()"; } // Source as a lambda PARAMETER, for the reason given on the encode side. if (te.kind == TypeExpr::Array && te.elements.size() == 1) { const TypeExpr& e = te.elements[0]; return "[&](const QVariant& __s){ " + lidlTypeToQt(te) + " __acc; for (const QVariant& __e : __s.toList()) __acc.append(" + qtFromVariantExpr(e, "__e") + "); return __acc; }(" + expr + ")"; } if (te.kind == TypeExpr::Map && te.elements.size() == 2) { const TypeExpr& v = te.elements[1]; return "[&](const QVariant& __s){ " + lidlTypeToQt(te) + " __acc; const QVariantMap __mm = __s.toMap(); " "for (auto __it = __mm.begin(); __it != __mm.end(); ++__it) __acc.insert(" + "__it.key(), " + qtFromVariantExpr(v, "__it.value()") + "); return __acc; }(" + expr + ")"; } // `?T`: an invalid (or null) QVariant is the empty state — absent and // explicit-null are the SAME state, as the two-state rule requires — and // anything else is a present T decoded by this same table. if (te.kind == TypeExpr::Optional && needsElementLoop(te)) { const TypeExpr& v = optionalValueType(te); const QString opt = lidlTypeToQt(te); return "[&](const QVariant& __s){ if (!__s.isValid() || __s.isNull()) return " + opt + "(); return " + opt + "(" + qtFromVariantExpr(v, "__s") + "); }(" + expr + ")"; } return expr; } // A method argument as passed to packVariantList: records convert, everything // else goes through unchanged (packVariantList wraps with QVariant::fromValue). static QString qtArgExpr(const TypeExpr& te, const QString& name) { return needsElementLoop(te) ? qtToVariantExpr(te, name) : name; } // A record field's Qt type, honouring BOTH optionality spellings. // // `?T` is std::optional, the same answer every other slot gets — a Qt // consumer's `Profile.nickname` is now a std::optional rather than a // QVariant it has to guess the payload type of, which is what the std surface // next door has always given (Codec>). `?any` stays QVariant: // `any` is the one row the widened table keeps untyped, and QVariant already // has exactly one empty inhabitant, so wrapping it would spell EMPTY twice and // make a two-state slot three-state. // // Routing through fieldIsOptional()/fieldOptionalType() is what makes the two // spellings identical: reading `f.type` alone made the flag spelling emit a // bare `T` (which cannot be empty at all) while the type spelling emitted an // optional, from one contract. static QString lidlFieldTypeQt(const FieldDecl& f) { return fieldIsOptional(f) ? lidlTypeToQt(fieldOptionalType(f)) : lidlTypeToQt(f.type); } static void emitRecords(QTextStream& s, const ModuleDecl& module) { if (module.types.empty()) return; for (const TypeDecl& t : module.types) { const QString n = qs(t.name); s << "/// `" << n << "` — a record declared by the `" << qs(module.name) << "` contract.\n"; s << "struct " << n << " {\n"; for (const FieldDecl& f : t.fields) s << " " << lidlFieldTypeQt(f) << " " << qs(f.name) << "{};\n"; s << "};\n\n"; } // Conversions come after ALL structs so records may reference each other. for (const TypeDecl& t : module.types) { const QString n = qs(t.name); s << "inline QVariant " << n << "ToVariant(const " << n << "& v)\n{\n"; s << " QVariantMap __m;\n"; for (const FieldDecl& f : t.fields) { if (fieldIsOptional(f)) { // A record field is a NAMED slot: empty is spelled by OMITTING // the key, not by inserting an empty value. Same rule the // cdylib record codec follows, on the other surface. // // The emptiness TEST follows the field's own spelling — // `.has_value()` for a std::optional, `.isValid()` for the // `?any` slot that stays a QVariant — because those are the two // types this surface can produce for an optional field. const QString fv = "v." + qs(f.name); const TypeExpr ot = fieldOptionalType(f); if (lidlQtNeedsElementLoop(ot)) { s << " if (" << fv << ".has_value())\n"; s << " __m.insert(\"" << qs(f.name) << "\", " << qtToVariantExpr(fieldValueType(f), "*" + fv) << ");\n"; } else { s << " if (" << fv << ".isValid())\n"; s << " __m.insert(\"" << qs(f.name) << "\", " << fv << ");\n"; } continue; } s << " __m.insert(\"" << qs(f.name) << "\", " << qtToVariantExpr(f.type, "v." + qs(f.name)) << ");\n"; } s << " return QVariant(__m);\n}\n\n"; s << "inline " << n << " " << n << "FromVariant(const QVariant& value)\n{\n"; s << " const QVariantMap __m = value.toMap();\n"; s << " " << n << " __out;\n"; for (const FieldDecl& f : t.fields) { if (fieldIsOptional(f)) { // Absent and null both arrive as an invalid QVariant — the same // state, as the contract requires — and the optional decode // below turns exactly that into the empty optional. A bare // conversion (`.toString()` on a flag-optional `tstr`) would // have turned "empty" into "", which is a VALUE. s << " __out." << qs(f.name) << " = " << qtFromVariantExpr(fieldOptionalType(f), "__m.value(\"" + qs(f.name) + "\")") << ";\n"; continue; } s << " __out." << qs(f.name) << " = " << qtFromVariantExpr(f.type, "__m.value(\"" + qs(f.name) + "\")") << ";\n"; } s << " return __out;\n}\n\n"; } } // --------------------------------------------------------------------------- // Header generation // --------------------------------------------------------------------------- QString lidlMakeHeader(const ModuleDecl& module, BindMode bindMode) { QString className = lidlToPascalCase(qs(module.name)); QString h; QTextStream s(&h); s << "#pragma once\n"; s << "#include \n"; s << "#include \n"; s << "#include \n"; s << "#include \n"; s << "#include \n"; s << "#include \n"; s << "#include \n"; s << "#include \n"; if (moduleUsesStdOptional(module)) s << "#include \n"; s << "#include \"logos_types.h\"\n"; s << "#include \"logos_api.h\"\n"; s << "#include \"logos_api_client.h\"\n"; s << "#include \"logos_call_error.h\"\n"; s << "#include \"logos_async_result.h\"\n"; s << "#include \"logos_object.h\"\n\n"; emitRecords(s, module); s << "class " << className << " {\n"; s << "public:\n"; if (bindMode == BindMode::Bound) s << " explicit " << className << "(LogosAPI* api, const QString& moduleName);\n\n"; else s << " explicit " << className << "(LogosAPI* api);\n\n"; s << " using RawEventCallback = std::function;\n"; s << " using EventCallback = std::function;\n\n"; s << " bool on(const QString& eventName, RawEventCallback callback);\n"; s << " bool on(const QString& eventName, EventCallback callback);\n"; for (const MethodDecl& md : module.methods) { QString ret = lidlTypeToQt(md.returnType); s << " " << ret << " " << md.name << "("; for (int i = 0; i < md.params.size(); ++i) { emitParam(s, lidlTypeToQt(md.params[i].type), md.params[i].name); if (i + 1 < md.params.size()) s << ", "; } // Optional error out-channel: pass a logos::CallError* to distinguish // a failed remote call from a legitimately default-valued result — // followed by an optional Timeout. Both trailing and defaulted, so // existing call sites (including ones passing `&err` positionally) // compile unchanged. Mirrors the legacy emitter in // generator_lib.cpp; the two must agree, since a consumer can // reach either (this one from a published `.lidl`, that one through the // module builder) for the same contract. if (!md.params.empty()) s << ", "; s << "logos::CallError* err = nullptr, Timeout timeout = Timeout());\n"; auto emitAsyncParams = [&]() { for (int i = 0; i < md.params.size(); ++i) { emitParam(s, lidlTypeToQt(md.params[i].type), md.params[i].name); if (i + 1 < md.params.size()) s << ", "; } if (!md.params.empty()) s << ", "; }; QString asyncCb = (ret == "void") ? QString("std::function") : QString("std::function"; s << " void " << md.name << "Async("; emitAsyncParams(); s << asyncCb << " callback, Timeout timeout = Timeout());\n"; // Result-carrying async entry point. Distinct name, not an overload: // std::function)> alongside std::function // is ambiguous for a generic lambda. s << " void " << md.name << "AsyncResult("; emitAsyncParams(); s << "std::function)> callback" << ", Timeout timeout = Timeout());\n"; } s << "\nprivate:\n"; s << " template\n"; s << " static QVariantList packVariantList(Args&&... args) {\n"; s << " QVariantList list;\n"; s << " list.reserve(sizeof...(Args));\n"; s << " using Expander = int[];\n"; s << " (void)Expander{0, (list.append(QVariant::fromValue(std::forward(args))), 0)...};\n"; s << " return list;\n"; s << " }\n"; s << " LogosAPI* m_api;\n"; s << " LogosAPIClient* m_client;\n"; s << " QString m_moduleName;\n"; s << "};\n"; return h; } // --------------------------------------------------------------------------- // Source generation // --------------------------------------------------------------------------- QString lidlMakeSource(const ModuleDecl& module, BindMode bindMode) { QString className = lidlToPascalCase(qs(module.name)); QString headerRel = qs(module.name) + "_api.h"; QString c; QTextStream s(&c); s << "#include \"" << headerRel << "\"\n\n"; s << "#include \n\n"; // Target expression for every remote call: a baked literal in Static // mode, the runtime m_moduleName member in Bound (interface) mode. const QString targetExpr = (bindMode == BindMode::Bound) ? QStringLiteral("m_moduleName") : (QStringLiteral("\"") + qs(module.name) + QStringLiteral("\"")); if (bindMode == BindMode::Bound) s << className << "::" << className << "(LogosAPI* api, const QString& moduleName) : m_api(api), m_client(api->getClient(moduleName)), m_moduleName(moduleName) {}\n\n"; else s << className << "::" << className << "(LogosAPI* api) : m_api(api), m_client(api->getClient(\"" << module.name << "\")), m_moduleName(QStringLiteral(\"" << module.name << "\")) {}\n\n"; s << "bool " << className << "::on(const QString& eventName, RawEventCallback callback) {\n"; s << " if (!callback) { qWarning() << \"" << className << ": ignoring empty event callback for\" << eventName; return false; }\n"; // Deferred: the module is usually NOT reachable at the moment a consumer // subscribes (init(), onContextReady()), and acquiring a replica there used // to block and then fail permanently. onEventWhenAvailable arms it when the // module appears. The return is ACCEPTED, not live. s << " return m_client->onEventWhenAvailable(m_moduleName, eventName, callback) != 0;\n"; s << "}\n\n"; s << "bool " << className << "::on(const QString& eventName, EventCallback callback) {\n"; s << " if (!callback) { qWarning() << \"" << className << ": ignoring empty event callback for\" << eventName; return false; }\n"; s << " return on(eventName, [callback](const QString&, const QVariantList& data) { callback(data); });\n"; s << "}\n\n"; for (const MethodDecl& md : module.methods) { QString ret = lidlTypeToQt(md.returnType); int nParams = md.params.size(); s << ret << " " << className << "::" << md.name << "("; for (int i = 0; i < nParams; ++i) { emitParam(s, lidlTypeToQt(md.params[i].type), md.params[i].name); if (i + 1 < nParams) s << ", "; } if (nParams > 0) s << ", "; s << "logos::CallError* err, Timeout timeout) {\n"; // Call through the err-out overload: with a logos::CallError* the // caller can distinguish a failed remote call from a legitimately // default-valued result; without it the historical default-on-failure // behavior is kept, plus a warning in the module log. s << " logos::CallError _err;\n"; if (ret != "void") s << " QVariant _result = "; else s << " "; // Pack each argument as ONE element via packVariantList (which wraps // with QVariant::fromValue). A braced `QVariantList{v}` or `<< v` would // CONCATENATE a QVariantList-typed arg (any `[T]` list) into the args // list, sending a 3-element [1,2,3] as three positional args instead of // one — the historical "typed arrays empty over the Qt path" bug. s << "m_client->invokeRemoteMethod(" << targetExpr << ", \"" << md.name << "\", packVariantList("; for (int i = 0; i < nParams; ++i) { s << qtArgExpr(md.params[i].type, qs(md.params[i].name)); if (i + 1 < nParams) s << ", "; } // The caller's deadline, not a hard-coded default: this is the overload // that carries BOTH the deadline and the error out-channel. s << "), timeout, &_err);\n"; s << " if (err) *err = _err;\n"; s << " else if (!_err.ok()) qWarning() << \"" << className << "::" << md.name << ": remote call failed:\" << QString::fromStdString(_err.message);\n"; if (ret != "void") s << " " << returnConversionFor(md.returnType, ret) << "\n"; s << "}\n\n"; // Shared between the two async entry points so they cannot drift in how // they marshal args or decode the reply. auto emitAsyncParams = [&]() { for (int i = 0; i < nParams; ++i) { emitParam(s, lidlTypeToQt(md.params[i].type), md.params[i].name); if (i + 1 < nParams) s << ", "; } if (nParams > 0) s << ", "; }; // Same one-element-per-arg packing as the sync path (see above): a // QVariantList-typed arg must not be spread across the args list. auto emitAsyncArgs = [&]() { s << "packVariantList("; for (int i = 0; i < nParams; ++i) { s << qtArgExpr(md.params[i].type, qs(md.params[i].name)); if (i + 1 < nParams) s << ", "; } s << ")"; }; // The QVariant -> typed-return expression, given the QVariant's name. auto asyncDecodeExpr = [&](const QString& var) -> QString { if (ret == "void") return QString(); if (ret == "QVariant") return var; return var + ".isValid() ? " + asyncReturnConversionFor(md.returnType, ret) + " : " + asyncDefaultVal(ret); }; s << "void " << className << "::" << md.name << "Async("; emitAsyncParams(); s << "std::function callback, Timeout timeout) {\n"; s << " if (!callback) return;\n"; s << " m_client->invokeRemoteMethodAsync(" << targetExpr << ", \"" << md.name << "\", "; emitAsyncArgs(); // ONE-argument lambda -> LogosAPIClient::AsyncResultCallback, i.e. the // historical value-only transport overload. s << ", [callback](QVariant v) {\n"; if (ret == "void") s << " callback();\n"; else s << " callback(" << asyncDecodeExpr("v") << ");\n"; s << " }, timeout);\n"; s << "}\n\n"; // Result-carrying async: a TWO-argument lambda, so it binds to the // transport's CallError-aware AsyncResultErrorCallback overload. The // value on failure is exactly what `Async` would have delivered; // what changes is that the callback can now tell. s << "void " << className << "::" << md.name << "AsyncResult("; emitAsyncParams(); s << "std::function)> callback, Timeout timeout) {\n"; s << " if (!callback) return;\n"; s << " m_client->invokeRemoteMethodAsync(" << targetExpr << ", \"" << md.name << "\", "; emitAsyncArgs(); s << ", [callback](QVariant v, const logos::CallError& _err) {\n"; s << " logos::AsyncResult<" << ret << "> _r;\n"; s << " _r.error = _err;\n"; if (ret == "void") s << " (void)v;\n"; else s << " _r.value = " << asyncDecodeExpr("v") << ";\n"; s << " callback(_r);\n"; s << " }, timeout);\n"; s << "}\n\n"; } return c; } // --------------------------------------------------------------------------- // metadata.json // --------------------------------------------------------------------------- QString lidlGenerateMetadataJson(const ModuleDecl& module) { QJsonObject obj; obj["name"] = qs(module.name); obj["version"] = module.version.empty() ? QStringLiteral("0.0.0") : qs(module.version); obj["type"] = "core"; obj["category"] = module.category.empty() ? QStringLiteral("general") : qs(module.category); obj["description"] = qs(module.description); obj["main"] = qs(module.name) + "_plugin"; QJsonArray deps; for (const std::string& d : module.depends) deps.append(qs(d)); obj["dependencies"] = deps; QJsonDocument doc(obj); return doc.toJson(QJsonDocument::Indented); } // --------------------------------------------------------------------------- // Full pipeline (from .lidl file) // --------------------------------------------------------------------------- int lidlGenerateClientStubs(const QString& lidlPath, const QString& outputDir, bool moduleOnly, QTextStream& out, QTextStream& err) { QFileInfo fi(lidlPath); if (!fi.exists()) { err << "LIDL file does not exist: " << lidlPath << "\n"; return 2; } QFile file(fi.canonicalFilePath().isEmpty() ? fi.absoluteFilePath() : fi.canonicalFilePath()); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { err << "Failed to open LIDL file: " << lidlPath << "\n"; return 3; } QString source = QString::fromUtf8(file.readAll()); file.close(); LidlParseResult pr = lidlParse(source); if (pr.hasError()) { err << lidlPath << ":" << pr.errorLine << ":" << pr.errorColumn << ": " << pr.error << "\n"; return 4; } LidlValidationResult vr = lidlValidate(pr.module); if (vr.hasErrors()) { for (const std::string& e : vr.errors) err << lidlPath << ": " << e << "\n"; return 5; } { QString recErr; if (!lidlCheckRecords(pr.module, &recErr)) { err << lidlPath << ": " << recErr << "\n"; return 5; } } const ModuleDecl& mod = pr.module; QString genDirPath = outputDir.isEmpty() ? QDir::current().filePath("logos-cpp-sdk/cpp/generated") : outputDir; QDir().mkpath(genDirPath); QString headerAbs = QDir(genDirPath).filePath(qs(mod.name) + "_api.h"); QString sourceAbs = QDir(genDirPath).filePath(qs(mod.name) + "_api.cpp"); { QFile f(headerAbs); if (!f.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text)) { err << "Failed to write: " << headerAbs << "\n"; return 6; } f.write(lidlMakeHeader(mod).toUtf8()); } { QFile f(sourceAbs); if (!f.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text)) { err << "Failed to write: " << sourceAbs << "\n"; return 7; } f.write(lidlMakeSource(mod).toUtf8()); } { QString metaPath = QDir(genDirPath).filePath("metadata.json"); QFile f(metaPath); if (!f.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text)) { err << "Failed to write: " << metaPath << "\n"; return 8; } f.write(lidlGenerateMetadataJson(mod).toUtf8()); } out << "Generated: " << headerAbs << " and " << sourceAbs << "\n"; if (!moduleOnly) { QDir genDir(genDirPath); QStringList headers = genDir.entryList(QStringList() << "*_api.h", QDir::Files | QDir::Readable); { QString content; QTextStream ss(&content); ss << "#pragma once\n#include \"logos_api.h\"\n#include \"logos_api_client.h\"\n\n"; for (const QString& h : headers) ss << "#include \"" << h << "\"\n"; ss << "\nstruct LogosModules {\n explicit LogosModules(LogosAPI* api) : api(api)"; for (const QString& h : headers) { QString base = h; base.chop(6); ss << ", \n " << base << "(api)"; } ss << " {}\n LogosAPI* api;\n"; for (const QString& h : headers) { QString base = h; base.chop(6); ss << " " << lidlToPascalCase(base) << " " << base << ";\n"; } ss << "};\n"; QFile f(genDir.filePath("logos_sdk.h")); if (!f.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text)) { err << "Failed to write umbrella\n"; return 9; } f.write(content.toUtf8()); } { QStringList sources = genDir.entryList(QStringList() << "*_api.cpp", QDir::Files | QDir::Readable); QString content; QTextStream ss(&content); ss << "#include \"logos_sdk.h\"\n\n"; for (const QString& c : sources) ss << "#include \"" << c << "\"\n"; ss << "\n"; QFile f(genDir.filePath("logos_sdk.cpp")); if (!f.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text)) { err << "Failed to write umbrella\n"; return 10; } f.write(content.toUtf8()); } out << "Generated: logos_sdk.h and logos_sdk.cpp\n"; } out << "Generated: metadata.json\n"; out.flush(); return 0; }