diff --git a/cpp-generator/experimental/lidl_gen_cdylib.cpp b/cpp-generator/experimental/lidl_gen_cdylib.cpp index 2570571..c8adda6 100644 --- a/cpp-generator/experimental/lidl_gen_cdylib.cpp +++ b/cpp-generator/experimental/lidl_gen_cdylib.cpp @@ -716,6 +716,9 @@ QString lidlMakeModuleImplExports(const ModuleDecl& module, // modules() was already wired by lidlEnsureModulesWired() above (before this // context-gated early return), so onContextReady can safely call // modules().... / subscribe to dependency events from the hook. + // The module's own registry name, which the generator knows statically. + // Set BEFORE the context so moduleName() is live inside onContextReady(). + s << " _logos_codegen_::maybeSetModuleName(lidlImpl(), \"" << module.name << "\");\n"; s << " _logos_codegen_::maybeSetContext(lidlImpl(), path, id, persist);\n"; s << "}\n\n"; diff --git a/cpp-generator/generator_lib.cpp b/cpp-generator/generator_lib.cpp index e39cc8d..19ff9c3 100644 --- a/cpp-generator/generator_lib.cpp +++ b/cpp-generator/generator_lib.cpp @@ -1649,6 +1649,31 @@ QString makeUmbrellaHeaderFromDeps(const QJsonArray& deps, const QStringList& in s << " std::map> m_" << ifaceName << "_bound;\n"; } + + // Untyped, BY-NAME access to a module this umbrella does not wrap. + // + // The typed members above cover `metadata.json#dependencies`, which is + // the right default and stays the ordinary way to call another module. + // But the by-name path already exists at every layer beneath this one + // (lp_client_create / lp_invoke, logos::LpClient), so a consumer that + // genuinely needs it — a proxy, a router, anything whose target is a + // runtime value — has been reaching around the umbrella to get it. + // Exposing it here is what makes that a supported surface rather than + // an accident. + // + // The origin is baked in, exactly as the typed members' is: an origin + // is asserted, never borrowed, and a wrong one authenticates as nobody + // and fails far from the call. Clients are cached per target, mirroring + // the bind_ state map above, because LpClient owns a connection. + // + // Pair it with LpClient::getMethods() — invoke without introspect is + // guessing. + s << " logos::LpClient& dynamic(const std::string& target) {\n"; + s << " auto& _c = m_dynamic[target];\n"; + s << " if (!_c) _c = std::make_unique(target, \"" << originName << "\");\n"; + s << " return *_c;\n"; + s << " }\n"; + s << " std::map> m_dynamic;\n"; s << "};\n"; return content; } diff --git a/cpp/logos_lp_client.h b/cpp/logos_lp_client.h index 66b583c..645caf1 100644 --- a/cpp/logos_lp_client.h +++ b/cpp/logos_lp_client.h @@ -164,6 +164,20 @@ public: &LpClient::resultTrampoline, box); } + // The target's method list, as the JSON the host reports. Empty on + // failure. Invoke-without-introspect is what makes a by-name call an + // escape hatch rather than an API: a caller that cannot ask what exists + // can only guess, and a wrong guess fails at runtime like a typo. + nlohmann::json getMethods() { + lp_client* c = ensure(); + if (!c) return nlohmann::json(); + char* out = lp_get_methods(c); + if (!out) return nlohmann::json(); + auto parsed = nlohmann::json::parse(out, nullptr, /*allow_exceptions=*/false); + lp_string_free(out); + return parsed.is_discarded() ? nlohmann::json() : parsed; + } + // Subscribe to `event`. The payload is delivered as a JSON array. The // returned handle owns the subscription — keep it alive (the generated // wrapper stores it) for as long as you want the callback to fire. diff --git a/cpp/logos_module_context.h b/cpp/logos_module_context.h index 58e56b9..861512a 100644 --- a/cpp/logos_module_context.h +++ b/cpp/logos_module_context.h @@ -85,6 +85,14 @@ public: // resources bundled next to the plugin (icons, qml/, schema files…). const std::string& modulePath() const { return m_modulePath; } + // This module's own registry name — the name other modules address it by, + // and the `origin` it authenticates as. Needed by any BY-NAME call: the + // typed wrappers bake their origin in at codegen time, but a dynamic call + // has to state it, and a wrong origin authenticates as nobody and fails far + // from the call site. Empty outside a framework-provisioned context, like + // the getters below. + const std::string& moduleName() const { return m_moduleName; } + // Short ID the host assigns to this instance. Stable across restarts // for the same on-disk persistence directory; multiple side-by-side // instances of the same module get distinct IDs. @@ -152,6 +160,16 @@ public: onContextReady(); } + // Framework-only — sets moduleName(). Separate from + // `_logosCoreSetContext_` on purpose: that signature is called by every + // generated provider, so widening it would break each one until + // regenerated, for a value the generator knows statically anyway. Called + // BEFORE the context setter, so moduleName() is already populated when + // onContextReady() fires. + void _logosCoreSetModuleName_(std::string moduleName) { + m_moduleName = std::move(moduleName); + } + // Framework-only — sets the typed `LogosModules` pointer that // `logos()` dereferences. Untyped (void*) at this layer because // the SDK header is shared by every module; the codegen-generated @@ -194,6 +212,7 @@ protected: virtual void onContextReady() {} private: + std::string m_moduleName; std::string m_modulePath; std::string m_instanceId; std::string m_instancePersistencePath; @@ -230,6 +249,19 @@ private: // --------------------------------------------------------------------------- namespace _logos_codegen_ { +template +inline auto maybeSetModuleName(T& impl, std::string moduleName) + -> std::enable_if_t> +{ + static_cast(impl)._logosCoreSetModuleName_(std::move(moduleName)); +} + +template +inline auto maybeSetModuleName(T&, std::string) + -> std::enable_if_t> +{ +} + template inline auto maybeSetContext(T& impl, std::string modulePath,