From 620f2e184c5de7736358eaaf6c9846e33cc46af3 Mon Sep 17 00:00:00 2001 From: Dario Gabriel Lipicar Date: Mon, 17 Aug 2026 23:14:58 -0300 Subject: [PATCH] feat(generator): a Qt-typed umbrella that needs no LogosAPI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Splits a consumer's TYPE SURFACE from its TRANSPORT. Until now the qt umbrella was `explicit LogosModules(LogosAPI* api)` while the lp one was default- constructible, so "Qt types" implicitly meant "has a LogosAPI" — and a cdylib module, whose provider surface is the std logos_module_impl.h C ABI and which holds no LogosAPI anywhere, could not have Qt-typed dependency wrappers at all. Its generated glue emits `new LogosModules()` unconditionally (lidl_gen_cdylib.cpp:693), so the combination did not merely misbehave, it did not compile. That was a codegen choice, not a law: the wrapper bodies already run over lp_*. `--binding api|origin` selects it, defaulting to `api`. A second enum rather than a third ApiStyle value, deliberately: ApiStyle names the type surface and is switched on by six emitters (makeHeader/makeSource/returnTypeFor/ paramTypeFor/toWireFor/fromWireFor); a "Qt types, explicit origin" member would force all six to answer a transport question whose honest answer is "same as Qt" every time. ApiStyle::Lp ignores the new axis — lp is origin-bound by construction — and that is asserted rather than assumed. The emitted umbrella bakes metadata.json#name as the origin literal: LogosModules() : test_fullapi_cpp(QStringLiteral("test_fullapi_qtproxy")) {} FullApi bind_full_api(const QString& moduleName) { return FullApi(QStringLiteral("test_fullapi_qtproxy"), moduleName); } Origin is the CONSUMER's own name and target is the dep — origin first in both bind_ overloads. This is the load-bearing property: LpBridge::forTarget derives origin from `api->moduleName()`, and reusing it silently gives a consumer the caller's identity, which has already preserved a privilege escalation once in this tree. An empty metadata name is refused at the CLI (exit 6, naming the file) and emits `#error` in the header: a module that cannot state its identity must not compile, and must never be handed a blank or borrowed one. Verified additive on 172 real metadata.json x 2 api-styles = 344 runs, all producing output, byte-identical old binary vs new. Mutation control: swapping bind_'s (origin, moduleName) to (moduleName, origin) fails the suite at MakeUmbrellaTest.QtExplicitOriginStatesTheConsumersOwnNameEverywhere. 281 -> 286 tests. Framing worth keeping: the origin is SELF-ASSERTED from the module's own metadata and is not attested by the transport. That is not a regression — `api->moduleName()` is equally process-stated — but "explicit origin" means the module names itself, not that the host vouches for the name. Co-Authored-By: Claude Opus 5 --- cpp-generator/generator_lib.cpp | 106 +++++++++++++++++++++- cpp-generator/generator_lib.h | 40 ++++++++- cpp-generator/main.cpp | 46 +++++++++- nix/tests-generator-cli.nix | 67 ++++++++++++++ tests/generator/test_make_umbrella.cpp | 117 +++++++++++++++++++++++++ 5 files changed, 370 insertions(+), 6 deletions(-) diff --git a/cpp-generator/generator_lib.cpp b/cpp-generator/generator_lib.cpp index eaaf291..e39cc8d 100644 --- a/cpp-generator/generator_lib.cpp +++ b/cpp-generator/generator_lib.cpp @@ -42,6 +42,41 @@ bool parseApiStyleFlag(const QStringList& args, ApiStyle& outStyle, QTextStream& return true; } +// `--binding api|origin` (both spellings, as above). Absent means FromApi, so +// every current invocation is unchanged. Lives here, next to UmbrellaBinding, +// for the same reason parseApiStyleFlag does: one table, no second copy to +// drift. +// +// An unrecognised value is REFUSED rather than defaulted. Defaulting a misspelt +// `--binding orgin` back to the LogosAPI umbrella would emit `LogosModules( +// LogosAPI*)` into a module that has no LogosAPI, and the diagnostic would +// arrive as a constructor mismatch in generated code rather than as a typo. +bool parseUmbrellaBindingFlag(const QStringList& args, UmbrellaBinding& outBinding, QTextStream& err) +{ + QString val; + for (int i = 0; i < args.size(); ++i) { + const QString& a = args.at(i); + if (a == "--binding") { + if (i + 1 < args.size()) val = args.at(i + 1); + break; + } + if (a.startsWith("--binding=")) { + val = a.section('=', 1); + break; + } + } + if (val == "origin") { + outBinding = UmbrellaBinding::ExplicitOrigin; + return true; + } + if (!val.isEmpty() && val != "api") { + err << "Unknown --binding value: " << val << " (expected 'api' or 'origin')\n"; + return false; + } + outBinding = UmbrellaBinding::FromApi; + return true; +} + QString toPascalCase(const QString& name) { QString out; @@ -1493,13 +1528,82 @@ QString makeSourceLp(const QString& moduleName, const QString& className, const // ── Umbrella (logos_sdk.h / logos_sdk.cpp) over a module's dependencies ────── -QString makeUmbrellaHeaderFromDeps(const QJsonArray& deps, const QStringList& interfaceNames, ApiStyle apiStyle, const QString& originName) +QString makeUmbrellaHeaderFromDeps(const QJsonArray& deps, const QStringList& interfaceNames, ApiStyle apiStyle, const QString& originName, UmbrellaBinding binding) { const QStringList depNames = dependencyNames(deps); QString content; QTextStream s(&content); + // Qt types, explicit origin: the umbrella a module with NO LogosAPI — a + // cdylib, whose provider surface is the std `logos_module_impl.h` C ABI — + // aggregates its Qt-typed dependency wrappers into. Structurally the Lp + // branch below with Qt spellings: default-constructible, so the generated + // glue's unconditional `new LogosModules()` compiles, and no LogosAPI + // member, so nothing in the module has to hold one. + // + // The per-dep wrappers are logos-qt-generator's + // (`--backend consumer --binding origin`); this emitter has no Qt-typed + // wrapper flavour to match it, and adding one would put two emitters back + // on the one artifact they currently agree on. + if (apiStyle == ApiStyle::Qt && binding == UmbrellaBinding::ExplicitOrigin) { + s << "#pragma once\n"; + s << "#include \n"; + // Only for the std::string bind_ overloads, matching the FromApi + // branch's rule. + if (!interfaceNames.isEmpty()) s << "#include \n"; + // Deliberately NO logos_api.h / logos_api_client.h: this umbrella names + // neither type, and a translation unit that includes it must be able to + // compile with no LogosAPI in scope at all. + for (const QString& depName : depNames) + s << "#include \"" << depName << "_api.h\"\n"; + for (const QString& ifaceName : interfaceNames) + s << "#include \"" << ifaceName << "_api.h\"\n"; + s << "\n"; + + // A module that does not know its own name must not compile. Every + // origin below would otherwise be the empty string, and an empty origin + // is not "no identity" to the transport — it is a client that + // authenticates as nobody, which fails far from here and looks like a + // capability bug. The one thing it must NEVER do is borrow a name. + if (originName.isEmpty()) { + s << "#error \"logos_sdk.h: the origin-bound umbrella needs the consuming " + "module's own name (metadata.json#name); none was given, and an origin " + "is asserted here, never derived or borrowed\"\n\n"; + } + + const QString origin = "QStringLiteral(\"" + originName + "\")"; + + s << "struct LogosModules {\n"; + s << " LogosModules()"; + bool first = true; + for (const QString& depName : depNames) { + s << (first ? " : " : ",\n "); + first = false; + s << depName << "(" << origin << ")"; + } + s << " {}\n"; + for (const QString& depName : depNames) + s << " " << toPascalCase(depName) << " " << depName << ";\n"; + // Bind factories. Unlike the Lp branch there is no umbrella-owned + // State: the Qt consumer wrapper is already a thin handle over a + // process-lifetime LpBridge keyed by (origin, target), so a + // `bind_x(...)` temporary's subscriptions outlive it exactly as they do + // on the LogosAPI-taking path. Same two overloads, same reason. + for (const QString& ifaceName : interfaceNames) { + const QString className = toPascalCase(ifaceName); + s << " " << className << " bind_" << ifaceName << "(const QString& moduleName) {\n"; + s << " return " << className << "(" << origin << ", moduleName);\n"; + s << " }\n"; + s << " " << className << " bind_" << ifaceName << "(const std::string& moduleName) {\n"; + s << " return " << className << "(" << origin + << ", QString::fromStdString(moduleName));\n"; + s << " }\n"; + } + s << "};\n"; + return content; + } + // Lp (Qt-free) umbrella: no LogosAPI. Each dep wrapper self-creates its // lp_client on behalf of `originName` (this module), so the struct is // default-constructible and the glue just does `new LogosModules()`. diff --git a/cpp-generator/generator_lib.h b/cpp-generator/generator_lib.h index a1bbbb8..b8ffda9 100644 --- a/cpp-generator/generator_lib.h +++ b/cpp-generator/generator_lib.h @@ -55,6 +55,39 @@ bool parseApiStyleFlag(const QStringList& args, ApiStyle& outStyle, QTextStream& // byte-for-byte unchanged. enum class BindMode { Static, Bound }; +// How the UMBRELLA binds its wrappers to a transport — the call ORIGIN, where +// BindMode above decides the call TARGET. +// FromApi — `explicit LogosModules(LogosAPI* api)`, each member built +// as `(api)` and each factory as `(api, name)`. +// The origin is derived, inside the wrapper, from +// `api->moduleName()`. The historical shape, and the default. +// ExplicitOrigin — `LogosModules()`, default-constructible, NO LogosAPI +// member and no `logos_api.h` include: each member is built +// as `(QStringLiteral(""))` and each +// factory as `(QStringLiteral(""), name)`. +// +// Deliberately a parameter and NOT a third ApiStyle value. ApiStyle names the +// TYPE SURFACE, and is switched on by makeHeader / makeSource / returnTypeFor / +// paramTypeFor / toWireFor / fromWireFor; a "Qt types, explicit origin" enum +// value would oblige every one of those to answer a question about transport +// binding that has no bearing on the types they map — and the honest answer in +// each would be "same as Qt". The axis being added here is orthogonal to the +// type surface, so it gets its own name. +// +// ApiStyle::Lp IGNORES this: the Qt-free umbrella has only one binding (it is +// origin-bound by construction, which is what this brings to the Qt surface). +// +// The origin is the CONSUMING module's own name, from `metadata.json#name`. An +// empty one is not defaulted or inferred — the emitted header carries an +// `#error` instead, because a wrapper that cannot state its own identity would +// otherwise open a connection under a blank one. +enum class UmbrellaBinding { FromApi, ExplicitOrigin }; + +// Parse `--binding api|origin` out of a raw argument list (both `--binding +// origin` and `--binding=origin`). Absent means FromApi. Returns false — having +// written a diagnostic to `err` — for a value the generator refuses. +bool parseUmbrellaBindingFlag(const QStringList& args, UmbrellaBinding& outBinding, QTextStream& err); + QString toPascalCase(const QString& name); QString normalizeType(QString t); QString mapParamType(const QString& qtType); @@ -121,7 +154,12 @@ QString makeSourceLp(const QString& moduleName, const QString& className, const // self-creates its lp_client on behalf of `originName` (the module being // generated for), so the struct is default-constructible. Qt emits the // LogosAPI-threading form, where `originName` is unused. -QString makeUmbrellaHeaderFromDeps(const QJsonArray& deps, const QStringList& interfaceNames, ApiStyle apiStyle = ApiStyle::Qt, const QString& originName = QString()); +// `binding` is trailing and defaulted so every current caller keeps the +// LogosAPI-threading umbrella, unchanged. With ExplicitOrigin the Qt umbrella +// becomes default-constructible and drops its LogosAPI — matching the shape the +// Lp flavour already has, and pairing with the wrappers logos-qt-generator +// emits under `--backend consumer --binding origin`. +QString makeUmbrellaHeaderFromDeps(const QJsonArray& deps, const QStringList& interfaceNames, ApiStyle apiStyle = ApiStyle::Qt, const QString& originName = QString(), UmbrellaBinding binding = UmbrellaBinding::FromApi); QString makeUmbrellaSourceFromDeps(const QJsonArray& deps, const QStringList& interfaceNames); #endif // GENERATOR_LIB_H diff --git a/cpp-generator/main.cpp b/cpp-generator/main.cpp index c6e259c..404b688 100644 --- a/cpp-generator/main.cpp +++ b/cpp-generator/main.cpp @@ -222,11 +222,30 @@ static int runUmbrellaMode(const QStringList& args, const QString& progName, ApiStyle apiStyle = ApiStyle::Qt; if (!parseApiStyleFlag(args, apiStyle, err)) return 1; + // `--binding api|origin` — the umbrella's transport binding (generator_lib.h, + // next to the UmbrellaBinding enum). + UmbrellaBinding binding = UmbrellaBinding::FromApi; + if (!parseUmbrellaBindingFlag(args, binding, err)) return 1; + + // With the Qt surface, `origin` means the per-dependency wrappers are + // logos-qt-generator's (`--backend consumer --binding origin`) and this + // run emits the UMBRELLA ONLY. The wrapper emitter reached below is the + // legacy Qt one, whose every constructor takes a LogosAPI — writing those + // next to an origin-bound umbrella would put two mutually incompatible + // wrapper flavours in one output directory, and the umbrella's members + // would not compile against them. Skipping is the honest outcome, and it + // is said out loud rather than inferred from an empty directory. + // + // Interface NAMES are still collected below, and still drive the + // `bind_(...)` factories; only the wrapper FILES are skipped. + const bool skipWrappers = + (apiStyle == ApiStyle::Qt && binding == UmbrellaBinding::ExplicitOrigin); + const int metaIdx = args.indexOf("--metadata"); if (metaIdx == -1 || metaIdx + 1 >= args.size()) { err << "Usage: " << progName << " --metadata /absolute/path/to/metadata.json --umbrella (or --general-only)" - " [--output-dir /path/to/output] [--api-style qt|lp]" + " [--output-dir /path/to/output] [--api-style qt|lp] [--binding api|origin]" " [--interface =[=]]" " [--dep =]\n"; return 1; @@ -316,10 +335,14 @@ static int runUmbrellaMode(const QStringList& args, const QString& progName, } // Generate one bound wrapper (_api.{h,cpp}) per interface. - if (!ifaceSpecs.isEmpty()) { + if (!ifaceSpecs.isEmpty() && !skipWrappers) { if (!generateInterfaceWrappers(ifaceSpecs, genDirPath, apiStyle, out, err)) { return 9; } + } else if (!ifaceSpecs.isEmpty()) { + err << "Note: --binding origin — emitting the umbrella only. The " + << ifaceSpecs.size() << " interface wrapper(s) must come from " + << "logos-qt-generator --backend consumer --bind bound --binding origin.\n"; } // Concrete dependencies generated from their published LIDL @@ -347,10 +370,14 @@ static int runUmbrellaMode(const QStringList& args, const QString& progName, haveDep.insert(sp.name); depSpecs.append(sp); } - if (!depSpecs.isEmpty()) { + if (!depSpecs.isEmpty() && !skipWrappers) { if (!generateInterfaceWrappers(depSpecs, genDirPath, apiStyle, out, err, BindMode::Static)) { return 9; } + } else if (!depSpecs.isEmpty()) { + err << "Note: --binding origin — emitting the umbrella only. The " + << depSpecs.size() << " dependency wrapper(s) must come from " + << "logos-qt-generator --backend consumer --bind static --binding origin.\n"; } QStringList interfaceNames; @@ -362,6 +389,17 @@ static int runUmbrellaMode(const QStringList& args, const QString& progName, // what those return. For the Lp (Qt-free) flavor the umbrella bakes this // module's name as the lp_client origin. const QString originName = obj.value("name").toString(); + // The origin is this module's OWN name, and with `--binding origin` it is + // the only thing standing between a generated wrapper and calling out under + // somebody else's identity. Refuse at the CLI as well as in the emitter + // (which writes an `#error`): failing here names the metadata file, which + // is where the fix is. + if (binding == UmbrellaBinding::ExplicitOrigin && originName.isEmpty()) { + err << "--binding origin needs the consuming module's own name, and " + << metaResolvedPath << " declares no \"name\". The origin is asserted, " + << "never derived from a caller.\n"; + return 6; + } const QDir genDir(genDirPath); { QFile outFile(genDir.filePath("logos_sdk.h")); @@ -369,7 +407,7 @@ static int runUmbrellaMode(const QStringList& args, const QString& progName, err << "Failed to write umbrella header: " << outFile.fileName() << "\n"; return 7; } - outFile.write(makeUmbrellaHeaderFromDeps(deps, interfaceNames, apiStyle, originName).toUtf8()); + outFile.write(makeUmbrellaHeaderFromDeps(deps, interfaceNames, apiStyle, originName, binding).toUtf8()); outFile.close(); } { diff --git a/nix/tests-generator-cli.nix b/nix/tests-generator-cli.nix index 39e07da..d42d846 100644 --- a/nix/tests-generator-cli.nix +++ b/nix/tests-generator-cli.nix @@ -81,6 +81,73 @@ pkgs.runCommand "${common.pname}-generator-cli-tests" [ -s ./gen/logos_sdk.h ] || fail "--general-only emitted no logos_sdk.h" echo "OK: --general-only still emits the umbrella" + # ── `--binding origin`: the umbrella a module with no LogosAPI needs ── + # + # Emitter-level assertions live in the gtest suite; these are the ones only + # the BINARY can answer — that the flag is wired to the mode at all, that an + # unrecognised value is refused rather than defaulted, and that a module + # with no name of its own is refused rather than given a blank identity. + cat > origin_metadata.json <<'EOF' + { + "name": "cli_origin_module", + "version": "1.0.0", + "type": "core", + "dependencies": ["dep_one", "dep_two"] + } + EOF + + logos-cpp-generator --metadata ./origin_metadata.json --general-only --api-style qt --binding origin --output-dir ./gen-origin >/dev/null 2>origin.err || { cat origin.err >&2; fail "--binding origin was refused"; } + [ -s ./gen-origin/logos_sdk.h ] || fail "--binding origin emitted no logos_sdk.h" + + # Default-constructible, so the cdylib glue's `new LogosModules()` compiles. + grep -q 'LogosModules() : dep_one(QStringLiteral("cli_origin_module"))' ./gen-origin/logos_sdk.h || { cat ./gen-origin/logos_sdk.h >&2 + fail "the origin-bound umbrella is not default-constructible"; } + + # THE property: the origin is this module's OWN name, never an api object's. + # `forTarget` derives an origin from `api->moduleName()`, and a wrapper + # built on a borrowed api calls out under the lender's identity — so the + # umbrella must hand every wrapper a stated name and hold no LogosAPI at all. + if grep -q 'LogosAPI' ./gen-origin/logos_sdk.h; then + cat ./gen-origin/logos_sdk.h >&2 + fail "the origin-bound umbrella still mentions LogosAPI" + fi + grep -q 'dep_two(QStringLiteral("cli_origin_module"))' ./gen-origin/logos_sdk.h || fail "a dependency was not handed the consuming module's own name" + echo "OK: --binding origin emits a default-constructible, LogosAPI-free umbrella" + + # The default is unchanged — same metadata, no flag, the historical shape. + logos-cpp-generator --metadata ./origin_metadata.json --general-only --api-style qt --output-dir ./gen-api >/dev/null 2>&1 || fail "the default (LogosAPI) umbrella regressed" + grep -q 'explicit LogosModules(LogosAPI\* api)' ./gen-api/logos_sdk.h || { cat ./gen-api/logos_sdk.h >&2 + fail "the default umbrella is no longer the LogosAPI-taking one"; } + echo "OK: the default binding still emits the LogosAPI umbrella" + + # A misspelt value is refused. Defaulting it back to the LogosAPI form would + # emit `LogosModules(LogosAPI*)` into a module that has none, and the + # diagnostic would land as a constructor mismatch in generated code. + set +e + logos-cpp-generator --metadata ./origin_metadata.json --general-only --api-style qt --binding orgin --output-dir ./gen-bad >badbinding.out 2>badbinding.err + status=$? + set -e + [ "$status" -ne 0 ] || fail "--binding orgin (misspelt) exited 0" + grep -q -- 'Unknown --binding value' badbinding.err || { cat badbinding.err >&2; fail "a bad --binding failed without saying why"; } + echo "OK: an unrecognised --binding is refused" + + # A module with no name cannot state an origin, and must not be given a + # blank one. Refused at the CLI, where the metadata file can be named. + cat > anonymous_metadata.json <<'EOF' + { + "version": "1.0.0", + "type": "core", + "dependencies": ["dep_one"] + } + EOF + set +e + logos-cpp-generator --metadata ./anonymous_metadata.json --general-only --api-style qt --binding origin --output-dir ./gen-anon >anon.out 2>anon.err + status=$? + set -e + [ "$status" -ne 0 ] || fail "--binding origin accepted metadata with no name" + grep -q "asserted" anon.err || { cat anon.err >&2; fail "the anonymous-origin refusal does not explain itself"; } + echo "OK: --binding origin refuses a module that cannot name itself" + mkdir -p "$out" echo "logos-cpp-generator CLI argument-surface tests passed" > "$out/result.txt" '' diff --git a/tests/generator/test_make_umbrella.cpp b/tests/generator/test_make_umbrella.cpp index 258a4ac..1a93dc1 100644 --- a/tests/generator/test_make_umbrella.cpp +++ b/tests/generator/test_make_umbrella.cpp @@ -127,3 +127,120 @@ TEST(MakeUmbrellaTest, NoDependenciesStillEmitsTheAggregate) EXPECT_TRUE(qt.contains("struct LogosModules {")) << qt.toStdString(); EXPECT_TRUE(qt.contains("LogosAPI* api;")) << qt.toStdString(); } + +// ── The origin-bound Qt umbrella (UmbrellaBinding::ExplicitOrigin) ─────────── +// +// The Qt umbrella used to have exactly one shape: `LogosModules(LogosAPI* api)`, +// with every member built as `(api)`. That single line is what kept the Qt +// type surface out of reach for a module with no LogosAPI — a cdylib, whose +// provider surface is the std `logos_module_impl.h` C ABI and whose generated +// glue emits an unconditional `new LogosModules()`. This flavour is that +// umbrella with the identity object removed and the module's OWN NAME baked in +// instead, matching the shape the Lp flavour has always had. +// +// The wrappers it aggregates are logos-qt-generator's +// (`--backend consumer --binding origin`); the two tools have to agree on a +// constructor signature, and these tests pin this side of it. The other side is +// pinned in logos-qt-sdk, which compiles both halves together +// (tests/qt-generator/fixtures/origin_umbrella_tu.cpp). + +TEST(MakeUmbrellaTest, QtExplicitOriginIsDefaultConstructibleAndHoldsNoLogosApi) +{ + const QString h = makeUmbrellaHeaderFromDeps(depsMixedForms(), {}, ApiStyle::Qt, + "sample_module", + UmbrellaBinding::ExplicitOrigin); + + // Default-constructible: what `new LogosModules()` in the cdylib glue needs. + EXPECT_TRUE(h.contains("LogosModules() : dep_a(QStringLiteral(\"sample_module\"))")) + << h.toStdString(); + EXPECT_FALSE(h.contains("LogosAPI")) << h.toStdString(); + EXPECT_FALSE(h.contains("logos_api.h")) << h.toStdString(); + EXPECT_FALSE(h.contains("logos_api_client.h")) << h.toStdString(); + + // Still the Qt type surface — same members, same PascalCase wrapper types, + // same includes. Only the binding moved. + EXPECT_TRUE(h.contains("DepA dep_a;")) << h.toStdString(); + EXPECT_TRUE(h.contains("DepB dep_b;")) << h.toStdString(); + EXPECT_TRUE(h.contains("DepC dep_c;")) << h.toStdString(); + EXPECT_TRUE(h.contains("#include \"dep_a_api.h\"")) << h.toStdString(); +} + +// THE assertion of the whole change: every origin the umbrella writes is the +// CONSUMING module's own name, stated as a literal. Not derived from an api +// object, not defaulted, not inherited from whoever constructed the umbrella. +// +// The trap this guards is specific and has been measured: `LpBridge::forTarget` +// reads the origin off `api->moduleName()`, so a wrapper built on a LogosAPI +// belonging to some OTHER module makes its calls under that module's identity +// and with its capabilities. A `bind_(...)` factory is where that would +// hide — it takes a name at runtime, and taking the WRONG one (the target's +// name reused as the origin, or a borrowed api) type-checks perfectly. +TEST(MakeUmbrellaTest, QtExplicitOriginStatesTheConsumersOwnNameEverywhere) +{ + const QString h = makeUmbrellaHeaderFromDeps(depsMixedForms(), {"some_iface"}, + ApiStyle::Qt, "sample_module", + UmbrellaBinding::ExplicitOrigin); + + // Members: origin first, target baked into the wrapper itself. + EXPECT_TRUE(h.contains("dep_b(QStringLiteral(\"sample_module\"))")) << h.toStdString(); + EXPECT_TRUE(h.contains("dep_c(QStringLiteral(\"sample_module\"))")) << h.toStdString(); + + // Bind factories: origin is the CONSUMER (a literal), target is the + // runtime argument. Both overloads, and in that order — swapping them would + // make every bound call originate from the provider being bound to. + EXPECT_TRUE(h.contains( + "return SomeIface(QStringLiteral(\"sample_module\"), moduleName);")) + << h.toStdString(); + EXPECT_TRUE(h.contains( + "return SomeIface(QStringLiteral(\"sample_module\"), QString::fromStdString(moduleName));")) + << h.toStdString(); + + // Nothing anywhere passes an api, and nothing derives a name. + EXPECT_FALSE(h.contains("(api")) << h.toStdString(); + EXPECT_FALSE(h.contains("moduleName()")) << h.toStdString(); +} + +// A module that cannot state its own name must not compile. Every origin would +// otherwise be the empty string, which is not "no identity" to the transport — +// it is a client authenticating as nobody, failing far from here and looking +// like a capability bug. The one thing the generator must never do is fill the +// gap by borrowing a name from somewhere. +TEST(MakeUmbrellaTest, QtExplicitOriginRefusesToGuessAnOrigin) +{ + const QString h = makeUmbrellaHeaderFromDeps(depsMixedForms(), {}, ApiStyle::Qt, + QString(), UmbrellaBinding::ExplicitOrigin); + EXPECT_TRUE(h.contains("#error")) << h.toStdString(); + EXPECT_TRUE(h.contains("never derived or borrowed")) << h.toStdString(); +} + +// Additive, and asserted as such rather than assumed: the default binding IS +// the LogosAPI-threading umbrella, byte for byte. Every module in the tree +// compiles against that output today. +TEST(MakeUmbrellaTest, TheDefaultBindingIsTheLogosApiUmbrellaUnchanged) +{ + const QStringList ifaces{"some_iface"}; + const QString defaulted = + makeUmbrellaHeaderFromDeps(depsMixedForms(), ifaces, ApiStyle::Qt, "sample_module"); + const QString explicitly = + makeUmbrellaHeaderFromDeps(depsMixedForms(), ifaces, ApiStyle::Qt, "sample_module", + UmbrellaBinding::FromApi); + EXPECT_EQ(defaulted, explicitly); + EXPECT_TRUE(defaulted.contains("explicit LogosModules(LogosAPI* api)")) << defaulted.toStdString(); + EXPECT_TRUE(defaulted.contains("return SomeIface(api, moduleName);")) << defaulted.toStdString(); +} + +// The Qt-free umbrella is origin-bound by construction, so the flag has nothing +// to say about it. Asserted rather than left implicit: an Lp branch that started +// reading `binding` would be a silent behaviour change for every universal and +// cdylib module in the tree. +TEST(MakeUmbrellaTest, LpIgnoresTheBindingFlag) +{ + const QString a = makeUmbrellaHeaderFromDeps(depsMixedForms(), {"some_iface"}, + ApiStyle::Lp, "sample_module", + UmbrellaBinding::FromApi); + const QString b = makeUmbrellaHeaderFromDeps(depsMixedForms(), {"some_iface"}, + ApiStyle::Lp, "sample_module", + UmbrellaBinding::ExplicitOrigin); + EXPECT_EQ(a, b); + EXPECT_TRUE(a.contains("dep_a(\"sample_module\")")) << a.toStdString(); +}