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
+42 -4
View File
@@ -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_<name>(...)` 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 <name>=<file.lidl|file.h>[=<ImplClass>]]"
" [--dep <name>=<file.lidl>]\n";
return 1;
@@ -316,10 +335,14 @@ static int runUmbrellaMode(const QStringList& args, const QString& progName,
}
// Generate one bound wrapper (<name>_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();
}
{