BepInEx / Il2CppInterop

A tool interoperate between CoreCLR and Il2Cpp at runtime
GNU Lesser General Public License v3.0
185 stars 59 forks source link

Il2CppSystem.ValueType parameters with default value throw an exception when not provided. #136

Open extraes opened 1 month ago

extraes commented 1 month ago

As an example and for ease of demonstration, unhollowed code from BONELAB is provided:

[CallerCount(14)]
[CachedScanResults(RefRangeStart = 929796, RefRangeEnd = 929810, XrefRangeStart = 929785, XrefRangeEnd = 929796, MetadataInitTokenRva = 0L, MetadataInitFlagRva = 0L)]
public unsafe static UniTask Delay([DefaultParameterValue(null)] int millisecondsDelay, bool ignoreTimeScale = false, PlayerLoopTiming delayTiming = PlayerLoopTiming.Update, CancellationToken cancellationToken = null)
{
    System.IntPtr* ptr = stackalloc System.IntPtr[4];
    *ptr = (nint)(&millisecondsDelay);
    *(bool**)((byte*)ptr + checked((nuint)1u * unchecked((nuint)sizeof(System.IntPtr)))) = &ignoreTimeScale;
    *(PlayerLoopTiming**)((byte*)ptr + checked((nuint)2u * unchecked((nuint)sizeof(System.IntPtr)))) = &delayTiming;
    *(System.IntPtr*)((byte*)ptr + checked((nuint)3u * unchecked((nuint)sizeof(System.IntPtr)))) = IL2CPP.il2cpp_object_unbox(IL2CPP.Il2CppObjectBaseToPtrNotNull(cancellationToken));
    System.Runtime.CompilerServices.Unsafe.SkipInit(out System.IntPtr exc);
    System.IntPtr pointer = IL2CPP.il2cpp_runtime_invoke(NativeMethodInfoPtr_Delay_Public_Static_UniTask_Int32_Boolean_PlayerLoopTiming_CancellationToken_0, (System.IntPtr)0, (void**)ptr, ref exc);
    Il2CppException.RaiseExceptionIfNecessary(exc);
    return new UniTask(pointer);
}

The relevant portions are the following:

The parameter is of class type CancellationToken, extending Il2CppSystem.ValueType (unhollowed version of the struct CancellationToken) In the source code for this method in the UniTask repository, this parameter is normally defined as CancellationToken cancellationToken = default(CancellationToken) The unhollowed parameter being defined as null causes a problem when this method is called without filling out all parameters, because obviously Il2CppObjectBaseToPtrNotNull throws an exception when it is null.

This can be remedied on a per-method basis by prefixing the offending methods and replacing parameters, but this is manual and still represents a bug in the generated IL. A solution could be making the IL mirror IL2CPP.Il2CppObjectBaseToPtrNotNull(cancellationToken ?? new CancellationToken()), or keep a cached copy of a default CancellationToken and retrieving that field, to avoid recreating objects, but there's more complexity to keeping that field and possibly making sure it's not collected.

ds5678 commented 1 month ago

This line is the one that's throwing.

*(System.IntPtr*)((byte*)ptr + checked((nuint)3u * unchecked((nuint)sizeof(System.IntPtr)))) = IL2CPP.il2cpp_object_unbox(IL2CPP.Il2CppObjectBaseToPtrNotNull(cancellationToken));

Should it be this instead?

*(System.IntPtr*)((byte*)ptr + checked((nuint)3u * unchecked((nuint)sizeof(System.IntPtr)))) = cancellationToken is null ? 0 : IL2CPP.il2cpp_object_unbox(IL2CPP.Il2CppObjectBaseToPtrNotNull(cancellationToken));
ds5678 commented 1 month ago

Or does the pointer need to be non-zero?

If so, your solution seems like the sensible thing to generate into the code.