Appearance
aowlspt_nameindex.h
Source: abi/aowlspt_nameindex.h — 660 lines, 18 file-scope functions.
What this header owns
Reproduced verbatim from the header's own banner comment — these notes are frequently the only written record of why the subsystem is shaped the way it is.
text
aowlspt_nameindex.h -- name -> code-RVA lookup from a generated offline index.
## What this replaces, and why it has to exist
By-name method resolution is DEAD on this build. `findClass`/`findMethod`
hand back non-nil handles into unmapped memory, and every `MethodInfo` probed
through them is unreadable -- measured 6/6 on the host thread AND 6/6 on the
Unity main thread, so it is not thread affinity. No MethodInfo means no
token, and no token means the `Il2CppCodeGenModule.methodPointers` table --
which is itself sound, hand-walked live and prologue byte-verified -- cannot
be indexed BY NAME at run time.
Every input to that resolution is available offline, though. So
`tools/il2cpp_nameindex.py` walks the metadata on the build machine, does
exactly what `tools/il2cpp_resolve.py` does (it imports it, rather than
reimplementing the walk, so the two cannot drift), and freezes the whole
answer into a sorted binary index. This file only reads that file. It calls
NOTHING in IL2CPP -- not one export, not one MethodInfo dereference. That is
the entire point: the dead path is not made more robust here, it is removed
from the question.
## What it does NOT do
It does not patch, bind, verify a prologue, or turn an RVA into a callable
pointer. It answers ONE question -- "what RVA does this name have" -- and the
caller takes that to the by-RVA binder, which owns address substitution,
prologue byte-verification against the startup snapshot, the `VirtualQuery`
and the il2cpp-section check. There is deliberately no second copy of any of
that in this file.
## The file format (little-endian throughout)
header, 40 bytes:
+0x00 "AOWLNIDX"
+0x08 u32 version (1)
+0x0C u32 count
+0x10 u64 imageKey -- build identity, verifiable from mapped headers
+0x18 u64 fileHash -- SHA-256(GameAssembly.dll)[0:8], build-time guard
+0x20 u32 flags, u32 pad
then FOUR parallel arrays of `count` (format version 2):
u64 hash[] ascending, UNIQUE (binary search touches only this)
u32 rva[]
u32 check[]
u16 share[] how many method keys resolve to that rva
## SHARED RVAs, and why the count is a FIELD and not a companion file
The IL2CPP backend folds identical compiled bodies, so unrelated methods in
different images share one address. Measured on this build: 6,261 RVAs are
reached by more than one method key, 28.3% of exact keys (46,608 of 164,922)
land on one, and 0x628110 -- the universal `ret 0` stub -- is shared by
6,438 methods. `Diz.Resources.EasyAssets::get_System` is one of 338 keys on
0x692A50.
CALLING a shared RVA is fine: it is the right code for the receiver passed.
DETOURING one is a write whose blast radius is every method that folded onto
it, and nothing at run time can tell them apart. So `share[]` exists, and
`aowl_nameidx_lookup_shared` hands the count to the caller.
share == 0 sharedness UNKNOWN. NEVER "unshared".
share == 1 reached by this method only.
share >= 2 that many methods resolve here (saturating at 65535).
It is a field rather than a stamped sidecar because a sidecar is a second
file that can go missing on its own -- which is exactly what happened: the
generator wrote one, nothing deployed it, and the host had no sharedness
data at all while by-name patching was live. One file, one stamp, one
staleness story. A version-1 index cannot answer the question, so it is
REFUSED at load by the same door that refuses a stale stamp, rather than
loaded and silently read as all-unshared.
Structure-of-arrays so the search walks a dense 8-byte-stride array instead of
striding a 16-byte record and pulling an rva+check into cache at every probe
it is going to discard.
## Why a hash and not the names
4.91 MB rather than ~8.8 MB, and the host never needs to print a name it was
not already handed by the caller. Two independent guards make that safe:
- a PRIMARY collision (two distinct keys, one u64) is detected OFFLINE over
the complete key set, and the generator REFUSES to emit. It cannot reach
this file.
- a lookup for a key that is NOT in the index could still land on a matching
primary. So every entry carries a 32-bit CHECK hash from an independent
basis and prime, verified after the search. A false hit needs both to
collide: ~2^-96. On a check mismatch this reports NOT FOUND, which is the
recoverable answer.
## Overloads
Keys carry arity, because a name does not identify a method:
`UnityEngine.AssetBundle::LoadAsset` has two, at 0x5250100 and 0x5250340.
"Ns.Type::Method/2" the 2-argument overload, exactly
"Ns.Type::Method/*" emitted by the generator ONLY where the name has a
single overload
So an arity of -1 -- the host's existing "a patch names a method, not a
signature" lookup -- maps to the `/*` key, which ANSWERS when there is
nothing to be ambiguous about and is ABSENT the moment there is. `LoadAsset`
with arity -1 returns NOT FOUND here, on purpose, and the caller must name
the overload it means.
The generator also DROPS any key that two metadata types gave two different
RVAs (4675 of them on this build -- closure types, nested types carrying no
namespace). There is no correct answer for those, so there is no answer.
## Staleness is the real hazard
A stale RVA is not a missing function, it is a WRONG-BUT-MAPPED one, which is
precisely the corruption the safety rules exist to prevent. So the index is
stamped and the stamp is checked at load:
`imageKey` is packed from PE header fields -- TimeDateStamp, SizeOfImage,
AddressOfEntryPoint, CheckSum -- that the loader maps verbatim and does not
touch. So it is reproducible from `GetModuleHandleA("GameAssembly.dll")` in
microseconds, with no file I/O and no 124 MB rehash at startup.
On mismatch this refuses the WHOLE index and serves nothing from it, rather
than answering with addresses from another build. Prologue byte-verification
before patching remains mandatory regardless: the stamp narrows the window,
it does not replace the check.
## Safety
The index buffer is our own heap, sized from a header validated against the
real file size before a single entry is read, so the search needs no guard
and adds none -- `aowl_p_p_seh` is not re-entrant and a nested guard would
DISARM an outer one. The one place that touches foreign memory is the mapped
PE header read in `init`, which is `aowl_is_readable`-gated and runs under a
single guard of its own at a point where no other guard is held. Every loop
is capped. The feature is flag-gated and default OFF, and self-disables after
AOWL_NIDX_MAX_FAULTS refusals.
Regenerate with:
python tools/il2cpp_nameindex.py gen \
D:/Games/Tarkov/GameAssembly.dll .cache/global-metadata.dec.dat \
build/aowlspt-names.idxConstants
AOWLSPT_NAMEINDEX_HAOWL_NIDX_CHK_BASISAOWL_NIDX_CHK_PRIMEAOWL_NIDX_ENTRYAOWL_NIDX_FNV_BASISAOWL_NIDX_FNV_PRIMEAOWL_NIDX_HEADERAOWL_NIDX_MAGIC0AOWL_NIDX_MAGIC1AOWL_NIDX_MAX_ENTRIESAOWL_NIDX_MAX_FAULTSAOWL_NIDX_MAX_FILEAOWL_NIDX_MAX_KEYAOWL_NIDX_MAX_PROBESAOWL_NIDX_MAX_STAMP_TRIESAOWL_NIDX_SHARE_UNKNOWNAOWL_NIDX_ST_DOSSPANAOWL_NIDX_ST_FAULTAOWL_NIDX_ST_LFANEWAOWL_NIDX_ST_NOMODULEAOWL_NIDX_ST_NOMZAOWL_NIDX_ST_NOPEAOWL_NIDX_ST_NTSPANAOWL_NIDX_ST_OKAOWL_NIDX_VERSION
Functions
| Signature | Line |
|---|---|
char aowl_nameidx_reason(void) | 213 |
int32_t aowl_nameidx_staged(void) | 217 |
int32_t aowl_nameidx_count(void) | 218 |
uint64_t aowl_nameidx_imagekey(void) | 219 |
uint64_t aowl_nameidx_filehash(void) | 220 |
void aowl_nidx_fail(const char *why) | 222 |
uint64_t aowl_nidx_h_primary(const char *s, int32_t n) | 228 |
uint32_t aowl_nidx_h_check(const char *s, int32_t n) | 235 |
void aowl_nidx_cat(char *dst, int32_t cap, int32_t *at, const char *src) | 249 |
void aowl_nidx_cathex(char *dst, int32_t cap, int32_t *at, uint64_t v) | 254 |
void aowl_nidx_image_key_probe(void *out) | 265 |
void aowl_nidx_read_stamp(aowl_nidx_stamp_t *st) | 315 |
int32_t aowl_nidx_verify_stamp(void) | 338 |
int32_t aowl_nameidx_init(const char *dir) | 427 |
int32_t aowl_nameidx_ready(void) | 563 |
void aowl_nameidx_shutdown(void) | 569 |
uint32_t aowl_nameidx_lookup_shared(const char *spec, int32_t arity, uint32_t *share_out) | 587 |
uint32_t aowl_nameidx_lookup(const char *spec, int32_t arity) | 654 |

