feat(generator): a Qt-typed umbrella that needs no LogosAPI

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_<iface>'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 <noreply@anthropic.com>
This commit is contained in:
Dario Gabriel Lipicar
2026-08-17 23:14:58 -03:00
co-authored by Claude Opus 5
parent 2ef1c25929
commit 620f2e184c
5 changed files with 370 additions and 6 deletions
+105 -1
View File
@@ -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 <QString>\n";
// Only for the std::string bind_<iface> overloads, matching the FromApi
// branch's rule.
if (!interfaceNames.isEmpty()) s << "#include <string>\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()`.