Skip to content

aowlspt_invoke2.h

Source: abi/aowlspt_invoke2.h — 521 lines, 29 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_invoke2.h -- DIRECT invocation of managed IL2CPP methods by static
RVA, with no reflection anywhere.

===========================================================================
WHY THIS EXISTS
===========================================================================

Post-1.0 this client's IL2CPP *reflection* surface is dead: the live P2-P5
probe proved `il2cpp_object_get_class`, `il2cpp_class_get_name`,
`il2cpp_value_box` and field iteration all FAULT, even on the Unity main
thread, and `il2cpp_runtime_invoke` crashed outright. Everything the host has
built since -- the version brand, botcap, the settings control walk -- is
therefore raw field reads and writes at fixed offsets.

Raw fields are enough to CHANGE the UI. They are not enough to CREATE it: a
new GameObject, a component attached to it, a Canvas child -- all of that is
managed code, and the only door left is to call that managed code.

The hypothesis this header implements: **IL2CPP AOT-compiles every managed
method into an ordinary native function, so it can be called directly at its
RVA as a native function pointer.** No MethodInfo lookup, no metadata query,
no reflection -- just a `call`, exactly as the game itself does.

===========================================================================
THE CALLING CONVENTION FOR THIS BUILD (1.1.0.1.46777), WITH EVIDENCE
===========================================================================

It is Win64 (Microsoft x64) with ONE addition: IL2CPP appends a hidden
trailing `const MethodInfo*` argument after the declared ones, in the next
free INTEGER register (and on the stack past the fourth). It is passed as a
literal NULL by the compiler for every ordinary (non-generic) method.

  instance:  RCX = this,  RDX/R8/R9 = arg0..arg2,  then MethodInfo*
  static:                 RCX/RDX/R8/R9 = arg0..arg3, then MethodInfo*
  floats:    XMM0..XMM3 by POSITION, per Win64 (a float in slot 1 is XMM1)
  returns:   RAX (integer/reference) or XMM0 (float/double), per Win64

Evidence 1 -- an INSTANCE method with one reference argument. From
`TMPro.TMP_DefaultControls::CreateUIElementRoot` @ RVA 0x5190080, which is
literally "make a new UI GameObject":

    51900c9: mov  rcx, [rip+0x1c52f88]   ; Il2CppClass* UnityEngine.GameObject
    51900d0: call 0x1805d9e20            ; il2cpp::vm::Object::New(klass)
    51900d5: xor  r8d, r8d               ; <-- R8 = MethodInfo* = NULL
    51900d8: mov  rdx, rdi               ; RDX = arg0, the name String*
    51900db: mov  rcx, rax               ; RCX = this, the fresh GameObject
    51900de: mov  rbx, rax
    51900e1: call 0x1852a8f40            ; UnityEngine.GameObject::.ctor(string)

`.ctor(string)` declares ONE parameter, and the call site sets THREE integer
registers. The third is the hidden MethodInfo*, and it is zero.

Evidence 2 -- a STATIC method with two reference arguments, from the same
function (`TMP_DefaultControls::SetParentAndAlign(GameObject, GameObject)`):

    519020e: xor  r8d, r8d               ; <-- R8 = MethodInfo* = NULL
    5190211: mov  rdx, rdi               ; RDX = arg1, the parent
    5190214: mov  rcx, rbx               ; RCX = arg0, the child
    5190217: call 0x1851903a0            ; ...::SetParentAndAlign

Two declared args in RCX/RDX -- no `this` shift, because it is static -- and
the MethodInfo* in the next register, again NULL.

Evidence 3 -- the callee side confirms it is genuinely ignored. Every simple
accessor compiles to a body that reads RCX and never touches the MethodInfo
register at all, e.g. `UnityEngine.Component::get_gameObject` @ 0x11F57E0:

    11f57e0: push rbx ; sub rsp,0x20
    11f57e6: mov  rax, [rip+0x5edecfb]   ; cached icall pointer
    11f57ed: mov  rbx, rcx               ; RCX = this  (RDX never read)
    ...      test rax,rax / jne -> resolve the icall by name on first use
    11f580d: mov  rcx, rbx ; add rsp,0x20 ; pop rbx ; jmp rax

and `UnityEngine.Time::get_frameCount` @ 0x52B3400 (static, zero args) never
reads RCX either. Note the lazy-resolve shape: an unresolved icall resolves
itself on first call, so calling these cold from a detour is safe.

Evidence 4 -- the ONE case where the MethodInfo* is load-bearing: a shared
generic instantiation. `UnityEngine.GameObject::AddComponent<T>` @ 0x2A9AE90:

    2a9aea4: cmp  qword [rdx+0x38], 0    ; <-- RDX *is* the MethodInfo*
    2a9aea9: mov  rdi, rdx
    2a9aeac: mov  rbp, rcx               ; RCX = this
    ...      call 0x180563290            ; initialise its RGCTX if unset
    2a9aed8: mov  rbx, [rdi+0x38]        ; the runtime generic context

