Skip to content

Invoke debugger function evaluations through managed UCO - #133659

Open
jkoritzinsky wants to merge 15 commits into
dev/jkoritzinsky/custom-attribute-constructorsfrom
dev/jkoritzinsky/func-eval-uco
Open

jkoritzinsky wants to merge 15 commits into
dev/jkoritzinsky/custom-attribute-constructorsfrom
dev/jkoritzinsky/func-eval-uco

Conversation

@jkoritzinsky

@jkoritzinsky jkoritzinsky commented Sep 11, 2026 •

Copy link
Copy Markdown
Member

Summary

Layer 3 of the stack for #123864, following #133658. The prior func-eval-specific proposal is #126809.

This layer started as replacing the final native managed-call dispatch with a managed UnmanagedCallersOnly entrypoint. It now moves most debugger function-evaluation preparation, invocation, storage, and copy-back into managed CoreLib while retaining the debugger-specific control flow and GC-safe bootstrap in native code.

Managed invocation and storage

  • Add Debugger.FunctionEvaluation, a runtime-owned UCO entrypoint that resolves the exact target method from its declaring RuntimeType and rooted IRuntimeMethodInfo.
  • Extend the shared reflection invocation emitter with debugger-specific thunks that prepare arguments, invoke methods and constructors, copy back writable arguments, and publish results.
  • Emit newobj for debugger object construction instead of preallocating an object and calling its constructor as an ordinary method. Existing-object constructor calls remain supported, and abstract/interface allocation is rejected.
  • Keep the entire managed call-address vector registered through GCFrameRegistration, using exact typed locals for memory-backed by-value ref structs so their managed byrefs are reported by normal JIT GC info.
  • Handle object, primitive, enum, pointer, value-type, nullable, literal, memory, register, and byref argument representations in managed code while preserving native-endian storage and debugger copy-back semantics.
  • Align nullable behavior with the existing debugger contract: direct value-type memory homes are mutated in place, while debugger handles are not treated as writable nullable homes. Exact register-backed nullable temporaries are boxed only after their MethodTable is instance-active.
  • Move ordinary value-type result storage into temporary GC-described external memory and defer boxing until after invocation and argument copy-back. Keep byref-like results in persistent external storage and raw byref results rooted directly in the debugger result slot.
  • Create result handles in managed code only after the result is fully prepared, and preserve object identity and updated field contents across collections and later debugger inspection.

Native debugger and GC integration

  • Retain native method/signature validation, generic resolution, class initialization, receiver validation inputs, register/home capture, abort state, exception publication, completion, and result ownership.
  • Capture contiguous register value-type snapshots before the first GC-triggering managed transition and describe them with their exact type layout.
  • Root non-leaf register references and register value-type snapshots with ExternalMemoryHandle registrations so collections during managed preparation relocate their contents correctly.
  • Represent external memory with a full tagged TypeHandle, allowing the runtime and cDAC to distinguish inline value types, object-reference slots, managed-byref slots, and unmanaged pointer/function-pointer locals.
  • Report each external-memory type's loader allocator during GC scanning, keeping collectible layouts alive while external storage is registered.
  • Add dedicated GC-safe object-register copy-back and native-endian scalar register copy-back paths.
  • Remove stale native argument copies and architecture-specific packing layers that are no longer needed, while preserving the architecture-specific register-home logic required by debugger IPC.
  • Treat the debugger UCO entrypoint as a runtime-internal reverse transition for exception handling, and exclude its debugger-only MethodDesc state on WebAssembly targets that do not build debug/ee.
  • Update the cDAC ExternalMemoryHandles reader, descriptors, tests, and authoritative contract documentation for the exact managed-local representation.

Correctness fixes included

Validation

Validation used the existing internal MDbg/xUnit debugger-test harness with a private CoreRun and ProjectK/CoreCLR-only configuration:

  • Targeted nested-byref invariant under target-only GCStress=0x3: 1/1 passed.
  • Revised normal matrix: 35/35 passed.
  • Targeted stress matrix: 4/4 passed.
  • Windows x64 and x86 Checked native, CoreLib, and ReadyToRun builds completed with zero warnings and errors.
  • cDAC unit tests and generated usage/documentation checks passed for the ExternalMemoryHandle descriptor changes.

Coverage includes moving GC, overlapping and nested byrefs, memory and register homes, non-leaf registers, receiver mutation, constructors, primitive/native-endian copy-back, nullable memory homes, byref/byref-like/value-type results, collectible generics and layouts, nested and exception-time eval, safe/rude/exception-unwind aborts, and post-eval inspection. Runtime hashes and loaded modules were audited; each abort scenario completed the callback, successfully evaluated Echo(42), detached, and exited MDbg with code 100. Test configurations were restored and no debugger/test processes survived the runs.

These results were recorded on the validated pre-refresh candidate. The final stack rebase preserved the layer's net diff; builds and tests were not repeated after the history rewrite at the user's request.

The final native invocation cleanup layer is #133660.


Stack created with GitHub Stacks CLI • Give Feedback 💬

