Skip to content

Convert remaining PREPARE_NONVIRTUAL_CALLSITE call sites to UnmanagedCallersOnly#125697

Open
Copilot wants to merge 6 commits intomainfrom
copilot/dotnet-123864-replace-prep-nonvirtual-calls
Open

Convert remaining PREPARE_NONVIRTUAL_CALLSITE call sites to UnmanagedCallersOnly#125697
Copilot wants to merge 6 commits intomainfrom
copilot/dotnet-123864-replace-prep-nonvirtual-calls

Conversation

Copy link
Contributor

Copilot AI commented Mar 17, 2026

Converts all remaining PREPARE_NONVIRTUAL_CALLSITE + CALL_MANAGED_METHOD* call sites (outside of the intentionally-skipped EH dispatch methods) to the UnmanagedCallersOnly/UnmanagedCallersOnlyCaller pattern, reducing GC-mode ceremony and improving call-site clarity.

Description

Infrastructure (vm/corelib.h, vm/metasig.h)

  • Updated DEFINE_METHOD signatures for ~23 methods to UCO-compatible form (pointer params + trailing Exception*)
  • Added corresponding DEFINE_METASIG_T entries; FEATURE_COMINTEROP/FEATURE_COMWRAPPERS-gated entries wrapped in matching #ifdef guards
  • BSTRMarshaler: kept original entries (used by IL-emit in ilmarshalers.cpp) and added _UCO-suffixed entries for native call sites
  • Renamed ASSEMBLY_NAME CTORCREATE_ASSEMBLY_SPEC to better reflect what the method does

