Skip to content

Conversation

@jkoritzinsky
Copy link
Member

@jkoritzinsky jkoritzinsky commented Dec 11, 2025

Roslyn should never mark a P/Invoke or a delegate's Invoke method as an async method, so there's not a good way to add tests without writing them in IL and it doesn't seem particularly useful to do so.

Fixes #121758

@github-actions github-actions bot added the needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners label Dec 11, 2025
@jkoritzinsky jkoritzinsky added area-Interop-coreclr runtime-async and removed needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners labels Dec 11, 2025
@dotnet-policy-service
Copy link
Contributor

Tagging subscribers to this area: @dotnet/interop-contrib
See info in area-owners.md if you want to be subscribed.

Copy link
Contributor

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.

Pull request overview

This PR resolves TODOs related to async method handling in the P/Invoke implementation. The changes replace placeholder TODO comments with explanatory comments about why async methods are not supported for P/Invoke scenarios, convert some assertions to preconditions, and adjust error handling in one case to defer exception throwing to the IL stub generation path.

Key changes:

  • Adds PRECONDITION checks for async methods in constructor contracts and function preconditions
  • Replaces TODO comments with clarifying comments explaining that async P/Invoke methods should use async variant stubs that call task-returning interop stubs
  • Changes MarshalingRequired to return TRUE for async methods (to trigger proper exception later) instead of throwing immediately

Comment on lines +3056 to +3064
PRECONDITION(!pMD->IsAsyncMethod());
PRECONDITION(callConv != NULL);
}
CONTRACTL_END;

CorInfoCallConvExtension callConvLocal;
IMDInternalImport* pInternalImport = pMD->GetMDImport();
CorPinvokeMap mappingFlags = pmMaxValue;
// TODO: (async) revisit and examine if this needs to be supported somehow
if (pMD->IsAsyncMethod())
ThrowHR(COR_E_NOTSUPPORTED);

Copy link

Copilot AI Dec 11, 2025

Choose a reason for hiding this comment

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

Replacing the runtime check with a PRECONDITION is incorrect. The removed runtime check at lines 3064 (if pMD->IsAsyncMethod() ThrowHR) was a necessary validation that throws COR_E_NOTSUPPORTED for async methods. PRECONDITION checks are typically only active in debug builds and are meant for validating programming contracts, not for runtime validation. The caller GetCallingConvention_IgnoreErrors does not validate that methods are not async before calling this function. This could allow async P/Invoke methods to proceed unchecked in release builds, leading to undefined behavior instead of a proper exception. The runtime check should be retained, similar to how DllImportInit (line 2783) and GetILStubForCalli (line 6087) handle async methods.

Copilot uses AI. Check for mistakes.
GC_NOTRIGGER;
SUPPORTS_DAC;
PRECONDITION(pMD != NULL);
PRECONDITION(!pMD->IsAsyncMethod());
Copy link

Copilot AI Dec 11, 2025

Choose a reason for hiding this comment

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

Inconsistent async method validation pattern between StubSigDesc constructors. The first constructor (line 103) now uses a PRECONDITION for the async check, but the second constructor at line 139 still uses an _ASSERTE inside a conditional block. For consistency and clarity, both constructors should use the same pattern. Consider either adding the PRECONDITION to the second constructor's CONTRACTL block (since pMD can be NULL in that constructor, you'd need to add it within the if block), or using the same validation approach in both places.

Copilot uses AI. Check for mistakes.

// TODO: (async) revisit and examine if this needs to be supported somehow
// unmanaged vararg signatures should never be marked as async.
// We don't support generating a interop IL stub for an async method.
Copy link

Copilot AI Dec 11, 2025

Choose a reason for hiding this comment

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

Missing article 'an' before 'interop'. The comment should read "We don't support generating an interop IL stub" for grammatical correctness.

Suggested change
// We don't support generating a interop IL stub for an async method.
// We don't support generating an interop IL stub for an async method.

Copilot uses AI. Check for mistakes.
@VSadov
Copy link
Member

VSadov commented Dec 11, 2025

Roslyn should never mark a P/Invoke or a delegate's Invoke method as an async method

Interestingly it allows Synchronized.
Like:

    [MethodImpl(MethodImplOptions.Synchronized)]
    [DllImport("kernel32.dll")]
    private static extern bool QueryPerformanceFrequency(out long frequency);

It results in

