@@ -38,11 +38,48 @@ struct PendingArgs
3838 private delegate int PendingCall ( IntPtr arg ) ;
3939 private readonly PendingCall _collectAction ;
4040
41- private ConcurrentQueue < IDisposable > _objQueue = new ConcurrentQueue < IDisposable > ( ) ;
41+ private ConcurrentQueue < IPyDisposable > _objQueue = new ConcurrentQueue < IPyDisposable > ( ) ;
4242 private bool _pending = false ;
4343 private readonly object _collectingLock = new object ( ) ;
4444 private IntPtr _pendingArgs ;
4545
46+ #region FINALIZER_CHECK
47+
48+ #if FINALIZER_CHECK
49+ private readonly object _queueLock = new object ( ) ;
50+ public bool RefCountValidationEnabled { get ; set ; } = true ;
51+ #else
52+ public readonly bool RefCountValidationEnabled = false ;
53+ #endif
54+ // Keep these declarations for compat even no FINALIZER_CHECK
55+ public class IncorrectFinalizeArgs : EventArgs
56+ {
57+ public IntPtr Handle { get ; internal set ; }
58+ public ICollection < IPyDisposable > ImpactedObjects { get ; internal set ; }
59+ }
60+
61+ public class IncorrectRefCountException : Exception
62+ {
63+ public IntPtr PyPtr { get ; internal set ; }
64+ private string _message ;
65+ public override string Message => _message ;
66+
67+ public IncorrectRefCountException ( IntPtr ptr )
68+ {
69+ PyPtr = ptr ;
70+ IntPtr pyname = Runtime . PyObject_Unicode ( PyPtr ) ;
71+ string name = Runtime . GetManagedString ( pyname ) ;
72+ Runtime . XDecref ( pyname ) ;
73+ _message = $ "{ name } may has a incorrect ref count";
74+ }
75+ }
76+
77+ public delegate bool IncorrectRefCntHandler ( object sender , IncorrectFinalizeArgs e ) ;
78+ public event IncorrectRefCntHandler IncorrectRefCntResovler ;
79+ public bool ThrowIfUnhandleIncorrectRefCount { get ; set ; } = true ;
80+
81+ #endregion
82+
4683 private Finalizer ( )
4784 {
4885 Enable = true ;
@@ -72,7 +109,7 @@ public List<WeakReference> GetCollectedObjects()
72109 return _objQueue . Select ( T => new WeakReference ( T ) ) . ToList ( ) ;
73110 }
74111
75- internal void AddFinalizedObject ( IDisposable obj )
112+ internal void AddFinalizedObject ( IPyDisposable obj )
76113 {
77114 if ( ! Enable )
78115 {
@@ -84,7 +121,12 @@ internal void AddFinalizedObject(IDisposable obj)
84121 // for avoiding that case, user should call GC.Collect manual before shutdown.
85122 return ;
86123 }
87- _objQueue . Enqueue ( obj ) ;
124+ #if FINALIZER_CHECK
125+ lock ( _queueLock )
126+ #endif
127+ {
128+ _objQueue . Enqueue ( obj ) ;
129+ }
88130 GC . ReRegisterForFinalize ( obj ) ;
89131 if ( _objQueue . Count >= Threshold )
90132 {
@@ -96,7 +138,7 @@ internal static void Shutdown()
96138 {
97139 if ( Runtime . Py_IsInitialized ( ) == 0 )
98140 {
99- Instance . _objQueue = new ConcurrentQueue < IDisposable > ( ) ;
141+ Instance . _objQueue = new ConcurrentQueue < IPyDisposable > ( ) ;
100142 return ;
101143 }
102144 Instance . DisposeAll ( ) ;
@@ -175,21 +217,29 @@ private void DisposeAll()
175217 {
176218 ObjectCount = _objQueue . Count
177219 } ) ;
178- IDisposable obj ;
179- while ( _objQueue . TryDequeue ( out obj ) )
220+ #if FINALIZER_CHECK
221+ lock ( _queueLock )
222+ #endif
180223 {
181- try
182- {
183- obj . Dispose ( ) ;
184- Runtime . CheckExceptionOccurred ( ) ;
185- }
186- catch ( Exception e )
224+ #if FINALIZER_CHECK
225+ ValidateRefCount ( ) ;
226+ #endif
227+ IPyDisposable obj ;
228+ while ( _objQueue . TryDequeue ( out obj ) )
187229 {
188- // We should not bother the main thread
189- ErrorHandler ? . Invoke ( this , new ErrorArgs ( )
230+ try
231+ {
232+ obj . Dispose ( ) ;
233+ Runtime . CheckExceptionOccurred ( ) ;
234+ }
235+ catch ( Exception e )
190236 {
191- Error = e
192- } ) ;
237+ // We should not bother the main thread
238+ ErrorHandler ? . Invoke ( this , new ErrorArgs ( )
239+ {
240+ Error = e
241+ } ) ;
242+ }
193243 }
194244 }
195245 }
@@ -202,5 +252,80 @@ private void ResetPending()
202252 _pendingArgs = IntPtr . Zero ;
203253 }
204254 }
255+
256+ #if FINALIZER_CHECK
257+ private void ValidateRefCount ( )
258+ {
259+ if ( ! RefCountValidationEnabled )
260+ {
261+ return ;
262+ }
263+ var counter = new Dictionary < IntPtr , long > ( ) ;
264+ var holdRefs = new Dictionary < IntPtr , long > ( ) ;
265+ var indexer = new Dictionary < IntPtr , List < IPyDisposable > > ( ) ;
266+ foreach ( var obj in _objQueue )
267+ {
268+ IntPtr [ ] handles = obj . GetTrackedHandles ( ) ;
269+ foreach ( var handle in handles )
270+ {
271+ if ( handle == IntPtr . Zero )
272+ {
273+ continue ;
274+ }
275+ if ( ! counter . ContainsKey ( handle ) )
276+ {
277+ counter [ handle ] = 0 ;
278+ }
279+ counter [ handle ] ++ ;
280+ if ( ! holdRefs . ContainsKey ( handle ) )
281+ {
282+ holdRefs [ handle ] = Runtime . Refcount ( handle ) ;
283+ }
284+ List < IPyDisposable > objs ;
285+ if ( ! indexer . TryGetValue ( handle , out objs ) )
286+ {
287+ objs = new List < IPyDisposable > ( ) ;
288+ indexer . Add ( handle , objs ) ;
289+ }
290+ objs . Add ( obj ) ;
291+ }
292+ }
293+ foreach ( var pair in counter )
294+ {
295+ IntPtr handle = pair . Key ;
296+ long cnt = pair . Value ;
297+ // Tracked handle's ref count is larger than the object's holds
298+ // it may take an unspecified behaviour if it decref in Dispose
299+ if ( cnt > holdRefs [ handle ] )
300+ {
301+ var args = new IncorrectFinalizeArgs ( )
302+ {
303+ Handle = handle ,
304+ ImpactedObjects = indexer [ handle ]
305+ } ;
306+ bool handled = false ;
307+ if ( IncorrectRefCntResovler != null )
308+ {
309+ var funcList = IncorrectRefCntResovler . GetInvocationList ( ) ;
310+ foreach ( IncorrectRefCntHandler func in funcList )
311+ {
312+ if ( func ( this , args ) )
313+ {
314+ handled = true ;
315+ break ;
316+ }
317+ }
318+ }
319+ if ( ! handled && ThrowIfUnhandleIncorrectRefCount )
320+ {
321+ throw new IncorrectRefCountException ( handle ) ;
322+ }
323+ }
324+ // Make sure no other references for PyObjects after this method
325+ indexer [ handle ] . Clear ( ) ;
326+ }
327+ indexer . Clear ( ) ;
328+ }
329+ #endif
205330 }
206331}
0 commit comments