C# — [UnmanagedCallersOnly] wrappers added to:

  • AssemblyLoadContext.CoreCLR.csInitializeDefaultContext, StartAssemblyLoad, StopAssemblyLoad, ResolveUnmanagedDll, ResolveUnmanagedDllUsingEvent
  • GC.CoreCLR.csRunFinalizers
  • Environment.CoreCLR.csInitializeCommandLineArgs
  • Thread.CoreCLR.csStartCallback, OnThreadExiting
  • AssemblyName.CoreCLR.csParseAsAssemblySpec, CreateAssemblyName
  • __ComObject.csReleaseAllData
  • ComWrappers.CoreCLR.csCallICustomQueryInterface, GetOrCreateComInterfaceForObjectWithGlobalMarshallingInstance, GetOrCreateObjectForComInstanceWithGlobalMarshallingInstance
  • DynamicInterfaceCastableHelpers.csIsInterfaceImplemented (UCO wrapper uses bool/bool* to match CLR_BOOL metasig), GetInterfaceImplementation
  • TypeNameResolver.CoreCLR.csGetTypeHelper (UCO wrapper uses bool parameters to match CLR_BOOL metasig)
  • StubHelpers.cs (BSTRMarshaler) — ConvertToManaged, ConvertToNative
  • EnumeratorToEnumVariantMarshaler.csInternalMarshalNativeToManaged
  • NativeLibrary.CoreCLR.csLoadLibraryCallbackStub (UCO wrapper uses bool to match CLR_BOOL metasig)
  • Lock.csInitializeForMonitor (under #if CORECLR)
  • AutoreleasePool.csCreateAutoreleasePool, DrainAutoreleasePool (under #if CORECLR)

C++ call sites converted in:
activitytracker.cpp, defaultassemblybinder.cpp, finalizerthread.cpp, syncblk.cpp, comsynchronizable.cpp, threads.cpp, corhost.cpp, dynamicinterfacecastable.cpp, typeparse.cpp, coreassemblyspec.cpp, assemblyspec.cpp, assemblynative.cpp, appdomain.cpp, runtimecallablewrapper.cpp, interoplibinterface_comwrappers.cpp, olevariant.cpp, interoputil.cpp, nativelibrary.cpp

Pre-allocations of AssemblyName objects removed from assemblynative.cpp and appdomain.cpp — the CreateAssemblyName UCO wrapper now allocates the object itself.

Bug fixes (addressed in review):

  • dynamicinterfacecastable.cpp: Fixed GC holes in CallIsInterfaceImplemented and CallGetInterfaceImplementationgc is now initialized to NULLs and GCPROTECT_BEGIN is entered before the GC-triggering GetManagedClassObject() call, with gc.obj assigned inside the protected scope.
  • olevariant.cpp: Removed shadowing BSTR bstr = NULL; declaration inside the GCPROTECT_BEGIN scope that caused InvokeThrowing's result to be discarded, always returning NULL.

Intentionally skipped: exceptionhandling.cpp/excep.cpp (RhThrowHwEx, RhThrowEx, RhRethrow — NORETURN EH dispatch) and dllimport.cpp (PREPARE_NONVIRTUAL_CALLSITE_USING_CODE — dynamic code pointer).


📍 Connect Copilot coding agent with Jira, Azure Boards or Linear to delegate work to Copilot in one click without leaving your project management tool.

Copilot AI and others added 5 commits March 17, 2026 18:11
…TE → UnmanagedCallersOnlyCaller conversion

Co-authored-by: jkoritzinsky <1571408+jkoritzinsky@users.noreply.github.com>
…e conversions

Co-authored-by: jkoritzinsky <1571408+jkoritzinsky@users.noreply.github.com>
…ies in feature guards

Co-authored-by: jkoritzinsky <1571408+jkoritzinsky@users.noreply.github.com>
…sing

Co-authored-by: jkoritzinsky <1571408+jkoritzinsky@users.noreply.github.com>
@dotnet-policy-service
Copy link
Contributor

Tagging subscribers to this area: @agocke
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 completes the conversion of remaining PREPARE_NONVIRTUAL_CALLSITE + CALL_MANAGED_METHOD* usages (excluding intentionally-skipped EH dispatch paths) to the UnmanagedCallersOnly / UnmanagedCallersOnlyCaller pattern, simplifying call sites and reducing GC-mode ceremony by routing native→managed calls through reverse-P/Invoke-compatible wrappers.

Changes:

  • Adds new [UnmanagedCallersOnly] wrappers in System.Private.CoreLib for native-callable entrypoints with trailing Exception* out-args.
  • Updates CoreCLR native call sites to use UnmanagedCallersOnlyCaller.InvokeThrowing* instead of PREPARE_NONVIRTUAL_CALLSITE + arg holders.
  • Updates CoreLib binder signatures (corelib.h, metasig.h) to match the new wrapper shapes (pointer params + trailing exception handle).

Reviewed changes

Copilot reviewed 34 out of 34 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
src/libraries/System.Private.CoreLib/src/System/Threading/Lock.cs Adds CORECLR-only UCO wrapper for lock inflation state transfer.
src/libraries/System.Private.CoreLib/src/System/Threading/AutoreleasePool.cs Adds CORECLR-only UCO wrappers for ObjC autorelease pool lifecycle.
src/coreclr/vm/typeparse.cpp Converts TypeNameResolver GetTypeHelper call site to UCO pattern and adjusts GC protection.
src/coreclr/vm/threads.cpp Converts thread-related managed callbacks (autorelease pool + OnThreadExiting) to UCO calls.
src/coreclr/vm/syncblk.cpp Converts Lock.InitializeForMonitor call site to UCO and adds GC protection.
src/coreclr/vm/runtimecallablewrapper.cpp Converts __ComObject.ReleaseAllData call site to UCO.
src/coreclr/vm/olevariant.cpp Converts BSTR marshaling call sites to UCO _UCO-suffixed targets.
src/coreclr/vm/nativelibrary.cpp Converts ALC unmanaged-DLL resolution and NativeLibrary callback stub to UCO.
src/coreclr/vm/metasig.h Adds new metasig definitions needed for UCO-compatible signatures.
src/coreclr/vm/interoputil.cpp Converts EnumeratorToEnumVariantMarshaler invocation to UCO and adds GC protection.
src/coreclr/vm/interoplibinterface_comwrappers.cpp Converts ComWrappers global marshalling entrypoints to UCO.
src/coreclr/vm/finalizerthread.cpp Converts GC.RunFinalizers invocation to UCO return-path helper.
src/coreclr/vm/dynamicinterfacecastable.cpp Converts DynamicInterfaceCastable helper calls to UCO and adjusts rooting strategy.
src/coreclr/vm/corhost.cpp Converts Environment.InitializeCommandLineArgs invocation to UCO and adds GC protection.
src/coreclr/vm/corelib.h Updates CoreLib binder method signatures to UCO-compatible forms (ptr params + Exception*).
src/coreclr/vm/coreassemblyspec.cpp Converts AssemblyName.ParseAsAssemblySpec invocation to UCO.
src/coreclr/vm/comsynchronizable.cpp Converts Thread.StartCallback invocation to UCO and adds GC protection.
src/coreclr/vm/assemblyspec.cpp Switches AssemblyName initialization from ctor to CreateAssemblyName UCO wrapper.
src/coreclr/vm/assemblynative.cpp Removes pre-allocation of AssemblyName; relies on new UCO wrapper to allocate.
src/coreclr/vm/appdomain.cpp Removes pre-allocation of AssemblyName during resolver path; relies on new UCO wrapper.
src/coreclr/binder/defaultassemblybinder.cpp Converts default ALC initialization call site to UCO.
src/coreclr/binder/activitytracker.cpp Converts ActivityTracker start/stop call sites to UCO.
src/coreclr/System.Private.CoreLib/src/System/__ComObject.cs Adds UCO wrapper for ReleaseAllData.
src/coreclr/System.Private.CoreLib/src/System/Threading/Thread.CoreCLR.cs Adds UCO wrappers for StartCallback and OnThreadExiting.
src/coreclr/System.Private.CoreLib/src/System/StubHelpers.cs Adds UCO wrappers for BSTRMarshaler ConvertToManaged/ConvertToNative.
src/coreclr/System.Private.CoreLib/src/System/Runtime/Loader/AssemblyLoadContext.CoreCLR.cs Adds UCO wrappers for unmanaged DLL resolution and assembly-load activity tracking.
src/coreclr/System.Private.CoreLib/src/System/Runtime/InteropServices/NativeLibrary.CoreCLR.cs Adds UCO wrapper for NativeLibrary.LoadLibraryCallbackStub.
src/coreclr/System.Private.CoreLib/src/System/Runtime/InteropServices/DynamicInterfaceCastableHelpers.cs Adds UCO wrappers for DynamicInterfaceCastable helper entrypoints.
src/coreclr/System.Private.CoreLib/src/System/Runtime/InteropServices/CustomMarshalers/EnumeratorToEnumVariantMarshaler.cs Adds UCO wrapper for InternalMarshalNativeToManaged.
src/coreclr/System.Private.CoreLib/src/System/Runtime/InteropServices/ComWrappers.CoreCLR.cs Adds UCO wrappers for ComWrappers global marshalling entrypoints.
src/coreclr/System.Private.CoreLib/src/System/Reflection/TypeNameResolver.CoreCLR.cs Adds UCO wrapper for VM-facing GetTypeHelper entrypoint.
src/coreclr/System.Private.CoreLib/src/System/Reflection/AssemblyName.CoreCLR.cs Adds UCO wrappers for ParseAsAssemblySpec and CreateAssemblyName.
src/coreclr/System.Private.CoreLib/src/System/GC.CoreCLR.cs Adds UCO wrapper for RunFinalizers.
src/coreclr/System.Private.CoreLib/src/System/Environment.CoreCLR.cs Adds UCO wrapper for InitializeCommandLineArgs.

Comment on lines +142 to +146
private static unsafe void GetTypeHelper(char* pTypeName, RuntimeAssembly* pRequestingAssembly, byte throwOnError, byte requireAssemblyQualifiedName, IntPtr unsafeAccessorMethod, RuntimeType* pResult, Exception* pException)
{
try
{
*pResult = GetTypeHelper(pTypeName, *pRequestingAssembly, throwOnError != 0, requireAssemblyQualifiedName != 0, unsafeAccessorMethod);
Comment on lines +25 to +35
[UnmanagedCallersOnly]
private static unsafe void IsInterfaceImplemented(IDynamicInterfaceCastable* pCastable, RuntimeType* pInterfaceType, byte throwIfNotImplemented, byte* pResult, Exception* pException)
{
try
{
*pResult = IsInterfaceImplemented(*pCastable, *pInterfaceType, throwIfNotImplemented != 0) ? (byte)1 : (byte)0;
}
catch (Exception ex)
{
*pException = ex;
}
Comment on lines +28 to +38
[UnmanagedCallersOnly]
private static unsafe void LoadLibraryCallbackStub(string* pLibraryName, Assembly* pAssembly, byte hasDllImportSearchPathFlags, uint dllImportSearchPathFlags, IntPtr* pResult, Exception* pException)
{
try
{
*pResult = LoadLibraryCallbackStub(*pLibraryName, *pAssembly, hasDllImportSearchPathFlags != 0, dllImportSearchPathFlags);
}
catch (Exception ex)
{
*pException = ex;
}
Comment on lines +21 to +31
struct {
OBJECTREF obj;
OBJECTREF managedType;
} gc;
gc.obj = *objPROTECTED;
gc.managedType = interfaceTypeHandle.GetManagedClassObject(); // GC triggers

CLR_BOOL isImplemented = FALSE;
GCPROTECT_BEGIN(gc);
UnmanagedCallersOnlyCaller isInterfaceImplemented(METHOD__DYNAMICINTERFACECASTABLEHELPERS__IS_INTERFACE_IMPLEMENTED);
isInterfaceImplemented.InvokeThrowing(&gc.obj, &gc.managedType, CLR_BOOL_ARG(throwIfNotImplemented), &isImplemented);
Comment on lines +52 to +59
} gc;
gc.obj = *objPROTECTED;
gc.managedType = interfaceTypeHandle.GetManagedClassObject(); // GC triggers
gc.result = NULL;

GCPROTECT_BEGIN(gc);
UnmanagedCallersOnlyCaller getInterfaceImplementation(METHOD__DYNAMICINTERFACECASTABLEHELPERS__GET_INTERFACE_IMPLEMENTATION);
getInterfaceImplementation.InvokeThrowing(&gc.obj, &gc.managedType, &gc.result);
CALL_MANAGED_METHOD(bstr, BSTR, args);
GCPROTECT_BEGIN(*pStringObj);
UnmanagedCallersOnlyCaller convertToNative(METHOD__BSTRMARSHALER__CONVERT_TO_NATIVE_UCO);
BSTR bstr = NULL;
@jkoritzinsky
Copy link
Member

@copilot please address the feedback from @copilot

…plicate bstr declaration

Co-authored-by: jkoritzinsky <1571408+jkoritzinsky@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Mar 18, 2026

@copilot please address the feedback from @copilot

Fixed in d465333:

  1. bytebool mismatches (TypeNameResolver.CoreCLR.cs, DynamicInterfaceCastableHelpers.cs, NativeLibrary.CoreCLR.cs): Changed byte/byte* UCO wrapper parameters to bool/bool* to match the CLR_BOOL (ELEMENT_TYPE_BOOLEAN) metasig entries, and simplified the body to use the bool values directly.

  2. GC holes in dynamicinterfacecastable.cpp: In both CallIsInterfaceImplemented and CallGetInterfaceImplementation, moved GCPROTECT_BEGIN(gc) before the GC-triggering GetManagedClassObject() call (initializing gc to NULLs first), then assigned gc.obj and gc.managedType inside the protected scope.

  3. Duplicate BSTR bstr in olevariant.cpp: Removed the shadowing BSTR bstr = NULL; inside the GCPROTECT_BEGIN scope so the outer bstr variable initialized at the top of the function receives the result from InvokeThrowing.

}

[UnmanagedCallersOnly]
private static unsafe uint RunFinalizers(Exception* pException)
Copy link
Member

Choose a reason for hiding this comment

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

Does this affect diagnosability of unhandled exceptions thrown by finalizers (e.g. from crash dumps)?

Also, the two RunFinalizers methods can be merged.

Copy link
Member

Choose a reason for hiding this comment

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

@jkoritzinsky See the "Exception Propagation Helper" section in #123864. I've not written the QCall yet, but I can share the details if you'd like.

{
try
{
pThread->StartCallback();
Copy link
Member

Choose a reason for hiding this comment

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

Same here. Does this affect diagnosability of unhandled exceptions?

}

[UnmanagedCallersOnly]
private static unsafe void OnThreadExiting(Thread* pThread, Exception* pException)
Copy link
Member

Choose a reason for hiding this comment

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

It is a bit misleading to call this OnThreadExiting since it is only ever called after the thread exited.

{
try
{
*pResult = GetTypeHelper(pTypeName, *pRequestingAssembly, throwOnError, requireAssemblyQualifiedName, unsafeAccessorMethod);
Copy link
Member

Choose a reason for hiding this comment

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

Actual work can be inlined here.

{
try
{
ParseAsAssemblySpec(pAssemblyName, pAssemblySpec);
Copy link
Member

Choose a reason for hiding this comment

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

Actual work can be inlined here.

{
try
{
*pResult = InternalMarshalNativeToManaged(pNativeData);
Copy link
Member

Choose a reason for hiding this comment

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

Actual work can be inlined here.

Copy link
Member

Choose a reason for hiding this comment

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

(This applies to all cases where the managed method exists for the sole purpose of being called by the VM, and it is not very complicated.)

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

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

5 participants