Skip to content

V2 Rewrite - #238

Draft
ds5678 wants to merge 502 commits into
BepInEx:masterfrom
ds5678:v2-rewrite
Draft

ds5678 wants to merge 502 commits into
BepInEx:masterfrom
ds5678:v2-rewrite

Conversation

@ds5678

@ds5678 ds5678 commented Oct 9, 2025 •

Copy link
Copy Markdown
Collaborator

This is mostly finished, but it's being tested and cleaned up.

Closes #70
Resolves #87
Closes #95
Resolves #97
Closes #103
Closes #107
Closes #108
Resolves #109
Resolves #127
Resolves #128
Resolves #136
Closes #139
Resolves #140
Resolves #141
Resolves #142
Closes #143
Resolves #144
Resolves #145
Resolves #146
Resolves #147
Resolves #148
Resolves #149
Resolves #150
Resolves #151
Resolves #152
Resolves #165
Resolves #166
Resolves #174
Resolves #175
Resolves #176
Resolves #182
Closes #187
Resolves #195
Resolves #207
Resolves #213
Closes #216
Resolves #217
Resolves #218
Resolves #221
Closes #222
Resolves #224
Closes #234
Resolves #240
Resolves #241
Closes #246
Closes #248
Resolves #249
Resolves #258
Resolves #265
Resolves #274

@ds5678
ds5678 force-pushed the v2-rewrite branch 3 times, most recently from fd4ac65 to df3a9b5 Compare April 14, 2026 04:55
@ds5678
ds5678 marked this pull request as ready for review May 15, 2026 22:45
@ds5678

ds5678 commented May 15, 2026

Copy link
Copy Markdown
Collaborator Author

ds5678 marked this pull request as ready for review

Just checking something. Disregard it

@ds5678
ds5678 marked this pull request as draft May 15, 2026 23:50
@ds5678 ds5678 added this to the 2.0.0 milestone May 17, 2026
@ds5678 ds5678 changed the title WIP: V2 Rewrite V2 Rewrite May 17, 2026
ds5678 and others added 16 commits October 9, 2026 18:16
Resolve trampoline helper methods on `TrampolineBuilder` instead of `TypeInjector` so trampoline generation calls the correct private static methods after consolidation.
Add FieldAccess helpers to convert GCHandle<T> to and from nint and safely read handle targets.
Update InjectedTypeGenerator to use the helpers for generated property getters and setters, centralizing handle conversion and disposal logic.
Update TypeInjector and RuntimeInvoke to use `EmitCalli` with `CallingConvention.Winapi` for unmanaged calls, and adjust `RuntimeInvoke` imports accordingly.
Add a Rank property to Il2CppArrayBase and implement it across Il2CppArrayRank1<T> through Il2CppArrayRank5<T>. Update rank-specific Unbox(object?) methods to accept any Il2CppArrayBase with a matching rank, rewrap the native pointer into the correct concrete array type, and preserve exact-type unboxing and invalid-cast behavior.
The integration guide still described the 1.x GeneratorOptions API, and its runtime section ended before the example.

It now shows Il2CppGame.Process with the default processing layers and both ways to pass Unity's libraries for unstripping, how to create and start Il2CppInteropRuntime with an IDetourProvider (which has to keep its detours alive, since the runtime discards the returned handle), and how to install Il2CppInteropDetourFactory for HarmonyX: in MonoMod's global detour context, not as DetourFactory.Current, which Il2CppInteropDetour uses for its own detours.
* Translate ldsflda in unstripped method bodies

Taking the address of a static field was not implemented, so every unstripped method that does was left without a body. Most of them are the accessors of static events, which the compiler implements with Interlocked.CompareExchange on the event's field: 171 of the 221 bodies that failed to translate on Unity 6000.5, such as the events of Unity.Profiling.Memory.MemoryProfiler.

Static fields now get an address accessor, like instance fields, which returns the address of the field in the static field data of its class, after running the class's static constructor. It uses il2cpp_class_get_static_field_data, which Il2Cpp exports from Unity 2021, so the accessors are only generated for games that export it. Thread static fields, which have no address there, throw NotSupportedException.

* Document field address accessors

The field design notes named the address accessors UnsafeField_, which the generator no longer uses. They now show GetFieldAddress_ and the new GetStaticFieldAddress_, and explain when static field addresses are available.

* Name static field address accessors like instance ones

Static field address accessors now use the GetFieldAddress_ prefix of the
instance field address accessors. A name that is already taken still gets
a suffix, as for any generated member.

* Generate static field address accessors for primitive types

Only the instance fields of primitive types are left without an address
accessor: their type is the .NET primitive, which ByReference<T> cannot
hold. Static fields of primitive types, such as IntPtr.Zero and
Boolean.TrueString, have Il2Cpp types like any other static field.
Unity 6000.6 writes metadata v108, which only Cpp2IL 1.0.9 reads. Against 1.0.9, the generator built for 1.0.8 fails with MissingMethodException: Il2CppInterfaceOffset.Type now returns an Il2CppType, and Il2CppTypeReflectionDataToContext.ToContext became Il2CppTypeToContext.ResolveIl2CppType.

