Mod API: Static import/export/hook metadata & resolution

This commit is contained in:
Luke Street
2026-07-13 00:53:37 -06:00
parent 8cc4ce94b8
commit 9687a9d95a
11 changed files with 993 additions and 554 deletions
+26 -40
View File
@@ -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<LinkPosMove>(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<LinkPosMove>(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<daAlink_c*>(args, 0));
int result = LinkExecute::g_orig(dusk::mods::arg<daAlink_c*>(args, 0));
if (retval != nullptr) {
*static_cast<int*>(retval) = result;
}
}
dusk::mods::hook_replace<&daAlink_c::execute>(svc_hook, on_execute_replace);
dusk::mods::hook_replace<LinkExecute>(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<HookshotHit>(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<T>(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<int>(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<CreateItem>(svc_hook, on_create_item_pre);
```
For reference parameters (e.g. `const cXyz& pos`), `arg_ref<cXyz>` 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
+31 -3
View File
@@ -67,9 +67,32 @@ struct ModSearchDir {
std::filesystem::path nativeLibDir;
};
struct ModMetaParsed {
uint32_t abiVersion = 0;
std::vector<ModMetaImport*> imports;
std::vector<ModMetaExport*> exports;
std::vector<ModMetaHookFn*> hookFns;
std::vector<ModMetaHookMem*> hookMems;
std::vector<ModMetaHookName*> hookNames;
};
inline const char* hook_mem_vtable_symbol(const ModMetaHookMem& rec) {
return reinterpret_cast<const char*>(&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<char>::length(vtable) + 1;
}
inline const char* hook_name_symbol(const ModMetaHookName& rec) {
return reinterpret_cast<const char*>(&rec) + sizeof(ModMetaHookName);
}
struct NativeMod {
std::unique_ptr<loader::NativeModule> 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<ConfigVar<bool> > cvarIsEnabled;
std::unique_ptr<ConfigVar<bool>> cvarIsEnabled;
config::Subscription enabledSubscription = 0;
bool active = false;
@@ -200,7 +228,7 @@ private:
std::string path;
};
std::vector<std::unique_ptr<LoadedMod> > m_mods;
std::vector<std::unique_ptr<LoadedMod>> m_mods;
std::vector<ModSearchDir> m_searchDirs;
std::filesystem::path m_cacheDir;
std::vector<Request> m_pendingRequests;
+114 -24
View File
@@ -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);
+48 -309
View File
@@ -2,11 +2,7 @@
#include "mods/svc/hook.h"
#include <array>
#include <cstddef>
#include <cstring>
#include <memory>
#include <string_view>
#include <type_traits>
namespace dusk::mods {
@@ -14,255 +10,20 @@ namespace dusk::mods {
template <class T>
T arg(void* argsRaw, int n) noexcept {
void** args = static_cast<void**>(argsRaw);
return *static_cast<std::add_pointer_t<std::remove_reference_t<T> > >(args[n]);
return *static_cast<std::add_pointer_t<std::remove_reference_t<T>>>(args[n]);
}
template <class T>
std::remove_reference_t<T>& arg_ref(void* argsRaw, int n) noexcept {
void** args = static_cast<void**>(argsRaw);
return *static_cast<std::add_pointer_t<std::remove_reference_t<T> > >(args[n]);
return *static_cast<std::add_pointer_t<std::remove_reference_t<T>>>(args[n]);
}
template <class F>
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 <size_t N>
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 <class T>
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<class daAlink_c>(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 <class C>
constexpr auto vtable_symbol() {
constexpr std::string_view name = class_name<C>();
constexpr bool simple = name.find_first_of(":<> ") == std::string_view::npos;
// "_ZTV" + decimal length + name / "??_7" + name + "@@6B@", NUL-terminated
std::array<char, name.size() + 12> 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<char>('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<size_t>(-1);
#if defined(_M_X64) || defined(__x86_64__)
const auto* p = static_cast<const uint8_t*>(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<int8_t>(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<int8_t>(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<const uint8_t*>(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<int32_t>(insn[0] << 6) >> 6;
p += static_cast<intptr_t>(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 <class C, class F>
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<const void*>(words[0]);
const size_t slot = vcall_slot_offset(fn);
if (slot == static_cast<size_t>(-1)) { // not a vcall thunk: direct address
*out = const_cast<void*>(fn);
return MOD_OK;
}
void* vtable = nullptr;
const ModResult resolved = hooks->resolve(mod_ctx, vtable_symbol<C>().data(), &vtable, nullptr);
if (resolved != MOD_OK) {
return resolved;
}
// ??_7 points at the first slot.
*out = *reinterpret_cast<void**>(static_cast<char*>(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<void*>(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<C>().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<void**>(static_cast<char*>(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 <class Tag, class R, class... A>
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 <auto Target>
struct Hook;
template <class C, class R, class... A, R (C::*Target)(A...)>
struct Hook<Target> : HookImpl<detail::TargetTag<Target>, R, C*, A...> {
static ModResult resolve_target(const HookService* hooks, void** out) {
return detail::member_target<C>(hooks, Target, out);
}
};
struct Hook<Target> : HookImpl<detail::TargetTag<Target>, R, C*, A...> {};
template <class C, class R, class... A, R (C::*Target)(A...) const>
struct Hook<Target> : HookImpl<detail::TargetTag<Target>, R, const C*, A...> {
static ModResult resolve_target(const HookService* hooks, void** out) {
return detail::member_target<C>(hooks, Target, out);
}
};
struct Hook<Target> : HookImpl<detail::TargetTag<Target>, R, const C*, A...> {};
template <class R, class... A, R (*Target)(A...)>
struct Hook<Target> : HookImpl<detail::TargetTag<Target>, R, A...> {
static ModResult resolve_target(const HookService*, void** out) {
*out = mfp_addr(Target);
return MOD_OK;
}
};
struct Hook<Target> : HookImpl<detail::TargetTag<Target>, 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<HookshotHit>(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 <FixedString Name, class Sig>
struct NamedHook;
template <FixedString Name, class R, class... A>
struct NamedHook<Name, R(A...)> : HookImpl<detail::NameTag<Name>, 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<Name, R(A...)> : HookImpl<detail::NameTag<Name>, 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<LinkExecute>(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 <class Entry>
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<void*>(Entry::trampoline),
reinterpret_cast<void**>(&Entry::g_orig));
}
template <auto Target>
ModResult hook_install(const HookService* hooks) {
return hook_install<Hook<Target> >(hooks);
}
template <class Entry>
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 <auto Target>
ModResult hook_add_pre(
const HookService* hooks, HookPreFn callback, const HookOptions* options = nullptr) {
return hook_add_pre<Hook<Target> >(hooks, callback, options);
}
template <class Entry>
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 <auto Target>
ModResult hook_add_post(
const HookService* hooks, HookPostFn callback, const HookOptions* options = nullptr) {
return hook_add_post<Hook<Target> >(hooks, callback, options);
}
template <class Entry>
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 <auto Target>
ModResult hook_replace(
const HookService* hooks, HookReplaceFn callback, const HookOptions* options = nullptr) {
return hook_replace<Hook<Target> >(hooks, callback, options);
}
} // namespace dusk::mods
+276
View File
@@ -0,0 +1,276 @@
#pragma once
#include "mods/api.h"
#include <array>
#include <cstddef>
#include <string_view>
#include <type_traits>
/*
* 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 <size_t N>
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 <class T>
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<class daAlink_c>(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 <class C>
constexpr auto vtable_symbol() {
constexpr std::string_view name = class_name<C>();
constexpr bool simple = name.find_first_of(":<> ") == std::string_view::npos;
// "_ZTV" + decimal length + name / "??_7" + name + "@@6B@", NUL-terminated
std::array<char, name.size() + 12> 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<char>('0' + len % 10);
len /= 10;
}
while (d != 0) {
out[n++] = digits[--d];
}
for (char c : name) {
out[n++] = c;
}
#endif
return out;
}
template <class F>
struct member_traits;
template <class C, class R, class... A>
struct member_traits<R (C::*)(A...)> {
using Class = C;
};
template <class C, class R, class... A>
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) {
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 <class F>
struct HookFnRecord {
ModMetaRecord rec;
uint32_t reserved;
F target;
void* resolved;
};
template <class F, size_t N>
struct HookMemRecord {
ModMetaRecord rec;
uint32_t reserved;
union {
F fn;
unsigned char raw[16];
} pmf;
void* resolved;
char names[N];
};
template <size_t N>
struct HookNameRecord {
ModMetaRecord rec;
uint32_t reserved;
void* resolved;
char name[N];
};
template <size_t N>
constexpr size_t align_up(size_t n) {
return (n + (N - 1)) & ~(N - 1);
}
template <auto Target, FixedString Disp>
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<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;
}
}
template <FixedString Name>
consteval auto make_hook_name_record() {
constexpr size_t len = sizeof(Name.chars) - 1;
HookNameRecord<align_up<8>(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
+21 -78
View File
@@ -1,60 +1,17 @@
#pragma once
#include "mods/api.h"
#include "mods/meta.hpp"
#include <cstdint>
#include <cstdio>
#include <type_traits>
#include <vector>
namespace dusk::mods {
template <class Service>
struct ServiceTraits;
namespace detail {
inline std::vector<ServiceImport>& imports() {
static std::vector<ServiceImport> entries;
return entries;
}
inline std::vector<ServiceExport>& exports() {
static std::vector<ServiceExport> 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<uint16_t>(major_value), \
static_cast<uint16_t>(min_minor_value), \
static_cast<uint32_t>(flags_value), \
&(variable), \
})
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))
#define IMPORT_SERVICE_VERSION(service_type, variable, min_minor_value) \
IMPORT_SERVICE_EX(service_type, variable, ::dusk::mods::ServiceTraits<service_type>::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<std::remove_cv_t<decltype(instance)> >::id)
instance, ::dusk::mods::ServiceTraits<std::remove_cv_t<decltype(instance)>>::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<uint16_t>(major_value), \
static_cast<uint16_t>(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<uint16_t>(major_value), \
static_cast<uint16_t>(minor_value), SERVICE_EXPORT_DEFERRED, nullptr)
+15 -12
View File
@@ -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<ClipperSphereClip>(&J3DUClipper::clip);
constexpr ClipperBoxClip kClipperBoxClip = static_cast<ClipperBoxClip>(&J3DUClipper::clip);
DEFINE_HOOK(&dDlst_shadowControl_c::imageDraw, GameShadowImageDraw);
DEFINE_HOOK(&dDlst_shadowControl_c::draw, GameShadowDraw);
DEFINE_HOOK(&drawCloudShadow, CloudShadowDraw);
DEFINE_HOOK(static_cast<int (J3DUClipper::*)(f32 const (*)[4], Vec, f32) const>(&J3DUClipper::clip),
ClipperSphereClip);
DEFINE_HOOK(
static_cast<int (J3DUClipper::*)(f32 const (*)[4], Vec*, Vec*) const>(&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<GameShadowImageDraw>(svc_hook, on_game_shadow_pre) != MOD_OK ||
dusk::mods::hook_add_pre<GameShadowDraw>(svc_hook, on_game_shadow_pre) != MOD_OK ||
dusk::mods::hook_add_pre<CloudShadowDraw>(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<kClipperSphereClip>(svc_hook, on_frustum_clip_pre) != MOD_OK ||
dusk::mods::hook_add_pre<kClipperBoxClip>(svc_hook, on_frustum_clip_pre) != MOD_OK)
if (dusk::mods::hook_add_pre<ClipperSphereClip>(svc_hook, on_frustum_clip_pre) != MOD_OK ||
dusk::mods::hook_add_pre<ClipperBoxClip>(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<GXCopyTex>(svc_hook, on_copy_tex_pre) != MOD_OK) {
if (dusk::mods::hook_add_pre<CopyTex>(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;
+165 -53
View File
@@ -4,6 +4,7 @@
#include <algorithm>
#include <chrono>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <string>
@@ -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<const uint8_t*>(meta->records_begin);
const auto* end = static_cast<const uint8_t*>(meta->records_end);
if (cursor == nullptr || end == nullptr || cursor > end ||
(reinterpret_cast<uintptr_t>(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<const uint8_t*>(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<const ModMetaRecord*>(cursor);
const size_t size = rec->size;
if (size < 8 || size % 8 != 0 || size > static_cast<size_t>(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<const ModMetaHeader*>(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<ModMetaImport*>(const_cast<uint8_t*>(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<ModMetaExport*>(const_cast<uint8_t*>(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<ModMetaHookFn*>(const_cast<uint8_t*>(cursor)));
break;
}
case MOD_META_HOOK_MEM: {
if (size <= sizeof(ModMetaHookMem)) {
return invalid("truncated hook record");
}
auto* record = reinterpret_cast<ModMetaHookMem*>(const_cast<uint8_t*>(cursor));
const char* strings = reinterpret_cast<const char*>(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<char>::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<ModMetaHookName*>(const_cast<uint8_t*>(cursor));
const char* name = reinterpret_cast<const char*>(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<ModGetManifestFn>("mod_get_manifest");
nativeMod->meta = nativeMod->handle->LookupSymbol<const ModMeta*>("mod_meta");
nativeMod->contextSymbol = nativeMod->handle->LookupSymbol<ModContext**>("mod_ctx");
nativeMod->fn_initialize = nativeMod->handle->LookupSymbol<ModInitializeFn>("mod_initialize");
nativeMod->fn_update = nativeMod->handle->LookupSymbol<ModUpdateFn>("mod_update");
nativeMod->fn_shutdown = nativeMod->handle->LookupSymbol<ModShutdownFn>("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);
}
}
+264
View File
@@ -6,6 +6,7 @@
#if DUSK_CODE_MODS
#include "dusk/logging.h"
#include "dusk/mods/log_buffer.hpp"
#include <algorithm>
#include <cstdint>
@@ -13,6 +14,7 @@
#include <exception>
#include <fmt/format.h>
#include <funchook.h>
#include <string>
#include <unordered_map>
#include <vector>
#endif
@@ -64,8 +66,29 @@ struct InstalledHook {
std::unordered_map<uintptr_t, HookSlot> s_registry;
std::unordered_map<uintptr_t, InstalledHook> s_installed;
std::unordered_map<const ModContext*, std::vector<uintptr_t>> 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<uintptr_t>(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<uintptr_t>(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<uintptr_t>(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<uintptr_t>(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<uintptr_t>(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<size_t>(-1);
#if defined(_M_X64) || defined(__x86_64__)
const auto* p = static_cast<const uint8_t*>(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<int8_t>(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<int8_t>(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<const uint8_t*>(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<int32_t>(insn[0] << 6) >> 6;
p += static_cast<intptr_t>(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<const void*>(words[0]);
const size_t slot = vcall_slot_offset(fn);
if (slot == static_cast<size_t>(-1)) { // not a vcall thunk: direct address
return const_cast<void*>(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<void**>(static_cast<char*>(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<void*>(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<void**>(static_cast<char*>(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<uintptr_t>(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("<fn>", "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,
+11
View File
@@ -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
+22 -35
View File
@@ -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<const void**>(serviceImport.slot) = nullptr;
if ((serviceImport.flags & SERVICE_IMPORT_OPTIONAL) != 0) {
*static_cast<const void**>(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<const void**>(serviceImport.slot) = service->service;
*static_cast<const void**>(serviceImport->slot) = service->service;
}
return true;