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
4 changes: 2 additions & 2 deletions src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3179,8 +3179,8 @@ private TypeCompareState compareTypesForCast(CORINFO_CLASS_STRUCT_* fromClass, C
// In R2R it is a breaking change for a previously positive
// cast to become negative, but not for a previously negative
// cast to become positive. So in R2R a negative result is
// always reported back as May.
if (result == TypeCompareState.MustNot)
// reported back as May, unless it can't change across versions.
if (result == TypeCompareState.MustNot && !IsNegativeCastResultVersionResilient(fromType, toType))
{
result = TypeCompareState.May;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

public interface INotImplemented
{
}

public interface INotImplementedByClass
{
}

public interface INotImplementedByCom
{
}

public interface IVariant<out T>
{
}

public interface ISource
{
}

public interface ITarget
{
}

public interface ISourceForOutsideTarget
{
}

public interface IDerivedFromOutside : IDisposable
{
}

public interface ITargetOfDerived
{
}

public interface IGenericSource<T>
{
}

public interface ITargetOfGeneric
{
}

public struct SourceStruct
{
public int Value;
}

public struct TargetStruct
{
public int Value;
}

public struct ControlStruct
{
public int Value;
}

public sealed class SealedClass
{
}

public sealed class VariantImplementation : IVariant<string>
{
}

[ComImport]
[Guid("8F4E2A61-5C9B-4D3E-A7F0-1B2C3D4E5F60")]
public sealed class ComClass
{
}

public static class CastFolding
{
[MethodImpl(MethodImplOptions.NoInlining)]
public static bool CharIsByte(char c) => (object)c is byte;

[MethodImpl(MethodImplOptions.NoInlining)]
public static bool StructIsOtherStruct(SourceStruct s) => (object)s is TargetStruct;

[MethodImpl(MethodImplOptions.NoInlining)]
public static bool StructIsInterface(ControlStruct s) => (object)s is INotImplemented;

[MethodImpl(MethodImplOptions.NoInlining)]
public static bool SealedClassIsInterface(SealedClass c) => (object)c is INotImplementedByClass;

[MethodImpl(MethodImplOptions.NoInlining)]
public static bool VariantCast(VariantImplementation v) => (object)v is IVariant<SealedClass>;

[MethodImpl(MethodImplOptions.NoInlining)]
public static bool ComClassIsInterface(ComClass c) => (object)c is INotImplementedByCom;

// Each guard gets a signal of its own: an unfolded IsAssignableFrom or IsAssignableTo leaves an import
// for its call, while the type handle imports for typeof remain even when the cast folds.
[MethodImpl(MethodImplOptions.NoInlining)]
public static bool InterfaceIsInterface() => typeof(ITarget).IsAssignableFrom(typeof(ISource));

[MethodImpl(MethodImplOptions.NoInlining)]
public static bool InterfaceIsOutsideInterface() => typeof(IComparable).IsAssignableFrom(typeof(ISourceForOutsideTarget));

[MethodImpl(MethodImplOptions.NoInlining)]
public static bool DerivedFromOutsideIsInterface() => typeof(IDerivedFromOutside).IsAssignableTo(typeof(ITargetOfDerived));

[MethodImpl(MethodImplOptions.NoInlining)]
public static bool SharedGenericIsInterface<T>() where T : class => typeof(ITargetOfGeneric).IsAssignableFrom(typeof(IGenericSource<T>));

[MethodImpl(MethodImplOptions.NoInlining)]
public static bool CallSharedGeneric() => SharedGenericIsInterface<string>();
}
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,74 @@ static void Validate(ReadyToRunReader reader)
}
}

