Skip to content

aowlspt/il2cpp

Source: aowl/src/aowlspt/il2cpp.nim — 896 lines.

Bindings to the IL2CPP runtime C API, as exported by GameAssembly.dll.

This module is the reason aowlspt can reach a post-1.0 Tarkov client at all, so it is worth being explicit about why it exists and why it looks like this.

Post-1.0 Tarkov is IL2CPP: the C# was translated to C++ ahead of time and there are no managed assemblies left to load a plugin into. The usual answer is a stack that reconstructs the managed world -- BepInEx 6 IL2CPP runs a CoreCLR inside the process, Cpp2IL rebuilds proxy assemblies out of the metadata, and Il2CppInterop marshals between the two. It works, and it is an enormous amount of machinery to arrive back where you started.

None of it is necessary. Unity's IL2CPP runtime exports its own C API from GameAssembly.dll -- 242 functions, by name, unmangled -- and that API is the exact shape aowlspt's ABI already wanted: look a type up by name, look a method up on it, invoke it with an argument array. So the post-1.0 client host calls the runtime directly. No second runtime in the process, no generated assemblies, no interop layer, and the host is written in the same language as the mods it hosts.

Why the indirect calls go through C. nimony will not cast between a pointer and a proc type -- in either direction, and via an integer as well. That rules out the ordinary shape for this, which is a record of function pointers filled in by GetProcAddress. Rather than give up the runtime symbol resolution (which is not optional: inside the game the module is already loaded, and out of it the path is only known to the caller), the indirect call is done in abi/aowlspt_shim.h, one trampoline per distinct signature. There are eighteen of them and they are each one line. Compile with --passC:-I<repo>/abi.

What this module deliberately does not do: it does not read global-metadata.dat. BSG ship that file encrypted and the runtime decrypts it for itself during il2cpp_init. Going around that would be breaking a protection rather than using an interface, so everything here goes in through the runtime's own front door and sees exactly what the runtime chose to expose.

Types

Il2CppPtr

nim
  Il2CppPtr* = nil pointer

aowl/src/aowlspt/il2cpp.nim:44

Il2CppDomain

nim
  Il2CppDomain* = Il2CppPtr

aowl/src/aowlspt/il2cpp.nim:46

Il2CppAssembly

nim
  Il2CppAssembly* = Il2CppPtr

aowl/src/aowlspt/il2cpp.nim:47

Il2CppImage

nim
  Il2CppImage* = Il2CppPtr

aowl/src/aowlspt/il2cpp.nim:48

Il2CppClass

nim
  Il2CppClass* = Il2CppPtr

aowl/src/aowlspt/il2cpp.nim:49

Il2CppObject

nim
  Il2CppObject* = Il2CppPtr

aowl/src/aowlspt/il2cpp.nim:50

Il2CppMethod

nim
  Il2CppMethod* = Il2CppPtr      ## `MethodInfo*`

aowl/src/aowlspt/il2cpp.nim:51

Il2CppField

nim
  Il2CppField* = Il2CppPtr       ## `FieldInfo*`

aowl/src/aowlspt/il2cpp.nim:52

Il2CppProperty

nim
  Il2CppProperty* = Il2CppPtr    ## `PropertyInfo*`

aowl/src/aowlspt/il2cpp.nim:53

Il2CppType

nim
  Il2CppType* = Il2CppPtr

aowl/src/aowlspt/il2cpp.nim:54

Il2CppString

nim
  Il2CppString* = Il2CppPtr

aowl/src/aowlspt/il2cpp.nim:55

Il2CppThread

nim
  Il2CppThread* = Il2CppPtr

aowl/src/aowlspt/il2cpp.nim:56

Il2CppException

nim
  Il2CppException* = Il2CppPtr

aowl/src/aowlspt/il2cpp.nim:57

Il2CppIter

nim
  Il2CppIter* = Il2CppPtr

aowl/src/aowlspt/il2cpp.nim:61

Entry

nim
  Entry* = enum
    eSetDataDir, eSetConfigDir, eInit, eShutdown,
    eDomainGet, eDomainGetAssemblies, eAssemblyGetImage,
    eImageGetName, eImageGetClassCount, eImageGetClass,
    eClassFromName, eClassGetName, eClassGetNamespace, eClassGetParent,
    eClassGetMethods, eClassGetFields, eClassGetProperties,
    eClassGetMethodFromName, eClassGetFieldFromName, eClassGetPropertyFromName,
    eClassGetType, eClassIsValuetype, eClassInstanceSize, eRuntimeClassInit,
    eMethodGetName, eMethodGetParamCount, eMethodGetReturnType,
    eMethodGetParam, eMethodGetClass,
    eFieldGetName, eFieldGetType, eFieldGetOffset,
    eFieldGetValue, eFieldSetValue, eFieldStaticGetValue, eFieldStaticSetValue,
    ePropertyGetName, ePropertyGetGetMethod, ePropertyGetSetMethod,
    eRuntimeInvoke, eObjectNew, eRuntimeObjectInit, eObjectGetClass,
    eValueBox, eObjectUnbox,
    eStringNew, eStringChars, eStringLength,
    eTypeGetName, eTypeGetObject,
    eThreadAttach, eThreadDetach, eThreadCurrent,
    eGcHandleNew, eGcHandleGetTarget, eGcHandleFree,
    eFree,
    ## Appended in one group, for the same reason as `eMethodGetFlags` below:
    ## the enum's order is `EntryNames`' order, and inserting renames every
    ## entry after the insertion point without touching a line of this file.
    eClassFromType, eFieldGetFlags, eClassStaticFieldData, eGcWriteBarrier,
    ## Appended rather than inserted: the enum's order is the order of
    ## `EntryNames`, and moving an existing member renames every entry
    ## after it without changing a line of this file.
    eMethodGetFlags,
    ## Managed-array construction, appended for the same append-only reason.
    ## `il2cpp_array_new(elementClass, length)` returns an `Il2CppArray*`; it is
    ## what lets a mod hand a `byte[]` to a managed method — e.g. building the
    ## replacement image bytes for `ImageConversion.LoadImage` (see
    ## `mods/textures`). Not `Essential`: a mod that never constructs an array
    ## does not need it, and a build that stripped it is still hostable.
    eArrayNew,
    ## Appended, again, rather than inserted -- same append-only reason. These
    ## two are the runtime half of the codeGenModule code-pointer fallback (see
    ## `abi/aowlspt_codegen.h`): with them the walk from a `MethodInfo*` to its
    ## declaring image's NAME and to its metadata TOKEN needs no guessed struct
    ## offset for `MethodInfo`, `Il2CppClass` or `Il2CppImage`. Both were
    ## confirmed present in `GameAssembly.dll`'s export directory for build
    ## 1.1.0.1.46777 (386 exports) before being added here. Not `Essential`:
    ## a build without them simply loses the fallback.
    eClassGetImage, eMethodGetToken

aowl/src/aowlspt/il2cpp.nim:110

Il2Cpp

nim
  Il2Cpp* = object
    handle*: Il2CppPtr
    loaded*: bool
    lastError*: int32
    ## Indexed by `ord(Entry)`. nimony has neither enum-indexed arrays nor
    ## iteration over an enum type, so the ordinal is used directly and
    ## `EntryNames` is kept in the same order by construction.
    fns*: array[NumEntries, Il2CppPtr]
    missing*: seq[string]

aowl/src/aowlspt/il2cpp.nim:204

Routines

nullPtr

nim
proc nullPtr*(): Il2CppPtr {.inline.}

aowl/src/aowlspt/il2cpp.nim:63

isNull

nim
proc isNull*(p: Il2CppPtr): bool {.inline.}

aowl/src/aowlspt/il2cpp.nim:66

has

nim
proc has*(rt: Il2Cpp; e: Entry): bool {.inline.}

aowl/src/aowlspt/il2cpp.nim:232

openIl2Cpp

nim
proc openIl2Cpp*(path: string = ""): Il2Cpp

Binds to the IL2CPP runtime.

With no path the already-loaded GameAssembly.dll is used, which is the in-game case and the only correct one there. With a path the module is loaded, which is for out-of-process tooling.

aowl/src/aowlspt/il2cpp.nim:235

missingEssential

nim
proc missingEssential*(rt: Il2Cpp): seq[string]

aowl/src/aowlspt/il2cpp.nim:271

readCString

nim
proc readCString*(p: Il2CppPtr): string

Copies a NUL-terminated ASCII string the runtime owns -- or REFUSES.

THE NIL CHECK BELOW CANNOT FIRE FOR THE CASE THAT KILLED THE CLIENT, AND THAT IS WHY THE REST OF THIS PROC EXISTS. Measured (fact #198, docs/IL2CPP_EXPORTS.md): 38 of the 241 il2cpp_* exports are TOKEN-GATED -- they take a trailing 32-byte token argument the stock signature does not have and memcmp it before doing any work. On mismatch they do not return NULL and do not abort: they tail-call a trap that seeds a per-thread MT19937-64 and returns a uniform random non-zero uint64. il2cpp_class_get_name is one of them. So p != nil passes, the first byteAt dereferences a random address, and the process dies with 0xC0000005 -- which is exactly what took the client down at 10:30:54 through fov's bindOnObject -> fullName -> className chain, faulting at +0x31 inside this loop.

A nil check is therefore not a check. What follows is: a VirtualQuery on the pointer (a random 64-bit address is overwhelmingly not committed readable memory) re-run at every page-sized step, and a printable-ASCII gate on every byte (a random address that IS mapped is overwhelmingly not an ASCII identifier). Both can fail on real input and both say so by returning "" and bumping gReadCStringRefusals, which every caller already treats as "the runtime did not answer" -- className, classNamespace, methodName, fieldName, propertyName, imageName and typeName all return "" when the export is simply absent.

This is a refusal, not a guarantee. It narrows a certain crash to an unlikely one; it does not make a gated export safe to call.

aowl/src/aowlspt/il2cpp.nim:294

setDataDir

nim
proc setDataDir*(rt: Il2Cpp; dir: string)

aowl/src/aowlspt/il2cpp.nim:361

setConfigDir

nim
proc setConfigDir*(rt: Il2Cpp; dir: string)

aowl/src/aowlspt/il2cpp.nim:366

init

nim
proc init*(rt: Il2Cpp; domainName: string): Il2CppDomain

aowl/src/aowlspt/il2cpp.nim:371

shutdown

nim
proc shutdown*(rt: Il2Cpp)

aowl/src/aowlspt/il2cpp.nim:376

domainGet

nim
proc domainGet*(rt: Il2Cpp): Il2CppDomain

aowl/src/aowlspt/il2cpp.nim:380

domainGetAssemblies

nim
proc domainGetAssemblies*(rt: Il2Cpp; domain: Il2CppDomain; count: var int): Il2CppPtr

Returns the raw Il2CppAssembly**; use assemblyAt to index it.

aowl/src/aowlspt/il2cpp.nim:384

assemblyAt

nim
proc assemblyAt*(assemblies: Il2CppPtr; index: int): Il2CppAssembly

Indexes the Il2CppAssembly** returned above. A pointer array, so the stride is one pointer.

aowl/src/aowlspt/il2cpp.nim:394

assemblyGetImage

nim
proc assemblyGetImage*(rt: Il2Cpp; a: Il2CppAssembly): Il2CppImage

aowl/src/aowlspt/il2cpp.nim:404

imageGetName

nim
proc imageGetName*(rt: Il2Cpp; i: Il2CppImage): string

aowl/src/aowlspt/il2cpp.nim:408

imageGetClassCount

nim
proc imageGetClassCount*(rt: Il2Cpp; i: Il2CppImage): int

aowl/src/aowlspt/il2cpp.nim:412

imageGetClass

nim
proc imageGetClass*(rt: Il2Cpp; i: Il2CppImage; index: int): Il2CppClass

aowl/src/aowlspt/il2cpp.nim:416

methodClass

nim
proc methodClass*(rt: Il2Cpp; m: Il2CppMethod): Il2CppClass

The type a method was declared on. il2cpp_method_get_class is an exported entry point, so this is an interface call rather than a read of MethodInfo.klass at an assumed offset.

aowl/src/aowlspt/il2cpp.nim:420

classGetImage

nim
proc classGetImage*(rt: Il2Cpp; c: Il2CppClass): Il2CppImage

The assembly image a class was defined in. Exported by the runtime, so this is an interface call and not a struct-offset read -- which matters, because it is the hop the codeGenModule code-pointer fallback needs and a guessed Il2CppClass.image offset would be exactly the kind of coin flip this project refuses.

aowl/src/aowlspt/il2cpp.nim:428

methodToken

nim
proc methodToken*(rt: Il2Cpp; m: Il2CppMethod): uint32

A method's metadata token. token and 0xFFFFFF is its RID within its own image, which is the index (minus one) into that image's Il2CppCodeGenModule.methodPointers. Zero means unavailable.

Called through a uint32-returning trampoline rather than the size_t one: the high half of RAX is unspecified for a uint32-returning callee, so reading it as a size_t can bring garbage back in the top 32 bits.

aowl/src/aowlspt/il2cpp.nim:437

classFromName

nim
proc classFromName*(rt: Il2Cpp; image: Il2CppImage; ns, name: string): Il2CppClass

aowl/src/aowlspt/il2cpp.nim:449

className

nim
proc className*(rt: Il2Cpp; c: Il2CppClass): string

aowl/src/aowlspt/il2cpp.nim:457

classNameSafe

nim
proc classNameSafe*(rt: Il2Cpp; c: Il2CppClass): string

className that returns "" instead of faulting when c is a class handed back by a blind enumeration that is not safe to dereference. For scanning every class in an image to find one by name; see the host's key dump.

aowl/src/aowlspt/il2cpp.nim:462

classNamespace

nim
proc classNamespace*(rt: Il2Cpp; c: Il2CppClass): string

aowl/src/aowlspt/il2cpp.nim:471

classParent

nim
proc classParent*(rt: Il2Cpp; c: Il2CppClass): Il2CppClass

aowl/src/aowlspt/il2cpp.nim:475

nextMethod

nim
proc nextMethod*(rt: Il2Cpp; c: Il2CppClass; iter: var Il2CppIter): Il2CppMethod

aowl/src/aowlspt/il2cpp.nim:479

nextField

nim
proc nextField*(rt: Il2Cpp; c: Il2CppClass; iter: var Il2CppIter): Il2CppField

aowl/src/aowlspt/il2cpp.nim:483

nextProperty

nim
proc nextProperty*(rt: Il2Cpp; c: Il2CppClass; iter: var Il2CppIter): Il2CppProperty

aowl/src/aowlspt/il2cpp.nim:487

findMethod

nim
proc findMethod*(rt: Il2Cpp; c: Il2CppClass; name: string; argc: int): Il2CppMethod

aowl/src/aowlspt/il2cpp.nim:492

findField

nim
proc findField*(rt: Il2Cpp; c: Il2CppClass; name: string): Il2CppField

aowl/src/aowlspt/il2cpp.nim:499

findProperty

nim
proc findProperty*(rt: Il2Cpp; c: Il2CppClass; name: string): Il2CppProperty

aowl/src/aowlspt/il2cpp.nim:505

classType

nim
proc classType*(rt: Il2Cpp; c: Il2CppClass): Il2CppType

aowl/src/aowlspt/il2cpp.nim:511

classIsValueType

nim
proc classIsValueType*(rt: Il2Cpp; c: Il2CppClass): bool

aowl/src/aowlspt/il2cpp.nim:515

classInstanceSize

nim
proc classInstanceSize*(rt: Il2Cpp; c: Il2CppClass): int

aowl/src/aowlspt/il2cpp.nim:519

runtimeClassInit

nim
proc runtimeClassInit*(rt: Il2Cpp; c: Il2CppClass)

aowl/src/aowlspt/il2cpp.nim:523

methodName

nim
proc methodName*(rt: Il2Cpp; m: Il2CppMethod): string

aowl/src/aowlspt/il2cpp.nim:527

methodParamCount

nim
proc methodParamCount*(rt: Il2Cpp; m: Il2CppMethod): int

aowl/src/aowlspt/il2cpp.nim:531

methodReturnType

nim
proc methodReturnType*(rt: Il2Cpp; m: Il2CppMethod): Il2CppType

aowl/src/aowlspt/il2cpp.nim:535

methodParam

nim
proc methodParam*(rt: Il2Cpp; m: Il2CppMethod; index: int): Il2CppType

aowl/src/aowlspt/il2cpp.nim:539

fieldName

nim
proc fieldName*(rt: Il2Cpp; f: Il2CppField): string

aowl/src/aowlspt/il2cpp.nim:543

fieldType

nim
proc fieldType*(rt: Il2Cpp; f: Il2CppField): Il2CppType

aowl/src/aowlspt/il2cpp.nim:547

fieldOffset

nim
proc fieldOffset*(rt: Il2Cpp; f: Il2CppField): int

aowl/src/aowlspt/il2cpp.nim:551

fieldGetValue

nim
proc fieldGetValue*(rt: Il2Cpp; obj: Il2CppObject; f: Il2CppField; into: Il2CppPtr)

aowl/src/aowlspt/il2cpp.nim:555

fieldSetValue

nim
proc fieldSetValue*(rt: Il2Cpp; obj: Il2CppObject; f: Il2CppField; value: Il2CppPtr)

aowl/src/aowlspt/il2cpp.nim:560

fieldStaticGetValue

nim
proc fieldStaticGetValue*(rt: Il2Cpp; f: Il2CppField; into: Il2CppPtr)

aowl/src/aowlspt/il2cpp.nim:565

fieldStaticSetValue

nim
proc fieldStaticSetValue*(rt: Il2Cpp; f: Il2CppField; value: Il2CppPtr)

aowl/src/aowlspt/il2cpp.nim:569

propertyName

nim
proc propertyName*(rt: Il2Cpp; p: Il2CppProperty): string

aowl/src/aowlspt/il2cpp.nim:573

propertyGetter

nim
proc propertyGetter*(rt: Il2Cpp; p: Il2CppProperty): Il2CppMethod

aowl/src/aowlspt/il2cpp.nim:577

propertySetter

nim
proc propertySetter*(rt: Il2Cpp; p: Il2CppProperty): Il2CppMethod

aowl/src/aowlspt/il2cpp.nim:581

invoke

nim
proc invoke*(rt: Il2Cpp; m: Il2CppMethod; obj: Il2CppObject; args: Il2CppPtr; exc: var Il2CppException): Il2CppObject

args is an Il2CppPtr to an array of pointers, or null for none.

The exception out-parameter is not optional. IL2CPP does not unwind through a C caller; a managed throw comes back here as a written-to pointer and an ignored one is a silently wrong result.

aowl/src/aowlspt/il2cpp.nim:585

objectNew

nim
proc objectNew*(rt: Il2Cpp; c: Il2CppClass): Il2CppObject

aowl/src/aowlspt/il2cpp.nim:597

objectClass

nim
proc objectClass*(rt: Il2Cpp; obj: Il2CppObject): Il2CppClass

aowl/src/aowlspt/il2cpp.nim:601

arrayNew

nim
proc arrayNew*(rt: Il2Cpp; elementClass: Il2CppClass; count: int): Il2CppPtr

Allocate a managed 1-D array of elementClass with count elements, e.g. a byte[] from the System.Byte class. Returns null if the runtime did not export il2cpp_array_new, if the class is null, or if count is negative — the caller then does nothing, which is the safe outcome.

The element data of the returned Il2CppArray* begins at the standard 64-bit IL2CPP array header size (32 bytes: Il2CppObject{klass, monitor} 16 + bounds 8 + max_length 8) past the pointer. A caller filling it writes count bytes from that offset, which stays inside the allocation.

aowl/src/aowlspt/il2cpp.nim:605

valueBox

nim
proc valueBox*(rt: Il2Cpp; c: Il2CppClass; data: Il2CppPtr): Il2CppObject

aowl/src/aowlspt/il2cpp.nim:619

objectUnbox

nim
proc objectUnbox*(rt: Il2Cpp; obj: Il2CppObject): Il2CppPtr

aowl/src/aowlspt/il2cpp.nim:623

newString

nim
proc newString*(rt: Il2Cpp; s: string): Il2CppString

aowl/src/aowlspt/il2cpp.nim:627

readString

nim
proc readString*(rt: Il2Cpp; s: Il2CppString): string

A managed System.String as UTF-8.

Only the BMP is decoded; a surrogate pair comes out as replacement characters rather than as one wrong character. Everything this is used for -- type names, member names, JSON -- is ASCII in practice, and quietly producing mojibake would be worse than visibly not handling it.

aowl/src/aowlspt/il2cpp.nim:632

typeName

nim
proc typeName*(rt: Il2Cpp; t: Il2CppType): string

The runtime allocates this one and expects it back.

aowl/src/aowlspt/il2cpp.nim:662

threadAttach

nim
proc threadAttach*(rt: Il2Cpp; domain: Il2CppDomain): Il2CppThread

Every thread that is not one the runtime created must attach before it calls in. Skipping this does not fail loudly -- it corrupts the GC's view of the stack and the process dies somewhere else, later.

aowl/src/aowlspt/il2cpp.nim:670

threadDetach

nim
proc threadDetach*(rt: Il2Cpp; t: Il2CppThread)

aowl/src/aowlspt/il2cpp.nim:677

threadCurrent

nim
proc threadCurrent*(rt: Il2Cpp): Il2CppThread

aowl/src/aowlspt/il2cpp.nim:681

gcHandleNew

nim
proc gcHandleNew*(rt: Il2Cpp; obj: Il2CppObject; pinned: bool): uint32

aowl/src/aowlspt/il2cpp.nim:685

gcHandleTarget

nim
proc gcHandleTarget*(rt: Il2Cpp; h: uint32): Il2CppObject

aowl/src/aowlspt/il2cpp.nim:689

gcHandleFree

nim
proc gcHandleFree*(rt: Il2Cpp; h: uint32)

aowl/src/aowlspt/il2cpp.nim:693

splitTypeName

nim
proc splitTypeName*(qualified: string; ns, name: var string)

Splits Namespace.Nested.Type at the last dot: IL2CPP keeps namespace and type name separate, and everything a mod author writes keeps them together.

aowl/src/aowlspt/il2cpp.nim:701

findClass

nim
proc findClass*(rt: Il2Cpp; qualified: string): Il2CppClass

Looks a type up by qualified name across every loaded assembly.

Searching all images rather than requiring an assembly name is the point: Tarkov and SPT move types between assemblies across releases, and a mod that named the assembly would break on a release where nothing about the type itself changed.

aowl/src/aowlspt/il2cpp.nim:718

methodPointer

nim
proc methodPointer*(rt: Il2Cpp; m: Il2CppMethod): Il2CppPtr

The compiled function behind a method.

IL2CPP exposes no accessor for this, so it is read directly: it is the first field of MethodInfo and has been for every Unity version this targets. That is a layout assumption rather than an interface, so the result is checked before it is used -- a pointer that is not in an executable page means the assumption is wrong on this build, and the caller gets nothing rather than a patch aimed at the middle of a struct.

aowl/src/aowlspt/il2cpp.nim:749

fullName

nim
proc fullName*(rt: Il2Cpp; c: Il2CppClass): string

aowl/src/aowlspt/il2cpp.nim:775

methodFlags

nim
proc methodFlags*(rt: Il2Cpp; m: Il2CppMethod): uint32

The method's attribute flags. Bit 0x10 is static, which is the one thing the detour path needs: a compiled instance method takes this in the first argument register and a static one does not, so getting this wrong shifts every argument by one and reports the wrong values with total confidence.

aowl/src/aowlspt/il2cpp.nim:785

methodIsStatic

nim
proc methodIsStatic*(rt: Il2Cpp; m: Il2CppMethod): bool

aowl/src/aowlspt/il2cpp.nim:795

classFromType

nim
proc classFromType*(rt: Il2Cpp; t: Il2CppType): Il2CppClass

The class behind a type, exactly.

The alternative is to take the type's name and look the class up by it, which works for System.Int32 and falls apart on a generic, an array or a nested type -- the printed name is not always a name the resolver accepts. Classifying a parameter is the difference between binding a method on the fast path and refusing it, so it is worth an entry point.

aowl/src/aowlspt/il2cpp.nim:798

boxHeaderBytes

nim
proc boxHeaderBytes*(rt: Il2Cpp): int

How much of a value type's reported instance size is object header -- measured, not assumed.

il2cpp_class_get_instance_size on the game reports a value type's boxed size, header plus payload, because that is the only form the runtime allocates. That is not universal: a stand-in can just as reasonably report the payload, and both answers are self-consistent, so no single number in isolation tells them apart.

One number whose payload is known does. System.Int32 holds four bytes by definition, so whatever this runtime reports for it, minus four, is the header. System.Double only checks that answer: eight bytes of payload must report four more than Int32 does, and if it does not, this runtime is not measuring what it is assumed to be, and the fallback stands.

Getting it wrong subtracts a header that is not there, which turns every small value type into a negative width and refuses it -- an enum reported as unclassifiable, a shaped call quietly falling back to reflection, and the log saying the shape was checked. Two mods hit exactly that before this existed; the host has the same calculation in invoke.nim.

aowl/src/aowlspt/il2cpp.nim:811

valueWidth

nim
proc valueWidth*(rt: Il2Cpp; c: Il2CppClass): int

The payload width of a value type, or 0 if it is not one. See boxHeaderBytes for why the header is measured rather than named.

aowl/src/aowlspt/il2cpp.nim:847

fieldFlags

nim
proc fieldFlags*(rt: Il2Cpp; f: Il2CppField): uint32

Field attributes. Bit 0x10 is static, which cannot be inferred reliably from the offset: a static field's offset is into the class's static data, not into an instance, and the two ranges can overlap.

aowl/src/aowlspt/il2cpp.nim:857

fieldIsStatic

nim
proc fieldIsStatic*(rt: Il2Cpp; f: Il2CppField): bool

aowl/src/aowlspt/il2cpp.nim:864

staticFieldData

nim
proc staticFieldData*(rt: Il2Cpp; c: Il2CppClass): Il2CppPtr

The base address of a class's static storage, which is what a static field's offset is relative to.

aowl/src/aowlspt/il2cpp.nim:867

writeBarrier

nim
proc writeBarrier*(rt: Il2Cpp; obj, field, value: Il2CppPtr)

Stores a reference into a field and tells the collector.

Writing the pointer directly is faster and is how an object gets collected while something still points at it: a generational collector needs to know that an old object now references a young one, and a plain store does not tell it. The crash arrives at the next collection, far from the write.

aowl/src/aowlspt/il2cpp.nim:873

hasWriteBarrier

nim
proc hasWriteBarrier*(rt: Il2Cpp): bool

aowl/src/aowlspt/il2cpp.nim:884

writeBarrierFn

nim
proc writeBarrierFn*(rt: Il2Cpp): Il2CppPtr

The resolved entry itself, for the fast path.

writeBarrier above is the pleasant form and costs a has test and an Il2Cpp copy per call. A FieldBinding is bound once and written many times, so it keeps this pointer instead and branches on nil -- which is also how it records that a runtime without the entry was bound against, rather than silently deciding that question again at every write.

aowl/src/aowlspt/il2cpp.nim:886

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