Crafter.Debug.Core is what the app side and the crafter-debug client both need, without the session: - Value: a JSON value with its own parser and printer, so no consumer interface sees a JSON library. Integers stay integers and whole doubles stay doubles; NaN and the infinities, which JSON cannot spell, are written as strings rather than hidden, because a debugger must not lose a NaN; invalid UTF-8 is replaced so output always parses. Objects keep insertion order, the order an agent reads. - Reflect: aggregate members and names through structured-binding packs and source_location, taken from Crafter.DB, plus scoped-enum names by probing a range of values. Self-tested with static_asserts so a compiler that changes function_name()'s shape fails the build. - Codec<T>: encode, decode and describe for bool, integers, floating point, strings, enums, optional, vector, array, string-keyed maps, Value, Required<T> and every aggregate. Decode reports every problem in a call with its path and, for a typo, the likely intended name, and never throws: a bad call cannot take an app down, which a wrongly typed parameter could in 3DForts' hand-written from_json. - Result, Error and Async: handler results, including ones that take frames to arrive, so callers need no ready:false polling. project.cpp now builds Crafter.Debug.Core as its own library, used by both the app library and the client. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
205 lines
8.4 KiB
C++
205 lines
8.4 KiB
C++
// SPDX-License-Identifier: LGPL-3.0-only
|
|
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
|
|
|
// Compile-time introspection without P2996 (clang 23 has no <meta>), taken
|
|
// from Crafter.DB:Reflect. For aggregates: structured-binding packs give the
|
|
// member count and per-member access, and std::source_location's
|
|
// function_name() spells a member's name when a pointer to a member of an
|
|
// extern object is a template argument. For scoped enums: function_name()
|
|
// spells a template argument that is an enumerator by its name and any other
|
|
// value as a cast, so probing a range of values finds every enumerator.
|
|
// When a clang ships <meta>, this partition is the only thing to replace.
|
|
export module Crafter.Debug.Core:Reflect;
|
|
import std;
|
|
|
|
namespace Crafter::Debug::Reflect {
|
|
template<class T>
|
|
struct CountHelper {
|
|
template<class U>
|
|
static consteval auto Count(U& u) {
|
|
auto&& [...m] = u;
|
|
return std::integral_constant<std::size_t, sizeof...(m)>{};
|
|
}
|
|
static constexpr std::size_t value = decltype(Count(std::declval<T&>()))::value;
|
|
};
|
|
|
|
// An instance whose member addresses are turned into text. Only its
|
|
// address is used, in constant expressions; no_destroy keeps the compiler
|
|
// from registering a destructor for it.
|
|
template<class T>
|
|
[[clang::no_destroy]] inline const T external{};
|
|
|
|
template<auto Probe>
|
|
consteval std::string_view RawName() {
|
|
return std::source_location::current().function_name();
|
|
}
|
|
|
|
template<class T>
|
|
consteval std::string_view RawTypeName() {
|
|
return std::source_location::current().function_name();
|
|
}
|
|
|
|
// The part of RawName<P>()'s text that spells P: what follows "Probe = "
|
|
// up to the closing bracket.
|
|
consteval std::string_view ProbeText(std::string_view raw) {
|
|
const std::size_t end = raw.rfind(']');
|
|
const std::size_t start = raw.rfind("Probe = ", end);
|
|
return raw.substr(start + 8, end - start - 8);
|
|
}
|
|
|
|
// The enumerator name for V, or "" when V is not an enumerator: clang
|
|
// spells those as a cast, "(Ns::Color)5".
|
|
template<auto V>
|
|
consteval std::string_view EnumeratorName() {
|
|
const std::string_view text = ProbeText(RawName<V>());
|
|
if (text.starts_with('(')) return {};
|
|
const std::size_t colon = text.rfind("::");
|
|
return colon == std::string_view::npos ? text : text.substr(colon + 2);
|
|
}
|
|
}
|
|
|
|
export namespace Crafter::Debug {
|
|
// True for class types whose non-static data members a structured
|
|
// binding can bind: public members, no bases carrying members, no
|
|
// user-declared constructors.
|
|
template<class T>
|
|
concept Aggregate = std::is_class_v<T> && std::is_aggregate_v<T> && requires { Reflect::CountHelper<T>::value; };
|
|
|
|
template<Aggregate T>
|
|
inline constexpr std::size_t memberCount = Reflect::CountHelper<T>::value;
|
|
|
|
// The I-th member of an aggregate lvalue.
|
|
template<std::size_t I, class T>
|
|
constexpr decltype(auto) Member(T&& t) noexcept {
|
|
auto&& [...m] = t;
|
|
return (m...[I]);
|
|
}
|
|
|
|
template<std::size_t I, class T>
|
|
using MemberType = std::remove_cvref_t<decltype(Member<I>(std::declval<T&>()))>;
|
|
|
|
// The declared name of the I-th member: "yaw". Parsed out of
|
|
// "std::string_view ...::RawName() [Probe = &external.yaw]".
|
|
template<std::size_t I, Aggregate T>
|
|
consteval std::string_view MemberName() {
|
|
constexpr std::string_view text = Reflect::ProbeText(Reflect::RawName<&Member<I>(Reflect::external<T>)>());
|
|
return text.substr(text.rfind('.') + 1);
|
|
}
|
|
|
|
// The type's name as the compiler spells it ("Game::AimArgs"), for help
|
|
// and errors; the spelling is not load-bearing.
|
|
template<class T>
|
|
consteval std::string_view TypeName() {
|
|
constexpr std::string_view raw = Reflect::RawTypeName<T>();
|
|
const std::size_t end = raw.rfind(']');
|
|
const std::size_t start = raw.rfind("T = ", end);
|
|
return raw.substr(start + 4, end - start - 4);
|
|
}
|
|
|
|
// Calls f.template operator()<I>(member) for every member, in order.
|
|
template<Aggregate T, class F>
|
|
constexpr void ForEachMember(T& t, F&& f) {
|
|
[&]<std::size_t... I>(std::index_sequence<I...>) {
|
|
(f.template operator()<I>(Member<I>(t)), ...);
|
|
}(std::make_index_sequence<memberCount<T>>{});
|
|
}
|
|
|
|
// The values probed for enumerator names, a half-open range clamped to
|
|
// the enum's underlying type. Specialize for an enum with values outside
|
|
// the default range; every probe is one template instantiation.
|
|
template<class E>
|
|
struct EnumRange {
|
|
static constexpr std::int64_t min = -128;
|
|
static constexpr std::int64_t max = 128;
|
|
};
|
|
|
|
template<class E>
|
|
struct Enumerator {
|
|
E value;
|
|
std::string_view name;
|
|
};
|
|
}
|
|
|
|
namespace Crafter::Debug::Reflect {
|
|
template<class E>
|
|
consteval std::int64_t ProbeMin() {
|
|
using U = std::underlying_type_t<E>;
|
|
return std::max<std::int64_t>(EnumRange<E>::min, static_cast<std::int64_t>(std::numeric_limits<U>::min()));
|
|
}
|
|
|
|
template<class E>
|
|
consteval std::int64_t ProbeMax() {
|
|
using U = std::underlying_type_t<E>;
|
|
constexpr std::int64_t limit = static_cast<std::uint64_t>(std::numeric_limits<U>::max()) >= static_cast<std::uint64_t>(std::numeric_limits<std::int64_t>::max())
|
|
? std::numeric_limits<std::int64_t>::max()
|
|
: static_cast<std::int64_t>(std::numeric_limits<U>::max()) + 1;
|
|
return std::min<std::int64_t>(EnumRange<E>::max, limit);
|
|
}
|
|
|
|
template<class E, std::size_t... I>
|
|
consteval auto EnumTable(std::index_sequence<I...>) {
|
|
constexpr std::int64_t first = ProbeMin<E>();
|
|
constexpr std::array<std::string_view, sizeof...(I)> names = {
|
|
EnumeratorName<static_cast<E>(first + static_cast<std::int64_t>(I))>()...
|
|
};
|
|
constexpr std::size_t count = static_cast<std::size_t>(std::ranges::count_if(names, [](std::string_view name) { return !name.empty(); }));
|
|
std::array<Enumerator<E>, count> table{};
|
|
std::size_t next = 0;
|
|
for (std::size_t i = 0; i < names.size(); ++i) {
|
|
if (!names[i].empty()) table[next++] = { static_cast<E>(first + static_cast<std::int64_t>(i)), names[i] };
|
|
}
|
|
return table;
|
|
}
|
|
}
|
|
|
|
export namespace Crafter::Debug {
|
|
// Every named value of a scoped enum within EnumRange, in value order.
|
|
// Scoped only: clang rejects casting an out-of-range value to an unscoped
|
|
// enum without a fixed underlying type, even in a template argument.
|
|
template<class E>
|
|
requires std::is_scoped_enum_v<E>
|
|
inline constexpr auto enumerators = Reflect::EnumTable<E>(std::make_index_sequence<static_cast<std::size_t>(Reflect::ProbeMax<E>() - Reflect::ProbeMin<E>())>{});
|
|
|
|
template<class E>
|
|
requires std::is_scoped_enum_v<E>
|
|
constexpr std::optional<std::string_view> EnumName(E value) {
|
|
for (const Enumerator<E>& e : enumerators<E>) {
|
|
if (e.value == value) return e.name;
|
|
}
|
|
return std::nullopt;
|
|
}
|
|
|
|
template<class E>
|
|
requires std::is_scoped_enum_v<E>
|
|
constexpr std::optional<E> EnumFromName(std::string_view name) {
|
|
for (const Enumerator<E>& e : enumerators<E>) {
|
|
if (e.name == name) return e.value;
|
|
}
|
|
return std::nullopt;
|
|
}
|
|
}
|
|
|
|
// Self-tests: a compiler whose function_name() text changes shape fails
|
|
// here, at the library's build, instead of producing garbage names.
|
|
namespace Crafter::Debug::Reflect {
|
|
struct ProbeStruct {
|
|
std::int32_t alpha;
|
|
double beta;
|
|
std::string gamma;
|
|
};
|
|
enum class ProbeEnum : std::uint8_t { Zero, One, Seven = 7 };
|
|
enum class ProbeSigned : std::int16_t { Minus = -3, Big = 100 };
|
|
|
|
static_assert(Aggregate<ProbeStruct>);
|
|
static_assert(memberCount<ProbeStruct> == 3);
|
|
static_assert(MemberName<0, ProbeStruct>() == "alpha");
|
|
static_assert(MemberName<2, ProbeStruct>() == "gamma");
|
|
static_assert(std::is_same_v<MemberType<1, ProbeStruct>, double>);
|
|
static_assert(TypeName<ProbeStruct>().ends_with("ProbeStruct"));
|
|
static_assert(enumerators<ProbeEnum>.size() == 3);
|
|
static_assert(EnumName(ProbeEnum::Seven) == "Seven");
|
|
static_assert(!EnumName(static_cast<ProbeEnum>(5)).has_value());
|
|
static_assert(EnumFromName<ProbeEnum>("One") == ProbeEnum::One);
|
|
static_assert(EnumName(ProbeSigned::Minus) == "Minus");
|
|
static_assert(enumerators<ProbeSigned>.size() == 2);
|
|
}
|