Files
Dario LipicarandClaude Opus 5 03842db5c1 feat(windows): named pipes, an explicit lp_* ABI, and a cross target (#58)
* feat(windows): port logos_socket_paths and add a cross target

logos_socket_paths.cpp is the only POSIX-bound file in logos-protocol. All of
it is unix-domain-socket machinery, and on Windows the local transport is named
pipes (QLocalServer maps a name to \\.\pipe\<name>), where none of the
assumptions hold: a pipe has no inode to lstat/chown/chmod -- access comes from
a security descriptor set at CreateNamedPipe time -- and a pipe cannot outlive
its last handle, so a hard-killed process leaves nothing behind.

isSocketDead and reapStaleSockets are therefore not merely unimplemented on
Windows, they are vacuous: the state they detect cannot arise. Both return the
fail-closed answer (false / 0), matching the documented contract that an
endpoint is never reported dead unless certain.

applySocketPerms deliberately does NOT no-op. With no policy requested it
returns true, as on POSIX. But when LOGOS_SOCKET_GROUP or LOGOS_SOCKET_MODE
*are* set it fails with an explanatory error, because silently returning true
would leave the endpoint more permissive than the operator asked for -- the one
direction this file is careful never to go (cf. the chgrp-then-chmod ordering
in the POSIX branch). Granting a pipe to a group needs a DACL plus a
group->SID resolver; until that exists, refuse loudly.

Also gates qt6.wrapQtAppsNoGuiHook behind !isWindows and sets dontWrapQtApps.
Both halves are required: the hook does not even evaluate for a mingw host, it
would be inert anyway (wrap-qt-apps-hook.sh skips anything that is not ELF or
Mach-O), and qtbase's setup hook hard-errors in qtPreHook unless
dontWrapQtApps is set.

Header contract updated per function. POSIX branch unchanged and still compiles.

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

* fix: make the Boost.System component optional, not required

find_package(Boost REQUIRED COMPONENTS system) hard-fails on Boost 1.89:

    Could not find a package configuration file provided by "boost_system"

Boost.System has been header-only for years, and 1.89 finally dropped the
compiled boost_system library, so no boost_systemConfig.cmake is installed at
all. The COMPONENTS request was not gratuitous though -- on 1.87 the
Boost::system imported target is only exported when the component is asked
for, which is what the previous comment recorded.

So ask optionally and fall back to Boost::headers, which supplies the same
header-only error_code either way. The choice is by BOOST VERSION, not by
platform: this is not a Windows quirk, it simply surfaced first there because
the Windows target pins a newer nixpkgs (Boost 1.89) than the native one
(Boost 1.87).

Verified both ways -- native aarch64-darwin still selects Boost::system:
    -- Boost.System target: Boost::system (Boost 1.87.0)
and the build completes unchanged.

Also adds QT_HOST_PATH / QT_ADDITIONAL_HOST_PACKAGES_PREFIX_PATH for the
Windows target. Qt6RemoteObjectsDependencies.cmake declares
    set(__qt_RemoteObjects_tool_deps "Qt6RemoteObjectsTools;6.11.1")
and Qt6RemoteObjectsTools holds repc, which must RUN on the build machine --
so under cross it lives in the build-platform Qt, not the mingw one. Without
these, find_package reports the thoroughly misleading "Expected Config file at
<qtbase>/lib/cmake/Qt6RemoteObjects ... does NOT exist": the TARGET config is
found fine; it is the HOST tool package that is missing. Every Qt-consuming
repo will need this, so it should be hoisted into logos-nix rather than
repeated.

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

* refactor: declare the lp_* C ABI explicitly instead of relying on auto-export

Adds LP_API (__declspec(dllexport) when building the shared library, default
visibility elsewhere) to the 21 lp_* entry points, and defines
LOGOS_PROTOCOL_BUILDING_SHARED for the shared target only, so the static
archive leaves LP_API empty and its consumers need no import library.

This is NOT a bug fix, contrary to what the concern in the Windows plan
suggested. Measured on the cross-built DLL, before and after:

    before:  export table 0x2ece (11982 symbols), lp_* present: 21
    after:   export table 0x15   (   21 symbols), lp_* present: 21

GNU ld's PE auto-export was already exporting lp_* -- along with roughly
twelve thousand other symbols. The worry was that logos_module_impl.h's
__declspec(dllexport) would disable auto-export image-wide and silently drop
lp_*; it does not, because no translation unit in logos_protocol includes that
header (it is listed in PROTOCOL_SOURCES for IDE visibility only).

What this does buy is worth having anyway: the exported surface is now the ABI
we actually declare rather than whatever happens to have external linkage, it
stops being contingent on auto-export staying enabled -- which the very next
TU to gain a dllexport would silently end -- and it drops ~12k incidental
symbols from the export table.

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

* fix: relax the Boost.System requirement in the EXPORTED cmake config too

The previous commit fixed cpp/CMakeLists.txt but left
logos-protocolConfig.cmake.in still doing

    find_dependency(Boost REQUIRED COMPONENTS system)

so logos-protocol itself built fine on Boost 1.89 while every CONSUMER of its
installed CMake package failed at configure time -- caught by logos-qt-sdk,
which is the first downstream repo to be cross-built.

Worth noting as a general trap: a package can be internally consistent and
still ship a broken contract, because the exported config is a separate
artifact from the build. Anything changed in one has to be checked in the
other.

Verified both directions: the Windows cross builds of logos-cpp-sdk and
logos-qt-sdk now succeed, and a native aarch64-darwin logos-qt-sdk build --
which consumes this same config against Boost 1.87 -- still succeeds.

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

* feat(windows): mark the types that must exist once per process

PE has no symbol interposition. ELF and Mach-O interpose across the whole
image set, so when liblogos_core exports TokenManager::instance() every
other image binds to that one definition and the function-local
`static TokenManager instance;` is genuinely a singleton. On Windows
every image that links liblogos_protocol.a / liblogos_qt_sdk.a statically
gets its own copy of the code and therefore its own statics -- measured:
NINE images in the Basecamp payload each defined
TokenManager::instance()::instance. The host saved a capability token
into its copy, the UI plugin read its own empty copy, and every
cross-module call was refused (29 "ModuleProxy: rejecting unauthorized
call").

LOGOS_SHARED_API marks the affected types. It expands to
__declspec(dllimport) only for a consumer that opts in with
LOGOS_SHARED_USE_DLL, and to nothing everywhere else -- off Windows, and
inside logos-protocol/logos-qt-sdk/liblogos_core themselves, so the
static archives compile byte-identically to before.

The dllimport is the load-bearing half, not the export: it rewrites the
reference to go through __imp_, so the plain symbol is never undefined
and GNU ld never pulls the archive member that would redefine it. Without
it the link still succeeds, binds to the archive, and gives no diagnostic
at all.

logos_shared_api.h records both wrong answers -- export everything
(collides with the static archive over LogosAPI) and export nothing
(today's silent per-image statics) -- so neither gets reinvented.

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

* fix(windows): let checks and devShells take the arg forAllSystems now passes

The cross-target commit added `inherit system;` to forAllSystems so the Windows
arm could tell which target it was building, but left `checks` and `devShells`
on the strict `({ pkgs }: ...)` pattern.  A Nix attrset pattern without `...` is
exact, so both stopped evaluating:

    error: function 'anonymous lambda' called with unexpected argument 'system'

on EVERY platform, not just Windows -- `nix flake check` and `ws develop
logos-protocol` are dead on this branch while they work on master.  `packages`
was unaffected because it goes through forAllTargets, which is why nothing
caught it.

Measured, same worktree, before and after:
  before: checks.aarch64-darwin -> the error above at flake.nix:52
  after:  checks.aarch64-darwin -> [ "tests" ]
          devShells.aarch64-darwin.default.name -> "nix-shell"
          packages -> [ aarch64-darwin aarch64-linux x86_64-darwin x86_64-linux
                        x86_64-windows ]

* chore(deps): re-pin logos-nix to the merged Windows overlay

The cross overlay landed in logos-nix#2.  This branch was locked to a
pre-merge rev, which has no `lib.forAllTargets` and no `lib.mkWindowsPkgs`,
so it could not evaluate standalone -- only against the unmerged branch.

Level 2 of the Windows chain; L1 (logos-nix) is merged.

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 09:44:25 -03:00

147 lines
4.3 KiB
C++

#ifndef LOGOS_TYPES_H
#define LOGOS_TYPES_H
#include <QDataStream>
#include <QVariant>
#include <stdexcept>
#include "logos_shared_api.h"
class LogosResultException : public std::runtime_error
{
public:
using std::runtime_error::runtime_error;
};
struct LogosResult
{
bool success;
// Error message if success is false
// Value can be retrieved like this:
//
// LogosResult result = someMethod();
// if (result.success) {
// QString someValue = result.getValue<QString>();
// // OR
// QString someValue = result.getString();
// }
QVariant value;
// LogosResult result = someMethod();
// if (!result.success) {
// QString error = result.getError();
// }
QVariant error;
template<typename T = QString>
T getError() const
{
if (success) {
throw LogosResultException("Attempted to get error from a successful LogosResult");
}
return error.value<T>();
}
template<typename T>
T getValue() const
{
if (!success) {
throw LogosResultException("Attempted to get value from a failed LogosResult: "
+ error.toString().toStdString());
}
return value.value<T>();
}
template<typename T>
T getValue(const QString &key, T defaultValue = T()) const
{
const QVariantMap &map = getValue<QVariantMap>();
if (!map.contains(key)) {
return defaultValue;
}
return qvariant_cast<T>(map.value(key));
}
template<typename T>
T getValue(int index, const QString &key, T defaultValue = T()) const
{
const QVariantList &list = getValue<QVariantList>();
if (index < 0 || index >= list.size()) {
return defaultValue;
}
return qvariant_cast<T>(list[index].toMap().value(key, defaultValue));
}
QString getString() const { return getValue<QString>(); }
QString getString(const QString &key, const QString &defaultValue = "") const
{
return getValue<QString>(key, defaultValue);
}
QString getString(int index, const QString &key, const QString &defaultValue = "") const
{
return getValue<QString>(index, key, defaultValue);
}
bool getBool() const { return getValue<bool>(); }
bool getBool(const QString &key) const { return getValue<bool>(key); }
bool getBool(int index, const QString &key) const { return getValue<bool>(index, key); }
int getInt() const { return getValue<int>(); }
int getInt(const QString &key, int defaultValue = 0) const
{
return getValue<int>(key, defaultValue);
}
int getInt(int index, const QString &key, int defaultValue = 0) const
{
return getValue<int>(index, key, defaultValue);
}
QVariantList getList() const { return getValue<QVariantList>(); }
QVariantList getList(const QString &key, const QVariantList &defaultValue = QVariantList()) const
{
return getValue<QVariantList>(key, defaultValue);
}
QVariantList getList(int index,
const QString &key,
const QVariantList &defaultValue = QVariantList()) const
{
return getValue<QVariantList>(index, key, defaultValue);
}
QVariantMap getMap() const { return getValue<QVariantMap>(); }
QVariantMap getMap(const QString &key, const QVariantMap &defaultValue = QVariantMap()) const
{
return getValue<QVariantMap>(key, defaultValue);
}
QVariantMap getMap(int index,
const QString &key,
const QVariantMap &defaultValue = QVariantMap()) const
{
return getValue<QVariantMap>(index, key, defaultValue);
}
};
// Provide (de)serialisation for being use as Remote Object.
//
// LOGOS_SHARED_API not because these hold state, but because they are the only
// out-of-line symbols in logos_types.cpp.obj: leaving them un-imported lets ld
// pull that object into a Windows consumer, and archive pull-in is transitive.
// The single-provider rule is per object file, not per class — see
// logos_shared_api.h.
LOGOS_SHARED_API QDataStream &operator<<(QDataStream &out, const LogosResult &result);
LOGOS_SHARED_API QDataStream &operator>>(QDataStream &in, LogosResult &result);
#endif