mirror of
https://github.com/logos-co/logos-cpp-sdk.git
synced 2026-08-31 09:41:06 +00:00
Run logos-lidl's injectIdentityMethods() on every ModuleDecl this generator
emits code from, and give the cdylib dispatch a body for the two methods it
adds.
Injection happens at EMISSION points, never at artifact points:
* generateInterfaceWrappers -- one load point covering both --dep and
--interface, so a consumer sees name()/version() on every dependency and
bound interface;
* --from-header --backend cdylib and --lidl --backend cdylib, so the provider
answers them;
* NOT --header-to-lidl, which writes the published contract.
The distinction is belt-and-braces rather than load-bearing: the injected
methods are `derived` and lidlSerialize omits those, so the .lidl a
--from-header build writes stays byte-identical to what --header-to-lidl writes
for the same header.
The dispatch emits a literal for a derived identity method instead of the usual
lidlImpl().<name>(...) -- the author's impl class has no such member, so
delegating would not compile. The literal is the module's own name and version,
so it cannot drift from the metadata the module was built with. A module that
declares name() itself is not derived and still reaches its impl.
290/290 tests pass, 4 new: that the emitted literal is the module's OWN version
(a test at 1.0.0 could not tell a correct generator from one that fell back),
that identity is listed for introspection as well as dispatched, that an
author's own name() still reaches the impl, and that a versionless declaration
falls back rather than emitting "" -- which would read as a failed call.
Requires logos-lidl#10; flake.lock pins that branch until it merges.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1011 lines
51 KiB
C++
1011 lines
51 KiB
C++
#include "lidl_gen_cdylib.h"
|
|
#include "lidl_emit_common.h"
|
|
|
|
#include <QTextStream>
|
|
|
|
#include <functional>
|
|
#include <set>
|
|
#include <string>
|
|
|
|
QString lidlToPascalCase(const QString& name);
|
|
QString lidlTypeToQt(const TypeExpr& te);
|
|
bool lidlIsStdConvertible(const TypeExpr& te);
|
|
|
|
namespace {
|
|
|
|
// The cdylib-supported subset: std-convertible LIDL types only — the same
|
|
// Qt-free set the std apiStyle handled, so any universal module that built
|
|
// under std also builds as a header-first cdylib.
|
|
// The records a contract DECLARES. A `Named` type is a record only if it is in
|
|
// here: `void` is not a LIDL builtin, so `-> void` arrives as Named("void") and
|
|
// treating every Named as a record is how the Rust generator once emitted
|
|
// `-> Void`. Same trap, same guard.
|
|
std::set<std::string> recordNames(const ModuleDecl& module)
|
|
{
|
|
std::set<std::string> out;
|
|
for (const TypeDecl& t : module.types) out.insert(t.name);
|
|
return out;
|
|
}
|
|
|
|
bool isRecord(const TypeExpr& te, const std::set<std::string>& recs)
|
|
{
|
|
return te.kind == TypeExpr::Named && recs.count(te.name) > 0;
|
|
}
|
|
|
|
bool typeSupported(const TypeExpr& te, bool isReturn, const std::set<std::string>& recs)
|
|
{
|
|
if (te.kind == TypeExpr::Primitive) {
|
|
if (te.name == "tstr" || te.name == "bstr" || te.name == "int"
|
|
|| te.name == "uint" || te.name == "float64" || te.name == "bool")
|
|
return true;
|
|
// any (LogosMap/LogosList/json) routes through nlohmann in either
|
|
// direction; result (StdLogosResult) and void only make sense as a
|
|
// return. All Qt-free.
|
|
if (te.name == "any")
|
|
return true;
|
|
if (isReturn && (te.name == "result" || te.name == "void"))
|
|
return true;
|
|
return false;
|
|
}
|
|
// A declared record is a generated struct with a generated codec.
|
|
if (isRecord(te, recs))
|
|
return true;
|
|
// `?T` — supported exactly when its VALUE type is.
|
|
//
|
|
// The value type is checked as a NON-return position on purpose: `result`
|
|
// and `void` are the two spellings that only make sense as a return, and
|
|
// neither can be optional. `void` is the absence of a value, so `?void` is
|
|
// meaningless; `result` already carries its own success/error discriminant,
|
|
// so `?result` would be a second one. `-> ?Point` and `-> ?tstr` are the
|
|
// real optional returns and stay eligible.
|
|
if (te.kind == TypeExpr::Optional) {
|
|
if (te.elements.empty()) return false;
|
|
return typeSupported(optionalValueType(te), /*isReturn=*/false, recs);
|
|
}
|
|
// Recurse rather than whitelisting element names: that admits [bstr],
|
|
// [[int]], [Record] and [{tstr: T}] in one rule, and keeps the gate and
|
|
// the spelling function agreeing about what is expressible.
|
|
if (te.kind == TypeExpr::Array && te.elements.size() == 1)
|
|
return typeSupported(te.elements[0], false, recs);
|
|
// Only tstr keys: the generated codec spells a map as
|
|
// std::map<std::string, T>, so a non-tstr key has no C++ spelling. This
|
|
// used to `return true` for ANY map, which admitted `{int: tstr}` and then
|
|
// silently produced a LogosMap that lost the key type.
|
|
if (te.kind == TypeExpr::Map) {
|
|
if (te.elements.size() != 2) return false;
|
|
const TypeExpr& k = te.elements[0];
|
|
if (!(k.kind == TypeExpr::Primitive && k.name == "tstr")) return false;
|
|
return typeSupported(te.elements[1], false, recs);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Qt-free spelling of a LIDL type (defined below). Forward-declared so the
|
|
// method-param decoder can spell composite `any` containers as their nlohmann
|
|
// aliases instead of Qt containers in this Qt-free TU.
|
|
QString lidlTypeToStdCdylib(const TypeExpr& te, const std::set<std::string>& recs);
|
|
|
|
// json arg expression -> std-typed C++ expression
|
|
// A method argument, decoded into the author's C++ type.
|
|
//
|
|
// EVERY typed value goes through the generated codec, which recurses — so a bstr
|
|
// keeps its canonical tag at ANY depth, a record decodes field by field with a
|
|
// path in the error, and a scalar is checked against its declared type.
|
|
//
|
|
// The scalars used to keep their nlohmann accessor verbatim, and that was the
|
|
// last hole in the type contract on this backend: `.get<uint64_t>()` on -1 wraps
|
|
// to 18446744073709551615 with no exception, so `echoUint(-1)` answered
|
|
// 18446744073709551615 here and `dispatch_failed` on the Rust provider — a
|
|
// silent sign flip on a nominal type, in a contract both providers share.
|
|
// `.get<int64_t>()` on 3.7 likewise truncated to 3 instead of rejecting.
|
|
//
|
|
// The comment that used to sit here justified the leniency by pointing at the
|
|
// conformance matrix cells that pinned it. That was circular: those cells exist
|
|
// to DOCUMENT the divergence, and their own `why` text says the strict behaviour
|
|
// is the correct one. The expectations moved with this change.
|
|
//
|
|
// `any` still passes through untouched — it is the one LIDL type that declares
|
|
// nothing, so there is nothing to check it against.
|
|
QString jsonArgToStd(const TypeExpr& te, const QString& expr, const QString& path,
|
|
const std::set<std::string>& recs)
|
|
{
|
|
// `?T` — decode is LIBERAL, and only by exactly one inhabitant.
|
|
//
|
|
// null decodes to empty; anything else is decoded as T by the SAME decoder a
|
|
// required T would get, so a present-but-wrong value fails with the same
|
|
// message at the same path. Optional widens the domain, it does not switch
|
|
// type checking off.
|
|
if (te.kind == TypeExpr::Optional && !te.elements.empty()) {
|
|
const QString cpp = lidlTypeToStdCdylib(te, recs);
|
|
const TypeExpr& vt = optionalValueType(te);
|
|
// `?any` collapses onto `any` (see lidlTypeToStdCdylib): untyped JSON
|
|
// already carries null, so there is no wrapper to build.
|
|
if (!cpp.startsWith("std::optional<"))
|
|
return jsonArgToStd(vt, expr, path, recs);
|
|
// A scalar `bstr` argument does NOT go through the codec — it gets the
|
|
// lenient bytes decode, so a caller may send the tagged form, a plain
|
|
// string, a number or a byte array. `?bstr` has to keep that, or the
|
|
// identical value would be accepted in a required slot and rejected in
|
|
// an optional one. Test for the empty inhabitant here and wrap.
|
|
if (vt.kind == TypeExpr::Primitive && vt.name == "bstr")
|
|
return "(" + expr + ".is_null() ? " + cpp + "() : " + cpp + "("
|
|
+ jsonArgToStd(vt, expr, path, recs) + "))";
|
|
// Everything else names std::optional<T> and lets
|
|
// Codec<std::optional<T>> map null -> nullopt in one expression.
|
|
return "logos::fromJson<" + cpp + ">(" + expr + ", \"" + path + "\")";
|
|
}
|
|
if (te.kind == TypeExpr::Primitive) {
|
|
if (te.name == "bstr")
|
|
return "logos::bytesFromJsonLenient(" + expr + ", \"" + path + "\")";
|
|
if (te.name == "any") return expr;
|
|
}
|
|
const QString cpp = lidlTypeToStdCdylib(te, recs);
|
|
// `[any]` / `{tstr:any}`. The ELEMENT type is unconstrained, so there is
|
|
// nothing to decode — but the SHAPE is declared, and it used to pass through
|
|
// unchecked ("as it always has"). That let a scalar reach a LogosList
|
|
// parameter, and a proxy forwarding it through a Qt-typed consumer turned
|
|
// "notalist" into ["n","o","t","a","l","i","s","t"] — qvariant_cast reads a
|
|
// QString as a sequential container. The downstream provider then saw a
|
|
// well-formed array and had nothing to refuse.
|
|
//
|
|
// Checked here rather than deeper: LogosList and LogosMap are both aliases
|
|
// of nlohmann::json, so no codec specialization can tell them apart. The
|
|
// value is still handed on unchanged, and the throw lands in the dispatch's
|
|
// existing catch as {"code":"dispatch_failed"} — the same answer, with the
|
|
// same message, that every non-Qt surface already gives.
|
|
if (cpp == "LogosList")
|
|
return "logos::jsonRequireArray(" + expr + ", \"" + path + "\")";
|
|
if (cpp == "LogosMap")
|
|
return "logos::jsonRequireObject(" + expr + ", \"" + path + "\")";
|
|
// A TYPED map does not NAME its C++ type — it hands the compiler a proxy and
|
|
// lets the author's own declaration pick it.
|
|
//
|
|
// `{tstr: T}` has two C++ spellings, std::map and std::unordered_map, and
|
|
// logos_codec.h specializes Codec for both. Naming one of them here would
|
|
// silently make the other a compile error in generated code the author never
|
|
// wrote: `logos::fromJson<std::map<...>>` returns a std::map, and a std::map
|
|
// does not convert to an unordered_map parameter. logos::JsonArg instantiates
|
|
// the conversion with the EXACT parameter type instead, so both spellings
|
|
// decode — through the same Codec, with the same path in the same error.
|
|
//
|
|
// Only maps: every other LIDL type has exactly one C++ spelling here, and
|
|
// JsonArg documents one type it cannot serve (std::optional<X>, whose own
|
|
// converting constructor out-ranks the proxy's conversion operator) — the
|
|
// Optional branch above returns before reaching this line.
|
|
if (te.kind == TypeExpr::Map)
|
|
return "logos::JsonArg(" + expr + ", \"" + path + "\")";
|
|
return "logos::fromJson<" + cpp + ">(" + expr + ", \"" + path + "\")";
|
|
}
|
|
|
|
// std-typed return variable -> json expression
|
|
QString stdReturnToJson(const MethodDecl& md, const QString& var,
|
|
const std::set<std::string>& recs)
|
|
{
|
|
const TypeExpr& te = md.returnType;
|
|
if (md.resultReturn) {
|
|
// StdLogosResult -> the canonical {success, value, error} object
|
|
// (same shape logos_json_convert emits for Qt LogosResult).
|
|
return "lidlResultToJson(" + var + ")";
|
|
}
|
|
// `jsonReturn` is set by the front end for any map/list return, but that no
|
|
// longer implies the C++ type IS nlohmann::json: a TYPED map now spells
|
|
// std::map<std::string, T>. Checking the flag before the spelling emitted
|
|
// `result.dump()` on a std::map. The spelling decides.
|
|
const QString cppRet = lidlTypeToStdCdylib(te, recs);
|
|
if (md.jsonReturn && (cppRet == "LogosMap" || cppRet == "LogosList")) {
|
|
return var; // LogosMap / LogosList are nlohmann::json already
|
|
}
|
|
if (te.kind == TypeExpr::Primitive) {
|
|
if (te.name == "bstr") return "logos::bytesToJson(" + var + ")";
|
|
if (te.name == "any") return var;
|
|
return "nlohmann::json(" + var + ")";
|
|
}
|
|
if (cppRet == "LogosMap" || cppRet == "LogosList")
|
|
return var;
|
|
// Same reason the map ARGUMENT does not name its type: `{tstr: T}` is both
|
|
// std::map and std::unordered_map, so let the return variable's own type be
|
|
// deduced rather than asserting one of them.
|
|
if (te.kind == TypeExpr::Map)
|
|
return "logos::toJson(" + var + ")";
|
|
// `nlohmann::json(v)` would serialize a vector<uint8_t> as a plain number
|
|
// array and a record not at all; the codec keeps bytes tagged at depth.
|
|
return "logos::toJson<" + cppRet + ">(" + var + ")";
|
|
}
|
|
|
|
// Qt-free spelling of a LIDL type. lidlTypeToStd() falls back to Qt containers
|
|
// (QVariant / QVariantMap / QVariantList) for the composite types, but a cdylib
|
|
// TU is Qt-free by definition and typeSupported() admits `any` and maps — so
|
|
// spell those as their nlohmann aliases (LogosMap / LogosList) instead. Without
|
|
// this the events sidecar emits a bare `QVariant` parameter and does not
|
|
// compile.
|
|
QString lidlTypeToStdCdylib(const TypeExpr& te, const std::set<std::string>& recs)
|
|
{
|
|
// `?T` -> std::optional<T>, EXCEPT over the untyped-JSON aliases.
|
|
//
|
|
// LogosMap / LogosList are nlohmann::json, and json already has `null` among
|
|
// its inhabitants — so std::optional<LogosMap> would give `?any` TWO empty
|
|
// spellings (nullopt and json(null)) and make it three-state, which is
|
|
// exactly what R1 forbids. `?any` therefore collapses onto `any`: same two
|
|
// states, one C++ type. (logos-lidl's validator warns on `?any` for the same
|
|
// reason, and the warning is about the spelling, not about this mapping.)
|
|
if (te.kind == TypeExpr::Optional && !te.elements.empty()) {
|
|
const QString inner = lidlTypeToStdCdylib(optionalValueType(te), recs);
|
|
if (inner == "LogosMap" || inner == "LogosList")
|
|
return inner;
|
|
return "std::optional<" + inner + ">";
|
|
}
|
|
if (te.kind == TypeExpr::Primitive && te.name == "any")
|
|
return "LogosMap";
|
|
// `{tstr: any}` and `[any]` keep their nlohmann aliases: every existing
|
|
// universal module spells them that way, and narrowing them would be a
|
|
// source break for no gain (they ARE untyped JSON).
|
|
if (te.kind == TypeExpr::Map && te.elements.size() == 2
|
|
&& te.elements[1].kind == TypeExpr::Primitive && te.elements[1].name == "any")
|
|
return "LogosMap";
|
|
if (te.kind == TypeExpr::Array && te.elements.size() == 1
|
|
&& te.elements[0].kind == TypeExpr::Primitive
|
|
&& te.elements[0].name == "any")
|
|
return "LogosList";
|
|
|
|
// A declared record is its generated struct.
|
|
if (isRecord(te, recs))
|
|
return qs(te.name);
|
|
// Recurse, so [bstr] is std::vector<std::vector<uint8_t>> and {tstr: Blob}
|
|
// is std::map<std::string, Blob>. lidlTypeToStd() would answer QVariantList
|
|
// / QVariantMap here — a Qt name in a Qt-FREE translation unit, which only
|
|
// failed to appear because the gate used to reject these types. Widening
|
|
// the gate makes that fallback a live leak, so composites must never reach
|
|
// it.
|
|
if (te.kind == TypeExpr::Array && te.elements.size() == 1)
|
|
return "std::vector<" + lidlTypeToStdCdylib(te.elements[0], recs) + ">";
|
|
if (te.kind == TypeExpr::Map && te.elements.size() == 2)
|
|
return "std::map<std::string, " + lidlTypeToStdCdylib(te.elements[1], recs) + ">";
|
|
|
|
return lidlTypeToStd(te);
|
|
}
|
|
|
|
// The C++ spelling of a RECORD FIELD, honouring both optionality spellings.
|
|
//
|
|
// `? name: T` and `name: ?T` are the same declaration and must produce
|
|
// byte-identical code (logos-lidl docs/spec.md, "Optionality"). That only holds
|
|
// because fieldIsOptional()/fieldValueType() reconcile them in the frontend —
|
|
// spelling one of the two out here would reintroduce the drift they exist to
|
|
// prevent. Never write `f.optional` or `f.type.kind == Optional` in a backend.
|
|
QString lidlFieldTypeCdylib(const FieldDecl& f, const std::set<std::string>& recs)
|
|
{
|
|
if (!fieldIsOptional(f))
|
|
return lidlTypeToStdCdylib(f.type, recs);
|
|
const QString inner = lidlTypeToStdCdylib(fieldValueType(f), recs);
|
|
// Same collapse as lidlTypeToStdCdylib: untyped JSON already has null.
|
|
if (inner == "LogosMap" || inner == "LogosList")
|
|
return inner;
|
|
return "std::optional<" + inner + ">";
|
|
}
|
|
|
|
// True when anything in the contract is optional — a record field by either
|
|
// spelling, a method parameter or return, or an event parameter. Gates the
|
|
// `#include <optional>` in the generated TUs, so a contract that declares no
|
|
// optional keeps its output byte-for-byte unchanged.
|
|
bool moduleUsesOptional(const ModuleDecl& module)
|
|
{
|
|
std::function<bool(const TypeExpr&)> mentions = [&](const TypeExpr& t) -> bool {
|
|
if (t.kind == TypeExpr::Optional) return true;
|
|
for (const TypeExpr& e : t.elements)
|
|
if (mentions(e)) return true;
|
|
return false;
|
|
};
|
|
for (const TypeDecl& t : module.types)
|
|
for (const FieldDecl& f : t.fields)
|
|
if (fieldIsOptional(f) || mentions(f.type)) return true;
|
|
for (const MethodDecl& md : module.methods) {
|
|
if (mentions(md.returnType)) return true;
|
|
for (const ParamDecl& pd : md.params)
|
|
if (mentions(pd.type)) return true;
|
|
}
|
|
for (const EventDecl& ed : module.events)
|
|
for (const ParamDecl& pd : ed.params)
|
|
if (mentions(pd.type)) return true;
|
|
return false;
|
|
}
|
|
|
|
// True when the module declares at least one `bstr` event parameter — the only
|
|
// reason the events sidecar needs the bytes encoder. Emitting it unconditionally
|
|
// leaves an unused static function (a -Wunused-function warning) in every module
|
|
// whose events carry no binary data.
|
|
// ── The generated codec ─────────────────────────────────────────────────────
|
|
//
|
|
// Emitted into the module's types header so the author's impl class and the
|
|
// generated dispatch share one definition of how a value crosses the wire.
|
|
//
|
|
// This is deliberately the same SHAPE as logos-protocol's logos_codec.h — and
|
|
// it exists as generated code only because that header cannot currently be
|
|
// included here: logos_json.h (which every universal module pulls in for
|
|
// LogosMap) and logos_codec.h both define logos::b64UrlEncode /
|
|
// b64UrlDecode / bytesToJson as inline, so including both in one translation
|
|
// unit is a redefinition error. Unify when that is resolved; the emitted
|
|
// specializations would then be the only generated part.
|
|
//
|
|
// The primary template is intentionally left UNDEFINED: an unsupported T is a
|
|
// compile error naming the type, never a silent default-constructed value.
|
|
// Emits ONE specialization per record the module declares — and nothing else.
|
|
//
|
|
// The generic half (scalars, bstr, the vector/map composition, the error paths)
|
|
// used to be emitted here too, ~186 lines of C++-emitting-C++ that mirrored
|
|
// logos-protocol's logos_codec.h by hand. It no longer is: logos_json.h stopped
|
|
// defining byte helpers that collided with that header, so a module TU can now
|
|
// include the canonical codec directly.
|
|
//
|
|
// That duplication was not free. The two copies had drifted (the emitted integer
|
|
// decode gated on is_number() where the canonical one checked
|
|
// is_number_integer() || is_number_unsigned()), they disagreed on padded base64,
|
|
// and every codec fix had to be written twice or it silently only half-applied.
|
|
//
|
|
// What remains is irreducible: a LIDL `type` is a per-contract struct whose field
|
|
// names and member types exist only in this module's header, and C++17 has no
|
|
// field reflection. Nesting composes for free — Codec<std::vector<Blob>> and
|
|
// deeper come from the shared generic half once Codec<::Blob> exists.
|
|
void emitRecordCodecs(QTextStream& s, const ModuleDecl& module,
|
|
const std::set<std::string>& recs)
|
|
{
|
|
if (module.types.empty()) return;
|
|
// Reopened so the specializations land beside the primary template they
|
|
// specialize. `::Name` because the author's record types are at global
|
|
// scope, while this is namespace logos::detail — without the qualifier the
|
|
// name would resolve inside logos::.
|
|
s << "namespace logos { namespace detail {\n\n";
|
|
// One specialization per declared record. Field order follows the contract.
|
|
for (const TypeDecl& t : module.types) {
|
|
const QString name = qs(t.name);
|
|
s << "template <> struct Codec<::" << name << ", void> {\n";
|
|
s << " static nlohmann::json to(const " << name << "& v) {\n";
|
|
s << " nlohmann::json out = nlohmann::json::object();\n";
|
|
for (const FieldDecl& f : t.fields) {
|
|
const QString ft = lidlFieldTypeCdylib(f, recs);
|
|
const QString fn = qs(f.name);
|
|
if (ft.startsWith("std::optional<")) {
|
|
// ENCODE: a record field is a NAMED slot, so empty is spelled by
|
|
// OMITTING the key — never by writing null. This is the half of
|
|
// the rule Codec<std::optional<T>> deliberately cannot do: a
|
|
// codec only ever sees a VALUE, so it emits the positional
|
|
// spelling (null) and leaves key omission to the one place that
|
|
// knows there IS a key. That place is here.
|
|
//
|
|
// The round trip is therefore CANONICALISING, not identity: a
|
|
// peer that sent `"f": null` gets the key back omitted, and both
|
|
// spellings mean the same state.
|
|
const QString vt = lidlTypeToStdCdylib(fieldValueType(f), recs);
|
|
s << " if (v." << fn << ".has_value())\n";
|
|
s << " out[\"" << fn << "\"] = Codec<" << vt << ">::to(*v."
|
|
<< fn << ");\n";
|
|
} else {
|
|
s << " out[\"" << fn << "\"] = Codec<" << ft << ">::to(v."
|
|
<< fn << ");\n";
|
|
}
|
|
}
|
|
s << " return out;\n }\n";
|
|
s << " static " << name << " from(const nlohmann::json& j, const std::string& path) {\n";
|
|
s << " if (!j.is_object()) detail::typeError(path, \"object\", j);\n";
|
|
s << " " << name << " out;\n";
|
|
for (const FieldDecl& f : t.fields) {
|
|
const QString ft = lidlFieldTypeCdylib(f, recs);
|
|
const QString fn = qs(f.name);
|
|
// A missing field is reported at its own path rather than
|
|
// default-constructed: a record that silently loses a field is the
|
|
// failure mode this whole layer exists to prevent.
|
|
//
|
|
// DECODE needs no optional branch, and that is the point: an absent
|
|
// key is already materialised as null right here, so absent and
|
|
// explicit null arrive at the codec indistinguishable. In an
|
|
// optional field Codec<std::optional<T>> answers nullopt for both;
|
|
// in a required one Codec<T> still rejects both. One expression,
|
|
// both halves of the rule.
|
|
s << " out." << fn << " = Codec<" << ft << ">::from(\n";
|
|
s << " j.contains(\"" << fn << "\") ? j.at(\"" << fn
|
|
<< "\") : nlohmann::json(),\n";
|
|
s << " path + \"." << fn << "\");\n";
|
|
}
|
|
s << " return out;\n }\n};\n\n";
|
|
}
|
|
s << "}} // namespace logos::detail\n\n";
|
|
}
|
|
|
|
// The Qt spelling of what actually crosses the Qt boundary.
|
|
//
|
|
// NOT lidlTypeToQt: that answers the CONSUMER's question ("what type does the
|
|
// caller hold?") and since records became real structs it answers `Blob` /
|
|
// `QList<Blob>`. Those names are correct in a generated consumer wrapper, where
|
|
// the struct exists — but this JSON is the module's getMethods(), read by the
|
|
// host to marshal a QVariant across the plugin boundary, and there is no
|
|
// metatype called `Blob`. Emitting it made the host SIGSEGV on the first call
|
|
// to any record method.
|
|
//
|
|
// A record IS a variant map at that boundary; the struct only exists inside the
|
|
// cdylib.
|
|
QString lidlTypeToQtWire(const TypeExpr& te, const std::set<std::string>& recs)
|
|
{
|
|
if (isRecord(te, recs))
|
|
return "QVariantMap";
|
|
if (te.kind == TypeExpr::Array && te.elements.size() == 1
|
|
&& isRecord(te.elements[0], recs))
|
|
return "QVariantList";
|
|
if (te.kind == TypeExpr::Map && te.elements.size() == 2
|
|
&& isRecord(te.elements[1], recs))
|
|
return "QVariantMap";
|
|
return lidlTypeToQt(te);
|
|
}
|
|
|
|
// True when any event parameter is spelled LogosMap / LogosList, so the sidecar
|
|
// needs <logos_json.h> for those aliases.
|
|
bool hasJsonEventParam(const ModuleDecl& module)
|
|
{
|
|
const std::set<std::string> recs = recordNames(module);
|
|
for (const EventDecl& ed : module.events)
|
|
for (const ParamDecl& pd : ed.params) {
|
|
const QString t = lidlTypeToStdCdylib(pd.type, recs);
|
|
if (t == "LogosMap" || t == "LogosList")
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// The generated base64 codec is GONE — all of it.
|
|
//
|
|
// #117 replaced the emitted generic codec with logos-protocol's logos_codec.h,
|
|
// but left behind the base64 pair it had grown around: an encoder
|
|
// (lidlB64UrlEncode / lidlBytesToJson) and a decoder (lidlB64Idx /
|
|
// lidlBytesFromJson), ~89 emitted lines in every module's export TU. The decoder
|
|
// had no call site at all — every byte parameter had already moved to
|
|
// logos::bytesFromJsonLenient — and the encoder was a byte-for-byte reimplementation
|
|
// of logos::bytesToJson, which is included via <logos_codec.h> in the very same
|
|
// translation unit.
|
|
//
|
|
// A second copy of an encoder is not free: this is the arrangement that let the
|
|
// emitted and canonical halves drift over padded base64 once already, and it is
|
|
// exactly the duplication #117's own comment set out to end. Scalar `bstr` slots
|
|
// now call logos::bytesToJson directly, which is what every composite slot
|
|
// (`[bstr]`, `{tstr: bstr}`, records) has been doing through logos::Codec since
|
|
// #117.
|
|
|
|
void emitInterfaceJson(QTextStream& s, const ModuleDecl& module)
|
|
{
|
|
const std::set<std::string> recs = recordNames(module);
|
|
s << "static nlohmann::json lidlInterfaceJson()\n{\n";
|
|
s << " nlohmann::json methods = nlohmann::json::array();\n";
|
|
for (const MethodDecl& md : module.methods) {
|
|
s << " {\n nlohmann::json obj;\n";
|
|
s << " obj[\"name\"] = \"" << md.name << "\";\n";
|
|
if (!md.description.empty()) {
|
|
QString esc = qs(md.description);
|
|
esc.replace('\\', "\\\\").replace('"', "\\\"").replace('\n', "\\n");
|
|
s << " obj[\"description\"] = \"" << esc << "\";\n";
|
|
}
|
|
QString sig = qs(md.name) + "(";
|
|
for (int i = 0; i < md.params.size(); ++i) {
|
|
sig += lidlTypeToQtWire(md.params[i].type, recs);
|
|
if (i + 1 < md.params.size()) sig += ",";
|
|
}
|
|
sig += ")";
|
|
s << " obj[\"signature\"] = \"" << sig << "\";\n";
|
|
s << " obj[\"returnType\"] = \"" << lidlTypeToQtWire(md.returnType, recs) << "\";\n";
|
|
s << " obj[\"isInvokable\"] = true;\n";
|
|
if (!md.params.empty()) {
|
|
s << " nlohmann::json params = nlohmann::json::array();\n";
|
|
for (const ParamDecl& pd : md.params) {
|
|
s << " params.push_back({{\"type\", \"" << lidlTypeToQtWire(pd.type, recs)
|
|
<< "\"}, {\"name\", \"" << pd.name << "\"}});\n";
|
|
}
|
|
s << " obj[\"parameters\"] = params;\n";
|
|
}
|
|
s << " methods.push_back(obj);\n }\n";
|
|
}
|
|
for (const EventDecl& ed : module.events) {
|
|
s << " {\n nlohmann::json obj;\n";
|
|
s << " obj[\"type\"] = \"event\";\n";
|
|
s << " obj[\"name\"] = \"" << ed.name << "\";\n";
|
|
if (!ed.description.empty()) {
|
|
QString esc = qs(ed.description);
|
|
esc.replace('\\', "\\\\").replace('"', "\\\"").replace('\n', "\\n");
|
|
s << " obj[\"description\"] = \"" << esc << "\";\n";
|
|
}
|
|
QString sig = qs(ed.name) + "(";
|
|
for (int i = 0; i < ed.params.size(); ++i) {
|
|
sig += lidlTypeToQtWire(ed.params[i].type, recs);
|
|
if (i + 1 < ed.params.size()) sig += ",";
|
|
}
|
|
sig += ")";
|
|
s << " obj[\"signature\"] = \"" << sig << "\";\n";
|
|
if (!ed.params.empty()) {
|
|
s << " nlohmann::json params = nlohmann::json::array();\n";
|
|
for (const ParamDecl& pd : ed.params) {
|
|
s << " params.push_back({{\"type\", \"" << lidlTypeToQtWire(pd.type, recs)
|
|
<< "\"}, {\"name\", \"" << pd.name << "\"}});\n";
|
|
}
|
|
s << " obj[\"parameters\"] = params;\n";
|
|
}
|
|
s << " methods.push_back(obj);\n }\n";
|
|
}
|
|
s << " return methods;\n}\n\n";
|
|
}
|
|
|
|
} // namespace
|
|
|
|
bool lidlCdylibSupported(const ModuleDecl& module, QString* error)
|
|
{
|
|
const std::set<std::string> recs = recordNames(module);
|
|
for (const MethodDecl& md : module.methods) {
|
|
for (const ParamDecl& pd : md.params) {
|
|
if (!typeSupported(pd.type, /*isReturn=*/false, recs)) {
|
|
if (error)
|
|
*error = QString("method '%1': parameter '%2' has a type outside the "
|
|
"cdylib-supported (Qt-free) subset")
|
|
.arg(qs(md.name), qs(pd.name));
|
|
return false;
|
|
}
|
|
}
|
|
// `void` is not a lidlBuiltinType, so the .lidl parser yields it as a
|
|
// Named type "void" (the impl-header parser writes "-> void"); an empty
|
|
// name is the in-memory void from the header path. Treat both as void.
|
|
const bool voidReturn =
|
|
md.returnType.name == "void"
|
|
|| (md.returnType.kind == TypeExpr::Primitive && md.returnType.name.empty());
|
|
if (!voidReturn && !md.jsonReturn && !md.resultReturn
|
|
&& !typeSupported(md.returnType, /*isReturn=*/true, recs)) {
|
|
if (error)
|
|
*error = QString("method '%1': return type outside the cdylib-supported "
|
|
"(Qt-free) subset").arg(qs(md.name));
|
|
return false;
|
|
}
|
|
}
|
|
for (const EventDecl& ed : module.events) {
|
|
for (const ParamDecl& pd : ed.params) {
|
|
if (!typeSupported(pd.type, /*isReturn=*/false, recs)) {
|
|
if (error)
|
|
*error = QString("event '%1': parameter '%2' has a type outside the "
|
|
"cdylib-supported (Qt-free) subset")
|
|
.arg(qs(ed.name), qs(pd.name));
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
QString lidlMakeTypesHeaderCdylib(const ModuleDecl& module)
|
|
{
|
|
const std::set<std::string> recs = recordNames(module);
|
|
QString c;
|
|
QTextStream s(&c);
|
|
s << "// AUTO-GENERATED by logos-cpp-generator --backend cdylib -- do not edit\n";
|
|
s << "//\n";
|
|
s << "// The record types `" << module.name << "` declares, plus the codec that moves\n";
|
|
s << "// them across the wire. Qt-FREE. The author's impl header includes this and\n";
|
|
s << "// writes the structs directly:\n";
|
|
s << "//\n";
|
|
s << "// Blob echoBlob(const Blob& v);\n";
|
|
s << "//\n";
|
|
s << "// rather than picking fields out of a LogosMap.\n";
|
|
s << "#pragma once\n";
|
|
s << "#include <logos_json.h>\n"; // LogosMap / LogosList aliases
|
|
s << "#include <logos_codec.h>\n"; // logos::Codec — the ONE definition
|
|
s << "#include <cstdint>\n";
|
|
s << "#include <map>\n";
|
|
// Only when the contract actually declares an optional: logos_codec.h
|
|
// already pulls <optional> in, so this is documentation of what the emitted
|
|
// codec names — and emitting it unconditionally would rewrite the types
|
|
// header of every contract that has no optional at all.
|
|
if (moduleUsesOptional(module))
|
|
s << "#include <optional>\n";
|
|
s << "#include <string>\n";
|
|
s << "#include <vector>\n\n";
|
|
|
|
// The structs themselves are the AUTHOR's: this file is included after the
|
|
// impl header, and the contract was derived from those very declarations,
|
|
// so emitting them again is a redefinition error. Only forward
|
|
// declarations, so the codec below can name them in any order.
|
|
if (!module.types.empty()) {
|
|
for (const TypeDecl& t : module.types)
|
|
s << "struct " << qs(t.name) << ";\n";
|
|
s << "\n";
|
|
}
|
|
|
|
emitRecordCodecs(s, module, recs);
|
|
return c;
|
|
}
|
|
|
|
QString lidlMakeModuleImplExports(const ModuleDecl& module,
|
|
const QString& implClass,
|
|
const QString& implHeader)
|
|
{
|
|
const std::set<std::string> recs = recordNames(module);
|
|
QString c;
|
|
QTextStream s(&c);
|
|
|
|
s << "// AUTO-GENERATED by logos-cpp-generator --cdylib -- do not edit\n";
|
|
s << "//\n";
|
|
s << "// The common module-impl C ABI exports (logos_module_impl.h) around the\n";
|
|
s << "// universal impl class `" << implClass << "`. Qt-FREE: compiled into the\n";
|
|
s << "// module's cdylib; the uniform Qt-plugin glue (or a future no-Qt host)\n";
|
|
s << "// drives it exclusively through these symbols.\n";
|
|
s << "#include \"" << implHeader << "\"\n";
|
|
s << "#include \"" << module.name << "_types.h\"\n";
|
|
s << "#include \"logos_module_impl.h\"\n";
|
|
s << "#include \"logos_protocol.h\"\n";
|
|
s << "#include \"logos_module_context.h\"\n";
|
|
s << "#include \"logos_result.h\"\n";
|
|
s << "#include <nlohmann/json.hpp>\n";
|
|
s << "#include <cstdlib>\n";
|
|
s << "#include <cstring>\n";
|
|
s << "#include <atomic>\n";
|
|
s << "#include <map>\n";
|
|
s << "#include <mutex>\n";
|
|
if (moduleUsesOptional(module))
|
|
s << "#include <optional>\n";
|
|
s << "#include <string>\n";
|
|
s << "#include <vector>\n";
|
|
// The Qt-free typed dependency surface: LogosModules (behind modules())
|
|
// built from this module's dependencies (metadata.json#dependencies),
|
|
// calling the lp_* C ABI — no Qt in the cdylib. The umbrella codegen
|
|
// emits logos_sdk.h for every cdylib module (empty when there are no
|
|
// dependencies), so this include is always available.
|
|
s << "#include \"logos_sdk.h\"\n";
|
|
s << "\n";
|
|
|
|
// -- shared statics ------------------------------------------------------
|
|
s << "namespace {\n\n";
|
|
s << implClass << "& lidlImpl()\n{\n static " << implClass << " impl;\n return impl;\n}\n\n";
|
|
s << "logos_module_emit_cb g_emitCb = nullptr;\n";
|
|
s << "void* g_emitUd = nullptr;\n";
|
|
s << "std::mutex g_emitMutex;\n";
|
|
s << "std::mutex g_ctxMutex;\n";
|
|
s << "bool g_ctxStored = false;\n";
|
|
s << "std::string g_ctxPath, g_ctxId, g_ctxPersist;\n";
|
|
s << "std::atomic<bool> g_hookFired{false};\n\n";
|
|
|
|
s << "char* lidlStrdup(const std::string& str)\n{\n";
|
|
s << " char* out = static_cast<char*>(std::malloc(str.size() + 1));\n";
|
|
s << " if (out) std::memcpy(out, str.data(), str.size() + 1);\n";
|
|
s << " return out;\n}\n\n";
|
|
|
|
s << "nlohmann::json lidlResultToJson(const StdLogosResult& r)\n{\n";
|
|
s << " nlohmann::json obj;\n";
|
|
s << " obj[\"success\"] = r.success;\n";
|
|
s << " obj[\"value\"] = r.value;\n";
|
|
s << " obj[\"error\"] = r.error.empty() ? nlohmann::json() : nlohmann::json(r.error);\n";
|
|
s << " return obj;\n}\n\n";
|
|
|
|
emitInterfaceJson(s, module);
|
|
s << "} // namespace\n\n";
|
|
|
|
// -- event wiring (install once, lazily) ---------------------------------
|
|
s << "static void lidlEnsureEmitWiring()\n{\n";
|
|
s << " static std::once_flag once;\n";
|
|
s << " std::call_once(once, []() {\n";
|
|
s << " _logos_codegen_::maybeSetEmitEvent(lidlImpl(),\n";
|
|
s << " [](const std::string& name, void* args) {\n";
|
|
s << " // cdylib events sidecar marshals into nlohmann::json\n";
|
|
s << " const nlohmann::json* payload = static_cast<const nlohmann::json*>(args);\n";
|
|
s << " std::lock_guard<std::mutex> lock(g_emitMutex);\n";
|
|
s << " if (g_emitCb) {\n";
|
|
s << " const std::string dumped = payload ? payload->dump() : \"[]\";\n";
|
|
s << " g_emitCb(name.c_str(), dumped.c_str(), g_emitUd);\n";
|
|
s << " }\n";
|
|
s << " });\n";
|
|
s << " });\n}\n\n";
|
|
|
|
// -- typed dependency surface (modules().<dep>...) -----------------------
|
|
// Wire modules() INDEPENDENTLY of the persistence context. Each dependency
|
|
// client bakes its target+origin at codegen time and creates its lp client
|
|
// lazily on first call, so modules() needs nothing from the context. A
|
|
// module with deps but no STORED context still must have it wired — gating
|
|
// it on the context latch (as it used to be) left m_logosModulesPtr null and
|
|
// segfaulted the first cross-module call when the daemon never delivered a
|
|
// context. No-op for impls that don't derive LogosModuleContext. Fired once
|
|
// from the FIRST lidlTryFireContext (i.e. the first dispatch / set_context /
|
|
// set_emit_callback), before the context-gated early return below.
|
|
s << "static void lidlEnsureModulesWired()\n{\n";
|
|
s << " static std::once_flag once;\n";
|
|
s << " std::call_once(once, []() {\n";
|
|
s << " _logos_codegen_::maybeSetLogosModules(lidlImpl(), new LogosModules());\n";
|
|
s << " });\n}\n\n";
|
|
|
|
// The context ready-latch: stamp the context + fire onContextReady ONCE,
|
|
// as soon as the module is fully wired (context stored AND the emit
|
|
// callback delivered) — at module load, before publication. Hosts that
|
|
// never wire an emit callback still get the hook before first dispatch
|
|
// (requireEmit = false fallback).
|
|
s << "static void lidlTryFireContext(bool requireEmit)\n{\n";
|
|
s << " lidlEnsureEmitWiring();\n";
|
|
s << " lidlEnsureModulesWired();\n";
|
|
s << " if (g_hookFired.load(std::memory_order_acquire)) return;\n";
|
|
s << " std::string path, id, persist;\n";
|
|
s << " {\n";
|
|
s << " std::lock_guard<std::mutex> lock(g_ctxMutex);\n";
|
|
s << " if (!g_ctxStored) return;\n";
|
|
s << " path = g_ctxPath; id = g_ctxId; persist = g_ctxPersist;\n";
|
|
s << " }\n";
|
|
s << " if (requireEmit) {\n";
|
|
s << " std::lock_guard<std::mutex> lock(g_emitMutex);\n";
|
|
s << " if (!g_emitCb) return;\n";
|
|
s << " }\n";
|
|
s << " g_hookFired.store(true, std::memory_order_release);\n";
|
|
// modules() was already wired by lidlEnsureModulesWired() above (before this
|
|
// context-gated early return), so onContextReady can safely call
|
|
// modules().<dep>... / subscribe to dependency events from the hook.
|
|
// The module's own registry name, which the generator knows statically.
|
|
// Set BEFORE the context so moduleName() is live inside onContextReady().
|
|
s << " _logos_codegen_::maybeSetModuleName(lidlImpl(), \"" << module.name << "\");\n";
|
|
s << " _logos_codegen_::maybeSetContext(lidlImpl(), path, id, persist);\n";
|
|
s << "}\n\n";
|
|
|
|
// -- exports -------------------------------------------------------------
|
|
s << "extern \"C\" {\n\n";
|
|
|
|
s << "char* logos_module_dispatch(const char* method, const char* args_json)\n{\n";
|
|
s << " if (!method) return nullptr;\n";
|
|
s << " lidlTryFireContext(false);\n";
|
|
s << " nlohmann::json args = nlohmann::json::array();\n";
|
|
s << " if (args_json && *args_json) {\n";
|
|
s << " args = nlohmann::json::parse(args_json, nullptr, false);\n";
|
|
s << " if (args.is_discarded() || !args.is_array()) return nullptr;\n";
|
|
s << " }\n";
|
|
s << " const std::string m(method);\n";
|
|
s << " try {\n";
|
|
|
|
for (const MethodDecl& md : module.methods) {
|
|
// The arity gate, and the one place the LIBERAL half of the decode rule
|
|
// reaches a POSITIONAL slot.
|
|
//
|
|
// A canonical encoder never changes arity: an empty positional slot is
|
|
// spelled null and still occupies its position. But absent and null are
|
|
// the same state on decode, so an optional trailing argument may also
|
|
// simply not be there. The gate therefore admits anything from the last
|
|
// REQUIRED parameter onwards, and each optional beyond it materialises
|
|
// as null exactly the way an absent record field already does. Below
|
|
// that point nothing changes: a missing required argument is still a
|
|
// hard reject, and a contract with no optional parameters emits the
|
|
// byte-identical `args.size() < <count>` it always did.
|
|
size_t minArgs = 0;
|
|
for (size_t i = 0; i < md.params.size(); ++i)
|
|
if (!paramIsOptional(md.params[i])) minArgs = i + 1;
|
|
s << " if (m == \"" << md.name << "\") {\n";
|
|
// A wrong argument COUNT is reported, not swallowed.
|
|
//
|
|
// This used to be `return nullptr`, and the Qt glue turns a NULL reply
|
|
// into an empty QVariant — indistinguishable from a method that
|
|
// legitimately returned nothing. "You passed 2 of 4 arguments" looked
|
|
// like a successful empty answer.
|
|
//
|
|
// The shape is the one logos-rust-sdk's args::invalid_args() already
|
|
// emits (src/args.rs), so a C++ and a Rust provider answer a malformed
|
|
// call identically — which is what that module's
|
|
// invalid_args_shape_matches_cpp test claims, and what was not true
|
|
// until now. Same three keys, same message text, same `origin`.
|
|
//
|
|
// Emitted only when the method has at least one REQUIRED parameter:
|
|
// `args.size() < 0` is unsigned-compared and always false, so a zero-arg
|
|
// method carried a dead branch (the Rust generator skips it for the same
|
|
// reason).
|
|
if (minArgs > 0) {
|
|
s << " if (args.size() < " << minArgs << ") {\n";
|
|
s << " nlohmann::json err{{\"code\", \"invalid_args\"},\n";
|
|
s << " {\"message\", \"expected " << minArgs
|
|
<< " arguments, got \" + std::to_string(args.size())},\n";
|
|
s << " {\"origin\", \"" << module.name << "\"}};\n";
|
|
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)
|
|
? QString("args.at(%1)").arg(i)
|
|
: QString("(args.size() > %1 ? args.at(%1) : nlohmann::json())").arg(i);
|
|
call += jsonArgToStd(md.params[i].type, expr,
|
|
QString("arg%1").arg(i), recs);
|
|
if (i + 1 < md.params.size()) call += ", ";
|
|
}
|
|
call += ")";
|
|
// `void` parses as a Named type "void" from a .lidl (it isn't a
|
|
// lidlBuiltinType); empty name is the header path's in-memory void.
|
|
const bool voidReturn =
|
|
md.returnType.name == "void"
|
|
|| (md.returnType.kind == TypeExpr::Primitive && md.returnType.name.empty())
|
|
|| lidlTypeToQt(md.returnType) == "void";
|
|
if (voidReturn) {
|
|
s << " " << call << ";\n";
|
|
s << " return lidlStrdup(\"true\");\n";
|
|
} else {
|
|
s << " auto result = " << call << ";\n";
|
|
s << " return lidlStrdup(" << stdReturnToJson(md, "result", recs) << ".dump());\n";
|
|
}
|
|
s << " }\n";
|
|
}
|
|
|
|
s << " } catch (const std::exception& e) {\n";
|
|
s << " nlohmann::json err{{\"code\", \"dispatch_failed\"}, {\"message\", e.what()},\n";
|
|
s << " {\"origin\", \"" << module.name << "\"}};\n";
|
|
s << " return lidlStrdup(err.dump());\n";
|
|
s << " }\n";
|
|
s << " return nullptr; // unknown method\n";
|
|
s << "}\n\n";
|
|
|
|
s << "char* logos_module_get_methods(void)\n{\n";
|
|
s << " return lidlStrdup(lidlInterfaceJson().dump());\n}\n\n";
|
|
|
|
s << "void logos_module_set_context(const char* module_path,\n";
|
|
s << " const char* instance_id,\n";
|
|
s << " const char* instance_persistence_path)\n{\n";
|
|
s << " {\n";
|
|
s << " std::lock_guard<std::mutex> lock(g_ctxMutex);\n";
|
|
s << " g_ctxPath = module_path ? module_path : \"\";\n";
|
|
s << " g_ctxId = instance_id ? instance_id : \"\";\n";
|
|
s << " g_ctxPersist = instance_persistence_path ? instance_persistence_path : \"\";\n";
|
|
s << " g_ctxStored = true;\n";
|
|
s << " }\n";
|
|
s << " lidlTryFireContext(true);\n";
|
|
s << "}\n\n";
|
|
|
|
s << "void logos_module_set_emit_callback(logos_module_emit_cb cb, void* user_data)\n{\n";
|
|
s << " {\n";
|
|
s << " std::lock_guard<std::mutex> lock(g_emitMutex);\n";
|
|
s << " g_emitCb = cb;\n";
|
|
s << " g_emitUd = user_data;\n";
|
|
s << " }\n";
|
|
s << " lidlTryFireContext(true);\n";
|
|
s << "}\n\n";
|
|
|
|
s << "int logos_module_accept_token(const char* module_name, const char* token)\n{\n";
|
|
s << " if (!module_name || !token) return -1;\n";
|
|
s << " // Seed the protocol's shared TokenManager so this module's OUTBOUND\n";
|
|
s << " // lp_client (modules().<dep>...) can authenticate calls. In\n";
|
|
s << " // particular the capability_module bootstrap token the host\n";
|
|
s << " // delivers at load lets the automatic requestModule flow fetch a\n";
|
|
s << " // per-target token on the first cross-module call. lp_token_save\n";
|
|
s << " // writes the same TokenManager::instance() the lp_client reads.\n";
|
|
s << " return lp_token_save(module_name, token);\n}\n\n";
|
|
|
|
// Guarded on the protocol MINOR that introduced the trust-root surface
|
|
// (0.3). The emitted module must still COMPILE against an older
|
|
// logos-protocol, which has neither lp_grant_host_services nor the
|
|
// logos_module_impl.h declaration — a module built against 0.2 simply has
|
|
// no grant entry point, which is the same fail-closed state as never being
|
|
// granted. Without this an older protocol is a hard compile error in
|
|
// generated code the author never sees.
|
|
s << "#if defined(LOGOS_PROTOCOL_VERSION_MINOR) && LOGOS_PROTOCOL_VERSION_MINOR >= 3\n";
|
|
s << "int logos_module_grant_host_services(const char* services_json)\n{\n";
|
|
s << " // Route the host's grant into THIS image's gate state.\n";
|
|
s << " //\n";
|
|
s << " // The grant has to travel over the C ABI rather than being\n";
|
|
s << " // recorded once by the host, and that is the whole reason this\n";
|
|
s << " // export exists: the host binary and this cdylib each link their\n";
|
|
s << " // own copy of logos-protocol, so each has its own process-global\n";
|
|
s << " // grant state, exactly as each has its own TokenManager. A grant\n";
|
|
s << " // the host records for itself is invisible to the gate a\n";
|
|
s << " // lp_token_keys() call checks HERE, so a gate 'simplified' into\n";
|
|
s << " // the host would silently never fire.\n";
|
|
s << " //\n";
|
|
s << " // Emitted unconditionally, for every module, rather than behind a\n";
|
|
s << " // codegen flag: which modules are privileged is the HOST's\n";
|
|
s << " // decision (it chooses what to push, and pushes nothing to an\n";
|
|
s << " // ordinary module), and lp_grant_host_services itself validates\n";
|
|
s << " // the names and fails closed. A per-module flag would only add a\n";
|
|
s << " // second place for the two to disagree.\n";
|
|
s << " //\n";
|
|
s << " // NOTE this is a declaration-and-audit boundary, NOT a defence\n";
|
|
s << " // against a hostile module: this cdylib links logos-protocol, so\n";
|
|
s << " // its own code can call lp_grant_host_services() directly and\n";
|
|
s << " // self-grant. What the gate buys is that the privilege is\n";
|
|
s << " // explicit, greppable and off by default, so no module acquires\n";
|
|
s << " // it by accident. Isolation between modules rests on process\n";
|
|
s << " // separation, the auth token and the target's allowedCallers.\n";
|
|
s << " return lp_grant_host_services(services_json);\n}\n";
|
|
s << "#endif\n\n";
|
|
|
|
s << "const char* logos_module_get_protocol_version(void)\n{\n";
|
|
s << " return LOGOS_PROTOCOL_VERSION_STRING;\n}\n\n";
|
|
|
|
s << "void logos_module_string_free(char* str)\n{\n";
|
|
s << " std::free(str);\n}\n\n";
|
|
|
|
s << "} // extern \"C\"\n";
|
|
return c;
|
|
}
|
|
|
|
QString lidlMakeEventsSourceCdylib(const ModuleDecl& module,
|
|
const QString& implClass,
|
|
const QString& implHeader)
|
|
{
|
|
QString c;
|
|
QTextStream s(&c);
|
|
s << "// AUTO-GENERATED by logos-cpp-generator --cdylib -- do not edit\n";
|
|
s << "// Typed `logos_events:` bodies, cdylib flavor: marshal into\n";
|
|
s << "// nlohmann::json and route through LogosModuleContext::emitEventImpl_\n";
|
|
s << "// (the export wrapper forwards to the host's emit callback).\n";
|
|
const std::set<std::string> recsEv = recordNames(module);
|
|
s << "#include \"" << implHeader << "\"\n";
|
|
s << "#include \"" << module.name << "_types.h\"\n";
|
|
s << "#include <nlohmann/json.hpp>\n\n";
|
|
s << "#include <cstdint>\n";
|
|
s << "#include <map>\n";
|
|
if (moduleUsesOptional(module))
|
|
s << "#include <optional>\n";
|
|
s << "#include <string>\n";
|
|
s << "#include <vector>\n";
|
|
// LogosMap / LogosList (nlohmann aliases) appear in the emitted signatures
|
|
// whenever an event carries a map or an `any` payload.
|
|
if (hasJsonEventParam(module))
|
|
s << "#include <logos_json.h>\n";
|
|
s << "\n";
|
|
|
|
// No local bytes encoder any more, and so no hasBytesEventParam() gate for
|
|
// it either: a `bstr` event parameter calls logos::bytesToJson, which the
|
|
// <logos_codec.h> pulled in by "<module>_types.h" above already provides.
|
|
// The gate existed only to keep the emitted copy from sitting unused in
|
|
// modules whose events carry no binary data.
|
|
|
|
for (const EventDecl& ed : module.events) {
|
|
s << "void " << implClass << "::" << ed.name << "(";
|
|
for (int i = 0; i < ed.params.size(); ++i) {
|
|
const QString stdType = lidlTypeToStdCdylib(ed.params[i].type, recsEv);
|
|
// Must match the author's declaration in the `logos_events:` block:
|
|
// the non-scalar types are conventionally taken by const-ref there.
|
|
// Records and std::map belong in that set too — they are structs and
|
|
// containers, and emitting them BY VALUE makes the generated
|
|
// definition not match the author's declaration, which is a compile
|
|
// error naming a parameter type mismatch rather than anything
|
|
// helpful.
|
|
if (stdType == "std::string" || stdType.startsWith("std::vector")
|
|
|| stdType.startsWith("std::map")
|
|
|| stdType.startsWith("std::optional")
|
|
|| isRecord(ed.params[i].type, recsEv)
|
|
|| stdType == "LogosMap" || stdType == "LogosList")
|
|
s << "const " << stdType << "& " << ed.params[i].name;
|
|
else
|
|
s << stdType << " " << ed.params[i].name;
|
|
if (i + 1 < ed.params.size()) s << ", ";
|
|
}
|
|
s << ")\n{\n";
|
|
s << " nlohmann::json args = nlohmann::json::array();\n";
|
|
for (const ParamDecl& pd : ed.params) {
|
|
const QString evStd = lidlTypeToStdCdylib(pd.type, recsEv);
|
|
// A record or a composite carrying bytes rides the generated codec,
|
|
// exactly like a method return — otherwise an event payload would be
|
|
// the one place a bstr silently loses its tag.
|
|
//
|
|
// An optional joins them: an event parameter is a POSITIONAL slot,
|
|
// so empty is spelled null and the argument list keeps its length.
|
|
// Codec<std::optional<T>>::to answers exactly that. (`?any` collapsed
|
|
// to LogosMap above and is excluded by the same guard the untyped
|
|
// aliases always were.)
|
|
if (evStd != "LogosMap" && evStd != "LogosList"
|
|
&& (isRecord(pd.type, recsEv)
|
|
|| pd.type.kind == TypeExpr::Array || pd.type.kind == TypeExpr::Map
|
|
|| pd.type.kind == TypeExpr::Optional)) {
|
|
s << " args.push_back(logos::toJson<" << evStd << ">("
|
|
<< pd.name << "));\n";
|
|
continue;
|
|
}
|
|
if (pd.type.kind == TypeExpr::Primitive && pd.type.name == "bstr")
|
|
s << " args.push_back(logos::bytesToJson(" << pd.name << "));\n";
|
|
else
|
|
s << " args.push_back(" << pd.name << ");\n";
|
|
}
|
|
s << " emitEventImpl_(\"" << ed.name << "\", &args);\n";
|
|
s << "}\n\n";
|
|
}
|
|
return c;
|
|
}
|