Files
logos-cpp-sdk/doctests/cpp-sdk-worker-thread-http.test.yaml
Dario LipicarandClaude Opus 5 198f0317ca feat(optional): ?T is two-state, and the generators finally read it (#125)
* feat(optional): ?T is two-state, and the generators finally read it

No generator in any language read the optional flag — it had never been
implemented. `?T` was a HARD REJECT on the cdylib backend ("module not
cdylib-eligible"), `std::optional<T>` in an impl header fell through to the
opaque `any` with no diagnostic, and a `? name: T` field was emitted as a
required `T`. Three real contracts in the workspace already declare optionals
and were silently getting one of those three answers.

`?T` is TWO-state: a value of T, or empty. Never three — "one LIDL type <-> one
type per language" leaves nowhere for a third state, because every target has
exactly one empty inhabitant.

ONE MEANING, TWO SPELLINGS. `? name: T` (the field flag) and `name: ?T` (the
type kind) are the same declaration. Backends no longer answer that themselves:
logos-lidl's fieldIsOptional/fieldValueType are re-exported from lidl_compat.h
and every site THIS COMMIT TOUCHES reads them, so the two spellings emit
byte-identical code on the cdylib and client backends. That
caught a live drift on the way in — lidlRecordCollidesWithBytesTag read `f.type`
and so refused `? _bytes: tstr` while letting `_bytes: ?tstr` straight through,
one declaration with two answers.

THE WIRE RULE DEPENDS ON THE SLOT. Absent and explicit null are the SAME state
on decode and DIFFERENT on encode:
  - decode is liberal, by exactly one inhabitant: in an optional slot absent and
    null both mean empty; in a required slot both stay errors. A present value
    goes through the decoder a required T would get, so a wrong type still fails
    at the same path — optional widens the domain, it does not switch checking
    off. `?bstr` therefore keeps the LENIENT bytes decode a bare `bstr` gets,
    rather than silently becoming stricter in the optional slot.
  - encode has one canonical form: empty OMITS the key where the slot is NAMED
    (a record field) and is spelled null where it is POSITIONAL (an argument, a
    return, an event parameter — no key to omit, and arity must not change). Key
    omission lives in the record emitter because a Codec only ever sees a value,
    never the slot it sits in. A round trip therefore canonicalises.
  - `?any` collapses onto `any`: nlohmann::json already carries null, so
    std::optional<LogosMap> would give the slot two spellings of empty.

The dispatch gate now admits a missing trailing optional argument and
materialises it as null, exactly the way a missing record field already was. A
method with no optional parameter emits the byte-identical gate it always did.

Header-first: `std::optional<T>` <-> `?T`, composing with records and
containers. `std::optional<std::optional<T>>` has NO LIDL type (three C++ states
over a two-state wire), so it maps down to `?T` — which makes the author's own
declaration stop compiling against the generated codec, deliberately — and says
so at derivation time instead of leaving a conversion error in generated code.

The Qt/Lp consumer surface is NOT fixed and does not pretend to be. The wrappers
real modules get come from legacy/main.cpp, where the AST is flattened to a
single Qt type-name string per slot before optionality could be seen; Qt has no
optional metatype, so `?T` lands on QVariant — the right shape (an invalid
QVariant is Qt's empty inhabitant) with no type. The generator now prints a Note
naming every flattened slot so an affected build is never silent, and
docs/project.md records exactly what a Qt consumer will still do with an
optional field.

Verified by output equivalence, not by a green build: the generator was built
before and after and run over every .lidl in the workspace plus the impl-header
fixtures, in cdylib, consumer-qt, consumer-lp, client and header-first modes.
428 of 465 artefacts are byte-identical; all 37 that differ belong to one of the
four contracts that declare an optional (the 38th path is the manifest). The
harness's sensitivity is pinned by a negative control: qt vs lp output differs
in 45 files. The emitted codec was additionally compiled under -Wall -Wextra and
run against the rules above — omission, absent==null, required-still-rejects,
present-but-wrong-still-fails, and canonicalising round trip.

Tests: 199 pass, 0 fail (180 before, 19 new).

Requires logos-lidl's optionality accessors and logos-protocol's
Codec<std::optional<T>>.

NOT FIXED, AND IT IS THE PATH THAT MATTERS MOST. The legacy interface-wrapper
path is untouched, and it is the one every real module builds through
(buildPlugin.nix:145 -> logos-cpp-generator --general-only). There the two
spellings still diverge:

  ? maybe: tstr   ->  QString maybe{};   __m.value("maybe").toString()
  maybe: ?tstr    ->  QVariant maybe{};  __m.value("maybe")

and --api-style lp diverges too, neither side being std::optional. So R3 holds
on the backends below and NOT on the Qt consumer a shipping module actually
gets. logos-chat-module -- the contract that prompted this work -- uses the
field-flag spelling, so it lands on the branch that silently defaults.

The cause is upstream of codegen: legacy/main.cpp's moduleRecordsToJson and
moduleMethodsToJson flatten every TypeExpr to a single Qt TYPE-NAME STRING, so
optionality (along with nesting, map key types and descriptions) is gone before
generator_lib.cpp sees it. Widening that interface is a larger change and is
deliberately not attempted here. The only R3 test on a Qt surface covers
lidl_gen_client.cpp, which is on no live build path.

* chore: re-pin logos-lidl to master for the optionality accessors

lidl_compat.h re-exports typeIsOptional / optionalValueType / fieldIsOptional /
fieldValueType / paramIsOptional / paramValueType, which landed in
logos-lidl#7. The pinned lidl predated it, so CI failed to compile.

logos-lidl 8c95d4f -> 35f33d8. Tests: 199 pass, 0 fail.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* chore: re-pin logos-protocol to master for Codec<std::optional<T>>

The generated record codecs emit Codec<std::optional<T>> for an optional
field; that specialisation landed in logos-protocol#37 and the pinned
protocol predated it.

Note this repo's own tests would NOT have caught the omission -- the
generator tests string-assert emitted text rather than compiling it, so a
missing codec specialisation only surfaces when a real module compiles
generated optional code (logos-test-modules' ext provider).

logos-protocol 4359557 -> 72754ab. Tests: 199 pass, 0 fail.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(doctests): override logos-lidl alongside every logos-cpp-sdk override

The doc-tests build downstream repos (logoscore-cli, capability_module,
accounts_module) with --override-input logos-cpp-sdk. Nix does not carry the
overridden input's OWN lock, so those builds got this branch's cpp-sdk source
while still resolving logos-lidl from their own, older locks. The shipped
share/lidl-frontend/lidl_compat.h then calls accessors that lidl does not
have:

  lidl_compat.h:46: error: 'paramValueType' has not been declared in 'lidl'
  lidl_compat.h:92: error: 'fieldValueType' was not declared in this scope

Every --override-input logos-cpp-sdk now has a matching
--override-input <same-path>/logos-cpp-sdk/logos-lidl.

This is specific to the override path. A normal consumer running
'nix flake update logos-cpp-sdk' inherits cpp-sdk's own lock, which pins the
lidl carrying these accessors, and is unaffected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(doctests): move logos-lidl at the qt-sdk nodes, not under logos-cpp-sdk

The doc-tests failed to build logos-qt-generator:

  share/lidl-frontend/lidl_compat.h:46: error: 'paramValueType' has not been
  declared in 'lidl'

MECHANISM. This SDK installs cpp-generator/experimental/lidl_compat.h into
$out/share/lidl-frontend/, and logos-qt-sdk's logos-qt-generator *compiles*
that installed header against qt-sdk's OWN logos-lidl input. Under
logos-qt-sdk, logos-lidl is a SIBLING of logos-cpp-sdk, not a descendant:

  logos-qt-sdk
  |-- logos-cpp-sdk   <- --override-input moves this to the commit under test
  `-- logos-lidl      <- stays on qt-sdk's lock (8c95d4f), lacks the accessors

logos-logoscore-cli and logos-module-builder both declare
`logos-qt-sdk.inputs.logos-cpp-sdk.follows = "logos-cpp-sdk"` but no lidl
follows, so overriding the SDK hands qt-sdk a new lidl_compat.h next to its
old lidl. The failing derivation is logos-qt-generator — not anything in
logos-cpp-sdk, which is why the previous attempt aimed at the wrong node.

THE FIX is one `<path-to-logos-qt-sdk>/logos-lidl` override per qt-sdk node
that ends up on the SDK under test. A tree-walk over the resolved lock found
four in logoscore-cli's closure and two per module build; with the overrides
applied the walk reports zero remaining.

WHAT WAS REMOVED, and why it was doing nothing:

  * The `.../logos-cpp-sdk/logos-lidl` overrides added in bef3ef5 were no-ops.
    With only `--override-input logos-cpp-sdk <sha>`, that node's logos-lidl
    already resolves to 35f33d87 out of cpp-sdk's own lock — nix >= 2.26
    carries an overridden input's lock, and CI runs Determinate Nix. Verified
    by resolving the lock with and without them: byte-identical.
  * The `logos-module-client/...` overrides never matched anything. Nix says so
    out loud ("does not match any input"): logoscore-cli has no such root
    input; module-client only appears under logos-test-modules/, outside the
    runtime closure. The prose claiming it pins the SDK is corrected too.

cpp-sdk-concurrent-dispatch is fixed here as well — it failed the same way and
carried no lidl overrides at all.

VERIFIED locally against bef3ef5, the exact commit CI failed on:

  * accounts .lgx  -> exit 0, logos-accounts_module-module-lib.lgx (5,939,898 B)
  * logoscore CLI  -> exit 0, ./logos/bin/logoscore reports
                      "logos-cpp-sdk bef3ef57d3f489073672e70a786c550df7edd003"
  * negative control (same command minus the single qt-sdk lidl flag) fails
    with CI's exact derivation,
    /nix/store/pf96n2ldvhy6sq39ygkh5zdqx7dcn4df-logos-qt-generator-0.1.0.drv
  * no "does not match any input" warnings remain on any command

The durable fix is a one-line bump of logos-qt-sdk's own flake.lock logos-lidl
to master (logos-lidl#7 is purely additive: six new inline helpers, nothing
removed or renamed). Once qt-sdk carries it, every override added here can go.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 07:19:47 -03:00

595 lines
25 KiB
YAML

name: "Calling a Module From a Worker Thread (HTTP Server)"
output: cpp-sdk-worker-thread-http.md
release: ""
intro: |
Logos inter-module calls travel over Qt Remote Objects, whose replicas only
work on the thread that owns them — the module's main/event-loop thread. So a
module that wants to call **another** module from a *worker* thread has a
problem: the call would otherwise run on the worker thread and hang on replica
acquisition (there's no event loop there to drive it).
That worker-thread case is not exotic — it's exactly what you hit the moment a
module embeds a server. The motivating example is a module that serves an HTTP
`/metrics` endpoint and, on each scrape, calls other modules to gather their
numbers. The HTTP server runs on its own thread; the inter-module calls happen
there.
This doc-test proves that path works on the SDK commit under test, and that the
module stays **pure C++** — it never touches Qt to make it work. The SDK does
the marshaling: `LogosAPIClient` transparently runs `getClient` /
`invokeRemoteMethod` / `requestObject` / `onEvent` on the module's owner thread
when they're called from another thread.
It is fully self-contained:
1. Create `sensor_module`, a tiny **callee** with one method, `readTemperature()`.
2. Create `http_module`, a **caller** that depends on `sensor_module`, embeds a
[libmicrohttpd](https://www.gnu.org/software/libmicrohttpd/) server, and on
every HTTP request calls `sensor_module.readTemperature()` **from the server
thread** through the generated `modules().sensor_module` wrapper.
3. Build **both** against the C++ SDK commit under test, run them in
`logoscore`, start the server, and `curl` it.
A green run means the `curl` got the sensor's reading back — i.e. the
worker-thread inter-module call completed instead of hanging. Without the SDK's
thread marshaling it would deadlock on the server thread and the `curl` would
time out.
what_you_build: "Two modules — a `sensor_module` callee and an `http_module` caller that embeds an HTTP server — built against this SDK commit and run in `logoscore`, where an HTTP request drives a cross-module call from the server's worker thread."
what_you_learn:
- Why inter-module calls must run on the module's owner thread (Qt Remote Objects replica affinity)
- How the SDK lets a module call another module from a worker thread without touching Qt
- How to embed a third-party C library (libmicrohttpd, via pkg-config) in a universal module
- How to drive a cross-module call from an HTTP handler and scrape it with `curl`
prerequisites:
- |
**Nix** with flakes enabled. Install from [nixos.org](https://nixos.org/download.html), then enable flakes:
```bash
mkdir -p ~/.config/nix
echo 'experimental-features = nix-command flakes' >> ~/.config/nix/nix.conf
```
Verify: `nix flake --help >/dev/null 2>&1 && echo "Flakes enabled"`
- "**git** — nix flakes only see files tracked by git."
- "**curl** — to scrape the endpoint."
- "A Linux or macOS machine."
sections:
- title: "Create the callee: sensor_module"
step: true
text: |
`sensor_module` is an ordinary `core` module in the pure-C++ (`interface:
universal`) style with a single method. `http_module` will call it on every
HTTP request.
steps:
- title: "metadata.json"
text: "No dependencies; `interface: universal` selects the pure-C++ pattern."
file:
path: sensor_module/metadata.json
language: json
content: |
{
"name": "sensor_module",
"version": "1.0.0",
"type": "core",
"category": "general",
"description": "A callee module: returns a temperature reading",
"main": "sensor_module_plugin",
"interface": "universal",
"dependencies": [],
"nix": {
"packages": {
"build": [],
"runtime": []
},
"external_libraries": [],
"cmake": {
"find_packages": [],
"extra_sources": []
}
}
}
- title: "CMakeLists.txt"
file:
path: sensor_module/CMakeLists.txt
language: cmake
content: |
cmake_minimum_required(VERSION 3.14)
project(SensorModulePlugin 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 sensor_module
SOURCES
src/sensor_module_impl.h
src/sensor_module_impl.cpp
)
- title: "flake.nix"
file:
path: sensor_module/flake.nix
language: nix
content: |
{
description = "Sensor core module - a callee for the worker-thread doc-test";
inputs = {
logos-module-builder.url = "github:logos-co/logos-module-builder{release}";
};
outputs = inputs@{ logos-module-builder, ... }:
logos-module-builder.lib.mkLogosModule {
src = ./.;
configFile = ./metadata.json;
flakeInputs = inputs;
};
}
- title: "src/sensor_module_impl.h — the class"
text: "A plain C++ class — no base, no Qt. Its one public method becomes callable over IPC."
file:
path: sensor_module/src/sensor_module_impl.h
language: cpp
content: |
#pragma once
#include <cstdint>
// A trivial sensor. http_module calls readTemperature() on every HTTP
// request, from its server thread.
class SensorModuleImpl {
public:
/// Returns the current temperature reading (degrees Celsius).
int64_t readTemperature();
};
- title: "src/sensor_module_impl.cpp — the implementation"
file:
path: sensor_module/src/sensor_module_impl.cpp
language: cpp
content: |
#include "sensor_module_impl.h"
int64_t SensorModuleImpl::readTemperature()
{
return 42;
}
- title: "Create the caller: http_module"
step: true
text: |
`http_module` declares `sensor_module` as a dependency (so the builder
generates a typed `modules().sensor_module` wrapper) and embeds an HTTP
server using **libmicrohttpd**. The server runs on its own thread; its
request handler calls `sensor_module.readTemperature()` from there. The
module code never mentions Qt — the SDK marshals the cross-module call onto
the module's owner thread.
steps:
- title: "metadata.json — declare the dependency and the C library"
text: |
`dependencies` lists `sensor_module`. `nix.packages.runtime` adds
`libmicrohttpd` (a build input, so the plugin can link it) and
`nix.packages.build` adds `pkg-config` so CMake can find it.
file:
path: http_module/metadata.json
language: json
content: |
{
"name": "http_module",
"version": "1.0.0",
"type": "core",
"category": "general",
"description": "A caller module: serves HTTP and reads sensor_module from the server thread",
"main": "http_module_plugin",
"interface": "universal",
"dependencies": ["sensor_module"],
"nix": {
"packages": {
"build": ["pkg-config"],
"runtime": ["libmicrohttpd"]
},
"external_libraries": [],
"cmake": {
"find_packages": [],
"extra_sources": []
}
}
}
- title: "CMakeLists.txt — link libmicrohttpd"
text: "After `logos_module(...)`, find libmicrohttpd via pkg-config and link it into the generated plugin target (`<name>_module_plugin`)."
file:
path: http_module/CMakeLists.txt
language: cmake
content: |
cmake_minimum_required(VERSION 3.14)
project(HttpModulePlugin 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 http_module
SOURCES
src/http_module_impl.h
src/http_module_impl.cpp
)
find_package(PkgConfig REQUIRED)
pkg_check_modules(MHD REQUIRED IMPORTED_TARGET libmicrohttpd)
target_link_libraries(http_module_module_plugin PRIVATE PkgConfig::MHD)
- title: "flake.nix — add the dependency input"
text: |
Declare `sensor_module` as a flake input (the input name **must match**
the dependency name). The `path:` value is a placeholder — we lock it to
the real sensor checkout in the build step.
file:
path: http_module/flake.nix
language: nix
content: |
{
description = "HTTP core module - reads sensor_module from its server thread";
inputs = {
logos-module-builder.url = "github:logos-co/logos-module-builder{release}";
# The module this one depends on. Placeholder path — locked to the
# real checkout in the build step via --override-input.
sensor_module.url = "path:/path/to/your/sensor_module";
};
outputs = inputs@{ logos-module-builder, sensor_module, ... }:
logos-module-builder.lib.mkLogosModule {
src = ./.;
configFile = ./metadata.json;
flakeInputs = inputs;
};
}
- title: "src/http_module_impl.h — the class"
text: |
Pure C++: `LogosModuleContext` (for `modules()`), a `std::mutex`, and an
opaque `void* m_daemon` (the libmicrohttpd handle stays out of the header
the generator parses). `start`/`stop` control the server; `readSensor`
does the cross-module call and is what the HTTP handler invokes.
file:
path: http_module/src/http_module_impl.h
language: cpp
content: |
#pragma once
#include <cstdint>
#include <mutex>
#include <logos_module_context.h> // LogosModuleContext base + modules()
// Serves HTTP via libmicrohttpd. On each request the server thread calls
// sensor_module through modules().sensor_module — the SDK marshals that
// call onto this module's owner thread. No Qt here.
class HttpModuleImpl : public LogosModuleContext {
public:
HttpModuleImpl() = default;
~HttpModuleImpl();
/// Start the HTTP server on `port`. Returns 1 on success, 0 on
/// failure (already running / bad port / bind error).
int64_t start(int64_t port);
/// Stop the HTTP server. Returns 1 if it was running, 0 otherwise.
int64_t stop();
/// Read sensor_module.readTemperature(). The HTTP handler calls this
/// from the server (worker) thread; exposed as a method so it can
/// also be driven directly for comparison.
int64_t readSensor();
private:
std::mutex m_mutex;
void* m_daemon = nullptr; // struct MHD_Daemon*
};
- title: "src/http_module_impl.cpp — the implementation"
text: |
The handler runs on a libmicrohttpd worker thread and calls
`self->readSensor()`, which goes through `modules().sensor_module`. That
cross-module call is what the SDK marshals onto the owner thread — the
whole point of this doc-test.
file:
path: http_module/src/http_module_impl.cpp
language: cpp
content: |
#include "http_module_impl.h"
#include <cstdint>
#include <string>
#include <microhttpd.h>
// Generated at build time: defines LogosModules with the typed
// modules().sensor_module accessor. Included only in the .cpp so the impl
// header the generator parses stays free of Qt / codegen types.
#include "logos_sdk.h"
namespace {
// libmicrohttpd access handler. `cls` is the HttpModuleImpl*. Runs on an
// MHD worker thread; the cross-module call inside is marshaled onto the
// module's owner thread by the SDK.
MHD_Result onRequest(void* cls, struct MHD_Connection* connection,
const char* /*url*/, const char* /*method*/,
const char* /*version*/, const char* /*upload_data*/,
size_t* /*upload_data_size*/, void** /*req_cls*/)
{
auto* self = static_cast<HttpModuleImpl*>(cls);
const std::string body =
"temperature " + std::to_string(self->readSensor()) + "\n";
MHD_Response* response = MHD_create_response_from_buffer(
body.size(), const_cast<char*>(body.data()), MHD_RESPMEM_MUST_COPY);
MHD_add_response_header(response, "Content-Type", "text/plain; charset=utf-8");
MHD_Result ret = MHD_queue_response(connection, MHD_HTTP_OK, response);
MHD_destroy_response(response);
return ret;
}
} // namespace
HttpModuleImpl::~HttpModuleImpl()
{
stop();
}
int64_t HttpModuleImpl::readSensor()
{
// Cross-module call. From the HTTP handler this runs on the server's
// worker thread; the SDK marshals it onto this module's owner thread.
return modules().sensor_module.readTemperature();
}
int64_t HttpModuleImpl::start(int64_t port)
{
std::lock_guard<std::mutex> lock(m_mutex);
if (m_daemon) return 0;
if (port <= 0 || port > 65535) return 0;
MHD_Daemon* daemon = MHD_start_daemon(
MHD_USE_INTERNAL_POLLING_THREAD, static_cast<uint16_t>(port),
nullptr, nullptr, &onRequest, this, MHD_OPTION_END);
if (!daemon) return 0;
m_daemon = daemon;
return 1;
}
int64_t HttpModuleImpl::stop()
{
std::lock_guard<std::mutex> lock(m_mutex);
if (!m_daemon) return 0;
MHD_stop_daemon(static_cast<MHD_Daemon*>(m_daemon));
m_daemon = nullptr;
return 1;
}
- title: "Build both modules against this SDK"
step: true
text: |
Nix flakes only see git-tracked files, so initialise a repo in each module
first, then build each `.lgx`, overriding `logos-cpp-sdk` to the commit
under test.
> The override URLs carry a `{release}` placeholder the runner expands to a
> concrete ref — locally this checkout's `HEAD`, in CI the commit being
> tested.
steps:
- title: "Initialise git repos"
run: |
(cd sensor_module && git init -q && git add -A)
(cd http_module && git init -q && git add -A)
check_file: "sensor_module/.git/HEAD"
- title: "Build the sensor's .lgx against this SDK"
run: |
nix build 'path:./sensor_module#lgx' \
--override-input logos-module-builder 'github:logos-co/logos-module-builder{release}' \
--override-input logos-module-builder/logos-cpp-sdk 'github:logos-co/logos-cpp-sdk{release}' \
--override-input logos-module-builder/logos-qt-sdk/logos-lidl 'github:logos-co/logos-lidl' \
-o sensor-lgx
code_block: |
nix build 'path:./sensor_module#lgx' \
--override-input logos-module-builder/logos-cpp-sdk 'github:logos-co/logos-cpp-sdk' \
--override-input logos-module-builder/logos-qt-sdk/logos-lidl 'github:logos-co/logos-lidl' \
-o sensor-lgx
post_text: "The sensor package is under `./sensor-lgx/`:"
extra_run:
run: "ls sensor-lgx/*.lgx"
- title: "Build the http module's .lgx against this SDK"
text: |
Lock `sensor_module` to the local checkout and override `logos-cpp-sdk`
in both builders, so the dependency wrapper and both plugins are built
against one consistent SDK.
run: |
nix build 'path:./http_module#lgx' \
--override-input sensor_module 'path:./sensor_module' \
--override-input logos-module-builder 'github:logos-co/logos-module-builder{release}' \
--override-input logos-module-builder/logos-cpp-sdk 'github:logos-co/logos-cpp-sdk{release}' \
--override-input logos-module-builder/logos-qt-sdk/logos-lidl 'github:logos-co/logos-lidl' \
--override-input sensor_module/logos-module-builder 'github:logos-co/logos-module-builder{release}' \
--override-input sensor_module/logos-module-builder/logos-cpp-sdk 'github:logos-co/logos-cpp-sdk{release}' \
--override-input sensor_module/logos-module-builder/logos-qt-sdk/logos-lidl 'github:logos-co/logos-lidl' \
-o http-lgx
code_block: |
nix build 'path:./http_module#lgx' \
--override-input sensor_module 'path:./sensor_module' \
--override-input logos-module-builder/logos-cpp-sdk 'github:logos-co/logos-cpp-sdk' \
--override-input logos-module-builder/logos-qt-sdk/logos-lidl 'github:logos-co/logos-lidl' \
--override-input sensor_module/logos-module-builder/logos-cpp-sdk 'github:logos-co/logos-cpp-sdk' \
--override-input sensor_module/logos-module-builder/logos-qt-sdk/logos-lidl 'github:logos-co/logos-lidl' \
-o http-lgx
post_text: "The http package is under `./http-lgx/`:"
extra_run:
run: "ls http-lgx/*.lgx"
- title: "Build the runtime and install both modules"
step: true
text: |
Build `logoscore` and `lgpm` (against this SDK), seed the modules directory
with the capability module, and install both modules.
steps:
- title: "Build logoscore against this SDK"
run: |
nix build 'github:logos-co/logos-logoscore-cli{release}' \
--override-input logos-cpp-sdk 'github:logos-co/logos-cpp-sdk{release}' \
--override-input logos-qt-sdk/logos-lidl 'github:logos-co/logos-lidl' \
--override-input logos-liblogos/logos-cpp-sdk 'github:logos-co/logos-cpp-sdk{release}' \
--override-input logos-liblogos/logos-qt-sdk/logos-lidl 'github:logos-co/logos-lidl' \
--override-input logos-capability-module/logos-module-builder 'github:logos-co/logos-module-builder{release}' \
--override-input logos-capability-module/logos-module-builder/logos-cpp-sdk 'github:logos-co/logos-cpp-sdk{release}' \
--override-input logos-capability-module/logos-module-builder/logos-qt-sdk/logos-lidl 'github:logos-co/logos-lidl' \
--override-input logos-capability-module/logos-module-builder/logos-test-framework/logos-qt-sdk/logos-lidl 'github:logos-co/logos-lidl' \
--out-link ./logos
code_block: |
nix build 'github:logos-co/logos-logoscore-cli' \
--override-input logos-cpp-sdk 'github:logos-co/logos-cpp-sdk' \
--override-input logos-qt-sdk/logos-lidl 'github:logos-co/logos-lidl' \
--override-input logos-liblogos/logos-cpp-sdk 'github:logos-co/logos-cpp-sdk' \
--override-input logos-liblogos/logos-qt-sdk/logos-lidl 'github:logos-co/logos-lidl' \
--override-input logos-capability-module/logos-module-builder/logos-cpp-sdk 'github:logos-co/logos-cpp-sdk' \
--override-input logos-capability-module/logos-module-builder/logos-qt-sdk/logos-lidl 'github:logos-co/logos-lidl' \
--override-input logos-capability-module/logos-module-builder/logos-test-framework/logos-qt-sdk/logos-lidl 'github:logos-co/logos-lidl' \
--out-link ./logos
check_file: "logos/bin/logoscore"
- title: "Build lgpm"
run: "nix build 'github:logos-co/logos-package-manager#cli' -o lgpm"
check_file: "lgpm/bin/lgpm"
- title: "Seed the modules directory with the capability module"
run: |
mkdir -p modules
cp -RL ./logos/modules/. ./modules/
check_file: "modules/capability_module/manifest.json"
- title: "Install the sensor"
run: "./lgpm/bin/lgpm --modules-dir ./modules --allow-unsigned install --file sensor-lgx/*.lgx"
expect_contains:
- "Installed to:"
- title: "Install the http module"
run: "./lgpm/bin/lgpm --modules-dir ./modules --allow-unsigned install --file http-lgx/*.lgx"
expect_contains:
- "Installed to:"
- title: "Confirm both modules are installed"
run: "./lgpm/bin/lgpm --modules-dir ./modules list"
expect_contains:
- "sensor_module"
- "http_module"
check_file: "modules/http_module/manifest.json"
- title: "Serve over HTTP and scrape from the worker thread"
step: true
text: |
Start the daemon, load both modules, then start the HTTP server and `curl`
it. The `curl` triggers a request whose handler — on the server's worker
thread — calls `sensor_module.readTemperature()`. Getting `temperature 42`
back is the proof that the worker-thread cross-module call completed.
steps:
- title: "Start the daemon"
run: "sh -c './logos/bin/logoscore -D -m ./modules > logs.txt 2>&1 &'"
code_block: "logoscore -D -m ./modules > logs.txt &"
- run: "sleep 3"
- title: "Load the sensor (the dependency first)"
run: "./logos/bin/logoscore load-module sensor_module"
code_block: "logoscore load-module sensor_module"
expect_contains:
- "sensor_module"
- title: "Load the http module"
run: "./logos/bin/logoscore load-module http_module"
code_block: "logoscore load-module http_module"
expect_contains:
- "http_module"
- title: "Read the sensor directly (main thread)"
text: |
Called via `logoscore`, `readSensor()` runs on the module's own event-loop
thread — the easy case. It returns the sensor's reading:
run: "./logos/bin/logoscore call http_module readSensor"
code_block: "logoscore call http_module readSensor"
expect_contains:
- '"result":42'
- title: "Start the HTTP server"
run: "./logos/bin/logoscore call http_module start 8080"
code_block: "logoscore call http_module start 8080"
expect_contains:
- '"result":1'
- run: "sleep 1"
- title: "Scrape it — the cross-module call now happens on the server thread"
text: |
The HTTP handler runs on a libmicrohttpd worker thread and calls
`sensor_module.readTemperature()` from there. The SDK marshals that call
onto the module's owner thread, so it completes and the response carries
the sensor's reading. Without the marshaling this request would hang.
run: "curl -s --max-time 15 http://127.0.0.1:8080/"
code_block: "curl http://127.0.0.1:8080/"
expect_contains:
- "temperature 42"
- title: "Stop the HTTP server"
run: "./logos/bin/logoscore call http_module stop"
code_block: "logoscore call http_module stop"
expect_contains:
- '"result":1'
- title: "Stop the daemon"
run: "./logos/bin/logoscore stop"
code_block: "logoscore stop"
- run: "sleep 2"
- title: "Confirm the daemon has stopped"
run: "./logos/bin/logoscore status || true"
code_block: "logoscore status"
expect_contains:
- '"status":"not_running"'
- title: "Recap"
text: |
| Call site | Thread | Result |
| --------- | ------ | ------ |
| `logoscore call http_module readSensor` | module event-loop thread | `42` |
| `curl http://127.0.0.1:8080/` → HTTP handler → `readSensor()` | libmicrohttpd worker thread | `temperature 42` |
Both reach `sensor_module.readTemperature()` through the generated
`modules().sensor_module` wrapper. The second does it from a worker thread —
and it works because the SDK marshals the call onto the module's owner
thread, where Qt Remote Objects replicas live. The module itself stays pure
C++. A green run is evidence that worker-thread inter-module calls work on
this SDK commit.