| File: Windows\Win32\System\Com\AgileComPointer.cs | Web Access |
| Project: src\winforms\src\System.Private.Windows.Core\src\Microsoft.Private.Windows.Core.csproj (Microsoft.Private.Windows.Core) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Windows.Win32.System.Com; namespace Windows.Win32.Foundation; /// <summary> /// Finalizable wrapper for COM pointers that gives agile access to the specified interface. /// </summary> /// <remarks> /// <para> /// This class should be used to hold all COM pointers that are stored as fields to ensure that they are /// safely finalized when needed. Finalization should be avoided whenever possible for performance and timely /// resource release (that is, this class should be disposed). /// </para> /// <para> /// Fields should be nulled out before calling <see cref="Dispose()"/>. Releasing the COM pointer during disposal /// can result in callbacks to containing classes. Rather than evaluate the risk of this for every class, always /// follow this pattern. <see cref="DisposeHelper.NullAndDispose"/> facilitates doing this safely. /// </para> /// </remarks> internal unsafe class AgileComPointer<TInterface> : #if DEBUG DisposalTracking.Tracker, #endif IDisposable where TInterface : unmanaged, IComIID { private uint _cookie; private readonly uint _memoryPressure; /// <summary> /// Creates an <see cref="AgileComPointer{TInterface}"/> for the given <paramref name="interface"/>. /// </summary> /// <param name="interface">The COM interface pointer.</param> /// <param name="takeOwnership"> /// Indicates whether to take ownership of the interface. If `<see langword="true"/>` this object will own the ref count. /// </param> /// <param name="memoryPressure">The amount of memory pressure to add.</param> /// <remarks> /// <para> /// <paramref name="memoryPressure"/> is used to help the GC know that this object is holding onto native memory. /// This is most useful for objects that can be created in large quantities, particularly if they are not disposable. /// </para> /// <para> /// Setting <paramref name="takeOwnership"/> to `<see langword="true"/>` will ensure that this object takes /// responsibility for releasing the COM interface when it is no longer needed. This is done by calling /// IUnknown.Release after the GIT adds a ref to the interface. /// </para> /// </remarks> /// <devdoc> /// Other options were explored for ensuring that pending finalizers are not out of control besides /// <paramref name="memoryPressure"/>. Caching the interface pointers in a static collection was considered, /// but this had no impact on the HtmlElement scenario, which would create a new COM object for every element /// access. Tracking creation counts and forcing the Finalizer to run was also considered, but that takes too /// much responsibility away from the GC- better to let it make the decision based on our additional pressure. /// </devdoc> #if DEBUG public AgileComPointer(TInterface* @interface, bool takeOwnership, uint memoryPressure = 0, bool trackDisposal = true) : base(trackDisposal) #else public AgileComPointer(TInterface* @interface, bool takeOwnership, uint memoryPressure = 0) #endif { _memoryPressure = 0; try { _cookie = GlobalInterfaceTable.RegisterInterface(@interface); if (memoryPressure > 0) { _memoryPressure = memoryPressure; GC.AddMemoryPressure(_memoryPressure); } } catch { // No need to clean if we couldn't register the interface. GC.SuppressFinalize(this); throw; } finally { if (takeOwnership) { // The GIT will add a ref to the given interface, release to effectively give ownership to the GIT. uint count = ((IUnknown*)@interface)->Release(); Debug.Assert(count >= 0); } } } /// <summary> /// Returns <see langword="true"/> if <paramref name="other"/> has the same pointer this /// <see cref="AgileComPointer{TInterface}"/> was created from. /// </summary> public bool IsSameNativeObject(AgileComPointer<TInterface> other) { // There is a chance that this AgileComPointer or the other has a proxy registered to the GIT. // A proxy's value can differ depending on the thread. In order to determine identity between two COM pointers, // both must be registered in GIT (this is already done when initializing an AgileComPointer), // queried for their IUnknowns on the same thread, and then have their values compared. // If two proxies refer to the same native object, querying them for IUnknown // on the same thread will always give the same value. using var currentUnknown = GetInterface<IUnknown>(); using var otherUnknown = other.GetInterface<IUnknown>(); return currentUnknown.Value == otherUnknown.Value; } /// <inheritdoc cref="IsSameNativeObject(AgileComPointer{TInterface})"/> public bool IsSameNativeObject(TInterface* other) { using var currentUnknown = GetInterface<IUnknown>(); using ComScope<IUnknown> otherUnknown = ComScope<IUnknown>.QueryFrom(other); return currentUnknown.Value == otherUnknown.Value; } /// <summary> /// Gets the default interface. Throws if failed. /// </summary> public ComScope<TInterface> GetInterface() { var scope = GlobalInterfaceTable.GetInterface<TInterface>(_cookie, out HRESULT hr); hr.ThrowOnFailure(); return scope; } /// <summary> /// Gets the specified interface. Throws if failed. /// </summary> public ComScope<TAsInterface> GetInterface<TAsInterface>() where TAsInterface : unmanaged, IComIID { var scope = TryGetInterface<TAsInterface>(out HRESULT hr); hr.ThrowOnFailure(); return scope; } /// <summary> /// Tries to get the default interface. /// </summary> public ComScope<TInterface> TryGetInterface(out HRESULT hr) => GlobalInterfaceTable.GetInterface<TInterface>(_cookie, out hr); /// <summary> /// Tries to get the specified interface. /// </summary> public ComScope<TAsInterface> TryGetInterface<TAsInterface>(out HRESULT hr) where TAsInterface : unmanaged, IComIID { var scope = GlobalInterfaceTable.GetInterface<TAsInterface>(_cookie, out hr); return scope; } /// <summary> /// Gets the managed object using the pointer /// this <see cref="AgileComPointer{TInterface}"/> was created from. /// </summary> public object GetManagedObject() { using var scope = GetInterface(); return ComHelpers.GetObjectForIUnknown(scope); } ~AgileComPointer() { Dispose(disposing: false); } public void Dispose() { Dispose(disposing: true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { // Clear the cookie before revoking the interface to guard against re-entry. uint cookie = Interlocked.Exchange(ref _cookie, 0); if (cookie == 0) { return; } if (_memoryPressure > 0) { GC.RemoveMemoryPressure(_memoryPressure); } HRESULT hr = GlobalInterfaceTable.RevokeInterface(cookie); if (disposing) { // Don't assert from the finalizer thread. Debug.Assert(hr.Succeeded); } } }