mirror of
https://github.com/logos-co/logos-plugin-qt.git
synced 2026-08-27 08:51:07 +00:00
feat: the Qt host runtime and the cdylib-glue generator
This repo was the Qt plugin BUILD backend — pure Nix functions plus a CMake
module, with no C++ target at all. But the two things a Qt host actually needs
at runtime lived in logos-qt-sdk, where they are neither a language wrapper over
the protocol C API nor codegen:
LogosAPI / LogosAPIProvider owns a LogosTransportHost per transport,
constructs ModuleProxy / ModuleHandshakeProxy,
publishes the handshake surface, seeds trust
anchors, injects the token validator
LogosProviderBase the base every generated provider derives
PluginInterface the Qt plugin loading contract
the cdylib->Qt glue generator the emitter that wraps a language-neutral
cdylib in a Qt plugin
They move here, so "swap the plugin technology" is a one-repo change.
ADDITIVE: the sources are COPIED and logos-qt-sdk is untouched. Removing them
there now would turn nine downstream masters red at once; that comes later,
after consumers are repointed.
qt_provider_object (and logos_qt_arg_decode, which its QMetaObject dispatch
needs) is carried deliberately even though it is legacy: logos_api_provider
falls back to wrapping a plain QObject in it, and the modules that rely on that
have not been migrated yet. It goes once they are.
The generator links Qt Core and logos-lidl only — never logos-cpp-sdk. It needed
exactly one helper from that SDK's shared frontend, lidlToPascalCase (~12
lines), which is inlined instead, the same way logos-view-module does it. It
also REFUSES `--backend <anything but cdylib>` rather than ignoring the flag:
callers are migrating from a tool where --backend was required and dispatched
on, so silently treating `--backend qt` as cdylib would emit confidently wrong
artifacts with a zero exit.
`rawLib`, `lib` and `cmake-module` deliberately do not reference the new
derivations, so a consumer that only wants the Nix build functions never
realises a Qt/protocol build. Proven, not assumed: with both new inputs
overridden to a bogus flake, cmake-module and rawLib still evaluate while
logos-qt-host fails — so the override bites and the cheap outputs really never
touch it.
Behaviour preservation is the whole claim of a relocation, so it is measured:
the emitted glue is BYTE-IDENTICAL to logos-qt-generator --backend cdylib over
every one of the 20 .lidl contracts in the workspace, in both single and
concurrency:multi mode (40 pairs, exit codes included), and single vs multi do
differ from each other, so both code paths were really exercised. The built
liblogos_qt_host.a is byte-identical to liblogos_qt_sdk.a, exporting the same
390 symbols.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
8846fc5626
commit
eac67af9e3
@@ -27,3 +27,9 @@ jobs:
|
||||
|
||||
- name: Header generator guard test
|
||||
run: nix build '.#checks.x86_64-linux.header-generator-guard'
|
||||
|
||||
- name: Build Qt host runtime
|
||||
run: nix build '.#checks.x86_64-linux.qt-host'
|
||||
|
||||
- name: Qt host glue generator test
|
||||
run: nix build '.#checks.x86_64-linux.qt-host-generator'
|
||||
|
||||
@@ -1,5 +1,32 @@
|
||||
# logos-plugin-qt
|
||||
|
||||
The Qt-specific build logic (CMake module, plugin compilation, header generation) as a standalone backend that logos-module-builder delegates to.
|
||||
Everything specific to running a Logos module as a **Qt 6 plugin**, in one repo:
|
||||
the build logic logos-module-builder delegates to, and the runtime that build
|
||||
produces plugins against. Keeping both here is what lets the plugin technology
|
||||
be swapped without touching the module builder or individual modules.
|
||||
|
||||
This enables swapping the plugin technology without changing the module builder or individual modules.
|
||||
## Outputs
|
||||
|
||||
| Output | What it is |
|
||||
|---|---|
|
||||
| `lib` / `rawLib` | The Nix build functions (`buildPlugin`, `buildHeaders`, `devShellInputs`). `rawLib` takes its Logos deps as arguments; `lib` pre-fills them from this flake. |
|
||||
| `packages.<sys>.cmake-module` | `LogosModule.cmake` — the CMake half of the plugin build. Also the `default` package. |
|
||||
| `packages.<sys>.logos-qt-host` | The **Qt host runtime** a plugin links: `LogosAPI`, `LogosAPIProvider`, `LogosProviderBase` + the `LOGOS_PROVIDER` / `LOGOS_METHOD` macros, the legacy `QtProviderObject` adapter, and `core/interface.h`. Static library, headers, and a `find_package(logos-qt-host)` config. |
|
||||
| `packages.<sys>.logos-qt-host-generator` | Emits the Qt plugin glue around a cdylib module's C ABI (`<name>_cdylib_glue.{h,cpp}`) from its LIDL contract. |
|
||||
|
||||
The first two are pure Nix / CMake and stay that way: nothing reachable from
|
||||
`lib`, `rawLib` or `cmake-module` mentions the two C++ derivations, so a
|
||||
consumer that only wants the build functions never realises a Qt or protocol
|
||||
build to get them.
|
||||
|
||||
## Layout
|
||||
|
||||
```
|
||||
lib/ the Nix build functions (buildPlugin, buildHeaders)
|
||||
cmake/ LogosModule.cmake
|
||||
cpp/ the Qt host runtime library (logos-qt-host)
|
||||
core/interface.h the legacy Qt plugin interface (PluginInterface)
|
||||
qt-host-generator/ the cdylib -> Qt-plugin glue emitter
|
||||
nix/ derivations for the two C++ outputs
|
||||
tests/ flake checks
|
||||
```
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
#ifndef PLUGIN_INTERFACE_H
|
||||
#define PLUGIN_INTERFACE_H
|
||||
|
||||
#include <QtPlugin>
|
||||
#include <QString>
|
||||
#include "../cpp/logos_api.h"
|
||||
|
||||
// Define the common base interface for all modules
|
||||
class PluginInterface
|
||||
{
|
||||
public:
|
||||
virtual ~PluginInterface() {}
|
||||
|
||||
// Common plugin methods
|
||||
virtual QString name() const = 0;
|
||||
virtual QString version() const = 0;
|
||||
|
||||
// TODO: this should be defined here and removed from the modules, but needs some work
|
||||
// Q_INVOKABLE void initLogos(LogosAPI* logosAPIInstance);
|
||||
|
||||
LogosAPI* logosAPI = nullptr;
|
||||
};
|
||||
|
||||
// Define the interface ID used by Qt's plugin system
|
||||
#define PluginInterface_iid "com.example.PluginInterface"
|
||||
|
||||
Q_DECLARE_INTERFACE(PluginInterface, PluginInterface_iid)
|
||||
|
||||
#endif // PLUGIN_INTERFACE_H
|
||||
@@ -0,0 +1,129 @@
|
||||
cmake_minimum_required(VERSION 3.14)
|
||||
project(LogosQtHost)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_AUTOMOC ON)
|
||||
|
||||
find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Core RemoteObjects)
|
||||
find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Core RemoteObjects)
|
||||
|
||||
# The protocol layer — transports, consumer core, token manager, the abstract
|
||||
# LogosProviderObject interface and the lp_* C ABI. This library is the Qt
|
||||
# HOST runtime on top of it: the object a Qt plugin is handed (LogosAPI), the
|
||||
# provider side that publishes it (LogosAPIProvider), and the provider base
|
||||
# classes a generated or hand-written plugin derives.
|
||||
if(NOT DEFINED LOGOS_PROTOCOL_ROOT)
|
||||
if(DEFINED ENV{LOGOS_PROTOCOL_ROOT})
|
||||
set(LOGOS_PROTOCOL_ROOT "$ENV{LOGOS_PROTOCOL_ROOT}")
|
||||
elseif(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/../../logos-protocol/cpp/logos_protocol.h")
|
||||
set(LOGOS_PROTOCOL_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../../logos-protocol")
|
||||
endif()
|
||||
endif()
|
||||
if(EXISTS "${LOGOS_PROTOCOL_ROOT}/lib/cmake/logos-protocol")
|
||||
# Installed protocol package → proper layered link.
|
||||
find_package(logos-protocol REQUIRED
|
||||
PATHS "${LOGOS_PROTOCOL_ROOT}/lib/cmake/logos-protocol" NO_DEFAULT_PATH)
|
||||
set(LP_TARGET logos-protocol::logos_protocol)
|
||||
elseif(EXISTS "${LOGOS_PROTOCOL_ROOT}/cpp/CMakeLists.txt")
|
||||
# Source checkout → build it as a subproject (dev convenience).
|
||||
add_subdirectory("${LOGOS_PROTOCOL_ROOT}/cpp"
|
||||
"${CMAKE_BINARY_DIR}/logos-protocol-build")
|
||||
set(LP_TARGET logos_protocol)
|
||||
else()
|
||||
message(FATAL_ERROR "logos-protocol not found. Set LOGOS_PROTOCOL_ROOT to an "
|
||||
"installed logos-protocol prefix or a source checkout.")
|
||||
endif()
|
||||
|
||||
set(QT_HOST_SOURCES
|
||||
logos_api.cpp
|
||||
logos_api.h
|
||||
logos_api_provider.cpp
|
||||
logos_api_provider.h
|
||||
logos_provider_object.cpp
|
||||
logos_provider_object.h
|
||||
qt_provider_object.cpp
|
||||
qt_provider_object.h
|
||||
# Not part of the host runtime proper, but qt_provider_object.cpp's
|
||||
# QMetaObject dispatch decodes every incoming argument through
|
||||
# logos::qtArgDecode. Carrying the legacy adapter means carrying this.
|
||||
logos_qt_arg_decode.cpp
|
||||
logos_qt_arg_decode.h
|
||||
)
|
||||
|
||||
add_library(logos_qt_host STATIC ${QT_HOST_SOURCES})
|
||||
|
||||
target_link_libraries(logos_qt_host PUBLIC
|
||||
${LP_TARGET}
|
||||
Qt${QT_VERSION_MAJOR}::Core
|
||||
Qt${QT_VERSION_MAJOR}::RemoteObjects
|
||||
)
|
||||
|
||||
target_include_directories(logos_qt_host PUBLIC
|
||||
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
|
||||
$<INSTALL_INTERFACE:include>
|
||||
)
|
||||
|
||||
set_target_properties(logos_qt_host PROPERTIES
|
||||
ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib"
|
||||
)
|
||||
|
||||
install(TARGETS logos_qt_host
|
||||
EXPORT logos-qt-hostTargets
|
||||
ARCHIVE DESTINATION lib
|
||||
LIBRARY DESTINATION lib
|
||||
RUNTIME DESTINATION bin
|
||||
INCLUDES DESTINATION include
|
||||
)
|
||||
|
||||
install(EXPORT logos-qt-hostTargets
|
||||
FILE logos-qt-hostTargets.cmake
|
||||
NAMESPACE logos-qt-host::
|
||||
DESTINATION lib/cmake/logos-qt-host
|
||||
)
|
||||
|
||||
include(CMakePackageConfigHelpers)
|
||||
configure_package_config_file(
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/logos-qt-hostConfig.cmake.in"
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/logos-qt-hostConfig.cmake"
|
||||
INSTALL_DESTINATION lib/cmake/logos-qt-host
|
||||
)
|
||||
write_basic_package_version_file(
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/logos-qt-hostConfigVersion.cmake"
|
||||
VERSION 0.1.0
|
||||
COMPATIBILITY SameMajorVersion
|
||||
)
|
||||
install(FILES
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/logos-qt-hostConfig.cmake"
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/logos-qt-hostConfigVersion.cmake"
|
||||
DESTINATION lib/cmake/logos-qt-host
|
||||
)
|
||||
|
||||
set(QT_HOST_PUBLIC_HEADERS
|
||||
logos_api.h
|
||||
logos_api_provider.h
|
||||
logos_provider_object.h
|
||||
qt_provider_object.h
|
||||
logos_qt_arg_decode.h
|
||||
)
|
||||
|
||||
# Headers keep their historical names so existing `#include "logos_api.h"`
|
||||
# lines resolve unchanged once consumers add this prefix's include dir.
|
||||
install(FILES ${QT_HOST_PUBLIC_HEADERS} DESTINATION include)
|
||||
|
||||
# Legacy Qt plugin interface (PluginInterface / initLogos(LogosAPI*)) —
|
||||
# installed at include/core/ exactly where logos-cpp-sdk, and then
|
||||
# logos-qt-sdk, shipped it. Consumers put include/core on the include path.
|
||||
install(FILES
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../core/interface.h
|
||||
DESTINATION include/core
|
||||
)
|
||||
|
||||
# interface.h reaches LogosAPI as `#include "../cpp/logos_api.h"`, i.e. it
|
||||
# resolves RELATIVE TO ITSELF, so from include/core/ it needs include/cpp/ to
|
||||
# exist. logos-qt-sdk satisfied that by shipping a second, source-export copy
|
||||
# of the headers from a separate derivation; here the same mirror is part of
|
||||
# the one install, which keeps the prefix self-consistent on its own. The flat
|
||||
# copy above stays the one consumers include by name — this exists only so the
|
||||
# relative shape inside interface.h keeps resolving, unchanged.
|
||||
install(FILES ${QT_HOST_PUBLIC_HEADERS} DESTINATION include/cpp)
|
||||
@@ -0,0 +1,14 @@
|
||||
@PACKAGE_INIT@
|
||||
|
||||
# logos-qt-hostConfig.cmake — consumed by `find_package(logos-qt-host)`.
|
||||
# Re-resolves the runtime's dependencies (Qt + logos-protocol, which itself
|
||||
# propagates Boost / OpenSSL / nlohmann_json) before importing the target.
|
||||
|
||||
include(CMakeFindDependencyMacro)
|
||||
|
||||
find_dependency(Qt6 REQUIRED COMPONENTS Core RemoteObjects)
|
||||
find_dependency(logos-protocol REQUIRED)
|
||||
|
||||
include("${CMAKE_CURRENT_LIST_DIR}/logos-qt-hostTargets.cmake")
|
||||
|
||||
check_required_components(logos-qt-host)
|
||||
@@ -0,0 +1,118 @@
|
||||
#include "logos_api.h"
|
||||
#include "logos_api_client.h"
|
||||
#include "logos_api_provider.h"
|
||||
#include "logos_thread_marshal.h"
|
||||
#include "token_manager.h"
|
||||
#include <QVariant>
|
||||
#include <string>
|
||||
|
||||
LogosAPI::LogosAPI(const QString& module_name, QObject *parent)
|
||||
: LogosAPI(module_name, LogosTransportSet{}, parent)
|
||||
{
|
||||
}
|
||||
|
||||
LogosAPI::LogosAPI(const QString& module_name,
|
||||
LogosTransportSet transports,
|
||||
QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_module_name(module_name)
|
||||
, m_provider(nullptr)
|
||||
, m_token_manager(nullptr)
|
||||
{
|
||||
m_provider = new LogosAPIProvider(m_module_name, std::move(transports), this);
|
||||
m_token_manager = &TokenManager::instance();
|
||||
qRegisterMetaType<LogosResult>("LogosResult");
|
||||
}
|
||||
|
||||
LogosAPI::LogosAPI(const std::string& module_name, QObject *parent)
|
||||
: LogosAPI(QString::fromStdString(module_name), parent)
|
||||
{
|
||||
}
|
||||
|
||||
LogosAPI::~LogosAPI()
|
||||
{
|
||||
// Provider and client will be automatically deleted as child objects
|
||||
// Token manager is a singleton, so we don't delete it
|
||||
|
||||
|
||||
}
|
||||
|
||||
LogosAPIProvider* LogosAPI::getProvider() const
|
||||
{
|
||||
return m_provider;
|
||||
}
|
||||
|
||||
LogosAPIClient* LogosAPI::getClient(const QString& target_module) const
|
||||
{
|
||||
// The no-transport overload is just shorthand for "use the
|
||||
// process-global default" — the explicit-transport overload below
|
||||
// is the single resolution path. Mode-awareness lives in the
|
||||
// factory, so this delegation preserves Mock/Local semantics.
|
||||
return getClient(target_module, LogosTransportConfigGlobal::getDefault());
|
||||
}
|
||||
|
||||
LogosAPIClient* LogosAPI::getClient(const std::string& target_module) const
|
||||
{
|
||||
return getClient(QString::fromStdString(target_module));
|
||||
}
|
||||
|
||||
LogosAPIClient* LogosAPI::getClient(const QString& target_module,
|
||||
const LogosTransportConfig& transport) const
|
||||
{
|
||||
// Create the client (and its consumers + transport replicas) on this
|
||||
// LogosAPI's owner thread — the module's main/event-loop thread — even when
|
||||
// called from a worker thread (e.g. an HTTP handler). Qt Remote Objects
|
||||
// replicas only work on the thread that created them, so construction (and
|
||||
// the cache it populates) must happen there. invokeRemoteMethod() then
|
||||
// marshals calls back to the same thread. See logos_thread_marshal.h.
|
||||
return logos::runOnOwnerThread(const_cast<LogosAPI*>(this),
|
||||
[&]() -> LogosAPIClient* {
|
||||
// Single cache, single construction path. Key composition mirrors
|
||||
// the factory's resolution rule (see LogosAPIClientCacheKey in
|
||||
// logos_api.h):
|
||||
// - Mock/Local mode: every cfg collapses to one cache slot per
|
||||
// target — switching cfg returns the same MockTransport-backed
|
||||
// client instead of allocating a duplicate.
|
||||
// - Remote mode: every distinguishing field of cfg matters, so
|
||||
// two callers with different TLS/codec settings get separate
|
||||
// clients (no risk of silently reusing an insecure transport).
|
||||
//
|
||||
// The capability_module transport — used by the client's
|
||||
// auto-`requestModule` flow — falls back to the registered
|
||||
// override (if any) or the global default. Two-arg getClient
|
||||
// intentionally doesn't expose a second transport here; callers
|
||||
// that care register the capability_module transport once via
|
||||
// setCapabilityModuleTransport() and the rest is plumbing.
|
||||
const LogosAPIClientCacheKey key{
|
||||
target_module, LogosModeConfig::getMode(), transport};
|
||||
auto it = m_clients.constFind(key);
|
||||
if (it != m_clients.constEnd()) return it.value();
|
||||
|
||||
const LogosTransportConfig capabilityTransport =
|
||||
m_capabilityModuleTransport.has_value()
|
||||
? *m_capabilityModuleTransport
|
||||
: LogosTransportConfigGlobal::getDefault();
|
||||
|
||||
LogosAPIClient* client = new LogosAPIClient(
|
||||
target_module, m_module_name, m_token_manager,
|
||||
transport, capabilityTransport,
|
||||
const_cast<LogosAPI*>(this));
|
||||
m_clients.insert(key, client);
|
||||
return client;
|
||||
});
|
||||
}
|
||||
|
||||
TokenManager* LogosAPI::getTokenManager() const
|
||||
{
|
||||
return m_token_manager;
|
||||
}
|
||||
|
||||
void LogosAPI::setCapabilityModuleTransport(const LogosTransportConfig& transport)
|
||||
{
|
||||
m_capabilityModuleTransport = transport;
|
||||
}
|
||||
|
||||
bool LogosAPI::setProperty(const char* name, const std::string& value)
|
||||
{
|
||||
return QObject::setProperty(name, QVariant(QString::fromStdString(value)));
|
||||
}
|
||||
+271
@@ -0,0 +1,271 @@
|
||||
#ifndef LOGOS_API_H
|
||||
#define LOGOS_API_H
|
||||
|
||||
#include "logos_mode.h"
|
||||
#include "logos_shared_api.h"
|
||||
#include "logos_transport_config.h"
|
||||
#include "logos_types.h"
|
||||
|
||||
#include <QHash>
|
||||
#include <QHashFunctions>
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
class LogosAPIClient;
|
||||
class LogosAPIProvider;
|
||||
class TokenManager;
|
||||
|
||||
// qHash for LogosTransportConfig — combined with operator== from
|
||||
// logos_transport_config.h, this lets QHash use it as a key. Lives here
|
||||
// rather than in logos_transport_config.h so that header stays Qt-free
|
||||
// (the SDK is being de-Qt'd; only Qt-using consumers like the cache
|
||||
// here pull in the QHash adapter).
|
||||
//
|
||||
// Every field that distinguishes one explicit-transport client from
|
||||
// another contributes to the hash; otherwise two callers with different
|
||||
// TLS or codec settings could land on the same bucket and cache-alias
|
||||
// onto a single client.
|
||||
inline size_t qHash(const LogosTransportConfig& cfg, size_t seed = 0) noexcept
|
||||
{
|
||||
return qHashMulti(seed,
|
||||
static_cast<int>(cfg.protocol),
|
||||
std::hash<std::string>{}(cfg.host),
|
||||
cfg.port,
|
||||
std::hash<std::string>{}(cfg.caFile),
|
||||
std::hash<std::string>{}(cfg.certFile),
|
||||
std::hash<std::string>{}(cfg.keyFile),
|
||||
cfg.verifyPeer,
|
||||
static_cast<int>(cfg.codec));
|
||||
}
|
||||
|
||||
// LogosAPIClient cache key. Mirrors the factory's transport-resolution
|
||||
// rule so two callers that would observe the same connection share a
|
||||
// cached client:
|
||||
//
|
||||
// - Mock / Local mode → transport is ignored at construction; key
|
||||
// ignores it too. Switching mode changes the
|
||||
// key (so cached clients don't bleed across
|
||||
// mode switches in tests).
|
||||
// - Remote mode → cfg picks the wire endpoint; key includes
|
||||
// the full LogosTransportConfig.
|
||||
//
|
||||
// Without the mode-aware comparison, calling
|
||||
// `getClient(x, tcp)` and `getClient(x, tcp_ssl)` in Mock mode would
|
||||
// allocate two clients pointing at functionally identical
|
||||
// MockTransportConnections.
|
||||
struct LogosAPIClientCacheKey {
|
||||
QString target;
|
||||
LogosMode mode;
|
||||
LogosTransportConfig transport; // only compared when mode == Remote
|
||||
};
|
||||
|
||||
inline bool operator==(const LogosAPIClientCacheKey& a,
|
||||
const LogosAPIClientCacheKey& b) noexcept
|
||||
{
|
||||
if (a.target != b.target) return false;
|
||||
if (a.mode != b.mode) return false;
|
||||
return a.mode == LogosMode::Remote ? a.transport == b.transport : true;
|
||||
}
|
||||
|
||||
inline size_t qHash(const LogosAPIClientCacheKey& k, size_t seed = 0) noexcept
|
||||
{
|
||||
if (k.mode == LogosMode::Remote) {
|
||||
return qHashMulti(seed, k.target, static_cast<int>(k.mode), k.transport);
|
||||
}
|
||||
// Mock / Local: transport is irrelevant — leave it out of the hash
|
||||
// so it can't bias which bucket the key lands in.
|
||||
return qHashMulti(seed, k.target, static_cast<int>(k.mode));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief LogosAPI provides a unified interface to the Logos SDK
|
||||
*
|
||||
* This class initializes and keeps instances of the client provider and token manager.
|
||||
*
|
||||
* LOGOS_SHARED_API because this is the object handed across the DLL boundary:
|
||||
* the host constructs a LogosAPI inside liblogos_core and passes the pointer to
|
||||
* the UI plugin through PluginInterface::logosAPI. Its constructor caches
|
||||
* `&TokenManager::instance()`, so on PE a plugin that links its own copy of
|
||||
* logos_api.cpp.obj caches a DIFFERENT singleton than the one the host wrote
|
||||
* the token into. Importing instead of re-linking is what makes the two agree.
|
||||
* See logos_shared_api.h in logos-protocol.
|
||||
*/
|
||||
class LOGOS_SHARED_API LogosAPI : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Construct a new LogosAPI instance
|
||||
* @param module_name The name of this module
|
||||
* @param parent Parent QObject
|
||||
*/
|
||||
explicit LogosAPI(const QString& module_name, QObject *parent = nullptr);
|
||||
|
||||
/**
|
||||
* @brief Construct a new LogosAPI with an explicit transport set.
|
||||
*
|
||||
* `transports` is empty ⇒ use the process-global default (back-compat).
|
||||
* Non-empty ⇒ provider publishes on every configured transport
|
||||
* (e.g. a daemon listing both LocalSocket and TCP+SSL so the CLI has
|
||||
* a fast in-process path *and* remote clients have a secure path).
|
||||
*/
|
||||
LogosAPI(const QString& module_name,
|
||||
LogosTransportSet transports,
|
||||
QObject *parent = nullptr);
|
||||
|
||||
/**
|
||||
* @brief Construct a new LogosAPI instance (const char* overload — resolves ambiguity)
|
||||
*/
|
||||
explicit LogosAPI(const char* module_name, QObject *parent = nullptr)
|
||||
: LogosAPI(QString(module_name), parent) {}
|
||||
|
||||
/**
|
||||
* @brief Construct with const char* and explicit transport set.
|
||||
*/
|
||||
LogosAPI(const char* module_name, LogosTransportSet transports, QObject *parent = nullptr)
|
||||
: LogosAPI(QString(module_name), std::move(transports), parent) {}
|
||||
|
||||
/**
|
||||
* @brief Construct a new LogosAPI instance (std::string overload)
|
||||
*/
|
||||
explicit LogosAPI(const std::string& module_name, QObject *parent = nullptr);
|
||||
|
||||
/**
|
||||
* @brief Construct with std::string and explicit transport set.
|
||||
*/
|
||||
LogosAPI(const std::string& module_name, LogosTransportSet transports, QObject *parent = nullptr)
|
||||
: LogosAPI(QString::fromStdString(module_name), std::move(transports), parent) {}
|
||||
|
||||
/**
|
||||
* @brief Destructor
|
||||
*/
|
||||
~LogosAPI();
|
||||
|
||||
/**
|
||||
* @brief The name of the module this LogosAPI belongs to.
|
||||
*
|
||||
* Every outbound call already carries it (LogosAPIClient's `origin`), it
|
||||
* was just never readable from the outside. Generated consumer wrappers
|
||||
* need it: a wrapper that reaches the transport through the lp C ABI must
|
||||
* name its origin at client-creation time, and the only handle it is given
|
||||
* is this object. Reading it here keeps the wrapper's constructor
|
||||
* signature — `(LogosAPI*)` / `(LogosAPI*, const QString&)` — unchanged,
|
||||
* so no call site moves.
|
||||
*/
|
||||
QString moduleName() const { return m_module_name; }
|
||||
|
||||
/**
|
||||
* @brief Get the client provider instance
|
||||
* @return LogosAPIProvider* Pointer to the provider
|
||||
*/
|
||||
LogosAPIProvider* getProvider() const;
|
||||
|
||||
/**
|
||||
* @brief Get the client instance for communicating with a module
|
||||
* @param target_module The module to communicate with
|
||||
* @return LogosAPIClient* Pointer to the client
|
||||
*/
|
||||
LogosAPIClient* getClient(const QString& target_module) const;
|
||||
|
||||
/**
|
||||
* @brief Get the client instance — const char* overload (resolves ambiguity)
|
||||
*/
|
||||
LogosAPIClient* getClient(const char* target_module) const
|
||||
{ return getClient(QString(target_module)); }
|
||||
|
||||
/**
|
||||
* @brief Get the client instance for communicating with a module (std::string overload)
|
||||
*/
|
||||
LogosAPIClient* getClient(const std::string& target_module) const;
|
||||
|
||||
/**
|
||||
* @brief Get a client that uses an *explicit* transport instead of
|
||||
* the process-global default.
|
||||
*
|
||||
* Use this when the caller needs to dial one module over a
|
||||
* particular protocol without side-effecting the rest of the
|
||||
* process. Canonical case: a CLI that talks only to `core_service`
|
||||
* over tcp_ssl — using `LogosTransportConfigGlobal::setDefault` for
|
||||
* that would also flip the same process's `LogosAPIProvider` into
|
||||
* trying to bind a tcp_ssl server, which the CLI has no cert for.
|
||||
*
|
||||
* Cached per (target_module, full LogosTransportConfig) — repeat
|
||||
* calls with the same target *and* the same transport return the
|
||||
* same client. The cache key covers every config field that can
|
||||
* distinguish two clients (protocol, host, port, codec, all TLS
|
||||
* settings), via the operator== / qHash defined alongside
|
||||
* LogosTransportConfig, so two callers with different TLS or codec
|
||||
* settings always get separate clients — no risk of silently
|
||||
* reusing an insecure connection where a secure one was asked for.
|
||||
*/
|
||||
LogosAPIClient* getClient(const QString& target_module,
|
||||
const LogosTransportConfig& transport) const;
|
||||
|
||||
/**
|
||||
* @brief Get the token manager instance
|
||||
* @return TokenManager* Pointer to the token manager
|
||||
*/
|
||||
TokenManager* getTokenManager() const;
|
||||
|
||||
/**
|
||||
* @brief Set the transport used by the SDK's auto-`requestModule`
|
||||
* token-fetch flow (inside LogosAPIClient::invokeRemoteMethod{,Async}).
|
||||
*
|
||||
* That flow always dials `capability_module` to fetch a per-target
|
||||
* token, regardless of which module is the actual call target.
|
||||
* Without an explicit transport it falls through to
|
||||
* LogosTransportConfigGlobal::getDefault() (LocalSocket), which
|
||||
* times out 20 s when capability_module is reachable only on TCP
|
||||
* (e.g. CLI on host, daemon in container).
|
||||
*
|
||||
* Callers that have read the daemon's per-module advertised
|
||||
* transports (e.g. from logoscore's daemon.json) should register
|
||||
* capability_module's transport here so getClient builds each
|
||||
* LogosAPIClient with the right capability_consumer.
|
||||
*
|
||||
* The setting only affects clients constructed *after* this call
|
||||
* — clients already in the cache keep whatever capability transport
|
||||
* they were built with.
|
||||
*/
|
||||
void setCapabilityModuleTransport(const LogosTransportConfig& transport);
|
||||
|
||||
using QObject::setProperty;
|
||||
|
||||
/**
|
||||
* @brief Set a dynamic property from a UTF-8 std::string (delegates to QVariant + QString).
|
||||
*/
|
||||
bool setProperty(const char* name, const std::string& value);
|
||||
|
||||
private:
|
||||
QString m_module_name;
|
||||
LogosAPIProvider* m_provider;
|
||||
// Single cache for both getClient overloads. Keyed by a
|
||||
// mode-aware composite (LogosAPIClientCacheKey above) so that:
|
||||
// - Mock/Local mode buckets ignore transport (the factory does too)
|
||||
// - Remote mode keys include the full LogosTransportConfig
|
||||
// - the no-transport overload resolves to the same key as an
|
||||
// explicit caller passing LogosTransportConfigGlobal::getDefault()
|
||||
// - mode switches don't return stale clients from the previous mode
|
||||
mutable QHash<LogosAPIClientCacheKey, LogosAPIClient*> m_clients;
|
||||
TokenManager* m_token_manager;
|
||||
// ABI note: this private layout is consumed by every plugin that
|
||||
// statically links libsdk. Inserting a field above m_token_manager
|
||||
// shifts its offset and SILENTLY breaks plugins compiled before
|
||||
// the change — they read garbage where m_token_manager used to
|
||||
// live, getClient() then constructs LogosAPIClients with a bogus
|
||||
// TokenManager*, and the first cross-process call segfaults.
|
||||
// Append new private members at the END only. (Long-term cure:
|
||||
// pimpl this class so sizeof / offsets become opaque.)
|
||||
//
|
||||
// Optional override for the capability_module transport used by
|
||||
// each LogosAPIClient's pre-built m_capability_consumer. Set via
|
||||
// setCapabilityModuleTransport(). nullopt = use the global default.
|
||||
std::optional<LogosTransportConfig> m_capabilityModuleTransport;
|
||||
};
|
||||
|
||||
#endif // LOGOS_API_H
|
||||
@@ -0,0 +1,306 @@
|
||||
#include "logos_api_provider.h"
|
||||
#include "logos_object.h"
|
||||
#include "logos_provider_object.h"
|
||||
#include "qt_provider_object.h"
|
||||
#include "module_proxy.h"
|
||||
#include "logos_api.h"
|
||||
#include "logos_instance.h"
|
||||
#include "logos_transport.h"
|
||||
#include "logos_transport_factory.h"
|
||||
#include "token_manager.h"
|
||||
#include <QDebug>
|
||||
#include <string>
|
||||
|
||||
LogosAPIProvider::LogosAPIProvider(const QString& module_name,
|
||||
LogosTransportSet transports,
|
||||
QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_registryUrl(LogosInstance::id(module_name))
|
||||
, m_moduleProxy(nullptr)
|
||||
, m_qtProviderObject(nullptr)
|
||||
{
|
||||
// Helper: defer-construct one host and only retain it if the
|
||||
// factory actually returned something. createHost() can return
|
||||
// nullptr — e.g. PlainTransportHost::start() failure (TCP bind,
|
||||
// SSL cert load) — and we don't want to leave a null entry that
|
||||
// would crash the publish/unpublish paths later.
|
||||
auto pushHost = [&](auto&& host, const char* label) {
|
||||
if (host) {
|
||||
m_transports.push_back(std::forward<decltype(host)>(host));
|
||||
} else {
|
||||
qWarning() << "LogosAPIProvider: createHost returned null"
|
||||
<< "for" << module_name << label
|
||||
<< "— transport disabled";
|
||||
}
|
||||
};
|
||||
|
||||
if (transports.empty()) {
|
||||
// Back-compat: one host, chosen by the global mode + transport config.
|
||||
pushHost(LogosTransportFactory::createHost(m_registryUrl), "(default)");
|
||||
} else {
|
||||
// One host per configured transport — lets a single provider serve
|
||||
// its object on several endpoints simultaneously (local-socket +
|
||||
// TCP, TCP + TCP+SSL, etc.).
|
||||
for (const auto& cfg : transports)
|
||||
pushHost(LogosTransportFactory::createHost(cfg, m_registryUrl), "(per-cfg)");
|
||||
}
|
||||
}
|
||||
|
||||
LogosAPIProvider::~LogosAPIProvider()
|
||||
{
|
||||
if (!m_registeredObjectName.isEmpty()) {
|
||||
// Defensive: m_transports should never contain nullptr (the
|
||||
// ctor filters them out via pushHost), but guard here too —
|
||||
// a future code path that pushes directly without going
|
||||
// through pushHost would otherwise crash on shutdown.
|
||||
for (auto& t : m_transports) {
|
||||
if (t) t->unpublishObject(m_registeredObjectName);
|
||||
}
|
||||
}
|
||||
if (!m_registeredHandshakeName.isEmpty()) {
|
||||
for (auto& t : m_transports) {
|
||||
if (t) t->unpublishObject(m_registeredHandshakeName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// QObject* path: auto-detects LogosProviderPlugin; falls back to QtProviderObject wrapper
|
||||
bool LogosAPIProvider::registerObject(const QString& name, QObject* object)
|
||||
{
|
||||
if (!object) {
|
||||
qWarning() << "LogosAPIProvider: Cannot register null object";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (name.isEmpty()) {
|
||||
qWarning() << "LogosAPIProvider: Cannot register object with empty name";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_moduleProxy) {
|
||||
qCritical() << "LogosAPIProvider: Object already registered. Only one registration per provider is allowed";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if this plugin implements LogosProviderPlugin (new API)
|
||||
LogosProviderPlugin* providerPlugin = qobject_cast<LogosProviderPlugin*>(object);
|
||||
if (providerPlugin) {
|
||||
qDebug() << "[LogosProviderObject] LogosAPIProvider: detected LogosProviderPlugin for" << name;
|
||||
LogosProviderObject* provider = providerPlugin->createProviderObject();
|
||||
if (provider) {
|
||||
return registerObject(name, provider);
|
||||
}
|
||||
qWarning() << "LogosAPIProvider: createProviderObject() returned null for" << name;
|
||||
}
|
||||
|
||||
// Legacy path: wrap QObject in QtProviderObject adapter
|
||||
qDebug() << "[LogosProviderObject] LogosAPIProvider: wrapping QObject in QtProviderObject for" << name;
|
||||
|
||||
m_qtProviderObject = new QtProviderObject(object, this);
|
||||
|
||||
// Handshake surface before init, business object after — see the
|
||||
// LogosProviderObject overload for why.
|
||||
publishHandshake(name, m_qtProviderObject);
|
||||
|
||||
m_qtProviderObject->init(qobject_cast<LogosAPI*>(parent()));
|
||||
|
||||
return publishProvider(name, m_qtProviderObject);
|
||||
}
|
||||
|
||||
bool LogosAPIProvider::registerObject(const std::string& name, QObject* object)
|
||||
{
|
||||
return registerObject(QString::fromStdString(name), object);
|
||||
}
|
||||
|
||||
// New path: LogosProviderObject* -> ModuleProxy -> transport
|
||||
bool LogosAPIProvider::registerObject(const QString& name, LogosProviderObject* provider)
|
||||
{
|
||||
if (!provider) {
|
||||
qWarning() << "LogosAPIProvider: Cannot register null provider";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (name.isEmpty()) {
|
||||
qWarning() << "LogosAPIProvider: Cannot register provider with empty name";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_moduleProxy) {
|
||||
qCritical() << "LogosAPIProvider: Object already registered. Only one registration per provider is allowed";
|
||||
return false;
|
||||
}
|
||||
|
||||
qDebug() << "[LogosProviderObject] LogosAPIProvider: registering LogosProviderObject directly for" << name;
|
||||
|
||||
// Publish the handshake surface BEFORE the initializer runs, and the
|
||||
// business object after it, as always. The initializer is synchronous and
|
||||
// routinely calls out — including capability_module's requestModule, which
|
||||
// capability answers by pushing a token back to this very module. With only
|
||||
// the business object, that push was unsatisfiable: capability waited for a
|
||||
// source that could not appear until the initializer returned, and the
|
||||
// initializer could not return until capability answered.
|
||||
//
|
||||
// Publishing the token-only surface early breaks that circle without
|
||||
// changing what callers of real methods see: they still block at acquire
|
||||
// until the business object appears, exactly as before.
|
||||
publishHandshake(name, provider);
|
||||
|
||||
provider->init(qobject_cast<LogosAPI*>(parent()));
|
||||
|
||||
return publishProvider(name, provider);
|
||||
}
|
||||
|
||||
void LogosAPIProvider::setTokenValidator(TokenValidator validator)
|
||||
{
|
||||
// Store first, then forward the member — a single source of truth, so the
|
||||
// proxy and the pending copy can't diverge if the validator carries state.
|
||||
m_pendingValidator = std::move(validator);
|
||||
if (m_moduleProxy) {
|
||||
m_moduleProxy->setTokenValidator(m_pendingValidator);
|
||||
}
|
||||
}
|
||||
|
||||
void LogosAPIProvider::seedHandshakeTrustAnchor()
|
||||
{
|
||||
QObject* api = parent();
|
||||
if (!api) {
|
||||
return;
|
||||
}
|
||||
|
||||
// The host publishes its token as a property on the LogosAPI object before
|
||||
// calling registerObject (logos-module-loader-qt module_initializer.cpp).
|
||||
// An older host that does not set it leaves the store empty — the handshake
|
||||
// surface then refuses, and the consumer falls back to the business object
|
||||
// exactly as it did before this surface existed.
|
||||
const QString hostToken = api->property("authToken").toString();
|
||||
if (hostToken.isEmpty()) {
|
||||
qDebug() << "[LogosProviderObject] LogosAPIProvider: no authToken property"
|
||||
<< "- handshake surface will refuse until the initializer seeds the"
|
||||
<< "token store; consumers fall back to the business object";
|
||||
return;
|
||||
}
|
||||
|
||||
// Never overwrite an entry the module already holds: this runs before init(),
|
||||
// so a non-empty value here came from somewhere with more context than us.
|
||||
TokenManager& tokens = TokenManager::instance();
|
||||
for (const QString& key : { QStringLiteral("core"), QStringLiteral("capability_module") }) {
|
||||
if (tokens.getToken(key).isEmpty()) {
|
||||
tokens.saveToken(key, hostToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void LogosAPIProvider::publishHandshake(const QString& name, LogosProviderObject* provider)
|
||||
{
|
||||
// The handshake proxy needs the ModuleProxy that will own the token store,
|
||||
// so build that now; publishProvider() reuses it rather than making another.
|
||||
if (!m_moduleProxy) {
|
||||
m_moduleProxy = new ModuleProxy(provider, this);
|
||||
if (m_pendingValidator) {
|
||||
m_moduleProxy->setTokenValidator(m_pendingValidator);
|
||||
}
|
||||
}
|
||||
|
||||
// Seed the trust anchor BEFORE the surface goes live.
|
||||
//
|
||||
// ModuleProxy::informModuleToken only accepts a caller whose token matches
|
||||
// TokenManager's "core" or "capability_module" entry. Those entries are
|
||||
// written by the module's own initializer — the generated cdylib glue reads
|
||||
// the host's `authToken` property and calls logos_module_accept_token("core")
|
||||
// / ("capability_module") — and init() runs AFTER this point, between
|
||||
// publishHandshake and publishProvider.
|
||||
//
|
||||
// That is precisely the window this surface exists to serve, so without this
|
||||
// the store is empty for the whole window and every push that arrives is
|
||||
// refused: the surface would be reachable and useless, and capability_module
|
||||
// would report a failed grant. (Measured before this line existed: the
|
||||
// rejection appeared in 29 of 34 runs and never in the pre-fix baseline.)
|
||||
//
|
||||
// This grants nothing new. It installs the same host-issued token the
|
||||
// initializer installs moments later, just early enough to be usable.
|
||||
seedHandshakeTrustAnchor();
|
||||
|
||||
m_handshakeProxy = new ModuleHandshakeProxy(m_moduleProxy, this);
|
||||
const QString handshakeName = logos::handshakeObjectName(name);
|
||||
|
||||
bool published = false;
|
||||
for (auto& t : m_transports) {
|
||||
if (!t) continue;
|
||||
if (t->publishObject(handshakeName, m_handshakeProxy)) published = true;
|
||||
}
|
||||
if (published) {
|
||||
m_registeredHandshakeName = handshakeName;
|
||||
qDebug() << "[LogosProviderObject] LogosAPIProvider: published handshake surface"
|
||||
<< handshakeName << "- token delivery is reachable while" << name
|
||||
<< "initializes";
|
||||
} else {
|
||||
// Not fatal: a transport that cannot carry the handshake surface just
|
||||
// means capability_module falls back to the business object, which is
|
||||
// exactly how things worked before this existed.
|
||||
qDebug() << "[LogosProviderObject] LogosAPIProvider: no transport published"
|
||||
<< handshakeName << "- capability_module will fall back to" << name;
|
||||
}
|
||||
}
|
||||
|
||||
bool LogosAPIProvider::publishProvider(const QString& name, LogosProviderObject* provider)
|
||||
{
|
||||
// publishHandshake() may already have created the proxy (it needs the token
|
||||
// store to exist before the initializer runs); reuse it so the token a peer
|
||||
// delivered early is the one the business object consults.
|
||||
if (!m_moduleProxy) {
|
||||
m_moduleProxy = new ModuleProxy(provider, this);
|
||||
}
|
||||
// Apply a validator installed before registration, before the proxy is
|
||||
// published on any transport (so no call can slip in unvalidated).
|
||||
if (m_pendingValidator) {
|
||||
m_moduleProxy->setTokenValidator(m_pendingValidator);
|
||||
}
|
||||
|
||||
// Publish on every configured transport. Success = any transport
|
||||
// accepted the publish (follow-up: surface per-transport failures).
|
||||
bool success = false;
|
||||
for (auto& t : m_transports) {
|
||||
if (!t) continue; // see ~LogosAPIProvider — defensive null-skip.
|
||||
if (t->publishObject(name, m_moduleProxy)) success = true;
|
||||
}
|
||||
if (success) {
|
||||
m_registeredObjectName = name;
|
||||
qDebug() << "[LogosProviderObject] LogosAPIProvider: successfully published" << name;
|
||||
} else {
|
||||
qCritical() << "LogosAPIProvider: Failed to publish" << name;
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
QString LogosAPIProvider::registryUrl() const
|
||||
{
|
||||
return m_registryUrl;
|
||||
}
|
||||
|
||||
bool LogosAPIProvider::saveToken(const QString& from_module_name, const QString& token)
|
||||
{
|
||||
if (!m_moduleProxy) {
|
||||
qWarning() << "LogosAPIProvider: Cannot save token - no module proxy available";
|
||||
return false;
|
||||
}
|
||||
|
||||
qDebug() << "LogosAPIProvider: Delegating saveToken to module proxy for:" << from_module_name;
|
||||
return m_moduleProxy->saveToken(from_module_name, token);
|
||||
}
|
||||
|
||||
void LogosAPIProvider::onEventResponse(LogosObject* object, const QString& eventName, const QVariantList& data)
|
||||
{
|
||||
qDebug() << "[LogosObject] LogosAPIProvider::onEventResponse" << eventName << "-> LogosObject::emitEvent";
|
||||
|
||||
if (eventName.isEmpty()) {
|
||||
qWarning() << "LogosAPIProvider: Event name cannot be empty";
|
||||
return;
|
||||
}
|
||||
if (!object) {
|
||||
qWarning() << "LogosAPIProvider: Cannot emit event on null object";
|
||||
return;
|
||||
}
|
||||
|
||||
object->emitEvent(eventName, data);
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
#ifndef LOGOS_API_PROVIDER_H
|
||||
#define LOGOS_API_PROVIDER_H
|
||||
|
||||
#include "logos_transport_config.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QVariant>
|
||||
#include <QVariantList>
|
||||
#include <QMap>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
class LogosTransportHost;
|
||||
class LogosObject;
|
||||
class ModuleProxy;
|
||||
class ModuleHandshakeProxy;
|
||||
class LogosProviderObject;
|
||||
class QtProviderObject;
|
||||
|
||||
/**
|
||||
* @brief LogosAPIProvider handles registering objects for access by consumers
|
||||
*
|
||||
* Supports two registration paths:
|
||||
* 1. registerObject(name, QObject*) — wraps in QtProviderObject, then ModuleProxy
|
||||
* 2. registerObject(name, LogosProviderObject*) — wraps directly in ModuleProxy
|
||||
* Both paths converge at ModuleProxy -> transport.
|
||||
*/
|
||||
class LogosAPIProvider : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
/**
|
||||
* @param module_name The module this provider belongs to.
|
||||
* @param transports Optional per-instance transport override. When empty,
|
||||
* the provider uses the process-global default. This
|
||||
* is what lets a daemon expose `core_service` on
|
||||
* TCP/TLS while keeping module-to-module traffic on
|
||||
* the local-socket default.
|
||||
*/
|
||||
explicit LogosAPIProvider(const QString& module_name,
|
||||
LogosTransportSet transports = {},
|
||||
QObject *parent = nullptr);
|
||||
// Back-compat overload
|
||||
LogosAPIProvider(const QString& module_name, QObject *parent)
|
||||
: LogosAPIProvider(module_name, LogosTransportSet{}, parent) {}
|
||||
~LogosAPIProvider();
|
||||
|
||||
/**
|
||||
* @brief Register a legacy QObject-based plugin.
|
||||
* Wraps in QtProviderObject, then ModuleProxy.
|
||||
*/
|
||||
bool registerObject(const QString& name, QObject* object);
|
||||
|
||||
/**
|
||||
* @brief Register a legacy QObject-based plugin — const char* overload (resolves ambiguity)
|
||||
*/
|
||||
bool registerObject(const char* name, QObject* object)
|
||||
{ return registerObject(QString(name), object); }
|
||||
|
||||
/**
|
||||
* @brief Register a legacy QObject-based plugin (std::string overload).
|
||||
*/
|
||||
bool registerObject(const std::string& name, QObject* object);
|
||||
|
||||
/**
|
||||
* @brief Register a new-API LogosProviderObject plugin.
|
||||
* Wraps directly in ModuleProxy.
|
||||
*/
|
||||
bool registerObject(const QString& name, LogosProviderObject* provider);
|
||||
|
||||
QString registryUrl() const;
|
||||
bool saveToken(const QString& from_module_name, const QString& token);
|
||||
|
||||
// Install an extra token authorizer, forwarded to the ModuleProxy. Consulted
|
||||
// in addition to the built-in issued-token scan, with the call's transport
|
||||
// ("local" | "tcp" | "tcp_ssl") so local_only tokens can be enforced. The
|
||||
// daemon backs this with TokenStore::lookupByToken so operator-issued named
|
||||
// tokens authorize. Safe to call before or after registerObject(); a
|
||||
// validator set before registration is applied when the proxy is created.
|
||||
using TokenValidator = std::function<bool(const QString& token,
|
||||
const QString& transportProtocol)>;
|
||||
void setTokenValidator(TokenValidator validator);
|
||||
|
||||
public slots:
|
||||
void onEventResponse(LogosObject* object, const QString& eventName, const QVariantList& data);
|
||||
|
||||
private:
|
||||
bool publishProvider(const QString& name, LogosProviderObject* provider);
|
||||
// Publishes the token-only handshake surface (logos::handshakeObjectName)
|
||||
// before the module's initializer runs, so capability_module can deliver a
|
||||
// token to a module that is still starting up. Best-effort: a transport that
|
||||
// declines it simply leaves capability_module falling back to the business
|
||||
// object, which is the pre-existing behaviour.
|
||||
void publishHandshake(const QString& name, LogosProviderObject* provider);
|
||||
// Installs the host-issued token as this module's "core"/"capability_module"
|
||||
// trust anchor before the handshake surface is published. Without it the
|
||||
// surface is reachable but refuses every push for the whole pre-init window
|
||||
// it exists to cover, because the initializer that normally seeds those
|
||||
// entries has not run yet. Never overwrites an existing entry, and is a no-op
|
||||
// on a host that does not publish an `authToken` property.
|
||||
void seedHandshakeTrustAnchor();
|
||||
|
||||
// One host per configured transport. Single-entry vector for back-compat.
|
||||
std::vector<std::unique_ptr<LogosTransportHost>> m_transports;
|
||||
QString m_registryUrl;
|
||||
QMap<QString, QString> m_tokens;
|
||||
ModuleProxy* m_moduleProxy;
|
||||
ModuleHandshakeProxy* m_handshakeProxy = nullptr;
|
||||
QString m_registeredHandshakeName;
|
||||
QtProviderObject* m_qtProviderObject;
|
||||
QString m_registeredObjectName;
|
||||
// Appended (never inserted mid-list): keeps every pre-existing member's
|
||||
// offset identical between builds, so a LogosAPIProvider is layout-safe even
|
||||
// if a future co-located consumer ever accessed one across a version
|
||||
// boundary. Held until the proxy exists (registerObject may come after the
|
||||
// setter).
|
||||
TokenValidator m_pendingValidator;
|
||||
};
|
||||
|
||||
#endif // LOGOS_API_PROVIDER_H
|
||||
@@ -0,0 +1,48 @@
|
||||
#include "logos_provider_object.h"
|
||||
#include "logos_api.h"
|
||||
#include "token_manager.h"
|
||||
#include <QDebug>
|
||||
|
||||
// The LogosProviderObject universal-interface defaults and Std bridges moved
|
||||
// to logos-protocol (logos_provider_interface.cpp) together with the abstract
|
||||
// interface. What remains here is LogosProviderBase — the developer-facing
|
||||
// base class — because it talks to LogosAPI, which layers above the protocol.
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// LogosProviderBase
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void LogosProviderBase::init(void* apiInstance)
|
||||
{
|
||||
m_logosAPI = static_cast<LogosAPI*>(apiInstance);
|
||||
qDebug() << "[LogosProviderObject] LogosProviderBase::init called";
|
||||
onInit(m_logosAPI);
|
||||
}
|
||||
|
||||
bool LogosProviderBase::informModuleToken(const QString& moduleName, const QString& token)
|
||||
{
|
||||
if (!m_logosAPI) {
|
||||
qWarning() << "[LogosProviderObject] informModuleToken: LogosAPI not available";
|
||||
return false;
|
||||
}
|
||||
|
||||
TokenManager* tokenManager = m_logosAPI->getTokenManager();
|
||||
if (!tokenManager) {
|
||||
qWarning() << "[LogosProviderObject] informModuleToken: TokenManager not available";
|
||||
return false;
|
||||
}
|
||||
|
||||
qDebug() << "[LogosProviderObject] Saving token for module:" << moduleName;
|
||||
tokenManager->saveToken(moduleName, token);
|
||||
return true;
|
||||
}
|
||||
|
||||
void LogosProviderBase::emitEvent(const QString& eventName, const QVariantList& data)
|
||||
{
|
||||
if (m_eventCallback) {
|
||||
qDebug() << "[LogosProviderObject] emitEvent:" << eventName;
|
||||
m_eventCallback(eventName, data);
|
||||
} else {
|
||||
qWarning() << "[LogosProviderObject] emitEvent: no listener set for" << eventName;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
#ifndef LOGOS_PROVIDER_OBJECT_H
|
||||
#define LOGOS_PROVIDER_OBJECT_H
|
||||
|
||||
#include <QString>
|
||||
#include <QVariant>
|
||||
#include <QVariantList>
|
||||
#include <QJsonArray>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "logos_json_convert.h"
|
||||
|
||||
// The abstract LogosProviderObject interface (the provider-side counterpart
|
||||
// of LogosObject, wrapped by ModuleProxy) moved to logos-protocol with the
|
||||
// transport layer — see logos_provider_interface.h. This header keeps its
|
||||
// historical name and continues to carry the developer-facing pieces:
|
||||
// LogosProviderBase (which hands a LogosAPI* to module code, hence it lives
|
||||
// here above the protocol layer), LogosProviderPlugin, and the
|
||||
// LOGOS_PROVIDER / LOGOS_METHOD macros.
|
||||
#include "logos_provider_interface.h"
|
||||
|
||||
class LogosAPI;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// LogosProviderBase — convenience base class for new-API modules
|
||||
//
|
||||
// Handles framework plumbing so the developer only writes business logic.
|
||||
// callMethod() and getMethods() are provided by generated code produced
|
||||
// by logos-cpp-generator --provider-header (analogous to Qt MOC).
|
||||
// ---------------------------------------------------------------------------
|
||||
class LogosProviderBase : public LogosProviderObject {
|
||||
public:
|
||||
// These two are implemented by generated code (logos_provider_dispatch.cpp):
|
||||
// QVariant callMethod(const QString& methodName, const QVariantList& args) override;
|
||||
// QJsonArray getMethods() override;
|
||||
|
||||
void setEventListener(EventCallback callback) override { m_eventCallback = callback; }
|
||||
bool informModuleToken(const QString& moduleName, const QString& token) override;
|
||||
void init(void* apiInstance) override;
|
||||
|
||||
protected:
|
||||
void emitEvent(const QString& eventName, const QVariantList& data);
|
||||
virtual void onInit(LogosAPI* api) {}
|
||||
LogosAPI* logosAPI() const { return m_logosAPI; }
|
||||
|
||||
private:
|
||||
EventCallback m_eventCallback;
|
||||
LogosAPI* m_logosAPI = nullptr;
|
||||
};
|
||||
|
||||
// LogosProviderPlugin (the plugin-detection interface) now lives in
|
||||
// logos-protocol's logos_provider_interface.h, included above — it stays
|
||||
// visible to existing includers of this header.
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Macros — the developer-facing API
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// LOGOS_PROVIDER: declares providerName/providerVersion and a private typedef.
|
||||
// Place at the top of the class body (like Q_OBJECT).
|
||||
#define LOGOS_PROVIDER(ClassName, Name, Version) \
|
||||
public: \
|
||||
QString providerName() const override { return Name; } \
|
||||
QString providerVersion() const override { return Version; } \
|
||||
QVariant callMethod(const QString& methodName, const QVariantList& args) override; \
|
||||
QJsonArray getMethods() override; \
|
||||
private: \
|
||||
using _LogosProviderThisType = ClassName;
|
||||
|
||||
// LOGOS_METHOD: marks a method as callable by the framework.
|
||||
// Expands to nothing — scanned by logos-cpp-generator to produce
|
||||
// callMethod() dispatch and getMethods() metadata (like Q_INVOKABLE + MOC).
|
||||
#define LOGOS_METHOD
|
||||
|
||||
#endif // LOGOS_PROVIDER_OBJECT_H
|
||||
@@ -0,0 +1,75 @@
|
||||
#include "logos_qt_arg_decode.h"
|
||||
|
||||
namespace logos {
|
||||
|
||||
namespace {
|
||||
|
||||
// One case per QMetaType the Qt type mapping can produce for a parameter.
|
||||
// Each hands off to the SAME QtArgCodec<T> the generated dispatch uses, so the
|
||||
// two provider sites cannot answer differently for the same argument.
|
||||
template <class T>
|
||||
QtArgVerdict decodeAs(const QVariant& in, const std::string& path, QVariant& out,
|
||||
std::string& error)
|
||||
{
|
||||
try {
|
||||
out = QVariant::fromValue(detail::QtArgCodec<T>::from(in, path));
|
||||
return QtArgVerdict::Ok;
|
||||
} catch (const CodecError& e) {
|
||||
error = e.what();
|
||||
return QtArgVerdict::Rejected;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
QtArgVerdict qtArgDecode(const QVariant& in, QMetaType paramType,
|
||||
const std::string& path, QVariant& out,
|
||||
std::string& error)
|
||||
{
|
||||
switch (paramType.id()) {
|
||||
case QMetaType::Bool: return decodeAs<bool>(in, path, out, error);
|
||||
case QMetaType::Int: return decodeAs<int>(in, path, out, error);
|
||||
case QMetaType::UInt: return decodeAs<unsigned int>(in, path, out, error);
|
||||
case QMetaType::Short: return decodeAs<short>(in, path, out, error);
|
||||
case QMetaType::UShort: return decodeAs<unsigned short>(in, path, out, error);
|
||||
case QMetaType::Long: return decodeAs<long>(in, path, out, error);
|
||||
case QMetaType::ULong: return decodeAs<unsigned long>(in, path, out, error);
|
||||
case QMetaType::LongLong: return decodeAs<qlonglong>(in, path, out, error);
|
||||
case QMetaType::ULongLong: return decodeAs<qulonglong>(in, path, out, error);
|
||||
case QMetaType::Double: return decodeAs<double>(in, path, out, error);
|
||||
case QMetaType::Float: return decodeAs<float>(in, path, out, error);
|
||||
|
||||
case QMetaType::QString: return decodeAs<QString>(in, path, out, error);
|
||||
case QMetaType::QByteArray: return decodeAs<QByteArray>(in, path, out, error);
|
||||
case QMetaType::QStringList: return decodeAs<QStringList>(in, path, out, error);
|
||||
case QMetaType::QVariantList: return decodeAs<QVariantList>(in, path, out, error);
|
||||
case QMetaType::QVariantMap: return decodeAs<QVariantMap>(in, path, out, error);
|
||||
case QMetaType::QJsonArray: return decodeAs<QJsonArray>(in, path, out, error);
|
||||
case QMetaType::QJsonObject: return decodeAs<QJsonObject>(in, path, out, error);
|
||||
|
||||
// `any`, QUrl, QChar, enums, pointers, LogosResult, anything a module
|
||||
// author invented: no LIDL counterpart, so no rule to check against. The
|
||||
// caller keeps whatever it did before — see the header comment.
|
||||
default:
|
||||
return QtArgVerdict::Unchecked;
|
||||
}
|
||||
}
|
||||
|
||||
nlohmann::json dispatchFailedJson(const std::string& origin,
|
||||
const std::string& message)
|
||||
{
|
||||
return nlohmann::json{{"code", "dispatch_failed"},
|
||||
{"message", message},
|
||||
{"origin", origin}};
|
||||
}
|
||||
|
||||
QVariant dispatchFailedVariant(const QString& origin, const QString& message)
|
||||
{
|
||||
QVariantMap m;
|
||||
m.insert(QStringLiteral("code"), QStringLiteral("dispatch_failed"));
|
||||
m.insert(QStringLiteral("message"), message);
|
||||
m.insert(QStringLiteral("origin"), origin);
|
||||
return m;
|
||||
}
|
||||
|
||||
} // namespace logos
|
||||
@@ -0,0 +1,233 @@
|
||||
#ifndef LOGOS_QT_ARG_DECODE_H
|
||||
#define LOGOS_QT_ARG_DECODE_H
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// logos_qt_arg_decode.h — decoding an incoming RPC argument into the Qt type a
|
||||
// provider method DECLARES. The Qt face of logos::fromJson<T>.
|
||||
//
|
||||
// WHY THIS EXISTS. A Qt-typed provider used to hand every incoming argument to
|
||||
// a Qt conversion — `args.at(0).toULongLong()` in the generated dispatch,
|
||||
// `QVariant::convert(paramType)` in the QMetaObject one. Both COERCE rather
|
||||
// than check, so a hostile value never reached the author's method body wearing
|
||||
// the shape it arrived in:
|
||||
//
|
||||
// echoUint(-1) ran the body with 18446744073709551615
|
||||
// echoInt(3.7) ran it with 4
|
||||
// echoBool(1) ran it with true
|
||||
// echoMap(5) ran it with {}
|
||||
// stringLength(42) ran it with "42"
|
||||
//
|
||||
// Every non-Qt surface — the generated cdylib dispatch, the Rust provider, the
|
||||
// plain wire — decodes the same argument through logos::fromJson<T> and answers
|
||||
// {"code":"dispatch_failed", ...} instead. That asymmetry meant argument
|
||||
// validation was not something a Qt module could rely on: the author's own
|
||||
// precondition checks never saw the value the caller actually sent.
|
||||
//
|
||||
// THE RULE IS THE CODEC'S. Nothing here re-derives what a legal value is. The
|
||||
// argument is encoded to canonical JSON with the same qvariantToNlohmann every
|
||||
// other Qt hop uses and handed to logos::fromJson<T>. The codec already owns
|
||||
// the nuance a hand-written check gets wrong — a WHOLE-VALUED float is a legal
|
||||
// integer (JSON does not distinguish 3 from 3.0, and an argument-typing CLI
|
||||
// produces 3.0 for "3.0") while 3.7 is refused; `bstr` keeps its documented
|
||||
// lenient form. A stricter check written here would diverge the moment the
|
||||
// codec learned something new, which is the failure this whole layer exists to
|
||||
// prevent.
|
||||
//
|
||||
// WHAT IS NOT CHECKED, AND WHY
|
||||
// * `any` (QVariant) declares nothing, so there is nothing to check it
|
||||
// against. The value is handed on EXACTLY as it arrived — not round-tripped
|
||||
// through JSON, which would reinterpret a one-key `_bytes` map as bytes.
|
||||
// * LogosList / LogosMap (QVariantList / QVariantMap) are shape-checked
|
||||
// (array-ness / object-ness) and then handed on unchanged. Element types
|
||||
// are erased by the Qt spelling itself: `[uint]` and `[any]` are both
|
||||
// QVariantList, so array-ness is the whole of the declared type at this
|
||||
// layer. Rebuilding the payload from JSON instead of passing it through
|
||||
// would retype nested elements (an int element would arrive as qlonglong)
|
||||
// for no validation gain.
|
||||
// * Types with no LIDL counterpart (QUrl, enums, pointers, LogosResult as a
|
||||
// parameter) are left to the caller's existing conversion. The codec has no
|
||||
// rule for them and inventing one would reject arguments that work today.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QMetaType>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QVariant>
|
||||
#include <QVariantList>
|
||||
#include <QVariantMap>
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <type_traits>
|
||||
#include <vector>
|
||||
|
||||
#include "logos_codec.h"
|
||||
#include "logos_json_convert.h"
|
||||
#include "logos_types.h" // LogosResult
|
||||
|
||||
namespace logos {
|
||||
|
||||
namespace detail {
|
||||
|
||||
// QtArgCodec<T>::from(v, path) -> T, throwing logos::CodecError on a mismatch.
|
||||
// An unsupported T leaves the trait incomplete, so a generator emitting a type
|
||||
// nobody taught this file about fails to COMPILE naming the type — never
|
||||
// silently falls back to a coercion.
|
||||
template <class T, class Enable = void> struct QtArgCodec;
|
||||
|
||||
// bool / int / uint / qlonglong / qulonglong / double / float — straight
|
||||
// through to the canonical codec, which owns signedness, range and the
|
||||
// whole-valued-float rule. `int` is checked against int32, not widened: a
|
||||
// method declaring `int` and handed 4294967296 used to run with 0.
|
||||
template <class T>
|
||||
struct QtArgCodec<T, std::enable_if_t<std::is_arithmetic_v<T>>> {
|
||||
static T from(const QVariant& v, const std::string& path)
|
||||
{
|
||||
return logos::fromJson<T>(qvariantToNlohmann(v), path);
|
||||
}
|
||||
};
|
||||
|
||||
template <> struct QtArgCodec<QString, void> {
|
||||
static QString from(const QVariant& v, const std::string& path)
|
||||
{
|
||||
return QString::fromStdString(
|
||||
logos::fromJson<std::string>(qvariantToNlohmann(v), path));
|
||||
}
|
||||
};
|
||||
|
||||
// bstr — Codec<std::vector<uint8_t>> is the lenient decoder by design (a Qt
|
||||
// consumer passing a QString and a CLI typing its arguments both produce a
|
||||
// plain string for a byte parameter), so that leniency is inherited here rather
|
||||
// than re-decided.
|
||||
template <> struct QtArgCodec<QByteArray, void> {
|
||||
static QByteArray from(const QVariant& v, const std::string& path)
|
||||
{
|
||||
const std::vector<uint8_t> bytes =
|
||||
logos::fromJson<std::vector<uint8_t>>(qvariantToNlohmann(v), path);
|
||||
return QByteArray(reinterpret_cast<const char*>(bytes.data()),
|
||||
static_cast<qsizetype>(bytes.size()));
|
||||
}
|
||||
};
|
||||
|
||||
// [tstr] — the one container whose element type the Qt spelling preserves, so
|
||||
// it is the one container whose elements are checked.
|
||||
template <> struct QtArgCodec<QStringList, void> {
|
||||
static QStringList from(const QVariant& v, const std::string& path)
|
||||
{
|
||||
const std::vector<std::string> items =
|
||||
logos::fromJson<std::vector<std::string>>(qvariantToNlohmann(v), path);
|
||||
QStringList out;
|
||||
out.reserve(static_cast<qsizetype>(items.size()));
|
||||
for (const std::string& s : items) out.append(QString::fromStdString(s));
|
||||
return out;
|
||||
}
|
||||
};
|
||||
|
||||
// LogosList — shape only (see the header comment). Decoding into
|
||||
// vector<nlohmann::json> is what produces the array-ness check AND the codec's
|
||||
// own diagnostic ("expected array at arg0, got string"); the value handed on is
|
||||
// the caller's, untouched.
|
||||
template <> struct QtArgCodec<QVariantList, void> {
|
||||
static QVariantList from(const QVariant& v, const std::string& path)
|
||||
{
|
||||
logos::fromJson<std::vector<nlohmann::json>>(qvariantToNlohmann(v), path);
|
||||
return v.toList();
|
||||
}
|
||||
};
|
||||
|
||||
template <> struct QtArgCodec<QVariantMap, void> {
|
||||
static QVariantMap from(const QVariant& v, const std::string& path)
|
||||
{
|
||||
logos::fromJson<std::map<std::string, nlohmann::json>>(
|
||||
qvariantToNlohmann(v), path);
|
||||
return v.toMap();
|
||||
}
|
||||
};
|
||||
|
||||
template <> struct QtArgCodec<QJsonArray, void> {
|
||||
static QJsonArray from(const QVariant& v, const std::string& path)
|
||||
{
|
||||
logos::fromJson<std::vector<nlohmann::json>>(qvariantToNlohmann(v), path);
|
||||
return qvariant_cast<QJsonArray>(v);
|
||||
}
|
||||
};
|
||||
|
||||
template <> struct QtArgCodec<QJsonObject, void> {
|
||||
static QJsonObject from(const QVariant& v, const std::string& path)
|
||||
{
|
||||
logos::fromJson<std::map<std::string, nlohmann::json>>(
|
||||
qvariantToNlohmann(v), path);
|
||||
return qvariant_cast<QJsonObject>(v);
|
||||
}
|
||||
};
|
||||
|
||||
// `any` — verbatim, deliberately. See the header comment.
|
||||
template <> struct QtArgCodec<QVariant, void> {
|
||||
static QVariant from(const QVariant& v, const std::string&) { return v; }
|
||||
};
|
||||
|
||||
// No LIDL counterpart as a PARAMETER; kept at today's behaviour so a provider
|
||||
// declaring one still compiles and still behaves the way it did.
|
||||
template <> struct QtArgCodec<LogosResult, void> {
|
||||
static LogosResult from(const QVariant& v, const std::string&)
|
||||
{
|
||||
return v.value<LogosResult>();
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace detail
|
||||
|
||||
// Compile-time entry point — what a generator emits, one call per parameter,
|
||||
// with the type the author WROTE. Throws logos::CodecError naming the path.
|
||||
template <class T>
|
||||
T qtArgFromVariant(const QVariant& v, const std::string& path)
|
||||
{
|
||||
return detail::QtArgCodec<std::decay_t<T>>::from(v, path);
|
||||
}
|
||||
|
||||
// `[Elem]` for a caller that still KNOWS the element type.
|
||||
//
|
||||
// A C++ signature spells every typed numeric array QVariantList, so neither the
|
||||
// QMetaObject dispatch nor a generator scanning a C++ header can check the
|
||||
// elements — array-ness is all they have. A LIDL-driven generator does have the
|
||||
// element type, and this is where it spends it. The value handed on is still
|
||||
// the caller's list, for the reason given in the header comment.
|
||||
template <class Elem>
|
||||
QVariantList qtArgListOf(const QVariant& v, const std::string& path)
|
||||
{
|
||||
logos::fromJson<std::vector<Elem>>(qvariantToNlohmann(v), path);
|
||||
return v.toList();
|
||||
}
|
||||
|
||||
// Runtime entry point — for the QMetaObject dispatch, which knows a parameter
|
||||
// only as a QMetaType.
|
||||
enum class QtArgVerdict {
|
||||
Ok, // `out` holds a value of exactly `paramType`
|
||||
Rejected, // `error` holds the codec's diagnostic
|
||||
Unchecked, // no LIDL counterpart; the caller keeps its own conversion
|
||||
};
|
||||
|
||||
QtArgVerdict qtArgDecode(const QVariant& in, QMetaType paramType,
|
||||
const std::string& path, QVariant& out,
|
||||
std::string& error);
|
||||
|
||||
// The canonical rejection value: the same {code, message, origin} object the
|
||||
// generated cdylib dispatch and the Rust provider return, so a rejected call
|
||||
// reads identically whichever provider produced it.
|
||||
nlohmann::json dispatchFailedJson(const std::string& origin,
|
||||
const std::string& message);
|
||||
|
||||
// The same object as a QVariantMap, for a provider whose dispatch returns
|
||||
// QVariant. Round-trips faithfully on every transport (qt_local pass-through,
|
||||
// QtRO serialization, and the plain wire's map<->JSON).
|
||||
QVariant dispatchFailedVariant(const QString& origin, const QString& message);
|
||||
|
||||
} // namespace logos
|
||||
|
||||
#endif // LOGOS_QT_ARG_DECODE_H
|
||||
@@ -0,0 +1,544 @@
|
||||
#include "qt_provider_object.h"
|
||||
#include "../core/interface.h"
|
||||
#include "logos_api.h"
|
||||
#include "token_manager.h"
|
||||
#include "logos_types.h"
|
||||
#include "logos_qt_arg_decode.h"
|
||||
#include <QDebug>
|
||||
#include <QMetaObject>
|
||||
#include <QMetaMethod>
|
||||
#include <QMetaType>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QStringList>
|
||||
#include <QUrl>
|
||||
|
||||
// ── QMetaObject dispatch helpers (moved from module_proxy.cpp) ──────────────
|
||||
|
||||
#define INVOKE_METHOD_WITH_RETURN(returnType, castType) \
|
||||
do { \
|
||||
castType* result = static_cast<castType*>(returnValue); \
|
||||
switch (args.size()) { \
|
||||
case 0: \
|
||||
return QMetaObject::invokeMethod(module, methodNameCStr, Qt::DirectConnection, Q_RETURN_ARG(returnType, *result)); \
|
||||
case 1: \
|
||||
return QMetaObject::invokeMethod(module, methodNameCStr, Qt::DirectConnection, Q_RETURN_ARG(returnType, *result), scopedArgs[0].arg); \
|
||||
case 2: \
|
||||
return QMetaObject::invokeMethod(module, methodNameCStr, Qt::DirectConnection, Q_RETURN_ARG(returnType, *result), scopedArgs[0].arg, scopedArgs[1].arg); \
|
||||
case 3: \
|
||||
return QMetaObject::invokeMethod(module, methodNameCStr, Qt::DirectConnection, Q_RETURN_ARG(returnType, *result), scopedArgs[0].arg, scopedArgs[1].arg, scopedArgs[2].arg); \
|
||||
case 4: \
|
||||
return QMetaObject::invokeMethod(module, methodNameCStr, Qt::DirectConnection, Q_RETURN_ARG(returnType, *result), scopedArgs[0].arg, scopedArgs[1].arg, scopedArgs[2].arg, scopedArgs[3].arg); \
|
||||
case 5: \
|
||||
return QMetaObject::invokeMethod(module, methodNameCStr, Qt::DirectConnection, Q_RETURN_ARG(returnType, *result), scopedArgs[0].arg, scopedArgs[1].arg, scopedArgs[2].arg, scopedArgs[3].arg, scopedArgs[4].arg); \
|
||||
default: \
|
||||
qWarning() << "QtProviderObject: Currently supports 0-5 arguments. Got:" << args.size(); \
|
||||
return false; \
|
||||
} \
|
||||
} while(0)
|
||||
|
||||
namespace {
|
||||
class ScopedQArg {
|
||||
public:
|
||||
ScopedQArg(QMetaMethodArgument a, std::function<void(const void*)> d)
|
||||
: arg(a), deleter(std::move(d)) {}
|
||||
|
||||
~ScopedQArg() {
|
||||
if (deleter) {
|
||||
deleter(arg.data);
|
||||
}
|
||||
}
|
||||
|
||||
ScopedQArg(ScopedQArg&& other)
|
||||
: arg(std::move(other.arg)), deleter(std::move(other.deleter)) {
|
||||
other.deleter = nullptr;
|
||||
}
|
||||
ScopedQArg& operator=(ScopedQArg&&) = delete;
|
||||
ScopedQArg(const ScopedQArg&) = delete;
|
||||
ScopedQArg& operator=(const ScopedQArg&) = delete;
|
||||
|
||||
QMetaMethodArgument arg;
|
||||
|
||||
private:
|
||||
std::function<void(const void*)> deleter;
|
||||
};
|
||||
|
||||
// Every `new T(...)` below is a copy-construction from a value that is
|
||||
// ALREADY a T, and every one of them uses parentheses deliberately.
|
||||
//
|
||||
// With braces, `new QVariantList{arg.toList()}` is list-initialization of a
|
||||
// QList<QVariant> from a single QVariantList — and because QVariantList is
|
||||
// implicitly convertible to QVariant, QList's initializer_list<QVariant>
|
||||
// constructor is also viable. Which one wins is exactly the question CWG
|
||||
// 2137 covers, and the compilers answer it differently: Clang picks the
|
||||
// copy constructor, GCC picks the initializer-list one and WRAPS the value,
|
||||
// so a two-element [1,2] reached the method body as [[1,2]], size 1. That
|
||||
// is why this only ever failed on Linux CI.
|
||||
//
|
||||
// Parentheses cannot select an initializer-list constructor, so they mean
|
||||
// the same thing on both compilers. QVariantList was the only type here the
|
||||
// divergence could reach — QStringList and QVariantMap are safe only
|
||||
// because QString and std::pair are not constructible from their own
|
||||
// container — which is precisely why the rule is applied uniformly rather
|
||||
// than to the one case that bit.
|
||||
auto toScopedQArgs(const QVariantList& args)
|
||||
{
|
||||
auto scopedArgs = std::vector<ScopedQArg>{};
|
||||
for (const auto& arg : args) {
|
||||
switch (arg.typeId()) {
|
||||
case QMetaType::Int: {
|
||||
auto value = new int(arg.toInt());
|
||||
scopedArgs.emplace_back(
|
||||
Q_ARG(int, *value),
|
||||
[](const void* data) { delete static_cast<const int*>(data); }
|
||||
);
|
||||
break;
|
||||
}
|
||||
case QMetaType::LongLong: {
|
||||
auto value = new qlonglong(arg.toLongLong());
|
||||
scopedArgs.emplace_back(
|
||||
Q_ARG(qlonglong, *value),
|
||||
[](const void* data) { delete static_cast<const qlonglong*>(data); }
|
||||
);
|
||||
break;
|
||||
}
|
||||
case QMetaType::ULongLong: {
|
||||
auto value = new qulonglong(arg.toULongLong());
|
||||
scopedArgs.emplace_back(
|
||||
Q_ARG(qulonglong, *value),
|
||||
[](const void* data) { delete static_cast<const qulonglong*>(data); }
|
||||
);
|
||||
break;
|
||||
}
|
||||
// A typed-numeric array ([int]/[uint]/[float64]/[bool]) maps to
|
||||
// QVariantList (only [tstr] maps to QStringList). Without this
|
||||
// case such an arg fell to the QString default below, stringified
|
||||
// to "" and failed the typed invokeMethod → an EMPTY list arrived.
|
||||
case QMetaType::QVariantList: {
|
||||
auto value = new QVariantList(arg.toList());
|
||||
scopedArgs.emplace_back(
|
||||
Q_ARG(QVariantList, *value),
|
||||
[](const void* data) { delete static_cast<const QVariantList*>(data); }
|
||||
);
|
||||
break;
|
||||
}
|
||||
case QMetaType::QVariantMap: {
|
||||
auto value = new QVariantMap(arg.toMap());
|
||||
scopedArgs.emplace_back(
|
||||
Q_ARG(QVariantMap, *value),
|
||||
[](const void* data) { delete static_cast<const QVariantMap*>(data); }
|
||||
);
|
||||
break;
|
||||
}
|
||||
case QMetaType::QStringList: {
|
||||
auto value = new QStringList(arg.toStringList());
|
||||
scopedArgs.emplace_back(
|
||||
Q_ARG(QStringList, *value),
|
||||
[](const void* data) { delete static_cast<const QStringList*>(data); }
|
||||
);
|
||||
break;
|
||||
}
|
||||
case QMetaType::QByteArray: {
|
||||
auto value = new QByteArray(arg.toByteArray());
|
||||
scopedArgs.emplace_back(
|
||||
Q_ARG(QByteArray, *value),
|
||||
[](const void* data) { delete static_cast<const QByteArray*>(data); }
|
||||
);
|
||||
break;
|
||||
}
|
||||
case QMetaType::QUrl: {
|
||||
auto value = new QUrl(arg.toUrl());
|
||||
scopedArgs.emplace_back(
|
||||
Q_ARG(QUrl, *value),
|
||||
[](const void* data) { delete static_cast<const QUrl*>(data); }
|
||||
);
|
||||
break;
|
||||
}
|
||||
case QMetaType::Bool: {
|
||||
auto value = new bool(arg.toBool());
|
||||
scopedArgs.emplace_back(
|
||||
Q_ARG(bool, *value),
|
||||
[](const void* data) { delete static_cast<const bool*>(data); }
|
||||
);
|
||||
break;
|
||||
}
|
||||
case QMetaType::Double: {
|
||||
auto value = new double(arg.toDouble());
|
||||
scopedArgs.emplace_back(
|
||||
Q_ARG(double, *value),
|
||||
[](const void* data) { delete static_cast<const double*>(data); }
|
||||
);
|
||||
break;
|
||||
}
|
||||
case QMetaType::Float: {
|
||||
auto value = new float(arg.toFloat());
|
||||
scopedArgs.emplace_back(
|
||||
Q_ARG(float, *value),
|
||||
[](const void* data) { delete static_cast<const float*>(data); }
|
||||
);
|
||||
break;
|
||||
}
|
||||
case QMetaType::QString:
|
||||
default: {
|
||||
auto value = new QString(arg.toString());
|
||||
scopedArgs.emplace_back(
|
||||
Q_ARG(QString, *value),
|
||||
[](const void* data) { delete static_cast<const QString*>(data); }
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return scopedArgs;
|
||||
}
|
||||
|
||||
bool invokeMethodByArgCount(QObject *module, const QString& methodName, const QVariantList& args, void* returnValue, const char* returnTypeName)
|
||||
{
|
||||
QByteArray methodNameBytes = methodName.toUtf8();
|
||||
const char* methodNameCStr = methodNameBytes.constData();
|
||||
|
||||
auto scopedArgs = toScopedQArgs(args);
|
||||
|
||||
if (returnValue == nullptr) {
|
||||
switch (args.size()) {
|
||||
case 0: return QMetaObject::invokeMethod(module, methodNameCStr, Qt::DirectConnection);
|
||||
case 1: return QMetaObject::invokeMethod(module, methodNameCStr, Qt::DirectConnection, scopedArgs[0].arg);
|
||||
case 2: return QMetaObject::invokeMethod(module, methodNameCStr, Qt::DirectConnection, scopedArgs[0].arg, scopedArgs[1].arg);
|
||||
case 3: return QMetaObject::invokeMethod(module, methodNameCStr, Qt::DirectConnection, scopedArgs[0].arg, scopedArgs[1].arg, scopedArgs[2].arg);
|
||||
case 4: return QMetaObject::invokeMethod(module, methodNameCStr, Qt::DirectConnection, scopedArgs[0].arg, scopedArgs[1].arg, scopedArgs[2].arg, scopedArgs[3].arg);
|
||||
case 5: return QMetaObject::invokeMethod(module, methodNameCStr, Qt::DirectConnection, scopedArgs[0].arg, scopedArgs[1].arg, scopedArgs[2].arg, scopedArgs[3].arg, scopedArgs[4].arg);
|
||||
default:
|
||||
qWarning() << "QtProviderObject: Currently supports 0-5 arguments. Got:" << args.size();
|
||||
return false;
|
||||
}
|
||||
} else if (strcmp(returnTypeName, "bool") == 0) {
|
||||
INVOKE_METHOD_WITH_RETURN(bool, bool);
|
||||
} else if (strcmp(returnTypeName, "int") == 0) {
|
||||
INVOKE_METHOD_WITH_RETURN(int, int);
|
||||
} else if (strcmp(returnTypeName, "double") == 0) {
|
||||
INVOKE_METHOD_WITH_RETURN(double, double);
|
||||
} else if (strcmp(returnTypeName, "float") == 0) {
|
||||
INVOKE_METHOD_WITH_RETURN(float, float);
|
||||
} else if (strcmp(returnTypeName, "QString") == 0) {
|
||||
INVOKE_METHOD_WITH_RETURN(QString, QString);
|
||||
} else if (strcmp(returnTypeName, "LogosResult") == 0) {
|
||||
INVOKE_METHOD_WITH_RETURN(LogosResult, LogosResult);
|
||||
} else if (strcmp(returnTypeName, "QVariant") == 0) {
|
||||
INVOKE_METHOD_WITH_RETURN(QVariant, QVariant);
|
||||
} else if (strcmp(returnTypeName, "QJsonArray") == 0) {
|
||||
INVOKE_METHOD_WITH_RETURN(QJsonArray, QJsonArray);
|
||||
} else if (strcmp(returnTypeName, "QVariantList") == 0) {
|
||||
INVOKE_METHOD_WITH_RETURN(QVariantList, QVariantList);
|
||||
} else if (strcmp(returnTypeName, "QVariantMap") == 0) {
|
||||
INVOKE_METHOD_WITH_RETURN(QVariantMap, QVariantMap);
|
||||
} else if (strcmp(returnTypeName, "QStringList") == 0) {
|
||||
INVOKE_METHOD_WITH_RETURN(QStringList, QStringList);
|
||||
} else {
|
||||
qWarning() << "QtProviderObject: Unsupported return type:" << returnTypeName;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── QtProviderObject implementation ─────────────────────────────────────────
|
||||
|
||||
QtProviderObject::QtProviderObject(QObject* module, QObject* parent)
|
||||
: QObject(parent)
|
||||
, m_module(module)
|
||||
{
|
||||
if (m_module) {
|
||||
connect(m_module, SIGNAL(eventResponse(QString, QVariantList)),
|
||||
this, SLOT(onWrappedEventResponse(QString, QVariantList)));
|
||||
qDebug() << "[LogosProviderObject] QtProviderObject: connected to QObject eventResponse signal";
|
||||
}
|
||||
}
|
||||
|
||||
QtProviderObject::~QtProviderObject()
|
||||
{
|
||||
qDebug() << "[LogosProviderObject] QtProviderObject: destroyed";
|
||||
}
|
||||
|
||||
void QtProviderObject::onWrappedEventResponse(const QString& eventName, const QVariantList& data)
|
||||
{
|
||||
if (m_eventCallback) {
|
||||
m_eventCallback(eventName, data);
|
||||
}
|
||||
}
|
||||
|
||||
void QtProviderObject::init(void* apiInstance)
|
||||
{
|
||||
if (!m_module) return;
|
||||
|
||||
LogosAPI* api = static_cast<LogosAPI*>(apiInstance);
|
||||
|
||||
int methodIndex = m_module->metaObject()->indexOfMethod("initLogos(LogosAPI*)");
|
||||
if (methodIndex != -1) {
|
||||
qDebug() << "[LogosProviderObject] QtProviderObject: calling initLogos on wrapped QObject";
|
||||
QMetaObject::invokeMethod(m_module, "initLogos",
|
||||
Qt::DirectConnection,
|
||||
Q_ARG(LogosAPI*, api));
|
||||
} else {
|
||||
qDebug() << "[LogosProviderObject] QtProviderObject: wrapped QObject has no initLogos, skipping";
|
||||
}
|
||||
}
|
||||
|
||||
QString QtProviderObject::providerName() const
|
||||
{
|
||||
PluginInterface* pi = qobject_cast<PluginInterface*>(m_module);
|
||||
return pi ? pi->name() : QString();
|
||||
}
|
||||
|
||||
QString QtProviderObject::providerVersion() const
|
||||
{
|
||||
PluginInterface* pi = qobject_cast<PluginInterface*>(m_module);
|
||||
return pi ? pi->version() : QString();
|
||||
}
|
||||
|
||||
void QtProviderObject::setEventListener(EventCallback callback)
|
||||
{
|
||||
m_eventCallback = std::move(callback);
|
||||
}
|
||||
|
||||
QVariant QtProviderObject::callMethod(const QString& methodName, const QVariantList& args)
|
||||
{
|
||||
if (!m_module) {
|
||||
qWarning() << "[LogosProviderObject] QtProviderObject::callMethod: null module";
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
if (methodName.isEmpty()) {
|
||||
qWarning() << "[LogosProviderObject] QtProviderObject::callMethod: empty method name";
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
// Special-case getPluginMethods (framework-level, not on the wrapped plugin)
|
||||
if (methodName == "getPluginMethods" && args.isEmpty()) {
|
||||
return QVariant(getMethods());
|
||||
}
|
||||
|
||||
// Special-case getPluginEvents / getPluginInterface. Legacy Qt modules have
|
||||
// no logos_events: section, so getMethods() (built here from QMetaObject)
|
||||
// only ever contains methods: events are always empty and the interface is
|
||||
// just the methods list.
|
||||
if (methodName == "getPluginEvents" && args.isEmpty()) {
|
||||
return QVariant(QJsonArray());
|
||||
}
|
||||
|
||||
if (methodName == "getPluginInterface" && args.isEmpty()) {
|
||||
return QVariant(getMethods());
|
||||
}
|
||||
|
||||
// No auth check here by design: this adapter has no token parameter and is
|
||||
// only ever reached through ModuleProxy::callRemoteMethod, which authorizes
|
||||
// the caller's token before dispatching (see ModuleProxy::isAuthorized).
|
||||
// The checks below are sanity guards on the wrapped plugin, not authz.
|
||||
PluginInterface* pluginInterface = qobject_cast<PluginInterface*>(m_module);
|
||||
if (!pluginInterface) {
|
||||
qWarning() << "[LogosProviderObject] QtProviderObject::callMethod: module is not a PluginInterface";
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
LogosAPI* api = pluginInterface->logosAPI;
|
||||
if (!api) {
|
||||
qWarning() << "[LogosProviderObject] QtProviderObject::callMethod: LogosAPI not available";
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
// Find method via QMetaObject
|
||||
const QMetaObject* metaObject = m_module->metaObject();
|
||||
int methodIndex = -1;
|
||||
for (int i = 0; i < metaObject->methodCount(); ++i) {
|
||||
QMetaMethod method = metaObject->method(i);
|
||||
if (method.name() == methodName && method.parameterCount() == args.size()) {
|
||||
methodIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (methodIndex == -1) {
|
||||
qWarning() << "[LogosProviderObject] QtProviderObject: method not found:" << methodName
|
||||
<< "with" << args.size() << "arguments";
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
QMetaMethod method = metaObject->method(methodIndex);
|
||||
QMetaType returnType = method.returnMetaType();
|
||||
|
||||
// Decode args against the types the method actually declares.
|
||||
//
|
||||
// This used to be a bare QVariant::convert(paramType), which COERCES: an
|
||||
// argument the declared type cannot represent silently became one it can
|
||||
// (echoInt(3.7) ran the body with 4; echoBool("hello") with true;
|
||||
// stringLength(42) with "42"), so the author's own precondition checks
|
||||
// never saw the value the caller sent. logos::qtArgDecode routes the value
|
||||
// through the canonical codec instead — the same rule the cdylib dispatch,
|
||||
// the Rust provider and the plain wire apply — and a mismatch becomes the
|
||||
// canonical {"code":"dispatch_failed", ...} answer rather than a plausible
|
||||
// wrong value.
|
||||
//
|
||||
// A type the codec has no rule for (QUrl, an enum, a pointer) reports
|
||||
// Unchecked and keeps the old conversion verbatim: inventing a rule for it
|
||||
// here would reject arguments that work today.
|
||||
QVariantList coercedArgs = args;
|
||||
for (int i = 0; i < method.parameterCount() && i < coercedArgs.size(); ++i) {
|
||||
QMetaType paramType = method.parameterMetaType(i);
|
||||
|
||||
QVariant decoded;
|
||||
std::string reason;
|
||||
const logos::QtArgVerdict verdict = logos::qtArgDecode(
|
||||
coercedArgs[i], paramType, "arg" + std::to_string(i), decoded, reason);
|
||||
|
||||
if (verdict == logos::QtArgVerdict::Rejected) {
|
||||
const QString message = QString::fromStdString(reason);
|
||||
qWarning() << "[LogosProviderObject] QtProviderObject: rejected"
|
||||
<< methodName << "-" << message;
|
||||
return logos::dispatchFailedVariant(providerName(), message);
|
||||
}
|
||||
if (verdict == logos::QtArgVerdict::Ok) {
|
||||
coercedArgs[i] = decoded;
|
||||
continue;
|
||||
}
|
||||
if (coercedArgs[i].metaType() != paramType) {
|
||||
QVariant converted = coercedArgs[i];
|
||||
if (converted.convert(paramType)) {
|
||||
coercedArgs[i] = converted;
|
||||
} else {
|
||||
qWarning() << "[LogosProviderObject] QtProviderObject: could not convert arg" << i
|
||||
<< "from" << coercedArgs[i].typeName()
|
||||
<< "to" << paramType.name()
|
||||
<< "for method" << methodName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool success = false;
|
||||
QVariant result;
|
||||
|
||||
if (returnType == QMetaType::fromType<void>()) {
|
||||
success = invokeMethodByArgCount(m_module, methodName, coercedArgs, nullptr, nullptr);
|
||||
if (success) result = QVariant(true);
|
||||
} else if (returnType == QMetaType::fromType<bool>()) {
|
||||
bool v = false;
|
||||
success = invokeMethodByArgCount(m_module, methodName, coercedArgs, &v, "bool");
|
||||
if (success) result = QVariant(v);
|
||||
} else if (returnType == QMetaType::fromType<int>()) {
|
||||
int v = 0;
|
||||
success = invokeMethodByArgCount(m_module, methodName, coercedArgs, &v, "int");
|
||||
if (success) result = QVariant(v);
|
||||
} else if (returnType == QMetaType::fromType<double>()) {
|
||||
double v = 0.0;
|
||||
success = invokeMethodByArgCount(m_module, methodName, coercedArgs, &v, "double");
|
||||
if (success) result = QVariant(v);
|
||||
} else if (returnType == QMetaType::fromType<float>()) {
|
||||
float v = 0.0f;
|
||||
success = invokeMethodByArgCount(m_module, methodName, coercedArgs, &v, "float");
|
||||
if (success) result = QVariant(v);
|
||||
} else if (returnType == QMetaType::fromType<QString>()) {
|
||||
QString v;
|
||||
success = invokeMethodByArgCount(m_module, methodName, coercedArgs, &v, "QString");
|
||||
if (success) result = QVariant(v);
|
||||
} else if (returnType == QMetaType::fromType<LogosResult>()) {
|
||||
LogosResult v;
|
||||
success = invokeMethodByArgCount(m_module, methodName, coercedArgs, &v, "LogosResult");
|
||||
if (success) result = QVariant::fromValue(v);
|
||||
} else if (returnType == QMetaType::fromType<QVariant>()) {
|
||||
QVariant v;
|
||||
success = invokeMethodByArgCount(m_module, methodName, coercedArgs, &v, "QVariant");
|
||||
if (success) result = v;
|
||||
} else if (returnType == QMetaType::fromType<QJsonArray>()) {
|
||||
QJsonArray v;
|
||||
success = invokeMethodByArgCount(m_module, methodName, coercedArgs, &v, "QJsonArray");
|
||||
if (success) result = QVariant(v);
|
||||
} else if (returnType == QMetaType::fromType<QVariantList>()) {
|
||||
QVariantList v;
|
||||
success = invokeMethodByArgCount(m_module, methodName, coercedArgs, &v, "QVariantList");
|
||||
if (success) result = QVariant::fromValue(v);
|
||||
} else if (returnType == QMetaType::fromType<QVariantMap>()) {
|
||||
QVariantMap v;
|
||||
success = invokeMethodByArgCount(m_module, methodName, coercedArgs, &v, "QVariantMap");
|
||||
if (success) result = QVariant::fromValue(v);
|
||||
} else if (returnType == QMetaType::fromType<QStringList>()) {
|
||||
QStringList v;
|
||||
success = invokeMethodByArgCount(m_module, methodName, coercedArgs, &v, "QStringList");
|
||||
if (success) result = QVariant(v);
|
||||
} else {
|
||||
qWarning() << "[LogosProviderObject] QtProviderObject: unsupported return type:"
|
||||
<< returnType.name() << "for method:" << methodName;
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
if (!success) {
|
||||
qWarning() << "[LogosProviderObject] QtProviderObject: failed to invoke" << methodName;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
bool QtProviderObject::informModuleToken(const QString& moduleName, const QString& token)
|
||||
{
|
||||
PluginInterface* pluginInterface = qobject_cast<PluginInterface*>(m_module);
|
||||
if (!pluginInterface) {
|
||||
qWarning() << "[LogosProviderObject] QtProviderObject::informModuleToken: not a PluginInterface";
|
||||
return false;
|
||||
}
|
||||
|
||||
LogosAPI* api = pluginInterface->logosAPI;
|
||||
if (!api) {
|
||||
qWarning() << "[LogosProviderObject] QtProviderObject::informModuleToken: LogosAPI not available";
|
||||
return false;
|
||||
}
|
||||
|
||||
TokenManager* tokenManager = api->getTokenManager();
|
||||
if (!tokenManager) {
|
||||
qWarning() << "[LogosProviderObject] QtProviderObject::informModuleToken: TokenManager not available";
|
||||
return false;
|
||||
}
|
||||
|
||||
qDebug() << "[LogosProviderObject] QtProviderObject: saving token for module:" << moduleName;
|
||||
tokenManager->saveToken(moduleName, token);
|
||||
return true;
|
||||
}
|
||||
|
||||
QJsonArray QtProviderObject::getMethods()
|
||||
{
|
||||
if (!m_module) return QJsonArray();
|
||||
|
||||
QJsonArray methodsArray;
|
||||
const QMetaObject* metaObject = m_module->metaObject();
|
||||
|
||||
for (int i = 0; i < metaObject->methodCount(); ++i) {
|
||||
QMetaMethod method = metaObject->method(i);
|
||||
|
||||
if (method.enclosingMetaObject() != metaObject) {
|
||||
continue;
|
||||
}
|
||||
|
||||
QJsonObject methodObj;
|
||||
methodObj["signature"] = QString::fromUtf8(method.methodSignature());
|
||||
methodObj["name"] = QString::fromUtf8(method.name());
|
||||
methodObj["returnType"] = QString::fromUtf8(method.typeName());
|
||||
methodObj["isInvokable"] = method.isValid() &&
|
||||
(method.methodType() == QMetaMethod::Method || method.methodType() == QMetaMethod::Slot);
|
||||
|
||||
if (method.parameterCount() > 0) {
|
||||
QJsonArray params;
|
||||
for (int p = 0; p < method.parameterCount(); ++p) {
|
||||
QJsonObject paramObj;
|
||||
paramObj["type"] = QString::fromUtf8(method.parameterTypeName(p));
|
||||
QByteArrayList paramNames = method.parameterNames();
|
||||
if (p < paramNames.size() && !paramNames.at(p).isEmpty()) {
|
||||
paramObj["name"] = QString::fromUtf8(paramNames.at(p));
|
||||
} else {
|
||||
paramObj["name"] = QString("param%1").arg(p);
|
||||
}
|
||||
params.append(paramObj);
|
||||
}
|
||||
methodObj["parameters"] = params;
|
||||
}
|
||||
|
||||
methodsArray.append(methodObj);
|
||||
}
|
||||
|
||||
return methodsArray;
|
||||
}
|
||||
|
||||
#include "moc_qt_provider_object.cpp"
|
||||
@@ -0,0 +1,47 @@
|
||||
#ifndef QT_PROVIDER_OBJECT_H
|
||||
#define QT_PROVIDER_OBJECT_H
|
||||
|
||||
#include "logos_provider_object.h"
|
||||
#include <QObject>
|
||||
|
||||
class PluginInterface;
|
||||
|
||||
// LEGACY, and carried here deliberately. This adapter is slated for deletion,
|
||||
// but LogosAPIProvider::registerObject still falls back to it for any plugin
|
||||
// that exposes a plain QObject instead of a LogosProviderPlugin, and the
|
||||
// modules that rely on that fallback have not been migrated yet. Dropping it
|
||||
// while relocating the host runtime would have changed behaviour for them, so
|
||||
// it comes across unchanged; it goes once the last legacy module is migrated.
|
||||
|
||||
/**
|
||||
* @brief Adapter that wraps an existing QObject-based plugin as a LogosProviderObject.
|
||||
*
|
||||
* This allows legacy plugins (using Q_INVOKABLE / Qt signals) to work through
|
||||
* the new LogosProviderObject interface without any changes to the plugin code.
|
||||
* All dispatch goes through QMetaObject — the same path that ModuleProxy used
|
||||
* to handle directly.
|
||||
*/
|
||||
class QtProviderObject : public QObject, public LogosProviderObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit QtProviderObject(QObject* module, QObject* parent = nullptr);
|
||||
~QtProviderObject() override;
|
||||
|
||||
QVariant callMethod(const QString& methodName, const QVariantList& args) override;
|
||||
bool informModuleToken(const QString& moduleName, const QString& token) override;
|
||||
QJsonArray getMethods() override;
|
||||
void setEventListener(EventCallback callback) override;
|
||||
void init(void* apiInstance) override;
|
||||
QString providerName() const override;
|
||||
QString providerVersion() const override;
|
||||
|
||||
private slots:
|
||||
void onWrappedEventResponse(const QString& eventName, const QVariantList& data);
|
||||
|
||||
private:
|
||||
QObject* m_module;
|
||||
EventCallback m_eventCallback;
|
||||
};
|
||||
|
||||
#endif // QT_PROVIDER_OBJECT_H
|
||||
Generated
+52
@@ -1,5 +1,30 @@
|
||||
{
|
||||
"nodes": {
|
||||
"logos-lidl": {
|
||||
"inputs": {
|
||||
"logos-nix": [
|
||||
"logos-nix"
|
||||
],
|
||||
"nixpkgs": [
|
||||
"logos-lidl",
|
||||
"logos-nix",
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1786415321,
|
||||
"narHash": "sha256-Oe98SavQSVGBIY7WIc8RQ5l+Bl4KLsdmjb+PyscfdNw=",
|
||||
"owner": "logos-co",
|
||||
"repo": "logos-lidl",
|
||||
"rev": "ffeebf2e90fa0c65e8c486988271fe0ca029d1e1",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "logos-co",
|
||||
"repo": "logos-lidl",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"logos-module": {
|
||||
"inputs": {
|
||||
"logos-nix": "logos-nix",
|
||||
@@ -61,6 +86,31 @@
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"logos-protocol": {
|
||||
"inputs": {
|
||||
"logos-nix": [
|
||||
"logos-nix"
|
||||
],
|
||||
"nixpkgs": [
|
||||
"logos-protocol",
|
||||
"logos-nix",
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1786572376,
|
||||
"narHash": "sha256-BOWenBBnlSikjS9ZVgJg6XJTa6ylov9Zxe0RarDN5iA=",
|
||||
"owner": "logos-co",
|
||||
"repo": "logos-protocol",
|
||||
"rev": "e6d5b575c25d9d26827aa4c6a9a203b175a7e0d8",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "logos-co",
|
||||
"repo": "logos-protocol",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1759036355,
|
||||
@@ -127,8 +177,10 @@
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"logos-lidl": "logos-lidl",
|
||||
"logos-module": "logos-module",
|
||||
"logos-nix": "logos-nix_2",
|
||||
"logos-protocol": "logos-protocol",
|
||||
"nixpkgs": [
|
||||
"logos-nix",
|
||||
"nixpkgs"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
description = "Logos Qt Plugin Backend — builds Logos modules as Qt 6 plugins";
|
||||
description = "Logos Qt Plugin Backend — builds Logos modules as Qt 6 plugins, and the Qt host runtime they link";
|
||||
|
||||
inputs = {
|
||||
logos-nix.url = "github:logos-co/logos-nix";
|
||||
@@ -7,9 +7,19 @@
|
||||
# When used via logos-module-builder, logosModule is injected by the builder.
|
||||
logos-module.url = "github:logos-co/logos-module";
|
||||
nixpkgs.follows = "logos-nix/nixpkgs";
|
||||
# The transport / consumer / token layer logos-qt-host is the Qt face of.
|
||||
logos-protocol = {
|
||||
url = "github:logos-co/logos-protocol";
|
||||
inputs.logos-nix.follows = "logos-nix";
|
||||
};
|
||||
# The canonical LIDL frontend logos-qt-host-generator parses contracts with.
|
||||
logos-lidl = {
|
||||
url = "github:logos-co/logos-lidl";
|
||||
inputs.logos-nix.follows = "logos-nix";
|
||||
};
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, logos-module, ... }:
|
||||
outputs = { self, nixpkgs, logos-module, logos-protocol, logos-lidl, ... }:
|
||||
let
|
||||
systems = [ "aarch64-darwin" "x86_64-darwin" "aarch64-linux" "x86_64-linux" ];
|
||||
|
||||
@@ -45,17 +55,39 @@
|
||||
# Raw export: no deps — for use by logos-module-builder
|
||||
rawLib = rawLib;
|
||||
|
||||
# Provide the cmake module as a package
|
||||
packages = forAllSystems ({ pkgs, ... }: {
|
||||
# The C++ half of this backend: the Qt host runtime a plugin links, and
|
||||
# the generator that emits the plugin around a cdylib module's C ABI.
|
||||
#
|
||||
# These are deliberately NOT reachable from `lib` / `rawLib` /
|
||||
# `cmake-module`. A consumer that only wants the Nix build functions or
|
||||
# the CMake module (logos-module-builder's common path) must not be made
|
||||
# to realise a Qt + protocol build to get them, and under Nix's laziness
|
||||
# it is not — as long as nothing in those attributes mentions these.
|
||||
packages = forAllSystems ({ pkgs, system, ... }: {
|
||||
cmake-module = pkgs.runCommand "logos-qt-plugin-cmake" {} ''
|
||||
mkdir -p $out/share/cmake/LogosModule
|
||||
cp ${./cmake/LogosModule.cmake} $out/share/cmake/LogosModule/LogosModule.cmake
|
||||
'';
|
||||
|
||||
logos-qt-host = import ./nix/qt-host.nix {
|
||||
inherit pkgs;
|
||||
src = ./.;
|
||||
protocolLib = logos-protocol.packages.${system}.logos-protocol-lib;
|
||||
};
|
||||
|
||||
logos-qt-host-generator = import ./nix/qt-host-generator.nix {
|
||||
inherit pkgs;
|
||||
src = ./qt-host-generator;
|
||||
logos-lidl = logos-lidl.packages.${system}.logos-lidl;
|
||||
};
|
||||
|
||||
# Unchanged: `default` is still the CMake module, so `nix build` on
|
||||
# this repo stays the cheap pure-Nix output it has always been.
|
||||
default = self.packages.${pkgs.system}.cmake-module;
|
||||
});
|
||||
|
||||
# Tests
|
||||
checks = forAllSystems ({ pkgs, ... }: {
|
||||
checks = forAllSystems ({ pkgs, system, ... }: {
|
||||
# Build a vanilla Qt plugin with no Logos SDK deps
|
||||
vanilla-plugin = import ./tests/test-vanilla-plugin.nix {
|
||||
inherit pkgs;
|
||||
@@ -71,6 +103,16 @@
|
||||
header-generator-guard = import ./tests/test-header-generator-guard.nix {
|
||||
inherit pkgs;
|
||||
};
|
||||
|
||||
# The Qt host runtime compiles and installs a usable CMake package.
|
||||
qt-host = self.packages.${system}.logos-qt-host;
|
||||
|
||||
# Drive the glue generator over a real contract and assert on the
|
||||
# emitted C++.
|
||||
qt-host-generator = import ./tests/test-qt-host-generator.nix {
|
||||
inherit pkgs;
|
||||
generator = self.packages.${system}.logos-qt-host-generator;
|
||||
};
|
||||
});
|
||||
|
||||
# Dev shell for working on the backend itself
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
# Builds logos-qt-host-generator — the cdylib -> Qt-plugin glue emitter.
|
||||
#
|
||||
# Qt Core plus the canonical LIDL frontend, and deliberately nothing else: the
|
||||
# one shared-frontend helper this backend uses is inlined (see
|
||||
# qt-host-generator/lidl_emit_common.h), so no SDK appears in this repo's
|
||||
# inputs on account of the generator.
|
||||
{ pkgs, src, logos-lidl }:
|
||||
|
||||
pkgs.stdenv.mkDerivation {
|
||||
pname = "logos-qt-host-generator";
|
||||
version = "0.1.0";
|
||||
|
||||
inherit src;
|
||||
|
||||
nativeBuildInputs = [
|
||||
pkgs.cmake
|
||||
pkgs.ninja
|
||||
pkgs.qt6.wrapQtAppsNoGuiHook
|
||||
];
|
||||
|
||||
buildInputs = [
|
||||
pkgs.qt6.qtbase
|
||||
logos-lidl
|
||||
];
|
||||
|
||||
cmakeFlags = [ "-GNinja" ];
|
||||
|
||||
meta = with pkgs.lib; {
|
||||
description = "Emits the Qt plugin glue around a cdylib module's C ABI";
|
||||
platforms = platforms.unix;
|
||||
mainProgram = "logos-qt-host-generator";
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
# Builds logos-qt-host — the Qt HOST RUNTIME a Logos Qt plugin links against:
|
||||
# LogosAPI (the object handed to initLogos), LogosAPIProvider (the transport
|
||||
# hosts, ModuleProxy/handshake publication and token-validator injection),
|
||||
# LogosProviderBase + the LOGOS_PROVIDER/LOGOS_METHOD macros, and the legacy
|
||||
# QMetaObject adapter. A static library plus its headers and CMake package
|
||||
# config, so a plugin build can `find_package(logos-qt-host)`.
|
||||
{ pkgs, src, protocolLib }:
|
||||
|
||||
pkgs.stdenv.mkDerivation {
|
||||
pname = "logos-qt-host";
|
||||
version = "0.1.0";
|
||||
|
||||
inherit src;
|
||||
|
||||
nativeBuildInputs = [
|
||||
pkgs.cmake
|
||||
pkgs.ninja
|
||||
pkgs.pkg-config
|
||||
pkgs.qt6.wrapQtAppsNoGuiHook
|
||||
];
|
||||
|
||||
buildInputs = [
|
||||
pkgs.qt6.qtbase
|
||||
pkgs.qt6.qtremoteobjects
|
||||
pkgs.boost
|
||||
pkgs.openssl
|
||||
pkgs.nlohmann_json
|
||||
protocolLib
|
||||
];
|
||||
|
||||
# Same propagation policy as logos-protocol / logos-qt-sdk: Qt is excluded
|
||||
# (setup-hook ordering), the protocol and the plain transport's deps are
|
||||
# carried so a consumer's find_dependency() resolves them.
|
||||
propagatedBuildInputs = [
|
||||
pkgs.boost
|
||||
pkgs.openssl
|
||||
pkgs.nlohmann_json
|
||||
protocolLib
|
||||
];
|
||||
|
||||
# The CMake project is cpp/, but the source tree must be the repo root:
|
||||
# cpp/CMakeLists.txt installs ../core/interface.h alongside its own headers.
|
||||
dontUseCmakeConfigure = true;
|
||||
|
||||
buildPhase = ''
|
||||
runHook preBuild
|
||||
|
||||
mkdir -p build-qt-host
|
||||
cd build-qt-host
|
||||
cmake ../cpp -GNinja -DCMAKE_INSTALL_PREFIX=$out \
|
||||
-DLOGOS_PROTOCOL_ROOT=${protocolLib}
|
||||
ninja
|
||||
cd ..
|
||||
|
||||
runHook postBuild
|
||||
'';
|
||||
|
||||
installPhase = ''
|
||||
runHook preInstall
|
||||
cmake --install build-qt-host
|
||||
runHook postInstall
|
||||
'';
|
||||
|
||||
meta = with pkgs.lib; {
|
||||
description = "Logos Qt host runtime — LogosAPI, provider base classes, Qt plugin glue";
|
||||
platforms = platforms.unix;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
project(LogosQtHostGenerator VERSION 0.1.0 LANGUAGES CXX)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
find_package(Qt6 REQUIRED COMPONENTS Core)
|
||||
|
||||
# logos-lidl: the canonical, language-neutral LIDL frontend (lexer/parser/AST).
|
||||
# Every Logos generator links the same one, so the parsed surface cannot skew
|
||||
# between them. Nothing else is needed here — the single helper this backend
|
||||
# borrowed from logos-cpp-sdk's shared frontend is inlined in
|
||||
# lidl_emit_common.h, which is what keeps that SDK out of this repo's inputs.
|
||||
find_package(logos-lidl REQUIRED)
|
||||
|
||||
add_executable(logos-qt-host-generator
|
||||
main.cpp
|
||||
lidl_gen_cdylib_glue.cpp
|
||||
)
|
||||
|
||||
target_include_directories(logos-qt-host-generator PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
|
||||
target_link_libraries(logos-qt-host-generator PRIVATE Qt6::Core logos-lidl::logos_lidl)
|
||||
|
||||
install(TARGETS logos-qt-host-generator RUNTIME DESTINATION bin)
|
||||
@@ -0,0 +1,42 @@
|
||||
#ifndef LIDL_COMPAT_H
|
||||
#define LIDL_COMPAT_H
|
||||
|
||||
// Bridge the copied cdylib-glue backend onto the canonical logos-lidl frontend.
|
||||
//
|
||||
// The backend (lidl_gen_cdylib_glue.{h,cpp}) came across from logos-qt-sdk
|
||||
// BYTE-IDENTICAL, so it still spells the AST types unqualified and still
|
||||
// streams std::string straight into a QTextStream. This header supplies
|
||||
// exactly those two affordances and nothing else. logos-cpp-sdk's
|
||||
// share/lidl-frontend has a much larger header of the same name (records
|
||||
// checks, the serializer/validator shims, the Qt/std type-name mappers); the
|
||||
// cdylib backend touches none of it, and pulling it in would put a
|
||||
// logos-cpp-sdk dependency on this repo for no gain.
|
||||
|
||||
#include "lidl/ast.hpp"
|
||||
#include "lidl/parser.hpp"
|
||||
|
||||
#include <QString>
|
||||
#include <QTextStream>
|
||||
#include <string>
|
||||
|
||||
// The canonical AST, in the global scope the backend references it from.
|
||||
using lidl::MethodDecl;
|
||||
using lidl::ModuleDecl;
|
||||
using lidl::TypeExpr;
|
||||
|
||||
// std::string -> QString, and let QTextStream accept std::string directly so
|
||||
// emission of AST string fields (`s << md.name`) compiles unchanged.
|
||||
inline QString qs(const std::string& s) { return QString::fromStdString(s); }
|
||||
inline QTextStream& operator<<(QTextStream& s, const std::string& v)
|
||||
{
|
||||
return s << QString::fromStdString(v);
|
||||
}
|
||||
|
||||
using LidlParseResult = lidl::ParseResult;
|
||||
|
||||
inline lidl::ParseResult lidlParse(const QString& source)
|
||||
{
|
||||
return lidl::parse(source.toStdString());
|
||||
}
|
||||
|
||||
#endif // LIDL_COMPAT_H
|
||||
@@ -0,0 +1,35 @@
|
||||
#ifndef LIDL_EMIT_COMMON_H
|
||||
#define LIDL_EMIT_COMMON_H
|
||||
|
||||
// The one shared-frontend helper the cdylib glue backend uses, inlined.
|
||||
//
|
||||
// logos-cpp-sdk distributes a header of this name under share/lidl-frontend
|
||||
// with four functions; the cdylib backend calls exactly one of them,
|
||||
// lidlToPascalCase, to derive the plugin class stem from the module name.
|
||||
// Twelve lines of definition are a far smaller thing to own than a
|
||||
// logos-cpp-sdk input on this repo, so the definition lives here and the
|
||||
// backend source stays byte-identical to the copy it came from.
|
||||
//
|
||||
// This is the same rule every other Logos generator applies to a module name
|
||||
// (logos-view-module's view-generator inlines it too); it must not drift, or
|
||||
// the class names in the emitted glue stop matching the ones the rest of the
|
||||
// toolchain expects.
|
||||
|
||||
#include <QString>
|
||||
|
||||
#include "lidl_compat.h"
|
||||
|
||||
inline QString lidlToPascalCase(const QString& name)
|
||||
{
|
||||
QString out;
|
||||
bool cap = true;
|
||||
for (QChar c : name) {
|
||||
if (!c.isLetterOrNumber()) { cap = true; continue; }
|
||||
if (cap) { out.append(c.toUpper()); cap = false; }
|
||||
else { out.append(c.toLower()); }
|
||||
}
|
||||
if (out.isEmpty()) return QString("Module");
|
||||
return out;
|
||||
}
|
||||
|
||||
#endif // LIDL_EMIT_COMMON_H
|
||||
@@ -0,0 +1,311 @@
|
||||
#include "lidl_gen_cdylib_glue.h"
|
||||
#include "lidl_emit_common.h"
|
||||
|
||||
#include <QStringList>
|
||||
#include <QTextStream>
|
||||
|
||||
QString lidlMakeCdylibGlueHeader(const ModuleDecl& module, bool multi)
|
||||
{
|
||||
const QString className = lidlToPascalCase(qs(module.name));
|
||||
QString c;
|
||||
QTextStream s(&c);
|
||||
|
||||
s << "// AUTO-GENERATED by logos-cpp-generator --cdylib -- do not edit\n";
|
||||
s << "//\n";
|
||||
s << "// The UNIFORM Qt-plugin glue over the common module-impl C ABI\n";
|
||||
s << "// (logos_module_impl.h). Identical regardless of the module's source\n";
|
||||
s << "// language (C++ or Rust): it only knows the C symbols, which are\n";
|
||||
s << "// linked in from the module's cdylib. logos_host loads it unchanged.\n";
|
||||
s << "#pragma once\n\n";
|
||||
s << "#include \"interface.h\"\n";
|
||||
s << "#include \"logos_api.h\"\n";
|
||||
s << "#include \"logos_provider_object.h\"\n";
|
||||
s << "#include \"logos_json_convert.h\"\n";
|
||||
s << "#include \"logos_module_impl.h\"\n";
|
||||
s << "#include \"logos_types.h\"\n";
|
||||
s << "#include <QObject>\n";
|
||||
s << "#include <QJsonArray>\n";
|
||||
s << "#include <QJsonDocument>\n";
|
||||
s << "#include <QSet>\n";
|
||||
s << "#include <QString>\n";
|
||||
s << "#include <QVariant>\n";
|
||||
s << "#include <QVariantList>\n";
|
||||
if (multi) {
|
||||
// concurrency:"multi" defers behind the ordinary callMethod: a worker
|
||||
// runs the handler and the result is pushed back as a completion event
|
||||
// (logos_async_dispatch.h) — no new provider/host vtable method.
|
||||
s << "#include \"logos_async_dispatch.h\"\n";
|
||||
s << "#include <QVariantMap>\n";
|
||||
s << "#include <atomic>\n";
|
||||
s << "#include <cstdint>\n";
|
||||
s << "#include <QThread>\n";
|
||||
}
|
||||
s << "#include <nlohmann/json.hpp>\n\n";
|
||||
|
||||
// LogosProviderBase (not the bare interface): its informModuleToken saves
|
||||
// into the HOST-stack TokenManager — what ModuleProxy validates INBOUND
|
||||
// calls against — exactly as C++ provider modules do. The glue then ALSO
|
||||
// forwards every token across the C ABI for the cdylib's own stack.
|
||||
s << "class " << className << "CdylibProvider : public LogosProviderBase {\n";
|
||||
s << "public:\n";
|
||||
if (multi) {
|
||||
s << " // concurrency:\"multi\": callMethod does NOT block — it hands the call to a\n";
|
||||
s << " // worker and returns a pending sentinel at once, then the worker pushes the\n";
|
||||
s << " // result back as a completion event. This is the SAME callMethod slot every\n";
|
||||
s << " // provider has — there is no extra provider/host vtable method, so the\n";
|
||||
s << " // provider ABI is unchanged and an old host loads + forwards this module.\n";
|
||||
}
|
||||
s << " QVariant callMethod(const QString& methodName, const QVariantList& args) override;\n";
|
||||
s << " QJsonArray getMethods() override;\n";
|
||||
s << " bool informModuleToken(const QString& moduleName, const QString& token) override;\n";
|
||||
s << " void setEventListener(EventCallback callback) override;\n";
|
||||
s << " QString providerName() const override { return QStringLiteral(\"" << module.name << "\"); }\n";
|
||||
s << " QString providerVersion() const override { return QStringLiteral(\"" << (module.version.empty() ? QStringLiteral("1.0.0") : qs(module.version)) << "\"); }\n";
|
||||
s << "protected:\n";
|
||||
s << " void onInit(LogosAPI* api) override;\n";
|
||||
s << "private:\n";
|
||||
s << " EventCallback m_eventCallback;\n";
|
||||
s << " static void emitTrampoline(const char* eventName, const char* dataJson, void* userData);\n";
|
||||
if (multi)
|
||||
s << " std::atomic<std::uint64_t> m_callCounter{0}; // unique deferred-call ids\n";
|
||||
s << "};\n\n";
|
||||
|
||||
// The root plugin must also implement PluginInterface — logos_host's
|
||||
// module_initializer hard-requires it before any provider detection.
|
||||
s << "class " << className << "CdylibPlugin : 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";
|
||||
s << "public:\n";
|
||||
s << " QString name() const override { return QStringLiteral(\"" << module.name << "\"); }\n";
|
||||
s << " QString version() const override { return QStringLiteral(\"" << (module.version.empty() ? QStringLiteral("1.0.0") : qs(module.version)) << "\"); }\n";
|
||||
s << " LogosProviderObject* createProviderObject() override {\n";
|
||||
s << " return new " << className << "CdylibProvider();\n";
|
||||
s << " }\n";
|
||||
s << "};\n";
|
||||
return c;
|
||||
}
|
||||
|
||||
QString lidlMakeCdylibGlueSource(const ModuleDecl& module, bool multi)
|
||||
{
|
||||
const QString className = lidlToPascalCase(qs(module.name));
|
||||
const QString provider = className + "CdylibProvider";
|
||||
|
||||
// Which methods return StdLogosResult — the glue re-materializes the Qt
|
||||
// LogosResult QVariant callers expect on the wire.
|
||||
QStringList resultMethods;
|
||||
for (const MethodDecl& md : module.methods)
|
||||
if (md.resultReturn) resultMethods << qs(md.name);
|
||||
|
||||
// Which methods return `void`. Derived from the shared contract, so every
|
||||
// cdylib backend converges here regardless of the language behind the C ABI.
|
||||
//
|
||||
// `void` is not a LIDL builtin — it parses as Named("void") — and the
|
||||
// backends handled that Named differently: the C++ cdylib has an arm that
|
||||
// returns "true", the Rust one falls to its catch-all and returns JSON null.
|
||||
// Null is the failure token further up (logos_json_convert turns it into an
|
||||
// invalid QVariant, which core_service reports as METHOD_FAILED), so the same
|
||||
// void method answered `true` from one provider and "the call failed" from
|
||||
// the other, on a contract they share.
|
||||
QStringList voidMethods;
|
||||
for (const MethodDecl& md : module.methods)
|
||||
if (!md.resultReturn && md.returnType.name == "void") voidMethods << qs(md.name);
|
||||
|
||||
QString c;
|
||||
QTextStream s(&c);
|
||||
s << "// AUTO-GENERATED by logos-cpp-generator --cdylib -- do not edit\n";
|
||||
s << "#include \"" << module.name << "_cdylib_glue.h\"\n\n";
|
||||
|
||||
if (!multi) {
|
||||
s << "QVariant " << provider << "::callMethod(const QString& methodName, const QVariantList& args)\n{\n";
|
||||
s << " nlohmann::json jArgs = nlohmann::json::array();\n";
|
||||
s << " for (const QVariant& a : args)\n";
|
||||
s << " jArgs.push_back(logos::qvariantToNlohmann(a));\n";
|
||||
s << " const std::string dumped = jArgs.dump();\n";
|
||||
s << " char* result = logos_module_dispatch(methodName.toUtf8().constData(), dumped.c_str());\n";
|
||||
s << " if (!result) return QVariant();\n";
|
||||
s << " nlohmann::json jResult = nlohmann::json::parse(result, nullptr, false);\n";
|
||||
s << " logos_module_string_free(result);\n";
|
||||
s << " if (jResult.is_discarded()) return QVariant();\n";
|
||||
if (!voidMethods.isEmpty()) {
|
||||
s << " // `void` methods answer QVariant(true) whatever the cdylib put on the\n";
|
||||
s << " // C ABI. An invalid QVariant is this slot's failure token, so a void\n";
|
||||
s << " // method needs SOME value to mean \"it ran\".\n";
|
||||
s << " static const QSet<QString> kVoidMethods = {";
|
||||
for (int i = 0; i < voidMethods.size(); ++i) {
|
||||
s << "QStringLiteral(\"" << voidMethods[i] << "\")";
|
||||
if (i + 1 < voidMethods.size()) s << ", ";
|
||||
}
|
||||
s << "};\n";
|
||||
s << " if (kVoidMethods.contains(methodName)) return QVariant(true);\n";
|
||||
}
|
||||
if (!resultMethods.isEmpty()) {
|
||||
s << " // StdLogosResult-returning methods: re-materialize the Qt LogosResult\n";
|
||||
s << " // QVariant the wire expects ({success, value, error} crossed the C ABI).\n";
|
||||
s << " static const QSet<QString> kResultMethods = {";
|
||||
for (int i = 0; i < resultMethods.size(); ++i) {
|
||||
s << "QStringLiteral(\"" << resultMethods[i] << "\")";
|
||||
if (i + 1 < resultMethods.size()) s << ", ";
|
||||
}
|
||||
s << "};\n";
|
||||
s << " if (kResultMethods.contains(methodName) && jResult.is_object()) {\n";
|
||||
s << " LogosResult lr;\n";
|
||||
s << " lr.success = jResult.value(\"success\", false);\n";
|
||||
s << " lr.value = logos::nlohmannToQVariant(jResult.value(\"value\", nlohmann::json()));\n";
|
||||
s << " lr.error = jResult.contains(\"error\") && jResult[\"error\"].is_string()\n";
|
||||
s << " ? QVariant(QString::fromStdString(jResult[\"error\"].get<std::string>()))\n";
|
||||
s << " : QVariant();\n";
|
||||
s << " return QVariant::fromValue(lr);\n";
|
||||
s << " }\n";
|
||||
}
|
||||
s << " return logos::nlohmannToQVariant(jResult);\n";
|
||||
s << "}\n\n";
|
||||
} else {
|
||||
// concurrency:"multi": callMethod does NOT block. Marshal the args, hand
|
||||
// the call to a worker thread (the cdylib's logos_module_dispatch is
|
||||
// thread-safe in multi mode), and return a PENDING SENTINEL immediately so
|
||||
// the dispatch thread is freed for other callers. When the worker
|
||||
// finishes it pushes the result back as a completion event keyed by
|
||||
// callId, over the provider's existing event listener; the consumer
|
||||
// transport awaits it. This rides the ORDINARY callMethod slot — there is
|
||||
// no new provider/host vtable method, so the provider ABI is unchanged and
|
||||
// an old host loads + forwards this module unmodified.
|
||||
s << "QVariant " << provider << "::callMethod(const QString& methodName, const QVariantList& args)\n{\n";
|
||||
s << " nlohmann::json jArgs = nlohmann::json::array();\n";
|
||||
s << " for (const QVariant& a : args)\n";
|
||||
s << " jArgs.push_back(logos::qvariantToNlohmann(a));\n";
|
||||
s << " const std::string dumped = jArgs.dump();\n";
|
||||
s << " const std::string method = methodName.toStdString();\n";
|
||||
if (!resultMethods.isEmpty()) {
|
||||
s << " static const QSet<QString> kResultMethods = {";
|
||||
for (int i = 0; i < resultMethods.size(); ++i) {
|
||||
s << "QStringLiteral(\"" << resultMethods[i] << "\")";
|
||||
if (i + 1 < resultMethods.size()) s << ", ";
|
||||
}
|
||||
s << "};\n";
|
||||
s << " const bool isResultMethod = kResultMethods.contains(methodName);\n";
|
||||
}
|
||||
if (!voidMethods.isEmpty()) {
|
||||
s << " static const QSet<QString> kVoidMethods = {";
|
||||
for (int i = 0; i < voidMethods.size(); ++i) {
|
||||
s << "QStringLiteral(\"" << voidMethods[i] << "\")";
|
||||
if (i + 1 < voidMethods.size()) s << ", ";
|
||||
}
|
||||
s << "};\n";
|
||||
s << " const bool isVoidMethod = kVoidMethods.contains(methodName);\n";
|
||||
}
|
||||
s << " const QString callId = QStringLiteral(\"lc-%1\").arg(\n";
|
||||
s << " static_cast<qulonglong>(m_callCounter.fetch_add(1, std::memory_order_relaxed)));\n";
|
||||
s << " EventCallback eventCb = m_eventCallback; // copied for the worker\n";
|
||||
// Run the handler on a real QThread, not a raw std::thread: if the handler
|
||||
// makes an outbound module->module call it spins nested QEventLoops (to
|
||||
// acquire the QtRO replica and await a deferred reply), and only a genuine
|
||||
// QThread carries a Qt event dispatcher that can drive those — an adopted
|
||||
// std::thread cannot pump the QtRO socket, so such calls would hang. The
|
||||
// client stays owned by this worker (inline, no cross-thread marshaling).
|
||||
if (!resultMethods.isEmpty())
|
||||
s << " QThread* worker = QThread::create([method, dumped, isResultMethod, callId, eventCb]() {\n";
|
||||
else
|
||||
s << " QThread* worker = QThread::create([method, dumped, callId, eventCb]() {\n";
|
||||
s << " char* result = logos_module_dispatch(method.c_str(), dumped.c_str());\n";
|
||||
s << " QVariant value;\n";
|
||||
s << " if (result) {\n";
|
||||
s << " nlohmann::json jResult = nlohmann::json::parse(result, nullptr, false);\n";
|
||||
s << " logos_module_string_free(result);\n";
|
||||
s << " if (!jResult.is_discarded()) {\n";
|
||||
if (!voidMethods.isEmpty()) {
|
||||
s << " if (isVoidMethod) {\n";
|
||||
s << " value = QVariant(true);\n";
|
||||
s << " } else\n";
|
||||
}
|
||||
if (!resultMethods.isEmpty()) {
|
||||
s << " if (isResultMethod && jResult.is_object()) {\n";
|
||||
s << " LogosResult lr;\n";
|
||||
s << " lr.success = jResult.value(\"success\", false);\n";
|
||||
s << " lr.value = logos::nlohmannToQVariant(jResult.value(\"value\", nlohmann::json()));\n";
|
||||
s << " lr.error = jResult.contains(\"error\") && jResult[\"error\"].is_string()\n";
|
||||
s << " ? QVariant(QString::fromStdString(jResult[\"error\"].get<std::string>()))\n";
|
||||
s << " : QVariant();\n";
|
||||
s << " value = QVariant::fromValue(lr);\n";
|
||||
s << " } else {\n";
|
||||
s << " value = logos::nlohmannToQVariant(jResult);\n";
|
||||
s << " }\n";
|
||||
} else {
|
||||
s << " { value = logos::nlohmannToQVariant(jResult); }\n";
|
||||
}
|
||||
s << " }\n";
|
||||
s << " }\n";
|
||||
s << " if (eventCb)\n";
|
||||
s << " eventCb(logos::callCompleteEvent(), QVariantList{ callId, value });\n";
|
||||
s << " });\n";
|
||||
s << " QObject::connect(worker, &QThread::finished, worker, &QThread::deleteLater);\n";
|
||||
s << " worker->start();\n";
|
||||
s << " QVariantMap pending;\n";
|
||||
s << " pending[logos::pendingCallKey()] = callId;\n";
|
||||
s << " return pending;\n";
|
||||
s << "}\n\n";
|
||||
}
|
||||
|
||||
s << "QJsonArray " << provider << "::getMethods()\n{\n";
|
||||
s << " char* json = logos_module_get_methods();\n";
|
||||
s << " if (!json) return QJsonArray();\n";
|
||||
s << " QJsonDocument doc = QJsonDocument::fromJson(QByteArray(json));\n";
|
||||
s << " logos_module_string_free(json);\n";
|
||||
s << " return doc.isArray() ? doc.array() : QJsonArray();\n";
|
||||
s << "}\n\n";
|
||||
|
||||
s << "bool " << provider << "::informModuleToken(const QString& moduleName, const QString& token)\n{\n";
|
||||
s << " // Host-stack save first: ModuleProxy validates INBOUND calls against\n";
|
||||
s << " // the host's TokenManager (LogosProviderBase saves there).\n";
|
||||
s << " const bool hostOk = LogosProviderBase::informModuleToken(moduleName, token);\n";
|
||||
s << " // Then forward across the C ABI: the cdylib's own protocol stack\n";
|
||||
s << " // (a separate static copy) authenticates the module's OUTBOUND calls.\n";
|
||||
s << " const bool implOk = logos_module_accept_token(moduleName.toUtf8().constData(),\n";
|
||||
s << " token.toUtf8().constData()) == 0;\n";
|
||||
s << " return hostOk && implOk;\n";
|
||||
s << "}\n\n";
|
||||
|
||||
s << "void " << provider << "::setEventListener(EventCallback callback)\n{\n";
|
||||
s << " m_eventCallback = std::move(callback);\n";
|
||||
s << " logos_module_set_emit_callback(&" << provider << "::emitTrampoline, this);\n";
|
||||
s << "}\n\n";
|
||||
|
||||
s << "void " << provider << "::emitTrampoline(const char* eventName, const char* dataJson, void* userData)\n{\n";
|
||||
s << " auto* self = static_cast<" << provider << "*>(userData);\n";
|
||||
s << " if (!self || !self->m_eventCallback || !eventName) return;\n";
|
||||
s << " nlohmann::json payload = dataJson\n";
|
||||
s << " ? nlohmann::json::parse(dataJson, nullptr, false)\n";
|
||||
s << " : nlohmann::json::array();\n";
|
||||
s << " if (payload.is_discarded() || !payload.is_array()) payload = nlohmann::json::array();\n";
|
||||
s << " self->m_eventCallback(QString::fromUtf8(eventName),\n";
|
||||
s << " logos::nlohmannArgsToQVariantList(payload));\n";
|
||||
s << "}\n\n";
|
||||
|
||||
s << "void " << provider << "::onInit(LogosAPI* api)\n{\n";
|
||||
s << " QObject* obj = api;\n";
|
||||
s << " if (!obj) return;\n";
|
||||
s << " // Token FIRST: the cdylib runs its own protocol stack (a separate\n";
|
||||
s << " // static copy with its own TokenManager). Seed it with the\n";
|
||||
s << " // host-issued auth token the initializer surfaces as a property\n";
|
||||
s << " // (set before registerObject, so it is visible here), under the\n";
|
||||
s << " // same keys the initializer uses (\"core\" / \"capability_module\")\n";
|
||||
s << " // — this authenticates the module's OUTBOUND calls (incl. the\n";
|
||||
s << " // capability requestModule flow). Seeding before the context\n";
|
||||
s << " // forward means on_context_ready/onContextReady can already make\n";
|
||||
s << " // authenticated calls whenever the impl's ready-latch fires.\n";
|
||||
s << " const QString authToken = obj->property(\"authToken\").toString();\n";
|
||||
s << " if (!authToken.isEmpty()) {\n";
|
||||
s << " logos_module_accept_token(\"core\", authToken.toUtf8().constData());\n";
|
||||
s << " logos_module_accept_token(\"capability_module\", authToken.toUtf8().constData());\n";
|
||||
s << " }\n";
|
||||
s << " // Context LAST — comes from the host's property stamping on the\n";
|
||||
s << " // LogosAPI object, forwarded across the C ABI; the cdylib never\n";
|
||||
s << " // sees Qt. (The impl fires its context-ready hook once BOTH the\n";
|
||||
s << " // context and the emit callback have been delivered.)\n";
|
||||
s << " logos_module_set_context(\n";
|
||||
s << " obj->property(\"modulePath\").toString().toUtf8().constData(),\n";
|
||||
s << " obj->property(\"instanceId\").toString().toUtf8().constData(),\n";
|
||||
s << " obj->property(\"instancePersistencePath\").toString().toUtf8().constData());\n";
|
||||
s << "}\n";
|
||||
return c;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// The Qt half of the cdylib module path: the uniform Qt-plugin glue that
|
||||
// wraps the (language-agnostic) module-impl C ABI. The Qt-FREE half — the
|
||||
// C-ABI impl-exports around a C++ impl class — stays with logos-cpp-sdk's
|
||||
// generator; this glue is emitted by logos-qt-generator.
|
||||
#pragma once
|
||||
|
||||
#include <QString>
|
||||
#include "lidl_compat.h"
|
||||
|
||||
// `multi` ⇒ the module was built with concurrency:"multi": also emit a
|
||||
// callMethodAsync override that drives the cdylib's logos_module_dispatch_async
|
||||
// (concurrent handler execution). Default false = single (sync callMethod only).
|
||||
QString lidlMakeCdylibGlueHeader(const ModuleDecl& module, bool multi = false);
|
||||
QString lidlMakeCdylibGlueSource(const ModuleDecl& module, bool multi = false);
|
||||
@@ -0,0 +1,118 @@
|
||||
// logos-qt-host-generator — the cdylib -> Qt-plugin glue for a Logos module.
|
||||
//
|
||||
// A cdylib module (Rust, or C++ compiled to the same shape) exposes the
|
||||
// language-neutral module-impl C ABI (logos_module_impl.h) and nothing else.
|
||||
// This tool emits the Qt plugin that logos_host actually loads around it:
|
||||
//
|
||||
// <name>_cdylib_glue.h the *CdylibProvider (LogosProviderBase) and the
|
||||
// *CdylibPlugin (PluginInterface +
|
||||
// LogosProviderPlugin, Q_PLUGIN_METADATA)
|
||||
// <name>_cdylib_glue.cpp callMethod / getMethods / informModuleToken /
|
||||
// setEventListener / onInit, each forwarding across
|
||||
// the C ABI
|
||||
//
|
||||
// The glue is uniform: it only knows the C symbols, so it is identical
|
||||
// whatever language sits behind them. That is why it belongs with the Qt
|
||||
// plugin BACKEND (this repo) rather than with a language SDK — the C-ABI
|
||||
// impl-exports on the other side of that boundary are logos-cpp-generator's
|
||||
// job, and the two halves meet only at logos_module_impl.h.
|
||||
//
|
||||
// Usage:
|
||||
// logos-qt-host-generator --lidl <contract.lidl>
|
||||
// [--concurrency multi] [--output-dir <dir>]
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QTextStream>
|
||||
|
||||
#include "lidl_compat.h"
|
||||
#include "lidl_gen_cdylib_glue.h"
|
||||
|
||||
namespace {
|
||||
|
||||
struct Out { QString file; QString content; };
|
||||
|
||||
int writeAll(const QList<Out>& outs, const QString& dir,
|
||||
QTextStream& out, QTextStream& err)
|
||||
{
|
||||
for (const Out& o : outs) {
|
||||
const QString abs = QDir(dir).filePath(o.file);
|
||||
QFile f(abs);
|
||||
if (!f.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text)) {
|
||||
err << "Failed to write: " << abs << "\n";
|
||||
return 1;
|
||||
}
|
||||
f.write(o.content.toUtf8());
|
||||
out << "Generated: " << abs << "\n";
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
QString argValue(const QStringList& args, const QString& flag)
|
||||
{
|
||||
const int i = args.indexOf(flag);
|
||||
return (i != -1 && i + 1 < args.size()) ? args.at(i + 1) : QString();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char* argv[])
|
||||
{
|
||||
QCoreApplication app(argc, argv);
|
||||
QTextStream out(stdout);
|
||||
QTextStream err(stderr);
|
||||
const QStringList args = app.arguments();
|
||||
|
||||
const QString lidlPath = argValue(args, "--lidl");
|
||||
// concurrency:"multi" (from metadata.json, fed by the builder) ⇒ emit the
|
||||
// deferred dispatch instead: callMethod hands the call to a worker and
|
||||
// returns a pending sentinel, and the result comes back as a completion
|
||||
// event. Anything other than the exact word "multi" means single.
|
||||
const bool multi = argValue(args, "--concurrency") == QStringLiteral("multi");
|
||||
QString outputDir = argValue(args, "--output-dir");
|
||||
|
||||
if (lidlPath.isEmpty()) {
|
||||
err << "Usage: logos-qt-host-generator --lidl <contract.lidl>\n"
|
||||
" [--concurrency multi] [--output-dir <dir>]\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
// This binary emits ONE backend, so --backend is not a mode selector here.
|
||||
// Accept the value the multi-backend generator used for this path, and
|
||||
// REFUSE any other, rather than ignoring the flag: callers are migrating
|
||||
// from a tool where --backend was required and dispatched on, so silently
|
||||
// treating `--backend qt` as cdylib would hand back confidently wrong
|
||||
// artifacts (the qt backend emits <name>_qt_glue.h + _dispatch.cpp +
|
||||
// _events.cpp — different files entirely) with a zero exit status.
|
||||
const QString backend = argValue(args, "--backend");
|
||||
if (!backend.isEmpty() && backend != QStringLiteral("cdylib")) {
|
||||
err << "Error: logos-qt-host-generator only emits the cdylib backend, "
|
||||
<< "but --backend " << backend << " was requested.\n";
|
||||
return 2;
|
||||
}
|
||||
if (outputDir.isEmpty())
|
||||
outputDir = QDir::current().filePath("generated");
|
||||
QDir().mkpath(outputDir);
|
||||
|
||||
QFile f(lidlPath);
|
||||
if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||
err << "Failed to open LIDL file: " << lidlPath << "\n";
|
||||
return 3;
|
||||
}
|
||||
LidlParseResult pr = lidlParse(QString::fromUtf8(f.readAll()));
|
||||
if (pr.hasError()) {
|
||||
err << lidlPath << ":" << pr.errorLine << ":" << pr.errorColumn
|
||||
<< ": " << pr.error << "\n";
|
||||
return 4;
|
||||
}
|
||||
const ModuleDecl& mod = pr.module;
|
||||
|
||||
QList<Out> outs;
|
||||
outs.append({qs(mod.name) + "_cdylib_glue.h", lidlMakeCdylibGlueHeader(mod, multi)});
|
||||
outs.append({qs(mod.name) + "_cdylib_glue.cpp", lidlMakeCdylibGlueSource(mod, multi)});
|
||||
|
||||
const int rc = writeAll(outs, outputDir, out, err);
|
||||
out.flush();
|
||||
return rc;
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
# Drives logos-qt-host-generator over a real LIDL contract and asserts on the
|
||||
# C++ it emits. The generator has no other test surface — nothing downstream
|
||||
# compiles its output inside this repo — so these greps are what stands between
|
||||
# a contract-parsing or emission regression and a module that fails to build
|
||||
# (or, worse, builds and forwards the wrong thing across the C ABI).
|
||||
{ pkgs, generator }:
|
||||
|
||||
pkgs.runCommand "logos-qt-host-generator-test" {
|
||||
nativeBuildInputs = [ generator ];
|
||||
} ''
|
||||
mkdir -p work && cd work
|
||||
cat > sample.lidl <<'EOF'
|
||||
module sample_probe {
|
||||
version "2.1.0"
|
||||
|
||||
method whoAmI() -> tstr
|
||||
method echoInt(v: int) -> int
|
||||
method doVoid() -> void
|
||||
method makeResult(ok: bool) -> result
|
||||
|
||||
event tickEvent(v: tstr)
|
||||
}
|
||||
EOF
|
||||
|
||||
# ---- single (default) concurrency ------------------------------------
|
||||
logos-qt-host-generator --lidl sample.lidl --output-dir out
|
||||
|
||||
for f in sample_probe_cdylib_glue.h sample_probe_cdylib_glue.cpp; do
|
||||
test -f "out/$f" || { echo "MISSING: $f"; exit 1; }
|
||||
done
|
||||
|
||||
h=out/sample_probe_cdylib_glue.h
|
||||
c=out/sample_probe_cdylib_glue.cpp
|
||||
|
||||
# The class stem is PascalCase of the module name; the provider derives
|
||||
# LogosProviderBase (NOT the bare interface) because that is what saves the
|
||||
# token into the host-stack TokenManager ModuleProxy validates against, and
|
||||
# the plugin must also implement PluginInterface or logos_host's
|
||||
# module_initializer refuses it before any provider detection.
|
||||
grep -q "class SampleProbeCdylibProvider : public LogosProviderBase" $h \
|
||||
|| { echo "provider class name/base wrong"; exit 1; }
|
||||
grep -q "class SampleProbeCdylibPlugin : public QObject, public PluginInterface, public LogosProviderPlugin" $h \
|
||||
|| { echo "plugin class name/bases wrong"; exit 1; }
|
||||
grep -q 'Q_PLUGIN_METADATA(IID LogosProviderPlugin_iid FILE "metadata.json")' $h \
|
||||
|| { echo "plugin metadata macro missing"; exit 1; }
|
||||
grep -q 'providerName() const override { return QStringLiteral("sample_probe"); }' $h \
|
||||
|| { echo "module name not carried into providerName()"; exit 1; }
|
||||
grep -q 'providerVersion() const override { return QStringLiteral("2.1.0"); }' $h \
|
||||
|| { echo "version not carried from the contract"; exit 1; }
|
||||
|
||||
# Every C-ABI entry point the glue exists to forward across. Losing any one
|
||||
# of these is a module that loads and then silently does nothing.
|
||||
for sym in logos_module_dispatch logos_module_string_free \
|
||||
logos_module_get_methods logos_module_accept_token \
|
||||
logos_module_set_emit_callback logos_module_set_context; do
|
||||
grep -q "$sym" $c || { echo "C-ABI forwarding lost: $sym"; exit 1; }
|
||||
done
|
||||
|
||||
# `void` and `result` returns are the two shapes the glue has to special-case
|
||||
# (an invalid QVariant is this slot's failure token, so a void method needs
|
||||
# SOME value; a result has to be re-materialized as a Qt LogosResult).
|
||||
grep -q 'kVoidMethods = {QStringLiteral("doVoid")}' $c \
|
||||
|| { echo "void method set not derived from the contract"; exit 1; }
|
||||
grep -q 'kResultMethods = {QStringLiteral("makeResult")}' $c \
|
||||
|| { echo "result method set not derived from the contract"; exit 1; }
|
||||
|
||||
# Single concurrency: callMethod BLOCKS on the C ABI and returns the answer.
|
||||
grep -q "char\* result = logos_module_dispatch(methodName.toUtf8().constData()" $c \
|
||||
|| { echo "single-concurrency callMethod is not the blocking dispatch"; exit 1; }
|
||||
if grep -q "pendingCallKey" $c; then
|
||||
echo "single concurrency emitted the deferred path"; exit 1
|
||||
fi
|
||||
|
||||
# ---- concurrency: multi ----------------------------------------------
|
||||
# A different code path entirely: callMethod hands the call to a worker and
|
||||
# returns a pending sentinel, and the result arrives as a completion event.
|
||||
logos-qt-host-generator --lidl sample.lidl --concurrency multi --output-dir out-multi
|
||||
|
||||
hm=out-multi/sample_probe_cdylib_glue.h
|
||||
cm=out-multi/sample_probe_cdylib_glue.cpp
|
||||
|
||||
grep -q '#include "logos_async_dispatch.h"' $hm \
|
||||
|| { echo "multi header missing the async-dispatch include"; exit 1; }
|
||||
grep -q "m_callCounter" $hm \
|
||||
|| { echo "multi header missing the deferred-call id counter"; exit 1; }
|
||||
# A real QThread, not a raw std::thread: a handler making an outbound
|
||||
# module->module call spins nested QEventLoops, which only a QThread's event
|
||||
# dispatcher can drive.
|
||||
grep -q "QThread::create" $cm \
|
||||
|| { echo "multi source does not run the handler on a QThread"; exit 1; }
|
||||
grep -q "pending\[logos::pendingCallKey()\] = callId;" $cm \
|
||||
|| { echo "multi source does not return the pending sentinel"; exit 1; }
|
||||
grep -q "eventCb(logos::callCompleteEvent(), QVariantList{ callId, value });" $cm \
|
||||
|| { echo "multi source does not push the completion event"; exit 1; }
|
||||
|
||||
# ---- refusals ---------------------------------------------------------
|
||||
# No contract at all, and an unparseable one, must both FAIL rather than
|
||||
# emit half a plugin.
|
||||
if logos-qt-host-generator --output-dir out-bad; then
|
||||
echo "generator accepted a run with no --lidl"; exit 1
|
||||
fi
|
||||
echo 'this file is not a LIDL contract' > broken.lidl
|
||||
if logos-qt-host-generator --lidl broken.lidl --output-dir out-bad; then
|
||||
echo "generator accepted an unparseable contract"; exit 1
|
||||
fi
|
||||
|
||||
touch $out
|
||||
''
|
||||
Reference in New Issue
Block a user