Skip to content

Support metadata v39 (Unity 6.x) - #925

Open
jdziat wants to merge 1 commit into
Perfare:masterfrom
jdziat:support-metadata-v39
Open

jdziat wants to merge 1 commit into
Perfare:masterfrom
jdziat:support-metadata-v39

Conversation

@jdziat

@jdziat jdziat commented Aug 9, 2026

Copy link
Copy Markdown

Support metadata v39 (Unity 6.x)

Adds support for IL2CPP global-metadata.dat version 39, as shipped by Unity 6.x.
Currently Metadata.cs rejects anything above v31.

Versions 32–38 were never shipped publicly, so v39 is gated explicitly
(version > 31 && version != 39) rather than by widening the range.

What changed in v39

1. Header descriptors became triples

Every table descriptor gained a trailing element count, so the classic
(offset, byteSize) pairs are now (offset, byteSize, count) triples. The set of
tables and their order are otherwise identical to v31 — there are exactly 31 of
each — so the counts are interleaved with [Version(Min = 39)] and no separate
header reader is needed.

The counts are read but not relied upon: array lengths still come from
byteSize / SizeOf(T), which doubles as a check that the struct sizes below are
right.

2. Indexes narrowed to uint16

Paired start/count indexes and most TypeIndex fields dropped from int32 to
uint16, with 0xFFFF as the "no value" sentinel:

Struct v31 v39
Il2CppTypeDefinition 88 76
Il2CppMethodDefinition 36 30
Il2CppFieldDefinition 12 10
Il2CppParameterDefinition 12 10
Il2CppEventDefinition 24 22
Il2CppImageDefinition 40 36
Il2CppGenericParameter 16 14
Il2CppFieldRef 8 6

The standalone TypeIndex arrays (interfaces, genericParameterConstraints)
narrowed to 2 bytes per entry as well. Il2CppPropertyDefinition,
Il2CppGenericContainer, Il2CppCustomAttributeDataRange and the
nestedTypes / vtableMethods arrays are unchanged.

These are read into [Version(Min = 39)] ushort fields and widened back into the
existing int fields by Metadata.FixIndexesV39, so nothing downstream needs to
know the format changed. Widening maps 0xFFFF to -1, which caps usable indexes
at 65534 — well above anything Unity currently emits (the test game peaked at
59841 Il2CppTypes).

3. Fields removed or merged

Il2CppTypeDefinition.parentIndex and elementTypeIndex now share one slot.
An enum's parent is always System.Enum and therefore redundant, so for enums the
slot carries the underlying type instead; every other type stores its parent as
before. FixIndexesV39 splits them back apart, recovering the parent from
System.Enum's own byvalTypeIndex.

This one is easy to get wrong and fails far from its cause. Read the slot as
parentIndex unconditionally and generated enums derive from System.Int32,
TypeDefinition.IsEnum goes false, and Mono.Cecil's WriteCustomAttributeEnumValue
throws ArgumentException on every enum-typed custom attribute argument — which
took out 75 of 81 dummy assemblies on the test game, while small assemblies with no
such attributes wrote fine.

The giveaway that it is not a parent index: two enums with different underlying
types hold different values there. On the test game a byte-backed enum held
29610 and an int-backed enum held 34664. Parent indexes could not differ.

Also:

  • Il2CppStringLiteral.length is gone. The table is now a sentinel-terminated
    array of data offsets (count is literals + 1), so each length is recovered from
    the following entry.
  • Il2CppCodeRegistration lost reversePInvokeWrapperCount / reversePInvokeWrappers.
    This is in the binary rather than metadata. Reading the v31 layout shifts every
    subsequent field by two slots and genericMethodPointersCount picks up a
    pointer value, producing an immediate OverflowException in Il2Cpp.Init.
    Il2CppMetadataRegistration is unchanged.
  • Il2CppAssemblyDefinition gained a 4-byte field after token. It mirrors the
    token RID in every assembly observed (1 for real assemblies, 0 for the synthetic
    __Generated module). Read only to keep the stride correct; named unknownV39
    since its purpose is unconfirmed.

Other changes

  • StructGenerator now emits .h files for v39. The runtime C structs are
    unchanged from v29, so it reuses HeaderConstants.HeaderV29.
  • DummyAssemblyExporter reports and skips an assembly that fails to serialise
    instead of letting one bad attribute abort the whole export. This is dormant
    now that v39 writes cleanly, but the previous behaviour lost all remaining
    assemblies. Happy to drop this if you would rather keep the PR to v39 only.

Verification

Tested against a Unity 6 title: metadata v39, 16586 type definitions, 114591
methods, 80 assemblies.

Output Result
dump.cs 48 MB, no embedded exceptions
il2cpp.h 73 MB
script.json 190 MB
stringliteral.json 1.2 MB
DummyDll 81/81 assemblies

