Hooks and firmware tags¶
A hook redirects one of EP147's own functions to the mod's code, which then calls the original. Mods use hooks to change behavior EP147 already has (what a button does, what a screen shows, how a value is computed) or to read something the core's services do not expose.
The cost is a tie to the firmware. A function's address moves from one EP147 build to the next, so a hook mod works only on the exact builds it was made for:
| Loads on | After a new official firmware | |
|---|---|---|
| Services | every firmware AZ-MODS supports | keeps working once AZ-MODS supports that firmware |
| Hooks | the listed builds only; any other shows Not made for this firmware before any of its code runs |
needs its addresses found again on the new build, and a new release |
In azmod.json, a services mod declares:
"firmware": ["any"], "hooks": false
and a hook mod lists its builds:
"firmware": ["ep147-..."], "hooks": true
A wrong address runs the hook with the wrong arguments inside EP147's code: a crash, or silent corruption.
Finding a function is reverse engineering on your own deck. Start from EP147 itself, at
/home/root/pdj/EP147 on the deck. It is an aarch64 ELF executable, readable over SSH
(SSH access) and loadable in any disassembler or decompiler. Its
firmware tag (below) names the build the addresses belong to.
The reference pages map what is already known: 354 named functions, vtables
and data addresses for each supported build, the objects behind decks and tracks, and the
control frames. reference/ghidra/ImportAzModsLabels.java applies those names to your own
copy of EP147 in Ghidra.
A mod publishes only its own code and the addresses it hooks, not firmware, files extracted from it, or decompiled code (Guidelines).
Firmware tags¶
A tag names one exact build of EP147:
ep147-0123456789abcdef
This example is not a real tag. A tag is ep147- followed by 16 lowercase hex digits: a
64-bit FNV-1a hash of EP147's executable segments, read from the program file.
| Changes when | Does not change for |
|---|---|
| any firmware update that changes EP147's code, even if the version users see stays the same | settings, tracks, the AZ-MODS version, installed mods, hooks already in place |
Every deck running that build reports the same tag, and so does the emulator.
| Where | How |
|---|---|
| On the deck | Mod Menu > Community Mods > Repositories, bottom line: For mod makers: this deck's firmware tag is ep147-... |
| Over SSH | the first line of /tmp/azmod-community/loaded (fw=ep147-...) |
| From a mod | core->fw_tag() |
Declaring a hook mod¶
azmod.json and the descriptor must agree:
"firmware": ["ep147-0123456789abcdef"],
"hooks": true
static const char *const FW_TAGS[] = { "ep147-0123456789abcdef", NULL };
| Declared | Result |
|---|---|
| a tag list without this deck's tag | Not made for this firmware; nothing of the mod runs |
"hooks": true |
the mod's page tells users Changes the deck software itself (uses hooks) before they install |
"any" and "hooks": true |
refused: Says "any" firmware but also patches code |
"any", or "hooks": false |
every claim_hook and claim_vslot is refused |
claim_hook¶
int claim_hook(const char *id, uintptr_t addr, void *hook, void **orig);
| Argument | |
|---|---|
addr |
the function's address in the running EP147; it must lie inside EP147's code |
hook |
the replacement, with exactly the same signature |
orig |
receives a pointer that runs the original function |
| returns | 0 on success, non-zero if refused; the reason is always in the mod's log |
The core replaces the function's first instruction with a branch to a jump island near it, and runs the displaced instruction, relocated, from a trampoline before jumping back. The swap is one aligned 4-byte store, so a thread already in the function sees either the old code or the new.
| Refusal in the log | Cause |
|---|---|
not inside EP147's code |
addr is outside EP147's executable segments |
overlaps the hook at 0x... patched by <owner> |
addr is within 16 bytes of an existing hook (the core's or another mod's) |
the hook installer declined it |
a null or misaligned addr, or one of the function's first four instructions depends on where it sits: b, bl, b.cond, cbz, cbnz, tbz, tbnz, adr or a literal ldr (adrp is re-encoded and accepted) |
no jump island within reach of it, ... |
no island in branch range of addr |
a mod for "any" firmware may not patch code, azmod.json does not declare "hooks": true |
see Declaring |
call it from init() |
the claim came after init returned, or from another mod's init |
called with this mod's id from outside its code |
the call did not come from the mod named by id |
If a function is declined, hook its caller or a function it calls instead. The core accepts a wrong address whenever its first instructions happen to be movable, so read the first instructions at each address in your own copy of EP147 and confirm they start the intended function.
Claim from init, last¶
typedef int (*some_fn_t)(void *self, int value);
static some_fn_t orig_some_fn;
static int my_some_fn(void *self, int value)
{
/* be quick: this runs on the deck's own thread, as often as the deck calls it */
return orig_some_fn(self, value); /* run the original, same arguments */
}
static int init(const az_core_v1 *core)
{
C = core;
if (!AZ_HAS(C, claim_hook) || !AZ_HAS(C, fw_tag)) return 1;
/* ... settings, register_screen, everything that can fail ... */
const struct fw_addrs *a = pick(core); /* see "Several firmware builds" below */
if (!a) return 2;
if (C->claim_hook(MOD_ID, a->some_fn, (void *)my_some_fn, (void **)&orig_some_fn) != 0)
return 3; /* refused: run without it, or not at all */
return 0;
}
A hook cannot be removed while EP147 runs. If init fails after a claim, the library stays in
memory with the hook live until the next start, and the Mod Menu shows
Its init() returned <n> (after patching code; reload to clear).
One address, one owner¶
An address belongs to the AZ-MODS core or to exactly one mod. A claim on an owned address is
refused, and the mod then either runs without that feature or returns non-zero from init. Code is never
patched outside claim_hook and claim_vslot.
Inside a hook¶
- Call the original as EP147 would have (same arguments), and return its result unless changing it is the point.
- It runs on whatever thread EP147 calls the function from, as often as EP147 calls it: no file or network access, no long-held locks, no allocations, no logging on every call.
- When the deck an object belongs to is not certain, pass the call through untouched.
deck_for_ds()anddeck_for_modeobj()answer "which deck is this?" for EP147's objects and return-1for unknown (API reference). - Keep state shared with
tickor a page in plain words, read and written with atomics (__atomic_load_n,__atomic_store_n).
claim_vslot: one virtual method¶
int claim_vslot(const char *id, uintptr_t slot, void *fn, void **orig);
This swaps one vtable entry instead of patching code, so it intercepts only calls made
through that vtable. slot is the address of the entry. The same declaration rules apply,
and a claim is also refused in these cases:
| Refused when | Log |
|---|---|
slot is not 8-byte aligned |
not an aligned pointer |
slot is outside EP147's non-executable segments |
not inside EP147's data |
fn is not inside the mod |
the new function is not inside this mod |
| the slot no longer holds a function in EP147's code | the slot does not hold a function in EP147's code (already swapped?) |
| another mod claimed it | already claimed by <id> |
The page's protection is restored after the swap, and *orig is published before the slot.
Several firmware builds in one release¶
List every tag in azmod.json and FW_TAGS, and keep one row of addresses per tag:
#include <string.h>
struct fw_addrs {
const char *tag;
uintptr_t some_fn; /* what you hook on this firmware */
};
static const struct fw_addrs FW[] = {
{ "ep147-0123456789abcdef", 0x0 /* your address for this firmware */ },
{ "ep147-fedcba9876543210", 0x0 /* your address for this firmware */ },
};
static const struct fw_addrs *pick(const az_core_v1 *core)
{
const char *tag = core->fw_tag();
for (size_t i = 0; i < sizeof FW / sizeof FW[0]; i++)
if (tag && strcmp(tag, FW[i].tag) == 0)
return &FW[i];
return NULL; /* never "use the first row" */
}
The core already refuses the mod on an unlisted build, so if pick() returns NULL, the
table and FW_TAGS disagree. Falling back to the first row would hook a wrong address.
A new firmware¶
- Read the new tag on a deck that has updated.
- Find the addresses again on that build, and test on it.
- Add the tag to
"firmware",FW_TAGSand the address table, bumpversion, and release.
Until that release, decks on the new firmware show Not made for this firmware and the mod stays off.
No hook needed¶
| To | Use |
|---|---|
| read or change pad and mode-button LEDs | frame_filter on AZ_BUS_ERP_LED (or on_frame) |
| read, change or swallow buttons, pads, jogs, knobs and faders | frame_filter on the input buses |
| take touches before EP147 sees them | touch_filter |
| draw over EP147's screens | surfaces (overlays) |
| know which deck an EP147 object belongs to | deck_for_ds, deck_for_modeobj |
All of these work with "firmware": ["any"]. The core fixes frame checksums after the last
filter. See the API reference.
Next: Packaging and publishing.