Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection.PortableExecutable;
using ILCompiler.ReadyToRun.Tests.TestCasesRunner;
using ILCompiler.Reflection.ReadyToRun;
Expand Down Expand Up @@ -432,6 +433,61 @@ static void Validate(ReadyToRunReader reader)
}
}

/// <summary>
/// #129813 / PR #129884: crossgen2 --strip-il-bodies must preserve the IL of non-async
/// Task/ValueTask-returning methods, which is needed to compile the runtime-async variant.
/// </summary>
[Fact]
public void RuntimeAsyncStripILBodiesPreservesTaskReturningIL()
{
var stripILBodies = new CompiledAssembly
{
AssemblyName = nameof(RuntimeAsyncStripILBodiesPreservesTaskReturningIL),
SourceResourceNames =
[
"RuntimeAsync/StripILBodies.cs",
"RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs",
],
Features = { RuntimeAsyncFeature },
};

new R2RTestRunner(_output).Run(new R2RTestCase(
nameof(RuntimeAsyncStripILBodiesPreservesTaskReturningIL),
[
new(nameof(RuntimeAsyncStripILBodiesPreservesTaskReturningIL), [new CrossgenAssembly(stripILBodies)])
{
Options = [Crossgen2Option.Composite, Crossgen2Option.Optimize, Crossgen2Option.StripILBodies],
Validate = Validate,
},
]));

static void Validate(ReadyToRunReader reader)
{
string diag;

string componentFile = Path.Combine(
Path.GetDirectoryName(reader.Filename)!,
nameof(RuntimeAsyncStripILBodiesPreservesTaskReturningIL) + ".dll");

Assert.True(R2RAssert.MethodILIsPresent(componentFile, "StripILBodies", "SyncTaskOfTForwarder", out diag), diag);
Assert.True(R2RAssert.MethodILIsPresent(componentFile, "StripILBodies", "SyncValueTaskOfTForwarder", out diag), diag);
Assert.True(R2RAssert.MethodILIsPresent(componentFile, "StripILBodies", "SyncTaskForwarder", out diag), diag);
Assert.True(R2RAssert.MethodILIsPresent(componentFile, "StripILBodies", "SyncValueTaskForwarder", out diag), diag);

Assert.True(R2RAssert.MethodILIsPresent(componentFile, "StripILBodies", "GenericIdentity", out diag), diag);
Assert.True(R2RAssert.MethodILIsPresent(componentFile, "GenericHolder`1", "MethodOnGenericType", out diag), diag);

Assert.True(R2RAssert.MethodILIsStripped(componentFile, "StripILBodies", "PlainStrippableMethod", out diag), diag);
Assert.True(R2RAssert.MethodILIsStripped(componentFile, "StripILBodies", "ComputeTag", out diag), diag);
Assert.True(R2RAssert.MethodILIsStripped(componentFile, "StripILBodies", "Root", out diag), diag);

Assert.True(R2RAssert.MethodILIsStripped(componentFile, "StripILBodies", "AsyncTaskMethod", out diag), diag);
Assert.True(R2RAssert.MethodILIsStripped(componentFile, "StripILBodies", "AsyncValueTaskMethod", out diag), diag);
Assert.True(R2RAssert.HasAsyncVariant(reader, "AsyncTaskMethod", out diag), diag);
Assert.True(R2RAssert.HasAsyncVariant(reader, "AsyncValueTaskMethod", out diag), diag);
}
}

/// <summary>
/// PR #123643: Async methods capturing GC refs across await points
/// produce ContinuationLayout fixups encoding the GC ref map.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// Test: crossgen2 --strip-il-bodies IL preservation.
// Validates that non-async Task/ValueTask-returning methods, generic methods,
// and methods on generic types keep their IL, while plain and async methods are stripped.
using System;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;

