diff --git a/docs/modding.md b/docs/modding.md index 6f2d1a8580..3823bd05a4 100644 --- a/docs/modding.md +++ b/docs/modding.md @@ -469,8 +469,14 @@ Mods may hook the vast majority of game functions, including file-local static, #include "mods/svc/hook.h" IMPORT_SERVICE(HookService, svc_hook); + +DEFINE_HOOK(&daAlink_c::posMove, LinkPosMove); +DEFINE_HOOK(&daAlink_c::execute, LinkExecute); ``` +Every hook target must be **declared** at namespace scope with `DEFINE_HOOK` (a target you can name in C++) or +`DEFINE_HOOK_SYMBOL` (a symbol name). + ### Pre-hooks Run before the original. Return `HOOK_SKIP_ORIGINAL` to cancel it (post-hooks still run). @@ -484,7 +490,7 @@ HookAction on_pos_move_pre(ModContext*, void* args, void* retval, void* userdata return HOOK_CONTINUE; } -dusk::mods::hook_add_pre<&daAlink_c::posMove>(svc_hook, on_pos_move_pre); +dusk::mods::hook_add_pre(svc_hook, on_pos_move_pre); ``` ### Post-hooks @@ -495,24 +501,22 @@ if any. ```cpp void on_pos_move_post(ModContext*, void* args, void* retval, void* userdata) { ... } -dusk::mods::hook_add_post<&daAlink_c::posMove>(svc_hook, on_pos_move_post); +dusk::mods::hook_add_post(svc_hook, on_pos_move_post); ``` ### Replace-hooks -Substitute the original entirely. Call through to it via `Hook<...>::g_orig` if needed: +Substitute the original entirely. Call through to it via the declaration's `g_orig` if needed: ```cpp -using ExecuteEntry = dusk::mods::Hook<&daAlink_c::execute>; - void on_execute_replace(ModContext*, void* args, void* retval, void*) { - int result = ExecuteEntry::g_orig(dusk::mods::arg(args, 0)); + int result = LinkExecute::g_orig(dusk::mods::arg(args, 0)); if (retval != nullptr) { *static_cast(retval) = result; } } -dusk::mods::hook_replace<&daAlink_c::execute>(svc_hook, on_execute_replace); +dusk::mods::hook_replace(svc_hook, on_execute_replace); ``` By default a second replace-hook on the same function is a conflict; `HookOptions` (`replace_policy`, `priority`, @@ -525,10 +529,8 @@ Functions you can't name in C++ (file-local statics, private class members, anyt symbol name instead. You must supply the signature along with the name. ```cpp -// static callback used by Link's hookshot collider in d_a_alink_hook.inc -using HookshotHit = dusk::mods::NamedHook< - "daAlink_hookshotAtHitCallBack", - void(fopAc_ac_c*, dCcD_GObjInf*, fopAc_ac_c*, dCcD_GObjInf*)>; +DEFINE_HOOK_SYMBOL("daAlink_hookshotAtHitCallBack", + void(fopAc_ac_c*, dCcD_GObjInf*, fopAc_ac_c*, dCcD_GObjInf*), HookshotHit); dusk::mods::hook_add_pre(svc_hook, on_hookshot_hit_pre); ... @@ -537,9 +539,16 @@ HookshotHit::g_orig(link, atObjInf, target, tgObjInf); // call through to the o Class member functions must include `Class*` as the first argument. -The install fails with the resolve error when the name is missing (`MOD_UNAVAILABLE`), ambiguous (`MOD_CONFLICT`), -or the manifest is absent (`MOD_UNSUPPORTED`). Unlike `Hook<&Class::method>`, the signature is **not** -compiler-checked: a mismatched signature will corrupt the call. +Two spellings work on every platform: + +- **Display names** (`daAlink_c::posMove`, `fapGm_Before`): the qualified name with no parameter list. They carry no + signature, so overload sets (and file-local statics sharing a name) return `MOD_CONFLICT`. +- **Decorated names** (`_ZN9daAlink_c7posMoveEv` / `?posMove@daAlink_c@@...`): the platform's mangled spelling in + dlsym convention (no Mach-O leading underscore). The escape hatch for overloads. + +Installing fails with `MOD_UNAVAILABLE` when it didn't resolve (missing, ambiguous, or no symbol manifest). Unlike +`DEFINE_HOOK`, the signature is **not** compiler-checked: a mismatched signature will corrupt the +call. ### Reading and writing arguments @@ -552,6 +561,8 @@ T& ref = dusk::mods::arg_ref(args, n); // read/write reference ``` ```cpp +DEFINE_HOOK(fopAcM_createItem, CreateItem); + // fpc_ProcID fopAcM_createItem(..., int itemNo, ...): turn heart drops into green rupees HookAction on_create_item_pre(ModContext*, void* args, void*, void*) { int& itemNo = dusk::mods::arg_ref(args, 1); @@ -561,36 +572,11 @@ HookAction on_create_item_pre(ModContext*, void* args, void*, void*) { return HOOK_CONTINUE; } -dusk::mods::hook_add_pre<&fopAcM_createItem>(svc_hook, on_create_item_pre); +dusk::mods::hook_add_pre(svc_hook, on_create_item_pre); ``` For reference parameters (e.g. `const cXyz& pos`), `arg_ref` yields a direct reference. -### Resolving symbols by name - -`resolve` looks a symbol up in the **symbol manifest**: a name→address map generated alongside every game build and -keyed to that exact binary. It covers the whole image, including functions that aren't exported (file-local statics), -which makes them hookable: - -```cpp -IMPORT_SERVICE(HookService, svc_hook); - -void* addr = nullptr; -uint32_t flags = 0; -if (svc_hook->resolve(mod_ctx, "GXSetProjection", &addr, &flags) == MOD_OK) { - // addr is the function's real address in the running game; hook or call it. -} -``` - -Two spellings work on every platform: - -- **Display names** (`daAlink_c::posMove`, `fapGm_Before`): the qualified name with no parameter list. They carry no - signature, so overload sets (and file-local statics sharing a name) return `MOD_CONFLICT`. -- **Decorated names** (`_ZN9daAlink_c7posMoveEv` / `?posMove@daAlink_c@@...`): the platform's mangled spelling in - dlsym convention (no Mach-O leading underscore). The escape hatch for overloads. - -`MOD_UNSUPPORTED` means the manifest is missing or was built for a different game binary. - ### Game code ABI contract A primary consideration when letting mods link against the game is maintaining ABI stability across Dusklight diff --git a/include/dusk/mod_loader.hpp b/include/dusk/mod_loader.hpp index 9d77f6984a..a326c8d807 100644 --- a/include/dusk/mod_loader.hpp +++ b/include/dusk/mod_loader.hpp @@ -67,9 +67,32 @@ struct ModSearchDir { std::filesystem::path nativeLibDir; }; +struct ModMetaParsed { + uint32_t abiVersion = 0; + std::vector imports; + std::vector exports; + std::vector hookFns; + std::vector hookMems; + std::vector hookNames; +}; + +inline const char* hook_mem_vtable_symbol(const ModMetaHookMem& rec) { + return reinterpret_cast(&rec) + sizeof(ModMetaHookMem); +} + +inline const char* hook_mem_display_name(const ModMetaHookMem& rec) { + const char* vtable = hook_mem_vtable_symbol(rec); + return vtable + std::char_traits::length(vtable) + 1; +} + +inline const char* hook_name_symbol(const ModMetaHookName& rec) { + return reinterpret_cast(&rec) + sizeof(ModMetaHookName); +} + struct NativeMod { std::unique_ptr handle; - const ModManifest* manifest = nullptr; + const ModMeta* meta = nullptr; + ModMetaParsed parsed; ModContext** contextSymbol = nullptr; ModInitializeFn fn_initialize = nullptr; @@ -111,6 +134,11 @@ enum class NativeModStatus : u8 { */ MissingExport, + /** + * Mod's metadata record section is malformed. + */ + InvalidMetadata, + /** * Unknown error loading the native mod. */ @@ -126,7 +154,7 @@ struct LoadedMod { // Native lib is dlopen'd in place and stays resident for the session. Reload is unsupported. bool inPlace = false; - std::unique_ptr > cvarIsEnabled; + std::unique_ptr> cvarIsEnabled; config::Subscription enabledSubscription = 0; bool active = false; @@ -200,7 +228,7 @@ private: std::string path; }; - std::vector > m_mods; + std::vector> m_mods; std::vector m_searchDirs; std::filesystem::path m_cacheDir; std::vector m_pendingRequests; diff --git a/include/mods/api.h b/include/mods/api.h index 85e2f76545..b2999336eb 100644 --- a/include/mods/api.h +++ b/include/mods/api.h @@ -23,7 +23,7 @@ extern "C" { #define MOD_EXTERN_C #endif -#define MOD_ABI_VERSION 5u +#define MOD_ABI_VERSION 6u #define MOD_ERROR_MESSAGE_SIZE 512u typedef struct ModContext ModContext; @@ -37,7 +37,8 @@ typedef enum ModResult { MOD_INVALID_ARGUMENT = 5, } ModResult; -static_assert(sizeof(ModResult) == 4, "mod SDK enums must be int-sized; do not build mods with -fshort-enums"); +static_assert(sizeof(ModResult) == 4, + "mod SDK enums must be int-sized; do not build mods with -fshort-enums"); typedef struct ModError { uint32_t struct_size; @@ -108,41 +109,130 @@ typedef enum ServiceExportFlags { SERVICE_EXPORT_DEFERRED = 1u << 0u, } ServiceExportFlags; -typedef struct ServiceImport { - uint32_t struct_size; - const char* service_id; +/* + * Mod metadata records. + * + * A mod's manifest is a sequence of records in a dedicated section of the native library ("modmeta" + * on ELF, "__DATA,__modmeta" on Mach-O, "modmeta$a/$d/$z" on PE). + * + * The records are pure data. Every string is inline and NUL-terminated. Fields documented as + * runtime-only hold relocated pointers that are meaningless on disk; static parsers recover their + * targets from the file's relocation/bind entries instead. + * + * Layout rules: + * - Little-endian, 8-byte aligned; every record size is a multiple of 8. + * - Parsers must skip all-zero 8-byte units (linker padding) and records of unknown kind. + * - Exactly one MOD_META_HEADER record per library. + * + * The IMPORT_SERVICE/EXPORT_SERVICE/DEFINE_HOOK macros emit these records; mods do not construct + * them by hand. + */ + +#define MOD_META_SERVICE_ID_SIZE 64u + +/* Records are 8-byte aligned so the linker packs them without padding; most are naturally + * aligned by their pointer fields, the alignment below covers the rest. */ +#if defined(__cplusplus) +#define MOD_META_ALIGN alignas(8) +#elif defined(_MSC_VER) +#define MOD_META_ALIGN __declspec(align(8)) +#else +#define MOD_META_ALIGN __attribute__((aligned(8))) +#endif + +typedef enum ModMetaKind { + MOD_META_PAD = 0, + MOD_META_HEADER = 1, + MOD_META_IMPORT = 2, + MOD_META_EXPORT = 3, + MOD_META_HOOK_FN = 4, + MOD_META_HOOK_MEM = 5, + MOD_META_HOOK_NAME = 6, +} ModMetaKind; + +typedef struct ModMetaRecord { + uint16_t size; /* total record size in bytes, a multiple of 8 */ + uint8_t kind; /* ModMetaKind */ + uint8_t flags; /* ServiceImportFlags / ServiceExportFlags for imports/exports */ +} ModMetaRecord; + +typedef struct MOD_META_ALIGN ModMetaHeader { + ModMetaRecord rec; + uint32_t abi_version; +} ModMetaHeader; + +static_assert(sizeof(ModMetaHeader) == 8); + +typedef struct MOD_META_ALIGN ModMetaImport { + ModMetaRecord rec; uint16_t major_version; uint16_t min_minor_version; - uint32_t flags; - void* slot; -} ServiceImport; + void* slot; /* runtime only */ + char service_id[MOD_META_SERVICE_ID_SIZE]; +} ModMetaImport; -typedef struct ServiceExport { - uint32_t struct_size; - const char* service_id; +static_assert(sizeof(ModMetaImport) == 16 + MOD_META_SERVICE_ID_SIZE); + +typedef struct MOD_META_ALIGN ModMetaExport { + ModMetaRecord rec; uint16_t major_version; uint16_t minor_version; - uint32_t flags; - const void* service; -} ServiceExport; + const void* service; /* runtime only */ + char service_id[MOD_META_SERVICE_ID_SIZE]; +} ModMetaExport; -typedef struct ModManifest { +static_assert(sizeof(ModMetaExport) == 16 + MOD_META_SERVICE_ID_SIZE); + +/* Hook on a function named at link time: `target` carries the &fn relocation. */ +typedef struct MOD_META_ALIGN ModMetaHookFn { + ModMetaRecord rec; + uint32_t reserved; + void* target; /* runtime only */ + void* resolved; /* runtime only */ +} ModMetaHookFn; + +static_assert(sizeof(ModMetaHookFn) == 24); + +/* + * Hook on a member function: `pmf` holds the compiler's pointer-to-member representation + * (non-virtual: a function address relocation; virtual Itanium/AAPCS: literal slot words). Two + * NUL-terminated strings follow `resolved`: the class vtable symbol (empty if the class name is not + * representable), then the stringified target for tooling display. + */ +typedef struct MOD_META_ALIGN ModMetaHookMem { + ModMetaRecord rec; + uint32_t reserved; + unsigned char pmf[16]; + void* resolved; /* runtime only */ +} ModMetaHookMem; + +static_assert(sizeof(ModMetaHookMem) == 32); + +/* + * Hook on a function by symbol name, for targets that cannot be named in C++ (file-local statics, + * private members). One NUL-terminated string follows `resolved`; it may be either the platform + * mangled name or the demangled qualified display name. + */ +typedef struct MOD_META_ALIGN ModMetaHookName { + ModMetaRecord rec; + uint32_t reserved; + void* resolved; /* runtime only */ +} ModMetaHookName; + +static_assert(sizeof(ModMetaHookName) == 16); + +typedef struct ModMeta { uint32_t struct_size; - uint32_t abi_version; - const ServiceImport* imports; - size_t import_count; - const ServiceExport* exports; - size_t export_count; -} ModManifest; + const void* records_begin; + const void* records_end; +} ModMeta; -typedef const ModManifest* (*ModGetManifestFn)(void); +MOD_EXPORT extern const ModMeta mod_meta; typedef ModResult (*ModInitializeFn)(ModError* out_error); typedef ModResult (*ModUpdateFn)(ModError* out_error); typedef ModResult (*ModShutdownFn)(ModError* out_error); -MOD_EXPORT const ModManifest* mod_get_manifest(void); - MOD_EXPORT ModResult mod_initialize(ModError* out_error); MOD_EXPORT ModResult mod_update(ModError* out_error); MOD_EXPORT ModResult mod_shutdown(ModError* out_error); diff --git a/include/mods/hook.hpp b/include/mods/hook.hpp index c58bd719c9..0f74ce23a6 100644 --- a/include/mods/hook.hpp +++ b/include/mods/hook.hpp @@ -2,11 +2,7 @@ #include "mods/svc/hook.h" -#include -#include -#include #include -#include #include namespace dusk::mods { @@ -14,255 +10,20 @@ namespace dusk::mods { template T arg(void* argsRaw, int n) noexcept { void** args = static_cast(argsRaw); - return *static_cast > >(args[n]); + return *static_cast>>(args[n]); } template std::remove_reference_t& arg_ref(void* argsRaw, int n) noexcept { void** args = static_cast(argsRaw); - return *static_cast > >(args[n]); + return *static_cast>>(args[n]); } -template -void* mfp_addr(F fn) noexcept { - void* p = nullptr; - static_assert(sizeof(fn) >= sizeof(void*), "unexpected function pointer size"); - std::memcpy(&p, &fn, sizeof(void*)); - return p; -} - -/* A string usable as a template argument: carries the hook target's symbol name and - * makes each NamedHook instantiation's static state unique. */ -template -struct FixedString { - char chars[N]{}; - constexpr FixedString(const char (&s)[N]) noexcept { - for (size_t i = 0; i < N; ++i) { - chars[i] = s[i]; - } - } -}; - -namespace detail { - -template -constexpr std::string_view class_name() { -#if defined(__clang__) || defined(__GNUC__) - // "... class_name() [T = daAlink_c]" / "... [with T = daAlink_c; ...]" - constexpr std::string_view fn = __PRETTY_FUNCTION__; - constexpr size_t start = fn.find("T = ") + 4; - return fn.substr(start, fn.find_first_of(";]", start) - start); -#elif defined(_MSC_VER) - // "... class_name(void)" - constexpr std::string_view fn = __FUNCSIG__; - constexpr size_t start = fn.find("class_name<") + 11; - constexpr std::string_view name = fn.substr(start, fn.rfind(">(") - start); - if constexpr (name.starts_with("class ")) { - return name.substr(6); - } else if constexpr (name.starts_with("struct ")) { - return name.substr(7); - } else { - return name; - } -#else -#error "unsupported compiler" -#endif -} - -/* The manifest name of C's vtable. Only unscoped, non-template class names are - * supported (an empty result fails resolution and the install reports it). */ -template -constexpr auto vtable_symbol() { - constexpr std::string_view name = class_name(); - constexpr bool simple = name.find_first_of(":<> ") == std::string_view::npos; - // "_ZTV" + decimal length + name / "??_7" + name + "@@6B@", NUL-terminated - std::array out{}; - if constexpr (!simple) { - return out; - } - size_t n = 0; -#if defined(_WIN32) - for (char c : {'?', '?', '_', '7'}) { - out[n++] = c; - } - for (char c : name) { - out[n++] = c; - } - for (char c : {'@', '@', '6', 'B', '@'}) { - out[n++] = c; - } -#else - for (char c : {'_', 'Z', 'T', 'V'}) { - out[n++] = c; - } - size_t len = name.size(); - char digits[8]{}; - size_t d = 0; - while (len != 0) { - digits[d++] = static_cast('0' + len % 10); - len /= 10; - } - while (d != 0) { - out[n++] = digits[--d]; - } - for (char c : name) { - out[n++] = c; - } -#endif - return out; -} - -#if defined(_WIN32) -/* Follow jump stubs, then match the MSVC vcall thunk a virtual mfp points at. - * Returns the vtable slot's byte offset, or npos when fn is not a vcall thunk. */ -inline size_t vcall_slot_offset(const void*& fn) noexcept { - constexpr size_t npos = static_cast(-1); -#if defined(_M_X64) || defined(__x86_64__) - const auto* p = static_cast(fn); - for (int i = 0; i < 8 && p[0] == 0xE9; ++i) { // incremental-link stubs - int32_t rel; - std::memcpy(&rel, p + 1, 4); - p += 5 + rel; - } - fn = p; - // The vptr load. Unoptimized clang-cl thunks spill/reload rcx first - // (push rax; mov [rsp], rcx; mov rcx, [rsp]), so scan a short window. - const uint8_t* q = nullptr; - for (int i = 0; i <= 12; ++i) { - if (p[i] == 0x48 && p[i + 1] == 0x8B && p[i + 2] == 0x01) { // mov rax, [rcx] - q = p + i + 3; - break; - } - } - if (q == nullptr) { - return npos; - } - if (q[0] == 0xFF && q[1] == 0x20) { // jmp [rax] (MSVC) - return 0; - } - if (q[0] == 0xFF && q[1] == 0x60) { // jmp [rax + imm8] - return static_cast(q[2]); - } - if (q[0] == 0xFF && q[1] == 0xA0) { // jmp [rax + imm32] - int32_t off; - std::memcpy(&off, q + 2, 4); - return off; - } - // clang-cl: mov rax, [rax + off]; (pop r10;) jmp rax. Requiring the jmp rax - // distinguishes the thunk from an ordinary getter that begins the same way. - if (q[0] == 0x48 && q[1] == 0x8B && (q[2] == 0x00 || q[2] == 0x40 || q[2] == 0x80)) { - size_t off = 0; - const uint8_t* r = q + 3; - if (q[2] == 0x40) { - off = static_cast(q[3]); - r = q + 4; - } else if (q[2] == 0x80) { - int32_t off32; - std::memcpy(&off32, q + 3, 4); - off = off32; - r = q + 7; - } - for (int i = 0; i <= 8; ++i) { - if (r[i] == 0xFF && r[i + 1] == 0xE0) { // jmp rax (48 REX optional) - return off; - } - } - } - return npos; -#elif defined(_M_ARM64) || defined(__aarch64__) - const auto* p = static_cast(fn); - uint32_t insn[3]; - for (int i = 0; i < 8; ++i) { // incremental-link `b` stubs - std::memcpy(insn, p, 4); - if ((insn[0] & 0xFC000000u) != 0x14000000u) { - break; - } - const auto imm26 = static_cast(insn[0] << 6) >> 6; - p += static_cast(imm26) * 4; - } - fn = p; - std::memcpy(insn, p, 12); - // ldr Xt, [x0]; ldr Xs, [Xt, #imm12*8]; br Xs - if ((insn[0] & 0xFFFFFFE0u) != 0xF9400000u) { - return npos; - } - const uint32_t t = insn[0] & 0x1Fu; - if ((insn[1] & 0xFFC003E0u) != (0xF9400000u | (t << 5))) { - return npos; - } - const uint32_t s = insn[1] & 0x1Fu; - if (insn[2] != (0xD61F0000u | (s << 5))) { - return npos; - } - return ((insn[1] >> 10) & 0xFFFu) * 8; -#else - (void)fn; - return npos; -#endif -} -#endif - -/* Code address of the member function a mfp designates. Virtual mfps don't carry - * one; recover it from the class's vtable (resolved from the symbol manifest), so - * Hook works uniformly on virtual and non-virtual members. */ -template -ModResult member_target(const HookService* hooks, F mfp, void** out) { - *out = nullptr; - uintptr_t words[sizeof(F) > sizeof(uintptr_t) ? 2 : 1] = {}; - std::memcpy(words, &mfp, sizeof(words) < sizeof(F) ? sizeof(words) : sizeof(F)); - -#if defined(_WIN32) - const void* fn = reinterpret_cast(words[0]); - const size_t slot = vcall_slot_offset(fn); - if (slot == static_cast(-1)) { // not a vcall thunk: direct address - *out = const_cast(fn); - return MOD_OK; - } - void* vtable = nullptr; - const ModResult resolved = hooks->resolve(mod_ctx, vtable_symbol().data(), &vtable, nullptr); - if (resolved != MOD_OK) { - return resolved; - } - // ??_7 points at the first slot. - *out = *reinterpret_cast(static_cast(vtable) + slot); -#else -#if defined(__aarch64__) || defined(__arm__) - // AAPCS C++ ABI: the virtual flag is bit 0 of the adjustment word (function - // addresses can't spare their low bit), and ptr holds the slot offset directly. - const bool isVirtual = (words[1] & 1) != 0; - const uintptr_t thisAdjust = words[1] >> 1; - const uintptr_t slotOffset = words[0]; -#else - // Itanium C++ ABI: virtual mfps set bit 0 of ptr; the slot offset is ptr - 1. - const bool isVirtual = (words[0] & 1) != 0; - const uintptr_t thisAdjust = words[1]; - const uintptr_t slotOffset = words[0] - 1; -#endif - if (!isVirtual) { // non-virtual: the address itself - *out = reinterpret_cast(words[0]); - return MOD_OK; - } - if (thisAdjust != 0) { - // this-adjusting mfp (member of a secondary base): the slot offset is - // relative to a vtable we can't locate. Hook the overrider by name instead. - return MOD_UNSUPPORTED; - } - void* vtable = nullptr; - const ModResult resolved = hooks->resolve(mod_ctx, vtable_symbol().data(), &vtable, nullptr); - if (resolved != MOD_OK) { - return resolved; - } - // _ZTV points at the offset-to-top slot; the address point mfps index from is - // two pointers in (past offset-to-top and the typeinfo pointer). - *out = *reinterpret_cast(static_cast(vtable) + 2 * sizeof(void*) + slotOffset); -#endif - return *out != nullptr ? MOD_OK : MOD_UNAVAILABLE; -} - -} // namespace detail - -/* Trampoline generator + per-target state shared by Hook and NamedHook. Tag makes - * each hooked target's statics distinct; the target address is filled in at install. */ +/* + * Trampoline generator + per-target state. Tag makes each hooked target's statics distinct; the + * target address comes from the declaration's metadata record, resolved by the host at mod + * initialization. + */ template struct HookImpl { static inline R (*g_orig)(A...) = nullptr; @@ -333,60 +94,60 @@ struct NameTag {}; } // namespace detail /* - * Typed hook on a function named at compile time (&daAlink_c::execute, &free_fn). - * Member functions may be virtual: the install decodes the member function pointer and hooks the - * class's own overrider. + * Typed base for a hook on a function named at compile time (&daAlink_c::execute, &free_fn). + * Instantiate through DEFINE_HOOK, which pairs it with the metadata record the host resolves. */ template struct Hook; template -struct Hook : HookImpl, R, C*, A...> { - static ModResult resolve_target(const HookService* hooks, void** out) { - return detail::member_target(hooks, Target, out); - } -}; +struct Hook : HookImpl, R, C*, A...> {}; template -struct Hook : HookImpl, R, const C*, A...> { - static ModResult resolve_target(const HookService* hooks, void** out) { - return detail::member_target(hooks, Target, out); - } -}; +struct Hook : HookImpl, R, const C*, A...> {}; template -struct Hook : HookImpl, R, A...> { - static ModResult resolve_target(const HookService*, void** out) { - *out = mfp_addr(Target); - return MOD_OK; - } -}; +struct Hook : HookImpl, R, A...> {}; /* - * Typed hook on a function by its symbol name, for targets you can't name in C++: file-local - * statics, private members, or symbols without a header. The signature is written free-style with - * the receiver first and is *not* compiler-checked. - * - * using HookshotHit = dusk::mods::NamedHook< - * "daAlink_hookshotAtHitCallBack", - * void(fopAc_ac_c*, dCcD_GObjInf*, fopAc_ac_c*, dCcD_GObjInf*)>; - * dusk::mods::hook_add_pre(svc_hook, on_hookshot_hit); + * Typed base for a hook on a function by its symbol name, for targets you can't name in C++: + * file-local statics, private members, or symbols without a header. The signature is written + * free-style with the receiver first and is *not* compiler-checked. Instantiate through + * DEFINE_HOOK_SYMBOL. */ template struct NamedHook; template -struct NamedHook : HookImpl, R, A...> { - static ModResult resolve_target(const HookService* hooks, void** out) { - HookSymbolFlags flags{}; - const ModResult resolved = hooks->resolve(mod_ctx, Name.chars, out, &flags); - if (resolved == MOD_OK && (flags & HOOK_SYMBOL_CODE) == 0) { - *out = nullptr; - return MOD_INVALID_ARGUMENT; - } - return resolved; +struct NamedHook : HookImpl, R, A...> {}; + +/* + * Declare a hook target. The declaration emits a metadata record that the host resolves at mod + * initialization. Every hook target must be declared. + * + * DEFINE_HOOK(&daAlink_c::execute, LinkExecute); + * DEFINE_HOOK_SYMBOL("daAlink_hookshotAtHitCallBack", + * void(fopAc_ac_c*, dCcD_GObjInf*, fopAc_ac_c*, dCcD_GObjInf*), HookshotHit); + * + * dusk::mods::hook_add_pre(svc_hook, on_link_execute); + * + * DEFINE_HOOK_SYMBOL names may be the platform mangled name (dlopen convention, no Mach-O + * leading underscore) or the demangled qualified display name; overloaded display names are + * 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}>(); \ + struct alias : ::dusk::mods::Hook<(target)> { \ + static void* resolved_target() { return mod_meta_hook_##alias.resolved; } \ + } + +#define DEFINE_HOOK_SYMBOL(name, sig, alias) \ + MOD_META_RECORD static constinit auto mod_meta_hook_##alias = \ + ::dusk::mods::detail::make_hook_name_record<::dusk::mods::FixedString{name}>(); \ + struct alias : ::dusk::mods::NamedHook<::dusk::mods::FixedString{name}, sig> { \ + static void* resolved_target() { return mod_meta_hook_##alias.resolved; } \ } -}; template ModResult hook_install(const HookService* hooks) { @@ -396,20 +157,16 @@ ModResult hook_install(const HookService* hooks) { Entry::hooks = hooks; if (Entry::target == nullptr) { - const ModResult resolved = Entry::resolve_target(hooks, &Entry::target); - if (resolved != MOD_OK) { - return resolved; + void* resolved = Entry::resolved_target(); + if (resolved == nullptr) { + return MOD_UNAVAILABLE; } + Entry::target = resolved; } return hooks->install(mod_ctx, Entry::target, reinterpret_cast(Entry::trampoline), reinterpret_cast(&Entry::g_orig)); } -template -ModResult hook_install(const HookService* hooks) { - return hook_install >(hooks); -} - template ModResult hook_add_pre( const HookService* hooks, HookPreFn callback, const HookOptions* options = nullptr) { @@ -421,12 +178,6 @@ ModResult hook_add_pre( return hooks->add_pre(mod_ctx, Entry::target, callback, options); } -template -ModResult hook_add_pre( - const HookService* hooks, HookPreFn callback, const HookOptions* options = nullptr) { - return hook_add_pre >(hooks, callback, options); -} - template ModResult hook_add_post( const HookService* hooks, HookPostFn callback, const HookOptions* options = nullptr) { @@ -438,12 +189,6 @@ ModResult hook_add_post( return hooks->add_post(mod_ctx, Entry::target, callback, options); } -template -ModResult hook_add_post( - const HookService* hooks, HookPostFn callback, const HookOptions* options = nullptr) { - return hook_add_post >(hooks, callback, options); -} - template ModResult hook_replace( const HookService* hooks, HookReplaceFn callback, const HookOptions* options = nullptr) { @@ -455,10 +200,4 @@ ModResult hook_replace( return hooks->replace(mod_ctx, Entry::target, callback, options); } -template -ModResult hook_replace( - const HookService* hooks, HookReplaceFn callback, const HookOptions* options = nullptr) { - return hook_replace >(hooks, callback, options); -} - } // namespace dusk::mods diff --git a/include/mods/meta.hpp b/include/mods/meta.hpp new file mode 100644 index 0000000000..e99334cbb3 --- /dev/null +++ b/include/mods/meta.hpp @@ -0,0 +1,276 @@ +#pragma once + +#include "mods/api.h" + +#include +#include +#include +#include + +/* + * modmeta records. Each IMPORT_SERVICE/EXPORT_SERVICE/DEFINE_HOOK use places one + * constant-initialized record object in the metadata section. + */ +#if defined(_WIN32) +#pragma section("modmeta$a", read, write) +#pragma section("modmeta$d", read, write) +#pragma section("modmeta$z", read, write) +#define MOD_META_RECORD __declspec(allocate("modmeta$d")) +#elif defined(__APPLE__) +#define MOD_META_RECORD __attribute__((section("__DATA,__modmeta"), used)) +#elif defined(__has_attribute) && __has_attribute(retain) +#define MOD_META_RECORD __attribute__((section("modmeta"), used, retain)) +#else +#define MOD_META_RECORD __attribute__((section("modmeta"), used)) +#endif + +/* Section bounds for the mod_meta descriptor */ +#if defined(_WIN32) +#define MOD_META_BOUNDS_DEFN \ + extern "C" { \ + __declspec(allocate("modmeta$a")) constinit unsigned long long mod_meta_bounds_begin = 0; \ + __declspec(allocate("modmeta$z")) constinit unsigned long long mod_meta_bounds_end = 0; \ + } +#define MOD_META_BOUNDS_BEGIN (&mod_meta_bounds_begin) +#define MOD_META_BOUNDS_END (&mod_meta_bounds_end) +#elif defined(__APPLE__) +extern "C" const unsigned char mod_meta_bounds_begin[] __asm("section$start$__DATA$__modmeta"); +extern "C" const unsigned char mod_meta_bounds_end[] __asm("section$end$__DATA$__modmeta"); +#define MOD_META_BOUNDS_DEFN +#define MOD_META_BOUNDS_BEGIN (mod_meta_bounds_begin) +#define MOD_META_BOUNDS_END (mod_meta_bounds_end) +#else +extern "C" const unsigned char __start_modmeta[]; +extern "C" const unsigned char __stop_modmeta[]; +#define MOD_META_BOUNDS_DEFN +#define MOD_META_BOUNDS_BEGIN (__start_modmeta) +#define MOD_META_BOUNDS_END (__stop_modmeta) +#endif + +namespace dusk::mods { + +/* A string usable as a template argument: carries a symbol/target name into record builders + * and makes each hook declaration's static state unique. */ +template +struct FixedString { + char chars[N]{}; + constexpr FixedString(const char (&s)[N]) noexcept { + for (size_t i = 0; i < N; ++i) { + chars[i] = s[i]; + } + } +}; + +namespace detail { + +template +constexpr std::string_view class_name() { +#if defined(__clang__) || defined(__GNUC__) + // "... class_name() [T = daAlink_c]" / "... [with T = daAlink_c; ...]" + constexpr std::string_view fn = __PRETTY_FUNCTION__; + constexpr size_t start = fn.find("T = ") + 4; + return fn.substr(start, fn.find_first_of(";]", start) - start); +#elif defined(_MSC_VER) + // "... class_name(void)" + constexpr std::string_view fn = __FUNCSIG__; + constexpr size_t start = fn.find("class_name<") + 11; + constexpr std::string_view name = fn.substr(start, fn.rfind(">(") - start); + if constexpr (name.starts_with("class ")) { + return name.substr(6); + } else if constexpr (name.starts_with("struct ")) { + return name.substr(7); + } else { + return name; + } +#else +#error "unsupported compiler" +#endif +} + +/* The symbol name of C's vtable. Only unscoped, non-template class names are supported (an + * empty result makes hooks on virtual members of C fail resolution, which is reported). */ +template +constexpr auto vtable_symbol() { + constexpr std::string_view name = class_name(); + constexpr bool simple = name.find_first_of(":<> ") == std::string_view::npos; + // "_ZTV" + decimal length + name / "??_7" + name + "@@6B@", NUL-terminated + std::array out{}; + if constexpr (!simple) { + return out; + } + size_t n = 0; +#if defined(_WIN32) + for (char c : {'?', '?', '_', '7'}) { + out[n++] = c; + } + for (char c : name) { + out[n++] = c; + } + for (char c : {'@', '@', '6', 'B', '@'}) { + out[n++] = c; + } +#else + for (char c : {'_', 'Z', 'T', 'V'}) { + out[n++] = c; + } + size_t len = name.size(); + char digits[8]{}; + size_t d = 0; + while (len != 0) { + digits[d++] = static_cast('0' + len % 10); + len /= 10; + } + while (d != 0) { + out[n++] = digits[--d]; + } + for (char c : name) { + out[n++] = c; + } +#endif + return out; +} + +template +struct member_traits; +template +struct member_traits { + using Class = C; +}; +template +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) { + 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]; + } +} + +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; +} + +consteval ModMetaHeader make_header() { + ModMetaHeader r{}; + r.rec = {sizeof(ModMetaHeader), MOD_META_HEADER, 0}; + r.abi_version = MOD_ABI_VERSION; + return r; +} + +/* + * Typed record variants: embedding the target as its native type makes the compiler emit the + * on-disk representation (relocations, PMF slot words) that static parsers read; the layouts + * match the byte-view structs in api.h. + */ + +template +struct HookFnRecord { + ModMetaRecord rec; + uint32_t reserved; + F target; + void* resolved; +}; + +template +struct HookMemRecord { + ModMetaRecord rec; + uint32_t reserved; + union { + F fn; + unsigned char raw[16]; + } pmf; + void* resolved; + char names[N]; +}; + +template +struct HookNameRecord { + ModMetaRecord rec; + uint32_t reserved; + void* resolved; + char name[N]; +}; + +template +constexpr size_t align_up(size_t n) { + return (n + (N - 1)) & ~(N - 1); +} + +template +consteval auto make_hook_record() { + using F = decltype(Target); + // 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; + } +} + +template +consteval auto make_hook_name_record() { + constexpr size_t len = sizeof(Name.chars) - 1; + HookNameRecord(len + 1)> r{}; + r.rec = {sizeof(r), MOD_META_HOOK_NAME, 0}; + for (size_t i = 0; i < len; ++i) { + r.name[i] = Name.chars[i]; + } + return r; +} + +} // namespace detail +} // namespace dusk::mods diff --git a/include/mods/service.hpp b/include/mods/service.hpp index d79b53f24d..e6c5c395e1 100644 --- a/include/mods/service.hpp +++ b/include/mods/service.hpp @@ -1,60 +1,17 @@ #pragma once #include "mods/api.h" +#include "mods/meta.hpp" #include #include #include -#include namespace dusk::mods { template struct ServiceTraits; -namespace detail { - -inline std::vector& imports() { - static std::vector entries; - return entries; -} - -inline std::vector& exports() { - static std::vector entries; - return entries; -} - -inline int register_import(ServiceImport entry) { - imports().push_back(entry); - return 0; -} - -inline int register_export(ServiceExport entry) { - exports().push_back(entry); - return 0; -} - -inline const ModManifest* manifest() { - static ModManifest manifest{ - sizeof(ModManifest), - MOD_ABI_VERSION, - nullptr, - 0, - nullptr, - 0, - }; - - auto& importEntries = imports(); - auto& exportEntries = exports(); - manifest.imports = importEntries.data(); - manifest.import_count = importEntries.size(); - manifest.exports = exportEntries.data(); - manifest.export_count = exportEntries.size(); - return &manifest; -} - -} // namespace detail - inline ModResult set_error(ModError* outError, ModResult code, const char* message) { if (outError != nullptr && outError->struct_size >= sizeof(ModError)) { outError->code = code; @@ -71,9 +28,16 @@ inline ModResult set_error(ModError* outError, ModResult code, const char* messa #define DEFINE_MOD() \ extern "C" { \ MOD_EXPORT ModContext* mod_ctx = nullptr; \ - MOD_EXPORT const ModManifest* mod_get_manifest(void) { \ - return ::dusk::mods::detail::manifest(); \ } \ + MOD_META_RECORD static constinit ModMetaHeader mod_meta_header_record = \ + ::dusk::mods::detail::make_header(); \ + MOD_META_BOUNDS_DEFN \ + extern "C" { \ + MOD_EXPORT constinit const ModMeta mod_meta = { \ + sizeof(ModMeta), \ + MOD_META_BOUNDS_BEGIN, \ + MOD_META_BOUNDS_END, \ + }; \ } // Declares `static const service_type* variable`, filled in by the host before mod_initialize. @@ -82,15 +46,10 @@ 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; \ - [[maybe_unused]] static const int mod_import_registration_##variable = \ - ::dusk::mods::detail::register_import(ServiceImport{ \ - sizeof(ServiceImport), \ - (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 = \ + ::dusk::mods::detail::make_import((service_id_value), static_cast(major_value), \ + static_cast(min_minor_value), static_cast(flags_value), \ + &(variable)) #define IMPORT_SERVICE_VERSION(service_type, variable, min_minor_value) \ IMPORT_SERVICE_EX(service_type, variable, ::dusk::mods::ServiceTraits::id, \ @@ -108,31 +67,15 @@ 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) \ - namespace { \ - const int mod_export_registration_##instance = \ - ::dusk::mods::detail::register_export(ServiceExport{ \ - sizeof(ServiceExport), \ - (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 = \ + ::dusk::mods::detail::make_export((service_id_value), (instance).header.major_version, \ + (instance).header.minor_version, SERVICE_EXPORT_STATIC, &(instance)) #define EXPORT_SERVICE(instance) \ EXPORT_SERVICE_AS( \ - instance, ::dusk::mods::ServiceTraits >::id) + instance, ::dusk::mods::ServiceTraits>::id) #define EXPORT_DEFERRED_SERVICE(token, service_id_value, major_value, minor_value) \ - namespace { \ - const int mod_deferred_export_registration_##token = \ - ::dusk::mods::detail::register_export(ServiceExport{ \ - sizeof(ServiceExport), \ - (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 = \ + ::dusk::mods::detail::make_export((service_id_value), static_cast(major_value), \ + static_cast(minor_value), SERVICE_EXPORT_DEFERRED, nullptr) diff --git a/mods/shadow_mod/src/mod.cpp b/mods/shadow_mod/src/mod.cpp index 0eec4fecef..4e3470c9be 100644 --- a/mods/shadow_mod/src/mod.cpp +++ b/mods/shadow_mod/src/mod.cpp @@ -93,10 +93,15 @@ constexpr float kMaxLightLookahead = 10000.0f; constexpr float kSunMoonDistance = 80000.0f; constexpr float kSunMoonZDistance = -48000.0f; -using ClipperSphereClip = int (J3DUClipper::*)(f32 const (*)[4], Vec, f32) const; -using ClipperBoxClip = int (J3DUClipper::*)(f32 const (*)[4], Vec*, Vec*) const; -constexpr ClipperSphereClip kClipperSphereClip = static_cast(&J3DUClipper::clip); -constexpr ClipperBoxClip kClipperBoxClip = static_cast(&J3DUClipper::clip); +DEFINE_HOOK(&dDlst_shadowControl_c::imageDraw, GameShadowImageDraw); +DEFINE_HOOK(&dDlst_shadowControl_c::draw, GameShadowDraw); +DEFINE_HOOK(&drawCloudShadow, CloudShadowDraw); +DEFINE_HOOK(static_cast(&J3DUClipper::clip), + ClipperSphereClip); +DEFINE_HOOK( + static_cast(&J3DUClipper::clip), + ClipperBoxClip); +DEFINE_HOOK(GXCopyTex, CopyTex); // Mirror of the WGSL Uniforms struct (keep in sync with res/shadow.wgsl). struct ShadowUniforms { @@ -1048,20 +1053,18 @@ MOD_EXPORT ModResult mod_initialize(ModError* error) { // Skip the game's own shadow rendering while the dynamic pass is active: the // shadowControl pair covers the actor real/blob shadows, drawCloudShadow the weather // cloud shadows. - if (dusk::mods::hook_add_pre<&dDlst_shadowControl_c::imageDraw>(svc_hook, on_game_shadow_pre) != - MOD_OK || - dusk::mods::hook_add_pre<&dDlst_shadowControl_c::draw>(svc_hook, on_game_shadow_pre) != - MOD_OK || - dusk::mods::hook_add_pre<&drawCloudShadow>(svc_hook, on_game_shadow_pre) != MOD_OK) + if (dusk::mods::hook_add_pre(svc_hook, on_game_shadow_pre) != MOD_OK || + dusk::mods::hook_add_pre(svc_hook, on_game_shadow_pre) != MOD_OK || + dusk::mods::hook_add_pre(svc_hook, on_game_shadow_pre) != MOD_OK) { return dusk::mods::set_error(error, MOD_ERROR, "failed to hook game shadow rendering"); } - if (dusk::mods::hook_add_pre(svc_hook, on_frustum_clip_pre) != MOD_OK || - dusk::mods::hook_add_pre(svc_hook, on_frustum_clip_pre) != MOD_OK) + if (dusk::mods::hook_add_pre(svc_hook, on_frustum_clip_pre) != MOD_OK || + dusk::mods::hook_add_pre(svc_hook, on_frustum_clip_pre) != MOD_OK) { return dusk::mods::set_error(error, MOD_ERROR, "failed to hook frustum clipping"); } - if (dusk::mods::hook_add_pre(svc_hook, on_copy_tex_pre) != MOD_OK) { + if (dusk::mods::hook_add_pre(svc_hook, on_copy_tex_pre) != MOD_OK) { return dusk::mods::set_error(error, MOD_ERROR, "failed to hook GXCopyTex"); } UiModsPanelDesc panelDesc = UI_MODS_PANEL_DESC_INIT; diff --git a/src/dusk/mods/loader/loader.cpp b/src/dusk/mods/loader/loader.cpp index e1bb2797c8..11077532fa 100644 --- a/src/dusk/mods/loader/loader.cpp +++ b/src/dusk/mods/loader/loader.cpp @@ -4,6 +4,7 @@ #include #include +#include #include #include #include @@ -16,6 +17,7 @@ #include "dusk/io.hpp" #include "dusk/mods/log_buffer.hpp" #include "dusk/mods/svc/config.hpp" +#include "dusk/mods/svc/hook.hpp" #include "dusk/mods/svc/registry.hpp" #include "dusk/ui/mods_window.hpp" #include "dusk/ui/ui.hpp" @@ -220,31 +222,149 @@ static ModMetadata load_metadata(const std::filesystem::path& modPath, ModBundle }; } -static bool validate_manifest(const ModManifest* manifest, LoadedMod& mod) { - if (manifest == nullptr) { - log::write(mod.metadata.id, LOG_LEVEL_ERROR, "returned a null mod manifest"); - mod.nativeStatus = NativeModStatus::MissingExport; +// True if the first `capacity` bytes of `str` contain a NUL. +static bool terminated_within(const char* str, size_t capacity) { + return std::memchr(str, '\0', capacity) != nullptr; +} + +static bool parse_meta(NativeMod& native, LoadedMod& mod) { + const ModMeta* meta = native.meta; + if (meta->struct_size < sizeof(ModMeta)) { + log::write(mod.metadata.id, LOG_LEVEL_ERROR, "mod_meta descriptor has invalid size {}", + meta->struct_size); + mod.nativeStatus = NativeModStatus::InvalidMetadata; return false; } - if (manifest->struct_size != sizeof(ModManifest)) { - log::write(mod.metadata.id, LOG_LEVEL_ERROR, "manifest has invalid size {} (expected {})", - manifest->struct_size, sizeof(ModManifest)); - mod.nativeStatus = NativeModStatus::ApiVersionMismatch; - return false; - } - if (manifest->abi_version != MOD_ABI_VERSION) { - log::write(mod.metadata.id, LOG_LEVEL_ERROR, "expects ABI v{} but engine is v{}, skipping", - manifest->abi_version, MOD_ABI_VERSION); - mod.nativeStatus = NativeModStatus::ApiVersionMismatch; - return false; - } - if ((manifest->import_count > 0 && manifest->imports == nullptr) || - (manifest->export_count > 0 && manifest->exports == nullptr)) + const auto* cursor = static_cast(meta->records_begin); + const auto* end = static_cast(meta->records_end); + if (cursor == nullptr || end == nullptr || cursor > end || + (reinterpret_cast(cursor) & 7) != 0) { - log::write(mod.metadata.id, LOG_LEVEL_ERROR, "manifest has invalid import/export arrays"); - mod.nativeStatus = NativeModStatus::MissingExport; + log::write(mod.metadata.id, LOG_LEVEL_ERROR, "mod_meta section bounds are invalid"); + mod.nativeStatus = NativeModStatus::InvalidMetadata; return false; } + + ModMetaParsed parsed; + size_t headerCount = 0; + const auto invalid = [&](std::string_view why) { + log::write(mod.metadata.id, LOG_LEVEL_ERROR, "invalid metadata record at offset {}: {}", + cursor - static_cast(meta->records_begin), why); + mod.nativeStatus = NativeModStatus::InvalidMetadata; + return false; + }; + + while (cursor < end) { + if (end - cursor < 8) { + return invalid("trailing bytes"); + } + uint64_t first = 0; + std::memcpy(&first, cursor, sizeof(first)); + if (first == 0) { // linker padding / bounds sentinel + cursor += 8; + continue; + } + + const auto* rec = reinterpret_cast(cursor); + const size_t size = rec->size; + if (size < 8 || size % 8 != 0 || size > static_cast(end - cursor)) { + return invalid("bad record size"); + } + + switch (rec->kind) { + case MOD_META_PAD: + break; + case MOD_META_HEADER: { + if (size < sizeof(ModMetaHeader)) { + return invalid("truncated header record"); + } + const auto* header = reinterpret_cast(rec); + ++headerCount; + parsed.abiVersion = header->abi_version; + break; + } + case MOD_META_IMPORT: { + if (size < sizeof(ModMetaImport)) { + return invalid("truncated import record"); + } + auto* record = reinterpret_cast(const_cast(cursor)); + if (!terminated_within(record->service_id, sizeof(record->service_id))) { + return invalid("unterminated import service id"); + } + parsed.imports.push_back(record); + break; + } + case MOD_META_EXPORT: { + if (size < sizeof(ModMetaExport)) { + return invalid("truncated export record"); + } + auto* record = reinterpret_cast(const_cast(cursor)); + if (!terminated_within(record->service_id, sizeof(record->service_id))) { + return invalid("unterminated export service id"); + } + parsed.exports.push_back(record); + break; + } + case MOD_META_HOOK_FN: { + if (size < sizeof(ModMetaHookFn)) { + return invalid("truncated hook record"); + } + parsed.hookFns.push_back( + reinterpret_cast(const_cast(cursor))); + break; + } + case MOD_META_HOOK_MEM: { + if (size <= sizeof(ModMetaHookMem)) { + return invalid("truncated hook record"); + } + auto* record = reinterpret_cast(const_cast(cursor)); + const char* strings = reinterpret_cast(cursor) + sizeof(ModMetaHookMem); + const size_t capacity = size - sizeof(ModMetaHookMem); + if (!terminated_within(strings, capacity)) { + return invalid("unterminated hook vtable symbol"); + } + const size_t vtableLen = std::char_traits::length(strings); + if (!terminated_within(strings + vtableLen + 1, capacity - vtableLen - 1)) { + return invalid("unterminated hook display name"); + } + parsed.hookMems.push_back(record); + break; + } + case MOD_META_HOOK_NAME: { + if (size <= sizeof(ModMetaHookName)) { + return invalid("truncated hook record"); + } + auto* record = reinterpret_cast(const_cast(cursor)); + const char* name = reinterpret_cast(cursor) + sizeof(ModMetaHookName); + if (!terminated_within(name, size - sizeof(ModMetaHookName))) { + return invalid("unterminated hook symbol name"); + } + parsed.hookNames.push_back(record); + break; + } + default: + // Additive record kinds may appear within a format version; skip them. + log::write(mod.metadata.id, LOG_LEVEL_DEBUG, "skipping unknown metadata record kind {}", + rec->kind); + break; + } + cursor += size; + } + + if (headerCount != 1) { + log::write(mod.metadata.id, LOG_LEVEL_ERROR, "expected 1 metadata header record, found {}", + headerCount); + mod.nativeStatus = NativeModStatus::InvalidMetadata; + return false; + } + if (parsed.abiVersion != MOD_ABI_VERSION) { + log::write(mod.metadata.id, LOG_LEVEL_ERROR, "expects ABI v{} but engine is v{}, skipping", + parsed.abiVersion, MOD_ABI_VERSION); + mod.nativeStatus = NativeModStatus::ApiVersionMismatch; + return false; + } + + native.parsed = std::move(parsed); return true; } @@ -276,6 +396,8 @@ static std::string native_status_message(const NativeModStatus status) { return "Mod ABI version mismatch"; case NativeModStatus::MissingExport: return "Missing required mod API exports"; + case NativeModStatus::InvalidMetadata: + return "Invalid mod metadata records"; case NativeModStatus::Unknown: return "Unknown mod load failure"; case NativeModStatus::None: @@ -375,13 +497,13 @@ void ModLoader::load_native(LoadedMod& mod, const std::string& dllEntry) { return; } - const auto getManifest = nativeMod->handle->LookupSymbol("mod_get_manifest"); + nativeMod->meta = nativeMod->handle->LookupSymbol("mod_meta"); nativeMod->contextSymbol = nativeMod->handle->LookupSymbol("mod_ctx"); nativeMod->fn_initialize = nativeMod->handle->LookupSymbol("mod_initialize"); nativeMod->fn_update = nativeMod->handle->LookupSymbol("mod_update"); nativeMod->fn_shutdown = nativeMod->handle->LookupSymbol("mod_shutdown"); - if (!getManifest || !nativeMod->contextSymbol || !nativeMod->fn_initialize || + if (!nativeMod->meta || !nativeMod->contextSymbol || !nativeMod->fn_initialize || !nativeMod->fn_update || !nativeMod->fn_shutdown) { log::write(mod.metadata.id, LOG_LEVEL_ERROR, @@ -391,8 +513,7 @@ void ModLoader::load_native(LoadedMod& mod, const std::string& dllEntry) { return; } - nativeMod->manifest = getManifest(); - if (!validate_manifest(nativeMod->manifest, mod)) { + if (!parse_meta(*nativeMod, mod)) { return; } @@ -427,28 +548,22 @@ void ModLoader::drain_retired_natives() { m_retiredNatives.clear(); } -static ModManifestInfo build_manifest_info(const ModManifest& manifest) { +static ModManifestInfo build_manifest_info(const ModMetaParsed& parsed) { ModManifestInfo info; - info.imports.reserve(manifest.import_count); - for (size_t i = 0; i < manifest.import_count; ++i) { - const auto& serviceImport = manifest.imports[i]; - if (serviceImport.struct_size != sizeof(ServiceImport) || - !svc::valid_service_id(serviceImport.service_id)) - { + info.imports.reserve(parsed.imports.size()); + for (const auto* record : parsed.imports) { + if (!svc::valid_service_id(record->service_id)) { continue; } - info.imports.push_back({serviceImport.service_id, serviceImport.major_version, - (serviceImport.flags & SERVICE_IMPORT_OPTIONAL) == 0}); + info.imports.push_back({record->service_id, record->major_version, + (record->rec.flags & SERVICE_IMPORT_OPTIONAL) == 0}); } - info.exports.reserve(manifest.export_count); - for (size_t i = 0; i < manifest.export_count; ++i) { - const auto& serviceExport = manifest.exports[i]; - if (serviceExport.struct_size != sizeof(ServiceExport) || - !svc::valid_service_id(serviceExport.service_id)) - { + info.exports.reserve(parsed.exports.size()); + for (const auto* record : parsed.exports) { + if (!svc::valid_service_id(record->service_id)) { continue; } - info.exports.push_back({serviceExport.service_id, serviceExport.major_version}); + info.exports.push_back({record->service_id, record->major_version}); } return info; } @@ -484,24 +599,20 @@ static bool required_deps_active(const LoadedMod& mod) { // A deferred export that was not published by the end of the provider's initialization can // never resolve, which is almost certainly a bug in the provider. static void warn_unpublished_deferred_exports(const LoadedMod& mod) { - if (!mod.active || !mod.native || mod.native->manifest == nullptr) { + if (!mod.active || !mod.native) { return; } - const auto& manifest = *mod.native->manifest; - for (size_t i = 0; i < manifest.export_count; ++i) { - const auto& serviceExport = manifest.exports[i]; - if (serviceExport.struct_size != sizeof(ServiceExport) || - (serviceExport.flags & SERVICE_EXPORT_DEFERRED) == 0) - { + for (const auto* serviceExport : mod.native->parsed.exports) { + if ((serviceExport->rec.flags & SERVICE_EXPORT_DEFERRED) == 0) { continue; } const auto* record = - svc::find_service_record(serviceExport.service_id, serviceExport.major_version); + svc::find_service_record(serviceExport->service_id, 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, serviceExport->major_version); } } } @@ -560,7 +671,7 @@ void ModLoader::try_load_mod( Log.error("Native mod '{}' failed to load, disabling", mod.metadata.id); fail_mod(mod, MOD_ERROR, native_status_message(mod.nativeStatus)); } else { - mod.manifestInfo = build_manifest_info(*mod.native->manifest); + mod.manifestInfo = build_manifest_info(mod.native->parsed); } } @@ -593,6 +704,8 @@ bool ModLoader::activate_mod(LoadedMod& mod) { return false; } + svc::hook_resolve_mod_records(mod); + *mod.native->contextSymbol = mod.context.get(); log::write(mod.metadata.id, LOG_LEVEL_TRACE, "calling mod_initialize"); @@ -929,7 +1042,7 @@ bool ModLoader::reload_bundle(LoadedMod& mod) { fail_mod(mod, MOD_ERROR, native_status_message(mod.nativeStatus)); return false; } - newInfo = build_manifest_info(*mod.native->manifest); + newInfo = build_manifest_info(mod.native->parsed); } else { mod.nativeStatus = NativeModStatus::None; ++mod.cacheGeneration; @@ -998,8 +1111,7 @@ void ModLoader::apply_lifecycle_change(LoadedMod& target, const bool reload) { if (register_static_service_exports(*mod)) { mod->servicesRegistered = true; } else { - log::write( - mod->metadata.id, LOG_LEVEL_ERROR, "failed to register service exports"); + log::write(mod->metadata.id, LOG_LEVEL_ERROR, "failed to register service exports"); deactivate_mod(*mod); } } diff --git a/src/dusk/mods/svc/hook.cpp b/src/dusk/mods/svc/hook.cpp index 41399bf97d..35324185e6 100644 --- a/src/dusk/mods/svc/hook.cpp +++ b/src/dusk/mods/svc/hook.cpp @@ -6,6 +6,7 @@ #if DUSK_CODE_MODS #include "dusk/logging.h" +#include "dusk/mods/log_buffer.hpp" #include #include @@ -13,6 +14,7 @@ #include #include #include +#include #include #include #endif @@ -64,8 +66,29 @@ struct InstalledHook { std::unordered_map s_registry; std::unordered_map s_installed; +std::unordered_map> s_declaredTargets; uint64_t s_nextOrder = 0; +bool declared_target(const ModContext* context, void* fnAddr) { + if (context == nullptr) { + return false; + } + const auto it = s_declaredTargets.find(context); + if (it == s_declaredTargets.end()) { + return false; + } + const auto addr = reinterpret_cast(fnAddr); + return std::ranges::find(it->second, addr) != it->second.end(); +} + +ModResult reject_undeclared(ModContext* context, void* fnAddr) { + log::write(mod_id_from_context(context), LOG_LEVEL_ERROR, + "tried to hook undeclared target {:p}; hook targets must be declared with " + "DEFINE_HOOK/DEFINE_HOOK_SYMBOL", + fnAddr); + return MOD_INVALID_ARGUMENT; +} + HookOptions normalize_options(const HookOptions* options) { if (options == nullptr || options->struct_size < sizeof(HookOptions)) { return HOOK_OPTIONS_INIT; @@ -200,6 +223,9 @@ ModResult hook_install(ModContext* context, void* fnAddr, void* trampolineFn, vo } fnAddr = resolve_import_thunk(fnAddr); + if (!declared_target(context, fnAddr)) { + return reject_undeclared(context, fnAddr); + } const auto key = reinterpret_cast(fnAddr); if (const auto it = s_installed.find(key); it != s_installed.end()) { auto& entry = it->second; @@ -243,6 +269,9 @@ ModResult hook_add_pre( return MOD_INVALID_ARGUMENT; } fnAddr = resolve_import_thunk(fnAddr); + if (!declared_target(context, fnAddr)) { + return reject_undeclared(context, fnAddr); + } auto& hooks = s_registry[reinterpret_cast(fnAddr)].pre; hooks.push_back({context, callback, normalize_options(options), s_nextOrder++}); sort_hooks(hooks); @@ -255,6 +284,9 @@ ModResult hook_add_post( return MOD_INVALID_ARGUMENT; } fnAddr = resolve_import_thunk(fnAddr); + if (!declared_target(context, fnAddr)) { + return reject_undeclared(context, fnAddr); + } auto& hooks = s_registry[reinterpret_cast(fnAddr)].post; hooks.push_back({context, nullptr, callback, normalize_options(options), s_nextOrder++}); sort_hooks(hooks); @@ -269,6 +301,9 @@ ModResult hook_replace( const HookOptions normalized = normalize_options(options); fnAddr = resolve_import_thunk(fnAddr); + if (!declared_target(context, fnAddr)) { + return reject_undeclared(context, fnAddr); + } auto& slot = s_registry[reinterpret_cast(fnAddr)]; if (slot.replace.replaceCallback == nullptr) { slot.replace = {context, callback, nullptr, normalized, s_nextOrder++}; @@ -371,8 +406,187 @@ ModResult hook_dispatch_post(ModContext*, void* fnAddr, void* args, void* retval return MOD_OK; } +#if defined(_WIN32) +/* Follow jump stubs, then match the MSVC vcall thunk a virtual mfp points at. + * Returns the vtable slot's byte offset, or npos when fn is not a vcall thunk. */ +size_t vcall_slot_offset(const void*& fn) noexcept { + constexpr size_t npos = static_cast(-1); +#if defined(_M_X64) || defined(__x86_64__) + const auto* p = static_cast(fn); + for (int i = 0; i < 8 && p[0] == 0xE9; ++i) { // incremental-link stubs + int32_t rel; + std::memcpy(&rel, p + 1, 4); + p += 5 + rel; + } + fn = p; + // The vptr load. Unoptimized clang-cl thunks spill/reload rcx first + // (push rax; mov [rsp], rcx; mov rcx, [rsp]), so scan a short window. + const uint8_t* q = nullptr; + for (int i = 0; i <= 12; ++i) { + if (p[i] == 0x48 && p[i + 1] == 0x8B && p[i + 2] == 0x01) { // mov rax, [rcx] + q = p + i + 3; + break; + } + } + if (q == nullptr) { + return npos; + } + if (q[0] == 0xFF && q[1] == 0x20) { // jmp [rax] (MSVC) + return 0; + } + if (q[0] == 0xFF && q[1] == 0x60) { // jmp [rax + imm8] + return static_cast(q[2]); + } + if (q[0] == 0xFF && q[1] == 0xA0) { // jmp [rax + imm32] + int32_t off; + std::memcpy(&off, q + 2, 4); + return off; + } + // clang-cl: mov rax, [rax + off]; (pop r10;) jmp rax. Requiring the jmp rax + // distinguishes the thunk from an ordinary getter that begins the same way. + if (q[0] == 0x48 && q[1] == 0x8B && (q[2] == 0x00 || q[2] == 0x40 || q[2] == 0x80)) { + size_t off = 0; + const uint8_t* r = q + 3; + if (q[2] == 0x40) { + off = static_cast(q[3]); + r = q + 4; + } else if (q[2] == 0x80) { + int32_t off32; + std::memcpy(&off32, q + 3, 4); + off = off32; + r = q + 7; + } + for (int i = 0; i <= 8; ++i) { + if (r[i] == 0xFF && r[i + 1] == 0xE0) { // jmp rax (48 REX optional) + return off; + } + } + } + return npos; +#elif defined(_M_ARM64) || defined(__aarch64__) + const auto* p = static_cast(fn); + uint32_t insn[3]; + for (int i = 0; i < 8; ++i) { // incremental-link `b` stubs + std::memcpy(insn, p, 4); + if ((insn[0] & 0xFC000000u) != 0x14000000u) { + break; + } + const auto imm26 = static_cast(insn[0] << 6) >> 6; + p += static_cast(imm26) * 4; + } + fn = p; + std::memcpy(insn, p, 12); + // ldr Xt, [x0]; ldr Xs, [Xt, #imm12*8]; br Xs + if ((insn[0] & 0xFFFFFFE0u) != 0xF9400000u) { + return npos; + } + const uint32_t t = insn[0] & 0x1Fu; + if ((insn[1] & 0xFFC003E0u) != (0xF9400000u | (t << 5))) { + return npos; + } + const uint32_t s = insn[1] & 0x1Fu; + if (insn[2] != (0xD61F0000u | (s << 5))) { + return npos; + } + return ((insn[1] >> 10) & 0xFFFu) * 8; +#else + (void)fn; + return npos; +#endif +} +#endif // _WIN32 + +bool resolve_symbol_checked(const char* symbol, bool requireCode, void** out, std::string& why) { + HookSymbolFlags flags{}; + switch (manifest::resolve(symbol, out, &flags)) { + case manifest::ResolveStatus::Ok: + if (requireCode && (flags & HOOK_SYMBOL_CODE) == 0) { + why = fmt::format("'{}' is not a code symbol", symbol); + return false; + } + return true; + case manifest::ResolveStatus::Unavailable: + why = "no symbol manifest for this build"; + return false; + case manifest::ResolveStatus::NotFound: + why = fmt::format("symbol '{}' not found", symbol); + return false; + case manifest::ResolveStatus::Ambiguous: + why = fmt::format("'{}' maps to more than one address; use the mangled name", symbol); + return false; + } + why = "unexpected resolve failure"; + return false; +} + +/* Decode a HOOK_MEM record's pointer-to-member representation into the target code address, + * mirroring what calling through the mfp would invoke. Virtual members hook the class's own + * overrider, read from its vtable (resolved from the symbol manifest). */ +void* resolve_member_record( + const ModMetaHookMem& record, const char* vtableSymbol, std::string& why) { + uintptr_t words[2]; + std::memcpy(words, record.pmf, sizeof(words)); + +#if defined(_WIN32) + const void* fn = reinterpret_cast(words[0]); + const size_t slot = vcall_slot_offset(fn); + if (slot == static_cast(-1)) { // not a vcall thunk: direct address + return const_cast(fn); + } + if (vtableSymbol[0] == '\0') { + why = "class name is not representable as a vtable symbol"; + return nullptr; + } + void* vtable = nullptr; + if (!resolve_symbol_checked(vtableSymbol, false, &vtable, why)) { + return nullptr; + } + // ??_7 points at the first slot. + return *reinterpret_cast(static_cast(vtable) + slot); +#else +#if defined(__aarch64__) || defined(__arm__) + // AAPCS C++ ABI: the virtual flag is bit 0 of the adjustment word (function + // addresses can't spare their low bit), and ptr holds the slot offset directly. + const bool isVirtual = (words[1] & 1) != 0; + const uintptr_t thisAdjust = words[1] >> 1; + const uintptr_t slotOffset = words[0]; +#else + // Itanium C++ ABI: virtual mfps set bit 0 of ptr; the slot offset is ptr - 1. + const bool isVirtual = (words[0] & 1) != 0; + const uintptr_t thisAdjust = words[1]; + const uintptr_t slotOffset = words[0] - 1; +#endif + if (!isVirtual) { // non-virtual: the address itself + return reinterpret_cast(words[0]); + } + if (thisAdjust != 0) { + // this-adjusting mfp (member of a secondary base): the slot offset is relative to a + // vtable we can't locate. Hook the overrider by name instead. + why = "virtual member of a secondary base; hook the overrider by name"; + return nullptr; + } + if (vtableSymbol[0] == '\0') { + why = "class name is not representable as a vtable symbol"; + return nullptr; + } + void* vtable = nullptr; + if (!resolve_symbol_checked(vtableSymbol, false, &vtable, why)) { + return nullptr; + } + // _ZTV points at the offset-to-top slot; the address point mfps index from is + // two pointers in (past offset-to-top and the typeinfo pointer). + void* target = + *reinterpret_cast(static_cast(vtable) + 2 * sizeof(void*) + slotOffset); + if (target == nullptr) { + why = "vtable slot is empty"; + } + return target; +#endif +} + void hook_remove_mod(LoadedMod& mod) { ModContext* context = mod.context.get(); + s_declaredTargets.erase(context); for (auto it = s_registry.begin(); it != s_registry.end();) { auto& slot = it->second; @@ -508,6 +722,56 @@ constexpr HookService s_hookService{ } // namespace +#if DUSK_CODE_MODS +void hook_resolve_mod_records(LoadedMod& mod) { + auto& declared = s_declaredTargets[mod.context.get()]; + declared.clear(); + if (!mod.native) { + return; + } + + const auto resolved = [&](void* target, void** slot) { + target = resolve_import_thunk(target); + *slot = target; + declared.push_back(reinterpret_cast(target)); + }; + const auto unresolved = [&](const char* what, std::string_view why, void** slot) { + *slot = nullptr; + log::write(mod.metadata.id, LOG_LEVEL_WARN, + "hook target '{}' did not resolve ({}); installing this hook will fail", what, why); + }; + + for (auto* record : mod.native->parsed.hookFns) { + if (record->target != nullptr) { + resolved(record->target, &record->resolved); + } else { + unresolved("", "null link-time target", &record->resolved); + } + } + for (auto* record : mod.native->parsed.hookMems) { + std::string why; + void* target = resolve_member_record(*record, hook_mem_vtable_symbol(*record), why); + if (target != nullptr) { + resolved(target, &record->resolved); + } else { + unresolved(hook_mem_display_name(*record), why, &record->resolved); + } + } + for (auto* record : mod.native->parsed.hookNames) { + const char* name = hook_name_symbol(*record); + std::string why; + void* target = nullptr; + if (resolve_symbol_checked(name, true, &target, why)) { + resolved(target, &record->resolved); + } else { + unresolved(name, why, &record->resolved); + } + } +} +#else +void hook_resolve_mod_records(LoadedMod&) {} +#endif // DUSK_CODE_MODS + constinit const ServiceModule g_hookModule{ .id = HOOK_SERVICE_ID, .majorVersion = HOOK_SERVICE_MAJOR, diff --git a/src/dusk/mods/svc/hook.hpp b/src/dusk/mods/svc/hook.hpp new file mode 100644 index 0000000000..d712a8054a --- /dev/null +++ b/src/dusk/mods/svc/hook.hpp @@ -0,0 +1,11 @@ +#pragma once + +namespace dusk::mods { +struct LoadedMod; +} + +namespace dusk::mods::svc { + +void hook_resolve_mod_records(LoadedMod& mod); + +} // namespace dusk::mods::svc diff --git a/src/dusk/mods/svc/registry.cpp b/src/dusk/mods/svc/registry.cpp index 338625230a..2f51791af3 100644 --- a/src/dusk/mods/svc/registry.cpp +++ b/src/dusk/mods/svc/registry.cpp @@ -217,29 +217,25 @@ void ModLoader::init_services() { } bool ModLoader::register_static_service_exports(LoadedMod& mod) { - if (!mod.native || mod.native->manifest == nullptr) { + if (!mod.native) { return true; } - const auto& manifest = *mod.native->manifest; - for (size_t i = 0; i < manifest.export_count; ++i) { - const auto& serviceExport = manifest.exports[i]; - if (serviceExport.struct_size != sizeof(ServiceExport) || - !svc::valid_service_id(serviceExport.service_id)) - { + for (const auto* serviceExport : mod.native->parsed.exports) { + if (!svc::valid_service_id(serviceExport->service_id)) { fail_mod(mod, MOD_INVALID_ARGUMENT, "Invalid service export descriptor"); return false; } - const bool deferred = (serviceExport.flags & SERVICE_EXPORT_DEFERRED) != 0; - if (!deferred && serviceExport.service == nullptr) { + const bool deferred = (serviceExport->rec.flags & SERVICE_EXPORT_DEFERRED) != 0; + if (!deferred && serviceExport->service == nullptr) { fail_mod(mod, MOD_INVALID_ARGUMENT, "Static service export has null service pointer"); return false; } const auto result = - svc::register_service(serviceExport.service_id, serviceExport.major_version, - serviceExport.minor_version, serviceExport.service, &mod, deferred); + svc::register_service(serviceExport->service_id, serviceExport->major_version, + serviceExport->minor_version, serviceExport->service, &mod, deferred); if (result != MOD_OK) { fail_mod(mod, result, "Service export registration failed"); return false; @@ -262,18 +258,13 @@ std::string ModLoader::describe_missing_import( // No record can also mean the provider failed or is disabled and its services were removed. for (const auto& other : mods()) { - if ((other.active && !other.loadFailed) || !other.native || - other.native->manifest == nullptr) - { + if ((other.active && !other.loadFailed) || !other.native) { continue; } - const auto& manifest = *other.native->manifest; - for (size_t i = 0; i < manifest.export_count; ++i) { - const auto& serviceExport = manifest.exports[i]; - if (serviceExport.struct_size == sizeof(ServiceExport) && - svc::valid_service_id(serviceExport.service_id) && - std::string_view{serviceExport.service_id} == serviceId && - serviceExport.major_version == majorVersion) + for (const auto* serviceExport : other.native->parsed.exports) { + if (svc::valid_service_id(serviceExport->service_id) && + std::string_view{serviceExport->service_id} == serviceId && + serviceExport->major_version == majorVersion) { return fmt::format("Required service {}@{} unavailable: provider '{}' {}", serviceId, majorVersion, other.metadata.id, @@ -286,35 +277,31 @@ std::string ModLoader::describe_missing_import( } bool ModLoader::resolve_service_imports(LoadedMod& mod) { - if (!mod.native || mod.native->manifest == nullptr) { + if (!mod.native) { return true; } - const auto& manifest = *mod.native->manifest; - for (size_t i = 0; i < manifest.import_count; ++i) { - const auto& serviceImport = manifest.imports[i]; - if (serviceImport.struct_size != sizeof(ServiceImport) || - !svc::valid_service_id(serviceImport.service_id) || serviceImport.slot == nullptr) - { + for (const auto* serviceImport : mod.native->parsed.imports) { + if (!svc::valid_service_id(serviceImport->service_id) || serviceImport->slot == nullptr) { fail_mod(mod, MOD_INVALID_ARGUMENT, "Invalid service import descriptor"); return false; } - const auto* service = svc::find_service( - serviceImport.service_id, serviceImport.major_version, serviceImport.min_minor_version); + const auto* service = svc::find_service(serviceImport->service_id, + serviceImport->major_version, serviceImport->min_minor_version); if (service == nullptr) { - *static_cast(serviceImport.slot) = nullptr; - if ((serviceImport.flags & SERVICE_IMPORT_OPTIONAL) != 0) { + *static_cast(serviceImport->slot) = nullptr; + if ((serviceImport->rec.flags & SERVICE_IMPORT_OPTIONAL) != 0) { continue; } fail_mod(mod, MOD_UNAVAILABLE, - describe_missing_import(serviceImport.service_id, serviceImport.major_version, - serviceImport.min_minor_version)); + describe_missing_import(serviceImport->service_id, serviceImport->major_version, + serviceImport->min_minor_version)); return false; } - *static_cast(serviceImport.slot) = service->service; + *static_cast(serviceImport->slot) = service->service; } return true;