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.
This commit is contained in:
Luke Street
2026-07-13 01:07:30 -06:00
parent 9687a9d95a
commit cca104584e
6 changed files with 118 additions and 90 deletions
+6 -2
View File
@@ -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);
+7 -3
View File
@@ -136,10 +136,14 @@ struct NamedHook<Name, R(A...)> : HookImpl<detail::NameTag<Name>, 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) \
+66 -59
View File
@@ -6,6 +6,7 @@
#include <cstddef>
#include <string_view>
#include <type_traits>
#include <utility>
/*
* modmeta records. Each IMPORT_SERVICE/EXPORT_SERVICE/DEFINE_HOOK use places one
@@ -141,44 +142,16 @@ struct member_traits<R (C::*)(A...) const> {
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 <size_t N>
struct HookMemNames {
char chars[N]{};
size_t len{};
};
template <auto Target, FixedString Disp>
consteval auto make_hook_record() {
using F = decltype(Target);
consteval auto make_hook_mem_names() {
using C = member_traits<decltype(Target)>::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<F>) {
using C = member_traits<F>::Class;
static_assert(sizeof(F) <= 16, "unsupported pointer-to-member representation");
constexpr auto vtbl = vtable_symbol<C>();
constexpr size_t vtblLen = std::string_view{vtbl.data()}.size();
constexpr size_t n = align_up<8>(vtblLen + 1 + dispLen + 1);
HookMemRecord<F, n> 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<F> && std::is_function_v<std::remove_pointer_t<F>>,
"hook target must be a function or member function");
HookFnRecord<F> r{};
r.rec = {sizeof(r), MOD_META_HOOK_FN, 0};
r.target = Target;
return r;
constexpr auto vtbl = vtable_symbol<C>();
constexpr size_t vtblLen = std::string_view{vtbl.data()}.size();
HookMemNames<align_up<8>(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 <auto Target, char... Cs>
struct HookMemHolder {
using F = decltype(Target);
static_assert(sizeof(F) <= 16, "unsupported pointer-to-member representation");
MOD_META_RECORD static constinit inline HookMemRecord<F, sizeof...(Cs)> record = {
{sizeof(HookMemRecord<F, sizeof...(Cs)>), MOD_META_HOOK_MEM, 0}, 0, {Target}, nullptr,
{Cs...}};
};
template <auto Target>
struct HookFnHolder {
using F = decltype(Target);
static_assert(std::is_pointer_v<F> && std::is_function_v<std::remove_pointer_t<F>>,
"hook target must be a function or member function");
MOD_META_RECORD static constinit inline HookFnRecord<F> record = {
{sizeof(HookFnRecord<F>), MOD_META_HOOK_FN, 0}, 0, Target, nullptr};
};
template <auto Target, FixedString Disp,
bool = std::is_member_function_pointer_v<decltype(Target)>>
struct HookRecordFor {
using Holder = HookFnHolder<Target>;
};
template <auto Target, FixedString Disp>
struct HookRecordFor<Target, Disp, true> {
template <class Seq>
struct Bind;
template <size_t... Is>
struct Bind<std::index_sequence<Is...>> {
using Type = HookMemHolder<Target, make_hook_mem_names<Target, Disp>().chars[Is]...>;
};
using Holder =
Bind<std::make_index_sequence<make_hook_mem_names<Target, Disp>().len>>::Type;
};
template <FixedString Name>
consteval auto make_hook_name_record() {
constexpr size_t len = sizeof(Name.chars) - 1;
+21 -10
View File
@@ -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<uint16_t>(major_value), \
static_cast<uint16_t>(min_minor_value), static_cast<uint8_t>(flags_value), \
&(variable))
MOD_META_RECORD static constinit ModMetaImport mod_meta_import_##variable = { \
{sizeof(ModMetaImport), MOD_META_IMPORT, static_cast<uint8_t>(flags_value)}, \
static_cast<uint16_t>(major_value), \
static_cast<uint16_t>(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<service_type>::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<std::remove_cv_t<decltype(instance)>>::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<uint16_t>(major_value), \
static_cast<uint16_t>(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<uint16_t>(major_value), \
static_cast<uint16_t>(minor_value), \
nullptr, \
::dusk::mods::detail::make_service_id(service_id_value), \
}
+8 -8
View File
@@ -288,7 +288,7 @@ static bool parse_meta(NativeMod& native, LoadedMod& mod) {
return invalid("truncated import record");
}
auto* record = reinterpret_cast<ModMetaImport*>(const_cast<uint8_t*>(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<ModMetaExport*>(const_cast<uint8_t*>(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);
}
}
}
+10 -8
View File
@@ -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<const void**>(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;
}