public static class StripILBodies
{
[MethodImpl(MethodImplOptions.NoInlining)]
public static Task<int> SyncTaskOfTForwarder(int value)
{
return Task.FromResult(value + 1);
}

[MethodImpl(MethodImplOptions.NoInlining)]
public static ValueTask<int> SyncValueTaskOfTForwarder(int value)
{
return new ValueTask<int>(value + 2);
}

[MethodImpl(MethodImplOptions.NoInlining)]
public static Task SyncTaskForwarder()
{
return Task.CompletedTask;
}

[MethodImpl(MethodImplOptions.NoInlining)]
public static ValueTask SyncValueTaskForwarder()
{
return default;
}

[MethodImpl(MethodImplOptions.NoInlining)]
public static T GenericIdentity<T>(T value)
{
return value;
}

public static class GenericHolder<T>
{
[MethodImpl(MethodImplOptions.NoInlining)]
public static int MethodOnGenericType(int a, int b)
{
return a + b;
}
}

[MethodImpl(MethodImplOptions.NoInlining)]
public static async Task<int> AsyncTaskMethod()
{
await Task.Yield();
return 42;
}

[MethodImpl(MethodImplOptions.NoInlining)]
public static async ValueTask AsyncValueTaskMethod()
{
await Task.Yield();
}

[MethodImpl(MethodImplOptions.NoInlining)]
public static int PlainStrippableMethod(int a, int b)
{
return a + b + ComputeTag();
}

[MethodImpl(MethodImplOptions.NoInlining)]
private static int ComputeTag()
{
return 12345;
}

[MethodImpl(MethodImplOptions.NoInlining)]
public static int Root()
{
return GenericIdentity<int>(1) + GenericHolder<string>.MethodOnGenericType(2, 3);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ internal enum Crossgen2Option
HotColdSplitting,
Optimize,
TargetArchArm,
StripILBodies,
}

internal static class Crossgen2OptionsExtensions
Expand All @@ -59,6 +60,7 @@ internal static class Crossgen2OptionsExtensions
Crossgen2Option.HotColdSplitting => $"--hot-cold-splitting",
Crossgen2Option.Optimize => $"--optimize",
Crossgen2Option.TargetArchArm => $"--targetarch:arm",
Crossgen2Option.StripILBodies => $"--strip-il-bodies",
_ => throw new ArgumentOutOfRangeException(nameof(kind)),
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1019,6 +1019,81 @@ public static bool HasCompiledMethod(ReadyToRunReader reader, string declaringTy
$"All compiled methods ({allMethods.Count}):\n {string.Join("\n ", allMethods.Select(m => $"{m.DeclaringType}:{m.Name}"))}";
return false;
}

/// <summary>
/// Reads the raw IL byte stream of a method definition from a component MSIL file.
/// </summary>
private static bool TryGetMethodIL(string msilFilePath, string declaringType, string methodName, out byte[] il, out string diagnostic)
{
il = Array.Empty<byte>();

if (!File.Exists(msilFilePath))
{
diagnostic = $"Component MSIL file not found: '{msilFilePath}'.";
return false;
}

using var fileStream = new FileStream(msilFilePath, FileMode.Open, FileAccess.Read);
using var peReader = new PEReader(fileStream);
MetadataReader mr = peReader.GetMetadataReader();
foreach (TypeDefinitionHandle typeHandle in mr.TypeDefinitions)
{
TypeDefinition type = mr.GetTypeDefinition(typeHandle);
if (mr.GetString(type.Name) != declaringType)
continue;

foreach (MethodDefinitionHandle methodHandle in type.GetMethods())
{
MethodDefinition method = mr.GetMethodDefinition(methodHandle);
if (mr.GetString(method.Name) != methodName)
continue;

int rva = method.RelativeVirtualAddress;
if (rva == 0)
{
diagnostic = $"Method '{declaringType}.{methodName}' has no IL body (RVA 0).";
return false;
}

il = peReader.GetMethodBody(rva).GetILBytes() ?? Array.Empty<byte>();
diagnostic = string.Empty;
return true;
}
}

diagnostic = $"Method '{declaringType}.{methodName}' not found in '{msilFilePath}'.";
return false;
}

/// <summary>
/// Returns true if the method's IL body was stripped by crossgen2 (--strip-il-bodies).
/// </summary>
public static bool MethodILIsStripped(string msilFilePath, string declaringType, string methodName, out string diagnostic)
{
if (!TryGetMethodIL(msilFilePath, declaringType, methodName, out byte[] il, out diagnostic))
return false;

bool stripped = il.AsSpan().SequenceEqual((ReadOnlySpan<byte>)[0x14, 0x7A]);
diagnostic = stripped
? $"IL of '{declaringType}.{methodName}' is stripped (ldnull; throw)."
: $"Expected IL of '{declaringType}.{methodName}' to be stripped, but it is present ({il.Length} bytes: {BitConverter.ToString(il)}).";
return stripped;
}

/// <summary>
/// Returns true if the method's full IL body is present in the component MSIL file.
/// </summary>
public static bool MethodILIsPresent(string msilFilePath, string declaringType, string methodName, out string diagnostic)
{
if (!TryGetMethodIL(msilFilePath, declaringType, methodName, out byte[] il, out diagnostic))
return false;

bool present = !il.AsSpan().SequenceEqual((ReadOnlySpan<byte>)[0x14, 0x7A]);
diagnostic = present
? $"IL of '{declaringType}.{methodName}' is present ({il.Length} bytes)."
: $"Expected IL of '{declaringType}.{methodName}' to be present, but it was stripped (ldnull; throw).";
return present;
}
}

/// <summary>
Expand Down