diff --git a/README.md b/README.md index ae32bf8..cc5afe2 100644 --- a/README.md +++ b/README.md @@ -44,8 +44,8 @@ nix run github:logos-co/logos-doctest -- run tests/tutorial-cpp-ui-app.test.yaml nix run github:logos-co/logos-doctest -- generate tests/tutorial-wrapping-c-library.test.yaml # Pin all GitHub URLs to a specific release tag -nix run github:logos-co/logos-doctest -- run tests/tutorial-wrapping-c-library.test.yaml --release tutorial-v2 -nix run github:logos-co/logos-doctest -- generate tests/tutorial-wrapping-c-library.test.yaml --release tutorial-v2 +nix run github:logos-co/logos-doctest -- run tests/tutorial-wrapping-c-library.test.yaml --release 0.2.0 +nix run github:logos-co/logos-doctest -- generate tests/tutorial-wrapping-c-library.test.yaml --release 0.2.0 ``` > **Tip:** developing against a local `logos-doctest` checkout? Swap `github:logos-co/logos-doctest` for `path:../logos-doctest` (or wherever your checkout lives) to run your local changes. @@ -90,7 +90,7 @@ nix run github:logos-co/logos-doctest -- run tests/tutorial-cpp-ui-app.test.yaml Press `q` to quit at any time. `--tui` needs an interactive terminal and the [`rich`](https://github.com/Textualize/rich) package — both are bundled in the doctest flake, so no extra install is needed when using `nix`. -The `--release` flag (or the `release` field in the YAML) pins all `{release}` placeholders in GitHub URLs to a git tag, so `github:logos-co/repo{release}#output` becomes `github:logos-co/repo/tutorial-v2#output`. Set it to `""` or omit it for latest. +The `--release` flag (or the `release` field in the YAML) pins all `{release}` placeholders in GitHub URLs to a git tag, so `github:logos-co/repo{release}#output` becomes `github:logos-co/repo/0.2.0#output`. Set it to `""` or omit it for latest. ## Example Modules @@ -122,13 +122,13 @@ This: By default `run.sh` resolves every `{release}` placeholder to the latest commit on each repo. Pass `--release TAG` to pin them all to a git tag, so the executed commands and the generated Markdown both reference that tag: ```bash -./run.sh --release tutorial-v3 +./run.sh --release 0.2.0 ``` Any further arguments are forwarded verbatim to the underlying `doctest run`/`generate` calls, so you can override a single repo's ref with `--release-for`: ```bash -./run.sh --release tutorial-v3 --release-for logos-basecamp=main +./run.sh --release 0.2.0 --release-for logos-basecamp=main ``` The `TAG` must exist on each referenced repo, or the `nix build`/`nix flake init` steps will fail to resolve it. Pinning expands each `{release}` placeholder so `github:logos-co/repo{release}#output` becomes `github:logos-co/repo/TAG#output`; omitting `--release` leaves them at latest. @@ -136,7 +136,7 @@ The `TAG` must exist on each referenced repo, or the `nix build`/`nix flake init To run against a local `logos-doctest` checkout instead of the published flake, export `DOCTEST`: ```bash -DOCTEST="nix run path:../logos-doctest --" ./run.sh --release tutorial-v3 +DOCTEST="nix run path:../logos-doctest --" ./run.sh --release 0.2.0 ``` > **Note:** the run requires [Nix with flakes](https://nixos.org/download.html) and pulls/builds real dependencies (Qt, the Logos SDK), so the first run is slow. On Linux, add `--continue-on-fail` to the `run` command in `run.sh` if a known-failing prerequisite step would otherwise stop the chain early. diff --git a/outputs/images/basecamp-calc-installed.png b/outputs/images/basecamp-calc-installed.png index 83c801e..1fa2343 100644 Binary files a/outputs/images/basecamp-calc-installed.png and b/outputs/images/basecamp-calc-installed.png differ diff --git a/outputs/images/basecamp-interface-docs.png b/outputs/images/basecamp-interface-docs.png new file mode 100644 index 0000000..de27837 Binary files /dev/null and b/outputs/images/basecamp-interface-docs.png differ diff --git a/outputs/images/basecamp-load-calculator.png b/outputs/images/basecamp-load-calculator.png index e24a877..60c110a 100644 Binary files a/outputs/images/basecamp-load-calculator.png and b/outputs/images/basecamp-load-calculator.png differ diff --git a/outputs/images/basecamp-load.png b/outputs/images/basecamp-load.png index ef39be2..07ac12b 100644 Binary files a/outputs/images/basecamp-load.png and b/outputs/images/basecamp-load.png differ diff --git a/outputs/images/calc-ui.png b/outputs/images/calc-ui.png index 2b0295c..cb204c1 100644 Binary files a/outputs/images/calc-ui.png and b/outputs/images/calc-ui.png differ diff --git a/outputs/logos-calc-aggregator-module/flake.nix b/outputs/logos-calc-aggregator-module/flake.nix index c0507d9..dc37a98 100644 --- a/outputs/logos-calc-aggregator-module/flake.nix +++ b/outputs/logos-calc-aggregator-module/flake.nix @@ -2,7 +2,7 @@ description = "Aggregator core module - composes calc_module and showcases LogosModuleContext"; inputs = { - logos-module-builder.url = "github:logos-co/logos-module-builder"; + logos-module-builder.url = "github:logos-co/logos-module-builder/0.2.0"; # The module this one depends on. Placeholder path — locked to your # real checkout in the build step via `--override-input`. diff --git a/outputs/logos-calc-aggregator-module/src/minimal_impl.cpp b/outputs/logos-calc-aggregator-module/src/minimal_impl.cpp new file mode 100644 index 0000000..ff94646 --- /dev/null +++ b/outputs/logos-calc-aggregator-module/src/minimal_impl.cpp @@ -0,0 +1,16 @@ +#include "minimal_impl.h" + +std::string MinimalImpl::greet(const std::string& name) +{ + std::string greeting = "Hello, " + name + "! Greetings from the minimal module."; + + // The generated event body routes the typed payload to every subscriber. + greeted(greeting); + + return greeting; +} + +std::string MinimalImpl::getStatus() +{ + return "Minimal module is running."; +} diff --git a/outputs/logos-calc-aggregator-module/src/minimal_impl.h b/outputs/logos-calc-aggregator-module/src/minimal_impl.h new file mode 100644 index 0000000..0f4cde2 --- /dev/null +++ b/outputs/logos-calc-aggregator-module/src/minimal_impl.h @@ -0,0 +1,35 @@ +#pragma once + +#include +#include "logos_module_context.h" + +/** + * @brief A minimal universal Logos module. + * + * In the universal authoring model you write only this implementation class. + * Its public methods ARE the module's API — callable by other modules and from + * the CLI (`logoscore -c`). The Qt plugin glue (the `*Plugin`/`*Interface` + * classes, `Q_PLUGIN_METADATA`, `initLogos` wiring) is generated from this + * header by `logos-module-builder`. + * + * Deriving `LogosModuleContext` gives you: + * - `modules()` — typed callers for anything in `metadata.json#dependencies` + * - typed event subscriptions (`modules().dep.on(...)`) + * - `onContextReady()` — override it to run once the module is wired + * + * Module code is Qt-free: use `std::string` and friends, not `QString`. + */ +class MinimalImpl : public LogosModuleContext +{ +public: + /// Returns a greeting and announces it as a typed `greeted` event. + std::string greet(const std::string& name); + + /// Returns a short status string. + std::string getStatus(); + +logos_events: + /// Emitted by greet() with the greeting it produced. Other modules + /// subscribe with `modules().minimal.onGreeted(...)`. + void greeted(const std::string& greeting); +}; diff --git a/outputs/logos-calc-module/flake.nix b/outputs/logos-calc-module/flake.nix index d889497..37b716a 100644 --- a/outputs/logos-calc-module/flake.nix +++ b/outputs/logos-calc-module/flake.nix @@ -2,7 +2,7 @@ description = "Calculator module - wraps libcalc C library for Logos"; inputs = { - logos-module-builder.url = "github:logos-co/logos-module-builder"; + logos-module-builder.url = "github:logos-co/logos-module-builder/0.2.0"; }; outputs = inputs@{ logos-module-builder, ... }: diff --git a/outputs/logos-calc-module/src/calc_module_impl.h b/outputs/logos-calc-module/src/calc_module_impl.h index 7dca43d..7ba6772 100644 --- a/outputs/logos-calc-module/src/calc_module_impl.h +++ b/outputs/logos-calc-module/src/calc_module_impl.h @@ -18,14 +18,37 @@ public: // ── Public API — every method here is callable over IPC ────────── // The generator maps C++ types onto the wire automatically: // int64_t ↔ int std::string ↔ QString bool ↔ bool + // + // A doc comment directly above a method becomes that method's + // `description` in the module's method introspection — surfaced + // by `lm`, `logoscore module-info`, and Basecamp's Methods list. + // Use `///` (one or more lines) or a `/** ... */` block; the + // comment's line breaks are preserved. (Plain `//` comments like + // this block are ignored, so they never leak into the API.) + + /// Adds two integers and returns the sum. int64_t add(int64_t a, int64_t b); + + /// Multiplies two integers and returns the product. int64_t multiply(int64_t a, int64_t b); + + // A multi-line description: consecutive `///` lines keep their breaks. + /// Computes the factorial n! of a non-negative integer. + /// Defined as n * (n-1) * ... * 1, with 0! = 1. int64_t factorial(int64_t n); + + /// Returns the nth Fibonacci number (0-indexed). int64_t fibonacci(int64_t n); + + // A `/** ... */` block comment works too (line breaks preserved). + /** + * Returns the version string of the wrapped libcalc C library. + * Read straight from the linked native library, not metadata.json. + */ std::string libVersion(); - // Fire-and-forget: looks up the version, then emits it as an event - // instead of returning it. Used by the QML tutorial (Part 2). + /// Looks up the library version and emits it as a `versionReady` + /// event instead of returning it. Used by the QML tutorial (Part 2). void libVersionNotify(); // ── Events ─────────────────────────────────────────────────────── @@ -33,6 +56,12 @@ public: // calc_module_events.cpp) that routes the typed args to subscribers // via the host's `eventResponse` mechanism. QML subscribes with // logos.onModuleEvent("calc_module", "versionReady"). + // + // A `///` doc comment documents the event too — it surfaces as the + // event's `description` alongside methods (`lm events`, `logoscore + // module-info`, and Basecamp's Interface screen). logos_events: + /// Emitted by libVersionNotify() once the library version is known. + /// Carries the version string read from libcalc. void versionReady(const std::string& version); }; diff --git a/outputs/logos-calc-module/src/external_lib_impl.cpp b/outputs/logos-calc-module/src/external_lib_impl.cpp new file mode 100644 index 0000000..c4b474f --- /dev/null +++ b/outputs/logos-calc-module/src/external_lib_impl.cpp @@ -0,0 +1,61 @@ +#include "external_lib_impl.h" + +// External library C API declarations. +// In a real module these come from the library's header file: +// extern "C" { +// void* example_init(const char* config); +// const char* example_process(void* handle, const char* input); +// void example_cleanup(void* handle); +// void example_free_string(const char* str); +// } + +ExternalLibImpl::~ExternalLibImpl() +{ + cleanup(); +} + +bool ExternalLibImpl::initLibrary(const std::string& config) +{ + if (m_initialized) { + return true; + } + + // In a real module you would call the external library's init function: + // m_libHandle = example_init(config.c_str()); + // if (!m_libHandle) return false; + (void)config; + + // For this template we just simulate success. + m_libHandle = reinterpret_cast(1); // Placeholder + m_initialized = true; + return true; +} + +std::string ExternalLibImpl::processData(const std::string& input) +{ + if (!m_initialized) { + return std::string(); + } + + // In a real module you would call the external library: + // const char* result = example_process(m_libHandle, input.c_str()); + // std::string output(result); + // example_free_string(result); // don't forget to free! + // return output; + + // For this template we return a placeholder result. + return "Processed: " + input; +} + +void ExternalLibImpl::cleanup() +{ + if (!m_initialized) { + return; + } + + // In a real module you would release the external library: + // if (m_libHandle) { example_cleanup(m_libHandle); m_libHandle = nullptr; } + + m_libHandle = nullptr; + m_initialized = false; +} diff --git a/outputs/logos-calc-module/src/external_lib_impl.h b/outputs/logos-calc-module/src/external_lib_impl.h new file mode 100644 index 0000000..bb85f79 --- /dev/null +++ b/outputs/logos-calc-module/src/external_lib_impl.h @@ -0,0 +1,37 @@ +#pragma once + +#include +#include "logos_module_context.h" + +// Include the external library header here. This would be the actual C header +// from the library you are wrapping: +// #include "lib/libexample.h" + +/** + * @brief A universal Logos module that wraps an external C/C++ library. + * + * You write only this implementation class — its public methods are the + * module's API. The Qt plugin glue is generated from this header. Deriving + * `LogosModuleContext` gives `modules()` (typed callers for dependencies), + * typed event subscriptions, and `onContextReady()`. + * + * Module code is Qt-free: use `std::string`, not `QString`. + */ +class ExternalLibImpl : public LogosModuleContext +{ +public: + ~ExternalLibImpl(); + + /// Initialize the external library. Returns true on success. + bool initLibrary(const std::string& config); + + /// Call into the external library and return its result. + std::string processData(const std::string& input); + + /// Release the external library's resources. + void cleanup(); + +private: + void* m_libHandle = nullptr; // Handle to the external library context + bool m_initialized = false; +}; diff --git a/outputs/logos-calc-ui-cpp/flake.nix b/outputs/logos-calc-ui-cpp/flake.nix index 61af2b9..4f892a4 100644 --- a/outputs/logos-calc-ui-cpp/flake.nix +++ b/outputs/logos-calc-ui-cpp/flake.nix @@ -2,7 +2,7 @@ description = "Calculator C++ UI plugin for Logos - QML view with process-isolated backend for calc_module"; inputs = { - logos-module-builder.url = "github:logos-co/logos-module-builder"; + logos-module-builder.url = "github:logos-co/logos-module-builder/0.2.0"; # Points at your local calc_module checkout. This is a placeholder — # you lock it to your actual path in the next step with diff --git a/outputs/logos-calc-ui/flake.nix b/outputs/logos-calc-ui/flake.nix index cf8e8de..1a484e9 100644 --- a/outputs/logos-calc-ui/flake.nix +++ b/outputs/logos-calc-ui/flake.nix @@ -2,7 +2,7 @@ description = "Calculator QML UI Plugin for Logos - frontend for calc_module"; inputs = { - logos-module-builder.url = "github:logos-co/logos-module-builder"; + logos-module-builder.url = "github:logos-co/logos-module-builder/0.2.0"; # Points at your local calc_module checkout. This is a placeholder — # you lock it to your actual path in the next step with diff --git a/outputs/logos-calc-via-interface-module/.gitignore b/outputs/logos-calc-via-interface-module/.gitignore new file mode 100644 index 0000000..71cba30 --- /dev/null +++ b/outputs/logos-calc-via-interface-module/.gitignore @@ -0,0 +1,6 @@ +# Nix build output +result +result-* + +# CMake build directory +build/ diff --git a/outputs/logos-calc-via-interface-module/CMakeLists.txt b/outputs/logos-calc-via-interface-module/CMakeLists.txt new file mode 100644 index 0000000..c3877d1 --- /dev/null +++ b/outputs/logos-calc-via-interface-module/CMakeLists.txt @@ -0,0 +1,17 @@ +cmake_minimum_required(VERSION 3.14) +project(CalcViaInterfacePlugin LANGUAGES CXX) + +if(DEFINED ENV{LOGOS_MODULE_BUILDER_ROOT}) + include($ENV{LOGOS_MODULE_BUILDER_ROOT}/cmake/LogosModule.cmake) +elseif(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/cmake/LogosModule.cmake") + include(cmake/LogosModule.cmake) +else() + message(FATAL_ERROR "LogosModule.cmake not found") +endif() + +logos_module( + NAME calc_via_interface + SOURCES + src/calc_via_interface_impl.h + src/calc_via_interface_impl.cpp +) diff --git a/outputs/logos-calc-via-interface-module/flake.nix b/outputs/logos-calc-via-interface-module/flake.nix new file mode 100644 index 0000000..cd77f53 --- /dev/null +++ b/outputs/logos-calc-via-interface-module/flake.nix @@ -0,0 +1,14 @@ +{ + description = "Core module that binds a calculator interface at runtime"; + + inputs = { + logos-module-builder.url = "github:logos-co/logos-module-builder/0.2.0"; + }; + + outputs = inputs@{ logos-module-builder, ... }: + logos-module-builder.lib.mkLogosModule { + src = ./.; + configFile = ./metadata.json; + flakeInputs = inputs; + }; +} diff --git a/outputs/logos-calc-via-interface-module/interfaces/calculator.h b/outputs/logos-calc-via-interface-module/interfaces/calculator.h new file mode 100644 index 0000000..e332379 --- /dev/null +++ b/outputs/logos-calc-via-interface-module/interfaces/calculator.h @@ -0,0 +1,34 @@ +#pragma once + +// A DEPENDENCY INTERFACE: a method/event contract that names no +// module. Any module whose API is a superset of this can satisfy +// it; the consumer binds it to a concrete module name at runtime. +// +// Written in the module's own language (pure C++). The generator +// reads the public methods + the `logos_events:` block and emits a +// BOUND wrapper class `Calculator` whose target module name is a +// runtime constructor argument — not baked in. +// +// Types are std (int64_t / std::string) because the consuming +// module is `interface: "universal"`; the bound wrapper inherits +// that api-style. + +#include +#include + +// Defines the `logos_events` token (expands to `public`) so this +// header is valid C++ on its own, not only as generator input. +#include + +class ICalculator { +public: + int64_t add(int64_t a, int64_t b); + int64_t multiply(int64_t a, int64_t b); + int64_t fibonacci(int64_t n); + std::string libVersion(); + +logos_events: + // Emitted by the provider; the consumer subscribes through the + // bound wrapper's generated onVersionReady(...) accessor. + void versionReady(const std::string& version); +}; diff --git a/outputs/logos-calc-via-interface-module/metadata.json b/outputs/logos-calc-via-interface-module/metadata.json new file mode 100644 index 0000000..4d8287d --- /dev/null +++ b/outputs/logos-calc-via-interface-module/metadata.json @@ -0,0 +1,27 @@ +{ + "name": "calc_via_interface", + "version": "1.0.0", + "type": "core", + "category": "general", + "description": "Binds a calculator interface to a module chosen at runtime", + "main": "calc_via_interface_plugin", + "interface": "universal", + "dependencies": [], + "interface_dependencies": [ + { "name": "calculator", "file": "interfaces/calculator.h", "impl_class": "ICalculator" } + ], + + "nix": { + "packages": { + "build": [], + "runtime": [] + }, + "external_libraries": [], + "cmake": { + "find_packages": [], + "extra_sources": [], + "extra_include_dirs": [], + "extra_link_libraries": [] + } + } +} diff --git a/outputs/logos-calc-via-interface-module/src/calc_via_interface_impl.cpp b/outputs/logos-calc-via-interface-module/src/calc_via_interface_impl.cpp new file mode 100644 index 0000000..22847e3 --- /dev/null +++ b/outputs/logos-calc-via-interface-module/src/calc_via_interface_impl.cpp @@ -0,0 +1,65 @@ +#include "calc_via_interface_impl.h" + +// Generated at build time by logos-cpp-generator. Because +// metadata.json lists `interface_dependencies`, LogosModules gains a +// `bind_calculator(moduleName)` factory returning the bound +// `Calculator` wrapper. Included only in the .cpp so the impl header +// the generator parses stays free of generated types. +#include "logos_sdk.h" + +// ── Synchronous binds ─────────────────────────────────────────────── + +int64_t CalcViaInterfaceImpl::sumVia(const std::string& provider, + int64_t a, int64_t b) { + // Bind once, then call normally — no module name on the call. + // Every generated method also takes an optional trailing + // logos::CallError* — the explicit way to tell a failed remote + // call apart from a legitimate result (without it, a failed + // call returns the type's default and only logs a warning). + auto calc = modules().bind_calculator(provider); + logos::CallError err; + const int64_t sum = calc.add(a, b, &err); + if (!err.ok()) return -1; // e.g. the bound module isn't loaded + return sum; +} + +int64_t CalcViaInterfaceImpl::productVia(const std::string& provider, + int64_t a, int64_t b) { + return modules().bind_calculator(provider).multiply(a, b); +} + +std::string CalcViaInterfaceImpl::versionVia(const std::string& provider) { + return modules().bind_calculator(provider).libVersion(); +} + +// ── Asynchronous bind ──────────────────────────────────────────────── + +std::string CalcViaInterfaceImpl::startFibVia(const std::string& provider, + int64_t n) { + // The generated async overload is `Async(args..., + // callback, timeout)`. It returns immediately; the reply lands in + // the callback on this module's event loop. The bound handle is a + // temporary, but the call is registered on the LogosAPI-owned + // client and the callback captures `this`, so it outlives it. + modules().bind_calculator(provider).fibonacciAsync(n, + [this](int64_t value) { m_lastFib = value; }); + return "queued"; +} + +int64_t CalcViaInterfaceImpl::lastFib() const { + return m_lastFib; +} + +// ── Event subscription ─────────────────────────────────────────────── + +std::string CalcViaInterfaceImpl::watchVersion(const std::string& provider) { + // onVersionReady(...) is generated from the interface's + // `logos_events:` block; the callback's arg type matches the event. + bool ok = modules().bind_calculator(provider).onVersionReady( + [this](const std::string& version) { m_lastVersion = version; }); + return ok ? "ok" : "failed"; +} + +std::string CalcViaInterfaceImpl::lastVersion() const { + return m_lastVersion; +} diff --git a/outputs/logos-calc-via-interface-module/src/calc_via_interface_impl.h b/outputs/logos-calc-via-interface-module/src/calc_via_interface_impl.h new file mode 100644 index 0000000..66b707c --- /dev/null +++ b/outputs/logos-calc-via-interface-module/src/calc_via_interface_impl.h @@ -0,0 +1,40 @@ +#pragma once + +#include +#include + +#include // LogosModuleContext base → modules() + +// Binds the `calculator` interface (interfaces/calculator.h) to a +// module name chosen at runtime and calls it through the generated, +// type-safe bound wrapper. It names no concrete module of its own — +// the provider is whatever string you pass in. +class CalcViaInterfaceImpl : public LogosModuleContext { +public: + CalcViaInterfaceImpl() = default; + ~CalcViaInterfaceImpl() = default; + + // ── Synchronous binds ────────────────────────────────────── + // Bind `calculator` to `provider`, then call it. The module + // name appears only at bind time, never on the call. + int64_t sumVia(const std::string& provider, int64_t a, int64_t b); + int64_t productVia(const std::string& provider, int64_t a, int64_t b); + std::string versionVia(const std::string& provider); + + // ── Asynchronous bind ────────────────────────────────────── + // Fire calculator.fibonacci(n) asynchronously against `provider` + // and return immediately ("queued"). Read the reply later with + // lastFib(). + std::string startFibVia(const std::string& provider, int64_t n); + int64_t lastFib() const; + + // ── Event subscription ───────────────────────────────────── + // Subscribe to the interface's `versionReady` event on + // `provider` via the generated onVersionReady(...) accessor. + std::string watchVersion(const std::string& provider); + std::string lastVersion() const; + +private: + int64_t m_lastFib = -1; + std::string m_lastVersion; +}; diff --git a/outputs/logos-calc-via-interface-module/src/minimal_impl.cpp b/outputs/logos-calc-via-interface-module/src/minimal_impl.cpp new file mode 100644 index 0000000..ff94646 --- /dev/null +++ b/outputs/logos-calc-via-interface-module/src/minimal_impl.cpp @@ -0,0 +1,16 @@ +#include "minimal_impl.h" + +std::string MinimalImpl::greet(const std::string& name) +{ + std::string greeting = "Hello, " + name + "! Greetings from the minimal module."; + + // The generated event body routes the typed payload to every subscriber. + greeted(greeting); + + return greeting; +} + +std::string MinimalImpl::getStatus() +{ + return "Minimal module is running."; +} diff --git a/outputs/logos-calc-via-interface-module/src/minimal_impl.h b/outputs/logos-calc-via-interface-module/src/minimal_impl.h new file mode 100644 index 0000000..0f4cde2 --- /dev/null +++ b/outputs/logos-calc-via-interface-module/src/minimal_impl.h @@ -0,0 +1,35 @@ +#pragma once + +#include +#include "logos_module_context.h" + +/** + * @brief A minimal universal Logos module. + * + * In the universal authoring model you write only this implementation class. + * Its public methods ARE the module's API — callable by other modules and from + * the CLI (`logoscore -c`). The Qt plugin glue (the `*Plugin`/`*Interface` + * classes, `Q_PLUGIN_METADATA`, `initLogos` wiring) is generated from this + * header by `logos-module-builder`. + * + * Deriving `LogosModuleContext` gives you: + * - `modules()` — typed callers for anything in `metadata.json#dependencies` + * - typed event subscriptions (`modules().dep.on(...)`) + * - `onContextReady()` — override it to run once the module is wired + * + * Module code is Qt-free: use `std::string` and friends, not `QString`. + */ +class MinimalImpl : public LogosModuleContext +{ +public: + /// Returns a greeting and announces it as a typed `greeted` event. + std::string greet(const std::string& name); + + /// Returns a short status string. + std::string getStatus(); + +logos_events: + /// Emitted by greet() with the greeting it produced. Other modules + /// subscribe with `modules().minimal.onGreeted(...)`. + void greeted(const std::string& greeting); +}; diff --git a/outputs/tutorial-composing-modules.md b/outputs/tutorial-composing-modules.md index 2084b3b..2a38454 100644 --- a/outputs/tutorial-composing-modules.md +++ b/outputs/tutorial-composing-modules.md @@ -37,7 +37,7 @@ Create a new directory and initialise it from the minimal module template: ### 1.1 Create the project from the template ```bash -nix flake init -t github:logos-co/logos-module-builder +nix flake init -t github:logos-co/logos-module-builder/0.2.0 ``` This scaffolds a `flake.nix`, `metadata.json`, `CMakeLists.txt`, and a `src/` directory pre-wired for `logos-module-builder`. As in Part 1 we use the newer **pure-C++ (`interface: universal`) pattern**, so we replace the template's example `src/` files with a single plain `*_impl.h` / `*_impl.cpp` class. @@ -130,7 +130,7 @@ Declare `calc_module` as a flake input. The input attribute name **must match** description = "Aggregator core module - composes calc_module and showcases LogosModuleContext"; inputs = { - logos-module-builder.url = "github:logos-co/logos-module-builder"; + logos-module-builder.url = "github:logos-co/logos-module-builder/0.2.0"; # The module this one depends on. Placeholder path — locked to your # real checkout in the build step via `--override-input`. @@ -455,7 +455,7 @@ Use `lm` to confirm the dependency and the public API made it into the binary. ### 5.1 Build `lm` ```bash -nix build 'github:logos-co/logos-module#lm' --out-link ./lm +nix build 'github:logos-co/logos-module/0.2.0#lm' --out-link ./lm ``` ### 5.2 View metadata — note the dependency @@ -498,11 +498,11 @@ Now the payoff: run `calc_aggregator` **and** its `calc_module` dependency under Build `logoscore` and the package manager, then install **both** modules into a `modules/` directory `logoscore` can scan. The aggregator comes from this project; `calc_module` from your Part 1 checkout: ```bash -nix build 'github:logos-co/logos-logoscore-cli' --out-link ./logos +nix build 'github:logos-co/logos-logoscore-cli/0.2.0' --out-link ./logos ``` ```bash -nix build 'github:logos-co/logos-package-manager#cli' --out-link ./pm +nix build 'github:logos-co/logos-package-manager/0.2.0#cli' --out-link ./pm ``` ```bash diff --git a/outputs/tutorial-cpp-ui-app.md b/outputs/tutorial-cpp-ui-app.md index 32f67c7..96bba1f 100644 --- a/outputs/tutorial-cpp-ui-app.md +++ b/outputs/tutorial-cpp-ui-app.md @@ -86,7 +86,7 @@ Create a new directory and initialise it from the C++ backend UI template: `mkdir logos-calc-ui-cpp && cd logos-calc-ui-cpp` ```bash -nix flake init -t github:logos-co/logos-module-builder#ui-qml-backend +nix flake init -t github:logos-co/logos-module-builder/0.2.0#ui-qml-backend ``` This scaffolds the **universal** UI backend template: a `metadata.json` with `"interface": "universal"`, an example `.rep` (`src/ui_example.rep`), and a single `*Backend` class (`src/ui_example_backend.h` / `.cpp`) — no hand-written interface or plugin files. We'll replace the `ui_example` files with our calculator's `.rep` + backend. @@ -698,7 +698,7 @@ The template already wires everything up. Update the description and point `calc description = "Calculator C++ UI plugin for Logos - QML view with process-isolated backend for calc_module"; inputs = { - logos-module-builder.url = "github:logos-co/logos-module-builder"; + logos-module-builder.url = "github:logos-co/logos-module-builder/0.2.0"; # Points at your local calc_module checkout. This is a placeholder — # you lock it to your actual path in the next step with diff --git a/outputs/tutorial-interface-dependencies.md b/outputs/tutorial-interface-dependencies.md new file mode 100644 index 0000000..6c2f0ac --- /dev/null +++ b/outputs/tutorial-interface-dependencies.md @@ -0,0 +1,604 @@ +# Tutorial: Dependency Interfaces — Bind a Module by Contract + +This tutorial builds `calc_via_interface`, a **core module that depends on an *interface*, not a concrete module**. Instead of naming `calc_module` (from [Part 1](tutorial-wrapping-c-library.md)) as a dependency and getting a fixed `modules().calc_module` wrapper, it declares a small **`calculator` interface** — a list of methods and one event — and **binds that interface to a module name chosen at runtime**. Any module whose API is a *superset* of the interface can satisfy it; `calc_module` is one such provider. You drive the whole thing from `logoscore` on the command line. + +**What you'll build:** A `calc_via_interface` core module that: + +- declares a **`calculator` interface** in its own language (a pure-C++ header with a `logos_events:` block) — `interfaces/calculator.h` +- lists it under `metadata.json`'s `interface_dependencies` — and names **no concrete module** in `dependencies` +- **binds** the interface to a runtime-chosen module with `modules().bind_calculator("calc_module")`, then calls it through the usual type-safe wrappers — **synchronously** (`add`, `multiply`, `libVersion`), **asynchronously** (`fibonacciAsync` + callback), and via a typed **event** subscription (`onVersionReady`) +- proves the **no-validation** contract: binding to a module that does not satisfy the interface fails as an ordinary remote-call error — no crash + +No Qt, no `LogosAPI`, no plugin boilerplate — one plain C++ class, plus a one-file interface contract. + +**What you'll learn:** + +- The difference between a concrete dependency (`dependencies`) and a dependency interface (`interface_dependencies`) +- How to declare an interface in pure C++ (methods + a `logos_events:` block) — or equivalently in `.lidl` +- How the generator turns an interface into a **bound** wrapper whose target module is a constructor argument, exposed as `modules().bind_(moduleName)` +- How to call a bound interface **synchronously** and **asynchronously**, and how to subscribe to its events — all type-safely +- Why binding is decoupled from loading, and what the "superset" / no-validation rule means in practice +- How to share one interface across repos via a flake input (the same wiring `dependencies` use) + +## Prerequisites + +- Completed [Part 1](tutorial-wrapping-c-library.md) — you have a working `calc_module` whose shared library is built (`libcalc.so`/`.dylib` in `logos-calc-module/lib/`). This tutorial only needs `calc_module` as a *runtime* provider; it is never named at build time. +- Nix with flakes enabled +- Basic familiarity with C++ + +--- + +## Step 1: Scaffold the Module Project + +Create a new directory and initialise it from the minimal module template: + +`mkdir logos-calc-via-interface-module && cd logos-calc-via-interface-module` + +### 1.1 Create the project from the template + +```bash +nix flake init -t github:logos-co/logos-module-builder/0.2.0 +``` + +This scaffolds a `flake.nix`, `metadata.json`, `CMakeLists.txt`, and a `src/` directory pre-wired for `logos-module-builder`. As in Part 1 we use the **pure-C++ (`interface: universal`) pattern**, so we replace the template's example `src/` files with our own plain `*_impl.h` / `*_impl.cpp`. + +### 1.2 Remove the template's example sources + +Delete the example Qt plugin the template ships — this tutorial supplies its own pure-C++ `src/` files: + +```bash +rm -f src/minimal_interface.h src/minimal_plugin.h src/minimal_plugin.cpp +``` + +--- + +## Step 2: Declare the Interface + +An **interface** is a method/event contract decoupled from any concrete module. You write it in the *same language as your module* — for a universal module that's a plain C++ header. It looks like an impl class, but you only declare signatures: the generator reads them to build a typed client. + +Put it in an `interfaces/` directory: + +### 2.1 `interfaces/calculator.h` — the contract + +The `calculator` interface names four methods and one event. It is deliberately a **subset** of what `calc_module` exposes (which also has `factorial`, `libVersionNotify`, …) — that is the *superset rule*: a provider may expose more than the interface requires. + +```cpp +#pragma once + +// A DEPENDENCY INTERFACE: a method/event contract that names no +// module. Any module whose API is a superset of this can satisfy +// it; the consumer binds it to a concrete module name at runtime. +// +// Written in the module's own language (pure C++). The generator +// reads the public methods + the `logos_events:` block and emits a +// BOUND wrapper class `Calculator` whose target module name is a +// runtime constructor argument — not baked in. +// +// Types are std (int64_t / std::string) because the consuming +// module is `interface: "universal"`; the bound wrapper inherits +// that api-style. + +#include +#include + +// Defines the `logos_events` token (expands to `public`) so this +// header is valid C++ on its own, not only as generator input. +#include + +class ICalculator { +public: + int64_t add(int64_t a, int64_t b); + int64_t multiply(int64_t a, int64_t b); + int64_t fibonacci(int64_t n); + std::string libVersion(); + +logos_events: + // Emitted by the provider; the consumer subscribes through the + // bound wrapper's generated onVersionReady(...) accessor. + void versionReady(const std::string& version); +}; +``` + +A few things to notice: + +- The class name (`ICalculator`) and method signatures are all the generator needs — no `LogosAPI`, no Qt, no reference to any concrete module. The one include (`logos_module_context.h`) just defines the `logos_events` token so the header is valid C++ on its own. +- `logos_events:` (like Qt's `signals:`) marks event declarations. The generator turns each into a typed `on(callback)` subscriber on the bound wrapper. +- You could write the exact same contract as a `.lidl` file instead — `interfaces/calculator.lidl` with `method add(a: int, b: int) -> int` … `event versionReady(version: tstr)`. The `.h` form is shown here because it matches a universal module's own language. + +--- + +## Step 3: Configure the Module + +Three config files declare the module, point it at the interface, and tell CMake how to build it. The key contrast with [Composing Modules](tutorial-composing-modules.md): there is **no concrete module** in `dependencies`. + +### 3.1 `metadata.json` — declare the interface dependency + +`interface_dependencies` lists the contracts this module binds at runtime. Each entry gives the interface `name` and the `file` that defines it (and, for a `.h` file, the `impl_class` whose signatures define the contract). `dependencies` stays **empty** — we never name `calc_module` at build time. + +```json +{ + "name": "calc_via_interface", + "version": "1.0.0", + "type": "core", + "category": "general", + "description": "Binds a calculator interface to a module chosen at runtime", + "main": "calc_via_interface_plugin", + "interface": "universal", + "dependencies": [], + "interface_dependencies": [ + { "name": "calculator", "file": "interfaces/calculator.h", "impl_class": "ICalculator" } + ], + + "nix": { + "packages": { + "build": [], + "runtime": [] + }, + "external_libraries": [], + "cmake": { + "find_packages": [], + "extra_sources": [], + "extra_include_dirs": [], + "extra_link_libraries": [] + } + } +} +``` + +| Field | What it does | +| ------------------------ | ----------------------------------------------------------------------------------------------------------------------------- | +| `interface` | `"universal"` — pure C++ impl, the builder generates the Qt plugin glue | +| `dependencies` | `[]` — **no concrete module** is named at build time | +| `interface_dependencies` | `[{ name, file, impl_class }]` — the builder generates the bound wrapper `Calculator` + the `modules().bind_calculator(...)` factory | + +For a `.h` interface the `impl_class` field is required (the class whose signatures define the contract). For a `.lidl` interface, omit it. To pull an interface from **another repo**, add an `"input"` field naming a flake input — covered in the final step. + +### 3.2 `CMakeLists.txt` — list your sources + +You list only your plain C++ files. The generated interface wrapper and plugin glue are compiled automatically. + +```cmake +cmake_minimum_required(VERSION 3.14) +project(CalcViaInterfacePlugin LANGUAGES CXX) + +if(DEFINED ENV{LOGOS_MODULE_BUILDER_ROOT}) + include($ENV{LOGOS_MODULE_BUILDER_ROOT}/cmake/LogosModule.cmake) +elseif(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/cmake/LogosModule.cmake") + include(cmake/LogosModule.cmake) +else() + message(FATAL_ERROR "LogosModule.cmake not found") +endif() + +logos_module( + NAME calc_via_interface + SOURCES + src/calc_via_interface_impl.h + src/calc_via_interface_impl.cpp +) +``` + +`NAME` must match `name` in `metadata.json` (`calc_via_interface`). No module dependency to wire here — the interface file is local to this repo. + +### 3.3 `flake.nix` — no module inputs needed + +Because there is no concrete dependency, the only input is the builder itself. (Contrast with Composing Modules, which had to add a `calc_module.url` input.) + +```nix +{ + description = "Core module that binds a calculator interface at runtime"; + + inputs = { + logos-module-builder.url = "github:logos-co/logos-module-builder/0.2.0"; + }; + + outputs = inputs@{ logos-module-builder, ... }: + logos-module-builder.lib.mkLogosModule { + src = ./.; + configFile = ./metadata.json; + flakeInputs = inputs; + }; +} +``` + +`flakeInputs = inputs` hands the builder everything it needs. It reads `interface_dependencies` from `metadata.json`, resolves the local `interfaces/calculator.h`, and runs `logos-cpp-generator` to emit the bound `Calculator` wrapper into `generated_code/`. + +--- + +## Step 4: Write the Module Class + +The module is one plain C++ class inheriting `LogosModuleContext` — that base gives it `modules()`, through which the generated `bind_calculator(name)` factory is reachable. Each method takes the **provider module name** as its first argument, so we can bind to different modules at runtime from `logoscore`. + +### 4.1 `src/calc_via_interface_impl.h` — the class + +Every `public` method becomes callable over IPC. They fall into three groups: synchronous binds, an asynchronous bind, and an event subscription. + +```cpp +#pragma once + +#include +#include + +#include // LogosModuleContext base → modules() + +// Binds the `calculator` interface (interfaces/calculator.h) to a +// module name chosen at runtime and calls it through the generated, +// type-safe bound wrapper. It names no concrete module of its own — +// the provider is whatever string you pass in. +class CalcViaInterfaceImpl : public LogosModuleContext { +public: + CalcViaInterfaceImpl() = default; + ~CalcViaInterfaceImpl() = default; + + // ── Synchronous binds ────────────────────────────────────── + // Bind `calculator` to `provider`, then call it. The module + // name appears only at bind time, never on the call. + int64_t sumVia(const std::string& provider, int64_t a, int64_t b); + int64_t productVia(const std::string& provider, int64_t a, int64_t b); + std::string versionVia(const std::string& provider); + + // ── Asynchronous bind ────────────────────────────────────── + // Fire calculator.fibonacci(n) asynchronously against `provider` + // and return immediately ("queued"). Read the reply later with + // lastFib(). + std::string startFibVia(const std::string& provider, int64_t n); + int64_t lastFib() const; + + // ── Event subscription ───────────────────────────────────── + // Subscribe to the interface's `versionReady` event on + // `provider` via the generated onVersionReady(...) accessor. + std::string watchVersion(const std::string& provider); + std::string lastVersion() const; + +private: + int64_t m_lastFib = -1; + std::string m_lastVersion; +}; +``` + +The provider name is a plain `std::string` parameter — that is the whole "bind at runtime" idea. The same handle code works for any module that satisfies `calculator`. + +### 4.2 `src/calc_via_interface_impl.cpp` — the implementation + +The `.cpp` includes the generated `logos_sdk.h` (which defines `LogosModules` and the `bind_calculator` factory), so the bind/call sites live here rather than in the header the generator parses. + +```cpp +#include "calc_via_interface_impl.h" + +// Generated at build time by logos-cpp-generator. Because +// metadata.json lists `interface_dependencies`, LogosModules gains a +// `bind_calculator(moduleName)` factory returning the bound +// `Calculator` wrapper. Included only in the .cpp so the impl header +// the generator parses stays free of generated types. +#include "logos_sdk.h" + +// ── Synchronous binds ─────────────────────────────────────────────── + +int64_t CalcViaInterfaceImpl::sumVia(const std::string& provider, + int64_t a, int64_t b) { + // Bind once, then call normally — no module name on the call. + // Every generated method also takes an optional trailing + // logos::CallError* — the explicit way to tell a failed remote + // call apart from a legitimate result (without it, a failed + // call returns the type's default and only logs a warning). + auto calc = modules().bind_calculator(provider); + logos::CallError err; + const int64_t sum = calc.add(a, b, &err); + if (!err.ok()) return -1; // e.g. the bound module isn't loaded + return sum; +} + +int64_t CalcViaInterfaceImpl::productVia(const std::string& provider, + int64_t a, int64_t b) { + return modules().bind_calculator(provider).multiply(a, b); +} + +std::string CalcViaInterfaceImpl::versionVia(const std::string& provider) { + return modules().bind_calculator(provider).libVersion(); +} + +// ── Asynchronous bind ──────────────────────────────────────────────── + +std::string CalcViaInterfaceImpl::startFibVia(const std::string& provider, + int64_t n) { + // The generated async overload is `Async(args..., + // callback, timeout)`. It returns immediately; the reply lands in + // the callback on this module's event loop. The bound handle is a + // temporary, but the call is registered on the LogosAPI-owned + // client and the callback captures `this`, so it outlives it. + modules().bind_calculator(provider).fibonacciAsync(n, + [this](int64_t value) { m_lastFib = value; }); + return "queued"; +} + +int64_t CalcViaInterfaceImpl::lastFib() const { + return m_lastFib; +} + +// ── Event subscription ─────────────────────────────────────────────── + +std::string CalcViaInterfaceImpl::watchVersion(const std::string& provider) { + // onVersionReady(...) is generated from the interface's + // `logos_events:` block; the callback's arg type matches the event. + bool ok = modules().bind_calculator(provider).onVersionReady( + [this](const std::string& version) { m_lastVersion = version; }); + return ok ? "ok" : "failed"; +} + +std::string CalcViaInterfaceImpl::lastVersion() const { + return m_lastVersion; +} +``` + +Everything flows through `modules().bind_calculator(provider)` — the factory the builder generated from `interface_dependencies`. There is no `modules().calc_module`, because `calc_module` is never a build-time dependency. The bound `Calculator` exposes the same typed sync/async/event API the name-baked wrappers do; the only difference is the target module is chosen when you bind. + +--- + +## Step 5: Build the Module + +### 5.1 Add a `.gitignore` and init the repo + +Nix flakes require a git repository, and only tracked files are visible — so `interfaces/calculator.h` must be committed for the generator to find it. Exclude build artifacts first: + +```text +# Nix build output +result +result-* + +# CMake build directory +build/ +``` + +Initialise the repo and stage the files (including `interfaces/`): + +```bash +git init && git add -A +``` + +### 5.2 Build + +For a universal module with an interface dependency, this is where `logos-cpp-generator` runs over both `src/calc_via_interface_impl.h` (plugin glue) and `interfaces/calculator.h` (the bound `Calculator` wrapper + `bind_calculator` factory), emitting everything under `generated_code/`: + +```bash +nix build +``` + +### 5.3 Check the output + +```bash +ls -la result/lib/ +``` + +You should see your plugin (extension depends on platform): + +``` +calc_via_interface_plugin.so # Linux +calc_via_interface_plugin.dylib # macOS +``` + +--- + +## Step 6: Inspect the Module + +Use `lm` to confirm the public API made it into the binary — and, tellingly, that there is **no module dependency**. + +### 6.1 Build `lm` + +```bash +nix build 'github:logos-co/logos-module/0.2.0#lm' --out-link ./lm +``` + +### 6.2 View metadata — note the empty dependency list + +```bash +./lm/bin/lm metadata result/lib/calc_via_interface_plugin.so # Linux +./lm/bin/lm metadata result/lib/calc_via_interface_plugin.dylib # macOS +``` + +``` +Plugin Metadata: +================ +Name: calc_via_interface +Version: 1.0.0 +Description: Binds a calculator interface to a module chosen at runtime +Type: core +Dependencies: +``` + +`Dependencies:` is empty — the module is coupled to the `calculator` *contract*, not to any module. + +### 6.3 List methods + +```bash +./lm/bin/lm methods result/lib/calc_via_interface_plugin.so # Linux +./lm/bin/lm methods result/lib/calc_via_interface_plugin.dylib # macOS +``` + +Every `public` method is here. `int64_t` shows up as `int` and `std::string` as `QString` — the wire types the generated glue exposes. + +--- + +## Step 7: Run it with `logoscore` + +Now the payoff: run `calc_via_interface` and bind its `calculator` interface to the real `calc_module` from Part 1. We use the `logoscore` **daemon** (`-D`) so module processes stay alive between `call` commands — needed for the async reply and the event subscription to survive from one call to the next. (Same daemon flow as [Part 1](tutorial-wrapping-c-library.md#step-6-test-with-logoscore) and [Composing Modules](tutorial-composing-modules.md#run-it-with-logoscore).) + +### 7.1 Build the runtime and package both modules + +Build `logoscore` and the package manager, then install **both** modules into a `modules/` directory. `calc_via_interface` comes from this project; `calc_module` from your Part 1 checkout — it is the *provider* we bind to, even though this module never declared it: + +```bash +nix build 'github:logos-co/logos-logoscore-cli/0.2.0' --out-link ./logos +``` + +```bash +nix build 'github:logos-co/logos-package-manager/0.2.0#cli' --out-link ./pm +``` + +```bash +mkdir -p modules +``` + +### 7.2 Install calc_via_interface + +```bash +nix build '.#lgx' --out-link result-iface-lgx +./pm/bin/lgpm --modules-dir ./modules install --file result-iface-lgx/*.lgx +``` + +### 7.3 Install calc_module (the runtime provider) + +Make sure `calc_module`'s shared library is built (from [Part 1](tutorial-wrapping-c-library.md#15-build-the-shared-library)), then package and install it: + +```bash +# Build libcalc if needed (Part 1, Step 1.5): +cd ../logos-calc-module/lib +gcc -shared -fPIC -o libcalc.so libcalc.c # Linux +# gcc -shared -fPIC -o libcalc.dylib libcalc.c # macOS +cd - +``` + +```bash +nix build 'path:../logos-calc-module#lgx' --out-link result-calc-lgx +./pm/bin/lgpm --modules-dir ./modules install --file result-calc-lgx/*.lgx +``` + +`modules/` now holds `calc_via_interface/` and `calc_module/`. Neither knows about the other at build time — they meet only at runtime, through the interface. + +### 7.4 Start the daemon and load both modules + +```bash +./logos/bin/logoscore -D -m ./modules & +``` + +```bash +sleep 4 +``` + +Load the provider and the consumer. The consumer declares no dependency, so we load `calc_module` explicitly: + +```bash +./logos/bin/logoscore load-module calc_module +``` + +```bash +./logos/bin/logoscore load-module calc_via_interface +``` + +### 7.5 Bind and call synchronously + +`sumVia` / `productVia` / `versionVia` each bind `calculator` to the module name you pass, then call through the bound wrapper. Bind to `calc_module`: + +```bash +./logos/bin/logoscore call calc_via_interface sumVia calc_module 3 5 +``` + +```bash +./logos/bin/logoscore call calc_via_interface productVia calc_module 3 5 +``` + +```bash +./logos/bin/logoscore call calc_via_interface versionVia calc_module +``` + +`sumVia(calc_module, 3, 5) = 8`, `productVia(calc_module, 3, 5) = 15`, and `versionVia(calc_module) = "1.0.0"` — all through `modules().bind_calculator("calc_module")`, with `calc_module` chosen at call time. + +### 7.6 Bind and call asynchronously + +`startFibVia` fires `calculator.fibonacci(n)` asynchronously against the bound module and returns `"queued"`. The reply arrives on the daemon's event loop; `lastFib()` reads it. With `n = 20`, `fib(20) = 6765`: + +```bash +./logos/bin/logoscore call calc_via_interface startFibVia calc_module 20 +``` + +```bash +sleep 1 +``` + +```bash +./logos/bin/logoscore call calc_via_interface lastFib +``` + +The bound wrapper's generated `fibonacciAsync(..., callback)` delivered `6765` to the callback after `startFibVia` had already returned — the typed **async** path, over a runtime-bound interface. + +### 7.7 Subscribe to a bound interface event + +`watchVersion` subscribes to the interface's `versionReady` event on the bound module. `calc_module.libVersionNotify()` makes `calc_module` emit it, and `lastVersion()` reads what the typed callback captured: + +```bash +./logos/bin/logoscore call calc_via_interface watchVersion calc_module +``` + +```bash +./logos/bin/logoscore call calc_module libVersionNotify +``` + +```bash +sleep 1 +``` + +```bash +./logos/bin/logoscore call calc_via_interface lastVersion +``` + +`watchVersion` registered the callback via the generated `onVersionReady(...)`; the event fired in between; `lastVersion()` returned `1.0.0` — a typed event subscription on a runtime-bound interface. + +### 7.8 Bind to a non-satisfying module (the no-validation rule) + +Binding does **not** validate that the target satisfies the interface — there is no build-time coupling to check against. A bad bind isn't caught at bind time; it surfaces when you **call** through it — no crash, and the daemon keeps serving. `sumVia` checks the wrapper's `logos::CallError` out-parameter (see its implementation above) and returns `-1` when the inner call fails; on a transport that fails slowly the outer call may instead time out (`RPC_FAILED` / `"status":"error"`). Either way `calc_module` keeps answering (we keep going with `|| true` so the tour continues): + +```bash +./logos/bin/logoscore call calc_via_interface sumVia no_such_module 3 5 2>&1 || true +``` + +The bound `no_such_module` couldn't be resolved, so the inner `add` call failed — exactly like any other call to an absent module. No crash, no conformance check. Because `sumVia` passes a `logos::CallError*`, it *sees* the failure (`err.code == "object_unavailable"`) and maps it to its own error convention; a call without the out-parameter would get the type's default value plus a warning in the module log. Swapping providers is just changing the string: `sumVia calc_module 3 5` returns `8`; `sumVia no_such_module 3 5` fails. **Any** module that really exposes `add`/`multiply`/`fibonacci`/`libVersion`/`versionReady` satisfies `calculator` and slots in unchanged. + +```bash +./logos/bin/logoscore stop +``` + +That completes the tour: a single interface, bound at runtime to a concrete module, driven type-safely for sync calls, async calls, and events — with no build-time dependency on the provider. + +--- + +## Step 8: Share an Interface Across Repos + +So far `interfaces/calculator.h` lived in this repo. To let *several* modules depend on the **same** contract, move it to its own repo (or a provider repo that publishes the interface it implements) and pull it in as a flake input — exactly how concrete `dependencies` are wired. + +Add an `"input"` field to the `interface_dependencies` entry, naming a flake input, with `file` relative to that input's root: + +```json +"interface_dependencies": [ + { "name": "calculator", "input": "calc_interfaces", "file": "interfaces/calculator.h", "impl_class": "ICalculator" } +] +``` + +and declare the matching input in `flake.nix` (the input attribute name must equal the `input` value): + +```nix +inputs = { + logos-module-builder.url = "github:logos-co/logos-module-builder/0.2.0"; + calc_interfaces.url = "github:your-org/logos-calc-interfaces"; +}; +``` + +The builder resolves `calc_interfaces` to a store path, hands the generator the resolved file, and emits the same bound `Calculator` wrapper — only the *source* of the contract moved. Nothing in `src/` changes. (In a workspace, run `ws sync-graph` after editing flake inputs.) + +That's the full picture. An interface is a contract you can keep local or share across repos; a module binds it to whatever provider it's given at runtime; and the generated, type-safe wrappers make the bound calls feel exactly like calling a concrete dependency — minus the coupling. + +--- + +## Recap + +| Concept | In the code | Seen via `logoscore` | +| -------------------------------- | -------------------------------------------------------- | --------------------------------------------------- | +| Interface declaration | `interfaces/calculator.h` (methods + `logos_events:`) | — | +| Declared, not depended-on | `interface_dependencies` set, `dependencies: []` | `lm metadata` shows empty `Dependencies:` | +| Bind at runtime | `modules().bind_calculator(provider)` | provider is a `call` argument | +| Typed **sync** call | `sumVia` / `productVia` / `versionVia` | `8`, `15`, `1.0.0` | +| Typed **async** call | `startFibVia` → `fibonacciAsync(..., cb)` | `queued`, then `6765` | +| Typed **event** subscription | `watchVersion` → `onVersionReady(cb)` | captured payload `1.0.0` | +| No-validation / superset rule | bind to any module name | `calc_module` → `8`; `no_such_module` → RPC error | +| Share across repos | `interface_dependencies[].input` + flake input | — | + +The interface coupled `calc_via_interface` to a *contract*, never to `calc_module`. Any module exposing that contract can be bound in its place — at runtime, by name. + +**Next:** see [Composing Modules](tutorial-composing-modules.md) for the concrete-dependency counterpart (`modules().calc_module`), or give this module a UI with [Part 2 (QML-only)](tutorial-qml-ui-app.md) / [Part 3 (C++ backend)](tutorial-cpp-ui-app.md). diff --git a/outputs/tutorial-qml-ui-app.md b/outputs/tutorial-qml-ui-app.md index 9299aff..315ea35 100644 --- a/outputs/tutorial-qml-ui-app.md +++ b/outputs/tutorial-qml-ui-app.md @@ -47,7 +47,7 @@ Create a new directory and initialise it from the QML module template: `mkdir logos-calc-ui && cd logos-calc-ui` ```bash -nix flake init -t github:logos-co/logos-module-builder#ui-qml +nix flake init -t github:logos-co/logos-module-builder/0.2.0#ui-qml ``` > **Note:** The generated `flake.nix` uses an unpinned `logos-module-builder` URL. Replace it with the pinned version shown in [Step 4](#step-4-update-flakenix) to ensure reproducible builds. @@ -330,7 +330,7 @@ The template already has everything wired up. Update the description and add `ca description = "Calculator QML UI Plugin for Logos - frontend for calc_module"; inputs = { - logos-module-builder.url = "github:logos-co/logos-module-builder"; + logos-module-builder.url = "github:logos-co/logos-module-builder/0.2.0"; # Points at your local calc_module checkout. This is a placeholder — # you lock it to your actual path in the next step with @@ -574,7 +574,7 @@ nix build '.#lgx-portable' --out-link result-lgx-portable Build the basecamp desktop shell: ```bash -nix build 'github:logos-co/logos-basecamp' -o basecamp-result +nix build 'github:logos-co/logos-basecamp/0.2.0' -o basecamp-result ``` Basecamp manages its own per-user data directory and preinstalls its bundled modules (`main_ui`, `package_manager`, …) from the build. It does **not** accept `--modules-dir` / `--ui-plugins-dir` flags; instead you point it at a data directory with `--user-dir` (or the `LOGOS_USER_DIR` env var), and it reads installed core modules from `/modules` and UI plugins from `/plugins` — exactly the directories `lgpm` writes to. @@ -586,7 +586,7 @@ For this tutorial we use an explicit data directory, `basecamp-data`, so the ins `lgpm` installs `.lgx` packages into a modules/plugins directory: ```bash -nix build 'github:logos-co/logos-package-manager#cli' --out-link ./pm +nix build 'github:logos-co/logos-package-manager/0.2.0#cli' --out-link ./pm ``` ### 8.4 Create the data directory @@ -645,7 +645,7 @@ The sidebar labels each UI plugin by its `name` from `metadata.json`, which is w The dev build above depends on nix store paths at runtime. For a self-contained portable build that works without nix: ```bash -nix build 'github:logos-co/logos-basecamp#bin-bundle-dir' -o basecamp-portable +nix build 'github:logos-co/logos-basecamp/0.2.0#bin-bundle-dir' -o basecamp-portable ``` ```bash diff --git a/outputs/tutorial-wrapping-c-library.md b/outputs/tutorial-wrapping-c-library.md index 4f3f40c..9c99e5a 100644 --- a/outputs/tutorial-wrapping-c-library.md +++ b/outputs/tutorial-wrapping-c-library.md @@ -40,10 +40,10 @@ For a module that wraps an external C library: `mkdir logos-calc-module && cd logos-calc-module` ```bash -nix flake init -t github:logos-co/logos-module-builder#with-external-lib +nix flake init -t github:logos-co/logos-module-builder/0.2.0#with-external-lib # Or for a plain module (no external library): -# nix flake init -t github:logos-co/logos-module-builder +# nix flake init -t github:logos-co/logos-module-builder/0.2.0 ``` This generates skeleton files (`flake.nix`, `metadata.json`, `CMakeLists.txt`, and a `src/` directory) pre-configured for the logos-module-builder. You then customize them for your specific library. @@ -327,7 +327,7 @@ Change `description`. Add flake inputs here if your module depends on other modu description = "Calculator module - wraps libcalc C library for Logos"; inputs = { - logos-module-builder.url = "github:logos-co/logos-module-builder"; + logos-module-builder.url = "github:logos-co/logos-module-builder/0.2.0"; }; outputs = inputs@{ logos-module-builder, ... }: @@ -579,7 +579,7 @@ Use the `lm` CLI tool (from `logos-module`) to inspect the compiled module binar The `lm` CLI inspects compiled module binaries. Build it from the `logos-module` repo: ```bash -nix build 'github:logos-co/logos-module#lm' --out-link ./lm +nix build 'github:logos-co/logos-module/0.2.0#lm' --out-link ./lm ``` ### 5.2 View metadata @@ -739,7 +739,7 @@ Interface screen. ### 6.1 Build logoscore ```bash -nix build 'github:logos-co/logos-logoscore-cli' --out-link ./logos +nix build 'github:logos-co/logos-logoscore-cli/0.2.0' --out-link ./logos ``` ### 6.2 Set up the modules directory @@ -751,7 +751,7 @@ nix build '.#lgx' ``` ```bash -nix build 'github:logos-co/logos-package-manager#cli' --out-link ./pm +nix build 'github:logos-co/logos-package-manager/0.2.0#cli' --out-link ./pm ``` ```bash @@ -897,7 +897,7 @@ Add a `tests` block to the `mkLogosModule` call. `mockCLibs` lists the external description = "Calculator module - wraps libcalc C library for Logos"; inputs = { - logos-module-builder.url = "github:logos-co/logos-module-builder"; + logos-module-builder.url = "github:logos-co/logos-module-builder/0.2.0"; }; outputs = inputs@{ logos-module-builder, ... }: @@ -1098,7 +1098,7 @@ nix build '.#lgx-portable' --out-link result-lgx-portable To install a portable package on another machine: ```bash -nix build 'github:logos-co/logos-package-manager#cli' --out-link ./pm +nix build 'github:logos-co/logos-package-manager/0.2.0#cli' --out-link ./pm ./pm/bin/lgpm --modules-dir ./modules install --file result-lgx-portable/*.lgx ``` @@ -1217,7 +1217,7 @@ Instead of pre-building the library and placing it in `lib/`, you can have Nix f description = "Module wrapping libfoo from GitHub"; inputs = { - logos-module-builder.url = "github:logos-co/logos-module-builder"; + logos-module-builder.url = "github:logos-co/logos-module-builder/0.2.0"; # Fetch the library source (non-flake) libfoo-src = { diff --git a/run.sh b/run.sh index fcc7d7e..96c448f 100755 --- a/run.sh +++ b/run.sh @@ -21,8 +21,8 @@ # that git tag (passed to both `run` and `generate` so the executed commands and # the generated Markdown agree). Any further args are forwarded verbatim to the # `run` and `generate` invocations, so e.g. `--release-for REPO=REF` also works: -# ./run.sh --release tutorial-v3 -# ./run.sh --release tutorial-v3 --release-for logos-basecamp=main +# ./run.sh --release 0.2.0 +# ./run.sh --release 0.2.0 --release-for logos-basecamp=main # set -euo pipefail @@ -109,8 +109,9 @@ if [ ! -d "${OUTPUT_DIR}" ]; then fi echo "==> Cleaning build artifacts from ${OUTPUT_DIR}/" -# --also calc-data: the Composing Modules tutorial creates a calc-data/ persistence -# directory (logoscore --persistence-path) that the default clean rules don't cover. -"${DOCTEST[@]}" clean "${OUTPUT_DIR}" --also calc-data --verbose +# --also calc-data / .logoscore: tutorials create per-instance persistence dirs +# (logoscore --persistence-path and the default .logoscore store) that the default +# clean rules don't cover. +"${DOCTEST[@]}" clean "${OUTPUT_DIR}" --also calc-data --also .logoscore --verbose echo "==> Done. Cleaned tutorial output is in ${OUTPUT_DIR}/" diff --git a/tests/tutorial-interface-dependencies.test.yaml b/tests/tutorial-interface-dependencies.test.yaml index ad98088..957d0d5 100644 --- a/tests/tutorial-interface-dependencies.test.yaml +++ b/tests/tutorial-interface-dependencies.test.yaml @@ -531,7 +531,7 @@ sections: ```nix inputs = { - logos-module-builder.url = "github:logos-co/logos-module-builder"; + logos-module-builder.url = "github:logos-co/logos-module-builder{release}"; calc_interfaces.url = "github:your-org/logos-calc-interfaces"; }; ``` diff --git a/tests/tutorial-wrapping-c-library.test.yaml b/tests/tutorial-wrapping-c-library.test.yaml index 57615a7..9c7c825 100644 --- a/tests/tutorial-wrapping-c-library.test.yaml +++ b/tests/tutorial-wrapping-c-library.test.yaml @@ -1189,7 +1189,7 @@ sections: description = "Module wrapping libfoo from GitHub"; inputs = { - logos-module-builder.url = "github:logos-co/logos-module-builder"; + logos-module-builder.url = "github:logos-co/logos-module-builder{release}"; # Fetch the library source (non-flake) libfoo-src = {