FieldAnalysisContext.ConstantValue now uses OverrideConstantValue only when UseOverrideConstantValue is set. Unstripped constants lost their values without it, and FieldAccessorProcessingLayer then failed with a NullReferenceException in GetInstantiatedStorageField. The flag is now set with the value.
Unbox of the array types threw an InvalidCastException for null, since the commit that let them unbox an array of the same rank with another element type, while the default implementation returns null for null. Reading a null array through interop therefore threw: a field of an object or an element of an array whose value is null, such as Dictionary's _buckets before its first entry, which the restored code of Dictionary<TKey, TValue>.TryInsert reads to decide whether to initialize the dictionary.
Il2CppObjectPool wraps an Il2Cpp object in the interop type of its
class, which Il2CppSystemTypeExtensions finds among the assemblies that
are already loaded. An interop assembly that no code has used yet is not
loaded, so wrapping an object of one of its types threw a
NullReferenceException ("Could not find system type for top-level
type"). A mod that enumerated the components of a scene, for example,
failed on the first component of a game type when it did not refer to
the game's own assembly itself.

If no loaded assembly has either of the names, the assembly is now
loaded by name, which the loader resolves as it resolves any other
reference to an interop assembly.
* Pass IL2CPP primitives as .NET primitives in native calls

TrampolineBuilder.GetNativeType represented every value type as a fixed-size struct of its IL2CPP size, including Il2CppSystem.Single and Il2CppSystem.Double. Native code passes and returns float and double in floating-point registers, but a struct of the same size in an integer register (Windows x64), so internal calls and injected methods with such parameters or return values received or returned garbage. For example, the unstripped Time.timeAsDouble never advanced.

Il2CppSystem's primitive types are now represented by the corresponding .NET primitives, except Char, which is represented by ushort because the marshaller converts a char to a one-byte ANSI character by default.

The stub Il2Cppmscorlib now declares the primitive types that it lacked, so that TrampolineBuilder finds them by type, as it finds Il2CppSystem.Boolean.

* Look up Il2CppSystem.Boolean in the table of primitive types

As suggested in review, GetNativeType finds the native type of Il2CppSystem.Boolean, byte, in _primitiveTypes, as it finds those of the other primitive types, rather than in a branch of its own.
* Avoid Type members that may be stripped in ToSystemType

Unity's managed code stripping can remove Type.IsSZArray and Type.IsConstructedGenericType from a game's corlib. ToSystemType and IsTypeDefinition called them, so when they were stripped, neither could be compiled ("Method not found: 'Il2CppSystem.Boolean Il2CppSystem.Type.get_IsSZArray()'"). The object pool converts the type of each class it meets for the first time, and delegate conversion converts the parameter types, so both failed.

ToSystemType now asks Il2Cpp for the type's kind (il2cpp_type_get_type), and IsTypeDefinition uses IsGenericType and IsGenericTypeDefinition, which ToSystemType already relies on.

GetSystemTypeDefinition also takes the image name from Il2Cpp instead of Type.Assembly. Type.Assembly creates an assembly object whose class may not be initialized yet. Wrapping that object made the object pool convert its type, which read Type.Assembly again, until the stack overflowed.

* Remove comment in Il2CppSystemTypeExtensions explaining stripping issues

Co-authored-by: Jeremy Pritts <49847914+ds5678@users.noreply.github.com>

* Check for constructed generic types in a method

IsTypeDefinition checks for a constructed generic type without
Type.IsConstructedGenericType, which may be stripped from a game's
corlib. The check is now a method, as the one for single-dimensional
arrays is.

---------

Co-authored-by: Jeremy Pritts <49847914+ds5678@users.noreply.github.com>
Before an injected class is set up, the classes of its field, property,
parameter and return types are registered. A ref or out parameter is a
ByReference<T>, which has no class, so its class pointer stayed zero:
the injector logged that the static constructor of ByReference<T> was
malformed and then failed to register it as a class, which made the
registration of the injected class throw.

The class of the type that a ByReference<T> refers to is registered
instead, which is what the parameter's Il2Cpp type is built from.
Unstripping restores many classes whose methods have ref or out
parameters, such as UnityEngine.LowLevel.PlayerLoop.
Il2Cpp checks a cast to a class with the type hierarchy of the other class: Class::IsAssignableFrom ends with HasParentUnsafe, which reads typeHierarchy without setting it up. Il2Cpp sets up the type hierarchy in Class::Init, which injected classes skip because they are registered as initialized, so the type hierarchy of an injected class stayed null. il2cpp_class_is_assignable_from crashed for a class derived from another injected class (seen on Unity 6000.5).

RegisterTypeInIl2Cpp now sets the type hierarchy from that of the base class, as Il2Cpp's SetupTypeHierarchy does.
* Let the garbage collector find the references of injected objects

Il2Cpp allocates the objects of a class whose has_references flag is clear as pointer-free memory (Object::NewAllocSpecific), which the garbage collector never scans. TypeInjector did not set the flag, so an Il2Cpp object referenced only by a field of an injected object could be collected while the injected object still pointed to it. In a test on Unity 6000.5, a string stored in a field of an injected object was collected, and the next string of the same size took its memory.

SetFields now sets has_references when the base class has references, or when a field has a reference type or a value type with references, as Il2Cpp's SetupFields does.

The fields were also laid out one after another without padding, so a reference after an Int32 field ended up at an unaligned offset, where the collector does not look for references. Each field is now aligned: a reference to the size of a pointer, and a value type to its alignment. Injected value types have no alignment of their own, so they are aligned to the size of a pointer. The size of the class is then rounded up to the largest alignment of its fields, as Il2Cpp's FieldLayout::LayoutFields does, so that the references in each element of an array of an injected value type are aligned as well.

* Set the alignment of injected classes

As suggested in review, SetFields stores the alignment it computes in the class's minimumAlignment, which il2cpp_class_value_size returns and Il2Cpp's own field layout reads for a field of a value type. Before, an injected class reported an alignment of 0, so a field of an injected struct was aligned like a pointer. Injected enums get the alignment of their underlying type, and GenericTypeInflater copies it.

MinimumAlignment is added to INativeClassStruct, to the struct generator and to each class handler, as the generator writes it; every supported Il2CppClass version has minimumAlignment.

Layouts now follow Il2Cpp's: for example, a field of an injected struct that holds two Int32 is aligned to 4 bytes, not 8.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment