Skip to content

Block inlines with different exception wrapping behavior #113849

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Apr 11, 2025

Conversation

AndyAyersMS
Copy link
Member

Fixes #113815

@Copilot Copilot AI review requested due to automatic review settings March 24, 2025 18:37
Copy link
Contributor

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot wasn't able to review any files in this pull request.

Files not reviewed (1)
  • src/coreclr/vm/jitinterface.cpp: Language not supported

@ghost ghost added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label Mar 24, 2025
@AndyAyersMS
Copy link
Member Author

@jkotas PTAL
cc @dotnet/jit-contrib

Module* pCalleeModule = pCallee->GetModule();
Module* pRootModule = pOrigCaller->GetModule();

if (pRootModule->IsRuntimeWrapExceptions() != pCalleeModule->IsRuntimeWrapExceptions())
Copy link
Member

@jkotas jkotas Mar 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These checks are cached cheap bit tests. We should do them first, and run the more expensive IL header decoder only when the modes do not match.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also looks like there are still issues with (lack of) an IL header.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Likely I need to handle transient methods here too.

@jkotas
Copy link
Member

jkotas commented Mar 24, 2025

I think we need the same check in crossgen2 for R2R

@AndyAyersMS
Copy link
Member Author

I think we need the same check in crossgen2 for R2R

Would the check go into CrossModuleInlineableUncached?

@jkotas
Copy link
Member

jkotas commented Mar 24, 2025

Would the check go into CrossModuleInlineableUncached?

I do not see caller and callee in CrossModuleInlineableUncached. I think it needs to be earlier.

@AndyAyersMS
Copy link
Member Author

Would the check go into CrossModuleInlineableUncached?

I do not see caller and callee in CrossModuleInlineableUncached. I think it needs to be earlier.

@jkotas I started in on crossgen2 support, but I can't see where custom attributes on modules are recorded, so could use some help finishing it off.

Also I think it would be better to either always return INLINE_FAIL from canInline or else distinguish between INLINE_FAIL and INLINE_NEVER and use the latter to short-circuit some work... at least to remember context-free decisions within the current process.

@AndyAyersMS
Copy link
Member Author

@davidwrighton maybe you can give me some pointers? How do we find module attributes in crossgen2? I see support for assembly attributes but nothing for modules.

@MichalStrehovsky
Copy link
Member

@davidwrighton maybe you can give me some pointers? How do we find module attributes in crossgen2? I see support for assembly attributes but nothing for modules.

We don't have higher level facilities for this in crossgen2, this will have to use System.Reflection.Metadata APIs, something along the lines of:

var reader = rootMod.MetadataReader;
var c = reader.StringComparer;
foreach (var attr in reader.GetModuleDefinition().GetCustomAttributes())
{
    if (reader.GetAttributeNamespaceAndName(attr, out var ns, out var n))
    {
        if (c.Equals(ns, "System.Runtime.CompilerServices") && c.Equals(n, "RuntimeCompatibilityAttribute"))
        {
            var dec = reader.GetCustomAttribute(attr).DecodeValue(new CustomAttributeTypeProvider(mod));
            // Go over dec.NamedArguments and find WrapNonExceptionThrows
        }
    }
}

We'll probably want to cache this on EcmaModule.

@AndyAyersMS
Copy link
Member Author

@davidwrighton maybe you can give me some pointers? How do we find module attributes in crossgen2? I see support for assembly attributes but nothing for modules.

We don't have higher level facilities for this in crossgen2, this will have to use System.Reflection.Metadata APIs, something along the lines of:

var reader = rootMod.MetadataReader;
var c = reader.StringComparer;
foreach (var attr in reader.GetModuleDefinition().GetCustomAttributes())
{
    if (reader.GetAttributeNamespaceAndName(attr, out var ns, out var n))
    {
        if (c.Equals(ns, "System.Runtime.CompilerServices") && c.Equals(n, "RuntimeCompatibilityAttribute"))
        {
            var dec = reader.GetCustomAttribute(attr).DecodeValue(new CustomAttributeTypeProvider(mod));
            // Go over dec.NamedArguments and find WrapNonExceptionThrows
        }
    }
}

We'll probably want to cache this on EcmaModule.

Thanks. By "this" do you mean "WrapNonExceptionThrows" or something more general?

@davidwrighton
Copy link
Member

@AndyAyersMS just WrapNonExceptionThrows. We don't need this concept much, so let's not build a generalized mechanism unless we need to.

EcmaModule rootModule = (root.OwningType as MetadataType)?.Module as EcmaModule;
EcmaModule calleeModule = (callee.OwningType as MetadataType)?.Module as EcmaModule;

