Skip to content

Latest commit

 

History

55 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

reflector-cpp-MsgEnv--transport

A single, configurable reflector that bridges a message environment (MQTT, sdbus, …) with a transport (asio client, asio server, ws, …). Instead of forking the whole project for every combination, you check out once and choose the pieces — and the output name — as build variables.

checkout  →  choose MsgEnv  →  choose transport  →  choose role  →  choose name  →  compile

Quick start

Windows (Visual Studio, a multi-config generator — pick Debug/Release at build time):

git clone --recursive <repo>
cmake -B build -G "Visual Studio 17 2022" -A x64 -DNAME=my-reflector -DMSGENV=MQTT -DTRANSPORT=asio -DROLE=client
cmake --build build --config Debug          # -> build/Debug/my-reflector.exe

Drop -G "Visual Studio 17 2022" to use the default generator. Open build/<NAME>.sln in Visual Studio if you want the IDE — it is generated from these variables, so you never hand-edit project GUIDs.

macOS (Makefiles, a single-config generator — pick Debug/Release at configure time with -DCMAKE_BUILD_TYPE; no --config on the build). This variant is MSGENV=NSDNC, which needs no broker at all — the ME is the system distributed-notification center:

git clone --recursive <repo>
cmake -S . -B build/os_darwin -DCMAKE_BUILD_TYPE=Release -DCMAKE_PREFIX_PATH=build/deps -DNAME=my-reflector -DMSGENV=NSDNC -DTRANSPORT=asio -DROLE=client 
cmake --build build/os_darwin               # -> build/os_darwin/<NAME>

-G Xcode works too (multi-config, like VS) if you want the IDE.

The knobs (CMakeLists.txt)

Variable Default Meaning
NAME reflector project and output .exe name (one variable)
MSGENV MQTT message environment (MQTT | sdbus | NSDNC) — selects cpp-msgenv-${MSGENV}.git
TRANSPORT asio transport library — selects cpp-${TRANSPORT}.git
ROLE client client | server — selects asio_${ROLE}.cpp

The swappable picks are pure string interpolation into the source paths:

add_executable(${NAME}
    main.cpp
    cpp-msgenv-${MSGENV}.git/IPSME_MsgEnv.cpp     # MQTT | sdbus | NSDNC
    cpp-${TRANSPORT}.git/asio_${ROLE}.cpp         # asio_client | asio_server
    ...)
set_target_properties(${NAME} PROPERTIES OUTPUT_NAME "${NAME}")

Adding a new transport or msgenv later = a new sibling folder, no CMakeLists.txt edit — just point the knob at it.

On the output name: with raw MSBuild the output .exe is $(TargetName), not the project name, so even a .vcxproj can vary it via a property (<TargetName>$(ReflectorName)</TargetName> + /p:ReflectorName=foo). What MSBuild can't cleanly vary is the project display name (it's the filename, plus a baked-in GUID). CMake sidesteps both: -DNAME= drives the project name, the .sln/.vcxproj filenames, and the .exe — all from one variable.

Prerequisites

Windows:

  • Visual Studio 2022 (MSVC v143).
  • mosquitto dev libs at C:/Program Files/mosquitto (override with -DMOSQUITTO=).
  • vcpkg tree at C:/Users/dev/vcpkg.git/installed/x64-windows providing nlohmann-json, nlohmann-json-schema-validator, jsoncons, asio (override with -DVCPKG_ROOT=).

