mirror of
https://github.com/logos-co/logos-cpp-sdk.git
synced 2026-08-31 01:31:10 +00:00
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:
co-authored by
Claude Opus 5
parent
2ef1c25929
commit
620f2e184c
@@ -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()`.
|
||||
|
||||
Reference in New Issue
Block a user