Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
1f7bb02
Implement an intrinsic for delegate lambdas
MichalPetryka Mar 22, 2026
54f8e6a
Fix build error
MichalPetryka Mar 22, 2026
b77f22e
Add more exception tests
MichalPetryka Mar 22, 2026
0a982ec
Fix IL tests
MichalPetryka Mar 22, 2026
d67df8f
Fix maxstack
MichalPetryka Mar 26, 2026
d4ef2bb
Merge remote-tracking branch 'upstream/main' into lambda-prototype
MichalPetryka Mar 26, 2026
104d92f
Fix ref assembly
MichalPetryka Mar 26, 2026
97bbe6c
Move test back to reflection
MichalPetryka Mar 26, 2026
42b5acb
Fix NAOT signature checks
MichalPetryka Mar 26, 2026
28bdf30
Try a Mono impl
MichalPetryka Mar 27, 2026
ae6fce3
Fix Mono build
MichalPetryka Mar 27, 2026
80c0c92
Fix mono more
MichalPetryka Mar 27, 2026
d6de96d
Fix build
MichalPetryka Mar 27, 2026
78613a5
Fix Mono trampolines
MichalPetryka Mar 28, 2026
f4c9444
Merge remote-tracking branch 'upstream/main' into lambda-prototype
MichalPetryka Apr 9, 2026
ab785c6
Improve NAOT implementation
MichalPetryka Apr 11, 2026
594e11a
Merge remote-tracking branch 'upstream/main' into lambda-prototype
MichalPetryka Apr 11, 2026
5e4b558
Cleanup unnecessary changes
MichalPetryka Apr 11, 2026
cc82656
Cleanup Mono
MichalPetryka Apr 11, 2026
dc61e7a
Fix formatting
MichalPetryka Apr 11, 2026
80370d1
Merge remote-tracking branch 'upstream/main' into lambda-prototype
MichalPetryka Apr 14, 2026
256034e
Merge remote-tracking branch 'upstream/main' into lambda-prototype
MichalPetryka Apr 23, 2026
d6b423d
Merge
MichalPetryka Apr 23, 2026
099afdd
Fix NativeAOT method scanning
MichalPetryka Apr 23, 2026
550c357
Merge remote-tracking branch 'upstream/main' into lambda-prototype
MichalPetryka May 27, 2026
084416a
Fix build
MichalPetryka May 27, 2026
7668899
Merge remote-tracking branch 'upstream/main' into lambda-prototype
MichalPetryka Jun 3, 2026
532a649
Move storage away from field
MichalPetryka Jun 3, 2026
6bb7d53
Merge remote-tracking branch 'upstream/main' into lambda-prototype
MichalPetryka Jun 7, 2026
97a761f
Fix NAOT frozen allocation
MichalPetryka Jun 8, 2026
b0d2772
Merge remote-tracking branch 'upstream/main' into lambda-prototype
MichalPetryka Jun 8, 2026
7dad1cf
Fix build
MichalPetryka Jun 8, 2026
af7616b
Fix formatting
MichalPetryka Jun 8, 2026
f72a4d7
Merge remote-tracking branch 'upstream/main' into lambda-prototype
MichalPetryka Jun 16, 2026
6ca14c8
Rerun generators
MichalPetryka Jun 16, 2026
4db9dd9
Remove FOH support
MichalPetryka Jun 17, 2026
c76562f
Merge remote-tracking branch 'upstream/main' into lambda-prototype
MichalPetryka Jun 17, 2026
923e386
Go back to field storage
MichalPetryka Jun 18, 2026
3740f5a
Handle lookups on NativeAOT
MichalPetryka Jun 18, 2026
0878d79
Remove DAM
MichalPetryka Jun 18, 2026
31d5636
Fix test failures
MichalPetryka Jun 18, 2026
3ea0886
Merge remote-tracking branch 'upstream/main' into lambda-prototype
MichalPetryka Jun 18, 2026
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 @@ -232,6 +232,41 @@ public static void PrepareDelegate(Delegate d)
PrepareDelegate(ObjectHandleOnStack.Create(ref d));
}

