using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.ComponentModel; using System.Linq; using System.Threading; using System.Threading.Tasks; namespace Python.Runtime { public class Finalizer { public class CollectArgs : EventArgs { public int ObjectCount { get; set; } } public class ErrorArgs : EventArgs { public ErrorArgs(Exception error) { Error = error ?? throw new ArgumentNullException(nameof(error)); } public bool Handled { get; set; } public Exception Error { get; } } public static readonly Finalizer Instance = new Finalizer(); public event EventHandler? BeforeCollect; public event EventHandler? ErrorHandler; const int DefaultThreshold = 200; [DefaultValue(DefaultThreshold)] public int Threshold { get; set; } = DefaultThreshold; [DefaultValue(true)] public bool Enable { get; set; } = true; private ConcurrentQueue _objQueue = new ConcurrentQueue(); private int _throttled; #region FINALIZER_CHECK #if FINALIZER_CHECK private readonly object _queueLock = new object(); internal bool RefCountValidationEnabled { get; set; } = true; #else internal bool RefCountValidationEnabled { get; set; } = false; #endif // Keep these declarations for compat even no FINALIZER_CHECK internal class IncorrectFinalizeArgs : EventArgs { public IncorrectFinalizeArgs(IntPtr handle, IReadOnlyCollection imacted) { Handle = handle; ImpactedObjects = imacted; } public IntPtr Handle { get; } public BorrowedReference Reference => new(Handle); public IReadOnlyCollection ImpactedObjects { get; } } internal class IncorrectRefCountException : Exception { public IntPtr PyPtr { get; internal set; } string? message; public override string Message { get { if (message is not null) return message; var gil = PythonEngine.AcquireLock(); try { using var pyname = Runtime.PyObject_Str(new BorrowedReference(PyPtr)); string name = Runtime.GetManagedString(pyname.BorrowOrThrow()) ?? Util.BadStr; message = $"<{name}> may has a incorrect ref count"; } finally { PythonEngine.ReleaseLock(gil); } return message; } } internal IncorrectRefCountException(IntPtr ptr) { PyPtr = ptr; } } internal delegate bool IncorrectRefCntHandler(object sender, IncorrectFinalizeArgs e); #pragma warning disable 414 internal event IncorrectRefCntHandler? IncorrectRefCntResolver = null; #pragma warning restore 414 internal bool ThrowIfUnhandleIncorrectRefCount { get; set; } = true; #endregion public void Collect() => this.DisposeAll(); internal void ThrottledCollect() { _throttled = unchecked(this._throttled + 1); if (!Enable || _throttled < Threshold) return; _throttled = 0; this.Collect(); } internal List GetCollectedObjects() { return _objQueue.ToList(); } internal void AddFinalizedObject(ref IntPtr obj) { if (!Enable || obj == IntPtr.Zero) { return; } #if FINALIZER_CHECK lock (_queueLock) #endif { this._objQueue.Enqueue(obj); } obj = IntPtr.Zero; } internal static void Shutdown() { Instance.DisposeAll(); } private void DisposeAll() { BeforeCollect?.Invoke(this, new CollectArgs() { ObjectCount = _objQueue.Count }); #if FINALIZER_CHECK lock (_queueLock) #endif { #if FINALIZER_CHECK ValidateRefCount(); #endif IntPtr obj; Runtime.PyErr_Fetch(out var errType, out var errVal, out var traceback); try { while (!_objQueue.IsEmpty) { if (!_objQueue.TryDequeue(out obj)) continue; IntPtr copyForException = obj; Runtime.XDecref(StolenReference.Take(ref obj)); try { Runtime.CheckExceptionOccurred(); } catch (Exception e) { var errorArgs = new ErrorArgs(e); ErrorHandler?.Invoke(this, errorArgs); if (!errorArgs.Handled) { throw new FinalizationException( "Python object finalization failed", disposable: copyForException, innerException: e); } } } } finally { // Python requires finalizers to preserve exception: // https://docs.python.org/3/extending/newtypes.html#finalization-and-de-allocation Runtime.PyErr_Restore(errType.StealNullable(), errVal.StealNullable(), traceback.StealNullable()); } } } #if FINALIZER_CHECK private void ValidateRefCount() { if (!RefCountValidationEnabled) { return; } var counter = new Dictionary(); var holdRefs = new Dictionary(); var indexer = new Dictionary>(); foreach (var obj in _objQueue) { var handle = obj; if (!counter.ContainsKey(handle)) { counter[handle] = 0; } counter[handle]++; if (!holdRefs.ContainsKey(handle)) { holdRefs[handle] = Runtime.Refcount(handle); } List objs; if (!indexer.TryGetValue(handle, out objs)) { objs = new List(); indexer.Add(handle, objs); } objs.Add(obj); } foreach (var pair in counter) { IntPtr handle = pair.Key; long cnt = pair.Value; // Tracked handle's ref count is larger than the object's holds // it may take an unspecified behaviour if it decref in Dispose if (cnt > holdRefs[handle]) { var args = new IncorrectFinalizeArgs() { Handle = handle, ImpactedObjects = indexer[handle] }; bool handled = false; if (IncorrectRefCntResolver != null) { var funcList = IncorrectRefCntResolver.GetInvocationList(); foreach (IncorrectRefCntHandler func in funcList) { if (func(this, args)) { handled = true; break; } } } if (!handled && ThrowIfUnhandleIncorrectRefCount) { throw new IncorrectRefCountException(handle); } } // Make sure no other references for PyObjects after this method indexer[handle].Clear(); } indexer.Clear(); } #endif } public class FinalizationException : Exception { public IntPtr Handle { get; } /// /// Gets the object, whose finalization failed. /// /// If this function crashes, you can also try , /// which does not attempt to increase the object reference count. /// public PyObject GetObject() => new(new BorrowedReference(this.Handle)); /// /// Gets the object, whose finalization failed without incrementing /// its reference count. This should only ever be called during debugging. /// When the result is disposed or finalized, the program will crash. /// public PyObject DebugGetObject() { IntPtr dangerousNoIncRefCopy = this.Handle; return new(StolenReference.Take(ref dangerousNoIncRefCopy)); } public FinalizationException(string message, IntPtr disposable, Exception innerException) : base(message, innerException) { if (disposable == IntPtr.Zero) throw new ArgumentNullException(nameof(disposable)); this.Handle = disposable; } } }