Skip to content

Reduce the GC handle cost of every RCW in ComWrappers - #132040

Open
Sergio0694 wants to merge 9 commits into
dotnet:mainfrom
Sergio0694:dev/comwrappers-skip-resurrection-handle
Open

Reduce the GC handle cost of every RCW in ComWrappers#132040
Sergio0694 wants to merge 9 commits into
dotnet:mainfrom
Sergio0694:dev/comwrappers-skip-resurrection-handle

Conversation

@Sergio0694

@Sergio0694 Sergio0694 commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Motivation

Every RCW that ComWrappers creates allocates two weak GC handles for its proxy object:

_proxyHandle = GCHandle.Alloc(comProxy, GCHandleType.Weak);
_proxyHandleTrackingResurrection = GCHandle.Alloc(comProxy, GCHandleType.WeakTrackResurrection);

The second one exists so that the NativeObjectWrapper releases only after the proxy has actually been collected, because the proxy may still call into the native object while finalizing. Until then other callers, notably the reference tracker runtime, must see the proxy as dead as soon as it is eligible for finalization, which is what the first, plain weak handle provides.

Allocating, clearing and freeing GC handles is a substantial part of the per-RCW cost. In a NativeAOT profile of CsWinRT, HndCreateHandle/TableAllocSingleHandleFromCache are prominent on the allocating thread, and QuickSort/CompareHandlesByFreeOrder — the GC handle table sorting free handles — account for roughly 35% of the finalizer thread. WeakTrackResurrection handles are also more expensive for the GC than plain weak handles, since they are processed in a separate, later phase.

What this changes

1. One handle per RCW instead of two, when the proxy declares no finalizer

The two handles only ever disagree in one window: while the RCW is unreachable but not yet collected. An RCW whose type declares no finalizer never enters that window on its own account, so a single handle can serve both purposes.

The subtlety is that it can still be put in that window by something else: another object's finalizer may hold a reference to the RCW, and resurrect it. So the handle that is kept has to be the one that tracks resurrection — the proxy handle is allocated as WeakTrackResurrection and the separate handle is dropped, rather than the other way around. That keeps the wrapper from tearing down state a resurrected RCW still needs, and reporting such an RCW as alive is also the honest answer, since it may be about to become reachable again.

An RCW that does declare a finalizer reaches that window by itself, and there the two meanings genuinely differ, so it keeps both handles.

Whether the proxy declares a finalizer is read straight off the MethodTable, via a new internal RuntimeHelpers.ObjectHasFinalizer on each runtime — modelled on the existing RuntimeHelpers.ObjectHasComponentSize, which has the same shape and the same "callers are required to keep obj alive" contract.

No public API change, and no opt-in required from callers: this applies automatically to any ComWrappers user whose proxies have no finalizer. That includes CsWinRT, whose WindowsRuntimeObject (the object it passes to GetOrRegisterObjectForComInstance) has no finalizer — the finalizer it needs lives on a separate WindowsRuntimeObjectReference object.

Note that this does not remove a GC cycle for such wrappers: GC.ReRegisterForFinalize would not have fired anyway, since s_nativeObjectWrapperTable keeps the wrapper alive while the proxy is alive. The saving is the handle itself.

2. Use WeakGCHandle<T> for both proxy handles

Both handles always track the proxy object, so a strongly typed weak handle expresses that directly: it allocates through GCHandle.InternalAlloc without revalidating the handle type, and skips the cast when reading the target.

This is safe even though _proxyHandle is mirrored in native code (NativeObjectWrapperObject in interoplibinterface_comwrappers.h, read via GetProxyHandle) and flows through the on-stack COM struct used for reference tracker callbacks (FindReferenceTargetsCallback.Instance.RootObject). GCHandle only alters the stored value for pinned handles:

IntPtr handle = InternalAlloc(value, type);
if (type == GCHandleType.Pinned) { handle |= 1; }
_handle = handle;