An instance method with ZERO declared parameters, and RDX is the MethodInfo*
-- which it dereferences. So a generic method cannot be called with NULL; it
needs the real `MethodInfo*` for that exact instantiation. The game keeps
those in .data cache slots (see AOWL_MI2_DATA_ADDCOMP_RECTTRANSFORM below).

===========================================================================
WHAT THIS HEADER PROVIDES
===========================================================================

 * A build-pinned target table: name + RVA + 16 prologue bytes, verified the
   same way `aowlspt_bridge.h` verifies a detour target -- VirtualQuery for
   committed executable memory, then memcmp of the prologue. On any other
   build the lookup returns NULL and every step simply does not run.
 * Typed call thunks, one per shape used by the ladder. They exist in C
   because casting a pointer to a function type is not expressible in nimony,
   and because the hidden MethodInfo* must be a real argument of the callee
   type rather than something bolted on afterwards.
 * The two IL2CPP *allocation* exports (`il2cpp_object_new`,
   `il2cpp_string_new`) plus the two *type-object* exports the AddComponent
   (Type) route would need. Allocation is proven to work on this build (the
   version brand allocates a String on the Unity thread); the type-object
   pair is UNPROVEN and is probed, not assumed.
 * A guarded reader for the game's own .data metadata cache slots, which is
   how a real generic `MethodInfo*` is obtained without reflection.

NOTHING here calls anything on its own. It is a table and a set of thunks;
`host/Aowlspt.Host.Il2Cpp/invoke2.nim` drives it, from inside a proven
Unity-thread detour, one step at a time, under the VEH/SEH guard.

All RVAs are for `GameAssembly.dll` imagebase 0x180000000, build
1.1.0.1.46777, and were resolved offline by `tools/il2cpp_resolve.py` +
the 185k-entry RVA map, then confirmed by disassembly of real call sites.

Constants

  • AOWLSPT_INVOKE2_H
  • AOWL_MI2_ADDCOMPONENT_GEN
  • AOWL_MI2_ADDCOMPONENT_TYPE
  • AOWL_MI2_DATA_ADDCOMP_RECTTRANSFORM
  • AOWL_MI2_DATA_GAMEOBJECT_CLASS
  • AOWL_MI2_GET_FRAMECOUNT
  • AOWL_MI2_GET_GAMEOBJECT
  • AOWL_MI2_GET_INSTANCE_ID
  • AOWL_MI2_GET_NAME
  • AOWL_MI2_GET_SCREEN_HEIGHT
  • AOWL_MI2_GET_SCREEN_WIDTH
  • AOWL_MI2_GO_CTOR_STRING
  • AOWL_MI2_GO_GET_TRANSFORM
  • AOWL_MI2_GO_SETACTIVE
  • AOWL_MI2_INSTANTIATE
  • AOWL_MI2_PROBE_NAME
  • AOWL_MI2_PROBE_TEXT
  • AOWL_MI2_SET_AS_FIRST_SIBLING
  • AOWL_MI2_SET_NAME
  • AOWL_MI2_SET_PARENT_ALIGN
  • AOWL_MI2_TARGET_COUNT

Types

  • struct AowlMi2Target

Functions

SignatureLine
void aowl_mi2_fn(int32_t i)275
char aowl_mi2_name(int32_t i)298
uint32_t aowl_mi2_rva(int32_t i)302
int32_t aowl_mi2_target_count(void)306
int32_t aowl_mi2_base_ok(void)307
int32_t aowl_mi2_ok_count(void)308
int32_t aowl_mi2_bad_count(void)309
void aowl_mi2_call_p_p(void* fn, void* self)327
int32_t aowl_mi2_call_i_p(void* fn, void* self)333
int32_t aowl_mi2_call_i_v(void* fn)339
void aowl_mi2_call_v_pp(void* fn, void* self, void* a0)345
void aowl_mi2_call_v_pb(void* fn, void* self, int32_t a0)351
void aowl_mi2_call_p_pp(void* fn, void* self, void* a0)357
void aowl_mi2_call_p_s1(void* fn, void* a0)363
void aowl_mi2_call_v_s2(void* fn, void* a0, void* a1)369
void aowl_mi2_call_generic0(void* fn, void* self, void* mi)378
void aowl_mi2_exports_init(void)409
int32_t aowl_mi2_have_object_new(void)424
int32_t aowl_mi2_have_type_route(void)427
void aowl_mi2_object_new(void* klass)431
void aowl_mi2_string_new(const char* s)436
void aowl_mi2_type_object_of(void* klass)443
char aowl_mi2_probe_name(void)461
char aowl_mi2_probe_text(void)462
void aowl_mi2_probe_name_str(void)463
void aowl_mi2_probe_text_str(void)466
void aowl_mi2_data_ptr(uint32_t rva)500
void aowl_mi2_go_class_slot(void)513
void aowl_mi2_addcomp_rect_mi(void)516

aoughwl — self-hosted platform for things n stuff. Contact / Support on Discord for access to the private backends.