Resolves #135245
Resolves #135247
Resolves #135249
Resolves #135251
Resolves #135252

Note

This PR description and implementation were prepared with GitHub Copilot. Commits that reuse am11's code retain the requested co-author attribution.

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 3 pipeline(s).
13 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: @agocke
See info in area-owners.md if you want to be subscribed.

@am11

am11 commented Sep 11, 2026

Copy link
Copy Markdown
Member

discussed on #126542.

nit: funceval was already split: #126809 so #126542 discussion is irrelevant.

@jkoritzinsky

Copy link
Copy Markdown
Member Author

Thanks, @am11. Corrected the summary to reference #126809 for the prior func-eval-specific proposal rather than #126542.

Note

This reply was prepared with GitHub Copilot.

@jkoritzinsky
jkoritzinsky force-pushed the dev/jkoritzinsky/func-eval-uco branch from 84969d7 to ce9480a Compare September 15, 2026 21:55
@jkoritzinsky
jkoritzinsky force-pushed the dev/jkoritzinsky/func-eval-uco branch from ce9480a to 10d25d9 Compare September 15, 2026 22:57
jkoritzinsky added a commit that referenced this pull request Oct 2, 2026
## Summary

Layer 1 of the stack for #123864, building on am11's #126542 and the
requested split in
#126542 (comment).
The dependent layers are #133658 (custom attributes), #133659
(func-eval), and #133660 (dead native machinery cleanup).

- Use shared, precompilable managed thunks for common
`MethodInfo.Invoke`, `ConstructorInfo.Invoke`, `MethodInvoker`, and
`ConstructorInvoker` shapes instead of the native reflection dispatcher.
- Cover bounded reference/primitive families: ordinary reference-type
instance/delegate calls, reference constructors through eight arguments,
selected primitive/enum-bearing constructors and methods, and targeted
static/reference-byref patterns. Comments identify the ASP.NET Core
callsites motivating the specialized shapes.
- Resolve virtual/interface targets for the actual receiver, and
preserve declared-type argument validation, enum widths/result identity,
reference copy-back, and collectible ownership.
- Keep unsupported signatures on the cached emitted fallback; this does
not add general struct/nullable/value-type-receiver support.
- Stay shared for the first **10,000 calls**, then specialize starting
on **call 10,001**. Promotion and every specialization strategy require
`RuntimeFeature.IsDynamicCodeCompiled`, not merely
`IsDynamicCodeSupported`.
- Preserve existing-object constructor invocation and make ReadyToRun's
explicit-this calli rewrite agree with the VM intrinsic.

No public API is added.

## Validation

The threshold update passed **1,964 reflection + 631 forced-emitted +
631 forced-shared** cases on Windows x64 Checked CoreCLR. Exact boundary
tests cover the object, span, and byref strategies; the old 100-call
runtime fails those tests as expected. A controlled CoreLib
configuration with **dynamic code supported but not compiled** passed
another **631 + 631** forced-mode cases and retained shared invocation
beyond 10,000 calls. That configuration is policy coverage, not a claim
of executing a native compiler-less platform.

The bounded matrix includes
virtual/interface/generic-virtual/default-interface dispatch, delegate
`DynamicInvoke` with static and multicast callbacks, enum
widths/results, reference out-parameter success/false/exception
behavior, moving GC, unsupported-shape boundaries, and collectible
methods. Later stack validation also executed the invocation suites on
Windows x86. Browser/WASI managed CoreLib/ABI generation was checked;
actual browser/WASI and big-endian runtime execution was not performed.

## Performance evidence

Local measurements used a Windows x64 EPYC 7763 Hyper-V VM. The ASP.NET
Core survey at `dotnet/aspnetcore` revision `a2ac63c3a56d` establishes
source-level invocation demand, **not measured performance rankings**.

Adding the bounded families avoided approximately **1.5–5 KiB of managed
allocation per first invocation** versus the earlier stack's emitted
fallback. An actual ASP.NET Core 10.0.9
`UseMiddleware`/`ActivatorUtilities` eight-reference-constructor startup
probe measured median **16.8753 → 14.4399 ms** and **9,560 → 5,376
bytes** across seven paired fresh-process runs. This did not measure
ordinary compiled request dispatch or default-constructor `Activator`
paths.

For the separate **100 → 10,000 threshold** comparison, matching Release
native hosts and baseline/changed CoreLib were run with ReadyToRun
disabled for both. Seventy-two fresh-process observations confirmed the
actual boundary. Selected warmed BenchmarkDotNet results were:

| Shape | Threshold 100 | Threshold 10,000 | Managed allocation |
|---|---:|---:|---:|
| Eight-reference constructor | 252.30 ns | 228.15 ns | 24 B in both |
| Instance primitive getter | 26.57 ns | 25.25 ns | 24 B in both |
| Static four-reference method | 115.95 ns | 94.97 ns | 0 B in both |

These are short VM-local measurements, not universal throughput
guarantees. Earlier expanded-shape testing at threshold 100 exposed a
profile-sensitive warmed `ConstructorInfo.Invoke` eight-argument case at
approximately 1.30× baseline; tiering-disabled/direct-loop controls and
the actual `ConstructorInvoker` middleware API did not reproduce that
effect. The threshold comparison above is a separate experiment.

---

<sub>Stack created with <a
href="https://github.com/github/gh-stack">GitHub Stacks CLI</a> • <a
href="https://gh.io/stacks-feedback">Give Feedback 💬</a></sub>

> [!NOTE]
> This PR description and implementation were prepared with GitHub
Copilot. Commits that reuse am11's code retain the requested co-author
attribution.

---------

Co-authored-by: Adeel Mujahid <3840695+am11@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@jkoritzinsky
jkoritzinsky force-pushed the dev/jkoritzinsky/func-eval-uco branch from 200ca9c to dfa532f Compare October 2, 2026 11:19
Comment thread src/coreclr/debug/ee/funceval.cpp
}


/*

@jkotas jkotas Oct 5, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

#126809 eliminated a lot of the layers that copy arguments around in this file. Is it possible to do it here? Or are there some fundamental problems with it?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I'm working on eliminating some of the copies and moving things to managed (and fixing a bunch of func-eval bugs I've found in the process). That's why this PR is still draft. Should be pushing it up soon I hope.

Comment on lines +9310 to +9326
if (addr == static_cast<CORDB_ADDRESS>(0))
{
VMPTR_TypeHandle layoutHandle = cv->m_type->m_typeHandleExact;
if (layoutHandle.IsNull())
{
EX_TRY
{
TypeInfoList types(bufferFrom, static_cast<int>(fullArgTypeNodeCount));
RSLockHolder lockHolder(GetProcess()->GetProcessLock());
hr = GetProcess()->GetDAC()->GetApproxTypeHandle(&types, &layoutHandle);
}
EX_CATCH_HRESULT(hr);
IfFailRet(hr);
}
bufferFrom[0].data.ClassTypeData.typeHandle = layoutHandle;
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This block fixes #135249

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This updates in this file fix #135251

jkoritzinsky and others added 15 commits October 7, 2026 15:45
Complete the third layer of #123864 on the shared invocation foundation from #126542. Use rooted receiver/argument/result storage and the common managed emitter instead of native ABI packing. Preserve raw byref and nullable results, in-place receivers, register/literal semantics, exception propagation and abort recovery. Correct native-helper abort contracts and true nullable backing-box copy-back.

Co-authored-by: Adeel Mujahid <3840695+am11@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
WebAssembly does not build or link debug/ee even though DEBUGGING_SUPPORTED is defined. Guard both the debugger UCO MethodDesc declaration and its EH comparison with TARGET_WASM instead of referencing an unavailable definition.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Add an internal InvokeForDebugger facade on RuntimeConstructorInfo that
forwards to its existing MethodBaseInvoker, so Debugger.InvokeFunction no
longer needs direct access to the constructor's Invoker property once it
is made private for encapsulation.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The writable copy is performed by PackArgumentStorage. The residual copy in GetFuncEvalArgStorage references parameters removed by the managed func-eval refactor.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Use debugger-specific emitted thunks for managed argument preparation, invocation, and copy-back. Retain native GC-safe input capture and debugger state handling, protect result and snapshot storage with external memory handles, and preserve reference identity and fresh external-value inspection across collections.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Register known references in serialized non-leaf register slots with external memory handles before managed entry. Read register-only value types from those GC-updated slots, while preserving native snapshots for leaf and mixed homes and leaving numeric register contents unchanged.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Run preparation, invocation, and copy-back within a single managed UCO entry. Use GCFrameRegistration for call storage, and move primitive capture, result handle creation, and memory/literal copy-back into CoreLib while retaining native bootstrap, register, and completion responsibilities.

Avoid the invalid cooperative-mode safe-point query uncovered by GCStress validation.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Replace the debugger-specific allocator QCall with RuntimeTypeHandle.InternalAllocNoChecks after activating each type at its actual allocation site. Preserve COM, GCStress, logging, large/finalizable object, and profiler/ETW behavior by falling back to the existing slow allocator when required.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Treat RAK_MEMREG and RAK_REGMEM as scalar 64-bit integer or enum homes rather than hypothetical GC-containing structs. Remove their GC-layout rooting and address registrations, retain scalar snapshots and copy-back, reduce the external-handle bound, and assert the verified invariant.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Register DebuggerEval::m_result[0] directly as the persistent interior root for raw byref returns. Tie root ownership to DebuggerEval cleanup, remove the refresh/controller ownership path, and unregister before exception publication repurposes the slot.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Report the MethodTable loader allocator during external-memory root promotion and remove the redundant debugger typed-owner allocator handle. Preserve the separate raw-byref result lifetime root.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Represent external memory with the exact managed local type, including byrefs and unmanaged pointers. Update runtime callers, cDAC descriptors, root walking, tests, and contract documentation.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: am11 <3840695+am11@users.noreply.github.com>

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This reverts commit cc83a28cb5e01c0c58ff4b73bef5423681110560.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

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