From efbbe5c47bcef17e25591c024b98ed876045b169 Mon Sep 17 00:00:00 2001 From: David Wrighton Date: Wed, 7 Oct 2026 13:51:22 -0700 Subject: [PATCH 1/3] Prototype allocator-scoped runtime delegate type factory Add Delegate.GetDelegateType with narrow CoreCLR QCalls and shared assemblies owned by signature loader allocators. Replace the feature-enabled Expressions custom delegate Emit path without a fallback, while preserving its existing dynamic-code scope. Include signature interoperability and lifetime tests, the reference API, and proposal validation evidence. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 489cae73-8e77-4116-8467-f467e9b710bc --- .../features/runtime-delegate-type-factory.md | 219 ++++++++++++++ .../System.Private.CoreLib.csproj | 1 + .../System/Delegate.TypeFactory.CoreCLR.cs | 148 +++++++++ src/coreclr/inc/utilcode.h | 3 +- src/coreclr/utilcode/util.cpp | 31 +- src/coreclr/vm/assembly.cpp | 5 +- src/coreclr/vm/assembly.hpp | 2 +- src/coreclr/vm/ceeload.h | 8 + src/coreclr/vm/comdelegate.cpp | 139 +++++++++ src/coreclr/vm/comdelegate.h | 4 + src/coreclr/vm/commodule.cpp | 41 +-- src/coreclr/vm/methodtablebuilder.cpp | 3 +- src/coreclr/vm/qcallentrypoints.cpp | 2 + .../src/System.Linq.Expressions.csproj | 4 +- .../Expressions/Compiler/DelegateHelpers.cs | 7 + .../DelegateType/GetDelegateTypeTests.cs | 48 +++ .../System.Private.CoreLib.Shared.projitems | 1 + .../src/System/Delegate.TypeFactory.cs | 113 +++++++ .../System.Runtime/ref/System.Runtime.cs | 2 + .../System/DelegateTests.cs | 281 ++++++++++++++++++ 20 files changed, 1026 insertions(+), 36 deletions(-) create mode 100644 docs/design/features/runtime-delegate-type-factory.md create mode 100644 src/coreclr/System.Private.CoreLib/src/System/Delegate.TypeFactory.CoreCLR.cs create mode 100644 src/libraries/System.Private.CoreLib/src/System/Delegate.TypeFactory.cs diff --git a/docs/design/features/runtime-delegate-type-factory.md b/docs/design/features/runtime-delegate-type-factory.md new file mode 100644 index 00000000000000..e1f4396d570202 --- /dev/null +++ b/docs/design/features/runtime-delegate-type-factory.md @@ -0,0 +1,219 @@ +# Runtime delegate-type factory proposal + +This is an unapproved API proposal and CoreCLR prototype, not a shipping API. +The prototype is on the local `api-proposal/runtime-delegate-factory` branch, +based on upstream main commit `2eb7113245f7c536ab876dca5e3533fb96c81bbf`. +It has not been published. + +## Background and motivation + +`Expression.GetDelegateType` uses `Func` and `Action` where possible, but otherwise +depends on managed Reflection.Emit to create a delegate with a runtime-implemented +constructor and `Invoke`. Callers needing byref, pointer, byref-like, or high-arity +signatures should not need the expression-tree library or the general-purpose +managed assembly/module/type/method builder implementation. + +Concrete consumers include Expressions' dynamic call-site machinery, +Microsoft.CSharp COM invocation, and CsWinRT ABI delegates with pointer and byref +parameters. [dotnet/runtime#74067](https://github.com/dotnet/runtime/issues/74067) +illustrates the latter scenario; this prototype does **not** solve its NativeAOT +code-generation limitation. + +## API proposal + +```csharp +namespace System; + +public abstract partial class Delegate +{ + [System.Diagnostics.CodeAnalysis.RequiresDynamicCode( + "Creating a delegate type may require generating code at runtime.")] + public static Type GetDelegateType(params Type[] typeArgs); +} +``` + +The parameter types precede the return type, just as in +`Expression.GetDelegateType`; `typeof(void)` denotes no return value. +Null arrays/elements, empty arrays, and `void` parameters are rejected. +Compatible signatures use the existing `Func` or `Action` type, including their +normal support for open generic arguments. Other signatures produce public, +sealed `MulticastDelegate` subclasses with a runtime constructor and `Invoke`. +Repeated requests with the same runtime type identities return the same type. + +```csharp +Type delegateType = Delegate.GetDelegateType( + typeof(int).MakeByRefType(), typeof(int)); +MethodInfo method = typeof(Example).GetMethod(nameof(Example.Increment))!; +Delegate increment = method.CreateDelegate(delegateType); +object?[] arguments = { 41 }; +int result = (int)increment.DynamicInvoke(arguments)!; // 42; arguments[0] is also 42. + +public static class Example +{ + public static int Increment(ref int value) => ++value; +} +``` + +This example requires `using System.Reflection;` in addition to `using System;`. + +## CoreCLR implementation + +Managed code performs validation, predefined-delegate selection, and signature +caching. Two narrow QCalls identify the owning loader allocator and create a +custom delegate. No managed Reflection.Emit builders or IL generation are used +by the factory. + +There is one generated assembly/module per allocator cache, not one per delegate +type. Noncollectible signatures share a process-lifetime cache and assembly. +Collectible caches are values in a `ConditionalWeakTable` keyed by the allocator's +managed object. Their signature snapshots, generated types, and assembly may +refer back to the key without making that lifetime permanent. + +The native factory reuses the loader-module selection rule for function-pointer +types: select the newest collectible allocator among signature types, or the +CoreLib allocator for a noncollectible signature. The generated assembly is +created **in that existing allocator**. Allocator references retain other +collectible signature dependencies. Retaining a generated type or delegate +therefore retains its signature dependencies; releasing those roots permits +the participating allocators and generated assembly to be collected. + +Only minimal in-memory metadata is generated: the assembly/module, a +`MulticastDelegate` parent reference, a type, and two methods. Native +`Assembly::CreateDynamic` and `COMDynamicWrite` loading machinery are reused, +but no assembly save, metadata delta, Hot Reload, or managed builder surface is +required. + +Method signatures use primitives and structural byref, pointer, array, and +generic-instantiation encodings, with runtime-owned `ELEMENT_TYPE_INTERNAL` +handles replacing metadata type-token leaves. This preserves exact identity +even when different collectible assemblies have identical assembly/type names. +Array shapes match ordinary Emit's lower-bound encoding so reconstructed +MemberRefs can resolve correctly. + +A native-only module flag permits internal handles during validation of factory +method definitions. Ordinary PE/byte-array assembly signatures remain +untrusted and still reject internal handles. Importing factory methods into +ordinary Reflection.Emit copies their module-independent signatures rather than +feeding handle bytes into the metadata-token translator. Existing collectible +dependency checks still apply. + +## Prototype boundaries and risks + +Custom creation currently requires closed runtime types. Unbaked `TypeBuilder` +inputs and open custom signatures are not implemented; standard `Func`/`Action` +construction retains its existing generic behavior. +Mono and NativeAOT use predefined delegates where possible but custom creation +throws `PlatformNotSupportedException`. A runtime supporting dynamic code is +required for custom creation in this prototype. + +With `FEATURE_RUNTIME_DELEGATE_FACTORY`, Expressions uses the runtime factory +unconditionally for custom delegate types and does not compile `AssemblyGen`. +There is no fallback to managed assembly/module/type/method builders, including +for inputs the factory rejects. `DynamicMethod`, `ILGenerator`, and their +lightweight helpers remain available for compiling expression bodies, and the +existing single `AssemblyBuilder.ForceAllowDynamicCode` scope is retained. +Other runtime flavors retain their existing implementation when the feature is +not enabled. Cross-runtime factory parity remains unimplemented. + +Expressions is an archived library; this adoption is prototype evidence, not a +proposed standalone feature contribution. + +Generated assembly names, type names, and grouping differ from the previous +Expressions implementation. Collection improves for collectible signatures, +and supported custom signatures are canonicalized across factory and +Expressions callers. These observable changes require review before production +adoption. Noncollectible metadata grows with the number of distinct custom +signatures, as it does for other process-lifetime generated types. + +## Prototype validation + +Validation was performed on Windows x64 against the upstream-main baseline +identified above. Checked runtime/CoreLib builds and the final Release build +completed without warnings or errors. + +Commands below are relative to the repository root: + +| Command | Result | +|---|---| +| `.\build.cmd clr+libs+host -arch x64 -rc checked` | Clean baseline build succeeded. | +| `.\build.cmd clr+libs -arch x64 -rc Release -lc Release` | Clean baseline and prototype builds succeeded. | +| `.\build.cmd clr.corelib+clr.nativecorelib+libs.pretest -arch x64 -rc Release -lc Release` | Final prototype CoreLib and testhost refresh succeeded. | +| `.\.dotnet\dotnet.exe build .\src\libraries\System.Linq.Expressions\tests\System.Linq.Expressions.Tests.csproj /t:Test /p:RuntimeConfiguration=Release /p:Configuration=Release` | 35,132 tests passed; no failures or skips. | +| `.\.dotnet\dotnet.exe build .\src\libraries\System.Runtime\tests\System.Runtime.Tests\System.Runtime.Tests.csproj /t:Test /p:RuntimeConfiguration=Release /p:Configuration=Release /p:CustomAfterMicrosoftCommonTargets=` | 78,568 tests total; 78,481 passed, 87 skipped, no failures, with the discovery workaround described below. | + +The unmodified System.Runtime suite fails during discovery with +`TypeLoadException` for +`System.Tests.ValueTypeTests+StructWithMutualGenericFieldA`. This was reproduced +on the clean upstream-main Release baseline as well as the prototype. +`` denotes a temporary, session-only MSBuild targets file that +removes `System\ValueTypeTests.cs` before compilation. No repository tests were +disabled or modified for this workaround; the result is not a passing run of +the full, unmodified System.Runtime suite. + +The new tests cover custom signature reflection, delegate binding, multicast +invocation, expression compilation, method-signature import and reconstruction, +concurrent caching, shared assemblies, and collectible dependency retention and +collection. Expressions tests inspect the built assembly's metadata to verify +that `AssemblyGen` and classic Emit builder dependencies are absent, apart from +the explicitly retained `AssemblyBuilder` dynamic-code scope. They also verify +that unsupported open custom signatures throw rather than falling back. + +Mono, NativeAOT, and other operating systems were not validated. + +## Performance evidence + +An ad hoc BenchmarkDotNet 0.16.0-preview.1 harness used +`Expression.GetDelegateType` on both the preserved clean baseline and prototype +Release CoreRun testhosts. This avoids requiring the new API on the baseline. +Warm cases reuse their input arrays and previously cached types. Cold creation +uses a distinct 18-parameter signature per iteration and one measured call to +avoid creating an unbounded number of process-lifetime types. + +The initial comparison used three warmup iterations, eight measurement +iterations, and one process launch per case: + +| Workload | Baseline mean | Prototype mean | Baseline managed allocation | Prototype managed allocation | +|---|---|---|---|---| +| Cached Action | 39.22 ns | 35.76 ns | 0 B | 0 B | +| Cached Func | 36.11 ns | 38.82 ns | 0 B | 0 B | +| Cached byref delegate | 35.81 ns | 35.98 ns | 0 B | 0 B | +| Cached high-arity delegate | 258.03 ns | 252.26 ns | 0 B | 0 B | +| Uncached high-arity delegate | 65.46 us | 83.62 us | 6.05 KB | 4.18 KB | + +The apparent cached Func difference was investigated with three process +launches per runtime, keeping the same warmup and measurement counts. +Baseline measured 38.58 ns (99.9% confidence interval: 36.18-40.98 ns); +the prototype measured 38.92 ns (36.42-41.42 ns), with no managed allocations. +The repeated result does not establish a regression in this unchanged warm +lookup path. + +Cold creation had broad overlapping confidence intervals and minimum-iteration +time warnings. Its medians were 62.20 us for the baseline and 58.05 us for the +prototype. Neither a cold speedup nor a cold regression is established by this +run. Measured managed allocations decreased by approximately 31%; this excludes +native metadata and loader allocations. The prototype's purpose is reducing +the managed Emit dependency and providing allocator-scoped ownership, not +promising a throughput improvement. + +## Alternatives + +| Alternative | Tradeoff | +|---|---| +| Keep `Expression.GetDelegateType` | Requires Expressions and its general managed Emit implementation; does not establish the requested allocator-scoped factory. | +| A small managed Emit wrapper | Hides the builders but retains their implementation dependency. | +| A byte-array assembly per signature | Requires normal portable metadata and binding, adds assembly overhead, and does not provide shared allocator-scoped storage. | +| MetadataUpdater | Adds an unrelated Hot Reload dependency and assembly eligibility/configuration restrictions; it cannot mutate an existing delegate signature. | +| Fully metadata-free synthesized types | Requires a separate type-loading/reflection/debugger integration path; minimal native metadata reuses existing delegate loading and reflection. | + +## Adoption catalog + +| Status | Site | Scope | +|---|---|---| +| Updated | `System.Linq.Expressions/.../Compiler/DelegateHelpers.cs` | All custom delegate requests use the runtime factory when the feature is enabled, without a classic Emit fallback. | +| Updated | `System.Linq.Expressions/src/System.Linq.Expressions.csproj` | Excludes `AssemblyGen` in feature-enabled builds. | +| Updated | `System.Linq.Expressions/tests/DelegateType/GetDelegateTypeTests.cs` | Exercises shared type identity and verifies the binary does not reference classic Emit builders, except the accepted dynamic-code scope. | +| Updated | `System.Runtime/tests/System.Runtime.Tests/System/DelegateTests.cs` | Binding, multicast, expression compilation, signature import, caching, and collectible lifetimes. | +| Candidate | `Microsoft.CSharp/.../RuntimeBinder/ComInterop/ComInvokeAction.cs` | Direct use instead of requesting the delegate through Expressions; deferred pending runtime parity and API approval. | +| Candidate | `Microsoft.CSharp/.../RuntimeBinder/DynamicDebuggerProxy.cs` | Existing `Expression.GetDelegateType` consumer; deferred to the area owner. | +| Candidate | `System.ComponentModel.Composition/.../Primitives/ExportedDelegate.cs` | Existing `Expression.GetDelegateType` consumer; requires framework-target and compatibility review. | +| Inapplicable | Ordinary statically declared delegates and predefined `Func`/`Action` consumers | Already have their required type; no dynamic factory needed. | diff --git a/src/coreclr/System.Private.CoreLib/System.Private.CoreLib.csproj b/src/coreclr/System.Private.CoreLib/System.Private.CoreLib.csproj index df450044461dbb..34203703ee4f02 100644 --- a/src/coreclr/System.Private.CoreLib/System.Private.CoreLib.csproj +++ b/src/coreclr/System.Private.CoreLib/System.Private.CoreLib.csproj @@ -131,6 +131,7 @@ + diff --git a/src/coreclr/System.Private.CoreLib/src/System/Delegate.TypeFactory.CoreCLR.cs b/src/coreclr/System.Private.CoreLib/src/System/Delegate.TypeFactory.CoreCLR.cs new file mode 100644 index 00000000000000..3a41b4f1b86737 --- /dev/null +++ b/src/coreclr/System.Private.CoreLib/src/System/Delegate.TypeFactory.CoreCLR.cs @@ -0,0 +1,148 @@ +// 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.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace System; + +public abstract partial class Delegate +{ + private static partial class DelegateTypeFactory + { + private static readonly DelegateTypeCache s_nonCollectibleCache = new(); + private static readonly ConditionalWeakTable s_collectibleCaches = new(); + + [ErrorHandler(typeof(QCallExceptionStatusMarshaller), ErrorLocation.HiddenLastParameter)] + [LibraryImport(RuntimeHelpers.QCall, EntryPoint = "Delegate_GetTypeLoaderAllocator")] + private static unsafe partial void GetTypeLoaderAllocator(nint* signature, int signatureLength, ObjectHandleOnStack loaderAllocator); + + [ErrorHandler(typeof(QCallExceptionStatusMarshaller), ErrorLocation.HiddenLastParameter)] + [LibraryImport(RuntimeHelpers.QCall, EntryPoint = "Delegate_CreateType")] + private static unsafe partial void CreateType(nint* signature, int signatureLength, ObjectHandleOnStack assembly, ObjectHandleOnStack result); + + internal static unsafe Type GetCustomDelegateType(Type[] typeArgs) + { + RuntimeType[] signature = new RuntimeType[typeArgs.Length]; + bool isCollectible = false; + for (int i = 0; i < signature.Length; i++) + { + if (typeArgs[i].UnderlyingSystemType is not RuntimeType type) + { + throw new ArgumentException(SR.Argument_MustBeRuntimeType, nameof(typeArgs)); + } + if (type.ContainsGenericParameters) + { + throw new ArgumentException(SR.Arg_UnboundGenParam, nameof(typeArgs)); + } + + signature[i] = type; + isCollectible |= type.IsCollectible; + } + + if (!RuntimeFeature.IsDynamicCodeSupported) + { + throw new PlatformNotSupportedException(SR.PlatformNotSupported_ReflectionEmit); + } + + if (!isCollectible) + { + return s_nonCollectibleCache.GetDelegateType(signature); + } + + nint[] handles = GetTypeHandles(signature); + LoaderAllocator? loaderAllocator = null; + fixed (nint* signaturePtr = handles) + { + GetTypeLoaderAllocator(signaturePtr, signature.Length, ObjectHandleOnStack.Create(ref loaderAllocator)); + } + + DelegateTypeCache cache = s_collectibleCaches.GetValue(loaderAllocator!, static _ => new DelegateTypeCache()); + Type result = cache.GetDelegateType(signature, handles); + GC.KeepAlive(signature); + return result; + } + + private static nint[] GetTypeHandles(RuntimeType[] signature) + { + nint[] handles = new nint[signature.Length]; + for (int i = 0; i < handles.Length; i++) + { + handles[i] = signature[i].TypeHandle.Value; + } + return handles; + } + + private sealed class DelegateTypeCache + { + private readonly Dictionary _types = new(SignatureComparer.Instance); + private RuntimeAssembly? _assembly; + + internal unsafe Type GetDelegateType(RuntimeType[] signature, nint[]? handles = null) + { + lock (_types) + { + if (_types.TryGetValue(signature, out Type? result)) + { + return result; + } + + handles ??= GetTypeHandles(signature); + RuntimeAssembly? assembly = _assembly; + try + { + fixed (nint* signaturePtr = handles) + { + CreateType(signaturePtr, signature.Length, ObjectHandleOnStack.Create(ref assembly), ObjectHandleOnStack.Create(ref result)); + } + } + finally + { + _assembly = assembly; + GC.KeepAlive(signature); + } + + _types.Add(signature, result!); + return result!; + } + } + } + + private sealed class SignatureComparer : IEqualityComparer + { + internal static readonly SignatureComparer Instance = new(); + + public bool Equals(RuntimeType[]? first, RuntimeType[]? second) + { + if (ReferenceEquals(first, second)) + { + return true; + } + if (first is null || second is null || first.Length != second.Length) + { + return false; + } + for (int i = 0; i < first.Length; i++) + { + if (first[i] != second[i]) + { + return false; + } + } + return true; + } + + public int GetHashCode(RuntimeType[] signature) + { + HashCode hash = default; + foreach (RuntimeType type in signature) + { + hash.Add(type); + } + return hash.ToHashCode(); + } + } + } +} diff --git a/src/coreclr/inc/utilcode.h b/src/coreclr/inc/utilcode.h index c605175ef7b62d..fbef331c33f508 100644 --- a/src/coreclr/inc/utilcode.h +++ b/src/coreclr/inc/utilcode.h @@ -2936,7 +2936,8 @@ HRESULT validateTokenSig( PCCOR_SIGNATURE pbSig, // [IN] Signature. ULONG cbSig, // [IN] Size in bytes of the signature. DWORD dwFlags, // [IN] Method flags. - IMDInternalImport* pImport); // [IN] Internal MD Import interface ptr + IMDInternalImport* pImport, // [IN] Internal MD Import interface ptr + bool allowInternalTypes = false); // Only for runtime-constructed signatures //***************************************************************************** // The registry keys and values that contain the information regarding diff --git a/src/coreclr/utilcode/util.cpp b/src/coreclr/utilcode/util.cpp index 7304af85d462d3..35acbd48de0ef8 100644 --- a/src/coreclr/utilcode/util.cpp +++ b/src/coreclr/utilcode/util.cpp @@ -1462,7 +1462,8 @@ HRESULT validateOneArg( SigParser *pSig, ULONG *pulNSentinels, // [IN/OUT] Number of sentinels IMDInternalImport* pImport, // [IN] Internal MD Import interface ptr - BOOL bNoVoidAllowed) // [IN] Flag indicating whether "void" is disallowed for this arg + BOOL bNoVoidAllowed, // [IN] Flag indicating whether "void" is disallowed for this arg + bool allowInternalTypes) { CONTRACTL @@ -1532,15 +1533,20 @@ HRESULT validateOneArg( case ELEMENT_TYPE_U: case ELEMENT_TYPE_I: break; + case ELEMENT_TYPE_INTERNAL: + if (!allowInternalTypes) + IfFailGo(VLDTR_E_SIG_BADELTYPE); + IfFailGo(pSig->GetPointer(nullptr)); + break; case ELEMENT_TYPE_PTR: // Validate the referenced type. - if(FAILED(hr = validateOneArg(tk, pSig, pulNSentinels, pImport, FALSE))) IfFailGo(hr); + if(FAILED(hr = validateOneArg(tk, pSig, pulNSentinels, pImport, FALSE, allowInternalTypes))) IfFailGo(hr); break; case ELEMENT_TYPE_BYREF: case ELEMENT_TYPE_PINNED: case ELEMENT_TYPE_SZARRAY: // Validate the referenced type. - if(FAILED(hr = validateOneArg(tk, pSig, pulNSentinels, pImport, TRUE))) IfFailGo(hr); + if(FAILED(hr = validateOneArg(tk, pSig, pulNSentinels, pImport, TRUE, allowInternalTypes))) IfFailGo(hr); break; case ELEMENT_TYPE_CMOD_OPT: case ELEMENT_TYPE_CMOD_REQD: @@ -1583,18 +1589,18 @@ HRESULT validateOneArg( // FNPTR signature must follow the rules of MethodDef // Validate and consume return type. - IfFailGo(validateOneArg(mdtMethodDef, pSig, NULL, pImport, FALSE)); + IfFailGo(validateOneArg(mdtMethodDef, pSig, NULL, pImport, FALSE, allowInternalTypes)); // Validate and consume the arguments. while(ulArgCnt--) { - IfFailGo(validateOneArg(mdtMethodDef, pSig, NULL, pImport, TRUE)); + IfFailGo(validateOneArg(mdtMethodDef, pSig, NULL, pImport, TRUE, allowInternalTypes)); } break; case ELEMENT_TYPE_ARRAY: // Validate and consume the base type. - IfFailGo(validateOneArg(tk, pSig, pulNSentinels, pImport, TRUE)); + IfFailGo(validateOneArg(tk, pSig, pulNSentinels, pImport, TRUE, allowInternalTypes)); // Validate that the rank is present. if (FAILED(pSig->GetData(&ulRank))) @@ -1651,7 +1657,7 @@ HRESULT validateOneArg( case ELEMENT_TYPE_GENERICINST: // Validate the generic type. - IfFailGo(validateOneArg(tk, pSig, pulNSentinels, pImport, TRUE)); + IfFailGo(validateOneArg(tk, pSig, pulNSentinels, pImport, TRUE, allowInternalTypes)); // Validate that parameter count is present. if (FAILED(pSig->GetData(&ulArgCnt))) @@ -1664,7 +1670,7 @@ HRESULT validateOneArg( // Validate and consume the parameters. while(ulArgCnt--) { - IfFailGo(validateOneArg(tk, pSig, NULL, pImport, TRUE)); + IfFailGo(validateOneArg(tk, pSig, NULL, pImport, TRUE, allowInternalTypes)); } break; @@ -1690,7 +1696,8 @@ HRESULT validateTokenSig( PCCOR_SIGNATURE pbSig, // [IN] Signature. ULONG cbSig, // [IN] Size in bytes of the signature. DWORD dwFlags, // [IN] Method flags. - IMDInternalImport* pImport) // [IN] Internal MD Import interface ptr + IMDInternalImport* pImport, // [IN] Internal MD Import interface ptr + bool allowInternalTypes) { CONTRACTL { @@ -1734,7 +1741,7 @@ HRESULT validateTokenSig( FALLTHROUGH; case mdtMemberRef: - if(i == IMAGE_CEE_CS_CALLCONV_FIELD) return validateOneArg(tk, &sig, NULL, pImport, TRUE); + if(i == IMAGE_CEE_CS_CALLCONV_FIELD) return validateOneArg(tk, &sig, NULL, pImport, TRUE, allowInternalTypes); // EXPLICITTHIS and native call convs are for stand-alone sigs only (for calli) if(((i != IMAGE_CEE_CS_CALLCONV_DEFAULT)&&( i != IMAGE_CEE_CS_CALLCONV_VARARG)) @@ -1754,7 +1761,7 @@ HRESULT validateTokenSig( case mdtFieldDef: if(i != IMAGE_CEE_CS_CALLCONV_FIELD) return VLDTR_E_MD_BADCALLINGCONV; - return validateOneArg(tk, &sig, NULL, pImport, TRUE); + return validateOneArg(tk, &sig, NULL, pImport, TRUE, allowInternalTypes); } // Is there any sig left for arguments? @@ -1777,7 +1784,7 @@ HRESULT validateTokenSig( // (at this moment ulArgCount = num.args+1, ulArgIx = (standalone sig. ? 1 :0); ) for(; ulArgIx < ulArgCount; ulArgIx++) { - if(FAILED(hr = validateOneArg(tk, &sig, &ulNSentinels, pImport, (ulArgIx!=0)))) return hr; + if(FAILED(hr = validateOneArg(tk, &sig, &ulNSentinels, pImport, (ulArgIx!=0), allowInternalTypes))) return hr; } // @todo: we allow junk to be at the end of the signature (we may not consume it all) diff --git a/src/coreclr/vm/assembly.cpp b/src/coreclr/vm/assembly.cpp index 89a8a3df033244..ed335ababaafdf 100644 --- a/src/coreclr/vm/assembly.cpp +++ b/src/coreclr/vm/assembly.cpp @@ -365,7 +365,7 @@ Assembly * Assembly::Create( return pAssembly; } // Assembly::Create -Assembly *Assembly::CreateDynamic(AssemblyBinder* pBinder, NativeAssemblyNameParts* pAssemblyNameParts, INT32 hashAlgorithm, INT32 access, LOADERALLOCATORREF* pKeepAlive) +Assembly *Assembly::CreateDynamic(AssemblyBinder* pBinder, NativeAssemblyNameParts* pAssemblyNameParts, INT32 hashAlgorithm, INT32 access, LOADERALLOCATORREF* pKeepAlive, LoaderAllocator* pExistingLoaderAllocator) { // WARNING: not backout clean CONTRACTL @@ -470,7 +470,8 @@ Assembly *Assembly::CreateDynamic(AssemblyBinder* pBinder, NativeAssemblyNamePar } else { - pLoaderAllocator = pBinderLoaderAllocator == nullptr ? pDomain->GetLoaderAllocator() : pBinderLoaderAllocator; + pLoaderAllocator = pExistingLoaderAllocator != nullptr ? pExistingLoaderAllocator : + (pBinderLoaderAllocator == nullptr ? pDomain->GetLoaderAllocator() : pBinderLoaderAllocator); } if (!createdNewAssemblyLoaderAllocator) diff --git a/src/coreclr/vm/assembly.hpp b/src/coreclr/vm/assembly.hpp index 1a9cb9827a547b..af0b7f2303aea9 100644 --- a/src/coreclr/vm/assembly.hpp +++ b/src/coreclr/vm/assembly.hpp @@ -166,7 +166,7 @@ class Assembly bool IsSystem() { WRAPPER_NO_CONTRACT; return m_pPEAssembly->IsSystem(); } - static Assembly* CreateDynamic(AssemblyBinder* pBinder, NativeAssemblyNameParts* pAssemblyNameParts, INT32 hashAlgorithm, INT32 access, LOADERALLOCATORREF* pKeepAlive); + static Assembly* CreateDynamic(AssemblyBinder* pBinder, NativeAssemblyNameParts* pAssemblyNameParts, INT32 hashAlgorithm, INT32 access, LOADERALLOCATORREF* pKeepAlive, LoaderAllocator* pExistingLoaderAllocator = nullptr); MethodDesc *GetEntryPoint(); diff --git a/src/coreclr/vm/ceeload.h b/src/coreclr/vm/ceeload.h index 2f680bed66da50..7baae6f8e5f1f0 100644 --- a/src/coreclr/vm/ceeload.h +++ b/src/coreclr/vm/ceeload.h @@ -645,6 +645,7 @@ class Module : public ModuleBase PROF_DISABLE_INLINING = 0x00000100, // indicates if Profiler disabled JIT Inlining event mask was set when loaded IS_ENC_CAPABLE = 0x00000200, // Cached result of IsEditAndContinueCapable() at Module creation + IS_RUNTIME_DELEGATE_MODULE = 0x00010000, // Signatures are constructed by the runtime and may contain TypeHandles // // Note: The values below must match the ones defined in @@ -933,6 +934,13 @@ class Module : public ModuleBase #endif BOOL IsReflectionEmit() const { WRAPPER_NO_CONTRACT; SUPPORTS_DAC; return (m_dwTransientFlags & IS_REFLECTION_EMIT) != 0; } + BOOL IsRuntimeDelegateModule() const { LIMITED_METHOD_CONTRACT; return (m_dwTransientFlags & IS_RUNTIME_DELEGATE_MODULE) != 0; } + void SetRuntimeDelegateModule() + { + LIMITED_METHOD_CONTRACT; + _ASSERTE(IsReflectionEmit()); + SetTransientFlagInterlocked(IS_RUNTIME_DELEGATE_MODULE); + } bool IsSystem() { WRAPPER_NO_CONTRACT; SUPPORTS_DAC; return m_pPEAssembly->IsSystem(); } virtual BOOL IsEditAndContinueCapable() const { return FALSE; } diff --git a/src/coreclr/vm/comdelegate.cpp b/src/coreclr/vm/comdelegate.cpp index ac41bcefaff092..ab2c8aacd9b348 100644 --- a/src/coreclr/vm/comdelegate.cpp +++ b/src/coreclr/vm/comdelegate.cpp @@ -22,6 +22,8 @@ #include "asmconstants.h" #include "virtualcallstub.h" #include "typestring.h" +#include "comdynamic.h" +#include "reflectclasswriter.h" #ifdef FEATURE_PORTABLE_ENTRYPOINTS #include "wasm/helpers.hpp" #endif // FEATURE_PORTABLE_ENTRYPOINTS @@ -31,6 +33,143 @@ #ifndef DACCESS_COMPILE +namespace +{ + void AppendDelegateSignatureType(SigBuilder& builder, TypeHandle type) + { + STANDARD_VM_CONTRACT; + + // ECMA-335 II.23.2.12, with runtime-owned TypeHandles in place of metadata type tokens. + CorElementType elementType = type.GetSignatureCorElementType(); + if (CorIsPrimitiveType(elementType)) + { + builder.AppendElementType(elementType); + } + else if (type.HasTypeParam()) + { + builder.AppendElementType(elementType); + AppendDelegateSignatureType(builder, type.GetTypeParam()); + if (elementType == ELEMENT_TYPE_ARRAY) + { + builder.AppendData(type.GetRank()); + builder.AppendData(0); + builder.AppendData(type.GetRank()); + for (DWORD i = 0; i < type.GetRank(); i++) + builder.AppendData(0); + } + } + else if (!type.IsTypeDesc() && type.GetMethodTable()->HasInstantiation()) + { + MethodTable* pMethodTable = type.GetMethodTable(); + Instantiation instantiation = pMethodTable->GetInstantiation(); + builder.AppendElementType(ELEMENT_TYPE_GENERICINST); + builder.AppendElementType(ELEMENT_TYPE_INTERNAL); + builder.AppendPointer(TypeHandle(pMethodTable->GetTypicalMethodTable()).AsPtr()); + builder.AppendData(instantiation.GetNumArgs()); + for (DWORD i = 0; i < instantiation.GetNumArgs(); i++) + AppendDelegateSignatureType(builder, instantiation[i]); + } + else + { + builder.AppendElementType(ELEMENT_TYPE_INTERNAL); + builder.AppendPointer(type.AsPtr()); + } + } +} + +extern "C" void QCALLTYPE Delegate_GetTypeLoaderAllocator(TypeHandle* signature, INT32 signatureLength, + QCall::ObjectHandleOnStack loaderAllocator, QCallExceptionStatus* qcallError) +{ + QCALL_CONTRACT; + + BEGIN_QCALL; + + _ASSERTE(signatureLength > 0); + LoaderAllocator* pLoaderAllocator = + ClassLoader::ComputeLoaderModuleForFunctionPointer(signature, signatureLength)->GetLoaderAllocator(); + GCX_COOP(); + loaderAllocator.Set(pLoaderAllocator->IsCollectible() ? pLoaderAllocator->GetExposedObject() : nullptr); + + END_QCALL; +} + +extern "C" void QCALLTYPE Delegate_CreateType(TypeHandle* signature, INT32 signatureLength, + QCall::ObjectHandleOnStack assembly, QCall::ObjectHandleOnStack result, QCallExceptionStatus* qcallError) +{ + QCALL_CONTRACT; + + BEGIN_QCALL; + + _ASSERTE(signatureLength > 0); + Module* pLoaderModule = ClassLoader::ComputeLoaderModuleForFunctionPointer(signature, signatureLength); + if (!pLoaderModule->IsCollectible()) + pLoaderModule = CoreLibBinder::GetModule(); + LoaderAllocator* pLoaderAllocator = pLoaderModule->GetLoaderAllocator(); + pLoaderAllocator->EnsureInstantiation(nullptr, Instantiation(signature, signatureLength)); + + Assembly* pAssembly; + { + GCX_COOP(); + if (assembly.Get() == nullptr) + { + NativeAssemblyNameParts name = {}; + name._pName = W("System.Runtime.GeneratedDelegates"); + LOADERALLOCATORREF keepAlive = nullptr; + GCPROTECT_BEGIN(keepAlive); + pAssembly = Assembly::CreateDynamic(pLoaderModule->GetAssembly()->GetPEAssembly()->GetAssemblyBinder(), + &name, 0, ASSEMBLY_ACCESS_RUN, &keepAlive, pLoaderAllocator); + pAssembly->GetModule()->SetRuntimeDelegateModule(); + assembly.Set(pAssembly->GetExposedObject()); + GCPROTECT_END(); + } + else + { + pAssembly = static_cast(assembly.Get())->GetAssembly(); + } + } + + _ASSERTE(pAssembly->GetLoaderAllocator() == pLoaderAllocator); + Module* pModule = pAssembly->GetModule(); + IMDInternalEmit* pEmitter = pModule->GetReflectionModule()->GetClassWriter()->GetEmitter(); + + mdAssemblyRef coreLibRef = pAssembly->AddAssemblyRef(CoreLibBinder::GetModule()->GetAssembly(), pEmitter); + pModule->ForceStoreAssemblyRef(coreLibRef, CoreLibBinder::GetModule()->GetAssembly()); + mdTypeRef parent; + IfFailThrow(pEmitter->DefineTypeRefByName(coreLibRef, W("System.MulticastDelegate"), &parent)); + + StackSString typeName; + typeName.Printf("Delegate%u", pModule->GetMDImport()->GetCountWithTokenKind(mdtTypeDef)); + mdTypeDef typeToken = COMDynamicWrite::DefineType(pModule, typeName.GetUnicode(), parent, + tdPublic | tdSealed | tdAutoLayout | tdAnsiClass, 0, nullptr); + + const COR_SIGNATURE constructorSignature[] = + { + IMAGE_CEE_CS_CALLCONV_DEFAULT | IMAGE_CEE_CS_CALLCONV_HASTHIS, + 2, ELEMENT_TYPE_VOID, ELEMENT_TYPE_OBJECT, ELEMENT_TYPE_I + }; + mdMethodDef methodToken; + IfFailThrow(pEmitter->DefineMethod(typeToken, W(".ctor"), + mdPublic | mdHideBySig | mdSpecialName | mdRTSpecialName, + constructorSignature, sizeof(constructorSignature), 0, miRuntime | miManaged, &methodToken)); + + SigBuilder invokeSignature; + invokeSignature.AppendByte(IMAGE_CEE_CS_CALLCONV_DEFAULT | IMAGE_CEE_CS_CALLCONV_HASTHIS); + invokeSignature.AppendData(signatureLength - 1); + AppendDelegateSignatureType(invokeSignature, signature[signatureLength - 1]); + for (INT32 i = 0; i < signatureLength - 1; i++) + AppendDelegateSignatureType(invokeSignature, signature[i]); + + DWORD signatureSize; + PCCOR_SIGNATURE signatureBlob = static_cast(invokeSignature.GetSignature(&signatureSize)); + IfFailThrow(pEmitter->DefineMethod(typeToken, W("Invoke"), + mdPublic | mdHideBySig | mdNewSlot | mdVirtual, + signatureBlob, signatureSize, 0, miRuntime | miManaged, &methodToken)); + + COMDynamicWrite::TermCreateClass(pModule, typeToken, result); + + END_QCALL; +} + #if defined(TARGET_X86) // Return an encoded shuffle entry describing a general register or stack offset that needs to be shuffled. diff --git a/src/coreclr/vm/comdelegate.h b/src/coreclr/vm/comdelegate.h index b3b7e8120fc64b..0655cadf7459cf 100644 --- a/src/coreclr/vm/comdelegate.h +++ b/src/coreclr/vm/comdelegate.h @@ -94,6 +94,10 @@ class COMDelegate extern "C" void QCALLTYPE Delegate_Construct(MethodTable* pDelegateMT, MethodTable* pTargetMT, PCODE method, BindToMethodDetails *pBindToMethodDetails, QCallExceptionStatus* qcallError); +extern "C" void QCALLTYPE Delegate_GetTypeLoaderAllocator(TypeHandle* signature, INT32 signatureLength, QCall::ObjectHandleOnStack loaderAllocator, QCallExceptionStatus* qcallError); + +extern "C" void QCALLTYPE Delegate_CreateType(TypeHandle* signature, INT32 signatureLength, QCall::ObjectHandleOnStack assembly, QCall::ObjectHandleOnStack result, QCallExceptionStatus* qcallError); + extern "C" PCODE QCALLTYPE Delegate_GetMulticastInvokeSlow(MethodTable* pDelegateMT, QCallExceptionStatus* qcallError); extern "C" PCODE QCALLTYPE Delegate_AdjustTarget(MethodTable* pMTTarg, PCODE method, QCallExceptionStatus* qcallError); diff --git a/src/coreclr/vm/commodule.cpp b/src/coreclr/vm/commodule.cpp index 771613b04aeb6b..e69ac97c32b8e6 100644 --- a/src/coreclr/vm/commodule.cpp +++ b/src/coreclr/vm/commodule.cpp @@ -11,6 +11,27 @@ #include "typeparse.h" #include "typekey.h" +static void ImportMethodSignature(Module* pSourceModule, RefClassWriter* pDestination, + PCCOR_SIGNATURE signature, ULONG signatureLength, CQuickBytes& importedSignature, ULONG* importedLength) +{ + STANDARD_VM_CONTRACT; + + if (pSourceModule->IsRuntimeDelegateModule()) + { + // Factory method signatures are module-independent. Metadata translation cannot parse internal TypeHandles. + importedSignature.ReSizeThrows(signatureLength); + memcpy(importedSignature.Ptr(), signature, signatureLength); + *importedLength = signatureLength; + } + else + { + IfFailThrow(pSourceModule->GetMDImport()->TranslateSigWithScope( + pSourceModule->GetAssembly()->GetMDImport(), nullptr, 0, + signature, signatureLength, pDestination->GetEmitter(), pDestination->GetEmitter(), + &importedSignature, importedLength)); + } +} + //************************************************** // GetTypeRef @@ -259,15 +280,7 @@ extern "C" INT32 QCALLTYPE ModuleBuilder_GetMemberRef(QCall::ModuleHandle pModul CQuickBytes qbNewSig; ULONG cbNewSig; - IfFailThrow( pRefedModule->GetMDImport()->TranslateSigWithScope( - pRefedAssembly->GetMDImport(), - NULL, 0, // hash value - pvComSig, - cbComSig, - pRCW->GetEmitter(), - pRCW->GetEmitter(), - &qbNewSig, - &cbNewSig) ); + ImportMethodSignature(pRefedModule, pRCW, pvComSig, cbComSig, qbNewSig, &cbNewSig); mdTypeRef tref; @@ -341,15 +354,7 @@ extern "C" INT32 QCALLTYPE ModuleBuilder_GetMemberRefOfMethodInfo(QCall::ModuleH COMPlusThrow(kNotSupportedException, W("NotSupported_CollectibleBoundNonCollectible")); } - IfFailThrow( pMeth->GetMDImport()->TranslateSigWithScope( - pRefedAssembly->GetMDImport(), - NULL, 0, // hash blob value - pvComSig, - cbComSig, - pRCW->GetEmitter(), - pRCW->GetEmitter(), - &qbNewSig, - &cbNewSig) ); + ImportMethodSignature(pMeth->GetModule(), pRCW, pvComSig, cbComSig, qbNewSig, &cbNewSig); // translate the name to unicode string MAKE_WIDEPTR_FROMUTF8(wszName, szName); diff --git a/src/coreclr/vm/methodtablebuilder.cpp b/src/coreclr/vm/methodtablebuilder.cpp index 9f23c2f433fbc6..0c5c86d7685e52 100644 --- a/src/coreclr/vm/methodtablebuilder.cpp +++ b/src/coreclr/vm/methodtablebuilder.cpp @@ -2713,7 +2713,8 @@ MethodTableBuilder::EnumerateClassMethods() // Signature validation if (!bmtProp.fNoSanityChecks && !isVtblGap) { - hr = validateTokenSig(tok,pMemberSignature,cMemberSignature,dwMemberAttrs,pMDInternalImport); + hr = validateTokenSig(tok,pMemberSignature,cMemberSignature,dwMemberAttrs,pMDInternalImport, + GetModule()->IsRuntimeDelegateModule()); if (FAILED(hr)) { BuildMethodTableThrowException(hr, BFA_BAD_SIGNATURE, mdMethodDefNil); diff --git a/src/coreclr/vm/qcallentrypoints.cpp b/src/coreclr/vm/qcallentrypoints.cpp index 11e954ef0d70d7..4f3cda6dcd726b 100644 --- a/src/coreclr/vm/qcallentrypoints.cpp +++ b/src/coreclr/vm/qcallentrypoints.cpp @@ -117,6 +117,8 @@ static const Entry s_QCall[] = DllImportEntry(Delegate_Construct) DllImportEntry(Delegate_CreateMethodInfo) DllImportEntry(Delegate_GetMethodDesc) + DllImportEntry(Delegate_GetTypeLoaderAllocator) + DllImportEntry(Delegate_CreateType) DllImportEntry(Environment_Exit) DllImportEntry(Environment_FailFast) DllImportEntry(Environment_GetProcessorCount) diff --git a/src/libraries/System.Linq.Expressions/src/System.Linq.Expressions.csproj b/src/libraries/System.Linq.Expressions/src/System.Linq.Expressions.csproj index 3823ac7617da78..53a5844663499a 100644 --- a/src/libraries/System.Linq.Expressions/src/System.Linq.Expressions.csproj +++ b/src/libraries/System.Linq.Expressions/src/System.Linq.Expressions.csproj @@ -4,6 +4,7 @@ $(NetCoreAppCurrent) true $(DefineConstants);FEATURE_FAST_CREATE + $(DefineConstants);FEATURE_RUNTIME_DELEGATE_FACTORY $(NoWarn);CA1859