so a weak GCHandle and a WeakGCHandle<T> hold bit-identical raw handles, and the layout is unchanged. Native code only ever treats it as an opaque OBJECTHANDLE, so the switch between the two weak handle kinds is invisible there.

The redundant IsAllocated checks around disposal are also dropped, since WeakGCHandle<T>.Dispose already handles a default handle.

Testing

This includes @AustinWise's test from #132058, which covers exactly the case above: an RCW with no finalizer of its own, held and resurrected by another object's finalizer. An earlier revision of this PR dropped the resurrection tracking handle outright, and that test is what showed it to be wrong.

  • ComWrappersTests, ComWrappersTestsBuiltInComDisabled, GcRestrictedCalloutReversePInvoke and WeakReferenceTest all pass against the modified CoreLib, built Checked so the CoreLibBinder layout checks and the Debug.Asserts are live.
  • Verified by reflection that the saving is really in effect: a proxy with no finalizer gets one handle and a proxy that declares one gets two.
  • Verified that RCW cache eviction for a collected proxy is unchanged from main for both proxy shapes, since the cache asks the proxy handle for its target and that handle's kind has changed.

Benchmark results

Measured on a 32-core Windows x64 machine, Release runtime, using the CsWinRT benchmark suite (ProjectedConstructionPerf) against both CsWinRT 2.3.0-prerelease and 3.0.0-preview on the same runtime build. Mean of 2 runs, in µs. main here is c99188c2f97, so it already includes #132033.

CsWinRT 2.x

Benchmark main this PR Δ
ConstructProjectedClassWithInt 1.248 1.095 −12.3%
ConstructFastAbiProjectedClassWithInt 1.217 1.065 −12.5%
ConstructDerivedFastAbiProjectedClassWithInt 1.205 1.069 −11.3%
ConstructProjectedClassWithInterface 1.527 1.391 −8.9%
ConstructManyProjectedClassesWithInt (10k/invoke) 1.243 1.116 −10.2%
ConstructProjectedClassWithString 5.001 4.935 −1.3%

CsWinRT 3.0

Benchmark main this PR Δ
ConstructProjectedClassWithInt 1.195 1.039 −13.1%
ConstructFastAbiProjectedClassWithInt 1.166 1.037 −11.1%
ConstructDerivedFastAbiProjectedClassWithInt 1.140 1.035 −9.2%
ConstructProjectedClassWithInterface 1.413 1.273 −9.9%
ConstructManyProjectedClassesWithInt (10k/invoke) 1.192 1.044 −12.4%
ConstructProjectedClassWithString 5.044 4.934 −2.2%

So roughly 9–13% off object construction on top of what #132033 already landed. ConstructProjectedClassWithString barely moves because it is dominated by HSTRING marshalling.

Collection counts are unchanged within noise, which is expected: this removes a handle rather than an allocation, and the Gen2 reduction previously reported for this work came from #132033, which is now in main.

Note

Parts of this pull request description were generated with GitHub Copilot. All benchmark numbers in it were measured locally.

Copilot AI lite review requested due to automatic review settings August 8, 2026 16:59
@dotnet-policy-service dotnet-policy-service Bot added the community-contribution Indicates that the PR has been added by a community member label Aug 8, 2026
@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.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR reduces per-RCW overhead in ComWrappers by avoiding an extra resurrection-tracking handle when the managed proxy type doesn’t require finalization, and by using strongly-typed WeakGCHandle<T> to streamline weak-handle operations. It also updates reference-tracker plumbing and tests to align with the new handle usage and (as noted in the description) includes the RCW-cache bucketing work it’s stacked on.

Changes:

  • Switch NativeObjectWrapper proxy handles to WeakGCHandle<object> and skip allocating the resurrection-tracking handle when RuntimeHelpers.ObjectHasFinalizer(comProxy) is false.
  • Partition the RCW cache into per-processor buckets and use WeakGCHandle<NativeObjectWrapper> for cached entries.
  • Update tracker/test call sites to use TryGetTarget rather than GCHandle.Target.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
