2026-03-31 16:29:35 -04:00
#include "lidl_gen_client.h"
2026-06-12 19:56:19 -03:00
#include "lidl_emit_common.h"
2026-03-31 16:29:35 -04:00
#include <QFile>
#include <QDir>
#include <QFileInfo>
#include <QJsonObject>
#include <QJsonArray>
#include <QJsonDocument>
#include <QTextStream>
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
static bool isRefType ( const QString & qt )
{
2026-07-28 17:24:49 -03:00
if ( qt == "QString" || qt == "QStringList" || qt == "QJsonArray"
|| qt == "QVariantList" || qt == "QVariantMap" || qt == "QByteArray" )
return true ;
// A record is a struct: pass it by const& too. Anything that is not a known
// Qt scalar/handle spelling is a generated record type.
return ! ( qt == "bool" || qt == "int" || qt == "double" || qt == "float"
|| qt == "void" || qt == "qlonglong" || qt == "qulonglong"
|| qt == "QVariant" || qt == "LogosResult" );
2026-03-31 16:29:35 -04:00
}
2026-06-16 21:00:42 -03:00
static void emitParam ( QTextStream & s , const QString & qtType , const std :: string & name )
2026-03-31 16:29:35 -04:00
{
if ( isRefType ( qtType ))
s << "const " << qtType << "& " << name ;
else
s << qtType << " " << name ;
}
2026-07-28 17:24:49 -03:00
static bool lidlIsRecord ( const TypeExpr & te );
static QString qtToVariantExpr ( const TypeExpr & te , const QString & expr );
static QString qtFromVariantExpr ( const TypeExpr & te , const QString & expr );
static QString returnConversionFor ( const TypeExpr & te , const QString & qt );
2026-08-22 18:17:44 -03:00
// A field's `?T` as a TypeExpr, whichever of the two spellings the author used
// (`? name: T` sets the flag and leaves the type T; `name: ?T` makes the type
// an Optional). Building one shape here is what makes the two emit identical
// code — the rule fieldIsOptional()/fieldValueType() exist to enforce.
static TypeExpr fieldOptionalType ( const FieldDecl & f )
{
if ( f . type . kind == TypeExpr :: Optional ) return f . type ;
TypeExpr o ;
o . kind = TypeExpr :: Optional ;
o . elements . push_back ( f . type );
return o ;
}
// Does this slot need a generator-emitted ELEMENT LOOP rather than a whole-value
// QVariant hop?
//
// Two reasons a slot can need one, and they are now the same question:
// * it mentions a RECORD — a struct with no Q_DECLARE_METATYPE, so
// QVariant::fromValue of it is a blob nothing can read back;
// * its Qt spelling is a TYPED container or a std::optional —
// QList<qulonglong>, QMap<QString, QByteArray>, std::optional<QString> —
// which QVariant handles even worse than a record: qvariantToNlohmann
// matches a CLOSED userType() set and answers null for it, and
// qvariant_cast back yields an EMPTY container. Silently, both ways.
//
// QStringList, QVariantList and QVariantMap are NOT in this set: they are in
// that closed set and cross whole, exactly as they always did.
static bool holdsRecordType ( const TypeExpr & te )
{
return lidlIsRecord ( te )
|| ( te . kind == TypeExpr :: Array && te . elements . size () == 1 && lidlIsRecord ( te . elements [ 0 ]))
|| ( te . kind == TypeExpr :: Map && te . elements . size () == 2 && lidlIsRecord ( te . elements [ 1 ]));
}
static bool needsElementLoop ( const TypeExpr & te )
{
return holdsRecordType ( te ) || lidlQtNeedsElementLoop ( te );
}
// Does any slot in the contract materialise a std::optional on this surface?
// Gates the generated `#include <optional>`, so a contract with no optional (or
// one whose only optionals are `?any`) keeps its header byte-for-byte.
static bool typeUsesStdOptional ( const TypeExpr & te )
{
if ( te . kind == TypeExpr :: Optional )
return lidlQtNeedsElementLoop ( te )
|| ( ! te . elements . empty () && typeUsesStdOptional ( optionalValueType ( te )));
for ( const TypeExpr & e : te . elements )
if ( typeUsesStdOptional ( e )) return true ;
return false ;
}
static bool moduleUsesStdOptional ( const ModuleDecl & m )
{
for ( const TypeDecl & t : m . types )
for ( const FieldDecl & f : t . fields ) {
// The field's EFFECTIVE type: the optional wrapper when either
// spelling makes it optional, the written type otherwise.
const TypeExpr eff = fieldIsOptional ( f ) ? fieldOptionalType ( f ) : f . type ;
if ( typeUsesStdOptional ( eff )) return true ;
}
for ( const MethodDecl & md : m . methods ) {
if ( typeUsesStdOptional ( md . returnType )) return true ;
for ( const ParamDecl & p : md . params ) if ( typeUsesStdOptional ( p . type )) return true ;
}
for ( const EventDecl & ed : m . events )
for ( const ParamDecl & p : ed . params ) if ( typeUsesStdOptional ( p . type )) return true ;
return false ;
}
2026-03-31 16:29:35 -04:00
static QString returnConversion ( const QString & qt )
{
if ( qt == "bool" ) return "return _result.toBool();" ;
2026-07-28 17:24:49 -03:00
// 64-bit, matching lidlTypeToQt: toInt() truncated a LIDL int/uint, and for
// uint it also read the value as signed.
if ( qt == "qlonglong" ) return "return _result.toLongLong();" ;
if ( qt == "qulonglong" ) return "return _result.toULongLong();" ;
2026-03-31 16:29:35 -04:00
if ( qt == "double" ) return "return _result.toDouble();" ;
if ( qt == "float" ) return "return _result.toFloat();" ;
if ( qt == "QString" ) return "return _result.toString();" ;
if ( qt == "QStringList" ) return "return _result.toStringList();" ;
if ( qt == "QJsonArray" ) return "return qvariant_cast<QJsonArray>(_result);" ;
if ( qt == "QVariantList" ) return "return _result.toList();" ;
if ( qt == "QVariantMap" ) return "return _result.toMap();" ;
if ( qt == "LogosResult" ) return "return _result.value<LogosResult>();" ;
return "return _result;" ;
}
2026-08-22 18:17:44 -03:00
// Records, containers holding them, and every TYPED container / optional decode
// through a generated element loop; everything else keeps the historical
// QVariant accessor. `[tstr]` and `[any]` stay on the accessor: QStringList and
// QVariantList are QVariant-native, so `.toStringList()` / `.toList()` is both
// correct and what shipped.
2026-07-28 17:24:49 -03:00
static QString returnConversionFor ( const TypeExpr & te , const QString & qt )
{
2026-08-22 18:17:44 -03:00
if ( needsElementLoop ( te ))
2026-07-28 17:24:49 -03:00
return "return " + qtFromVariantExpr ( te , "_result" ) + ";" ;
return returnConversion ( qt );
}
// The async twin of returnConversionFor: `v` is the wire QVariant.
//
// A record-bearing return MUST decode field by field here too. The wire carries
// a QVariantMap and no Q_DECLARE_METATYPE is emitted for the struct, so
// `qvariant_cast<Status>(v)` does not fail — it silently returns a
// DEFAULT-CONSTRUCTED Status, and the caller sees empty fields with no
// diagnostic. That is the worst failure mode available: the sync path is
// correct, so the same call is right or wrong depending only on which overload
// the caller reached for.
static QString asyncReturnConversionFor ( const TypeExpr & te , const QString & qt )
{
2026-08-22 18:17:44 -03:00
if ( needsElementLoop ( te ))
2026-07-28 17:24:49 -03:00
return qtFromVariantExpr ( te , "v" );
return "qvariant_cast<" + qt + ">(v)" ;
}
2026-03-31 16:29:35 -04:00
static QString asyncDefaultVal ( const QString & qt )
{
if ( qt == "bool" ) return "false" ;
if ( qt == "int" || qt == "double" || qt == "float" ) return "0" ;
if ( qt == "QString" ) return "QString()" ;
if ( qt == "QStringList" ) return "QStringList()" ;
if ( qt == "QJsonArray" ) return "QJsonArray()" ;
if ( qt == "QVariantList" ) return "QVariantList()" ;
if ( qt == "QVariantMap" ) return "QVariantMap()" ;
return qt + "{}" ;
}
2026-07-28 17:24:49 -03:00
// ---------------------------------------------------------------------------
// Records
//
// A `type Foo { … }` in the contract becomes a real C++ struct plus two inline
// conversions, so a Qt consumer says `Status s = client.makeStatus();` instead
// of digging fields out of a QVariantMap. One LIDL type, one type per language.
//
// bstr fields are QByteArray on purpose: logos-protocol's QVariant<->JSON
// conversion already materialises the canonical {"_bytes": base64url} form as a
// QByteArray and back (logos_json_convert.cpp), so the record conversions stay
// pure field mapping and binary survives at any depth for free.
// ---------------------------------------------------------------------------
static bool lidlIsRecord ( const TypeExpr & te )
{
return te . kind == TypeExpr :: Named && ! te . name . empty ();
}
// value expression of the Qt type -> QVariant
2026-08-22 18:17:44 -03:00
//
// The loop is emitted whenever the surface type is not QVariant-native — for a
// record (a struct with no metatype) and now equally for every TYPED container
// and optional. `QVariant::fromValue(QList<qulonglong>)` is not a compile
// error and not a runtime warning; it produces a QVariant that
// qvariantToNlohmann answers `null` for, because that function matches a CLOSED
// userType() set. So the whole value must never cross — only its elements, one
// at a time, each of which IS in that set.
2026-07-28 17:24:49 -03:00
static QString qtToVariantExpr ( const TypeExpr & te , const QString & expr )
{
if ( lidlIsRecord ( te ))
return qs ( te . name ) + "ToVariant(" + expr + ")" ;
2026-08-22 18:17:44 -03:00
// THE SOURCE IS A LAMBDA PARAMETER, never a local bound inside the body.
// These loops nest — `[[uint]]` puts one inside another — and every level
// wants the same short names, so a body-local (or a range-for over a name
// the loop itself declares) would be self-referential: it compiles, and it
// reads uninitialised memory. An ARGUMENT is evaluated in the ENCLOSING
// scope, before the inner names exist.
if ( te . kind == TypeExpr :: Array && te . elements . size () == 1 && needsElementLoop ( te )) {
return "[&](const auto& __c){ QVariantList __l; for (const auto& __e : __c) __l.append("
+ qtToVariantExpr ( te . elements [ 0 ], "__e" ) + "); return QVariant(__l); }("
+ expr + ")" ;
2026-07-28 17:24:49 -03:00
}
2026-08-22 18:17:44 -03:00
if ( te . kind == TypeExpr :: Map && te . elements . size () == 2 && needsElementLoop ( te )) {
return "[&](const auto& __c){ QVariantMap __m; for (auto __it = __c.begin(); "
"__it != __c.end(); ++__it) __m.insert(__it.key(), "
+ qtToVariantExpr ( te . elements [ 1 ], "__it.value()" ) + "); return QVariant(__m); }("
+ expr + ")" ;
}
// `?T` -> std::optional<T>: EMPTY is the invalid QVariant, which is Qt's
// single empty inhabitant and what the wire's `null` becomes. `?any` never
// reaches here (it is still spelled QVariant, so needsElementLoop is false)
// and rides the fromValue below unchanged.
if ( te . kind == TypeExpr :: Optional && needsElementLoop ( te )) {
const TypeExpr & v = optionalValueType ( te );
return "[&](const auto& __c){ return __c.has_value() ? " + qtToVariantExpr ( v , "*__c" )
+ " : QVariant(); }(" + expr + ")" ;
2026-07-28 17:24:49 -03:00
}
return "QVariant::fromValue(" + expr + ")" ;
}
// QVariant expression -> value of the Qt type
static QString qtFromVariantExpr ( const TypeExpr & te , const QString & expr )
{
if ( lidlIsRecord ( te ))
return qs ( te . name ) + "FromVariant(" + expr + ")" ;
if ( te . kind == TypeExpr :: Primitive ) {
const QString n = qs ( te . name );
if ( n == "tstr" ) return expr + ".toString()" ;
if ( n == "bstr" ) return expr + ".toByteArray()" ;
if ( n == "int" ) return expr + ".toLongLong()" ;
if ( n == "uint" ) return expr + ".toULongLong()" ;
if ( n == "float64" ) return expr + ".toDouble()" ;
if ( n == "bool" ) return expr + ".toBool()" ;
}
2026-08-22 18:17:44 -03:00
// Source as a lambda PARAMETER, for the reason given on the encode side.
2026-07-28 17:24:49 -03:00
if ( te . kind == TypeExpr :: Array && te . elements . size () == 1 ) {
const TypeExpr & e = te . elements [ 0 ];
2026-08-22 18:17:44 -03:00
return "[&](const QVariant& __s){ " + lidlTypeToQt ( te )
+ " __acc; for (const QVariant& __e : __s.toList()) __acc.append("
+ qtFromVariantExpr ( e , "__e" ) + "); return __acc; }(" + expr + ")" ;
2026-07-28 17:24:49 -03:00
}
if ( te . kind == TypeExpr :: Map && te . elements . size () == 2 ) {
const TypeExpr & v = te . elements [ 1 ];
2026-08-22 18:17:44 -03:00
return "[&](const QVariant& __s){ " + lidlTypeToQt ( te )
+ " __acc; const QVariantMap __mm = __s.toMap(); "
"for (auto __it = __mm.begin(); __it != __mm.end(); ++__it) __acc.insert("
+ "__it.key(), " + qtFromVariantExpr ( v , "__it.value()" ) + "); return __acc; }("
+ expr + ")" ;
}
// `?T`: an invalid (or null) QVariant is the empty state — absent and
// explicit-null are the SAME state, as the two-state rule requires — and
// anything else is a present T decoded by this same table.
if ( te . kind == TypeExpr :: Optional && needsElementLoop ( te )) {
const TypeExpr & v = optionalValueType ( te );
const QString opt = lidlTypeToQt ( te );
return "[&](const QVariant& __s){ if (!__s.isValid() || __s.isNull()) return " + opt
+ "(); return " + opt + "(" + qtFromVariantExpr ( v , "__s" ) + "); }(" + expr + ")" ;
2026-07-28 17:24:49 -03:00
}
return expr ;
}
// A method argument as passed to packVariantList: records convert, everything
// else goes through unchanged (packVariantList wraps with QVariant::fromValue).
static QString qtArgExpr ( const TypeExpr & te , const QString & name )
{
2026-08-22 18:17:44 -03:00
return needsElementLoop ( te ) ? qtToVariantExpr ( te , name ) : name ;
2026-07-28 17:24:49 -03:00
}
2026-07-31 07:19:47 -03:00
// A record field's Qt type, honouring BOTH optionality spellings.
//
2026-08-22 18:17:44 -03:00
// `?T` is std::optional<T>, the same answer every other slot gets — a Qt
// consumer's `Profile.nickname` is now a std::optional<QString> rather than a
// QVariant it has to guess the payload type of, which is what the std surface
// next door has always given (Codec<std::optional<T>>). `?any` stays QVariant:
// `any` is the one row the widened table keeps untyped, and QVariant already
// has exactly one empty inhabitant, so wrapping it would spell EMPTY twice and
// make a two-state slot three-state.
//
// Routing through fieldIsOptional()/fieldOptionalType() is what makes the two
// spellings identical: reading `f.type` alone made the flag spelling emit a
// bare `T` (which cannot be empty at all) while the type spelling emitted an
// optional, from one contract.
2026-07-31 07:19:47 -03:00
static QString lidlFieldTypeQt ( const FieldDecl & f )
{
2026-08-22 18:17:44 -03:00
return fieldIsOptional ( f ) ? lidlTypeToQt ( fieldOptionalType ( f ))
: lidlTypeToQt ( f . type );
2026-07-31 07:19:47 -03:00
}
2026-07-28 17:24:49 -03:00
static void emitRecords ( QTextStream & s , const ModuleDecl & module )
{
if ( module . types . empty ()) return ;
for ( const TypeDecl & t : module . types ) {
const QString n = qs ( t . name );
s << "/// `" << n << "` — a record declared by the `" << qs ( module . name ) << "` contract. \n " ;
s << "struct " << n << " { \n " ;
for ( const FieldDecl & f : t . fields )
2026-07-31 07:19:47 -03:00
s << " " << lidlFieldTypeQt ( f ) << " " << qs ( f . name ) << "{}; \n " ;
2026-07-28 17:24:49 -03:00
s << "}; \n\n " ;
}
// Conversions come after ALL structs so records may reference each other.
for ( const TypeDecl & t : module . types ) {
const QString n = qs ( t . name );
s << "inline QVariant " << n << "ToVariant(const " << n << "& v) \n { \n " ;
s << " QVariantMap __m; \n " ;
2026-07-31 07:19:47 -03:00
for ( const FieldDecl & f : t . fields ) {
if ( fieldIsOptional ( f )) {
// A record field is a NAMED slot: empty is spelled by OMITTING
2026-08-22 18:17:44 -03:00
// the key, not by inserting an empty value. Same rule the
2026-07-31 07:19:47 -03:00
// cdylib record codec follows, on the other surface.
2026-08-22 18:17:44 -03:00
//
// The emptiness TEST follows the field's own spelling —
// `.has_value()` for a std::optional, `.isValid()` for the
// `?any` slot that stays a QVariant — because those are the two
// types this surface can produce for an optional field.
const QString fv = "v." + qs ( f . name );
const TypeExpr ot = fieldOptionalType ( f );
if ( lidlQtNeedsElementLoop ( ot )) {
s << " if (" << fv << ".has_value()) \n " ;
s << " __m.insert( \" " << qs ( f . name ) << " \" , "
<< qtToVariantExpr ( fieldValueType ( f ), "*" + fv ) << "); \n " ;
} else {
s << " if (" << fv << ".isValid()) \n " ;
s << " __m.insert( \" " << qs ( f . name ) << " \" , " << fv << "); \n " ;
}
2026-07-31 07:19:47 -03:00
continue ;
}
2026-07-28 17:24:49 -03:00
s << " __m.insert( \" " << qs ( f . name ) << " \" , "
<< qtToVariantExpr ( f . type , "v." + qs ( f . name )) << "); \n " ;
2026-07-31 07:19:47 -03:00
}
2026-07-28 17:24:49 -03:00
s << " return QVariant(__m); \n } \n\n " ;
s << "inline " << n << " " << n << "FromVariant(const QVariant& value) \n { \n " ;
s << " const QVariantMap __m = value.toMap(); \n " ;
s << " " << n << " __out; \n " ;
2026-07-31 07:19:47 -03:00
for ( const FieldDecl & f : t . fields ) {
if ( fieldIsOptional ( f )) {
// Absent and null both arrive as an invalid QVariant — the same
2026-08-22 18:17:44 -03:00
// state, as the contract requires — and the optional decode
// below turns exactly that into the empty optional. A bare
// conversion (`.toString()` on a flag-optional `tstr`) would
// have turned "empty" into "", which is a VALUE.
s << " __out." << qs ( f . name ) << " = "
<< qtFromVariantExpr ( fieldOptionalType ( f ),
"__m.value( \" " + qs ( f . name ) + " \" )" )
<< "; \n " ;
2026-07-31 07:19:47 -03:00
continue ;
}
2026-07-28 17:24:49 -03:00
s << " __out." << qs ( f . name ) << " = "
<< qtFromVariantExpr ( f . type , "__m.value( \" " + qs ( f . name ) + " \" )" ) << "; \n " ;
2026-07-31 07:19:47 -03:00
}
2026-07-28 17:24:49 -03:00
s << " return __out; \n } \n\n " ;
}
}
2026-03-31 16:29:35 -04:00
// ---------------------------------------------------------------------------
// Header generation
// ---------------------------------------------------------------------------
2026-06-05 20:44:43 -03:00
QString lidlMakeHeader ( const ModuleDecl & module , BindMode bindMode )
2026-03-31 16:29:35 -04:00
{
2026-06-16 21:00:42 -03:00
QString className = lidlToPascalCase ( qs ( module . name ));
2026-03-31 16:29:35 -04:00
QString h ;
QTextStream s ( & h );
s << "#pragma once \n " ;
s << "#include <QString> \n " ;
s << "#include <QVariant> \n " ;
s << "#include <QStringList> \n " ;
s << "#include <QJsonArray> \n " ;
s << "#include <QVariantList> \n " ;
s << "#include <QVariantMap> \n " ;
s << "#include <functional> \n " ;
s << "#include <utility> \n " ;
2026-08-22 18:17:44 -03:00
if ( moduleUsesStdOptional ( module )) s << "#include <optional> \n " ;
2026-03-31 16:29:35 -04:00
s << "#include \" logos_types.h \"\n " ;
s << "#include \" logos_api.h \"\n " ;
s << "#include \" logos_api_client.h \"\n " ;
2026-06-12 19:56:19 -03:00
s << "#include \" logos_call_error.h \"\n " ;
2026-08-03 10:19:08 -03:00
s << "#include \" logos_async_result.h \"\n " ;
2026-03-31 16:29:35 -04:00
s << "#include \" logos_object.h \"\n\n " ;
2026-07-28 17:24:49 -03:00
emitRecords ( s , module );
2026-03-31 16:29:35 -04:00
s << "class " << className << " { \n " ;
s << "public: \n " ;
2026-06-05 20:44:43 -03:00
if ( bindMode == BindMode :: Bound )
s << " explicit " << className << "(LogosAPI* api, const QString& moduleName); \n\n " ;
else
s << " explicit " << className << "(LogosAPI* api); \n\n " ;
2026-03-31 16:29:35 -04:00
s << " using RawEventCallback = std::function<void(const QString&, const QVariantList&)>; \n " ;
s << " using EventCallback = std::function<void(const QVariantList&)>; \n\n " ;
s << " bool on(const QString& eventName, RawEventCallback callback); \n " ;
s << " bool on(const QString& eventName, EventCallback callback); \n " ;
for ( const MethodDecl & md : module . methods ) {
QString ret = lidlTypeToQt ( md . returnType );
s << " " << ret << " " << md . name << "(" ;
for ( int i = 0 ; i < md . params . size (); ++ i ) {
emitParam ( s , lidlTypeToQt ( md . params [ i ]. type ), md . params [ i ]. name );
if ( i + 1 < md . params . size ()) s << ", " ;
}
2026-06-12 19:56:19 -03:00
// Optional error out-channel: pass a logos::CallError* to distinguish
2026-08-03 10:19:08 -03:00
// a failed remote call from a legitimately default-valued result —
// followed by an optional Timeout. Both trailing and defaulted, so
// existing call sites (including ones passing `&err` positionally)
// compile unchanged. Mirrors the legacy emitter in
2026-08-19 15:49:30 -03:00
// generator_lib.cpp; the two must agree, since a consumer can
2026-08-03 10:19:08 -03:00
// reach either (this one from a published `.lidl`, that one through the
// module builder) for the same contract.
2026-06-16 21:00:42 -03:00
if ( ! md . params . empty ()) s << ", " ;
2026-08-03 10:19:08 -03:00
s << "logos::CallError* err = nullptr, Timeout timeout = Timeout()); \n " ;
auto emitAsyncParams = [ & ]() {
for ( int i = 0 ; i < md . params . size (); ++ i ) {
emitParam ( s , lidlTypeToQt ( md . params [ i ]. type ), md . params [ i ]. name );
if ( i + 1 < md . params . size ()) s << ", " ;
}
if ( ! md . params . empty ()) s << ", " ;
};
2026-03-31 16:29:35 -04:00
QString asyncCb = ( ret == "void" )
? QString ( "std::function<void()>" )
: QString ( "std::function<void(" ) + ret + ")>" ;
s << " void " << md . name << "Async(" ;
2026-08-03 10:19:08 -03:00
emitAsyncParams ();
2026-03-31 16:29:35 -04:00
s << asyncCb << " callback, Timeout timeout = Timeout()); \n " ;
2026-08-03 10:19:08 -03:00
// Result-carrying async entry point. Distinct name, not an overload:
// std::function<void(AsyncResult<T>)> alongside std::function<void(T)>
// is ambiguous for a generic lambda.
s << " void " << md . name << "AsyncResult(" ;
emitAsyncParams ();
s << "std::function<void(logos::AsyncResult<" << ret << ">)> callback"
<< ", Timeout timeout = Timeout()); \n " ;
2026-03-31 16:29:35 -04:00
}
s << " \n private: \n " ;
s << " template<typename... Args> \n " ;
s << " static QVariantList packVariantList(Args&&... args) { \n " ;
s << " QVariantList list; \n " ;
s << " list.reserve(sizeof...(Args)); \n " ;
s << " using Expander = int[]; \n " ;
s << " (void)Expander{0, (list.append(QVariant::fromValue(std::forward<Args>(args))), 0)...}; \n " ;
s << " return list; \n " ;
s << " } \n " ;
s << " LogosAPI* m_api; \n " ;
s << " LogosAPIClient* m_client; \n " ;
s << " QString m_moduleName; \n " ;
s << "}; \n " ;
return h ;
}
// ---------------------------------------------------------------------------
// Source generation
// ---------------------------------------------------------------------------
2026-06-05 20:44:43 -03:00
QString lidlMakeSource ( const ModuleDecl & module , BindMode bindMode )
2026-03-31 16:29:35 -04:00
{
2026-06-16 21:00:42 -03:00
QString className = lidlToPascalCase ( qs ( module . name ));
QString headerRel = qs ( module . name ) + "_api.h" ;
2026-03-31 16:29:35 -04:00
QString c ;
QTextStream s ( & c );
s << "#include \" " << headerRel << " \"\n\n " ;
s << "#include <QDebug> \n\n " ;
2026-06-05 20:44:43 -03:00
// Target expression for every remote call: a baked literal in Static
// mode, the runtime m_moduleName member in Bound (interface) mode.
const QString targetExpr = ( bindMode == BindMode :: Bound )
? QStringLiteral ( "m_moduleName" )
2026-06-16 21:00:42 -03:00
: ( QStringLiteral ( " \" " ) + qs ( module . name ) + QStringLiteral ( " \" " ));
2026-06-05 20:44:43 -03:00
if ( bindMode == BindMode :: Bound )
s << className << "::" << className << "(LogosAPI* api, const QString& moduleName) : m_api(api), m_client(api->getClient(moduleName)), m_moduleName(moduleName) {} \n\n " ;
else
s << className << "::" << className << "(LogosAPI* api) : m_api(api), m_client(api->getClient( \" "
<< module . name << " \" )), m_moduleName(QStringLiteral( \" " << module . name << " \" )) {} \n\n " ;
2026-03-31 16:29:35 -04:00
s << "bool " << className << "::on(const QString& eventName, RawEventCallback callback) { \n " ;
s << " if (!callback) { qWarning() << \" " << className << ": ignoring empty event callback for \" << eventName; return false; } \n " ;
2026-08-10 16:07:18 -03:00
// Deferred: the module is usually NOT reachable at the moment a consumer
// subscribes (init(), onContextReady()), and acquiring a replica there used
// to block and then fail permanently. onEventWhenAvailable arms it when the
// module appears. The return is ACCEPTED, not live.
s << " return m_client->onEventWhenAvailable(m_moduleName, eventName, callback) != 0; \n " ;
2026-03-31 16:29:35 -04:00
s << "} \n\n " ;
s << "bool " << className << "::on(const QString& eventName, EventCallback callback) { \n " ;
s << " if (!callback) { qWarning() << \" " << className << ": ignoring empty event callback for \" << eventName; return false; } \n " ;
s << " return on(eventName, [callback](const QString&, const QVariantList& data) { callback(data); }); \n " ;
s << "} \n\n " ;
for ( const MethodDecl & md : module . methods ) {
QString ret = lidlTypeToQt ( md . returnType );
int nParams = md . params . size ();
s << ret << " " << className << "::" << md . name << "(" ;
for ( int i = 0 ; i < nParams ; ++ i ) {
emitParam ( s , lidlTypeToQt ( md . params [ i ]. type ), md . params [ i ]. name );
if ( i + 1 < nParams ) s << ", " ;
}
2026-06-12 19:56:19 -03:00
if ( nParams > 0 ) s << ", " ;
2026-08-03 10:19:08 -03:00
s << "logos::CallError* err, Timeout timeout) { \n " ;
2026-03-31 16:29:35 -04:00
2026-06-12 19:56:19 -03:00
// Call through the err-out overload: with a logos::CallError* the
// caller can distinguish a failed remote call from a legitimately
// default-valued result; without it the historical default-on-failure
// behavior is kept, plus a warning in the module log.
s << " logos::CallError _err; \n " ;
2026-03-31 16:29:35 -04:00
if ( ret != "void" ) s << " QVariant _result = " ;
else s << " " ;
2026-07-19 01:03:30 -03:00
// Pack each argument as ONE element via packVariantList (which wraps
// with QVariant::fromValue). A braced `QVariantList{v}` or `<< v` would
// CONCATENATE a QVariantList-typed arg (any `[T]` list) into the args
// list, sending a 3-element [1,2,3] as three positional args instead of
// one — the historical "typed arrays empty over the Qt path" bug.
s << "m_client->invokeRemoteMethod(" << targetExpr << ", \" " << md . name << " \" , packVariantList(" ;
2026-06-12 19:56:19 -03:00
for ( int i = 0 ; i < nParams ; ++ i ) {
2026-07-28 17:24:49 -03:00
s << qtArgExpr ( md . params [ i ]. type , qs ( md . params [ i ]. name ));
2026-06-12 19:56:19 -03:00
if ( i + 1 < nParams ) s << ", " ;
2026-03-31 16:29:35 -04:00
}
2026-08-03 10:19:08 -03:00
// The caller's deadline, not a hard-coded default: this is the overload
// that carries BOTH the deadline and the error out-channel.
s << "), timeout, &_err); \n " ;
2026-06-12 19:56:19 -03:00
s << " if (err) *err = _err; \n " ;
s << " else if (!_err.ok()) qWarning() << \" " << className << "::" << md . name
<< ": remote call failed: \" << QString::fromStdString(_err.message); \n " ;
2026-03-31 16:29:35 -04:00
if ( ret != "void" )
2026-07-28 17:24:49 -03:00
s << " " << returnConversionFor ( md . returnType , ret ) << " \n " ;
2026-03-31 16:29:35 -04:00
s << "} \n\n " ;
2026-08-03 10:19:08 -03:00
// Shared between the two async entry points so they cannot drift in how
// they marshal args or decode the reply.
auto emitAsyncParams = [ & ]() {
for ( int i = 0 ; i < nParams ; ++ i ) {
emitParam ( s , lidlTypeToQt ( md . params [ i ]. type ), md . params [ i ]. name );
if ( i + 1 < nParams ) s << ", " ;
}
if ( nParams > 0 ) s << ", " ;
};
2026-07-19 01:03:30 -03:00
// Same one-element-per-arg packing as the sync path (see above): a
// QVariantList-typed arg must not be spread across the args list.
2026-08-03 10:19:08 -03:00
auto emitAsyncArgs = [ & ]() {
s << "packVariantList(" ;
for ( int i = 0 ; i < nParams ; ++ i ) {
s << qtArgExpr ( md . params [ i ]. type , qs ( md . params [ i ]. name ));
if ( i + 1 < nParams ) s << ", " ;
}
s << ")" ;
};
// The QVariant -> typed-return expression, given the QVariant's name.
auto asyncDecodeExpr = [ & ]( const QString & var ) -> QString {
if ( ret == "void" ) return QString ();
if ( ret == "QVariant" ) return var ;
return var + ".isValid() ? " + asyncReturnConversionFor ( md . returnType , ret )
+ " : " + asyncDefaultVal ( ret );
};
s << "void " << className << "::" << md . name << "Async(" ;
emitAsyncParams ();
s << "std::function<void(" << ( ret == "void" ? "void" : ret ) << ")> callback, Timeout timeout) { \n " ;
s << " if (!callback) return; \n " ;
s << " m_client->invokeRemoteMethodAsync(" << targetExpr << ", \" " << md . name << " \" , " ;
emitAsyncArgs ();
// ONE-argument lambda -> LogosAPIClient::AsyncResultCallback, i.e. the
// historical value-only transport overload.
2026-03-31 16:29:35 -04:00
s << ", [callback](QVariant v) { \n " ;
2026-08-03 10:19:08 -03:00
if ( ret == "void" ) s << " callback(); \n " ;
else s << " callback(" << asyncDecodeExpr ( "v" ) << "); \n " ;
s << " }, timeout); \n " ;
s << "} \n\n " ;
// Result-carrying async: a TWO-argument lambda, so it binds to the
// transport's CallError-aware AsyncResultErrorCallback overload. The
// value on failure is exactly what `<name>Async` would have delivered;
// what changes is that the callback can now tell.
s << "void " << className << "::" << md . name << "AsyncResult(" ;
emitAsyncParams ();
s << "std::function<void(logos::AsyncResult<" << ret << ">)> callback, Timeout timeout) { \n " ;
s << " if (!callback) return; \n " ;
s << " m_client->invokeRemoteMethodAsync(" << targetExpr << ", \" " << md . name << " \" , " ;
emitAsyncArgs ();
s << ", [callback](QVariant v, const logos::CallError& _err) { \n " ;
s << " logos::AsyncResult<" << ret << "> _r; \n " ;
s << " _r.error = _err; \n " ;
if ( ret == "void" ) s << " (void)v; \n " ;
else s << " _r.value = " << asyncDecodeExpr ( "v" ) << "; \n " ;
s << " callback(_r); \n " ;
2026-03-31 16:29:35 -04:00
s << " }, timeout); \n " ;
s << "} \n\n " ;
}
return c ;
}
// ---------------------------------------------------------------------------
// metadata.json
// ---------------------------------------------------------------------------
QString lidlGenerateMetadataJson ( const ModuleDecl & module )
{
QJsonObject obj ;
2026-06-16 21:00:42 -03:00
obj [ "name" ] = qs ( module . name );
obj [ "version" ] = module . version . empty () ? QStringLiteral ( "0.0.0" ) : qs ( module . version );
2026-03-31 16:29:35 -04:00
obj [ "type" ] = "core" ;
2026-06-16 21:00:42 -03:00
obj [ "category" ] = module . category . empty () ? QStringLiteral ( "general" ) : qs ( module . category );
obj [ "description" ] = qs ( module . description );
obj [ "main" ] = qs ( module . name ) + "_plugin" ;
2026-03-31 16:29:35 -04:00
QJsonArray deps ;
2026-06-16 21:00:42 -03:00
for ( const std :: string & d : module . depends )
deps . append ( qs ( d ));
2026-03-31 16:29:35 -04:00
obj [ "dependencies" ] = deps ;
QJsonDocument doc ( obj );
return doc . toJson ( QJsonDocument :: Indented );
}
// ---------------------------------------------------------------------------
// Full pipeline (from .lidl file)
// ---------------------------------------------------------------------------
int lidlGenerateClientStubs ( const QString & lidlPath , const QString & outputDir ,
bool moduleOnly , 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 );
2026-06-16 21:00:42 -03:00
if ( vr . hasErrors ()) { for ( const std :: string & e : vr . errors ) err << lidlPath << ": " << e << " \n " ; return 5 ; }
2026-07-28 17:24:49 -03:00
{
QString recErr ;
if ( ! lidlCheckRecords ( pr . module , & recErr )) { err << lidlPath << ": " << recErr << " \n " ; return 5 ; }
}
2026-03-31 16:29:35 -04:00
const ModuleDecl & mod = pr . module ;
QString genDirPath = outputDir . isEmpty () ? QDir :: current (). filePath ( "logos-cpp-sdk/cpp/generated" ) : outputDir ;
QDir (). mkpath ( genDirPath );
2026-06-16 21:00:42 -03:00
QString headerAbs = QDir ( genDirPath ). filePath ( qs ( mod . name ) + "_api.h" );
QString sourceAbs = QDir ( genDirPath ). filePath ( qs ( mod . name ) + "_api.cpp" );
2026-03-31 16:29:35 -04:00
{ QFile f ( headerAbs ); if ( ! f . open ( QIODevice :: WriteOnly | QIODevice :: Truncate | QIODevice :: Text )) { err << "Failed to write: " << headerAbs << " \n " ; return 6 ; } f . write ( lidlMakeHeader ( mod ). toUtf8 ()); }
{ QFile f ( sourceAbs ); if ( ! f . open ( QIODevice :: WriteOnly | QIODevice :: Truncate | QIODevice :: Text )) { err << "Failed to write: " << sourceAbs << " \n " ; return 7 ; } f . write ( lidlMakeSource ( mod ). toUtf8 ()); }
{ QString metaPath = QDir ( genDirPath ). filePath ( "metadata.json" ); QFile f ( metaPath ); if ( ! f . open ( QIODevice :: WriteOnly | QIODevice :: Truncate | QIODevice :: Text )) { err << "Failed to write: " << metaPath << " \n " ; return 8 ; } f . write ( lidlGenerateMetadataJson ( mod ). toUtf8 ()); }
out << "Generated: " << headerAbs << " and " << sourceAbs << " \n " ;
if ( ! moduleOnly ) {
QDir genDir ( genDirPath );
QStringList headers = genDir . entryList ( QStringList () << "*_api.h" , QDir :: Files | QDir :: Readable );
{ QString content ; QTextStream ss ( & content );
ss << "#pragma once \n #include \" logos_api.h \"\n #include \" logos_api_client.h \"\n\n " ;
for ( const QString & h : headers ) ss << "#include \" " << h << " \"\n " ;
ss << " \n struct LogosModules { \n explicit LogosModules(LogosAPI* api) : api(api)" ;
for ( const QString & h : headers ) { QString base = h ; base . chop ( 6 ); ss << ", \n " << base << "(api)" ; }
ss << " {} \n LogosAPI* api; \n " ;
for ( const QString & h : headers ) { QString base = h ; base . chop ( 6 ); ss << " " << lidlToPascalCase ( base ) << " " << base << "; \n " ; }
ss << "}; \n " ;
QFile f ( genDir . filePath ( "logos_sdk.h" )); if ( ! f . open ( QIODevice :: WriteOnly | QIODevice :: Truncate | QIODevice :: Text )) { err << "Failed to write umbrella \n " ; return 9 ; } f . write ( content . toUtf8 ()); }
{ QStringList sources = genDir . entryList ( QStringList () << "*_api.cpp" , QDir :: Files | QDir :: Readable );
QString content ; QTextStream ss ( & content ); ss << "#include \" logos_sdk.h \"\n\n " ;
for ( const QString & c : sources ) ss << "#include \" " << c << " \"\n " ; ss << " \n " ;
QFile f ( genDir . filePath ( "logos_sdk.cpp" )); if ( ! f . open ( QIODevice :: WriteOnly | QIODevice :: Truncate | QIODevice :: Text )) { err << "Failed to write umbrella \n " ; return 10 ; } f . write ( content . toUtf8 ()); }
out << "Generated: logos_sdk.h and logos_sdk.cpp \n " ;
}
out << "Generated: metadata.json \n " ;
out . flush ();
return 0 ;
}