// If this inline crosses module boundaries, ensure the modules agree on exception wrapping behavior.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be directly in CorInfoImpl.cs. It is not a policy decision whether the inlining is allowed for this case. It is required for correctness, so it should not be abstracted by the policy.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok. I can then undo some of the other changes, since the policy will no longer need to know the root method.

@@ -16,6 +16,8 @@ public partial class EcmaModule : ModuleDesc
{
private readonly PEReader _peReader;
protected readonly MetadataReader _metadataReader;
private bool _isWrapNonExceptionThrowsKnown;
private bool _isWrapNonExceptionThrows;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These may need to be volatile.

@AndyAyersMS
Copy link
Member Author

@jkotas PTAL.

I haven't added a test case and am not sure this gets properly tested anywhere.

@AndyAyersMS
Copy link
Member Author

I finally got around to testing this in crossgen, and the inline is still happening.

It looks like the attribute is actually on the assembly, not on the module?

In my C# calling assembly:

.assembly x
{
  .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilationRelaxationsAttribute::.ctor(int32) = ( 01 00 08 00 00 00 00 00 ) 
  .custom instance void [System.Runtime]System.Runtime.CompilerServices.RuntimeCompatibilityAttribute::.ctor() = ( 01 00 01 00 54 02 16 57 72 61 70 4E 6F 6E 45 78   // ....T..WrapNonEx
                                                                                                                   63 65 70 74 69 6F 6E 54 68 72 6F 77 73 01 )       // ceptionThrows.

And the runtime seems to look it up on the assembly, even though it gets associated with the module...?

BOOL Module::IsRuntimeWrapExceptions()
{
CONTRACTL
{
NOTHROW;
if (IsRuntimeWrapExceptionsStatusComputed()) GC_NOTRIGGER; else GC_TRIGGERS;
MODE_ANY;
}
CONTRACTL_END
if (!(IsRuntimeWrapExceptionsStatusComputed()))
{
HRESULT hr;
BOOL fRuntimeWrapExceptions = FALSE;
IMDInternalImport *mdImport = GetAssembly()->GetMDImport();
mdToken token;
IfFailGo(mdImport->GetAssemblyFromScope(&token));
const BYTE *pVal;
ULONG cbVal;
hr = mdImport->GetCustomAttributeByName(token,
RUNTIMECOMPATIBILITY_TYPE,
(const void**)&pVal, &cbVal);
// Parse the attribute
if (hr == S_OK)

@jkotas
Copy link
Member

jkotas commented Apr 9, 2025

`Yes, it looks like it. The attribute sources say the attribute target is an assembly:

[AttributeUsage(AttributeTargets.Assembly, Inherited = false, AllowMultiple = false)]

And the runtime seems to look it up on the assembly, even though it gets associated with the module...?

My guess is that it is a left-over from fragile NGen. Fragile NGen allowed precomputing information associated with Module structure. So in order for the attribute value to be computed at NGen time, caches like this were moved from Assembly to Module structure.

@AndyAyersMS
Copy link
Member Author

If I modify my EcmaModule code to look at assembly attributes then the inline is blocked as expected. I can push that change, or would you suggest revising the whole concept to be assembly-based throughout?

@jkotas
Copy link
Member

jkotas commented Apr 9, 2025

I think it is fine to keep it on Module. Assembly and Module are 1:1 in .NET Core. The managed type system even has EcmaAssembly inheriting from EcmaModule.

@jkotas
Copy link
Member

jkotas commented Apr 9, 2025

Are you going to add a test as well?

@AndyAyersMS
Copy link
Member Author

AndyAyersMS commented Apr 9, 2025

Are you going to add a test as well?

I think the existing outerloop R2R pri1 composite tests should cover this (if they also compile the wrappers). But we may need to mark the IL method that throws in the normal runtime tests with aggressive inlining.

Let me try that in a separate PR and see if I can get it to fail.

Comment on lines 700 to 730
if (!_isWrapNonExceptionThrowsKnown)
{
var reader = MetadataReader;
var c = reader.StringComparer;
foreach (var attr in reader.GetAssemblyDefinition().GetCustomAttributes())
{
if (reader.GetAttributeNamespaceAndName(attr, out var ns, out var n))
{
if (c.Equals(ns, "System.Runtime.CompilerServices") && c.Equals(n, "RuntimeCompatibilityAttribute"))
{
var dec = reader.GetCustomAttribute(attr).DecodeValue(new CustomAttributeTypeProvider(this));

foreach (var arg in dec.NamedArguments)
{
if (arg.Name == "WrapNonExceptionThrows")
{
if (!(arg.Value is bool))
ThrowHelper.ThrowBadImageFormatException();
_isWrapNonExceptionThrows = (bool)arg.Value;
break;
}
}
}
}
}

_isWrapNonExceptionThrowsKnown = true;
}
return _isWrapNonExceptionThrows;
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (!_isWrapNonExceptionThrowsKnown)
{
var reader = MetadataReader;
var c = reader.StringComparer;
foreach (var attr in reader.GetAssemblyDefinition().GetCustomAttributes())
{
if (reader.GetAttributeNamespaceAndName(attr, out var ns, out var n))
{
if (c.Equals(ns, "System.Runtime.CompilerServices") && c.Equals(n, "RuntimeCompatibilityAttribute"))
{
var dec = reader.GetCustomAttribute(attr).DecodeValue(new CustomAttributeTypeProvider(this));
foreach (var arg in dec.NamedArguments)
{
if (arg.Name == "WrapNonExceptionThrows")
{
if (!(arg.Value is bool))
ThrowHelper.ThrowBadImageFormatException();
_isWrapNonExceptionThrows = (bool)arg.Value;
break;
}
}
}
}
}
_isWrapNonExceptionThrowsKnown = true;
}
return _isWrapNonExceptionThrows;
}
}
if (!_isWrapNonExceptionThrowsComputed)
{
_isWrapNonExceptionThrows = ComputeIsWrapNonExceptionThrows();
_isWrapNonExceptionThrowsComputed = true;
}
return _isWrapNonExceptionThrows;
}
}
private bool ComputeIsWrapNonExceptionThrows()
{
var reader = MetadataReader;
var c = reader.StringComparer;
foreach (var attr in reader.GetAssemblyDefinition().GetCustomAttributes())
{
if (reader.GetAttributeNamespaceAndName(attr, out var ns, out var n))
{
if (c.Equals(ns, "System.Runtime.CompilerServices") && c.Equals(n, "RuntimeCompatibilityAttribute"))
{
var dec = reader.GetCustomAttribute(attr).DecodeValue(new CustomAttributeTypeProvider(this));
foreach (var arg in dec.NamedArguments)
{
if (arg.Name == "WrapNonExceptionThrows")
{
if (!(arg.Value is bool))
ThrowHelper.ThrowBadImageFormatException();
return (bool)arg.Value;
}
}
}
}
}
return false;
}

The AOT compilers use a pattern with separate method that computes the cached value. It makes the getter cheaper and inlining candidate.

// We will either find or create transient method details.
_ASSERTE(!cxt.HasTransientMethodDetails());

// IL methods with no RVA indicate there is no implementation defined in metadata.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is copy&paste from CEEInfo::getMethodInfo. Can this be factored into a method to avoid duplicate code?

@AndyAyersMS
Copy link
Member Author

Let me try that in a separate PR and see if I can get it to fail.

I haven't been able to get the expected failure yet in CI. Not sure why yet....

@AndyAyersMS
Copy link
Member Author

Let me try that in a separate PR and see if I can get it to fail.

I haven't been able to get the expected failure yet in CI. Not sure why yet....

Aha, the composite mode tests in R2R outerloop do not enable jit optimizations. If I enable optimization, then the composite R2R tests for Directed_3 will fail.

Seems like running these tests with optimizations enabled is desirable, since very few people will build unoptimized R2R images.

AndyAyersMS added a commit to AndyAyersMS/runtime that referenced this pull request Apr 10, 2025
Also mark `filter` test entry point as `aggressiveinlining` to create a cross-assembly
wrapped exception throw test when crossgen2 runs in composite mode.

(see  dotnet#113815 / dotnet#113849)
AndyAyersMS added a commit to AndyAyersMS/runtime that referenced this pull request Apr 10, 2025
Also mark `filter` test entry point as `aggressiveinlining` to create a cross-assembly
wrapped exception throw test when crossgen2 runs in composite mode.

(see  dotnet#113815 / dotnet#113849)
AndyAyersMS added a commit that referenced this pull request Apr 10, 2025
@AndyAyersMS
Copy link
Member Author

Now that #114464 is merged, and I've added aggressive inlining to the IL test case, crossgen2 tests should fail on that test, if the fix doesn't work.

@AndyAyersMS
Copy link
Member Author

/azp run runtime-coreclr crossgen2 outerloop

Copy link

Azure Pipelines successfully started running 1 pipeline(s).

@AndyAyersMS
Copy link
Member Author

@jkotas PTAL...

Copy link
Member

@jkotas jkotas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

@AndyAyersMS AndyAyersMS merged commit 982912a into dotnet:main Apr 11, 2025
145 of 149 checks passed
@github-actions github-actions bot locked and limited conversation to collaborators May 11, 2025
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Test failure: JIT/Directed/throwbox/filter/filter.dll
4 participants