[MethodImpl(MethodImplOptions.NoInlining)]
private static unsafe TDelegate CreateSharedDelegate<TDelegate>(nint method, ref TDelegate? storage) where TDelegate : Delegate
{
Debug.Assert(typeof(TDelegate) is RuntimeType);
Debug.Assert(typeof(TDelegate).IsAssignableTo(typeof(Delegate)));

MethodTable* methodTable = ((RuntimeType)typeof(TDelegate)).GetNativeTypeHandle().AsMethodTable();

Delegate newDelegate = CreateSharedDelegateHelper(method, ref Unsafe.As<TDelegate?, Delegate?>(ref storage), methodTable);
Debug.Assert(newDelegate is TDelegate);
return Unsafe.As<TDelegate>(newDelegate);
}

[MethodImpl(MethodImplOptions.NoInlining)]
private static unsafe Delegate CreateSharedDelegateHelper(nint method, ref Delegate? storage, MethodTable* pMT)
{
ArgumentNullException.ThrowIfNull(method);

Debug.Assert(RuntimeTypeHandle.GetRuntimeType(pMT).IsDelegate());

Delegate? newDelegate = null;
CreateDelegate(method, pMT, ObjectHandleOnStack.Create(ref newDelegate));

if (newDelegate is null)
{
throw new NotSupportedException();
}

Debug.Assert(newDelegate.GetType() == RuntimeTypeHandle.GetRuntimeType(pMT));
return Interlocked.CompareExchange(ref storage, newDelegate, null) ?? newDelegate;
}

[LibraryImport(QCall, EntryPoint = "Delegate_CreateDelegate")]
private static unsafe partial void CreateDelegate(nint method, MethodTable* pMT, ObjectHandleOnStack objHandle);

/// <summary>
/// If a hash code has been assigned to the object, it is returned. Otherwise zero is
/// returned.
Expand Down
2 changes: 2 additions & 0 deletions src/coreclr/inc/corinfo.h
Original file line number Diff line number Diff line change
Expand Up @@ -590,6 +590,8 @@ enum CorInfoHelpFunc
CORINFO_HELP_ALLOC_CONTINUATION_METHOD,
CORINFO_HELP_ALLOC_CONTINUATION_CLASS,

CORINFO_HELP_CREATE_DELEGATE,

CORINFO_HELP_COUNT,
};

Expand Down
2 changes: 2 additions & 0 deletions src/coreclr/inc/jithelpers.h
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,8 @@
DYNAMICJITHELPER(CORINFO_HELP_ALLOC_CONTINUATION_METHOD, NULL, METHOD__ASYNC_HELPERS__ALLOC_CONTINUATION_METHOD)
DYNAMICJITHELPER(CORINFO_HELP_ALLOC_CONTINUATION_CLASS, NULL, METHOD__ASYNC_HELPERS__ALLOC_CONTINUATION_CLASS)

DYNAMICJITHELPER(CORINFO_HELP_CREATE_DELEGATE, NULL, METHOD__RUNTIME_HELPERS__CREATE_SHARED_DELEGATE_HELPER)

#undef JITHELPER
#undef DYNAMICJITHELPER
#undef JITHELPER
Expand Down
125 changes: 125 additions & 0 deletions src/coreclr/jit/importercalls.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3550,6 +3550,7 @@ GenTree* Compiler::impIntrinsic(CORINFO_CLASS_HANDLE clsHnd,
case NI_System_Activator_AllocatorOf:
case NI_System_Activator_DefaultConstructorOf:
case NI_System_Runtime_CompilerServices_RuntimeHelpers_IsReferenceOrContainsReferences:
case NI_System_Runtime_CompilerServices_RuntimeHelpers_GetDelegate:
mustExpand = true;
break;

Expand Down Expand Up @@ -3694,6 +3695,126 @@ GenTree* Compiler::impIntrinsic(CORINFO_CLASS_HANDLE clsHnd,
break;
}

case NI_System_Runtime_CompilerServices_RuntimeHelpers_GetDelegate:
{
assert(sig->sigInst.methInstCount == 1);

GenTree* aotInfo = nullptr;
if (IsNativeAot())
{
StackEntry methodStack = impStackTop(1);

CORINFO_METHOD_HANDLE targetMethod = NO_METHOD_HANDLE;
if (methodStack.val->OperIs(GT_FTN_ADDR))
{
targetMethod = methodStack.val->AsFptrVal()->gtFptrMethod;
}
else if (methodStack.seTypeInfo.IsMethod())
{
targetMethod = methodStack.seTypeInfo.GetMethodPointerInfo()->m_token.hMethod;
}

if (targetMethod == NO_METHOD_HANDLE)
{
JITDUMP("Delegate literals on NativeAOT require a direct ftn ptr\n");
return nullptr;
}

CORINFO_SIG_INFO callSig;
info.compCompHnd->getMethodSig(targetMethod, &callSig);

CORINFO_CLASS_HANDLE closureType = NO_CLASS_HANDLE;
if (callSig.hasThis())
{
closureType = info.compCompHnd->getMethodClass(targetMethod);
}
else if (callSig.numArgs != 0)
{
info.compCompHnd->getArgType(&callSig, callSig.args, &closureType);
}

bool throwIfClosed = closureType == NO_CLASS_HANDLE || eeIsValueClass(closureType);
if (!throwIfClosed && callSig.hasThis())
{
assert(closureType != NO_CLASS_HANDLE);
TypeCompareState state = info.compCompHnd->isGenericType(closureType);
// we should always see the declaring type enough to know if it's generic
assert(state != TypeCompareState::May);

throwIfClosed = state == TypeCompareState::Must;
}

uint32_t infoBits = callSig.hasThis() ? 0 : 1;
infoBits |= throwIfClosed ? 2 : 0;
infoBits |= callSig.totalILArgs() << 2;
aotInfo = gtNewIconNode(infoBits);

JITDUMP(
"NativeAOT delegate creation info: method %s closure %s static %u throw if closed %u args %u\n",
eeGetMethodFullName(targetMethod),
closureType == NO_CLASS_HANDLE ? "none" : eeGetClassName(closureType), infoBits & 1,
infoBits >> 1 & 1, infoBits >> 2);
}

CORINFO_SIG_INFO exactSig;
info.compCompHnd->getMethodSig(pResolvedToken->hMethod, &exactSig);
CORINFO_CLASS_HANDLE delegateType = exactSig.sigInst.methInst[0];

GenTree* delegateMT;
bool isShared = eeIsSharedInst(delegateType);
if (isShared)
{
if (!IsNativeAot())
{
// TODO: impl lookups for delegate type on CoreCLR
return nullptr;
}

CORINFO_RESOLVED_TOKEN resolvedToken;
resolvedToken.tokenContext = impTokenLookupContextHandle;
resolvedToken.tokenScope = info.compScopeHnd;
resolvedToken.token = memberRef;
resolvedToken.tokenType = CORINFO_TOKENKIND_Method;

CORINFO_GENERICHANDLE_RESULT embedInfo;
info.compCompHnd->expandRawHandleIntrinsic(&resolvedToken, info.compMethodHnd, &embedInfo);

delegateMT =
impLookupToTree(&embedInfo.lookup, gtTokenToIconFlags(memberRef), embedInfo.compileTimeHandle);
}
else
{
delegateMT = gtNewIconEmbClsHndNode(delegateType);
}

GenTree* storage = impPopStack().val;
GenTree* methodPtr = impPopStack().val;

GenTree* storageClone;
storage = impCloneExpr(storage, &storageClone, CHECK_SPILL_ALL,
nullptr DEBUGARG("RuntimeHelpers.GetDelegate storage"));

unsigned delegateSlot = lvaGrabTemp(false DEBUGARG("delegateSlot"));
impStoreToTemp(delegateSlot, gtNewIndir(TYP_REF, storage), CHECK_SPILL_ALL);

GenTreeOp* nullcheck = gtNewOperNode(GT_EQ, TYP_INT, gtNewLclVarNode(delegateSlot), gtNewNull());

GenTreeCall* helper = gtNewHelperCallNode(CORINFO_HELP_CREATE_DELEGATE, TYP_REF, methodPtr,
storageClone, delegateMT, aotInfo);

GenTree* storeCold = gtNewTempStore(delegateSlot, helper);
GenTreeColon* colon = gtNewColonNode(TYP_VOID, storeCold, gtNewNothingNode());
GenTreeQmark* qmark = gtNewQmarkNode(TYP_VOID, nullcheck, colon);
qmark->SetThenNodeLikelihood(0);

impAppendTree(qmark, CHECK_SPILL_ALL, impCurStmtDI);

lvaSetClass(delegateSlot, delegateType, !isShared);
retNode = gtNewLclVarNode(delegateSlot);
JITDUMP("Expanded GetDelegate for %s\n", eeGetClassName(delegateType));
break;
}

case NI_System_Runtime_CompilerServices_RuntimeHelpers_IsKnownConstant:
{
GenTree* op1 = impPopStack().val;
Expand Down Expand Up @@ -11185,6 +11306,10 @@ NamedIntrinsic Compiler::lookupNamedIntrinsic(CORINFO_METHOD_HANDLE method)
{
result = NI_System_Runtime_CompilerServices_RuntimeHelpers_InitializeArray;
}
else if (strcmp(methodName, "GetDelegate") == 0)
{
result = NI_System_Runtime_CompilerServices_RuntimeHelpers_GetDelegate;
}
else if (strcmp(methodName, "IsKnownConstant") == 0)
{
result = NI_System_Runtime_CompilerServices_RuntimeHelpers_IsKnownConstant;
Expand Down
1 change: 1 addition & 0 deletions src/coreclr/jit/namedintrinsiclist.h
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ enum NamedIntrinsic : unsigned short

NI_System_Runtime_CompilerServices_RuntimeHelpers_CreateSpan,
NI_System_Runtime_CompilerServices_RuntimeHelpers_InitializeArray,
NI_System_Runtime_CompilerServices_RuntimeHelpers_GetDelegate,
NI_System_Runtime_CompilerServices_RuntimeHelpers_IsKnownConstant,
NI_System_Runtime_CompilerServices_RuntimeHelpers_IsReferenceOrContainsReferences,
NI_System_Runtime_CompilerServices_RuntimeHelpers_GetMethodTable,
Expand Down
5 changes: 5 additions & 0 deletions src/coreclr/jit/utils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1771,6 +1771,11 @@ void HelperCallProperties::init()
isAllocator = true;
break;

case CORINFO_HELP_CREATE_DELEGATE:
mutatesHeap = true;
nonNullReturn = true;
break;

default:
// The most pessimistic results are returned for these helpers.
mutatesHeap = true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,13 @@ internal unsafe partial class FrozenObjectHeapManager
public T? TryAllocateObject<T>() where T : class
{
MethodTable* pMT = MethodTable.Of<T>();
return Unsafe.As<T>(TryAllocateObject(pMT, pMT->BaseSize));
return Unsafe.As<T>(TryAllocateObject(pMT));
}

public object TryAllocateObject(MethodTable* pMT)
{
Debug.Assert(!pMT->IsValueType);
return TryAllocateObject(pMT, pMT->BaseSize);
}

private object? TryAllocateObject(MethodTable* type, nuint objectSize)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,12 @@ internal static unsafe Delegate CreateDelegate(MethodTable* delegateEEType, IntP

Delegate del = (Delegate)RuntimeImports.RhNewObject(delegateEEType);

FillDelegate(del, ldftnResult, thisObject, isStatic, isOpen);
return del;
}

internal static void FillDelegate(Delegate del, nint ldftnResult, object thisObject, bool isStatic, bool isOpen)
{
// What? No constructor call? That's right, and it's not an oversight. All "construction" work happens in
// the Initialize() methods. This helper has a hard dependency on this invariant.

Expand Down Expand Up @@ -444,7 +450,6 @@ internal static unsafe Delegate CreateDelegate(MethodTable* delegateEEType, IntP
del.InitializeClosedInstanceWithoutNullCheck(thisObject, ldftnResult);
}
}
return del;
}

private unsafe Delegate NewMulticastDelegate(Wrapper[] invocationList, int invocationCount, bool thisIsMultiCastAlready = false)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Numerics;
using System.Reflection;
using System.Reflection.Runtime.General;
using System.Reflection.Runtime.MethodInfos;
using System.Runtime.InteropServices;
using System.Runtime.Serialization;
using System.Threading;

using Internal.Reflection.Augments;
using Internal.Reflection.Core.Execution;
using Internal.Runtime;
using Internal.Runtime.Augments;

Expand Down Expand Up @@ -277,6 +280,113 @@ public static void PrepareDelegate(Delegate d)
{
}

[MethodImpl(MethodImplOptions.NoInlining)]
private static unsafe TDelegate CreateSharedDelegate<TDelegate>(nint method, ref TDelegate? storage) where TDelegate : Delegate
{
ArgumentNullException.ThrowIfNull(method);

Debug.Assert(typeof(TDelegate).IsAssignableTo(typeof(Delegate)));

MethodBase? methodBase = ReflectionAugments.GetMethodBaseFromStartAddressIfAvailable(method);

if (methodBase == null)
{
throw new PlatformNotSupportedException();
}
Comment thread
MichalPetryka marked this conversation as resolved.

ReadOnlySpan<ParameterInfo> parameters = methodBase.GetParametersAsSpan();

int paramCount = parameters.Length;
bool isStatic = methodBase.IsStatic;

Type? closureType;
if (isStatic)
{
closureType = parameters.Length > 0 ? parameters[0].ParameterType : null;
}
else
{
closureType = methodBase.DeclaringType;
paramCount++; // count 'this'
}
bool throwIfClosed = closureType is null || closureType.IsValueType ||
(!isStatic && closureType.IsGenericType);

uint info = isStatic ? 1u : 0u;
info |= throwIfClosed ? 2u : 0u;
info |= (uint)paramCount << 2;

Delegate newDelegate = CreateSharedDelegateHelper(method, ref Unsafe.As<TDelegate?, Delegate?>(ref storage), MethodTable.Of<TDelegate>(), info);
Debug.Assert(newDelegate is TDelegate);
return Unsafe.As<TDelegate>(newDelegate);
}

// This method is used by the JIT as a helper
[MethodImpl(MethodImplOptions.NoInlining)]
private static unsafe Delegate CreateSharedDelegateHelper(nint method, ref Delegate? storage, MethodTable* pMT, uint info)
{
RuntimeType delegateType = Type.GetTypeFromMethodTable(pMT);
Debug.Assert(delegateType.GetRuntimeTypeInfo().IsDelegate);

bool isStatic = (info & 1) != 0;
bool throwIfClosed = (info & 2) != 0;
int paramCount = (int)(info >> 2);

MethodInfo invokeMethod = Delegate.GetInvokeMethod(delegateType);
int invokeCount = invokeMethod.GetParametersAsSpan().Length;

bool isOpen = invokeCount == paramCount;

// reject cases needing valid instances
// we block delegates closed over null valuetypes since we'd just always NRE in the unboxing stub
// reject instance methods on generic types, those require proper targets
if (!isOpen && throwIfClosed)
{
throw new NotSupportedException();
}

Delegate? newDelegate = null;
lock (FrozenDelegateCache.CacheLock)
{
ref Delegate? reference = ref CollectionsMarshal.GetValueRefOrAddDefault(FrozenDelegateCache.Cache, (method, delegateType), out bool exists);
if (exists)
{
Debug.Assert(reference.GetType() == delegateType);
newDelegate = reference;
}
else
{
object frozen = FrozenObjectHeapManager.Instance.TryAllocateObject(pMT);
if (frozen is not null)
{
Debug.Assert(frozen.GetType() == delegateType);
newDelegate = Unsafe.As<Delegate>(frozen);
Delegate.FillDelegate(newDelegate, method, null, isStatic, isOpen);

reference = newDelegate;
}
}
}

if (newDelegate is null)
{
object nonPinned = RuntimeImports.RhNewObject(pMT);

Debug.Assert(nonPinned.GetType() == delegateType);
newDelegate = Unsafe.As<Delegate>(nonPinned);
Delegate.FillDelegate(newDelegate, method, null, isStatic, isOpen);
}

Debug.Assert(newDelegate is not null);
return Interlocked.CompareExchange(ref storage, newDelegate, null) ?? newDelegate;
}

private static class FrozenDelegateCache
{
public static readonly Lock CacheLock = new();
public static readonly Dictionary<(nint, Type), Delegate> Cache = [];
}

[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2072:UnrecognizedReflectionPattern",
Justification = "Constructed MethodTable of a Nullable forces a constructed MethodTable of the element type")]
public static unsafe object GetUninitializedObject(
Expand Down
Loading
Loading