mirror of
https://github.com/logos-co/logos-cpp-sdk.git
synced 2026-08-27 15:51:10 +00:00
* 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>
595 lines
25 KiB
YAML
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.
|