Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 102 additions & 9 deletions cpp/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
cmake_minimum_required(VERSION 3.14)
project(LogosSDK)

set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_AUTOMOC ON)

# Find Qt packages
find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Core RemoteObjects)
find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Core RemoteObjects)

# Plain-C++ transport dependencies (no Qt)
find_package(Boost REQUIRED)
find_package(OpenSSL REQUIRED)
find_package(nlohmann_json REQUIRED)

# SDK sources
set(SDK_SOURCES
logos_types.cpp
Expand All @@ -34,6 +39,10 @@ set(SDK_SOURCES
qt_provider_object.cpp
qt_provider_object.h
logos_transport.h
logos_transport.cpp
logos_transport_config.h
logos_transport_config_json.h
logos_transport_config_json.cpp
logos_transport_factory.cpp
logos_transport_factory.h
logos_registry.h
Expand All @@ -51,32 +60,105 @@ set(SDK_SOURCES
implementations/mock/mock_transport.h
implementations/mock/mock_registry.h
implementations/mock/logos_mock.h
# Plain-C++ wire stack (no Qt)
implementations/plain/rpc_value.h
implementations/plain/rpc_message.h
implementations/plain/rpc_message.cpp
implementations/plain/wire_codec.h
implementations/plain/json_mapping.h
implementations/plain/json_mapping.cpp
implementations/plain/json_codec.h
implementations/plain/json_codec.cpp
implementations/plain/cbor_codec.h
implementations/plain/cbor_codec.cpp
implementations/plain/rpc_framing.h
implementations/plain/rpc_framing.cpp
implementations/plain/incoming_call_handler.h
implementations/plain/rpc_connection.h
implementations/plain/rpc_server.h
implementations/plain/rpc_server.cpp
implementations/plain/io_context_pool.h
implementations/plain/io_context_pool.cpp
implementations/plain/qvariant_rpc_value.h
implementations/plain/qvariant_rpc_value.cpp
implementations/plain/plain_logos_object.h
implementations/plain/plain_logos_object.cpp
implementations/plain/plain_transport_host.h
implementations/plain/plain_transport_host.cpp
implementations/plain/plain_transport_connection.h
implementations/plain/plain_transport_connection.cpp
)

# Create the SDK library as STATIC instead of SHARED
add_library(logos_sdk STATIC ${SDK_SOURCES})

# Link Qt libraries
target_link_libraries(logos_sdk PUBLIC Qt${QT_VERSION_MAJOR}::Core Qt${QT_VERSION_MAJOR}::RemoteObjects)
# Link Qt + plain-C++ transport deps
target_link_libraries(logos_sdk PUBLIC
Qt${QT_VERSION_MAJOR}::Core
Qt${QT_VERSION_MAJOR}::RemoteObjects
Boost::headers
OpenSSL::SSL
Comment on lines +104 to +109
Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The plain transport uses Boost.Asio (and boost::system::error_code), which commonly requires linking Boost.System. Here the target links only Boost::headers; that can cause downstream link errors on platforms where Boost.System is not header-only, and Boost::headers may not exist when Boost is found via CMake’s FindBoost module. Consider requesting/linking Boost::system (and Boost::boost/headers) explicitly.

Copilot uses AI. Check for mistakes.
OpenSSL::Crypto
nlohmann_json::nlohmann_json
)

# Include directories
# Include directories — use BUILD_INTERFACE / INSTALL_INTERFACE generator
# expressions so the path stored in the exported target points at the
# install tree for downstream consumers and at the source tree only
# while we're building the SDK itself. install(EXPORT ...) refuses
# raw source-tree paths.
target_include_directories(logos_sdk PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_SOURCE_DIR}/implementations/qt_local
${CMAKE_CURRENT_SOURCE_DIR}/implementations/qt_remote
${CMAKE_CURRENT_SOURCE_DIR}/implementations/mock
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/implementations/qt_local>
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/implementations/qt_remote>
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/implementations/mock>
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/implementations/plain>
$<INSTALL_INTERFACE:include>
$<INSTALL_INTERFACE:include/implementations/qt_local>
$<INSTALL_INTERFACE:include/implementations/qt_remote>
$<INSTALL_INTERFACE:include/implementations/mock>
$<INSTALL_INTERFACE:include/implementations/plain>
)

# Set output directories for static library
set_target_properties(logos_sdk PROPERTIES
ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib"
)

# Install the library
# Install the library into an EXPORT set so we can ship a CMake Config
# file that downstream consumers `find_package` to get an imported
# target with all the right INTERFACE_LINK_LIBRARIES (OpenSSL, Boost,
# nlohmann_json, Qt). Without this, every consumer that find_library's
# liblogos_sdk.a has to manually link the SDK's transitive deps.
install(TARGETS logos_sdk
EXPORT logos-cpp-sdkTargets
ARCHIVE DESTINATION lib
LIBRARY DESTINATION lib
RUNTIME DESTINATION bin
INCLUDES DESTINATION include
)

install(EXPORT logos-cpp-sdkTargets
FILE logos-cpp-sdkTargets.cmake
NAMESPACE logos-cpp-sdk::
DESTINATION lib/cmake/logos-cpp-sdk
)

include(CMakePackageConfigHelpers)
configure_package_config_file(
"${CMAKE_CURRENT_SOURCE_DIR}/logos-cpp-sdkConfig.cmake.in"
"${CMAKE_CURRENT_BINARY_DIR}/logos-cpp-sdkConfig.cmake"
INSTALL_DESTINATION lib/cmake/logos-cpp-sdk
)
write_basic_package_version_file(
"${CMAKE_CURRENT_BINARY_DIR}/logos-cpp-sdkConfigVersion.cmake"
VERSION 0.1.0
COMPATIBILITY SameMajorVersion
)
install(FILES
"${CMAKE_CURRENT_BINARY_DIR}/logos-cpp-sdkConfig.cmake"
"${CMAKE_CURRENT_BINARY_DIR}/logos-cpp-sdkConfigVersion.cmake"
DESTINATION lib/cmake/logos-cpp-sdk
)

# Install headers
Expand All @@ -95,13 +177,24 @@ install(FILES
logos_provider_object.h
qt_provider_object.h
logos_transport.h
logos_transport_config.h
logos_transport_config_json.h
logos_transport_factory.h
logos_registry.h
logos_registry_factory.h
logos_json.h
DESTINATION include
)

install(FILES
implementations/plain/rpc_value.h
implementations/plain/rpc_message.h
implementations/plain/wire_codec.h
implementations/plain/json_codec.h
implementations/plain/rpc_framing.h
DESTINATION include/implementations/plain
)

install(FILES
implementations/qt_local/local_transport.h
DESTINATION include/implementations/qt_local
Expand Down
27 changes: 27 additions & 0 deletions cpp/implementations/plain/cbor_codec.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#include "cbor_codec.h"
#include "json_mapping.h"

#include <nlohmann/json.hpp>

namespace logos::plain {

using json = nlohmann::json;

std::vector<uint8_t> CborCodec::encode(const AnyMessage& msg)
{
const json j = messageToJson(msg);
return json::to_cbor(j);
}

AnyMessage CborCodec::decode(MessageType tag, const uint8_t* data, std::size_t len)
{
json j;
try {
j = json::from_cbor(data, data + len, /*strict=*/true, /*allow_exceptions=*/true);
} catch (const std::exception& e) {
throw CodecError(std::string("cbor parse failed: ") + e.what());
}
return jsonToMessage(tag, j);
}

} // namespace logos::plain
28 changes: 28 additions & 0 deletions cpp/implementations/plain/cbor_codec.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#ifndef LOGOS_PLAIN_CBOR_CODEC_H
#define LOGOS_PLAIN_CBOR_CODEC_H

#include "wire_codec.h"

namespace logos::plain {

// CborCodec — same logical message layout as JsonCodec (shared via
// json_mapping.{h,cpp}), serialized with nlohmann::json::to_cbor /
// from_cbor. Matches JSON wire-for-wire in logical content; wire bytes
// are binary CBOR rather than UTF-8 JSON text.
//
// Useful when you want smaller/faster on-the-wire encoding without
// swapping to a wholly different codec family. Paired transports on the
// daemon can offer both JSON and CBOR; clients pick per-connection via
// --client-codec.
class CborCodec : public IWireCodec {
public:
std::vector<uint8_t> encode(const AnyMessage&) override;
AnyMessage decode(MessageType tag,
const uint8_t* data,
std::size_t len) override;
std::string name() const override { return "cbor"; }
};

} // namespace logos::plain

#endif // LOGOS_PLAIN_CBOR_CODEC_H
60 changes: 60 additions & 0 deletions cpp/implementations/plain/incoming_call_handler.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
#ifndef LOGOS_PLAIN_INCOMING_CALL_HANDLER_H
#define LOGOS_PLAIN_INCOMING_CALL_HANDLER_H

#include "rpc_message.h"

#include <functional>

namespace logos::plain {

// -----------------------------------------------------------------------------
// IncomingCallHandler — provider-side dispatch hook.
//
// rpc_connection hands inbound Call / Methods / Subscribe / Unsubscribe /
// Token messages to a handler that the Qt-boundary layer implements. The
// handler is what talks to the published QObject (ModuleProxy); this
// interface deliberately speaks only plain C++ types so the wire stack
// stays Qt-free.
//
// The reply callbacks can be invoked synchronously (from inside the
// handler) or asynchronously from a different thread — rpc_connection
// serializes the actual frame write internally.
// -----------------------------------------------------------------------------
class IncomingCallHandler {
public:
virtual ~IncomingCallHandler() = default;

using CallReply = std::function<void(ResultMessage)>;
using MethodsReply = std::function<void(MethodsResultMessage)>;
using EventSink = std::function<void(EventMessage)>;

virtual void onCall(const CallMessage& req, CallReply reply) = 0;

virtual void onMethods(const MethodsMessage& req, MethodsReply reply) = 0;

// `sink` stays alive until onUnsubscribe fires or the connection
// dies. The handler must call `sink(evt)` on every matching emission.
//
// `connectionId` is an opaque per-connection token (the rpc layer
// passes the connection's `this` pointer). The handler keys sinks
// by it so a subsequent onUnsubscribe / onConnectionClosed can
// remove only the sinks belonging to that connection — sub/unsub
// frames don't carry a subscriber identifier on the wire.
virtual void onSubscribe(const SubscribeMessage& req, EventSink sink,
const void* connectionId) = 0;

virtual void onUnsubscribe(const UnsubscribeMessage& req,
const void* connectionId) = 0;

// Called when a connection is torn down (graceful close or error)
// so the handler can drop any sinks still keyed to it. Without
// this, a dropped client leaks subscriptions and the host keeps
// fanning events into dead sinks.
virtual void onConnectionClosed(const void* connectionId) = 0;

virtual void onToken(const TokenMessage& req) = 0;
};

} // namespace logos::plain

#endif // LOGOS_PLAIN_INCOMING_CALL_HANDLER_H
28 changes: 28 additions & 0 deletions cpp/implementations/plain/io_context_pool.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#include "io_context_pool.h"

namespace logos::plain {

IoContextPool::IoContextPool()
: m_ioc()
, m_guard(boost::asio::make_work_guard(m_ioc))
, m_worker([this]{ m_ioc.run(); })
{
}

IoContextPool::~IoContextPool()
{
// Drop the work guard so run() can return once all outstanding work
// completes, then stop forcefully if something lingers.
m_guard.reset();
m_ioc.stop();
if (m_worker.joinable())
m_worker.join();
}

IoContextPool& IoContextPool::shared()
{
static IoContextPool pool;
return pool;
}

} // namespace logos::plain
44 changes: 44 additions & 0 deletions cpp/implementations/plain/io_context_pool.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#ifndef LOGOS_PLAIN_IO_CONTEXT_POOL_H
#define LOGOS_PLAIN_IO_CONTEXT_POOL_H

#include <boost/asio/executor_work_guard.hpp>
#include <boost/asio/io_context.hpp>

#include <memory>
#include <thread>

namespace logos::plain {

// -----------------------------------------------------------------------------
// IoContextPool — owns a single boost::asio::io_context and a worker thread
// that runs it until the pool is destroyed.
//
// One pool per SDK process is sufficient for our traffic (a handful of
// concurrent connections). If we ever need more parallelism, swap for a
// multi-thread pool (one io_context per thread + round-robin dispatch).
//
// Access the shared pool via `sharedPool()`; tests / special cases can
// construct their own.
// -----------------------------------------------------------------------------
class IoContextPool {
public:
IoContextPool();
~IoContextPool();

IoContextPool(const IoContextPool&) = delete;
IoContextPool& operator=(const IoContextPool&) = delete;

boost::asio::io_context& ioContext() { return m_ioc; }

// Process-wide default pool. Thread-safe lazy init.
static IoContextPool& shared();

private:
boost::asio::io_context m_ioc;
boost::asio::executor_work_guard<boost::asio::io_context::executor_type> m_guard;
std::thread m_worker;
};

} // namespace logos::plain

#endif // LOGOS_PLAIN_IO_CONTEXT_POOL_H
28 changes: 28 additions & 0 deletions cpp/implementations/plain/json_codec.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#include "json_codec.h"
#include "json_mapping.h"

#include <nlohmann/json.hpp>

namespace logos::plain {

using json = nlohmann::json;

std::vector<uint8_t> JsonCodec::encode(const AnyMessage& msg)
{
const json j = messageToJson(msg);
const std::string s = j.dump();
return std::vector<uint8_t>(s.begin(), s.end());
}

AnyMessage JsonCodec::decode(MessageType tag, const uint8_t* data, std::size_t len)
{
json j;
try {
j = json::parse(data, data + len);
} catch (const std::exception& e) {
throw CodecError(std::string("json parse failed: ") + e.what());
}
return jsonToMessage(tag, j);
}

} // namespace logos::plain
Loading
Loading