tutorial-v4 release
Update artifacts for tutorial-v4. Regenerate outputs/ against the 0.2.0
release tags: every {release} reference (logos-basecamp, logos-logoscore-cli,
logos-module-builder, logos-package-manager, logos-module) now resolves to
0.2.0 in both the generated .md tutorials and the example module flake.nix
files.
- Add the missing {release} placeholder to the advanced flake.nix examples in
tutorial-wrapping-c-library and tutorial-interface-dependencies so they pin
consistently.
- run.sh: clean now also removes the .logoscore persistence dirs; --release
examples updated from tutorial-vN to 0.2.0 (repos carry semver tags;
logos-tutorial carries the tutorial-vN tag).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@@ -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.
|
||||
|
||||
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
@@ -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`.
|
||||
|
||||
@@ -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.";
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#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<Event>(...)`)
|
||||
* - `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);
|
||||
};
|
||||
@@ -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, ... }:
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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<void*>(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;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#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;
|
||||
};
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
# Nix build output
|
||||
result
|
||||
result-*
|
||||
|
||||
# CMake build directory
|
||||
build/
|
||||
@@ -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
|
||||
)
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
@@ -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 <cstdint>
|
||||
#include <string>
|
||||
|
||||
// Defines the `logos_events` token (expands to `public`) so this
|
||||
// header is valid C++ on its own, not only as generator input.
|
||||
#include <logos_module_context.h>
|
||||
|
||||
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);
|
||||
};
|
||||
@@ -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": []
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 `<method>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;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
#include <logos_module_context.h> // 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;
|
||||
};
|
||||
@@ -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.";
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#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<Event>(...)`)
|
||||
* - `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);
|
||||
};
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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_<interface>(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 <cstdint>
|
||||
#include <string>
|
||||
|
||||
// Defines the `logos_events` token (expands to `public`) so this
|
||||
// header is valid C++ on its own, not only as generator input.
|
||||
#include <logos_module_context.h>
|
||||
|
||||
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<Event>(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 <cstdint>
|
||||
#include <string>
|
||||
|
||||
#include <logos_module_context.h> // 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 `<method>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).
|
||||
@@ -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 `<dir>/modules` and UI plugins from `<dir>/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
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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}/"
|
||||
|
||||
@@ -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";
|
||||
};
|
||||
```
|
||||
|
||||
@@ -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 = {
|
||||
|
||||