diff --git a/.gitignore b/.gitignore index ed478515..3e1853d2 100644 --- a/.gitignore +++ b/.gitignore @@ -223,6 +223,9 @@ _pkginfo.txt # but keep track of directories ending in .cache !?*.[Cc]ache/ +# Helix Editor local configuration files +/.helix/ + # Others ClientBin/ ~$* diff --git a/Documentation/Class-Injection.md b/Documentation/Class-Injection.md index c9d6dfc7..c625e741 100644 --- a/Documentation/Class-Injection.md +++ b/Documentation/Class-Injection.md @@ -55,13 +55,13 @@ public class MyClass : SomeIL2CPPClass { // Used by IL2CPP when creating new instances of this class public MyClass(IntPtr ptr) : base(ptr) { } - + // Used by managed code when creating new instances of this class public MyClass() : base(ClassInjector.DerivedConstructorPointer()) { ClassInjector.DerivedConstructorBody(this); } - + // Any other methods } @@ -79,25 +79,81 @@ var someInstance = new MyClass(pointer); * `[HideFromIl2Cpp]` can be used to prevent a method from being exposed to il2cpp -## Caveats - -* Injected class instances are handled by IL2CPP garbage collection. This means that an object may be collected even if - it's referenced from managed domain. Attempting to use that object afterwards will result - in `ObjectCollectedException`. Conversely, managed representation of injected object will not be garbage collected as - long as it's referenced from IL2CPP domain. -* It might be possible to create a cross-domain reference loop that will prevent objects from being garbage collected. - Avoid doing anything that will result in injected class instances (indirectly) storing references to itself. The - simplest example of how to leak memory is this: +## Garbage Collection + +* Managed instances of injected classes hold strong handles to their unmanaged counterparts. + This means it is unlikely for an unmanaged object to be collected while it is referenced from the managed domain. + If this rare case does occur, an attempt to use to injected instance will throw an `ObjectCollectedException`. +* If there are no extant references to the managed instance, it will be garbage collected, + releasing the strong handle it holds and allowing the underlying unmanaged object to eventually be collected in turn. + When this occurs, however, the finalizer for the managed instance will not be run, + delaying execution until the unmanaged object is also ready to be garbage collected. +* The unmanaged-to-managed mapping is a one-to-many relationship. + While during typical execution there will be exactly zero, or exactly one extant managed object, + in certain situations, such as when the object pool is disabled, there may be any number.# +* Finalizers are supported for injected classes, but note that the managed injector (`~MyClass`) is not invoked predictably. + If you want to implement a custom finalizer, define a `private void Il2CppFinalize()` method on your class (see below). +* Due to the implementation of finalizers, the "resurrection pattern" does not translate directly to injected types. + If you do not call `ClassInjector.ResurrectObject` during an object's finalizer, + the unmanaged object will be garbage collected and future use will throw `ObjectCollectedException`s. + **NB:** You will still also need to call `Il2CppSystem.GC.ReRegisterForFinalize` if applicable. + +
+A detailed example of finalizer behavior for injected classes +
+ +Consider the following example: ```c# -class Injected: Il2CppSystem.Object { - Il2CppSystem.Collections.Generic.List list = new ...; - public Injected() { - list.Add(this); // reference to itself through an IL2CPP list. This will prevent both this and list from being garbage collected, ever. +class Foo : Il2CppSystem.Object +{ + public Foo(IntPtr ptr) : base(ptr) { } + public Foo() : this(ClassInjector.DerivedConstructorPointer()) + { + ClassInjector.DerivedConstructorBody(this); + } + + private void Il2CppFinalize() + { + // ... } } + +ClassInjector.RegisterTypeInIl2Cpp(); + +Foo foobar = new(); +// ... function ends ... ``` +In the above example, the events occur in this order: + +1. `Foo` is injected into the il2cpp domain: + * The class injector walks the superclasses of `Foo` and collects all the `Il2CppFinalize` methods. + * These finalizers are organised in a chain from subclass to superclass. + * The injected class is created with an unmanaged finalizer which we can hook into later. +2. The parameterless `Foo` constructor is called: + * An unmanaged instance of the injected class is created. + * A strong handle to the unmanaged instance is put into the managed instance of `Foo`. +3. The managed instance of `Foo` goes out of scope, allowing this instance to be garbage collected. +4. Some time later, the managed GC will run its finalizer: + * Eventually, the GC reaches the finalizer of `Il2CppObjectBase` + where the strong handle to the unmanaged instance is released. + * Assuming no other managed or unmanaged references to the unmanaged object exist, + the unmanaged instance of `Foo` can now be garbage collected. + * Here, the managed instance of `Foo` is destroyed by the garbage collector, + but the unmanaged instance still exists temporarily. +5. When the unmanaged GC acknowleges this, the unmanaged object's finalizer is called, firing the hook we installed: + * A fresh managed instance of `Foo` is created. + * This fresh instance is "downgraded" and its strong handle becomes a weak handle, + preventing this instance from blocking the garbage collection of the unmanaged instance. + * We follow the links in the finalizer chain, running the user-specified finalization method (finally!). +6. The unmanaged object, which has no extant references, is garbage collected with its managed finalizer having run exactly once. + +Note that this complexity is present only for injected classes with finalizers. +Injected classes without an `Il2CppFinalize` method are collected without running any hooks. + +
+ ## Fields injection > TODO: Describe how field injection works based on [#24](https://github.com/BepInEx/Il2CppAssemblyUnhollower/pull/24) @@ -107,4 +163,3 @@ class Injected: Il2CppSystem.Object { * Not all members are exposed to Il2Cpp side - no properties, events or static methods will be visible to Il2Cpp reflection. Fields are exported, but the feature is fairly limited. * Only a limited set of types is supported for method signatures - \ No newline at end of file diff --git a/Il2CppInterop.Runtime/DelegateSupport.cs b/Il2CppInterop.Runtime/DelegateSupport.cs index 88a6528b..8bce992b 100644 --- a/Il2CppInterop.Runtime/DelegateSupport.cs +++ b/Il2CppInterop.Runtime/DelegateSupport.cs @@ -5,11 +5,10 @@ using System.Reflection.Emit; using System.Runtime.InteropServices; using System.Text; -using Il2CppInterop.Common; using Il2CppInterop.Runtime.Injection; using Il2CppInterop.Runtime.InteropTypes; +using Il2CppInterop.Runtime.InteropTypes.Fields; using Il2CppInterop.Runtime.Runtime; -using Microsoft.Extensions.Logging; using Object = Il2CppSystem.Object; using ValueType = Il2CppSystem.ValueType; @@ -133,10 +132,14 @@ private static Delegate GenerateNativeToManagedTrampoline(Il2CppSystem.Reflectio bodyBuilder.Emit(OpCodes.Ldarg_0); bodyBuilder.Emit(OpCodes.Call, - typeof(ClassInjectorBase).GetMethod(nameof(ClassInjectorBase.GetMonoObjectFromIl2CppPointer))!); - bodyBuilder.Emit(OpCodes.Castclass, typeof(Il2CppToMonoDelegateReference)); + typeof(Il2CppObjectPool).GetMethod(nameof(Il2CppObjectPool.Get))! + .MakeGenericMethod(typeof(Il2CppToMonoDelegateReference))); bodyBuilder.Emit(OpCodes.Ldfld, - typeof(Il2CppToMonoDelegateReference).GetField(nameof(Il2CppToMonoDelegateReference.ReferencedDelegate))); + typeof(Il2CppToMonoDelegateReference).GetField(nameof(Il2CppToMonoDelegateReference.MethodInfo))); + bodyBuilder.Emit(OpCodes.Call, + typeof(Il2CppValueField).GetMethod(nameof(Il2CppValueField.Get))); + bodyBuilder.Emit(OpCodes.Call, + typeof(DelegateSupport).GetMethod(nameof(DelegateSupport.GetStoredDelegate), BindingFlags.NonPublic | BindingFlags.Static)); for (var i = 0; i < managedParameters.Length; i++) { @@ -190,15 +193,10 @@ private static Delegate GenerateNativeToManagedTrampoline(Il2CppSystem.Reflectio bodyBuilder.Emit(OpCodes.Stloc, returnLocal); } - var exceptionLocal = bodyBuilder.DeclareLocal(typeof(Exception)); bodyBuilder.BeginCatchBlock(typeof(Exception)); - bodyBuilder.Emit(OpCodes.Stloc, exceptionLocal); - bodyBuilder.Emit(OpCodes.Ldstr, "Exception in IL2CPP-to-Managed trampoline, not passing it to il2cpp: "); - bodyBuilder.Emit(OpCodes.Ldloc, exceptionLocal); - bodyBuilder.Emit(OpCodes.Callvirt, typeof(object).GetMethod(nameof(ToString))!); - bodyBuilder.Emit(OpCodes.Call, - typeof(string).GetMethod(nameof(string.Concat), new[] { typeof(string), typeof(string) })!); - bodyBuilder.Emit(OpCodes.Call, typeof(DelegateSupport).GetMethod(nameof(LogError), BindingFlags.Static | BindingFlags.NonPublic)!); + bodyBuilder.Emit(OpCodes.Ldstr, "IL2CPP-to-Managed delegate trampoline"); + bodyBuilder.Emit(OpCodes.Call, typeof(ClassInjector).GetMethod(nameof(ClassInjector.LogException), + BindingFlags.Static | BindingFlags.NonPublic)!); bodyBuilder.EndExceptionBlock(); @@ -209,11 +207,6 @@ private static Delegate GenerateNativeToManagedTrampoline(Il2CppSystem.Reflectio return trampoline.CreateDelegate(GetOrCreateDelegateType(signature, managedMethod)); } - private static void LogError(string message) - { - Logger.Instance.LogError("{Message}", message); - } - public static TIl2Cpp? ConvertDelegate(Delegate @delegate) where TIl2Cpp : Il2CppObjectBase { if (@delegate == null) @@ -289,6 +282,8 @@ private static void LogError(string message) methodInfo.IsMarshalledFromNative = true; var delegateReference = new Il2CppToMonoDelegateReference(@delegate, methodInfo.Pointer); + // Leak the object so we never have to do this again. + GCHandle.Alloc(delegateReference); Il2CppSystem.Delegate converted; if (UnityVersionHandler.MustUseDelegateConstructor) @@ -317,6 +312,9 @@ private static void LogError(string message) return converted.Cast(); } + private static readonly ConcurrentDictionary s_storedDelegates = new(); + private static Delegate GetStoredDelegate(IntPtr methodInfoPtr) => s_storedDelegates[methodInfoPtr]; + internal class MethodSignature : IEquatable { public readonly bool ConstructedFromNative; @@ -390,8 +388,7 @@ public override bool Equals(object obj) private class Il2CppToMonoDelegateReference : Object { - public IntPtr MethodInfo; - public Delegate ReferencedDelegate; + public Il2CppValueField MethodInfo; public Il2CppToMonoDelegateReference(IntPtr obj0) : base(obj0) { @@ -402,15 +399,15 @@ public Il2CppToMonoDelegateReference(Delegate referencedDelegate, IntPtr methodI { ClassInjector.DerivedConstructorBody(this); - ReferencedDelegate = referencedDelegate; - MethodInfo = methodInfo; + MethodInfo!.Set(methodInfo); + s_storedDelegates[methodInfo] = referencedDelegate; } - ~Il2CppToMonoDelegateReference() + private void Il2CppFinalize() { Marshal.FreeHGlobal(MethodInfo); - MethodInfo = IntPtr.Zero; - ReferencedDelegate = null; + MethodInfo.Set(IntPtr.Zero); + s_storedDelegates.TryRemove(MethodInfo, out _); } } } diff --git a/Il2CppInterop.Runtime/Injection/ClassInjector.cs b/Il2CppInterop.Runtime/Injection/ClassInjector.cs index 6db08110..7955324d 100644 --- a/Il2CppInterop.Runtime/Injection/ClassInjector.cs +++ b/Il2CppInterop.Runtime/Injection/ClassInjector.cs @@ -5,7 +5,6 @@ using System.Reflection; using System.Reflection.Emit; using System.Runtime.InteropServices; -using System.Runtime.Serialization; using System.Text; using Il2CppInterop.Common; using Il2CppInterop.Runtime.Attributes; @@ -64,6 +63,17 @@ public class RegisterTypeOptions public Il2CppInterfaceCollection? Interfaces { get; init; } = null; } +internal record FinalizerChain(Delegate finalizer, FinalizerChain? next) +{ + public FinalizerChain? next = next; + + public void Execute(T obj) where T : Il2CppObjectBase + { + ((Action)finalizer)(obj); + next?.Execute(obj); + } +} + public static unsafe partial class ClassInjector { /// type.FullName @@ -74,19 +84,12 @@ public static unsafe partial class ClassInjector InflatedMethodFromContextDictionary = new(); private static readonly ConcurrentDictionary InvokerCache = new(); + private static readonly ConcurrentDictionary DerivedConstructorBodyCache = new(); + internal static readonly ConcurrentDictionary FinalizerChainCache = new(); private static readonly ConcurrentDictionary<(Type type, FieldAttributes attrs), IntPtr> _injectedFieldTypes = new(); - private static readonly VoidCtorDelegate FinalizeDelegate = Finalize; - - public static void ProcessNewObject(Il2CppObjectBase obj) - { - var pointer = obj.Pointer; - var handle = GCHandle.Alloc(obj, GCHandleType.Normal); - AssignGcHandle(pointer, handle); - } - public static IntPtr DerivedConstructorPointer() { return IL2CPP.il2cpp_object_new(Il2CppClassPointerStore @@ -95,30 +98,32 @@ public static IntPtr DerivedConstructorPointer() public static void DerivedConstructorBody(Il2CppObjectBase objectBase) { - if (objectBase.isWrapped) - return; - var fields = objectBase.GetType() - .GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly) + var ctor = DerivedConstructorBodyCache.GetOrAdd(objectBase.GetType(), CreateDerivedConstructorBodyDelegate); + ctor(objectBase); + } + + private static InjectedInitializationDelegate CreateDerivedConstructorBodyDelegate(Type type) + { + var method = new DynamicMethod("DerivedConstructorBodyDelegate", MethodAttributes.Public | MethodAttributes.Static, + CallingConventions.Standard, typeof(void), new[] { typeof(Il2CppObjectBase) }, type, true); + + var fields = type + .GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) .Where(IsFieldEligible) .ToArray(); - foreach (var field in fields) - field.SetValue(objectBase, field.FieldType.GetConstructor( - BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, - new[] { typeof(Il2CppObjectBase), typeof(string) }, Array.Empty()) - .Invoke(new object[] { objectBase, field.Name }) - ); - var ownGcHandle = GCHandle.Alloc(objectBase, GCHandleType.Normal); - AssignGcHandle(objectBase.Pointer, ownGcHandle); + var il = method.GetILGenerator(); + il.Emit(OpCodes.Ldarg_0); + Il2CppObjectInitializer.EmitInjectedInitialization(il, type, fields); + il.Emit(OpCodes.Ret); + + return method.CreateDelegate(); } - public static void AssignGcHandle(IntPtr pointer, GCHandle gcHandle) + public static void ResurrectObject(Il2CppObjectBase obj) { - var handleAsPointer = GCHandle.ToIntPtr(gcHandle); - if (pointer == IntPtr.Zero) throw new NullReferenceException(nameof(pointer)); - ClassInjectorBase.GetInjectedData(pointer)->managedGcHandle = GCHandle.ToIntPtr(gcHandle); + Il2CppObjectBase.Upgrade(obj); } - public static bool IsTypeRegisteredInIl2Cpp() where T : class { return IsTypeRegisteredInIl2Cpp(typeof(T)); @@ -220,6 +225,31 @@ public static void RegisterTypeInIl2Cpp(Type type, RegisterTypeOptions options) $"Type with FullName {type.FullName} is already injected. Don't inject the same type twice, or use a different namespace"); } + static void InternFinalizerChain(Type? type) + { + FinalizerChain? last = null; + while (type != null) + { + if (FinalizerChainCache.TryGetValue(type, out FinalizerChain? next)) + { + if (last != null) last.next = next; + break; + } + + if (type.GetMethod("Il2CppFinalize", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly, new Type[0]) is MethodInfo m) + { + next = new(m.CreateDelegate(typeof(Action<>).MakeGenericType(type)), last); + if (last != null) last.next = next; + FinalizerChainCache[type] = next; + last = next; + } + + type = type.BaseType; + } + } + + InternFinalizerChain(type); + var interfaceFunctionCount = interfaces.Sum(i => i.MethodCount); var classPointer = UnityVersionHandler.NewClass(baseClassPointer.VtableCount + interfaceFunctionCount); @@ -311,14 +341,10 @@ public static void RegisterTypeInIl2Cpp(Type type, RegisterTypeOptions options) var methodPointerArray = (Il2CppMethodInfo**)Marshal.AllocHGlobal(methodCount * IntPtr.Size); classPointer.Methods = methodPointerArray; - methodPointerArray[0] = ConvertStaticMethod(FinalizeDelegate, "Finalize", classPointer); + methodPointerArray[0] = ConvertStaticMethod(CreateFinalizeMethod(type), "Finalize", classPointer); var finalizeMethod = UnityVersionHandler.Wrap(methodPointerArray[0]); - var fieldsToInitialize = type - .GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) - .Where(IsFieldEligible) - .ToArray(); - if (!type.IsAbstract) methodPointerArray[1] = ConvertStaticMethod(CreateEmptyCtor(type, fieldsToInitialize), ".ctor", classPointer); + if (!type.IsAbstract) methodPointerArray[1] = ConvertStaticMethod(CreateEmptyCtor(type), ".ctor", classPointer); var infos = new Dictionary<(string, int, bool), int>(eligibleMethods.Length); for (var i = 0; i < eligibleMethods.Length; i++) { @@ -537,7 +563,7 @@ private static bool IsTypeSupported(Type type) return typeof(Il2CppObjectBase).IsAssignableFrom(type); } - private static bool IsFieldEligible(FieldInfo field) + internal static bool IsFieldEligible(FieldInfo field) { if (!field.FieldType.IsGenericType) return field.FieldType == typeof(Il2CppStringField); var genericTypeDef = field.FieldType.GetGenericTypeDefinition(); @@ -734,71 +760,62 @@ private static bool IsMethodEligible(MethodInfo method) return converted.MethodInfoPointer; } - private static VoidCtorDelegate CreateEmptyCtor(Type targetType, FieldInfo[] fieldsToInitialize) + private static void DefaultFinalize(IntPtr ptr) { } + + internal static bool IsTypeManuallyFinalizable(Type targetType) { - var method = new DynamicMethod("FromIl2CppCtorDelegate", MethodAttributes.Public | MethodAttributes.Static, + return FinalizerChainCache.ContainsKey(targetType); + } + + private static void RunFinalizer(IntPtr ptr) where T : Il2CppObjectBase + { + T ephemeral = Il2CppObjectInitializer.NewWeak(ptr); + ClassInjector.FinalizerChainCache[typeof(T)]!.Execute(ephemeral); + } + + private static VoidCtorDelegate CreateFinalizeMethod(Type targetType) + { + if (!IsTypeManuallyFinalizable(targetType)) return DefaultFinalize; + + var method = new DynamicMethod("FinalizeIl2CppObject", MethodAttributes.Public | MethodAttributes.Static, CallingConventions.Standard, typeof(void), new[] { typeof(IntPtr) }, targetType, true); var body = method.GetILGenerator(); + Label tryBlock = body.BeginExceptionBlock(); - var monoCtor = targetType.GetConstructor(new[] { typeof(IntPtr) }); - if (monoCtor != null) - { - body.Emit(OpCodes.Ldarg_0); - body.Emit(OpCodes.Newobj, monoCtor); - } - else - { - var local = body.DeclareLocal(targetType); - body.Emit(OpCodes.Ldtoken, targetType); - body.Emit(OpCodes.Call, - typeof(Type).GetMethod(nameof(Type.GetTypeFromHandle), BindingFlags.Public | BindingFlags.Static)!); - body.Emit(OpCodes.Call, - typeof(FormatterServices).GetMethod(nameof(FormatterServices.GetUninitializedObject), - BindingFlags.Public | BindingFlags.Static)!); - body.Emit(OpCodes.Stloc, local); - body.Emit(OpCodes.Ldloc, local); - body.Emit(OpCodes.Ldarg_0); - body.Emit(OpCodes.Call, - typeof(Il2CppObjectBase).GetMethod(nameof(Il2CppObjectBase.CreateGCHandle), - BindingFlags.NonPublic | BindingFlags.Instance)!); - body.Emit(OpCodes.Ldloc, local); - body.Emit(OpCodes.Ldc_I4_1); - body.Emit(OpCodes.Stfld, - typeof(Il2CppObjectBase).GetField(nameof(Il2CppObjectBase.isWrapped), - BindingFlags.NonPublic | BindingFlags.Instance)!); - body.Emit(OpCodes.Ldloc, local); - body.Emit(OpCodes.Call, - targetType.GetConstructor(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, - Type.EmptyTypes, Array.Empty())!); - body.Emit(OpCodes.Ldloc, local); - } - - foreach (var field in fieldsToInitialize) - { - body.Emit(OpCodes.Dup); - body.Emit(OpCodes.Dup); - body.Emit(OpCodes.Ldstr, field.Name); - body.Emit(OpCodes.Newobj, field.FieldType.GetConstructor( - BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, - new[] { typeof(Il2CppObjectBase), typeof(string) }, Array.Empty()) - ); - body.Emit(OpCodes.Stfld, field); - } + // Finalize from within the object pool: + body.Emit(OpCodes.Ldarg_0); + body.Emit(OpCodes.Call, + typeof(ClassInjector).GetMethod(nameof(ClassInjector.RunFinalizer), + BindingFlags.NonPublic | BindingFlags.Static)!.MakeGenericMethod(targetType)); - body.Emit(OpCodes.Call, typeof(ClassInjector).GetMethod(nameof(ProcessNewObject))!); + // Catch any exceptions: + body.BeginCatchBlock(typeof(Exception)); + body.Emit(OpCodes.Ldstr, "Injected object finalizer"); + body.Emit(OpCodes.Call, typeof(ClassInjector).GetMethod(nameof(LogException), BindingFlags.Static | BindingFlags.NonPublic)!); + body.EndExceptionBlock(); body.Emit(OpCodes.Ret); - var @delegate = (VoidCtorDelegate)method.CreateDelegate(typeof(VoidCtorDelegate)); + var @delegate = method.CreateDelegate(); GCHandle.Alloc(@delegate); // pin it forever return @delegate; } - public static void Finalize(IntPtr ptr) + private static VoidCtorDelegate CreateEmptyCtor(Type targetType) { - var gcHandle = ClassInjectorBase.GetGcHandlePtrFromIl2CppObject(ptr); - GCHandle.FromIntPtr(gcHandle).Free(); + var method = new DynamicMethod("FromIl2CppCtorDelegate", MethodAttributes.Public | MethodAttributes.Static, + CallingConventions.Standard, typeof(void), new[] { typeof(IntPtr) }, targetType, true); + + var body = method.GetILGenerator(); + body.Emit(OpCodes.Ldarg_0); + body.Emit(OpCodes.Call, typeof(Il2CppObjectInitializer).GetMethod(nameof(Il2CppObjectInitializer.New), BindingFlags.NonPublic | BindingFlags.Static)!.MakeGenericMethod(targetType)); + body.Emit(OpCodes.Pop); + body.Emit(OpCodes.Ret); + + var @delegate = method.CreateDelegate(); + GCHandle.Alloc(@delegate); // pin it forever + return @delegate; } private static Delegate GetOrCreateInvoker(MethodInfo monoMethod) @@ -937,8 +954,7 @@ private static Delegate CreateTrampoline(MethodInfo monoMethod) body.Emit(OpCodes.Ldarg_0); body.Emit(OpCodes.Call, - typeof(ClassInjectorBase).GetMethod(nameof(ClassInjectorBase.GetMonoObjectFromIl2CppPointer))!); - body.Emit(OpCodes.Castclass, monoMethod.DeclaringType); + typeof(Il2CppObjectPool).GetMethod(nameof(Il2CppObjectPool.Get))!.MakeGenericMethod(monoMethod.DeclaringType)); var indirectVariables = new LocalBuilder[managedParameters.Length]; @@ -1030,16 +1046,9 @@ void HandleTypeConversion(Type type) } // body.Emit(OpCodes.Ret); // breaks coreclr - var exceptionLocal = body.DeclareLocal(typeof(Exception)); body.BeginCatchBlock(typeof(Exception)); - body.Emit(OpCodes.Stloc, exceptionLocal); - body.Emit(OpCodes.Ldstr, "Exception in IL2CPP-to-Managed trampoline, not passing it to il2cpp: "); - body.Emit(OpCodes.Ldloc, exceptionLocal); - body.Emit(OpCodes.Callvirt, typeof(object).GetMethod(nameof(ToString))!); - body.Emit(OpCodes.Call, - typeof(string).GetMethod(nameof(string.Concat), new[] { typeof(string), typeof(string) })!); - body.Emit(OpCodes.Call, typeof(ClassInjector).GetMethod(nameof(LogError), BindingFlags.Static | BindingFlags.NonPublic)!); - + body.Emit(OpCodes.Ldstr, "IL2CPP-to-Managed trampoline"); + body.Emit(OpCodes.Call, typeof(ClassInjector).GetMethod(nameof(LogException), BindingFlags.Static | BindingFlags.NonPublic)!); body.EndExceptionBlock(); if (managedReturnVariable != null) @@ -1058,9 +1067,9 @@ void HandleTypeConversion(Type type) return @delegate; } - private static void LogError(string message) + internal static void LogException(Exception ex, string location) { - Logger.Instance.LogError("{Message}", message); + Logger.Instance.LogError($"Exception in {location}, not passing it to il2cpp: {ex}"); } private static string ExtractSignature(MethodInfo monoMethod) @@ -1168,6 +1177,8 @@ internal static Type SystemTypeFromIl2CppType(Il2CppTypeStruct* typePointer) return RewriteType(type); } + private delegate void InjectedInitializationDelegate(Il2CppObjectBase instance); + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] private delegate void InvokerDelegateMetadataV29(IntPtr methodPointer, Il2CppMethodInfo* methodInfo, IntPtr obj, IntPtr* args, IntPtr* returnValue); diff --git a/Il2CppInterop.Runtime/InteropTypes/Il2CppObjectBase.cs b/Il2CppInterop.Runtime/InteropTypes/Il2CppObjectBase.cs index 08e09523..e01c5149 100644 --- a/Il2CppInterop.Runtime/InteropTypes/Il2CppObjectBase.cs +++ b/Il2CppInterop.Runtime/InteropTypes/Il2CppObjectBase.cs @@ -1,19 +1,16 @@ using System; -using System.Reflection; -using System.Reflection.Emit; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -using System.Runtime.Serialization; using Il2CppInterop.Runtime.Runtime; namespace Il2CppInterop.Runtime.InteropTypes; public class Il2CppObjectBase { - private static readonly MethodInfo _unboxMethod = typeof(Il2CppObjectBase).GetMethod(nameof(Unbox)); internal bool isWrapped; internal IntPtr pooledPtr; + private bool wasDestroyed; private nint myGcHandle; public Il2CppObjectBase(IntPtr pointer) @@ -21,6 +18,11 @@ public Il2CppObjectBase(IntPtr pointer) CreateGCHandle(pointer); } + ~Il2CppObjectBase() + { + Il2CppObjectPool.Free(myGcHandle, pooledPtr); + } + public IntPtr ObjectClass => IL2CPP.il2cpp_object_get_class(Pointer); public IntPtr Pointer @@ -54,6 +56,24 @@ internal void CreateGCHandle(IntPtr objHdl) return; myGcHandle = IL2CPP.il2cpp_gchandle_new(objHdl, false); + + isWrapped = true; + } + + internal static void Downgrade(Il2CppObjectBase obj) + { + nint strong = obj.myGcHandle; + obj.myGcHandle = IL2CPP.il2cpp_gchandle_new_weakref(obj.Pointer, false); + IL2CPP.il2cpp_gchandle_free(strong); + } + + internal static void Upgrade(Il2CppObjectBase obj) + { + nint weak = obj.myGcHandle; + obj.myGcHandle = IL2CPP.il2cpp_gchandle_new(obj.Pointer, false); + IL2CPP.il2cpp_gchandle_free(weak); + Il2CppObjectPool.Intern(obj); + } public T Cast() where T : Il2CppObjectBase @@ -81,71 +101,6 @@ public T Unbox() where T : unmanaged return UnboxUnsafe(Pointer); } - private static readonly Type[] _intPtrTypeArray = { typeof(IntPtr) }; - private static readonly MethodInfo _getUninitializedObject = typeof(RuntimeHelpers).GetMethod(nameof(RuntimeHelpers.GetUninitializedObject))!; - private static readonly MethodInfo _getTypeFromHandle = typeof(Type).GetMethod(nameof(Type.GetTypeFromHandle))!; - private static readonly MethodInfo _createGCHandle = typeof(Il2CppObjectBase).GetMethod(nameof(CreateGCHandle), BindingFlags.Instance | BindingFlags.NonPublic)!; - private static readonly FieldInfo _isWrapped = typeof(Il2CppObjectBase).GetField(nameof(isWrapped), BindingFlags.Instance | BindingFlags.NonPublic)!; - - internal static class InitializerStore - { - private static Func? _initializer; - - private static Func Create() - { - var type = Il2CppClassPointerStore.CreatedTypeRedirect ?? typeof(T); - - var dynamicMethod = new DynamicMethod($"Initializer<{typeof(T).AssemblyQualifiedName}>", type, _intPtrTypeArray); - dynamicMethod.DefineParameter(0, ParameterAttributes.None, "pointer"); - - var il = dynamicMethod.GetILGenerator(); - - if (type.GetConstructor(new[] { typeof(IntPtr) }) is { } pointerConstructor) - { - // Base case: Il2Cpp constructor => call it directly - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Newobj, pointerConstructor); - } - else - { - // Special case: We have a parameterless constructor - // However, it could be be user-made or implicit - // In that case we set the GCHandle and then call the ctor and let GC destroy any objects created by DerivedConstructorPointer - - // var obj = (T)RuntimeHelpers.GetUninitializedObject(type); - il.Emit(OpCodes.Ldtoken, type); - il.Emit(OpCodes.Call, _getTypeFromHandle); - il.Emit(OpCodes.Call, _getUninitializedObject); - il.Emit(OpCodes.Castclass, type); - - // obj.CreateGCHandle(pointer); - il.Emit(OpCodes.Dup); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Callvirt, _createGCHandle); - - // obj.isWrapped = true; - il.Emit(OpCodes.Dup); - il.Emit(OpCodes.Ldc_I4_1); - il.Emit(OpCodes.Stfld, _isWrapped); - - var parameterlessConstructor = type.GetConstructor(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, Type.EmptyTypes); - if (parameterlessConstructor != null) - { - // obj..ctor(); - il.Emit(OpCodes.Dup); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Callvirt, parameterlessConstructor); - } - } - - il.Emit(OpCodes.Ret); - - return dynamicMethod.CreateDelegate>(); - } - - public static Func Initializer => _initializer ??= Create(); - } - public T? TryCast() where T : Il2CppObjectBase { var nestedTypeClassPointer = Il2CppClassPointerStore.NativeClassPtr; @@ -156,19 +111,6 @@ private static Func Create() if (!IL2CPP.il2cpp_class_is_assignable_from(nestedTypeClassPointer, ownClass)) return null; - if (RuntimeSpecificsStore.IsInjected(ownClass)) - { - if (ClassInjectorBase.GetMonoObjectFromIl2CppPointer(Pointer) is T monoObject) return monoObject; - } - - return InitializerStore.Initializer(Pointer); - } - - ~Il2CppObjectBase() - { - IL2CPP.il2cpp_gchandle_free(myGcHandle); - - if (pooledPtr == IntPtr.Zero) return; - Il2CppObjectPool.Remove(pooledPtr); + return Il2CppObjectInitializer.New(Pointer); } } diff --git a/Il2CppInterop.Runtime/Runtime/Il2CppObjectInitializer.cs b/Il2CppInterop.Runtime/Runtime/Il2CppObjectInitializer.cs new file mode 100644 index 00000000..74dbbaac --- /dev/null +++ b/Il2CppInterop.Runtime/Runtime/Il2CppObjectInitializer.cs @@ -0,0 +1,137 @@ +using System; +using System.Linq; +using System.Reflection; +using System.Reflection.Emit; +using System.Runtime.CompilerServices; +using Il2CppInterop.Runtime.Injection; +using Il2CppInterop.Runtime.InteropTypes; + +namespace Il2CppInterop.Runtime.Runtime; + +internal static class Il2CppObjectInitializer +{ + internal static T New(IntPtr ptr) + { + return InitializerStore.Initialize(ptr); + } + + /// + /// Creates a proxy to a new T without holding a strong handle. + /// + internal static T NewWeak(IntPtr ptr) where T : Il2CppObjectBase + { + return InitializerStore.InitializeWeak(ptr); + } + + private static readonly MethodInfo _getUninitializedObject = typeof(RuntimeHelpers).GetMethod(nameof(RuntimeHelpers.GetUninitializedObject))!; + private static readonly MethodInfo _getTypeFromHandle = typeof(Type).GetMethod(nameof(Type.GetTypeFromHandle))!; + private static readonly MethodInfo _createGCHandle = typeof(Il2CppObjectBase).GetMethod(nameof(Il2CppObjectBase.CreateGCHandle), BindingFlags.Instance | BindingFlags.NonPublic)!; + + private static void EmitCtorCall(ILGenerator il, Type type) + { + if (type.GetConstructor(new[] { typeof(IntPtr) }) is { } pointerConstructor) + { + // Base case: Il2Cpp constructor => call it directly + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Newobj, pointerConstructor); + } + else + { + // Special case: We have a parameterless constructor + // However, it could be be user-made or implicit + // In that case we set the GCHandle and then call the ctor and let GC destroy any objects created by DerivedConstructorPointer + + // var obj = (T)RuntimeHelpers.GetUninitializedObject(type); + il.Emit(OpCodes.Ldtoken, type); + il.Emit(OpCodes.Call, _getTypeFromHandle); + il.Emit(OpCodes.Call, _getUninitializedObject); + il.Emit(OpCodes.Castclass, type); + + // obj.CreateGCHandle(pointer); + il.Emit(OpCodes.Dup); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Call, _createGCHandle); + + var parameterlessConstructor = type.GetConstructor(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, Type.EmptyTypes); + if (parameterlessConstructor != null) + { + // obj..ctor(); + il.Emit(OpCodes.Dup); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Callvirt, parameterlessConstructor); + } + } + } + + + internal static void EmitInjectedInitialization(ILGenerator il, Type type, FieldInfo[] fieldsToInitialize) + { + EmitFieldInitialization(il, type, fieldsToInitialize); + EmitGCHandling(il, type, false); + } + + private static readonly MethodInfo _intern = typeof(Il2CppObjectPool).GetMethod(nameof(Il2CppObjectPool.Intern))!; + private static readonly MethodInfo _downgrade = typeof(Il2CppObjectBase).GetMethod(nameof(Il2CppObjectBase.Downgrade), BindingFlags.NonPublic | BindingFlags.Static)!; + + private static void EmitGCHandling(ILGenerator il, Type type, bool weak) + { + if (!weak) + { + il.Emit(OpCodes.Call, _intern); + } + else + { + il.Emit(OpCodes.Call, _downgrade); + } + } + + private static void EmitFieldInitialization(ILGenerator il, Type type, FieldInfo[] fieldsToInitialize) + { + foreach (var field in fieldsToInitialize) + { + il.Emit(OpCodes.Dup); + il.Emit(OpCodes.Dup); + il.Emit(OpCodes.Ldstr, field.Name); + il.Emit(OpCodes.Newobj, field.FieldType.GetConstructor( + BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, + new[] { typeof(Il2CppObjectBase), typeof(string) }, Array.Empty())! + ); + il.Emit(OpCodes.Stfld, field); + } + } + + private static readonly Type[] _intPtrTypeArray = { typeof(IntPtr) }; + + private static class InitializerStore + { + public delegate T Initializer(IntPtr pointer); + + public static Initializer? initialize; + public static Initializer? initializeWithoutGlue; + public static Initializer Initialize => initialize ??= Create(false); + public static Initializer InitializeWeak => initializeWithoutGlue ??= Create(true); + + private static Initializer Create(bool weak) + { + var type = Il2CppClassPointerStore.CreatedTypeRedirect ?? typeof(T); + + var fieldsToInitialize = type + .GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) + .Where(ClassInjector.IsFieldEligible) + .ToArray(); + + string methodName = weak ? "InitializeWeak" : "Initialize"; + var dynamicMethod = new DynamicMethod($"{methodName}<{typeof(T).AssemblyQualifiedName}>", type, _intPtrTypeArray); + dynamicMethod.DefineParameter(0, ParameterAttributes.None, "pointer"); + + var il = dynamicMethod.GetILGenerator(); + EmitCtorCall(il, type); + EmitFieldInitialization(il, type, fieldsToInitialize); + il.Emit(OpCodes.Dup); + EmitGCHandling(il, type, weak); + il.Emit(OpCodes.Ret); + + return dynamicMethod.CreateDelegate(); + } + } +} diff --git a/Il2CppInterop.Runtime/Runtime/Il2CppObjectPool.cs b/Il2CppInterop.Runtime/Runtime/Il2CppObjectPool.cs index bd70a8ad..276d8834 100644 --- a/Il2CppInterop.Runtime/Runtime/Il2CppObjectPool.cs +++ b/Il2CppInterop.Runtime/Runtime/Il2CppObjectPool.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Concurrent; -using System.Runtime.CompilerServices; using Il2CppInterop.Runtime.InteropTypes; using Object = Il2CppSystem.Object; @@ -17,27 +16,43 @@ internal static void Remove(IntPtr ptr) s_cache.TryRemove(ptr, out _); } - public static T Get(IntPtr ptr) + internal static void Free(nint unmanagedGcHandle, IntPtr ptr) + { + if (IL2CPP.il2cpp_gchandle_get_target(unmanagedGcHandle) != IntPtr.Zero) + IL2CPP.il2cpp_gchandle_free(unmanagedGcHandle); + Remove(ptr); + } + + public static void Intern(Il2CppObjectBase obj) { - if (ptr == IntPtr.Zero) return default; + IntPtr ptr = obj.Pointer; + obj.pooledPtr = ptr; + s_cache[ptr] = new(obj); + } - var ownClass = IL2CPP.il2cpp_object_get_class(ptr); - if (RuntimeSpecificsStore.IsInjected(ownClass)) + public static T Get(IntPtr ptr) + { + if (ptr == IntPtr.Zero) { - var monoObject = ClassInjectorBase.GetMonoObjectFromIl2CppPointer(ptr); - if (monoObject is T monoObjectT) return monoObjectT; + return default!; } - if (DisableCaching) return Il2CppObjectBase.InitializerStore.Initializer(ptr); + if (DisableCaching) return Il2CppObjectInitializer.New(ptr); - if (s_cache.TryGetValue(ptr, out var reference) && reference.TryGetTarget(out var cachedObject)) + if (s_cache.TryGetValue(ptr, out var reference)) { - if (cachedObject is T cachedObjectT) return cachedObjectT; - cachedObject.pooledPtr = IntPtr.Zero; - // This leaves the case when you cast to an interface handled as if nothing was cached + if (reference.TryGetTarget(out var cachedObject)) + { + if (cachedObject is T cachedObjectT) return cachedObjectT; + + // This leaves the case when you cast to an interface, handled as if nothing was cached + } + + // If the cached object no longer exists, delete the irrelevant entry if we can: + Remove(ptr); } - var newObj = Il2CppObjectBase.InitializerStore.Initializer(ptr); + var newObj = Il2CppObjectInitializer.New(ptr); unsafe { var nativeClassStruct = UnityVersionHandler.Wrap((Il2CppClass*)Il2CppClassPointerStore.NativeClassPtr); @@ -47,9 +62,6 @@ public static T Get(IntPtr ptr) } } - var il2CppObjectBase = Unsafe.As(ref newObj); - s_cache[ptr] = new WeakReference(il2CppObjectBase); - il2CppObjectBase.pooledPtr = ptr; return newObj; } }