Each layout claim was checked against a property that breaks if the decode is
wrong, rather than by eyeballing plausible output:

  • declaringTypeIndex's non-sentinel count is exactly the nestedTypes table
    length (7685).
  • Image typeStart/typeCount chain contiguously and sum to exactly the type
    definition count (16586).
  • Assembly referencedAssemblyStart/Count chain to exactly the
    referencedAssemblies count (475).
  • mscorlib's public key token reads b77a5c561934e089 at version 4.0.0.0.
  • unresolvedVirtualCallCount in the binary matches the metadata's
    unresolvedVirtualCallParameterRanges count (4259).
  • codeGenModules resolves to the same address found independently by scanning
    the binary for the module table.
  • All 843 generic type definitions populate the genericContainerIndex lane and
    no enum does, confirming which lane is which.

Method addresses, field offsets and constant values were also cross-checked
against a separate parser written from scratch, and against disassembly of
specific methods.

Notes for review

  • I do not have a second v39 title to test against, so the unknownV39 field in
    Il2CppAssemblyDefinition and the exact exportedTypeStart/exportedTypeCount
    encoding in Il2CppImageDefinition are inferred from one sample. Neither is
    read by the dumper; both matter only for struct stride, which is verified.
  • v39 continues v29's compressed-integer encoding for constant blob values, so
    GetConstantValueFromBlob's existing Version >= 29 branches already apply. No
    change was needed, but it is worth knowing when hand-checking constants: the
    byte 0x08 in the default value blob decodes to 4, not 8.

🤖 Generated with Claude Code

Adds parsing for IL2CPP global-metadata.dat version 39, which changes the
on-disk layout in three ways relative to v31.

Header: every table descriptor gained a trailing element count, turning the
(offset, size) pairs into (offset, size, count) triples. The set of tables and
their order are otherwise unchanged, so the counts are interleaved via
[Version(Min = 39)] rather than needing a bespoke header reader.

Narrowed indexes: paired start/count indexes and most TypeIndex fields dropped
from int32 to uint16, with 0xFFFF as the "no value" sentinel.
Il2CppTypeDefinition 88 -> 76 bytes, Il2CppMethodDefinition 36 -> 30,
Il2CppFieldDefinition 12 -> 10. These are read into [Version(Min = 39)] ushort
fields and widened back into the existing int fields by Metadata.FixIndexesV39,
so no downstream consumer needs to know about the change.

Removed and merged fields:

- Il2CppTypeDefinition.parentIndex and elementTypeIndex now share a slot. An
  enum's parent is always System.Enum and therefore redundant, so for enums the
  slot carries the underlying type instead. FixIndexesV39 splits them back
  apart. Without this, generated enums derive from their underlying type,
  TypeDefinition.IsEnum is false, and Mono.Cecil cannot serialise any
  enum-typed custom attribute argument - on the test game that alone broke
  75 of 81 dummy assemblies.
- Il2CppStringLiteral.length is gone. The table is now a sentinel-terminated
  array of data offsets, so each length is recovered from the next entry.
- Il2CppCodeRegistration lost reversePInvokeWrapperCount and
  reversePInvokeWrappers. Il2CppMetadataRegistration is unchanged.
- Il2CppAssemblyDefinition gained a 4-byte field after token. It mirrors the
  token RID in every assembly observed and is read only to keep the stride
  correct.

Also enables .h struct generation for v39, whose runtime C structs are
unchanged from v29, and makes DummyAssemblyExporter report and skip an assembly
that fails to serialise rather than aborting the entire export.

Verified against a Unity 6 title (metadata v39, 16586 type definitions, 80
assemblies): dump.cs, il2cpp.h, script.json, stringliteral.json and 81/81 dummy
assemblies all generate cleanly. Method addresses, field offsets and constant
values were cross-checked against an independent parser written from scratch.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@iameli

iameli commented Sep 7, 2026

Copy link
Copy Markdown

Tried against Big Walk and got this:

Initializing metadata...
System.ArgumentException: An item with the same key has already been added. Key: 51145
   at System.Collections.Generic.Dictionary`2.TryInsert(TKey key, TValue value, InsertionBehavior behavior)
   at System.Linq.Enumerable.ToDictionary[TSource,TKey](TSource[] source, Func`2 keySelector, IEqualityComparer`1 comparer)
   at System.Linq.Enumerable.ToDictionary[TSource,TKey](IEnumerable`1 source, Func`2 keySelector)
   at Il2CppDumper.Metadata..ctor(Stream stream) in C:\Users\iameli\code\Il2CppDumper\Il2CppDumper\Il2Cpp\Metadata.cs:line 104
   at Il2CppDumper.Program.Init(String il2cppPath, String metadataPath, Metadata& metadata, Il2Cpp& il2Cpp) in C:\Users\iameli\code\Il2CppDumper\Il2CppDumper\Program.cs:line 123
   at Il2CppDumper.Program.Main(String[] args) in C:\Users\iameli\code\Il2CppDumper\Il2CppDumper\Program.cs:line 97
Press any key to exit...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants