mirror of
https://github.com/logos-co/logos-cpp-sdk.git
synced 2026-08-31 01:31:10 +00:00
* 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>
761 lines
33 KiB
C++
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;
|
|
}
|