Unhandled exception. System.TypeLoadException: Synchronized attribute cannot be used with this method type.
   at Program.Main()

Perhaps MethodImplOptions.Async should result in a similar error.

In theory we could just ignore Async on DllImport methods, since we spec'd it as

The [MethodImpl(MethodImplOptions.Async)] only has effect when applied to method definitions with CIL implementation.

The runtime could be more strict than the spec and making it an error as it is the case with Synchronized might be more consistent.

@VSadov
Copy link
Member

VSadov commented Dec 11, 2025

I can think of only two ways to get async involved with DllImport.

  • Non-task Async method.

Roslyn does not like adding Async outside of corlib/AsyncHelpers, but should be possible in IL.
Perhaps should produce the same error as with Synchronized

The runtime also does not allow non-task Async methods outside of corlib, so maybe that will error out first.

    [MethodImpl(MethodImplOptions.Async)]
    [DllImport("kernel32.dll")]
    private static extern bool QueryPerformanceFrequency(out long frequency);
  • Task-returning pinvokes:
    [DllImport("kernel32.dll")]
    private static extern Task QueryPerformanceFrequency(out long frequency);

    [DllImport("kernel32.dll")]
    private static extern ValueTask QueryPerformanceCounter(out long counter);

I am not sure if either of above can work right now.
Ideally the behavior with async enabled should be the same.

@jkoritzinsky
Copy link
Member Author

@VSadov good point on MethodImpl attribute. I'll add some tests to make sure we don't crash.

@jkoritzinsky
Copy link
Member Author

@VSadov I've rewritten this PR to have the following implementation:

Loading a P/Invoke with the async bit set fails to load.

The async variant for a P/Invoke method (if even usable) will not be considered a P/Invoke (similar to how we handled this for COM interop).

Jitting a method with UnmanagedCallersOnly with the async bit set fails with InvalidProgramException.

dllimport.cpp asserts that it never sees a method with the async bit set.

@VSadov
Copy link
Member

VSadov commented Dec 16, 2025

The [MethodImpl(MethodImplOptions.Async)] + [DllImport] scenario could be hard to test since Roslyn would not allow MethodImplOptions.Async on random methods.

The scenario like the following is testable though. It is also a preexisting scenario, so regression is a bigger deal. Perhaps we should have a test.

    [DllImport("kernel32.dll")]
    private static extern Task QueryPerformanceFrequency(out long frequency);

    [DllImport("kernel32.dll")]
    private static extern ValueTask QueryPerformanceCounter(out long counter);

Otherwise looks good to me.

IDS_EE_NDIRECT_DISABLEDMARSHAL_LCID "The LCIDConversionAttribute is not supported when runtime marshalling is disabled."
IDS_EE_NDIRECT_DISABLEDMARSHAL_PRESERVESIG "Setting PreserveSig to false for a P/Invoke is not supported when runtime marshalling is disabled."
IDS_EE_NDIRECT_DISABLEDMARSHAL_VARARGS "Using a variable argument list in a P/Invoke is not supported when runtime marshalling is disabled."
BFA_ASYNC_PINVOKE_METHOD "A PInvoke method cannot be marked as async."
Copy link
Member

Choose a reason for hiding this comment

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

All BFA_.... are in a block together. Can this be part of that block? (Alternatively, do not call it BFA...)

#endif // FEATURE_COMINTEROP
if (type == mcPInvoke)
{
// For P/Invoke methods,
Copy link
Member

Choose a reason for hiding this comment

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

Can a valid P/Invoke actually return Task (marshalled as IUknown) that can awaited by the caller? Should we have a test for it?

if (pMD->HasClassOrMethodInstantiation())
EX_THROW(EEResourceException, (kInvalidProgramException, W("InvalidProgram_GenericMethod")));

// No async methods
Copy link
Member

Choose a reason for hiding this comment

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

We have tests for the other negative cases in src\tests\Interop\UnmanagedCallersOnly\InvalidCallbacks.il . We can add the async one too for completeness.

type = mcPInvoke;
}

if (type == mcPInvoke && IsMiAsync(dwImplFlags))
Copy link
Member

Choose a reason for hiding this comment

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

Should we reject MiAsync on anything that is not mcIL or mcInstantiated?

For example, FCalls or delegate methods marked with MiAsync are not going to work well either. We just do not have them at the moment.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[RuntimeAsync] 6 TODOs in dllimport.cpp

3 participants