diff --git a/cpp-generator/experimental/lidl_emit_common.cpp b/cpp-generator/experimental/lidl_emit_common.cpp index 14c4e28..77aa918 100644 --- a/cpp-generator/experimental/lidl_emit_common.cpp +++ b/cpp-generator/experimental/lidl_emit_common.cpp @@ -33,19 +33,28 @@ QString lidlTypeToQt(const TypeExpr& te) if (te.name == "result") return "LogosResult"; if (te.name == "any") return "QVariant"; return "QVariant"; + case TypeExpr::Named: + // A record declared by the contract: its generated struct. One LIDL + // type, one type per language — a record is not a QVariant blob. + return QString::fromStdString(te.name); case TypeExpr::Array: if (te.elements.size() == 1 && te.elements[0].kind == TypeExpr::Primitive && te.elements[0].name == "tstr") { return "QStringList"; } + // A list of records is a typed list: QVariantList could not hold a + // record without Q_DECLARE_METATYPE, and the point of a record is that + // the consumer gets the struct. + if (te.elements.size() == 1 && te.elements[0].kind == TypeExpr::Named) + return "QList<" + QString::fromStdString(te.elements[0].name) + ">"; return "QVariantList"; case TypeExpr::Map: + if (te.elements.size() == 2 && te.elements[1].kind == TypeExpr::Named) + return "QMap"; return "QVariantMap"; case TypeExpr::Optional: return "QVariant"; - case TypeExpr::Named: - return "QVariant"; } return "QVariant"; } diff --git a/cpp-generator/experimental/lidl_gen_client.cpp b/cpp-generator/experimental/lidl_gen_client.cpp index 5a3f9cd..6985a0e 100644 --- a/cpp-generator/experimental/lidl_gen_client.cpp +++ b/cpp-generator/experimental/lidl_gen_client.cpp @@ -17,8 +17,14 @@ static bool isRefType(const QString& qt) { - return qt == "QString" || qt == "QStringList" || qt == "QJsonArray" - || qt == "QVariantList" || qt == "QVariantMap" || qt == "QByteArray"; + 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) @@ -29,6 +35,11 @@ static void emitParam(QTextStream& s, const QString& qtType, const std::string& 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); + static QString returnConversion(const QString& qt) { if (qt == "bool") return "return _result.toBool();"; @@ -47,6 +58,19 @@ static QString returnConversion(const QString& qt) return "return _result;"; } +// Records (and containers holding them) decode through the generated +// conversions; everything else keeps the historical QVariant accessor. +static QString returnConversionFor(const TypeExpr& te, const QString& qt) +{ + const bool holdsRecord = + 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])); + if (holdsRecord) + return "return " + qtFromVariantExpr(te, "_result") + ";"; + return returnConversion(qt); +} + static QString asyncDefaultVal(const QString& qt) { if (qt == "bool") return "false"; @@ -59,6 +83,114 @@ static QString asyncDefaultVal(const QString& qt) 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 +static QString qtToVariantExpr(const TypeExpr& te, const QString& expr) +{ + if (lidlIsRecord(te)) + return qs(te.name) + "ToVariant(" + expr + ")"; + if (te.kind == TypeExpr::Array && te.elements.size() == 1 + && (lidlIsRecord(te.elements[0]) || te.elements[0].kind != TypeExpr::Primitive)) { + return "[&]{ QVariantList __l; for (const auto& __e : " + expr + ") __l.append(" + + qtToVariantExpr(te.elements[0], "__e") + "); return QVariant(__l); }()"; + } + if (te.kind == TypeExpr::Map && te.elements.size() == 2 + && (lidlIsRecord(te.elements[1]) || te.elements[1].kind != TypeExpr::Primitive)) { + return "[&]{ QVariantMap __m; for (auto __it = " + expr + ".begin(); __it != " + expr + + ".end(); ++__it) __m.insert(__it.key(), " + + qtToVariantExpr(te.elements[1], "__it.value()") + "); return QVariant(__m); }()"; + } + 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()"; + } + if (te.kind == TypeExpr::Array && te.elements.size() == 1) { + const TypeExpr& e = te.elements[0]; + return "[&]{ " + lidlTypeToQt(te) + " __acc; for (const QVariant& __e : " + expr + + ".toList()) __acc.append(" + qtFromVariantExpr(e, "__e") + "); return __acc; }()"; + } + if (te.kind == TypeExpr::Map && te.elements.size() == 2) { + const TypeExpr& v = te.elements[1]; + return "[&]{ " + lidlTypeToQt(te) + " __acc; const QVariantMap __mm = " + expr + + ".toMap(); for (auto __it = __mm.begin(); __it != __mm.end(); ++__it) __acc.insert(" + + "__it.key(), " + qtFromVariantExpr(v, "__it.value()") + "); return __acc; }()"; + } + 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) +{ + const bool holdsRecord = + 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])); + return holdsRecord ? qtToVariantExpr(te, name) : name; +} + +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 << " " << lidlTypeToQt(f.type) << " " << 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) + 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) + s << " __out." << qs(f.name) << " = " + << qtFromVariantExpr(f.type, "__m.value(\"" + qs(f.name) + "\")") << ";\n"; + s << " return __out;\n}\n\n"; + } +} + // --------------------------------------------------------------------------- // Header generation // --------------------------------------------------------------------------- @@ -84,6 +216,8 @@ QString lidlMakeHeader(const ModuleDecl& module, BindMode bindMode) s << "#include \"logos_call_error.h\"\n"; s << "#include \"logos_object.h\"\n\n"; + emitRecords(s, module); + s << "class " << className << " {\n"; s << "public:\n"; if (bindMode == BindMode::Bound) @@ -243,7 +377,7 @@ QString lidlMakeSource(const ModuleDecl& module, BindMode bindMode) // 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 << md.params[i].name; + s << qtArgExpr(md.params[i].type, qs(md.params[i].name)); if (i + 1 < nParams) s << ", "; } s << "), Timeout(), &_err);\n"; @@ -252,7 +386,7 @@ QString lidlMakeSource(const ModuleDecl& module, BindMode bindMode) << ": remote call failed:\" << QString::fromStdString(_err.message);\n"; if (ret != "void") - s << " " << returnConversion(ret) << "\n"; + s << " " << returnConversionFor(md.returnType, ret) << "\n"; s << "}\n\n"; s << "void " << className << "::" << md.name << "Async("; @@ -267,7 +401,7 @@ QString lidlMakeSource(const ModuleDecl& module, BindMode bindMode) // QVariantList-typed arg must not be spread across the args list. s << " m_client->invokeRemoteMethodAsync(" << targetExpr << ", \"" << md.name << "\", packVariantList("; for (int i = 0; i < nParams; ++i) { - s << md.params[i].name; + s << qtArgExpr(md.params[i].type, qs(md.params[i].name)); if (i + 1 < nParams) s << ", "; } s << ")"; diff --git a/tests/experimental/test_lidl_gen_client.cpp b/tests/experimental/test_lidl_gen_client.cpp index d33dd67..caba2b0 100644 --- a/tests/experimental/test_lidl_gen_client.cpp +++ b/tests/experimental/test_lidl_gen_client.cpp @@ -250,3 +250,61 @@ TEST(LidlGenClient, VoidReturnMethod) // The source should just call the method without capturing return EXPECT_FALSE(s.contains("QVariant _result = m_client->invokeRemoteMethod(\"test\", \"doStuff\"")); } + +// Records: a `type` decl becomes a real C++ struct in the generated header, so a +// Qt consumer says `Status s = client.makeStatus();` rather than digging fields +// out of a QVariantMap. Additive — nothing generated records before this. +static ModuleDecl makeRecordModule() +{ + ModuleDecl m; + m.name = "info_module"; + m.version = "1.0.0"; + + TypeDecl rec; + rec.name = "Status"; + FieldDecl a; a.name = "port"; a.type = { TypeExpr::Primitive, "uint", {} }; + FieldDecl b; b.name = "blob"; b.type = { TypeExpr::Primitive, "bstr", {} }; + rec.fields = {a, b}; + m.types.push_back(rec); + + { + MethodDecl md; + md.name = "describeStatus"; + md.returnType = { TypeExpr::Primitive, "tstr", {} }; + ParamDecl p; p.name = "s"; p.type = { TypeExpr::Named, "Status", {} }; + md.params.push_back(p); + m.methods.push_back(md); + } + { + MethodDecl md; + md.name = "makeStatuses"; + TypeExpr elem = { TypeExpr::Named, "Status", {} }; + md.returnType = { TypeExpr::Array, "", { elem } }; + m.methods.push_back(md); + } + return m; +} + +TEST(LidlGenClient, RecordsBecomeStructsWithConversions) +{ + const QString h = lidlMakeHeader(makeRecordModule(), BindMode::Bound); + + // The struct, at the 1-1 Qt spellings: 64-bit unsigned, QByteArray for bytes. + EXPECT_TRUE(h.contains("struct Status {")) << h.toStdString(); + EXPECT_TRUE(h.contains("qulonglong port{};")) << h.toStdString(); + EXPECT_TRUE(h.contains("QByteArray blob{};")) << h.toStdString(); + + // Conversions both ways. + EXPECT_TRUE(h.contains("inline QVariant StatusToVariant(const Status& v)")) << h.toStdString(); + EXPECT_TRUE(h.contains("inline Status StatusFromVariant(const QVariant& value)")) << h.toStdString(); + // A bstr field is a QByteArray: logos-protocol's QVariant<->JSON conversion + // already materialises the tagged {"_bytes":…} form as QByteArray, so binary + // survives without record-specific bytes handling. + EXPECT_TRUE(h.contains("__out.blob = __m.value(\"blob\").toByteArray();")) << h.toStdString(); + + // Methods speak the record: by const& in, typed list out. A QVariantList + // could not hold a Status without Q_DECLARE_METATYPE. + EXPECT_TRUE(h.contains("describeStatus(const Status& s")) << h.toStdString(); + EXPECT_TRUE(h.contains("QList makeStatuses(")) << h.toStdString(); +} + diff --git a/tests/experimental/test_lidl_type_mapping.cpp b/tests/experimental/test_lidl_type_mapping.cpp index ddf4b3f..43053db 100644 --- a/tests/experimental/test_lidl_type_mapping.cpp +++ b/tests/experimental/test_lidl_type_mapping.cpp @@ -86,10 +86,13 @@ TEST(LidlTypeToQt, OptionalType) EXPECT_EQ(lidlTypeToQt(te), "QVariant"); } -TEST(LidlTypeToQt, NamedType) +// A Named type is a RECORD declared by the contract, and the client generator +// emits a struct of that name — so the Qt spelling is the struct, not an opaque +// QVariant. One LIDL type, one type per language. +TEST(LidlTypeToQt, NamedTypeIsItsRecordStruct) { TypeExpr te = { TypeExpr::Named, "MyStruct", {} }; - EXPECT_EQ(lidlTypeToQt(te), "QVariant"); + EXPECT_EQ(lidlTypeToQt(te), "MyStruct"); } // ---------------------------------------------------------------------------