Files
Dario LipicarandClaude Opus 4.8 7b62ac2017 Per-event documentation + getPluginEvents introspection (#71)
* Add per-event documentation + getPluginEvents introspection

Mirror the per-method documentation pipeline for events. Events
(declared in a universal module's logos_events: section) now carry a
description parsed from their /// doc comments, and are introspectable
at runtime via a new getPluginEvents framework call.

- lidl_ast: EventDecl gains a description field.
- impl_header_parser: capture the event's doc comment (previously
  discarded) and an optional metadata.json events[].description.
- lidl_gen_provider: generated universal provider emits
  getEvents() override, mirroring getMethods() (name/signature/
  parameters/description; no returnType/isInvokable — events are void).
- logos_provider_object: default-empty virtual getEvents() so the
  legacy provider path and QtProviderObject inherit empty.
- module_proxy / qt_provider_object: intercept getPluginEvents next to
  the getPluginMethods special-case.
- docs: spec + README event-documentation notes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Add unit tests for event documentation + getEvents generation

Address review feedback (#71): cover the event-introspection paths that
previously only had method-side tests.

- impl_header_parser test: assert metadata.json events[].description is
  parsed; new documented_events fixture asserts `///` doc-comment
  capture on a logos_events: block (multi-line joined with \n,
  adjacent-only, plain // ignored).
- lidl_gen_provider test: assert the generated dispatch contains
  getEvents() emitting each event's name/signature/parameters and an
  escaped description, and that events carry no returnType/isInvokable.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Fold event introspection into getMethods() to keep the provider ABI stable

The previous approach added a getEvents() virtual to LogosProviderObject,
which inserted a new vtable slot and shifted every later slot — an ABI
break that would misdispatch virtual calls whenever an old and new
host/module were mixed across the in-process plugin boundary.

Instead, report events INSIDE the existing getMethods() call: it now
returns the module's whole interface, with each entry tagged
type "method" or "event" (events omit returnType/isInvokable). The
provider vtable is therefore byte-for-byte unchanged, so old/new hosts
and modules stay binary-compatible — a new host reading an old module
sees no event entries (zero events), and an old host reading a new
module just ignores the "type" field (cosmetic). An entry with no
"type" is treated as a method.

- logos_provider_object.h: remove the getEvents() virtual; document that
  getMethods() carries both, and why.
- generator (lidl_gen_provider): emit events as type "event" entries
  inside getMethods(); tag methods type "method"; no getEvents() output.
- module_proxy / qt_provider_object: getPluginMethods()/getPluginEvents()
  are now type-filtered views of getMethods(), plus a new
  getPluginInterface() returning the whole list. (These are name-
  dispatched Q_INVOKABLEs, not vtable surface — adding them is safe.)
- tests: generator asserts events fold into getMethods() tagged "event";
  ModuleProxy asserts the three filtered views; parser tests unchanged.
- docs: spec/project/docs/README updated, incl. an ABI rationale note.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 17:06:12 -03:00

761 lines
33 KiB
C++

#include "lidl_gen_provider.h"
#include "lidl_gen_client.h" // lidlToPascalCase, lidlTypeToQt
#include "lidl_parser.h"
#include "lidl_serializer.h"
#include "lidl_validator.h"
#include <QFile>
#include <QDir>
#include <QFileInfo>
#include <QJsonObject>
#include <QJsonArray>
#include <QJsonDocument>
#include <QTextStream>
// ---------------------------------------------------------------------------
// Type mapping: LIDL → C++ std types
// ---------------------------------------------------------------------------
bool lidlIsStdConvertible(const TypeExpr& te)
{
if (te.kind == TypeExpr::Primitive) {
return te.name == "tstr" || te.name == "bstr"
|| te.name == "int" || te.name == "uint"
|| te.name == "float64" || te.name == "bool";
}
if (te.kind == TypeExpr::Array && te.elements.size() == 1) {
const TypeExpr& elem = te.elements[0];
if (elem.kind == TypeExpr::Primitive) {
return elem.name == "tstr" || elem.name == "bstr"
|| elem.name == "int" || elem.name == "uint"
|| elem.name == "float64" || elem.name == "bool";
}
}
return false;
}
QString lidlTypeToStd(const TypeExpr& te)
{
if (te.kind == TypeExpr::Primitive) {
if (te.name == "tstr") return "std::string";
if (te.name == "bstr") return "std::vector<uint8_t>";
if (te.name == "int") return "int64_t";
if (te.name == "uint") return "uint64_t";
if (te.name == "float64") return "double";
if (te.name == "bool") return "bool";
if (te.name == "result") return "LogosResult";
if (te.name == "any") return "QVariant";
return "QVariant";
}
if (te.kind == TypeExpr::Array && te.elements.size() == 1) {
const TypeExpr& elem = te.elements[0];
if (elem.kind == TypeExpr::Primitive) {
if (elem.name == "tstr") return "std::vector<std::string>";
if (elem.name == "bstr") return "std::vector<std::vector<uint8_t>>";
if (elem.name == "int") return "std::vector<int64_t>";
if (elem.name == "uint") return "std::vector<uint64_t>";
if (elem.name == "float64") return "std::vector<double>";
if (elem.name == "bool") return "std::vector<bool>";
}
return "QVariantList";
}
if (te.kind == TypeExpr::Map) return "QVariantMap";
if (te.kind == TypeExpr::Optional) return "QVariant";
if (te.kind == TypeExpr::Named) return "QVariant";
return "QVariant";
}
// ---------------------------------------------------------------------------
// Conversion helpers: Qt type ↔ std type
// ---------------------------------------------------------------------------
static QString qtParamToStd(const TypeExpr& te, const QString& paramName)
{
if (!lidlIsStdConvertible(te))
return paramName;
if (te.kind == TypeExpr::Primitive) {
if (te.name == "tstr") return paramName + ".toStdString()";
if (te.name == "bstr") return "std::vector<uint8_t>(" + paramName + ".begin(), " + paramName + ".end())";
if (te.name == "int") return "static_cast<int64_t>(" + paramName + ")";
if (te.name == "uint") return "static_cast<uint64_t>(" + paramName + ")";
return paramName;
}
if (te.kind == TypeExpr::Array && te.elements.size() == 1) {
const TypeExpr& elem = te.elements[0];
if (elem.kind == TypeExpr::Primitive && elem.name == "tstr")
return "lidlToStdStringVector(" + paramName + ")";
return "lidlToStdVector_" + elem.name + "(" + paramName + ")";
}
return paramName;
}
static QString stdReturnToQt(const TypeExpr& te, const QString& varName)
{
if (!lidlIsStdConvertible(te))
return varName;
if (te.kind == TypeExpr::Primitive) {
if (te.name == "tstr") return "QString::fromStdString(" + varName + ")";
if (te.name == "bstr") return "QByteArray(reinterpret_cast<const char*>(" + varName + ".data()), static_cast<int>(" + varName + ".size()))";
if (te.name == "int") return "static_cast<int>(" + varName + ")";
if (te.name == "uint") return "static_cast<int>(" + varName + ")";
return varName;
}
if (te.kind == TypeExpr::Array && te.elements.size() == 1) {
const TypeExpr& elem = te.elements[0];
if (elem.kind == TypeExpr::Primitive && elem.name == "tstr")
return "lidlToQStringList(" + varName + ")";
return "lidlToQVariantList_" + elem.name + "(" + varName + ")";
}
return varName;
}
static QString variantToQtArg(const TypeExpr& te, int argIdx)
{
QString a = "args.at(" + QString::number(argIdx) + ")";
QString qt = lidlTypeToQt(te);
if (qt == "QString") return a + ".toString()";
if (qt == "QByteArray") return a + ".toByteArray()";
if (qt == "int") return a + ".toInt()";
if (qt == "double") return a + ".toDouble()";
if (qt == "bool") return a + ".toBool()";
if (qt == "QStringList") return a + ".toStringList()";
if (qt == "QVariantList") return a + ".toList()";
if (qt == "QVariantMap") return a + ".toMap()";
if (qt == "LogosResult") return a + ".value<LogosResult>()";
return a;
}
// ---------------------------------------------------------------------------
// Provider header generation
// ---------------------------------------------------------------------------
QString lidlMakeProviderHeader(const ModuleDecl& module,
const QString& implClass,
const QString& implHeader)
{
QString className = lidlToPascalCase(module.name);
QString providerObjectClass = className + "ProviderObject";
QString pluginClass = className + "Plugin";
QString h;
QTextStream s(&h);
s << "// AUTO-GENERATED by logos-cpp-generator -- do not edit\n";
s << "#pragma once\n\n";
s << "#include <QObject>\n";
s << "#include <QString>\n";
s << "#include <QVariant>\n";
s << "#include <QStringList>\n";
s << "#include <QVariantList>\n";
s << "#include <QVariantMap>\n";
s << "#include <QByteArray>\n";
s << "#include <QJsonArray>\n";
s << "#include <string>\n";
s << "#include <vector>\n";
s << "#include <cstdint>\n";
s << "#include <functional>\n\n";
s << "#include \"logos_provider_object.h\"\n";
s << "#include \"interface.h\"\n";
s << "#include \"logos_types.h\"\n";
// The generated provider always emits an `onInit(LogosAPI*) override`
// that — when the impl class inherits from LogosModuleContext —
// copies the three runtime-injected properties (modulePath,
// instanceId, instancePersistencePath) into the context base AND
// builds a per-module `LogosModules` aggregate so impls can call
// other modules without ever touching the raw `LogosAPI`. Both
// dispatch paths sit behind SFINAE'd helpers in
// logos_module_context.h, so modules that don't inherit
// LogosModuleContext compile unchanged (the helpers resolve to
// no-op overloads).
s << "#include \"logos_api.h\"\n";
s << "#include \"logos_module_context.h\"\n";
// logos_sdk.h is generated alongside this header by the codegen's
// umbrella pass (lidl_gen_client.cpp / legacy/main.cpp) and defines
// the per-module `LogosModules` struct from the module's
// metadata.json#dependencies list. Always present in the build's
// generated_code/ directory; included unconditionally so the
// onInit override below can construct an instance.
s << "#include \"logos_sdk.h\"\n";
s << "#include <memory>\n";
s << "#include <type_traits>\n\n";
s << "#include \"" << implHeader << "\"\n\n";
// Conversion helpers (only if needed)
bool needsStringVecHelper = false;
for (const MethodDecl& md : module.methods) {
for (const ParamDecl& pd : md.params) {
if (pd.type.kind == TypeExpr::Array && pd.type.elements.size() == 1
&& pd.type.elements[0].kind == TypeExpr::Primitive
&& pd.type.elements[0].name == "tstr")
needsStringVecHelper = true;
}
if (md.returnType.kind == TypeExpr::Array && md.returnType.elements.size() == 1
&& md.returnType.elements[0].kind == TypeExpr::Primitive
&& md.returnType.elements[0].name == "tstr")
needsStringVecHelper = true;
}
if (needsStringVecHelper) {
s << "namespace {\n";
s << "inline QStringList lidlToQStringList(const std::vector<std::string>& v) {\n";
s << " QStringList result;\n";
s << " result.reserve(static_cast<int>(v.size()));\n";
s << " for (const auto& s : v)\n";
s << " result.append(QString::fromStdString(s));\n";
s << " return result;\n";
s << "}\n\n";
s << "inline std::vector<std::string> lidlToStdStringVector(const QStringList& list) {\n";
s << " std::vector<std::string> result;\n";
s << " result.reserve(static_cast<size_t>(list.size()));\n";
s << " for (const auto& s : list)\n";
s << " result.push_back(s.toStdString());\n";
s << " return result;\n";
s << "}\n";
s << "} // anonymous namespace\n\n";
}
// Emit nlohmannToQVariant helper if any method returns LogosMap / LogosList / StdLogosResult
bool needsNlohmannHelper = false;
bool needsResultHelper = false;
for (const MethodDecl& md : module.methods) {
if (md.jsonReturn) needsNlohmannHelper = true;
if (md.resultReturn) { needsNlohmannHelper = true; needsResultHelper = true; }
}
if (needsNlohmannHelper) {
s << "#include <nlohmann/json.hpp>\n\n";
s << "namespace {\n";
s << "inline QVariant nlohmannToQVariant(const nlohmann::json& j) {\n";
s << " if (j.is_null()) return QVariant();\n";
s << " if (j.is_boolean()) return QVariant(j.get<bool>());\n";
s << " if (j.is_number_integer()) return QVariant(static_cast<qlonglong>(j.get<int64_t>()));\n";
s << " if (j.is_number_unsigned()) return QVariant(static_cast<qulonglong>(j.get<uint64_t>()));\n";
s << " if (j.is_number_float()) return QVariant(j.get<double>());\n";
s << " if (j.is_string()) return QVariant(QString::fromStdString(j.get<std::string>()));\n";
s << " if (j.is_array()) {\n";
s << " QVariantList list;\n";
s << " list.reserve(static_cast<int>(j.size()));\n";
s << " for (const auto& elem : j)\n";
s << " list.append(nlohmannToQVariant(elem));\n";
s << " return QVariant(list);\n";
s << " }\n";
s << " if (j.is_object()) {\n";
s << " QVariantMap map;\n";
s << " for (auto it = j.begin(); it != j.end(); ++it)\n";
s << " map.insert(QString::fromStdString(it.key()), nlohmannToQVariant(it.value()));\n";
s << " return QVariant(map);\n";
s << " }\n";
s << " return QVariant();\n";
s << "}\n";
if (needsResultHelper) {
s << "\n";
s << "#include \"logos_result.h\"\n";
s << "inline LogosResult stdResultToQt(const StdLogosResult& r) {\n";
s << " LogosResult qr;\n";
s << " qr.success = r.success;\n";
s << " qr.value = nlohmannToQVariant(r.value);\n";
s << " qr.error = r.error.empty() ? QVariant() : QVariant(QString::fromStdString(r.error));\n";
s << " return qr;\n";
s << "}\n";
}
s << "} // anonymous namespace\n\n";
}
// --- ProviderObject class ---
s << "class " << providerObjectClass << " : public LogosProviderBase {\n";
s << " LOGOS_PROVIDER(" << providerObjectClass << ", \""
<< module.name << "\", \"" << (module.version.isEmpty() ? "0.0.0" : module.version) << "\")\n\n";
s << "public:\n";
for (const MethodDecl& md : module.methods) {
QString qtRet = lidlTypeToQt(md.returnType);
bool retConvertible = lidlIsStdConvertible(md.returnType);
s << " " << qtRet << " " << md.name << "(";
for (int i = 0; i < md.params.size(); ++i) {
QString qt = lidlTypeToQt(md.params[i].type);
if (qt == "QString" || qt == "QByteArray" || qt == "QStringList"
|| qt == "QVariantList" || qt == "QVariantMap" || qt == "LogosResult")
s << "const " << qt << "& " << md.params[i].name;
else
s << qt << " " << md.params[i].name;
if (i + 1 < md.params.size()) s << ", ";
}
s << ") {\n";
if (qtRet == "void") {
s << " m_impl." << md.name << "(";
for (int i = 0; i < md.params.size(); ++i) {
s << qtParamToStd(md.params[i].type, md.params[i].name);
if (i + 1 < md.params.size()) s << ", ";
}
s << ");\n";
} else if (md.jsonReturn) {
// LogosMap / LogosList: impl returns nlohmann::json, convert to Qt type
s << " auto _result = m_impl." << md.name << "(";
for (int i = 0; i < md.params.size(); ++i) {
s << qtParamToStd(md.params[i].type, md.params[i].name);
if (i + 1 < md.params.size()) s << ", ";
}
s << ");\n";
if (qtRet == "QVariantMap")
s << " return nlohmannToQVariant(_result).toMap();\n";
else
s << " return nlohmannToQVariant(_result).toList();\n";
} else if (md.resultReturn) {
// StdLogosResult: impl returns pure-C++ result, convert to Qt LogosResult
s << " auto _result = m_impl." << md.name << "(";
for (int i = 0; i < md.params.size(); ++i) {
s << qtParamToStd(md.params[i].type, md.params[i].name);
if (i + 1 < md.params.size()) s << ", ";
}
s << ");\n";
s << " return stdResultToQt(_result);\n";
} else if (retConvertible) {
s << " auto _result = m_impl." << md.name << "(";
for (int i = 0; i < md.params.size(); ++i) {
s << qtParamToStd(md.params[i].type, md.params[i].name);
if (i + 1 < md.params.size()) s << ", ";
}
s << ");\n";
s << " return " << stdReturnToQt(md.returnType, "_result") << ";\n";
} else {
s << " return m_impl." << md.name << "(";
for (int i = 0; i < md.params.size(); ++i) {
s << qtParamToStd(md.params[i].type, md.params[i].name);
if (i + 1 < md.params.size()) s << ", ";
}
s << ");\n";
}
s << " }\n\n";
}
// Always emit an `onInit` override that:
// 1. copies the three runtime-injected LogosAPI properties
// (modulePath, instanceId, instancePersistencePath) into the
// impl when it opts in by inheriting LogosModuleContext;
// 2. builds a `LogosModules` aggregate (auto-generated by the
// codegen's umbrella pass — `generated_code/logos_sdk.h`)
// from the LogosAPI and threads its pointer into the same
// context base, giving the impl typed access to its
// declared dependencies via `modules().<dep>...`.
//
// Both wire-ups go through SFINAE'd helpers
// (`_logos_codegen_::maybeSet*`) so impls that don't inherit
// LogosModuleContext compile unchanged and pay zero runtime cost
// (the no-op overloads inline away). The full LogosAPI is never
// exposed to user code.
//
// `m_logosModules` is owned by the provider object, lifetime
// matched to it — the impl is a member of this same provider
// (`m_impl`), so the pointer the context holds stays valid for
// the impl's entire lifetime.
s << "protected:\n";
s << " void onInit(LogosAPI* api) override {\n";
s << " if (!api) return;\n";
s << " _logos_codegen_::maybeSetContext(m_impl,\n";
s << " api->property(\"modulePath\").toString().toStdString(),\n";
s << " api->property(\"instanceId\").toString().toStdString(),\n";
s << " api->property(\"instancePersistencePath\").toString().toStdString());\n";
s << " m_logosModules = std::make_unique<LogosModules>(api);\n";
s << " _logos_codegen_::maybeSetLogosModules(m_impl, m_logosModules.get());\n";
// Wire the impl's `logos_events:` declarations to LogosProviderBase's
// emitEvent. Codegen-emitted `<name>_events.cpp` bodies call
// `this->emitEventImpl_(name, &args)`; the lambda below casts the
// void* back to QVariantList and routes through the existing wire.
s << " _logos_codegen_::maybeSetEmitEvent(m_impl,\n";
s << " [this](const std::string& name, void* args) {\n";
s << " emitEvent(QString::fromStdString(name),\n";
s << " *static_cast<QVariantList*>(args));\n";
s << " });\n";
s << " }\n\n";
if (!module.events.isEmpty()) {
s << "protected:\n";
for (const EventDecl& ed : module.events) {
QString methodName = "emit" + lidlToPascalCase(ed.name);
s << " void " << methodName << "(";
for (int i = 0; i < ed.params.size(); ++i) {
QString qt = lidlTypeToQt(ed.params[i].type);
if (qt == "QString" || qt == "QByteArray" || qt == "QStringList"
|| qt == "QVariantList" || qt == "QVariantMap" || qt == "LogosResult")
s << "const " << qt << "& " << ed.params[i].name;
else
s << qt << " " << ed.params[i].name;
if (i + 1 < ed.params.size()) s << ", ";
}
s << ") {\n";
s << " emitEvent(\"" << ed.name << "\", QVariantList{";
for (int i = 0; i < ed.params.size(); ++i) {
s << "QVariant::fromValue(" << ed.params[i].name << ")";
if (i + 1 < ed.params.size()) s << ", ";
}
s << "});\n";
s << " }\n\n";
}
}
s << "private:\n";
// Built by onInit; lives for the provider's full lifetime. The impl
// (declared next, so destroyed first — reverse-of-construction
// order) holds only a non-owning pointer to it via the
// LogosModuleContext base. Using unique_ptr instead of a direct
// member so the build doesn't require LogosModules to be
// default-constructible (it isn't — it takes a LogosAPI*).
s << " std::unique_ptr<LogosModules> m_logosModules;\n";
s << " " << implClass << " m_impl;\n";
s << "};\n\n";
// --- Plugin/Loader class ---
s << "class " << pluginClass << " : public QObject, public PluginInterface, public LogosProviderPlugin {\n";
s << " Q_OBJECT\n";
s << " Q_PLUGIN_METADATA(IID LogosProviderPlugin_iid FILE \"metadata.json\")\n";
s << " Q_INTERFACES(PluginInterface LogosProviderPlugin)\n\n";
s << "public:\n";
s << " QString name() const override { return QStringLiteral(\"" << module.name << "\"); }\n";
s << " QString version() const override { return QStringLiteral(\""
<< (module.version.isEmpty() ? "0.0.0" : module.version) << "\"); }\n";
s << " LogosProviderObject* createProviderObject() override {\n";
s << " return new " << providerObjectClass << "();\n";
s << " }\n";
s << "};\n";
return h;
}
// ---------------------------------------------------------------------------
// Dispatch source generation
// ---------------------------------------------------------------------------
QString lidlMakeProviderDispatch(const ModuleDecl& module)
{
QString className = lidlToPascalCase(module.name);
QString providerObjectClass = className + "ProviderObject";
QString c;
QTextStream s(&c);
s << "// AUTO-GENERATED by logos-cpp-generator -- do not edit\n";
s << "#include \"" << module.name << "_qt_glue.h\"\n";
s << "#include <QJsonArray>\n";
s << "#include <QJsonObject>\n";
s << "#include <QVariant>\n";
s << "#include <QString>\n";
s << "#include \"logos_types.h\"\n\n";
// --- callMethod ---
s << "QVariant " << providerObjectClass
<< "::callMethod(const QString& methodName, const QVariantList& args)\n{\n";
for (const MethodDecl& md : module.methods) {
QString qtRet = lidlTypeToQt(md.returnType);
s << " if (methodName == \"" << md.name << "\") {\n";
if (qtRet == "void") {
s << " " << md.name << "(";
for (int i = 0; i < md.params.size(); ++i) {
s << variantToQtArg(md.params[i].type, i);
if (i + 1 < md.params.size()) s << ", ";
}
s << ");\n";
s << " return QVariant(true);\n";
} else {
s << " return QVariant::fromValue(" << md.name << "(";
for (int i = 0; i < md.params.size(); ++i) {
s << variantToQtArg(md.params[i].type, i);
if (i + 1 < md.params.size()) s << ", ";
}
s << "));\n";
}
s << " }\n";
}
s << " qWarning() << \"" << providerObjectClass
<< "::callMethod: unknown method:\" << methodName;\n";
s << " return QVariant();\n";
s << "}\n\n";
// --- getMethods ---
s << "QJsonArray " << providerObjectClass << "::getMethods()\n{\n";
s << " QJsonArray methods;\n";
for (const MethodDecl& md : module.methods) {
QString qtRet = lidlTypeToQt(md.returnType);
s << " {\n";
s << " QJsonObject obj;\n";
s << " obj[\"type\"] = QStringLiteral(\"method\");\n";
s << " obj[\"name\"] = QStringLiteral(\"" << md.name << "\");\n";
s << " obj[\"returnType\"] = QStringLiteral(\"" << qtRet << "\");\n";
s << " obj[\"isInvokable\"] = true;\n";
if (!md.description.isEmpty()) {
QString escDesc = md.description;
escDesc.replace('\\', "\\\\");
escDesc.replace('"', "\\\"");
escDesc.replace('\n', "\\n");
s << " obj[\"description\"] = QStringLiteral(\"" << escDesc << "\");\n";
}
QString sig = md.name + "(";
for (int i = 0; i < md.params.size(); ++i) {
sig += lidlTypeToQt(md.params[i].type);
if (i + 1 < md.params.size()) sig += ",";
}
sig += ")";
s << " obj[\"signature\"] = QStringLiteral(\"" << sig << "\");\n";
if (!md.params.isEmpty()) {
s << " QJsonArray params;\n";
for (int i = 0; i < md.params.size(); ++i) {
s << " params.append(QJsonObject{{\"type\", QStringLiteral(\""
<< lidlTypeToQt(md.params[i].type) << "\")}, {\"name\", QStringLiteral(\""
<< md.params[i].name << "\")}});\n";
}
s << " obj[\"parameters\"] = params;\n";
}
s << " methods.append(obj);\n";
s << " }\n";
}
// Events are appended to the SAME interface list, tagged type "event" (and
// with no returnType/isInvokable — they are void/fire-and-forget). Folding
// them into getMethods() instead of adding a getEvents() vtable slot keeps
// LogosProviderObject's vtable layout stable, so old/new hosts and modules
// stay binary-compatible. Callers split the list back out by "type" (see
// ModuleProxy::getPluginMethods/getPluginEvents/getPluginInterface).
for (const EventDecl& ed : module.events) {
s << " {\n";
s << " QJsonObject obj;\n";
s << " obj[\"type\"] = QStringLiteral(\"event\");\n";
s << " obj[\"name\"] = QStringLiteral(\"" << ed.name << "\");\n";
if (!ed.description.isEmpty()) {
QString escDesc = ed.description;
escDesc.replace('\\', "\\\\");
escDesc.replace('"', "\\\"");
escDesc.replace('\n', "\\n");
s << " obj[\"description\"] = QStringLiteral(\"" << escDesc << "\");\n";
}
QString sig = ed.name + "(";
for (int i = 0; i < ed.params.size(); ++i) {
sig += lidlTypeToQt(ed.params[i].type);
if (i + 1 < ed.params.size()) sig += ",";
}
sig += ")";
s << " obj[\"signature\"] = QStringLiteral(\"" << sig << "\");\n";
if (!ed.params.isEmpty()) {
s << " QJsonArray params;\n";
for (int i = 0; i < ed.params.size(); ++i) {
s << " params.append(QJsonObject{{\"type\", QStringLiteral(\""
<< lidlTypeToQt(ed.params[i].type) << "\")}, {\"name\", QStringLiteral(\""
<< ed.params[i].name << "\")}});\n";
}
s << " obj[\"parameters\"] = params;\n";
}
s << " methods.append(obj);\n";
s << " }\n";
}
s << " return methods;\n";
s << "}\n";
return c;
}
// ---------------------------------------------------------------------------
// Events source generation — Qt-MOC-style bodies for `logos_events:` decls
// ---------------------------------------------------------------------------
//
// The impl header declares typed event prototypes in a `logos_events:`
// section. The compiler sees them as ordinary public-method declarations
// (the macro expands to `public:`). This generator emits the matching
// definitions in a sidecar `<name>_events.cpp` — exactly the role that
// `moc_*.cpp` plays for Qt's `signals:`. Each body marshals typed args
// into a `QVariantList` and calls `LogosModuleContext::emitEventImpl_`,
// which the provider's onInit wired to LogosProviderBase::emitEvent (and
// onward over QRO).
// Returns a C++ expression of static type QVariant for the given
// std-typed parameter. Mirrors the type-mapping table in lidlTypeToStd.
static QString stdParamToQVariantExpr(const TypeExpr& te, const QString& pn)
{
if (te.kind == TypeExpr::Primitive) {
if (te.name == "tstr")
return "QVariant(QString::fromStdString(" + pn + "))";
if (te.name == "bstr")
return "QVariant(QByteArray(reinterpret_cast<const char*>(" + pn
+ ".data()), static_cast<int>(" + pn + ".size())))";
if (te.name == "int")
return "QVariant(static_cast<qlonglong>(" + pn + "))";
if (te.name == "uint")
return "QVariant(static_cast<qulonglong>(" + pn + "))";
if (te.name == "float64") return "QVariant(" + pn + ")";
if (te.name == "bool") return "QVariant(" + pn + ")";
}
if (te.kind == TypeExpr::Array && te.elements.size() == 1
&& te.elements[0].kind == TypeExpr::Primitive
&& te.elements[0].name == "tstr") {
// std::vector<std::string> -> QStringList wrapped in QVariant
return "QVariant([&](){ QStringList _l; _l.reserve(static_cast<int>(" + pn
+ ".size())); for (const auto& _e : " + pn
+ ") _l.append(QString::fromStdString(_e)); return _l; }())";
}
// Fallback — let QVariant::fromValue figure it out (works for primitives
// and any QMetaType-registered user type).
return "QVariant::fromValue(" + pn + ")";
}
QString lidlMakeEventsSource(const ModuleDecl& module,
const QString& implClass,
const QString& implHeader)
{
QString c;
QTextStream s(&c);
s << "// AUTO-GENERATED by logos-cpp-generator -- do not edit\n";
s << "//\n";
s << "// Bodies for `logos_events:` methods declared in " << implHeader << ".\n";
s << "// Each call marshals typed args into a QVariantList and routes\n";
s << "// them through LogosModuleContext::emitEventImpl_, which the\n";
s << "// generated provider wires to LogosProviderBase::emitEvent.\n";
s << "#include \"" << implHeader << "\"\n";
s << "#include <QString>\n";
s << "#include <QByteArray>\n";
s << "#include <QStringList>\n";
s << "#include <QVariant>\n";
s << "#include <QVariantList>\n";
s << "#include <cstdint>\n";
s << "#include <string>\n";
s << "#include <vector>\n\n";
for (const EventDecl& ed : module.events) {
// Signature — mirrors the prototype the impl declared.
s << "void " << implClass << "::" << ed.name << "(";
for (int i = 0; i < ed.params.size(); ++i) {
QString stdType = lidlTypeToStd(ed.params[i].type);
const TypeExpr& te = ed.params[i].type;
// Pass-by-const-ref for non-trivial std types; by-value for
// primitives. Mirrors the existing method-signature shape.
bool byRef = (te.kind == TypeExpr::Array)
|| (te.kind == TypeExpr::Primitive
&& (te.name == "tstr" || te.name == "bstr"));
if (byRef) s << "const " << stdType << "& " << ed.params[i].name;
else s << stdType << " " << ed.params[i].name;
if (i + 1 < ed.params.size()) s << ", ";
}
s << ") {\n";
s << " QVariantList _args{";
for (int i = 0; i < ed.params.size(); ++i) {
s << stdParamToQVariantExpr(ed.params[i].type, ed.params[i].name);
if (i + 1 < ed.params.size()) s << ", ";
}
s << "};\n";
s << " this->emitEventImpl_(\"" << ed.name << "\", &_args);\n";
s << "}\n\n";
}
return c;
}
// ---------------------------------------------------------------------------
// Full pipeline (from .lidl file)
// ---------------------------------------------------------------------------
int lidlGenerateProviderGlue(const QString& lidlPath,
const QString& implClass,
const QString& implHeader,
const QString& outputDir,
QTextStream& out, QTextStream& err)
{
QFileInfo fi(lidlPath);
if (!fi.exists()) {
err << "LIDL file does not exist: " << lidlPath << "\n";
return 2;
}
QFile file(fi.canonicalFilePath().isEmpty() ? fi.absoluteFilePath() : fi.canonicalFilePath());
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
err << "Failed to open LIDL file: " << lidlPath << "\n";
return 3;
}
QString source = QString::fromUtf8(file.readAll());
file.close();
LidlParseResult pr = lidlParse(source);
if (pr.hasError()) {
err << lidlPath << ":" << pr.errorLine << ":" << pr.errorColumn
<< ": " << pr.error << "\n";
return 4;
}
LidlValidationResult vr = lidlValidate(pr.module);
if (vr.hasErrors()) {
for (const QString& e : vr.errors)
err << lidlPath << ": " << e << "\n";
return 5;
}
const ModuleDecl& mod = pr.module;
QString genDirPath = outputDir.isEmpty()
? QDir::current().filePath("generated")
: outputDir;
QDir().mkpath(genDirPath);
QString glueHeaderAbs = QDir(genDirPath).filePath(mod.name + "_qt_glue.h");
{
QFile f(glueHeaderAbs);
if (!f.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text)) {
err << "Failed to write glue header: " << glueHeaderAbs << "\n";
return 6;
}
f.write(lidlMakeProviderHeader(mod, implClass, implHeader).toUtf8());
}
QString dispatchAbs = QDir(genDirPath).filePath(mod.name + "_dispatch.cpp");
{
QFile f(dispatchAbs);
if (!f.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text)) {
err << "Failed to write dispatch source: " << dispatchAbs << "\n";
return 7;
}
f.write(lidlMakeProviderDispatch(mod).toUtf8());
}
out << "Generated: " << glueHeaderAbs << "\n";
out << "Generated: " << dispatchAbs << "\n";
// Events bodies + LIDL sidecar: emitted whenever the module declares
// any events. The sidecar gets shipped in the dep's headers-* output
// by buildPlugin.nix's installPhase so consumer-side codegen can
// discover events without reintrospecting the .dylib.
if (!mod.events.isEmpty()) {
QString eventsAbs = QDir(genDirPath).filePath(mod.name + "_events.cpp");
{
QFile f(eventsAbs);
if (!f.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text)) {
err << "Failed to write events source: " << eventsAbs << "\n";
return 8;
}
f.write(lidlMakeEventsSource(mod, implClass, implHeader).toUtf8());
}
out << "Generated: " << eventsAbs << "\n";
QString lidlAbs = QDir(genDirPath).filePath(mod.name + ".lidl");
{
QFile f(lidlAbs);
if (!f.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text)) {
err << "Failed to write LIDL sidecar: " << lidlAbs << "\n";
return 9;
}
f.write(lidlSerialize(mod).toUtf8());
}
out << "Generated: " << lidlAbs << "\n";
}
out.flush();
return 0;
}