Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1291,6 +1291,11 @@ private static void AddWrapperToReferenceTrackerHandleCache(NativeObjectWrapper
}
}

internal void RemoveWrappersFromCache(IEnumerable<NativeObjectWrapper> wrappers)
{
_rcwCache.RemoveAll(wrappers);
}

private sealed class RcwCache
{
private readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim();
Expand Down Expand Up @@ -1399,24 +1404,47 @@ public void Remove(IntPtr comPointer, NativeObjectWrapper wrapper)
_lock.EnterWriteLock();
try
{
// TryGetOrCreateObjectForComInstanceInternal may have put a new entry into the cache
// in the time between the GC cleared the contents of the GC handle but before the
// NativeObjectWrapper finalizer ran.
// Only remove the entry if the target of the GC handle is the NativeObjectWrapper
// or is null (indicating that the corresponding NativeObjectWrapper has been scheduled for finalization).
if (_cache.TryGetValue(comPointer, out GCHandle cachedRef)
&& (wrapper == cachedRef.Target
|| cachedRef.Target is null))
Remove_Locked(comPointer, wrapper);
}
finally
{
_lock.ExitWriteLock();
}
}

public void RemoveAll(IEnumerable<NativeObjectWrapper> wrappers)
{
_lock.EnterWriteLock();
try
{
foreach (NativeObjectWrapper wrapper in wrappers)
{
_cache.Remove(comPointer);
cachedRef.Free();
Remove_Locked(wrapper.ExternalComObject, wrapper);
}
}
finally
{
_lock.ExitWriteLock();
}
}

private void Remove_Locked(IntPtr comPointer, NativeObjectWrapper wrapper)
{
Debug.Assert(_lock.IsWriteLockHeld);
// This method is used in a scenario where we already have a lock on the cache, so we can skip acquiring the lock again.
// TryGetOrCreateObjectForComInstanceInternal may have put a new entry into the cache
// in the time between the GC cleared the contents of the GC handle but before the
// NativeObjectWrapper finalizer ran.
// Only remove the entry if the target of the GC handle is the NativeObjectWrapper
// or is null (indicating that the corresponding NativeObjectWrapper has been scheduled for finalization).
if (_cache.TryGetValue(comPointer, out GCHandle cachedRef)
&& (wrapper == cachedRef.Target
|| cachedRef.Target is null))
{
_cache.Remove(comPointer);
cachedRef.Free();
}
}
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,8 @@ internal static void ReleaseExternalObjectsFromCurrentThread()

IntPtr contextToken = GetContextToken();

List<object> objects = new List<object>();
List<ReferenceTrackerNativeObjectWrapper> wrappersToRemove = [];
List<object> objects = [];

// Here we aren't part of a GC callback, so other threads can still be running
// who are adding and removing from the collection. This means we can possibly race
Expand All @@ -85,10 +86,19 @@ internal static void ReleaseExternalObjectsFromCurrentThread()
// Separate the wrapper from the tracker runtime prior to
// passing them.
nativeObjectWrapper.DisconnectTracker();

if (nativeObjectWrapper.ComWrappers == GlobalInstanceForTrackerSupport)
{
wrappersToRemove.Add(nativeObjectWrapper);
Comment on lines 88 to +92
Copy link

Copilot AI Mar 19, 2026

Choose a reason for hiding this comment

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

ReleaseExternalObjectsFromCurrentThread calls DisconnectTracker() for all ReferenceTrackerNativeObjectWrapper instances in the current context, regardless of owning ComWrappers, but only removes wrappers from the RCW cache when nativeObjectWrapper.ComWrappers == GlobalInstanceForTrackerSupport. This leaves disconnected wrappers in other ComWrappers instances' RCW caches, so those instances can still return a stale/disconnected wrapper for the same COM address later. Consider removing from each wrapper's owning ComWrappers cache (eg group wrappers by nativeObjectWrapper.ComWrappers and call RemoveWrappersFromCache per instance), or alternatively limit the disconnect/release flow to only wrappers owned by the global tracker-support instance.

Copilot uses AI. Check for mistakes.
}
}
}
}

// Remove the native object wrappers from the cache
// so we don't return released wrappers to the user if the native COM object
// happens to be reused.
GlobalInstanceForTrackerSupport.RemoveWrappersFromCache(wrappersToRemove);
GlobalInstanceForTrackerSupport.ReleaseObjects(objects);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,10 @@ protected override void ReleaseObjects(IEnumerable objects)
Assert.NotNull(o);
}

throw new Exception() { HResult = ReleaseObjectsCallAck };
if (ReturnInvalid)
{
throw new Exception() { HResult = ReleaseObjectsCallAck };
}
}

private unsafe ComInterfaceEntry* ComputeVtablesForTestObject(Test obj, out int count)
Expand Down Expand Up @@ -461,6 +464,34 @@ private static void ValidateNotifyEndOfReferenceTrackingOnThread()
// Trigger the thread lifetime end API and verify the callback occurs.
int hr = MockReferenceTrackerRuntime.Trigger_NotifyEndOfReferenceTrackingOnThread();
Assert.Equal(GlobalComWrappers.ReleaseObjectsCallAck, hr);

// Validate that the RCW cache gets cleared when we call NotifyEndOfReferenceTrackingOnThread
GlobalComWrappers.Instance.ReturnInvalid = false;
IntPtr tracker = MockReferenceTrackerRuntime.CreateTrackerObject();
try
{
object rcw = GlobalComWrappers.Instance.GetOrCreateObjectForComInstance(tracker, CreateObjectFlags.TrackerObject);

// Make sure that we keep the tracker object alive even after we notify end of reference tracking on this thread.
Marshal.AddRef(tracker);

const int S_OK = 0;
Assert.Equal(S_OK, MockReferenceTrackerRuntime.Trigger_NotifyEndOfReferenceTrackingOnThread());

// We should get a new RCW after we've released the reference tracked objects on this thread.
object rcwNew = GlobalComWrappers.Instance.GetOrCreateObjectForComInstance(tracker, CreateObjectFlags.TrackerObject);

Assert.NotSame(rcw, rcwNew);
}
finally
{
if (tracker != IntPtr.Zero)
{
// Release the extra ref we added above and the original ref from CreateTrackerObject.
Marshal.Release(tracker);
Marshal.Release(tracker);
}
}
}
}
}
Expand Down
Loading