macOS (MSGENV=NSDNC — no mosquitto anywhere; NSDNC is a system service):

  • Xcode command-line tools (clang).
  • nlohmann-json via Homebrew (header-only; the validator's config pulls it in).
  • build/deps/ holding the deps that must NOT come from brew as dylibs (a dylib would leave an installed agent with a broken /opt/homebrew dependency): nlohmann_json_schema_validator built STATIC (tag 2.3.0) and the standalone asio headers. -DCMAKE_PREFIX_PATH=build/deps points CMake at it; recipes in the reflector's Makefile (darwin section).

Configuring & building

You don't need -D on every build-D is a configure-time thing. Once you've run cmake -B build -D... once, the values are saved in build/CMakeCache.txt, and after that:

# Windows (multi-config: the config is a BUILD-time pick)
cmake -B build -G "Visual Studio 17 2022" -A x64 -DNAME=... -DROLE=...   # once
cmake --build build --config Debug                                       # repeat freely, no -D

# macOS (single-config Makefiles: the config is a CONFIGURE-time pick, cached like the knobs)
cmake -S . -B build/os_darwin -DCMAKE_BUILD_TYPE=Release -DNAME=... -DROLE=... -DCMAKE_PREFIX_PATH=build/deps   # once
cmake --build build/os_darwin                                            # repeat freely, no -D

Repeated builds need zero -D. You only touch -D again when you want to change a knob (or clean + re-edit the defaults). On macOS that includes CMAKE_BUILD_TYPE — switching Debug/Release is a re-configure (or a second build dir, e.g. build/os_darwin-dbg).

Debug vs Release — run the Release build (a Windows constraint)

The --config Debug above is fine for the compile loop, but on Windows the Debug binary segfaults on startup and cannot be run (observed: it dies immediately with an access violation before it connects to the broker). Build and run Release:

cmake --build build --config Release        # -> build/Release/<NAME>.exe

Why: the vendored runtime DLLs — mosquitto (C:/Program Files/mosquitto) and the vcpkg x64-windows tree (nlohmann-json-schema-validator, openssl) — are Release builds. An MSVC Debug exe (/MDd, _ITERATOR_DEBUG_LEVEL=2) loading Release (/MD) DLLs is a CRT / iterator-debug ABI mismatch, which crashes the moment a std type crosses that boundary. Release-against-Release matches, so the run binary is always build/Release/<NAME>.exe. (To run Debug you'd need Debug builds of every vendored DLL — not worth it; build Release.)

macOS has no such constraint: nothing is vendored as a Release DLL — the validator links static and CoreFoundation is a system framework — so a -DCMAKE_BUILD_TYPE=Debug build runs fine (in its own build dir, per the single-config note above).

Editing the set(... CACHE ...) defaults works, but only on a fresh configure. That's the catch: CACHE STRING means "use this unless already cached." So if build/ already exists, editing set(NAME "foo" ...) is ignored — the old cached value wins. To make an edited default take effect, configure fresh:

./clean.sh                 # (or: rm -rf build)  -> clears the cache
cmake -B build -G "..."    # fresh configure picks up the new defaults
cmake --build build

That's exactly why the clean command pairs with this: edit defaults → clean → configure → build, no -D anywhere.

Don't add FORCE to the set() to dodge this — FORCE would also stop -D from ever overriding. The plain CACHE STRING is what keeps both paths working.

Named combos without either dance — that's what CMakePresets.json is for:

{ "configurePresets": [
  { "name": "client-mqtt", "generator": "Visual Studio 17 2022",
    "binaryDir": "build/client-mqtt",
    "cacheVariables": { "NAME": "reflector-client", "MSGENV": "MQTT", "ROLE": "client" } },
  { "name": "server-mqtt", "inherits": "client-mqtt",
    "binaryDir": "build/server-mqtt",
    "cacheVariables": { "NAME": "reflector-server", "ROLE": "server" } },
  { "name": "mac", "hidden": true,
    "cacheVariables": { "CMAKE_BUILD_TYPE": "Release", "CMAKE_PREFIX_PATH": "build/deps" } },
  { "name": "client-nsdnc", "inherits": "mac",
    "binaryDir": "build/client-nsdnc",
    "cacheVariables": { "NAME": "reflector-client", "MSGENV": "NSDNC", "ROLE": "client" } },
  { "name": "server-nsdnc", "inherits": "client-nsdnc",
    "binaryDir": "build/server-nsdnc",
    "cacheVariables": { "NAME": "reflector-server", "ROLE": "server" } }
]}

then just cmake --preset client-mqtt && cmake --build build/client-mqtt (or --preset server-nsdnc etc. on the mac). Note the split: the hidden mac base carries only the platform boilerplate (single-config CMAKE_BUILD_TYPE, build/deps; no generator entry = the platform default), while the knobs stay per-combo — each preset is just a saved knob combination, exactly like the mqtt pair above. A preset is never required: -DROLE=server on a plain configure line works the same on every platform.

Cleaning

./clean.sh (macOS / Linux / Git-Bash) or clean.cmd (Windows) removes all CMake-generated project files (Xcode / Visual Studio / Makefiles), build trees and caches — leaving only the base source + CMakeLists.txt + README.


Architecture: one shared core, thin per-variant shells

Everything identical across variants lives in a shared reflector core; each variant is just the two libraries it names. The goal is to avoid the combinatorial fork (X-Y, X-Z, W-Y, W-Z = four repos for a 2×2 — and four places to fix every bug).

SHARED (written once):
  cpp-reflector-core.git/    reflector.hpp     ← bridge logic (deframe/dedup/publish ⇄ enframe/write)
  cpp-asio.git/              asio_client.{h,cpp}, asio_server.{h,cpp}  ← transports, same read-cb/write API
  cpp-msgenv-MQTT.git/       IPSME_MsgEnv.{h,cpp}     ← msgenv impl A (MQTT / mosquitto)
  cpp-msgenv-sdbus.git/      IPSME_MsgEnv.{h,cpp}     ← msgenv impl B (sdbus / Linux)
  cpp-msgenv-NSDNC.git/      IPSME_MsgEnv.{h,cpp}     ← msgenv impl C (NSDistributedNotificationCenter / macOS)
  cpp-l4end-framing.git/     cpp-msg_cache-dedup.git/ ← shared helpers

THIN per-variant (the only thing that differs):
  main.cpp (~15 lines)  +  CMake knobs

The core (sketch)

// cpp-reflector-core.git/reflector.hpp
// Transport = anything with .start() and .write(std::string); delivers read bytes via the cb you give it.
// MsgEnv    = anything with .publish(const char*) and .subscribe(cb, void*).
template <typename Transport, typename MsgEnv>
class Reflector {
    duplicate          _dedup;
    std::vector<char>  _buf;
    Transport&         _transport;
    MsgEnv&            _msgenv;
public:
    Reflector(Transport& t, MsgEnv& m) : _transport(t), _msgenv(m) {}

    void on_read(const char* data, std::size_t len) {           // asio -> msgenv
        l4end::deframe(&_buf, data, len, [this](std::string msg){
            if (_dedup.exists(msg)) return;
            _msgenv.publish(msg.c_str());
        });
    }
    bool on_msg(std::string msg) {                              // msgenv -> asio
        _dedup.cache(msg, t_entry_context(30s));
        _transport.write(l4end::enframe(msg));
        return true;
    }
    void start() {
        _msgenv.subscribe(&trampoline, this);                  // void* user-data carries `this`
        _transport.start();
    }
private:
    static void trampoline(const char* m, void* self) { static_cast<Reflector*>(self)->on_msg(m); }
};

The thin shell (sketch) — the only thing that differs per variant

#include "cpp-reflector-core.git/reflector.hpp"
#include "cpp-msgenv-MQTT.git/IPSME_MsgEnv.h"
#include "cpp-asio.git/asio_client.h"            // server variant: asio_server.h

int main() {
    mosquitto_lib_init();
    IPSME_MsgEnv msgenv;
    asio_client  transport;                       // server variant: asio_server transport(4999);
    Reflector    reflector(transport, msgenv);
    transport.set_read_cb([&](const char* d, std::size_t n){ reflector.on_read(d, n); });
    reflector.start();
    /* loop: msgenv.process_msgs(); */
    mosquitto_lib_cleanup();
}

Why this works

  • A variant = pick 2 libs: two #includes + two CMake knobs. No forked logic.
  • Template core = zero runtime cost (no vtables); the lib is fixed per binary. If you ever need to choose at runtime, swap the template for a common base (ITransport/IMsgEnv) — same layout, just virtual calls.
  • The read callback is wired in main (not the transport ctor) so Reflector and the transport can reference each other — the one plumbing tweak vs. today (move the read callback to a set_read_cb()/start(cb)).

The one discipline it depends on

The interchangeable libs must keep byte-identical public APIs — every IPSME_MsgEnv exposes the same publish/subscribe/process_msgs, every transport the same start/write + read callback. That contract is what lets the core compile against any combination unchanged.


Status

  • CMakeLists.txt builds the MQTT / asio / client combo (verified: Windows, Linux) and the NSDNC / asio / client combo (verified: macOS) — the MSGENV knob swap is proven in both directions, byte-identical public APIs and all.
  • main.cpp is currently the client project's main as-is — not yet reduced to the thin shell above, so ROLE is effectively pinned to client until the core is extracted and main is slimmed. The knobs and layout are in place for that next step.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages