Repository navigation
[release/11.0.1xx] [Foundation] Reuse managed wrappers during constructor chaining - #26835
Conversation
Objective-C initializer chaining can invoke another managed constructor for a native object whose GCHandle ivar already points at the original wrapper. The constructor trampoline previously allocated a second wrapper anyway, which then tried to install another GCHandle and produced the managed-reference warning. Reuse a compatible wrapper already stored in the native ivar, and let repeated base initialization recognize when the existing GCHandle targets the same managed instance. A failed GCHandle installation still reports a genuinely different wrapper, including the race where another wrapper wins between the lookup and the set. Extend the constructor-chaining test to verify that the parameterless managed constructor runs on the same instance returned by the outer constructor. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Avoid retaining the native object again when an Objective-C initializer chains into another managed constructor on the same wrapper. The wrapper already owns the reference transferred by the original allocation, so the second retain could not be balanced by the single wrapper's disposal path. Reject disposed and finalizer-queue wrappers during ivar lookup, and preserve the incoming constructor flags when a live compatible wrapper is reused. Extend the same behavior to managed-static, trimmable-static, and NativeAOT registrars. Generated constructor trampolines now call a shared generic helper to reuse a wrapper from the native gchandle ivar or allocate an uninitialized one before invoking the real constructor. This replaces the synthetic cloned constructor previously added to user types. Assert that constructor chaining retains wrapper identity and leaves the native object with a retain count of one. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7c477a8e-5fc5-4f8e-8edb-a7b7d97cd21b
Only query the native GCHandle ivar when CreateManagedRef is re-entered for a wrapper that already owns a managed reference. Normal user-type construction now avoids the additional native message send while constructor chaining still recognizes and reuses the existing wrapper. Clear the constructor-chaining test's static instance reference in a finally block so the disposed wrapper cannot remain rooted or leak state into another test. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7c477a8e-5fc5-4f8e-8edb-a7b7d97cd21b
…tor registrar output The constructor registrar now emits callbacks that go through NSObject.GetOrCreateNSObjectForConstructor<T>, so the generated code references that method, the HasManagedRef property, and the real NSObjectFlag-based constructor of each type. At the same time the synthetic (IntPtr, IManagedRegistrar) constructors that the old generation scheme required are no longer emitted, so they drop out of the preserved API surface. These four tvOS expectation files record that intended shift in the registrar output. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7c477a8e-5fc5-4f8e-8edb-a7b7d97cd21b
The constructor registrar changes altered which members the registrar preserves, and the tvOS expectation files were already updated to match. Update the eight remaining iOS and Mac Catalyst expectation files so they describe the same output. The baselines now include NSObject.GetOrCreateNSObjectForConstructor<T>, NSObject.HasManagedRef, and the UIApplicationDelegate NSObjectFlag constructor. They remove the obsolete synthetic (IntPtr, IManagedRegistrar) constructors, including ActionDispatcher on Mac Catalyst. This addresses review feedback and prevents the app-size preserved API tests from failing. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7c477a8e-5fc5-4f8e-8edb-a7b7d97cd21b
The previous cross-platform baseline update covered the shared registrar changes but did not match the generated CI output exactly. Apply the remaining differences verbatim from the Azure DevOps updated-expected-sizes artifacts. The Mac Catalyst CoreCLR baselines now preserve the ActionDispatcher static constructor, EventHandler constructor, and Action and DoubleAction selector fields. The iOS NativeAOT and NativeAOT-TrimmableStatic expected app sizes also record the approximately 15 KB cost of the constructor registrar changes. Using the CI artifacts keeps these app-size and preserved-API expectations authoritative. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7c477a8e-5fc5-4f8e-8edb-a7b7d97cd21b
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Broad constructor preservation can unnecessarily inflate trimmed and NativeAOT applications.
Review effort: Balanced
Findings: 1
Open (1)
What changed in this PR
Backports constructor-wrapper reuse to prevent duplicate GCHandles during Objective-C constructor chaining.
Changes:
- Reuses compatible live
NSObjectwrappers across registrar modes. - Avoids duplicate native retains and synthetic constructors.
- Extends regression tests and updates size/API baselines.
| File | Description |
|---|---|
src/Foundation/NSObject2.cs |
Adds wrapper reuse and retain handling. |
tools/dotnet-linker/AppBundleRewriter.cs |
Resolves the shared constructor helper. |
tools/dotnet-linker/Steps/ManagedRegistrarStep.cs |
Emits helper-based constructor trampolines. |
tests/monotouch-test/ObjCRuntime/RegistrarTest.cs |
Verifies wrapper identity and retain count. |
tests/dotnet/UnitTests/expected/TVOS-MonoVM-preservedapis.txt |
Updates preserved APIs. |
tests/dotnet/UnitTests/expected/TVOS-MonoVM-interpreter-preservedapis.txt |
Updates preserved APIs. |
tests/dotnet/UnitTests/expected/TVOS-CoreCLR-R2R-preservedapis.txt |
Updates preserved APIs. |
tests/dotnet/UnitTests/expected/TVOS-CoreCLR-Interpreter-preservedapis.txt |
Updates preserved APIs. |
tests/dotnet/UnitTests/expected/MacOSX-NativeAOT-TrimmableStatic-size.txt |
Updates size baseline. |
tests/dotnet/UnitTests/expected/MacOSX-NativeAOT-size.txt |
Updates size baseline. |
tests/dotnet/UnitTests/expected/MacOSX-CoreCLR-R2R-size.txt |
Updates size baseline. |
tests/dotnet/UnitTests/expected/MacOSX-CoreCLR-Interpreter-TrimmableStatic-size.txt |
Updates size baseline. |
tests/dotnet/UnitTests/expected/MacOSX-CoreCLR-Interpreter-size.txt |
Updates size baseline. |
tests/dotnet/UnitTests/expected/MacCatalyst-MonoVM-preservedapis.txt |
Updates preserved APIs. |
tests/dotnet/UnitTests/expected/MacCatalyst-MonoVM-interpreter-preservedapis.txt |
Updates preserved APIs. |
tests/dotnet/UnitTests/expected/MacCatalyst-CoreCLR-R2R-preservedapis.txt |
Updates preserved APIs. |
tests/dotnet/UnitTests/expected/MacCatalyst-CoreCLR-Interpreter-preservedapis.txt |
Updates preserved APIs. |
tests/dotnet/UnitTests/expected/iOS-NativeAOT-TrimmableStatic-size.txt |
Updates size baseline. |
tests/dotnet/UnitTests/expected/iOS-NativeAOT-size.txt |
Updates size baseline. |
tests/dotnet/UnitTests/expected/iOS-MonoVM-preservedapis.txt |
Updates preserved APIs. |
tests/dotnet/UnitTests/expected/iOS-MonoVM-interpreter-preservedapis.txt |
Updates preserved APIs. |
tests/dotnet/UnitTests/expected/iOS-CoreCLR-R2R-preservedapis.txt |
Updates preserved APIs. |
tests/dotnet/UnitTests/expected/iOS-CoreCLR-Interpreter-preservedapis.txt |
Updates preserved APIs. |
✅ API diff for current PR / commitNET (empty diffs)✅ API diff vs stableNET (empty diffs)ℹ️ Generator diffGenerator Diff: vsdrops (html) vsdrops (raw diff) gist (raw diff) - Please review changes) Pipeline on Agent |
🚀 [CI Build #6552ff6] Test results 🚀Test results✅ All tests passed on VSTS: test results. 🎉 All 269 tests passed 🎉 Tests counts✅ assembly-processing: All 1 tests passed. Html Report (VSDrops) Download macOS tests✅ Tests on macOS Sonoma (14): All 5 tests passed. Html Report (VSDrops) Download Linux Build VerificationPipeline on Agent |

Objective-C initializers may chain to another managed constructor on the same native object. The constructor trampoline previously created a second managed wrapper, which attempted to install another GCHandle and emitted:
Tried to create a managed reference from an object that already has a managed referenceReuse a compatible live wrapper from the native GCHandle ivar instead. Avoid taking a second native reference when the reused wrapper's constructor chain re-enters
NSObject, and reject disposed or finalizer-queue wrappers.Apply the same wrapper-reuse behavior to the managed-static, trimmable-static, and NativeAOT registrar trampolines through a shared generic helper. This also removes the synthetic constructors previously added to user types.
Extend the constructor-chaining regression test to verify that the nested constructor runs on the original wrapper and that the native retain count remains balanced.
Validated with the targeted iOS test using:
managed-static-registrarrelease|nativeaotAll configurations passed without the duplicate managed-reference warning.
🤖 Pull request created by Copilot
Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com
Backport of #26672.