mirror of
https://github.com/logos-co/logos-cpp-sdk.git
synced 2026-08-31 09:41:06 +00:00
Mirrors logos-protocol fix/integral-float-accept. The generator emits its OWN codec into <name>_types.h, so the rule has to be applied twice or half the platform disagrees — the same split that made the original scalar fix need two sites. #115 made the emitted integer codecs check signedness, and in doing so they started rejecting 3.0 as well as 3.7. Four test_basic_module_cpp cases pass a whole-valued double where the contract declares an integer, and they are right: JSON does not distinguish 3 from 3.0, and this same codec accepts an integral number for float64 on exactly that reasoning. A float now decodes as an integer when it has no fractional part and fits. 3.7 is still refused. Also adds <cmath> to the emitted include set for std::modf. verified: test-modules 176/176 with the four cases restored, conformance matrix unchanged at 170 pass / 2 xfail. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
916 lines
46 KiB
C++
916 lines
46 KiB
C++
#include "lidl_gen_cdylib.h"
|
|
#include "lidl_emit_common.h"
|
|
|
|
#include <QTextStream>
|
|
|
|
#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;
|
|
// 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)
|
|
{
|
|
if (te.kind == TypeExpr::Primitive) {
|
|
if (te.name == "bstr") return "lidlBytesFromJson(" + expr + ")";
|
|
if (te.name == "any") return expr;
|
|
}
|
|
const QString cpp = lidlTypeToStdCdylib(te, recs);
|
|
if (cpp == "LogosMap" || cpp == "LogosList")
|
|
return expr; // untyped JSON passes through, as it always has
|
|
return "logos_gen::Codec<" + cpp + ">::from(" + 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 "lidlBytesToJson(" + var + ")";
|
|
if (te.name == "any") return var;
|
|
return "nlohmann::json(" + var + ")";
|
|
}
|
|
if (cppRet == "LogosMap" || cppRet == "LogosList")
|
|
return 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_gen::Codec<" + cppRet + ">::to(" + 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)
|
|
{
|
|
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);
|
|
}
|
|
|
|
// 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.
|
|
void emitGeneratedCodec(QTextStream& s, const ModuleDecl& module,
|
|
const std::set<std::string>& recs)
|
|
{
|
|
s << "namespace logos_gen {\n\n";
|
|
s << "// Codec<T>::to / ::from — the one place a value's wire form is decided.\n";
|
|
s << "// The primary template is undefined on purpose: an unsupported T is a\n";
|
|
s << "// compile error naming the type, not a silent default.\n";
|
|
s << "template <class T> struct Codec;\n\n";
|
|
|
|
s << "[[noreturn]] inline void lidlTypeError(const char* want, const std::string& path,\n";
|
|
s << " const nlohmann::json& got)\n{\n";
|
|
s << " throw std::runtime_error(std::string(\"expected \") + want + \" at \" + path\n";
|
|
s << " + \", got \" + std::string(got.type_name()));\n}\n\n";
|
|
|
|
// Scalars that need no more than a category check.
|
|
struct Scalar { const char* cpp; const char* want; const char* check; const char* get; };
|
|
const Scalar scalars[] = {
|
|
{"std::string", "string", "is_string()", "get<std::string>()"},
|
|
{"double", "number", "is_number()", "get<double>()"},
|
|
{"bool", "boolean", "is_boolean()", "get<bool>()"},
|
|
};
|
|
for (const Scalar& sc : scalars) {
|
|
s << "template <> struct Codec<" << sc.cpp << "> {\n";
|
|
s << " static nlohmann::json to(const " << sc.cpp << "& v) { return nlohmann::json(v); }\n";
|
|
s << " static " << sc.cpp << " from(const nlohmann::json& j, const std::string& path) {\n";
|
|
s << " if (!j." << sc.check << ") lidlTypeError(\"" << sc.want << "\", path, j);\n";
|
|
s << " return j." << sc.get << ";\n }\n};\n\n";
|
|
}
|
|
|
|
// The integers are spelled out rather than driven from the table above,
|
|
// because a category check is not enough for them and the shortcuts are
|
|
// silent rather than loud:
|
|
//
|
|
// is_number() admits a FLOAT, and .get<int64_t>() TRUNCATES it — 3.7
|
|
// arrived as 3 instead of being rejected.
|
|
// is_number() admits a NEGATIVE, and .get<uint64_t>() WRAPS it — -1
|
|
// arrived as 18446744073709551615, a sign flip on a nominal value.
|
|
//
|
|
// Both used to be pinned as conformance expectations, which made the C++
|
|
// provider disagree with the Rust one (which rejects) on a contract they
|
|
// share. Rejecting is the correct half of that disagreement: a value the
|
|
// declared type cannot represent must not reach the author wearing another.
|
|
s << "template <> struct Codec<int64_t> {\n";
|
|
s << " static nlohmann::json to(const int64_t& v) { return nlohmann::json(v); }\n";
|
|
s << " static int64_t from(const nlohmann::json& j, const std::string& path) {\n";
|
|
s << " if (j.is_number_float()) {\n";
|
|
s << " const double d = j.get<double>();\n";
|
|
s << " double ip = 0.0;\n";
|
|
s << " if (std::modf(d, &ip) != 0.0)\n";
|
|
s << " lidlTypeError(\"integer\", path, j);\n";
|
|
s << " if (d < -9223372036854775808.0 || d >= 9223372036854775808.0)\n";
|
|
s << " lidlTypeError(\"signed integer in range\", path, j);\n";
|
|
s << " return static_cast<int64_t>(d);\n";
|
|
s << " }\n";
|
|
s << " if (!j.is_number_integer() && !j.is_number_unsigned())\n";
|
|
s << " lidlTypeError(\"integer\", path, j);\n";
|
|
s << " if (j.is_number_unsigned()\n";
|
|
s << " && j.get<uint64_t>() > uint64_t(std::numeric_limits<int64_t>::max()))\n";
|
|
s << " lidlTypeError(\"signed integer in range\", path, j);\n";
|
|
s << " return j.get<int64_t>();\n }\n};\n\n";
|
|
|
|
s << "template <> struct Codec<uint64_t> {\n";
|
|
s << " static nlohmann::json to(const uint64_t& v) { return nlohmann::json(v); }\n";
|
|
s << " static uint64_t from(const nlohmann::json& j, const std::string& path) {\n";
|
|
s << " if (j.is_number_float()) {\n";
|
|
s << " const double d = j.get<double>();\n";
|
|
s << " double ip = 0.0;\n";
|
|
s << " if (std::modf(d, &ip) != 0.0)\n";
|
|
s << " lidlTypeError(\"integer\", path, j);\n";
|
|
s << " if (d < 0.0 || d >= 18446744073709551616.0)\n";
|
|
s << " lidlTypeError(\"unsigned integer in range\", path, j);\n";
|
|
s << " return static_cast<uint64_t>(d);\n";
|
|
s << " }\n";
|
|
s << " if (!j.is_number_integer() && !j.is_number_unsigned())\n";
|
|
s << " lidlTypeError(\"integer\", path, j);\n";
|
|
s << " if (!j.is_number_unsigned() && j.get<int64_t>() < 0)\n";
|
|
s << " lidlTypeError(\"unsigned integer\", path, j);\n";
|
|
s << " return j.get<uint64_t>();\n }\n};\n\n";
|
|
|
|
// bstr. The FULL specialization wins over the generic vector rule below,
|
|
// which is what keeps bytes tagged at every depth instead of being
|
|
// serialized as a plain array of numbers.
|
|
s << "template <> struct Codec<std::vector<uint8_t>> {\n";
|
|
s << " static nlohmann::json to(const std::vector<uint8_t>& v) { return logos::bytesToJson(v); }\n";
|
|
s << " static std::vector<uint8_t> from(const nlohmann::json& j, const std::string& path) {\n";
|
|
s << " if (j.is_object() && j.size() == 1 && j.contains(\"_bytes\")\n";
|
|
s << " && j.at(\"_bytes\").is_string())\n";
|
|
s << " return logos::jsonToBytes(j);\n";
|
|
s << " lidlTypeError(\"bytes\", path, j);\n }\n};\n\n";
|
|
|
|
// Untyped JSON passes through unchanged — `any`, and the LogosMap/LogosList
|
|
// aliases, are all nlohmann::json.
|
|
s << "template <> struct Codec<nlohmann::json> {\n";
|
|
s << " static nlohmann::json to(const nlohmann::json& v) { return v; }\n";
|
|
s << " static nlohmann::json from(const nlohmann::json& j, const std::string&) { return j; }\n";
|
|
s << "};\n\n";
|
|
|
|
s << "template <class T> struct Codec<std::vector<T>> {\n";
|
|
s << " static nlohmann::json to(const std::vector<T>& v) {\n";
|
|
s << " nlohmann::json out = nlohmann::json::array();\n";
|
|
s << " for (const T& e : v) out.push_back(Codec<T>::to(e));\n";
|
|
s << " return out;\n }\n";
|
|
s << " static std::vector<T> from(const nlohmann::json& j, const std::string& path) {\n";
|
|
s << " if (!j.is_array()) lidlTypeError(\"array\", path, j);\n";
|
|
s << " std::vector<T> out;\n out.reserve(j.size());\n";
|
|
s << " for (size_t i = 0; i < j.size(); ++i)\n";
|
|
s << " out.push_back(Codec<T>::from(j.at(i), path + \"[\" + std::to_string(i) + \"]\"));\n";
|
|
s << " return out;\n }\n};\n\n";
|
|
|
|
s << "template <class T> struct Codec<std::map<std::string, T>> {\n";
|
|
s << " static nlohmann::json to(const std::map<std::string, T>& v) {\n";
|
|
s << " nlohmann::json out = nlohmann::json::object();\n";
|
|
s << " for (const auto& kv : v) out[kv.first] = Codec<T>::to(kv.second);\n";
|
|
s << " return out;\n }\n";
|
|
s << " static std::map<std::string, T> from(const nlohmann::json& j, const std::string& path) {\n";
|
|
s << " if (!j.is_object()) lidlTypeError(\"object\", path, j);\n";
|
|
s << " std::map<std::string, T> out;\n";
|
|
s << " for (auto it = j.begin(); it != j.end(); ++it)\n";
|
|
s << " out.emplace(it.key(), Codec<T>::from(it.value(), path + \".\" + it.key()));\n";
|
|
s << " return out;\n }\n};\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 << "> {\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 = lidlTypeToStdCdylib(f.type, recs);
|
|
s << " out[\"" << qs(f.name) << "\"] = Codec<" << ft << ">::to(v."
|
|
<< qs(f.name) << ");\n";
|
|
}
|
|
s << " return out;\n }\n";
|
|
s << " static " << name << " from(const nlohmann::json& j, const std::string& path) {\n";
|
|
s << " if (!j.is_object()) lidlTypeError(\"object\", path, j);\n";
|
|
s << " " << name << " out;\n";
|
|
for (const FieldDecl& f : t.fields) {
|
|
const QString ft = lidlTypeToStdCdylib(f.type, 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.
|
|
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_gen\n\n";
|
|
}
|
|
|
|
bool hasBytesEventParam(const ModuleDecl& module)
|
|
{
|
|
for (const EventDecl& ed : module.events)
|
|
for (const ParamDecl& pd : ed.params)
|
|
if (pd.type.kind == TypeExpr::Primitive && pd.type.name == "bstr")
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
// 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 SCALAR tagged-bytes helpers. A `[bstr]` (and bytes at any deeper
|
|
// nesting) rides logos_gen::Codec instead: its full specialization for
|
|
// std::vector<uint8_t> beats the generic vector rule, so one mechanism covers
|
|
// [bstr], [[bstr]] and {tstr: [bstr]} alike. #111 emitted a dedicated depth-1
|
|
// list codec here; the generic one subsumes it, and keeping both left an
|
|
// unused static in every module that mentioned [bstr].
|
|
void emitBytesEncodeHelpers(QTextStream& s)
|
|
{
|
|
s << "// Canonical tagged bytes form {\"_bytes\": base64url} (see logos_protocol.h)\n";
|
|
s << "std::string lidlB64UrlEncode(const std::vector<uint8_t>& bytes)\n{\n";
|
|
s << " static const char* alpha = \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_\";\n";
|
|
s << " std::string out;\n";
|
|
s << " size_t i = 0;\n";
|
|
s << " while (i + 3 <= bytes.size()) {\n";
|
|
s << " uint32_t n = (uint32_t(bytes[i]) << 16) | (uint32_t(bytes[i+1]) << 8) | uint32_t(bytes[i+2]);\n";
|
|
s << " out += alpha[(n >> 18) & 0x3f]; out += alpha[(n >> 12) & 0x3f];\n";
|
|
s << " out += alpha[(n >> 6) & 0x3f]; out += alpha[n & 0x3f];\n";
|
|
s << " i += 3;\n }\n";
|
|
s << " if (i < bytes.size()) {\n";
|
|
s << " uint32_t n = uint32_t(bytes[i]) << 16;\n";
|
|
s << " if (i + 1 < bytes.size()) n |= uint32_t(bytes[i+1]) << 8;\n";
|
|
s << " out += alpha[(n >> 18) & 0x3f]; out += alpha[(n >> 12) & 0x3f];\n";
|
|
s << " if (i + 1 < bytes.size()) out += alpha[(n >> 6) & 0x3f];\n";
|
|
s << " }\n return out;\n}\n\n";
|
|
|
|
s << "nlohmann::json lidlBytesToJson(const std::vector<uint8_t>& bytes)\n{\n";
|
|
s << " return nlohmann::json{{\"_bytes\", lidlB64UrlEncode(bytes)}};\n}\n\n";
|
|
|
|
}
|
|
|
|
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";
|
|
s << "#include <cstdint>\n";
|
|
s << "#include <cmath>\n"; // the integer codecs accept whole-valued floats
|
|
s << "#include <limits>\n"; // ...and range-check
|
|
s << "#include <map>\n";
|
|
s << "#include <stdexcept>\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";
|
|
}
|
|
|
|
emitGeneratedCodec(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";
|
|
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";
|
|
|
|
emitBytesEncodeHelpers(s);
|
|
|
|
s << "int lidlB64Idx(char ch)\n{\n";
|
|
s << " if (ch >= 'A' && ch <= 'Z') return ch - 'A';\n";
|
|
s << " if (ch >= 'a' && ch <= 'z') return ch - 'a' + 26;\n";
|
|
s << " if (ch >= '0' && ch <= '9') return ch - '0' + 52;\n";
|
|
s << " if (ch == '-') return 62;\n if (ch == '_') return 63;\n return -1;\n}\n\n";
|
|
|
|
s << "std::vector<uint8_t> lidlBytesFromJson(const nlohmann::json& j)\n{\n";
|
|
s << " std::vector<uint8_t> out;\n";
|
|
s << " // Lenient bytes decode (matches the std path, where a QString or\n";
|
|
s << " // QByteArray arg both became bytes): a caller may send the tagged\n";
|
|
s << " // {\"_bytes\": base64url} form, a plain string (raw UTF-8 bytes), or\n";
|
|
s << " // an array of byte values. Only the tagged form needs base64.\n";
|
|
s << " if (j.is_string()) {\n";
|
|
s << " const std::string s = j.get<std::string>();\n";
|
|
s << " out.assign(s.begin(), s.end());\n";
|
|
s << " return out;\n";
|
|
s << " }\n";
|
|
s << " if (j.is_number()) {\n";
|
|
s << " // A number arg becomes its decimal text as bytes — matches\n";
|
|
s << " // Qt's QVariant(int)->QByteArray, so a caller (or the\n";
|
|
s << " // logoscore CLI's type auto-detection) passing a bare number\n";
|
|
s << " // to a bytes param behaves the same as the Qt path.\n";
|
|
s << " const std::string s = j.dump();\n";
|
|
s << " out.assign(s.begin(), s.end());\n";
|
|
s << " return out;\n";
|
|
s << " }\n";
|
|
s << " if (j.is_array()) {\n";
|
|
s << " for (const auto& e : j)\n";
|
|
s << " if (e.is_number_integer() || e.is_number_unsigned())\n";
|
|
s << " out.push_back(static_cast<uint8_t>(e.get<int64_t>() & 0xff));\n";
|
|
s << " return out;\n";
|
|
s << " }\n";
|
|
s << " if (!j.is_object() || j.size() != 1 || !j.contains(\"_bytes\") || !j[\"_bytes\"].is_string())\n";
|
|
s << " return out;\n";
|
|
s << " const std::string s64 = j[\"_bytes\"].get<std::string>();\n";
|
|
s << " size_t i = 0;\n";
|
|
s << " while (i + 4 <= s64.size()) {\n";
|
|
s << " int a = lidlB64Idx(s64[i]), b = lidlB64Idx(s64[i+1]), c2 = lidlB64Idx(s64[i+2]), d = lidlB64Idx(s64[i+3]);\n";
|
|
s << " if (a < 0 || b < 0 || c2 < 0 || d < 0) return {};\n";
|
|
s << " uint32_t n = (uint32_t(a) << 18) | (uint32_t(b) << 12) | (uint32_t(c2) << 6) | uint32_t(d);\n";
|
|
s << " out.push_back((n >> 16) & 0xff); out.push_back((n >> 8) & 0xff); out.push_back(n & 0xff);\n";
|
|
s << " i += 4;\n }\n";
|
|
s << " size_t rem = s64.size() - i;\n";
|
|
s << " if (rem == 2 || rem == 3) {\n";
|
|
s << " int a = lidlB64Idx(s64[i]), b = lidlB64Idx(s64[i+1]);\n";
|
|
s << " if (a < 0 || b < 0) return {};\n";
|
|
s << " uint32_t n = (uint32_t(a) << 18) | (uint32_t(b) << 12);\n";
|
|
s << " out.push_back((n >> 16) & 0xff);\n";
|
|
s << " if (rem == 3) {\n";
|
|
s << " int c2 = lidlB64Idx(s64[i+2]);\n";
|
|
s << " if (c2 < 0) return {};\n";
|
|
s << " n |= uint32_t(c2) << 6;\n";
|
|
s << " out.push_back((n >> 8) & 0xff);\n";
|
|
s << " }\n }\n 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.
|
|
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) {
|
|
s << " if (m == \"" << md.name << "\") {\n";
|
|
s << " if (args.size() < " << md.params.size() << ") return nullptr;\n";
|
|
QString call = "lidlImpl()." + qs(md.name) + "(";
|
|
for (int i = 0; i < md.params.size(); ++i) {
|
|
call += jsonArgToStd(md.params[i].type,
|
|
QString("args.at(%1)").arg(i),
|
|
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";
|
|
|
|
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";
|
|
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";
|
|
|
|
// Only the modules that actually emit binary event payloads need the bytes
|
|
// encoder; emitting it everywhere would leave it unused (and warned about).
|
|
if (hasBytesEventParam(module)) {
|
|
s << "namespace {\n\n";
|
|
emitBytesEncodeHelpers(s);
|
|
s << "} // namespace\n\n";
|
|
}
|
|
|
|
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")
|
|
|| 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.
|
|
if (evStd != "LogosMap" && evStd != "LogosList"
|
|
&& (isRecord(pd.type, recsEv)
|
|
|| pd.type.kind == TypeExpr::Array || pd.type.kind == TypeExpr::Map)) {
|
|
s << " args.push_back(logos_gen::Codec<" << evStd << ">::to("
|
|
<< pd.name << "));\n";
|
|
continue;
|
|
}
|
|
if (pd.type.kind == TypeExpr::Primitive && pd.type.name == "bstr")
|
|
s << " args.push_back(lidlBytesToJson(" << pd.name << "));\n";
|
|
else
|
|
s << " args.push_back(" << pd.name << ");\n";
|
|
}
|
|
s << " emitEventImpl_(\"" << ed.name << "\", &args);\n";
|
|
s << "}\n\n";
|
|
}
|
|
return c;
|
|
}
|