src/tests/Interop/COM/ComWrappers/API/Program.cs Tightens the “no lock around QI” regression test to ensure the nested call targets the same COM instance and asserts completion.
src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/TrackerObjectManager.cs Uses ProxyHandle.TryGetTarget when collecting proxies to release.
src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/ComWrappers.cs Core change: uses WeakGCHandle for proxy handles, conditionally allocates resurrection tracking, and partitions RCW cache into buckets.
src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/RuntimeHelpers.CoreCLR.cs Adds RuntimeHelpers.ObjectHasFinalizer (CoreCLR).
src/coreclr/nativeaot/System.Private.CoreLib/src/System/Runtime/InteropServices/TrackerObjectManager.NativeAot.cs Updates NativeAOT reference-tracker callback plumbing to use WeakGCHandle<object>.
src/coreclr/nativeaot/System.Private.CoreLib/src/System/Runtime/CompilerServices/RuntimeHelpers.NativeAot.cs Adds RuntimeHelpers.ObjectHasFinalizer (NativeAOT).

Copilot AI review requested due to automatic review settings August 8, 2026 17:11
@Sergio0694
Sergio0694 force-pushed the dev/comwrappers-skip-resurrection-handle branch from f61051e to 6ac4779 Compare August 8, 2026 17:11

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.

Suppressed comments (4)

src/coreclr/nativeaot/System.Private.CoreLib/src/System/Runtime/InteropServices/TrackerObjectManager.NativeAot.cs:248

  • RootObject.TryGetTarget can fail for a weak handle. The current code ignores the return value and may pass null into AddReferencePath, which expects non-null object arguments and can throw during a GC callback. Bail out early when the source object is no longer available.
            _ = ((FindReferenceTargetsCallback.Instance*)pThis)->RootObject.TryGetTarget(out object? sourceObject);

            if (!TryGetObject(referenceTrackerTarget, out object? targetObject))
            {
                return HResults.S_FALSE;

src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/ComWrappers.cs:617

  • ComWrappers now calls RuntimeHelpers.ObjectHasFinalizer, but that intrinsic is only added for CoreCLR and NativeAOT in this PR. RuntimeHelpers.Mono.cs does not define it, so Mono builds will fail unless a Mono implementation/stub is added (even if COM interop is unsupported there).
                if (RuntimeHelpers.ObjectHasFinalizer(comProxy))
                {
                    _proxyHandleTrackingResurrection = new WeakGCHandle<object>(comProxy, trackResurrection: true);
                }

                // 'ObjectHasFinalizer' reads the MethodTable, which requires the object to be kept alive
                GC.KeepAlive(comProxy);

src/tests/Interop/COM/ComWrappers/API/Program.cs:1123

  • The worker thread is created as a foreground thread. If Join times out (or the thread deadlocks), the test process can hang even after the assertion fails because the thread will keep the process alive. Mark the thread as background (or otherwise ensure it cannot outlive the test) to prevent hangs on failure paths.
                    Thread thread = new Thread(() =>
                    {
                        // Make sure that ComWrappers isn't locking in GetOrCreateObjectForComInstance
                        // around the QI call by calling it on a different thread from within a QI call to register a new managed wrapper
                        // for a COM object representing "this".

src/coreclr/nativeaot/System.Private.CoreLib/src/System/Runtime/InteropServices/TrackerObjectManager.NativeAot.cs:190

  • DetachNonPromotedObjects currently skips wrappers whose proxy weak handle has already been cleared (i.e., TryGetTarget returns false). Those objects are precisely the ones that are not promoted and should be detached/notified. Preserve the previous behavior by treating a missing target as "not promoted".
                ReferenceTrackerNativeObjectWrapper? nativeObjectWrapper = Unsafe.As<ReferenceTrackerNativeObjectWrapper>(weakNativeObjectWrapperHandle.Target);
                if (nativeObjectWrapper != null &&
                    nativeObjectWrapper.TrackerObject != IntPtr.Zero &&
                    nativeObjectWrapper.ProxyHandle.TryGetTarget(out object? proxyTarget) &&
                    !RuntimeImports.RhIsPromoted(proxyTarget))

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @dotnet/interop-contrib
See info in area-owners.md if you want to be subscribed.

@AaronRobinsonMSFT

Copy link
Copy Markdown
Member

/azp list

@azure-pipelines

Copy link
Copy Markdown
CI/CD Pipelines for this repository:

@AaronRobinsonMSFT

Copy link
Copy Markdown
Member

/azp run runtime-coreclr gcstress0x3-gcstress0xc

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).

@AustinWise

AustinWise commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

If the proxy's type declares no finalizer, it can never observe the native object after it becomes unreachable, and it can never be resurrected.

I don't think this assumption is true. Some other object's finalizer could resurrect the proxy. I have a test in #132058 that passes on main but fails with this exception on this branch due to the NativeObjectWrapper cleaning up its state too early:

System.ArgumentNullException: Value cannot be null. (Parameter 'pUnk')
   at System.ArgumentNullException.Throw(String paramName)
   at System.Runtime.InteropServices.ComWrappers.TryGetComInstance(Object obj, IntPtr& unknown)
   at ComWrappersTests.Program.<ValidateNativeObjectWrapperResurrection>g__AliveAlive|15_1(ComWrappers cw, WeakGCHandle`1 handle, IntPtr unmanagedObj) in src/tests/Interop/COM/ComWrappers/API/Program.cs:line 691
   at ComWrappersTests.Program.ValidateNativeObjectWrapperResurrection() in src/tests/Interop/COM/ComWrappers/API/Program.cs:line 663

Sergio0694 and others added 6 commits August 15, 2026 13:11
Every RCW allocates two weak GC handles for its proxy: a plain weak one, and a second 'WeakTrackResurrection' one that exists only so the NativeObjectWrapper is cleaned up after the proxy's finalizer has run (the proxy's finalizer may access the native object).

If the proxy's type declares no finalizer, it can never observe the native object once it becomes unreachable, and it can never be resurrected, so the second handle would always be cleared at the same time as the first. Detect that case from the MethodTable and skip allocating it. This is worth doing because allocating, clearing and freeing GC handles is a substantial part of the cost of every RCW, and resurrection tracking handles are more expensive for the GC to process than plain weak ones.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The handle always tracks the proxy object, so a strongly typed weak handle expresses that directly: it allocates through GCHandle.InternalAlloc without revalidating the handle type, and skips the cast when reading the target.

Only this handle is converted. The '_proxyHandle' field and the handles in 'GCHandleSet' are mirrored in native code (see 'NativeObjectWrapperObject' in interoplibinterface_comwrappers.h, read via 'GetProxyHandle') and flow through the on-stack COM struct used for reference tracker callbacks, so converting those is a wider change that needs to be validated against the native side.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Both handles tracking the proxy object are now strongly typed weak handles. They allocate through GCHandle.InternalAlloc without revalidating the handle type, and skip the cast when reading the target.

The layout is unchanged, so the native mirror of 'NativeObjectWrapper' still matches: 'GCHandle' only alters the stored value for pinned handles, so a weak handle holds the raw handle in both representations. This also applies to the on-stack COM struct used for reference tracker callbacks, which passes the handle straight through to native code.

The redundant 'IsAllocated' checks around the disposal calls are also dropped, as 'WeakGCHandle<T>.Dispose' already handles a default handle.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Skipping the resurrection tracking handle for an RCW whose type declares
no finalizer was wrong. Such an RCW never becomes eligible for
finalization on its own account, but something else's finalizer can hold
on to it and resurrect it, and then this wrapper would already have torn
down state the RCW still needs.

The two handles only disagree while the RCW is unreachable but not yet
collected. An RCW that declares no finalizer only reaches that state by
way of someone else holding it, and in that case reporting it as alive is
both what keeps this wrapper from releasing too early and the honest
answer, as it may be about to become reachable again. So rather than
dropping the second handle, let the proxy handle track resurrection and
drop the separate one, which costs the same single handle per RCW as
before while behaving correctly.

An RCW that does declare a finalizer does reach that state by itself, and
there the two meanings genuinely differ, so it keeps both handles.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 15, 2026 22:08
@Sergio0694
Sergio0694 force-pushed the dev/comwrappers-skip-resurrection-handle branch from 6ac4779 to 869fe82 Compare August 15, 2026 22:09
@Sergio0694
Sergio0694 marked this pull request as ready for review August 15, 2026 22:09
@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.

@Sergio0694

Copy link
Copy Markdown
Contributor Author

@AustinWise I've ported your new test here and updated the PR to keep the test passing, good find! 😄
I kept the attribution for that commit you did with the new test.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.

Suppressed comments (2)

src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/ComWrappers.cs:624

  • RuntimeHelpers.ObjectHasFinalizer is used here, but this helper is only added in the CoreCLR and NativeAOT RuntimeHelpers partials in this PR. System.Private.CoreLib for Mono imports the shared ComWrappers.cs (via System.Private.CoreLib.Shared.projitems), so Mono builds will fail unless RuntimeHelpers.Mono also provides ObjectHasFinalizer (or this call is guarded appropriately).
                bool proxyHasFinalizer = RuntimeHelpers.ObjectHasFinalizer(comProxy);

                _proxyHandle = new WeakGCHandle<object>(comProxy, trackResurrection: !proxyHasFinalizer);

                if (proxyHasFinalizer)
                {
                    _proxyHandleTrackingResurrection = new WeakGCHandle<object>(comProxy, trackResurrection: true);
                }

                // 'ObjectHasFinalizer' reads the MethodTable, which requires the object to be kept alive
                GC.KeepAlive(comProxy);

src/coreclr/nativeaot/System.Private.CoreLib/src/System/Runtime/InteropServices/TrackerObjectManager.NativeAot.cs:244

  • RootObject.TryGetTarget's return value is ignored, so sourceObject can be null (or TryGetTarget can throw if the handle is uninitialized). That null then flows into AddReferencePath(sourceObject, ...), which expects a non-null object and is reached from an [UnmanagedCallersOnly] entrypoint. This should early-return S_FALSE if the root object isn't available.
            _ = ((FindReferenceTargetsCallback.Instance*)pThis)->RootObject.TryGetTarget(out object? sourceObject);

@AaronRobinsonMSFT

Copy link
Copy Markdown
Member

/azp run runtime-coreclr gcstress0x3-gcstress0xc

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).

- Say that ObjectHasFinalizer covers an inherited finalizer too, rather
  than only one the type declares, which is what the underlying flag
  means on both runtimes.
- Drop AggressiveInlining from it. The JIT inlines it into the wrapper
  constructor either way, so the attribute wasn't buying anything.
- Assert that the reference tracker callback's root object is still set,
  and make the nullability explicit at the point it is passed on.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 15, 2026 23:11

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.

Suppressed comments (3)

src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/RuntimeHelpers.CoreCLR.cs:455

  • RuntimeHelpers.ObjectHasFinalizer is a tiny helper that’s called on a hot path (RCW construction). ObjectHasComponentSize is marked AggressiveInlining, but this new helper isn’t, which makes it more likely to stay as a call and add overhead at scale. Consider applying the same inlining hint for consistency and to keep the per-RCW cost minimal.
        internal static unsafe bool ObjectHasFinalizer(object obj)

src/coreclr/nativeaot/System.Private.CoreLib/src/System/Runtime/CompilerServices/RuntimeHelpers.NativeAot.cs:219

  • RuntimeHelpers.ObjectHasFinalizer is analogous in shape/usage to ObjectHasComponentSize, but unlike that helper it isn’t marked AggressiveInlining. Since this is used during RCW construction, keeping it inlined helps minimize overhead on a hot path and keeps the two helpers consistent.
        internal static unsafe bool ObjectHasFinalizer(object obj)

src/tests/Interop/COM/ComWrappers/API/Program.cs:695

  • resurrecter.UnmanagedWrapper is treated as non-null (it’s passed to TryGetComInstance and then FinalRelease() is called). If it ever becomes null unexpectedly, this will fail with a less-informative NRE/arg-null. Adding an explicit assertion (and using null-forgiving where needed) makes failures clearer and avoids nullable-flow warnings.
                Assert.True(handle.TryGetTarget(out Resurrecter resurrecter));
                ManualReleaseITestObjectWrapper? unmanagedWrapper = resurrecter.UnmanagedWrapper;
                Assert.NotNull(resurrecter);
                Assert.True(ComWrappers.TryGetComInstance(unmanagedWrapper, out IntPtr unmanagedObjOther));
                Assert.Equal(unmanagedObj, unmanagedObjOther);

Reading the MethodTable requires the object to stay alive, and having
every caller remember that is easy to get wrong. Do it in the helper
instead, the way GetMultiDimensionalArrayRank already does, and drop the
call at the one use site.

The generated code for the wrapper constructor is unchanged: same 353
bytes, same 102 instructions, and an identical instruction stream.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 16, 2026 01:26

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.

Suppressed comments (2)

src/tests/Interop/COM/ComWrappers/API/Program.cs:691

  • AssertNativeObjectWrapperAlive treats resurrecter.UnmanagedWrapper as nullable but then uses it without asserting non-null, while also asserting resurrecter non-null redundantly (it’s an out value from TryGetTarget). Capture the wrapper via Assert.NotNull(...) so the intent is explicit and the subsequent calls are safe.
                Assert.True(handle.TryGetTarget(out Resurrecter resurrecter));
                ManualReleaseITestObjectWrapper? unmanagedWrapper = resurrecter.UnmanagedWrapper;
                Assert.NotNull(resurrecter);
                Assert.True(ComWrappers.TryGetComInstance(unmanagedWrapper, out IntPtr unmanagedObjOther));

src/tests/Interop/COM/ComWrappers/API/Program.cs:633

  • Resurrecter is declared using a parameterless primary constructor (class Resurrecter()), which is unusual in this test suite and adds an unnecessary dependency on that syntax. Use a normal class declaration to keep the test style consistent and avoid confusion about constructor semantics.

This issue also appears on line 688 of the same file.

        class Resurrecter()

NativeAOT has no collectible types, so the MethodTable can't go away
while the call is in progress and there is nothing to keep alive. This
matches ObjectHasComponentSize right above it, which doesn't have one
either.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 16, 2026 01:37

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.

Suppressed comments (1)

src/tests/Interop/COM/ComWrappers/API/Program.cs:692

  • Assert.NotNull(resurrecter); is redundant (the local is non-null) and the code doesn’t assert that resurrecter.UnmanagedWrapper is non-null before using it. That weakens the test intent and can turn failures into a NullReferenceException at TryGetComInstance / FinalRelease instead of a clear assertion failure.
                Assert.True(handle.TryGetTarget(out Resurrecter resurrecter));
                ManualReleaseITestObjectWrapper? unmanagedWrapper = resurrecter.UnmanagedWrapper;
                Assert.NotNull(resurrecter);
                Assert.True(ComWrappers.TryGetComInstance(unmanagedWrapper, out IntPtr unmanagedObjOther));
                Assert.Equal(unmanagedObj, unmanagedObjOther);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-Interop-coreclr community-contribution Indicates that the PR has been added by a community member tenet-performance Performance related issue

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants