Repository navigation
[Mono] RuntimeTypeHandle.Equals seems to be broken in Release mode #90800
Description
Description
A recent Xamarin iOS PR (dotnet/macios#18742) ran into an issue in .NET 8/Mono with the RuntimeTypeHandle.Equals method.
The Xamarin tooling generates several lookup functions in IL using Mono.Cecil which look something like this:
public uint LookupTypeId(RuntimeTypeHandle handle) {
if (handle.Equals(typeof(NSString).TypeHandle)) {
return 1u;
}
if (handle.Equals(typeof(NSSet).TypeHandle)) {
return 2u;
}
// ...
return unchecked((uint)-1);
}While this worked fine in .NET 7, in .NET 8 calling for example LookupTypeId(typeof(NSString).TypeHandle) won't return the correct value (4294967295 instead of 1).
Reproduction Steps
The issue can be reproduced with a modified HelloWorld Mono sample:
<!-- src/mono/sample/LookupTable/LookupTable.ilproj -->
<Project Sdk="Microsoft.NET.Sdk.IL">
<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>$(NetCoreAppCurrent)</TargetFramework>
</PropertyGroup>
</Project>// src/mono/sample/LookupTable/LookupTable.il
.assembly extern mscorlib{}
.assembly LookupTable{}
.class public auto ansi abstract sealed beforefieldinit LookupTable
extends [mscorlib]System.Object
{
.method public hidebysig static
int32 LookupTypeId (
valuetype [mscorlib]System.RuntimeTypeHandle handle
) cil managed
{
.maxstack 8
IL_aaaa: ldarga.s handle
ldtoken [mscorlib]System.String
call instance bool [mscorlib]System.RuntimeTypeHandle::Equals(valuetype [mscorlib]System.RuntimeTypeHandle)
brfalse.s IL_aaab
ldc.i4.2
ret
IL_aaab: ldc.i4.0
ret
}
}<!-- src/mono/sample/HelloWorld/HelloWorld.csproj -->
<ItemGroup>
<ProjectReference Include="..\LookupTable\LookupTable.ilproj" />
</ItemGroup>// src/mono/sample/HelloWorld/Program.cs
if (LookupTable.LookupTypeId(typeof(string).TypeHandle) == 2) {
System.Console.WriteLine("FOUND");
} else {
System.Console.WriteLine("NOT FOUND");
}Build the mono runtime with: ./build.sh mono+libs -c Release
Build and run the sample with mono -C src/mono/sample/HelloWorld MONO_CONFIG=Release MONO_ARCH=arm64 clean run
Expected behavior
The program should print FOUND.
Actual behavior
The program prints NOT FOUND.
Regression?
The generated lookup tables worked well before bumping to .NET 8.
Known Workarounds
When we disable inlining of the RuntimeTypeHandle.Equals method in src/mono/System.Private.CoreLib/src/System/RuntimeTypeHandle.cs, the lookup function works correctly:
[MethodImpl(MethodImplOptions.NoInlining)]
public bool Equals(RuntimeTypeHandle handle)
{
return value == handle.Value;
}Configuration
- .NET 8 (
e9ce3aac5440b8d5b76bddfea6285e546083589d) - building on macOS 13.5
- ARM64 (M1)
- Specific to
Releaseconfiguration
Other information
@filipnavara managed to workaround the issue by preventing inlining of the Equals method and so we believe that the culprit is inlining.
Activity
ivanpovazan commentedon Aug 18, 2023
/cc: @lambdageek
lambdageek commentedon Aug 18, 2023
yikes
lambdageek commentedon Aug 18, 2023
Is it important that the generated IL goes through
mscorliband notSystem.Runtimeto find the referenced types? I wonder if it's something about type forwarding and/or trimmingsimonrozsival commentedon Aug 18, 2023
@lambdageek I don't think so. This is an example of the actual generated code:
.method private final hidebysig newslot virtual instance uint32 LookupTypeId ( valuetype [System.Private.CoreLib]System.RuntimeTypeHandle handle ) cil managed { .override method instance uint32 [Microsoft.MacCatalyst]ObjCRuntime.IManagedRegistrar::LookupTypeId(valuetype [System.Private.CoreLib]System.RuntimeTypeHandle) // Method begins at RVA 0x224c // Header size: 12 // Code size: 26 (0x1a) .maxstack 2 // if (handle.Equals(typeof(AppDelegate).TypeHandle)) IL_0000: ldarga.s handle IL_0002: ldtoken MySingleView.AppDelegate IL_0007: call instance bool [System.Private.CoreLib]System.RuntimeTypeHandle::Equals(valuetype [System.Private.CoreLib]System.RuntimeTypeHandle) IL_000c: brfalse IL_0017 // return 0u; IL_0011: ldc.i4 0 IL_0016: ret // return uint.MaxValue; IL_0017: nop IL_0018: ldc.i4.m1 IL_0019: ret } // end of method __Registrar__::LookupTypeIdSorry for the confusion.
lambdageek commentedon Aug 18, 2023
It's a jit problem. If I run in release mode with interp, it finds the type
lambdageek commentedon Aug 18, 2023
It's the inliner. Running with
--traceor with-O=-inlinereturns FOUNDlambdageek commentedon Aug 18, 2023
It's specifically something about inlining the caller of
RuntimeTypeHandle::Equals(RuntimeTypeHandle)In this snippet if I change
LookupTable::Somethingto benoinlining, the result is FOUND. if I remove it, it's NOT FOUND2 remaining items
lambdageek commentedon Aug 18, 2023
@jandupej I think maybe it's a bug in
mono_decompose_vtype_opts. I think changing the instruction type in-place was wrong.If I change it like this:
the code works.
I'm not really sure why. The basic block BEFORE LOWER-VTYPE-OPTS looks like this for both versions:
The problem happens with that last
vmove vmove R48 <- R39.In the version where decompose does a fallthru, the code works and the output AFTER LOWER-VTYPE-OPTS is like this:
Note that the last
vmove R48 <- R39turned intoIn the version where we overwrite the instruction opcode, instead the output is:
And the last
vmove R48 <- R39turned intowhich seems wrong - it's copying the value in R48 into R48 again. but there's nothing that writes a value into R48!
But just looking at the code for decompose it's really not obvious to me why doing an extra pass over this basic block would mess things up so much for that three opcode sequence
any ideas?
lambdageek commentedon Aug 18, 2023
Oh. Got it. It's a typo. It's always a typo
In the
!src_varcase, the last argument shouldn't beins->dregit should beins->sreg1filipnavara commentedon Aug 18, 2023
Great find 😅
Fix typo in mono_decompose_vtype_opts
Fix typo in mono_decompose_vtype_opts
[mini] Fix typo in mono_decompose_vtype_opts (#90825)
[release/8.0-rc1] [mini] Fix typo in mono_decompose_vtype_opts (#90832)
Add workaround for dotnet/runtime#90800
rolfbjarne commentedon Aug 22, 2023
@lambdageek looks like this fix is not in .NET 8, only main (.NET 9) and .NET 8 rc1?
rolfbjarne commentedon Aug 22, 2023
Ah nvm, I was looking for a PR into release/8.0, but it seems fixes in release/8.0-rc1 are automatically backported.
Revert "Add workaround for dotnet/runtime#90800"