Skip to content

Prototype: allocator-scoped runtime delegate-type factory (not for merge) - #135372

Draft
davidwrighton wants to merge 3 commits into
dotnet:mainfrom
davidwrighton:api-proposal/runtime-delegate-factory
Draft

davidwrighton wants to merge 3 commits into
dotnet:mainfrom
davidwrighton:api-proposal/runtime-delegate-factory

Conversation

@davidwrighton

Copy link
Copy Markdown
Member

Warning

This is an exploratory prototype for discussion only. This is not the likely direction we will take for this problem. It is not intended to merge as-is or ship the proposed API.

Summary

Prototype a dedicated CoreLib delegate-type factory, System.Runtime.CompilerServices.RuntimeHelpers.GetDelegateType(params Type[] typeArgs), returning Type with the same parameter-list/return-type convention as Expression.GetDelegateType.

  • CoreCLR uses two narrow QCalls and minimal runtime-owned metadata rather than the managed Reflection.Emit builder surface. Generated types share an assembly per signature loader-allocator cache, with collectible dependency retention and collection.
  • Expressions delegates custom-type creation to CoreLib unconditionally. AssemblyGen and its classic Emit fallback are removed; the existing single AssemblyBuilder.ForceAllowDynamicCode scope remains, as do lightweight expression-body compilation dependencies.
  • Mono moves equivalent Emit-based creation into CoreLib with a shared process-lifetime assembly and signature cache. NativeAOT retains predefined Func/Action support and rejects custom generation with PlatformNotSupportedException.

The public API is unapproved and exposed only as prototype evidence. This PR is not an API approval or production-adoption request.

The design/proposal document contains implementation details, alternatives, compatibility risks, exact validation commands, and benchmark methodology.

Boundaries

Custom creation requires closed runtime types; open custom signatures and unbaked TypeBuilder inputs are not implemented. Mono retains its existing process-lifetime dynamic-assembly behavior; CoreCLR's allocator-scoped collectible implementation is not ported to Mono here. Generated names, grouping, and canonical type identity differ from the previous Expressions implementation.

This is not metadata-free type synthesis: CoreCLR still creates minimal native in-memory metadata. It does not depend on MetadataUpdater/Hot Reload and does not permit ELEMENT_TYPE_INTERNAL in ordinary PE or byte-array-loaded metadata.

Validation

Local Windows x64 Release builds for CoreCLR, Mono, and NativeAOT succeeded with API compatibility validation enabled and no warnings or errors.

Runtime Expressions Runtime / delegate coverage
CoreCLR 35,132 passed; no failures or skips 78,570 total: 78,482 passed, 88 skipped, no failures, with the discovery isolation below
Mono 30,774 passed; no failures or skips Targeted delegate tests: 67 total, 60 passed, seven skipped, no failures
NativeAOT Predefined delegates and custom-generation rejection exercised through Expressions by the delegate tests; full Expressions suite not run Published native delegate tests: 71 total, 63 passed, eight skipped, no failures

The unmodified CoreCLR System.Runtime suite fails during discovery on ValueTypeTests.StructWithMutualGenericFieldA; this was reproduced on clean upstream main. The broad run above uses a temporary, session-only targets file excluding System\ValueTypeTests.cs, not a repository test quarantine, and is not a passing run of the full unmodified suite.

Mono's dynamic-method DynamicInvoke byref writeback difference was reproduced with statically declared and legacy Emit-generated delegates as well as factory delegates; that assertion remains CoreCLR-specific. Other binding and invocation coverage runs on both runtimes.

The NativeAOT test target compiled successfully but its script could not launch the unqualified executable name. Running the published executable by absolute path produced the passing result above. Other operating systems were not validated.

Measurements

Final BenchmarkDotNet comparison against preserved clean upstream-main CoreRun testhosts:

Workload Baseline mean Prototype mean Baseline managed allocation Prototype managed allocation
Cached Action 42.78 ns 38.53 ns 0 B 0 B
Cached Func 37.50 ns 36.90 ns 0 B 0 B
Cached byref delegate 40.82 ns 37.99 ns 0 B 0 B
Cached high-arity delegate 276.96 ns 253.47 ns 0 B 0 B
Uncached high-arity delegate 59.76 us 53.57 us 6.05 KB 4.18 KB

These short runs do not establish a throughput improvement. Cold-creation confidence intervals overlap and the benchmark reports minimum-iteration warnings. Measured managed allocation decreased by approximately 31%, excluding native metadata and loader allocations. Mono and NativeAOT were not benchmarked.

Note

This PR description and prototype implementation were generated with GitHub Copilot assistance.

davidwrighton and others added 3 commits October 7, 2026 13:51
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
Move the prototype API off Delegate and route Expressions custom delegate creation through CoreLib on every runtime flavor. Move the existing Emit machinery into Mono CoreLib with a shared assembly and signature cache; preserve NativeAOT custom-generation rejection. Remove AssemblyGen and the feature gate, retain the accepted dynamic-code scope, and record cross-runtime validation and benchmark evidence.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 489cae73-8e77-4116-8467-f467e9b710bc
Mark the proposal as exploratory discussion evidence, not a design intended to merge as-is, before publishing the draft PR.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 489cae73-8e77-4116-8467-f467e9b710bc
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 4 pipeline(s).
12 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @steveisok, @dotnet/area-system-reflection
See info in area-owners.md if you want to be subscribed.

This branch has not been deployed

No deployments
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.

1 participant