diff --git a/cpp-generator/experimental/lidl_compat.h b/cpp-generator/experimental/lidl_compat.h index 9e0372c..f702c7b 100644 --- a/cpp-generator/experimental/lidl_compat.h +++ b/cpp-generator/experimental/lidl_compat.h @@ -10,6 +10,7 @@ // gen_cdylib) stay here — they are the C++/Qt-specific parts. #include "lidl/ast.hpp" +#include "lidl/identity.hpp" #include "lidl/parser.hpp" #include "lidl/serializer.hpp" #include "lidl/validator.hpp" @@ -71,6 +72,27 @@ inline lidl::ValidationResult lidlValidate(const ModuleDecl& module) return lidl::validate(module); } +// Add the derived module identity methods — name() and version() — to a +// ModuleDecl that is about to have CODE emitted from it. Returns false and +// fills `error` when the module declares one of those reserved names with an +// incompatible signature. +// +// Emission only. Never call this before serializing a .lidl: the published +// contract stays exactly what the author wrote, and the provider and every +// consumer each add the identity methods from this one function, so the two +// sides cannot disagree about them. Injecting into the artifact instead would +// make `--header-to-lidl` and `--from-header` disagree about the same module, +// and would make the two methods indistinguishable from author-declared ones. +inline bool lidlInjectIdentity(ModuleDecl& module, QString* error) +{ + const lidl::IdentityInjection r = lidl::injectIdentityMethods(module); + if (r.hasError()) { + if (error) *error = qs(r.error); + return false; + } + return true; +} + // A record whose ONLY field is a `tstr` named `_bytes` is indistinguishable on // the wire from a canonical tagged byte string: `isTaggedBytes()` is checked // BEFORE `is_object()` in both logos_codec.h and logos_json_convert.cpp, so diff --git a/cpp-generator/experimental/lidl_gen_cdylib.cpp b/cpp-generator/experimental/lidl_gen_cdylib.cpp index 3bce799..eb5d383 100644 --- a/cpp-generator/experimental/lidl_gen_cdylib.cpp +++ b/cpp-generator/experimental/lidl_gen_cdylib.cpp @@ -794,6 +794,20 @@ QString lidlMakeModuleImplExports(const ModuleDecl& module, s << " return lidlStrdup(err.dump());\n"; s << " }\n"; } + // A derived method (lidl/identity.hpp) has no member on the impl class + // to call — the generator owns its body. name()/version() answer from + // the module declaration, which the builder derives from metadata.json, + // so the reported value cannot drift from the built one. + if (md.derived && lidl::isIdentityMethod(md.name)) { + const QString literal = md.name == lidl::kIdentityName + ? qs(module.name) + : (module.version.empty() ? QStringLiteral("1.0.0") : qs(module.version)); + s << " auto result = std::string(\"" << literal << "\");\n"; + s << " return lidlStrdup(" << stdReturnToJson(md, "result", recs) + << ".dump());\n"; + s << " }\n"; + continue; + } QString call = "lidlImpl()." + qs(md.name) + "("; for (size_t i = 0; i < md.params.size(); ++i) { const QString expr = (i < minArgs) diff --git a/cpp-generator/main.cpp b/cpp-generator/main.cpp index 3e1c36e..58e7a02 100644 --- a/cpp-generator/main.cpp +++ b/cpp-generator/main.cpp @@ -168,6 +168,18 @@ static bool generateInterfaceWrappers(const QVector& ifaces, } } + { + // Consumers see name()/version() on every dependency and bound + // interface. Added here rather than read from the .lidl: the + // artifact carries only what the author wrote, and the provider + // adds the same two methods from the same function. + QString idErr; + if (!lidlInjectIdentity(mod, &idErr)) { + err << spec.path << ": " << idErr << "\n"; + return false; + } + } + noteOptionalPositionalSlots(mod, spec.path, err); const QString className = toPascalCase(spec.name); @@ -591,7 +603,14 @@ int main(int argc, char* argv[]) return 4; } - const ModuleDecl& mod = pr.module; + ModuleDecl mod = pr.module; + { + QString idErr; + if (!lidlInjectIdentity(mod, &idErr)) { + err << headerPath << ": " << idErr << "\n"; + return 4; + } + } QString genDirPath = outputDir.isEmpty() ? QDir::current().filePath("generated") : outputDir; @@ -617,6 +636,9 @@ int main(int argc, char* argv[]) if (!mod.events.empty()) outs.append({qs(mod.name) + "_events_cdylib.cpp", lidlMakeEventsSourceCdylib(mod, implClass, implHeader)}); + // Identity methods are `derived`, and lidlSerialize omits + // those — so this stays byte-identical to what + // --header-to-lidl writes for the same header. outs.append({qs(mod.name) + ".lidl", lidlSerialize(mod)}); for (const Out& o : outs) { const QString abs = QDir(genDirPath).filePath(o.file); @@ -692,7 +714,17 @@ int main(int argc, char* argv[]) << " (line " << pr.errorLine << ")\n"; return 4; } - const ModuleDecl& mod = pr.module; + ModuleDecl mod = pr.module; + { + // Contract-first: the committed .lidl is untouched; the + // provider's dispatch and method listing gain the identity + // methods the same way every consumer does. + QString idErr; + if (!lidlInjectIdentity(mod, &idErr)) { + err << lidlPath << ": " << idErr << "\n"; + return 4; + } + } QString cdErr; if (!lidlCdylibSupported(mod, &cdErr)) { err << "Error: module not cdylib-eligible: " << cdErr << "\n"; diff --git a/flake.lock b/flake.lock index 3044656..596b44b 100644 --- a/flake.lock +++ b/flake.lock @@ -12,11 +12,11 @@ ] }, "locked": { - "lastModified": 1786415321, - "narHash": "sha256-Oe98SavQSVGBIY7WIc8RQ5l+Bl4KLsdmjb+PyscfdNw=", + "lastModified": 1787240151, + "narHash": "sha256-WHmisUvYA8DQ2ZxSF2mM2INTEuxfyF07ypPOf+lOik4=", "owner": "logos-co", "repo": "logos-lidl", - "rev": "ffeebf2e90fa0c65e8c486988271fe0ca029d1e1", + "rev": "dd3440183b14a9d94758ff3c5a322875cc6a3147", "type": "github" }, "original": { diff --git a/tests/experimental/test_lidl_gen_cdylib.cpp b/tests/experimental/test_lidl_gen_cdylib.cpp index 93df922..4a9514e 100644 --- a/tests/experimental/test_lidl_gen_cdylib.cpp +++ b/tests/experimental/test_lidl_gen_cdylib.cpp @@ -694,3 +694,89 @@ TEST(LidlGenCdylib, GrantExportIsEmittedForEveryModuleNotJustPrivilegedOnes) EXPECT_TRUE(src.contains("logos_module_grant_host_services")) << src.toStdString(); } + +// --- Module identity --------------------------------------------------------- +// +// name()/version() are injected into the contract by the frontend +// (lidl/identity.hpp) and marked `derived`. The dispatch must answer them from +// the module DECLARATION -- the impl class has no such member, so delegating +// would not compile, and reading anything else would let the reported value +// drift from the metadata the module was built with. + +namespace { + +ModuleDecl moduleWithIdentity(const char* name, const char* version) +{ + ModuleDecl m; + m.name = name; + m.version = version; + lidl::injectIdentityMethods(m); + return m; +} + +QString implExportsFor(const ModuleDecl& m) +{ + return lidlMakeModuleImplExports(m, "SomeImpl", "some_impl.h"); +} + +} // namespace + +TEST(LidlGenCdylib, IdentityMethodsAnswerFromTheModuleDeclaration) +{ + const QString src = implExportsFor(moduleWithIdentity("weather_module", "2.4.1")); + + // The literal is the module's OWN version, not a default. A generator that + // fell back to "1.0.0" here would be indistinguishable from a correct one + // on the many modules that happen to be at 1.0.0. + EXPECT_TRUE(src.contains("if (m == \"name\")")) << src.toStdString(); + EXPECT_TRUE(src.contains("std::string(\"weather_module\")")) << src.toStdString(); + EXPECT_TRUE(src.contains("if (m == \"version\")")) << src.toStdString(); + EXPECT_TRUE(src.contains("std::string(\"2.4.1\")")) << src.toStdString(); + + // ...and never through the impl class, which has no such member. + EXPECT_FALSE(src.contains("lidlImpl().name(")) << src.toStdString(); + EXPECT_FALSE(src.contains("lidlImpl().version(")) << src.toStdString(); +} + +TEST(LidlGenCdylib, IdentityMethodsAreListedForIntrospection) +{ + // `lm methods` and every untyped caller read this listing, so an identity + // method that dispatches but is not advertised is only half present. + const QString src = implExportsFor(moduleWithIdentity("weather_module", "2.4.1")); + EXPECT_TRUE(src.contains("obj[\"name\"] = \"name\"")) << src.toStdString(); + EXPECT_TRUE(src.contains("obj[\"name\"] = \"version\"")) << src.toStdString(); + EXPECT_TRUE(src.contains("obj[\"signature\"] = \"name()\"")) << src.toStdString(); +} + +TEST(LidlGenCdylib, AnAuthorsOwnIdentityMethodStillReachesTheImpl) +{ + // A module MAY implement name() itself (logos-delivery-module does). It is + // then not `derived`, so it must dispatch like any other author method -- + // silently shadowing it with a generated literal would change behaviour. + ModuleDecl m; + m.name = "delivery_module"; + m.version = "1.0.0"; + MethodDecl mine; + mine.name = "name"; + mine.returnType = prim("tstr"); + m.methods.push_back(mine); + lidl::injectIdentityMethods(m); + + const QString src = implExportsFor(m); + EXPECT_TRUE(src.contains("lidlImpl().name(")) << src.toStdString(); + EXPECT_FALSE(src.contains("std::string(\"delivery_module\")")) << src.toStdString(); + // version() was still injected, and is still generated. + EXPECT_TRUE(src.contains("std::string(\"1.0.0\")")) << src.toStdString(); +} + +TEST(LidlGenCdylib, AVersionlessModuleFallsBackRatherThanEmittingEmpty) +{ + // A ModuleDecl with no version reaches here from a synthetic/interface + // contract. Emitting "" would make version() answer the empty string, + // which reads as a failure rather than as "unversioned". + ModuleDecl m; + m.name = "bare_module"; + lidl::injectIdentityMethods(m); + EXPECT_TRUE(implExportsFor(m).contains("std::string(\"1.0.0\")")) + << implExportsFor(m).toStdString(); +}