|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Threading;
using static System.Runtime.InteropServices.ComWrappers;
namespace System.Runtime.InteropServices
{
internal static partial class TrackerObjectManager
{
internal static volatile IntPtr s_trackerManager;
internal static volatile bool s_hasTrackingStarted;
internal static volatile bool s_isGlobalPeggingOn = true;
internal static DependentHandleList s_referenceCache;
// Used during GC callback
// Indicates if walking the external objects is needed.
// (i.e. Have any IReferenceTracker instances been found?)
public static bool ShouldWalkExternalObjects()
{
return s_trackerManager != IntPtr.Zero;
}
// Used during GC callback
// Called before wrapper is about to be finalized (the same lifetime as short weak handle).
public static void BeforeWrapperFinalized(IntPtr referenceTracker)
{
Debug.Assert(referenceTracker != IntPtr.Zero);
// Notify tracker runtime that we are about to finalize a wrapper
// (same timing as short weak handle) for this object.
// They need this information to disconnect weak refs and stop firing events,
// so that they can avoid resurrecting the object.
IReferenceTracker.DisconnectFromTrackerSource(referenceTracker);
}
// Used during GC callback
// Begin the reference tracking process for external objects.
public static void BeginReferenceTracking()
{
if (!ShouldWalkExternalObjects())
{
return;
}
Debug.Assert(!s_hasTrackingStarted);
Debug.Assert(s_isGlobalPeggingOn);
s_hasTrackingStarted = true;
// Let the tracker runtime know we are about to walk external objects so that
// they can lock their reference cache. Note that the tracker runtime doesn't need to
// unpeg all external objects at this point and they can do the pegging/unpegging.
// in FindTrackerTargetsCompleted.
Debug.Assert(s_trackerManager != IntPtr.Zero);
IReferenceTrackerManager.ReferenceTrackingStarted(s_trackerManager);
// From this point, the tracker runtime decides whether a target
// should be pegged or not as the global pegging flag is now off.
s_isGlobalPeggingOn = false;
// Time to walk the external objects
WalkExternalTrackerObjects();
}
// Used during GC callback
// End the reference tracking process for external object.
public static void EndReferenceTracking()
{
if (!s_hasTrackingStarted || !ShouldWalkExternalObjects())
{
return;
}
// Let the tracker runtime know the external object walk is done and they need to:
// 1. Unpeg all managed object wrappers (mow) if the (mow) needs to be unpegged
// (i.e. when the (mow) is only reachable by other external tracker objects).
// 2. Peg all mows if the mow needs to be pegged (i.e. when the above condition is not true)
// 3. Unlock reference cache when they are done.
Debug.Assert(s_trackerManager != IntPtr.Zero);
IReferenceTrackerManager.ReferenceTrackingCompleted(s_trackerManager);
s_isGlobalPeggingOn = true;
s_hasTrackingStarted = false;
}
public static bool AddReferencePath(object target, object foundReference)
{
return s_referenceCache.AddDependentHandle(target, foundReference);
}
private static bool HasReferenceTrackerManager
=> s_trackerManager != IntPtr.Zero;
private static bool TryRegisterReferenceTrackerManager(IntPtr referenceTrackerManager)
{
return Interlocked.CompareExchange(ref s_trackerManager, referenceTrackerManager, IntPtr.Zero) == IntPtr.Zero;
}
internal static bool IsGlobalPeggingEnabled => s_isGlobalPeggingOn;
private static void RegisterGCCallbacks()
{
unsafe
{
delegate* unmanaged<int, void> gcStartCallback = &GCStartCollection;
delegate* unmanaged<int, void> gcStopCallback = &GCStopCollection;
delegate* unmanaged<int, void> gcAfterMarkCallback = &GCAfterMarkPhase;
if (!RuntimeImports.RhRegisterGcCallout(RuntimeImports.GcRestrictedCalloutKind.StartCollection, (IntPtr)gcStartCallback) ||
!RuntimeImports.RhRegisterGcCallout(RuntimeImports.GcRestrictedCalloutKind.EndCollection, (IntPtr)gcStopCallback) ||
!RuntimeImports.RhRegisterGcCallout(RuntimeImports.GcRestrictedCalloutKind.AfterMarkPhase, (IntPtr)gcAfterMarkCallback))
{
throw new OutOfMemoryException();
}
}
}
// Used during GC callback
[UnmanagedCallersOnly]
private static void GCStartCollection(int condemnedGeneration)
{
if (condemnedGeneration >= 2)
{
s_referenceCache.Reset();
BeginReferenceTracking();
}
}
// Used during GC callback
[UnmanagedCallersOnly]
private static void GCStopCollection(int condemnedGeneration)
{
if (condemnedGeneration >= 2)
{
EndReferenceTracking();
}
}
// Used during GC callback
[UnmanagedCallersOnly]
private static void GCAfterMarkPhase(int condemnedGeneration)
{
DetachNonPromotedObjects();
}
// Used during GC callback
internal static unsafe void WalkExternalTrackerObjects()
{
bool walkFailed = false;
foreach (GCHandle weakNativeObjectWrapperHandle in s_referenceTrackerNativeObjectWrapperCache)
{
ReferenceTrackerNativeObjectWrapper? nativeObjectWrapper = Unsafe.As<ReferenceTrackerNativeObjectWrapper>(weakNativeObjectWrapperHandle.Target);
if (nativeObjectWrapper != null &&
nativeObjectWrapper.TrackerObject != IntPtr.Zero)
{
FindReferenceTargetsCallback.Instance callback = new(nativeObjectWrapper.ProxyHandle);
int hr = IReferenceTracker.FindTrackerTargets(nativeObjectWrapper.TrackerObject, (IntPtr)(void*)&callback);
if (hr < 0)
{
walkFailed = true;
break;
}
}
}
// Report whether walking failed or not.
if (walkFailed)
{
s_isGlobalPeggingOn = true;
}
IReferenceTrackerManager.FindTrackerTargetsCompleted(s_trackerManager, walkFailed);
}
// Used during GC callback
internal static void DetachNonPromotedObjects()
{
foreach (GCHandle weakNativeObjectWrapperHandle in s_referenceTrackerNativeObjectWrapperCache)
{
ReferenceTrackerNativeObjectWrapper? nativeObjectWrapper = Unsafe.As<ReferenceTrackerNativeObjectWrapper>(weakNativeObjectWrapperHandle.Target);
if (nativeObjectWrapper != null &&
nativeObjectWrapper.TrackerObject != IntPtr.Zero &&
!RuntimeImports.RhIsPromoted(nativeObjectWrapper.ProxyHandle.Target))
{
// Notify the wrapper it was not promoted and is being collected.
BeforeWrapperFinalized(nativeObjectWrapper.TrackerObject);
}
}
}
}
// Callback implementation of IFindReferenceTargetsCallback
[EagerStaticClassConstruction]
internal static unsafe class FindReferenceTargetsCallback
{
// Define an on-stack compatible COM instance to avoid allocating
// a temporary instance.
[StructLayout(LayoutKind.Sequential)]
internal ref struct Instance
{
private readonly IntPtr _vtable; // First field is IUnknown based vtable.
public GCHandle RootObject;
public Instance(GCHandle handle)
{
_vtable = (IntPtr)Unsafe.AsPointer(in FindReferenceTargetsCallback.Vftbl);
RootObject = handle;
}
}
#pragma warning disable CS3016
[UnmanagedCallersOnly(CallConvs = [typeof(CallConvMemberFunction)])]
#pragma warning restore CS3016
private static unsafe int IFindReferenceTargetsCallback_QueryInterface(IntPtr pThis, Guid* guid, IntPtr* ppObject)
{
if (*guid == IID_IFindReferenceTargetsCallback || *guid == IID_IUnknown)
{
*ppObject = pThis;
return HResults.S_OK;
}
else
{
return HResults.COR_E_INVALIDCAST;
}
}
#pragma warning disable CS3016
[UnmanagedCallersOnly(CallConvs = [typeof(CallConvMemberFunction)])]
#pragma warning restore CS3016
private static unsafe int IFindReferenceTargetsCallback_FoundTrackerTarget(IntPtr pThis, IntPtr referenceTrackerTarget)
{
if (referenceTrackerTarget == IntPtr.Zero)
{
return HResults.E_POINTER;
}
object sourceObject = ((FindReferenceTargetsCallback.Instance*)pThis)->RootObject.Target!;
if (!TryGetObject(referenceTrackerTarget, out object? targetObject))
{
return HResults.S_FALSE;
}
if (sourceObject == targetObject)
{
return HResults.S_FALSE;
}
// Notify the runtime a reference path was found.
return TrackerObjectManager.AddReferencePath(sourceObject, targetObject) ? HResults.S_OK : HResults.S_FALSE;
}
internal struct ReferenceTargetsVftbl
{
public delegate* unmanaged[MemberFunction]<IntPtr, Guid*, IntPtr*, int> QueryInterface;
public delegate* unmanaged[MemberFunction]<IntPtr, uint> AddRef;
public delegate* unmanaged[MemberFunction]<IntPtr, uint> Release;
public delegate* unmanaged[MemberFunction]<IntPtr, IntPtr, int> FoundTrackerTarget;
}
[FixedAddressValueType]
internal static readonly ReferenceTargetsVftbl Vftbl;
#pragma warning disable CA1810 // Initialize reference type static fields inline
// We want this to be explicitly written out to ensure we match the "pre-inited vtable" pattern.
static FindReferenceTargetsCallback()
#pragma warning restore CA1810 // Initialize reference type static fields inline
{
ComWrappers.GetUntrackedIUnknownImpl(out Vftbl.AddRef, out Vftbl.Release);
Vftbl.QueryInterface = &IFindReferenceTargetsCallback_QueryInterface;
Vftbl.FoundTrackerTarget = &IFindReferenceTargetsCallback_FoundTrackerTarget;
}
}
// This is used during a GC callback so it needs to be free of any managed allocations.
internal unsafe struct DependentHandleList
{
private int _freeIndex; // The next available slot
private int _capacity; // Total numbers of slots available in the list
private IntPtr* _pHandles; // All handles
private int _shrinkHint; // How many times we've consistently seen "hints" that a
// shrink is needed
private const int DefaultCapacity = 100; // Default initial capacity of this list
private const int ShrinkHintThreshold = 10; // The number of hints we've seen before we really
// shrink the list
public bool AddDependentHandle(object target, object dependent)
{
if (_freeIndex >= _capacity)
{
// We need a bigger dependent handle array
if (!Grow())
return false;
}
IntPtr handle = _pHandles[_freeIndex];
if (handle != default)
{
RuntimeImports.RhHandleSet(handle, target);
RuntimeImports.RhHandleSetDependentSecondary(handle, dependent);
}
else
{
_pHandles[_freeIndex] = RuntimeImports.RhpHandleAllocDependent(target, dependent);
if (_pHandles[_freeIndex] == default)
{
return false;
}
}
_freeIndex++;
return true;
}
public bool Reset()
{
// Allocation for the first time
if (_pHandles == null)
{
_capacity = DefaultCapacity;
#if TARGET_WINDOWS
_pHandles = (IntPtr*)Interop.Ucrtbase.calloc((nuint)_capacity, (nuint)sizeof(IntPtr));
#else
_pHandles = (IntPtr*)Interop.Sys.Calloc((nuint)_capacity, (nuint)sizeof(IntPtr));
#endif
return _pHandles != null;
}
// If we are not using half of the handles last time, it is a hint that probably we need to shrink
if (_freeIndex < _capacity / 2 && _capacity > DefaultCapacity)
{
_shrinkHint++;
// Only shrink if we consistently seen such hint more than ShrinkHintThreshold times
if (_shrinkHint > ShrinkHintThreshold)
{
Shrink();
_shrinkHint = 0;
}
}
else
{
// Reset shrink hint and start over the counting
_shrinkHint = 0;
}
// Clear all the handles that were used
for (int index = 0; index < _freeIndex; index++)
{
IntPtr handle = _pHandles[index];
if (handle != default)
{
RuntimeImports.RhHandleSet(handle, null);
RuntimeImports.RhHandleSetDependentSecondary(handle, null);
}
}
_freeIndex = 0;
return true;
}
private bool Shrink()
{
int newCapacity = _capacity / 2;
// Free all handles that will go away
for (int index = newCapacity; index < _capacity; index++)
{
if (_pHandles[index] != default)
{
RuntimeImports.RhHandleFree(_pHandles[index]);
// Assign them back to null in case the reallocation fails
_pHandles[index] = default;
}
}
// Shrink the size of the memory
#if TARGET_WINDOWS
IntPtr* pNewHandles = (IntPtr*)Interop.Ucrtbase.realloc(_pHandles, (nuint)(newCapacity * sizeof(IntPtr)));
#else
IntPtr* pNewHandles = (IntPtr*)Interop.Sys.Realloc(_pHandles, (nuint)(newCapacity * sizeof(IntPtr)));
#endif
if (pNewHandles == null)
return false;
_pHandles = pNewHandles;
_capacity = newCapacity;
return true;
}
private bool Grow()
{
int newCapacity = _capacity * 2;
#if TARGET_WINDOWS
IntPtr* pNewHandles = (IntPtr*)Interop.Ucrtbase.realloc(_pHandles, (nuint)(newCapacity * sizeof(IntPtr)));
#else
IntPtr* pNewHandles = (IntPtr*)Interop.Sys.Realloc(_pHandles, (nuint)(newCapacity * sizeof(IntPtr)));
#endif
if (pNewHandles == null)
return false;
for (int index = _capacity; index < newCapacity; index++)
{
pNewHandles[index] = default;
}
_pHandles = pNewHandles;
_capacity = newCapacity;
return true;
}
}
}
|