From cca104584e85e6f9dc4ccacc3b022557bb25b610 Mon Sep 17 00:00:00 2001 From: Luke Street Date: Mon, 13 Jul 2026 01:07:30 -0600 Subject: [PATCH] Mod API: Aggregate-initialize import/export records Starting with VS 18 / cl 14.51, MSVC's constant evaluator rejects a pointer-to-member in a constant initializer that also contains any computed data, a consteval call or even a copy of a constexpr object (C2127). Hook records are now aggregate-initialized from literals only: the computed name bytes (vtable symbol + display name) are baked into char... template arguments and sizes/kinds are spelled inline. --- include/mods/api.h | 8 +- include/mods/hook.hpp | 10 ++- include/mods/meta.hpp | 125 +++++++++++++++++--------------- include/mods/service.hpp | 31 +++++--- src/dusk/mods/loader/loader.cpp | 16 ++-- src/dusk/mods/svc/registry.cpp | 18 +++-- 6 files changed, 118 insertions(+), 90 deletions(-) diff --git a/include/mods/api.h b/include/mods/api.h index b2999336eb..5a21127203 100644 --- a/include/mods/api.h +++ b/include/mods/api.h @@ -156,6 +156,10 @@ typedef struct ModMetaRecord { uint8_t flags; /* ServiceImportFlags / ServiceExportFlags for imports/exports */ } ModMetaRecord; +typedef struct ModMetaServiceId { + char chars[MOD_META_SERVICE_ID_SIZE]; /* NUL-terminated */ +} ModMetaServiceId; + typedef struct MOD_META_ALIGN ModMetaHeader { ModMetaRecord rec; uint32_t abi_version; @@ -168,7 +172,7 @@ typedef struct MOD_META_ALIGN ModMetaImport { uint16_t major_version; uint16_t min_minor_version; void* slot; /* runtime only */ - char service_id[MOD_META_SERVICE_ID_SIZE]; + ModMetaServiceId service_id; } ModMetaImport; static_assert(sizeof(ModMetaImport) == 16 + MOD_META_SERVICE_ID_SIZE); @@ -178,7 +182,7 @@ typedef struct MOD_META_ALIGN ModMetaExport { uint16_t major_version; uint16_t minor_version; const void* service; /* runtime only */ - char service_id[MOD_META_SERVICE_ID_SIZE]; + ModMetaServiceId service_id; } ModMetaExport; static_assert(sizeof(ModMetaExport) == 16 + MOD_META_SERVICE_ID_SIZE); diff --git a/include/mods/hook.hpp b/include/mods/hook.hpp index 0f74ce23a6..94d5bce797 100644 --- a/include/mods/hook.hpp +++ b/include/mods/hook.hpp @@ -136,10 +136,14 @@ struct NamedHook : HookImpl, R, A...> {}; * ambiguous and need the mangled form. */ #define DEFINE_HOOK(target, alias) \ - MOD_META_RECORD static constinit auto mod_meta_hook_##alias = \ - ::dusk::mods::detail::make_hook_record<(target), ::dusk::mods::FixedString{#target}>(); \ + [[maybe_unused]] static const void* const mod_meta_hook_##alias = \ + &::dusk::mods::detail::HookRecordFor<(target), \ + ::dusk::mods::FixedString{#target}>::Holder::record; \ struct alias : ::dusk::mods::Hook<(target)> { \ - static void* resolved_target() { return mod_meta_hook_##alias.resolved; } \ + static void* resolved_target() { \ + return ::dusk::mods::detail::HookRecordFor<(target), \ + ::dusk::mods::FixedString{#target}>::Holder::record.resolved; \ + } \ } #define DEFINE_HOOK_SYMBOL(name, sig, alias) \ diff --git a/include/mods/meta.hpp b/include/mods/meta.hpp index e99334cbb3..e3e9c236df 100644 --- a/include/mods/meta.hpp +++ b/include/mods/meta.hpp @@ -6,6 +6,7 @@ #include #include #include +#include /* * modmeta records. Each IMPORT_SERVICE/EXPORT_SERVICE/DEFINE_HOOK use places one @@ -141,44 +142,16 @@ struct member_traits { using Class = C; }; -consteval size_t cstr_len(const char* s) { - size_t n = 0; - while (s[n] != '\0') { - ++n; - } - return n; -} - -consteval void copy_service_id(char (&dst)[MOD_META_SERVICE_ID_SIZE], const char* id) { +consteval ModMetaServiceId make_service_id(const char* id) { + ModMetaServiceId out{}; size_t n = 0; for (; id[n] != '\0'; ++n) { if (n + 1 >= MOD_META_SERVICE_ID_SIZE) { throw "service id exceeds MOD_META_SERVICE_ID_SIZE"; } - dst[n] = id[n]; + out.chars[n] = id[n]; } -} - -consteval ModMetaImport make_import( - const char* serviceId, uint16_t major, uint16_t minMinor, uint8_t flags, void* slot) { - ModMetaImport r{}; - r.rec = {sizeof(ModMetaImport), MOD_META_IMPORT, flags}; - r.major_version = major; - r.min_minor_version = minMinor; - r.slot = slot; - copy_service_id(r.service_id, serviceId); - return r; -} - -consteval ModMetaExport make_export( - const char* serviceId, uint16_t major, uint16_t minor, uint8_t flags, const void* service) { - ModMetaExport r{}; - r.rec = {sizeof(ModMetaExport), MOD_META_EXPORT, flags}; - r.major_version = major; - r.minor_version = minor; - r.service = service; - copy_service_id(r.service_id, serviceId); - return r; + return out; } consteval ModMetaHeader make_header() { @@ -227,40 +200,74 @@ constexpr size_t align_up(size_t n) { return (n + (N - 1)) & ~(N - 1); } +template +struct HookMemNames { + char chars[N]{}; + size_t len{}; +}; + template -consteval auto make_hook_record() { - using F = decltype(Target); +consteval auto make_hook_mem_names() { + using C = member_traits::Class; // Strip the leading '&' of the stringified target expression for display. constexpr size_t dispFrom = Disp.chars[0] == '&' ? 1 : 0; constexpr size_t dispLen = sizeof(Disp.chars) - 1 - dispFrom; - if constexpr (std::is_member_function_pointer_v) { - using C = member_traits::Class; - static_assert(sizeof(F) <= 16, "unsupported pointer-to-member representation"); - constexpr auto vtbl = vtable_symbol(); - constexpr size_t vtblLen = std::string_view{vtbl.data()}.size(); - constexpr size_t n = align_up<8>(vtblLen + 1 + dispLen + 1); - HookMemRecord r{}; - r.rec = {sizeof(r), MOD_META_HOOK_MEM, 0}; - r.pmf.fn = Target; - size_t at = 0; - for (size_t i = 0; i < vtblLen; ++i) { - r.names[at++] = vtbl[i]; - } - r.names[at++] = '\0'; - for (size_t i = 0; i < dispLen; ++i) { - r.names[at++] = Disp.chars[dispFrom + i]; - } - return r; - } else { - static_assert(std::is_pointer_v && std::is_function_v>, - "hook target must be a function or member function"); - HookFnRecord r{}; - r.rec = {sizeof(r), MOD_META_HOOK_FN, 0}; - r.target = Target; - return r; + constexpr auto vtbl = vtable_symbol(); + constexpr size_t vtblLen = std::string_view{vtbl.data()}.size(); + HookMemNames(vtblLen + 1 + dispLen + 1)> r{}; + size_t at = 0; + for (size_t i = 0; i < vtblLen; ++i) { + r.chars[at++] = vtbl[i]; } + r.chars[at++] = '\0'; + for (size_t i = 0; i < dispLen; ++i) { + r.chars[at++] = Disp.chars[dispFrom + i]; + } + r.len = sizeof(r.chars); + return r; } +/* + * MSVC constant-evaluates a pointer-to-member only when every other operand in the + * initializer is a literal: no consteval calls, constexpr-object copies, or default + * member initializers. + */ +template +struct HookMemHolder { + using F = decltype(Target); + static_assert(sizeof(F) <= 16, "unsupported pointer-to-member representation"); + MOD_META_RECORD static constinit inline HookMemRecord record = { + {sizeof(HookMemRecord), MOD_META_HOOK_MEM, 0}, 0, {Target}, nullptr, + {Cs...}}; +}; + +template +struct HookFnHolder { + using F = decltype(Target); + static_assert(std::is_pointer_v && std::is_function_v>, + "hook target must be a function or member function"); + MOD_META_RECORD static constinit inline HookFnRecord record = { + {sizeof(HookFnRecord), MOD_META_HOOK_FN, 0}, 0, Target, nullptr}; +}; + +template > +struct HookRecordFor { + using Holder = HookFnHolder; +}; + +template +struct HookRecordFor { + template + struct Bind; + template + struct Bind> { + using Type = HookMemHolder().chars[Is]...>; + }; + using Holder = + Bind().len>>::Type; +}; + template consteval auto make_hook_name_record() { constexpr size_t len = sizeof(Name.chars) - 1; diff --git a/include/mods/service.hpp b/include/mods/service.hpp index e6c5c395e1..f54b3f2149 100644 --- a/include/mods/service.hpp +++ b/include/mods/service.hpp @@ -46,10 +46,13 @@ inline ModResult set_error(ModError* outError, ModResult code, const char* messa #define IMPORT_SERVICE_EX( \ service_type, variable, service_id_value, major_value, min_minor_value, flags_value) \ static const service_type* variable = nullptr; \ - MOD_META_RECORD static constinit ModMetaImport mod_meta_import_##variable = \ - ::dusk::mods::detail::make_import((service_id_value), static_cast(major_value), \ - static_cast(min_minor_value), static_cast(flags_value), \ - &(variable)) + MOD_META_RECORD static constinit ModMetaImport mod_meta_import_##variable = { \ + {sizeof(ModMetaImport), MOD_META_IMPORT, static_cast(flags_value)}, \ + static_cast(major_value), \ + static_cast(min_minor_value), \ + &(variable), \ + ::dusk::mods::detail::make_service_id(service_id_value), \ + } #define IMPORT_SERVICE_VERSION(service_type, variable, min_minor_value) \ IMPORT_SERVICE_EX(service_type, variable, ::dusk::mods::ServiceTraits::id, \ @@ -67,15 +70,23 @@ inline ModResult set_error(ModError* outError, ModResult code, const char* messa IMPORT_OPTIONAL_SERVICE_VERSION(service_type, variable, 0) #define EXPORT_SERVICE_AS(instance, service_id_value) \ - MOD_META_RECORD static constinit ModMetaExport mod_meta_export_##instance = \ - ::dusk::mods::detail::make_export((service_id_value), (instance).header.major_version, \ - (instance).header.minor_version, SERVICE_EXPORT_STATIC, &(instance)) + MOD_META_RECORD static constinit ModMetaExport mod_meta_export_##instance = { \ + {sizeof(ModMetaExport), MOD_META_EXPORT, SERVICE_EXPORT_STATIC}, \ + (instance).header.major_version, \ + (instance).header.minor_version, \ + &(instance), \ + ::dusk::mods::detail::make_service_id(service_id_value), \ + } #define EXPORT_SERVICE(instance) \ EXPORT_SERVICE_AS( \ instance, ::dusk::mods::ServiceTraits>::id) #define EXPORT_DEFERRED_SERVICE(token, service_id_value, major_value, minor_value) \ - MOD_META_RECORD static constinit ModMetaExport mod_meta_export_##token = \ - ::dusk::mods::detail::make_export((service_id_value), static_cast(major_value), \ - static_cast(minor_value), SERVICE_EXPORT_DEFERRED, nullptr) + MOD_META_RECORD static constinit ModMetaExport mod_meta_export_##token = { \ + {sizeof(ModMetaExport), MOD_META_EXPORT, SERVICE_EXPORT_DEFERRED}, \ + static_cast(major_value), \ + static_cast(minor_value), \ + nullptr, \ + ::dusk::mods::detail::make_service_id(service_id_value), \ + } diff --git a/src/dusk/mods/loader/loader.cpp b/src/dusk/mods/loader/loader.cpp index 11077532fa..9431160bd2 100644 --- a/src/dusk/mods/loader/loader.cpp +++ b/src/dusk/mods/loader/loader.cpp @@ -288,7 +288,7 @@ static bool parse_meta(NativeMod& native, LoadedMod& mod) { return invalid("truncated import record"); } auto* record = reinterpret_cast(const_cast(cursor)); - if (!terminated_within(record->service_id, sizeof(record->service_id))) { + if (!terminated_within(record->service_id.chars, sizeof(record->service_id.chars))) { return invalid("unterminated import service id"); } parsed.imports.push_back(record); @@ -299,7 +299,7 @@ static bool parse_meta(NativeMod& native, LoadedMod& mod) { return invalid("truncated export record"); } auto* record = reinterpret_cast(const_cast(cursor)); - if (!terminated_within(record->service_id, sizeof(record->service_id))) { + if (!terminated_within(record->service_id.chars, sizeof(record->service_id.chars))) { return invalid("unterminated export service id"); } parsed.exports.push_back(record); @@ -552,18 +552,18 @@ static ModManifestInfo build_manifest_info(const ModMetaParsed& parsed) { ModManifestInfo info; info.imports.reserve(parsed.imports.size()); for (const auto* record : parsed.imports) { - if (!svc::valid_service_id(record->service_id)) { + if (!svc::valid_service_id(record->service_id.chars)) { continue; } - info.imports.push_back({record->service_id, record->major_version, + info.imports.push_back({record->service_id.chars, record->major_version, (record->rec.flags & SERVICE_IMPORT_OPTIONAL) == 0}); } info.exports.reserve(parsed.exports.size()); for (const auto* record : parsed.exports) { - if (!svc::valid_service_id(record->service_id)) { + if (!svc::valid_service_id(record->service_id.chars)) { continue; } - info.exports.push_back({record->service_id, record->major_version}); + info.exports.push_back({record->service_id.chars, record->major_version}); } return info; } @@ -608,11 +608,11 @@ static void warn_unpublished_deferred_exports(const LoadedMod& mod) { continue; } const auto* record = - svc::find_service_record(serviceExport->service_id, serviceExport->major_version); + svc::find_service_record(serviceExport->service_id.chars, serviceExport->major_version); if (record != nullptr && record->service == nullptr) { log::write(mod.metadata.id, LOG_LEVEL_WARN, "declared deferred service '{}@{}' but never published it during initialization", - serviceExport->service_id, serviceExport->major_version); + serviceExport->service_id.chars, serviceExport->major_version); } } } diff --git a/src/dusk/mods/svc/registry.cpp b/src/dusk/mods/svc/registry.cpp index 2f51791af3..2033d47475 100644 --- a/src/dusk/mods/svc/registry.cpp +++ b/src/dusk/mods/svc/registry.cpp @@ -222,7 +222,7 @@ bool ModLoader::register_static_service_exports(LoadedMod& mod) { } for (const auto* serviceExport : mod.native->parsed.exports) { - if (!svc::valid_service_id(serviceExport->service_id)) { + if (!svc::valid_service_id(serviceExport->service_id.chars)) { fail_mod(mod, MOD_INVALID_ARGUMENT, "Invalid service export descriptor"); return false; } @@ -234,7 +234,7 @@ bool ModLoader::register_static_service_exports(LoadedMod& mod) { } const auto result = - svc::register_service(serviceExport->service_id, serviceExport->major_version, + svc::register_service(serviceExport->service_id.chars, serviceExport->major_version, serviceExport->minor_version, serviceExport->service, &mod, deferred); if (result != MOD_OK) { fail_mod(mod, result, "Service export registration failed"); @@ -262,8 +262,8 @@ std::string ModLoader::describe_missing_import( continue; } for (const auto* serviceExport : other.native->parsed.exports) { - if (svc::valid_service_id(serviceExport->service_id) && - std::string_view{serviceExport->service_id} == serviceId && + if (svc::valid_service_id(serviceExport->service_id.chars) && + std::string_view{serviceExport->service_id.chars} == serviceId && serviceExport->major_version == majorVersion) { return fmt::format("Required service {}@{} unavailable: provider '{}' {}", @@ -282,12 +282,14 @@ bool ModLoader::resolve_service_imports(LoadedMod& mod) { } for (const auto* serviceImport : mod.native->parsed.imports) { - if (!svc::valid_service_id(serviceImport->service_id) || serviceImport->slot == nullptr) { + if (!svc::valid_service_id(serviceImport->service_id.chars) || + serviceImport->slot == nullptr) + { fail_mod(mod, MOD_INVALID_ARGUMENT, "Invalid service import descriptor"); return false; } - const auto* service = svc::find_service(serviceImport->service_id, + const auto* service = svc::find_service(serviceImport->service_id.chars, serviceImport->major_version, serviceImport->min_minor_version); if (service == nullptr) { *static_cast(serviceImport->slot) = nullptr; @@ -296,8 +298,8 @@ bool ModLoader::resolve_service_imports(LoadedMod& mod) { } fail_mod(mod, MOD_UNAVAILABLE, - describe_missing_import(serviceImport->service_id, serviceImport->major_version, - serviceImport->min_minor_version)); + describe_missing_import(serviceImport->service_id.chars, + serviceImport->major_version, serviceImport->min_minor_version)); return false; }