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 = $" 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;
}
}
}