[Theory]
[InlineData(false)]
[InlineData(true)]
public void CastFoldingRespectsVersionBubble(bool inputBubble)
{
var castFolding = new CompiledAssembly
{
AssemblyName = nameof(CastFoldingRespectsVersionBubble),
SourceResourceNames = ["CastFolding/CastFolding.cs"],
};

// With --input-bubble and no explicit bubble references, the framework is in the version bubble.
List<Crossgen2Option> options = inputBubble ? [Crossgen2Option.InputBubble, Crossgen2Option.Optimize] : [Crossgen2Option.Optimize];

new R2RTestRunner(_output).Run(new R2RTestCase(
nameof(CastFoldingRespectsVersionBubble),
[
new(nameof(CastFoldingRespectsVersionBubble), [new CrossgenAssembly(castFolding)])
{
Options = options,
Validate = Validate,
},
]));

void Validate(ReadyToRunReader reader)
{
// Box and isinst reference their types through NEW_OBJECT/IS_INSTANCE_OF imports
// or TYPE_HANDLE fixups depending on the target, so look for the type in any import.
var formattingOptions = new SignatureFormattingOptions();
var importedTypes = new HashSet<string>();
foreach (ReadyToRunImportSection section in reader.ImportSections)
{
foreach (ReadyToRunImportSection.ImportSectionEntry entry in section.Entries)
{
string? signature = entry.Signature?.ToString(formattingOptions);
int kindStart = signature?.LastIndexOf(" (", StringComparison.Ordinal) ?? -1;
if (kindStart > 0)
importedTypes.Add(signature![..kindStart]);
}
}

string diagnostic = $"Imported types: [{string.Join(", ", importedTypes)}]";

// Value type casts to other non-interface types fold, along with their boxes.
Assert.False(importedTypes.Contains("char"), diagnostic);
Assert.False(importedTypes.Contains("byte"), diagnostic);
Assert.False(importedTypes.Contains("SourceStruct"), diagnostic);
Assert.False(importedTypes.Contains("TargetStruct"), diagnostic);

// Casts to interfaces fold only when every type in the hierarchy is in the version bubble,
// which includes System.Object and System.ValueType only with the framework in the bubble.
Assert.True(importedTypes.Contains("ControlStruct") != inputBubble, diagnostic);
Assert.True(importedTypes.Contains("INotImplemented") != inputBubble, diagnostic);
Assert.True(importedTypes.Contains("INotImplementedByClass") != inputBubble, diagnostic);

// Interfaces have no base type, so typeof(...) assignability checks between them fold whenever
// the target and every inherited interface are in the version bubble, including for shared
// generics. Per assembly, IsAssignableFrom stays only for the IComparable target and
// IsAssignableTo only for the interface that inherits IDisposable.
Assert.True(importedTypes.Any(s => s.Contains("System.Type.IsAssignableFrom(", StringComparison.Ordinal)) != inputBubble, diagnostic);
Assert.True(importedTypes.Any(s => s.Contains("System.Type.IsAssignableTo(", StringComparison.Ordinal)) != inputBubble, diagnostic);

// Variant and COM casts are never folded.
Assert.True(importedTypes.Contains("IVariant`1<SealedClass>"), diagnostic);
Assert.True(importedTypes.Contains("INotImplementedByCom"), diagnostic);
}
}

[ConditionalFact(typeof(TestPaths), nameof(TestPaths.IsWasmTarget))]
public void WasmWebcilModule()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ internal static class Crossgen2OptionsExtensions
public static string ToArg(this Crossgen2Option kind) => kind switch
{
Crossgen2Option.Composite => $"--composite",
Crossgen2Option.InputBubble => $"--input-bubble",
Crossgen2Option.InputBubble => $"--inputbubble",
Crossgen2Option.HotColdSplitting => $"--hot-cold-splitting",
Crossgen2Option.Optimize => $"--optimize",
Crossgen2Option.StripILBodies => $"--strip-il-bodies",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3952,6 +3952,44 @@ void ValidateSafetyOfUsingTypeEquivalenceOfType(TypeDesc type)
}
}

/// <summary>
/// Returns true if a cast from an object of exact type <paramref name="fromType"/> to
/// <paramref name="toType"/> that fails now also fails in every later version of its dependencies.
/// </summary>
private bool IsNegativeCastResultVersionResilient(TypeDesc fromType, TypeDesc toType)
{
if (fromType.HasTypeEquivalence || toType.HasTypeEquivalence)
return false;

// A value type can gain interfaces (or become an enum), but its other cast targets are fixed.
if (fromType.IsValueType && !toType.IsInterface && !toType.IsWellKnownType(WellKnownType.Enum))
return true;

// Otherwise every type that decides the cast must version with this code. Array and variant casts
// also depend on element types and generic arguments, and COM objects can be cast to interfaces
// through QueryInterface.
if (fromType is not DefType || toType is not DefType || toType.HasVariance)
return false;

CompilationModuleGroup moduleGroup = _compilation.CompilationModuleGroup;
if (!moduleGroup.VersionsWithType(toType))
return false;

for (TypeDesc type = fromType; type is not null; type = type.BaseType)
{
if (type.IsComImport || !moduleGroup.VersionsWithType(type))
return false;
}

foreach (DefType iface in fromType.RuntimeInterfaces)
{
if (!moduleGroup.VersionsWithType(iface))
return false;
}

return true;
}

private bool notifyMethodInfoUsage(CORINFO_METHOD_STRUCT_* ftn)
{
MethodDesc method = HandleToObject(ftn);
Expand Down
Loading