Skip to content

Commit f83c884

Browse files
authored
Merge pull request pythonnet#692 from amos402/pyobject-finalizer
PyObject finalizer
2 parents 5ee234a + 4eff81e commit f83c884

17 files changed

+903
-246
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,12 @@ This document follows the conventions laid out in [Keep a CHANGELOG][].
2121
- Catches exceptions thrown in C# iterators (yield returns) and rethrows them in python ([#475][i475])([#693][p693])
2222
- Implemented GetDynamicMemberNames() for PyObject to allow dynamic object members to be visible in the debugger ([#443][i443])([#690][p690])
2323
- Incorporated reference-style links to issues and pull requests in the CHANGELOG ([#608][i608])
24+
- Added PyObject finalizer support, Python objects referred by C# can be auto collect now ([#692][p692]).
2425
- Added detailed comments about aproaches and dangers to handle multi-app-domains ([#625][p625])
2526
- Python 3.7 support, builds and testing added. Defaults changed from Python 3.6 to 3.7 ([#698][p698])
2627

2728
### Changed
29+
- PythonException included C# call stack
2830

2931
- Reattach python exception traceback information (#545)
3032
- PythonEngine.Intialize will now call `Py_InitializeEx` with a default value of 0, so signals will not be configured by default on embedding. This is different from the previous behaviour, where `Py_Initialize` was called instead, which sets initSigs to 1. ([#449][i449])

src/embed_tests/Python.EmbeddingTest.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@
8888
<Compile Include="TestCustomMarshal.cs" />
8989
<Compile Include="TestDomainReload.cs" />
9090
<Compile Include="TestExample.cs" />
91+
<Compile Include="TestFinalizer.cs" />
9192
<Compile Include="TestPyAnsiString.cs" />
9293
<Compile Include="TestPyFloat.cs" />
9394
<Compile Include="TestPyInt.cs" />

src/embed_tests/TestFinalizer.cs

Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
using NUnit.Framework;
2+
using Python.Runtime;
3+
using System;
4+
using System.Linq;
5+
using System.Threading;
6+
7+
namespace Python.EmbeddingTest
8+
{
9+
public class TestFinalizer
10+
{
11+
private int _oldThreshold;
12+
13+
[SetUp]
14+
public void SetUp()
15+
{
16+
_oldThreshold = Finalizer.Instance.Threshold;
17+
PythonEngine.Initialize();
18+
Exceptions.Clear();
19+
}
20+
21+
[TearDown]
22+
public void TearDown()
23+
{
24+
Finalizer.Instance.Threshold = _oldThreshold;
25+
PythonEngine.Shutdown();
26+
}
27+
28+
private static void FullGCCollect()
29+
{
30+
GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced);
31+
GC.WaitForPendingFinalizers();
32+
}
33+
34+
[Test]
35+
public void CollectBasicObject()
36+
{
37+
Assert.IsTrue(Finalizer.Instance.Enable);
38+
39+
int thId = Thread.CurrentThread.ManagedThreadId;
40+
Finalizer.Instance.Threshold = 1;
41+
bool called = false;
42+
EventHandler<Finalizer.CollectArgs> handler = (s, e) =>
43+
{
44+
Assert.AreEqual(thId, Thread.CurrentThread.ManagedThreadId);
45+
Assert.GreaterOrEqual(e.ObjectCount, 1);
46+
called = true;
47+
};
48+
49+
WeakReference shortWeak;
50+
WeakReference longWeak;
51+
{
52+
MakeAGarbage(out shortWeak, out longWeak);
53+
}
54+
FullGCCollect();
55+
// The object has been resurrected
56+
Assert.IsFalse(shortWeak.IsAlive);
57+
Assert.IsTrue(longWeak.IsAlive);
58+
59+
{
60+
var garbage = Finalizer.Instance.GetCollectedObjects();
61+
Assert.NotZero(garbage.Count);
62+
Assert.IsTrue(garbage.Any(T => ReferenceEquals(T.Target, longWeak.Target)));
63+
}
64+
65+
Assert.IsFalse(called);
66+
Finalizer.Instance.CollectOnce += handler;
67+
try
68+
{
69+
Finalizer.Instance.CallPendingFinalizers();
70+
}
71+
finally
72+
{
73+
Finalizer.Instance.CollectOnce -= handler;
74+
}
75+
Assert.IsTrue(called);
76+
}
77+
78+
private static void MakeAGarbage(out WeakReference shortWeak, out WeakReference longWeak)
79+
{
80+
PyLong obj = new PyLong(1024);
81+
shortWeak = new WeakReference(obj);
82+
longWeak = new WeakReference(obj, true);
83+
obj = null;
84+
}
85+
86+
private static long CompareWithFinalizerOn(PyObject pyCollect, bool enbale)
87+
{
88+
// Must larger than 512 bytes make sure Python use
89+
string str = new string('1', 1024);
90+
Finalizer.Instance.Enable = true;
91+
FullGCCollect();
92+
FullGCCollect();
93+
pyCollect.Invoke();
94+
Finalizer.Instance.Collect();
95+
Finalizer.Instance.Enable = enbale;
96+
97+
// Estimate unmanaged memory size
98+
long before = Environment.WorkingSet - GC.GetTotalMemory(true);
99+
for (int i = 0; i < 10000; i++)
100+
{
101+
// Memory will leak when disable Finalizer
102+
new PyString(str);
103+
}
104+
FullGCCollect();
105+
FullGCCollect();
106+
pyCollect.Invoke();
107+
if (enbale)
108+
{
109+
Finalizer.Instance.Collect();
110+
}
111+
112+
FullGCCollect();
113+
FullGCCollect();
114+
long after = Environment.WorkingSet - GC.GetTotalMemory(true);
115+
return after - before;
116+
117+
}
118+
119+
/// <summary>
120+
/// Because of two vms both have their memory manager,
121+
/// this test only prove the finalizer has take effect.
122+
/// </summary>
123+
[Test]
124+
[Ignore("Too many uncertainties, only manual on when debugging")]
125+
public void SimpleTestMemory()
126+
{
127+
bool oldState = Finalizer.Instance.Enable;
128+
try
129+
{
130+
using (PyObject gcModule = PythonEngine.ImportModule("gc"))
131+
using (PyObject pyCollect = gcModule.GetAttr("collect"))
132+
{
133+
long span1 = CompareWithFinalizerOn(pyCollect, false);
134+
long span2 = CompareWithFinalizerOn(pyCollect, true);
135+
Assert.Less(span2, span1);
136+
}
137+
}
138+
finally
139+
{
140+
Finalizer.Instance.Enable = oldState;
141+
}
142+
}
143+
144+
class MyPyObject : PyObject
145+
{
146+
public MyPyObject(IntPtr op) : base(op)
147+
{
148+
}
149+
150+
protected override void Dispose(bool disposing)
151+
{
152+
base.Dispose(disposing);
153+
GC.SuppressFinalize(this);
154+
throw new Exception("MyPyObject");
155+
}
156+
internal static void CreateMyPyObject(IntPtr op)
157+
{
158+
Runtime.Runtime.XIncref(op);
159+
new MyPyObject(op);
160+
}
161+
}
162+
163+
[Test]
164+
public void ErrorHandling()
165+
{
166+
bool called = false;
167+
EventHandler<Finalizer.ErrorArgs> handleFunc = (sender, args) =>
168+
{
169+
called = true;
170+
Assert.AreEqual(args.Error.Message, "MyPyObject");
171+
};
172+
Finalizer.Instance.Threshold = 1;
173+
Finalizer.Instance.ErrorHandler += handleFunc;
174+
try
175+
{
176+
WeakReference shortWeak;
177+
WeakReference longWeak;
178+
{
179+
MakeAGarbage(out shortWeak, out longWeak);
180+
var obj = (PyLong)longWeak.Target;
181+
IntPtr handle = obj.Handle;
182+
shortWeak = null;
183+
longWeak = null;
184+
MyPyObject.CreateMyPyObject(handle);
185+
obj.Dispose();
186+
obj = null;
187+
}
188+
FullGCCollect();
189+
Finalizer.Instance.Collect();
190+
Assert.IsTrue(called);
191+
}
192+
finally
193+
{
194+
Finalizer.Instance.ErrorHandler -= handleFunc;
195+
}
196+
}
197+
198+
[Test]
199+
public void ValidateRefCount()
200+
{
201+
if (!Finalizer.Instance.RefCountValidationEnabled)
202+
{
203+
Assert.Pass("Only run with FINALIZER_CHECK");
204+
}
205+
IntPtr ptr = IntPtr.Zero;
206+
bool called = false;
207+
Finalizer.IncorrectRefCntHandler handler = (s, e) =>
208+
{
209+
called = true;
210+
Assert.AreEqual(ptr, e.Handle);
211+
Assert.AreEqual(2, e.ImpactedObjects.Count);
212+
// Fix for this test, don't do this on general environment
213+
Runtime.Runtime.XIncref(e.Handle);
214+
return false;
215+
};
216+
Finalizer.Instance.IncorrectRefCntResolver += handler;
217+
try
218+
{
219+
ptr = CreateStringGarbage();
220+
FullGCCollect();
221+
Assert.Throws<Finalizer.IncorrectRefCountException>(() => Finalizer.Instance.Collect());
222+
Assert.IsTrue(called);
223+
}
224+
finally
225+
{
226+
Finalizer.Instance.IncorrectRefCntResolver -= handler;
227+
}
228+
}
229+
230+
private static IntPtr CreateStringGarbage()
231+
{
232+
PyString s1 = new PyString("test_string");
233+
// s2 steal a reference from s1
234+
PyString s2 = new PyString(s1.Handle);
235+
return s1.Handle;
236+
}
237+
238+
}
239+
}

src/embed_tests/TestPyAnsiString.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ public void TestCtorPtr()
6363
const string expected = "foo";
6464

6565
var t = new PyAnsiString(expected);
66+
Runtime.Runtime.XIncref(t.Handle);
6667
var actual = new PyAnsiString(t.Handle);
6768

6869
Assert.AreEqual(expected, actual.ToString());

src/embed_tests/TestPyFloat.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ public void Dispose()
2525
public void IntPtrCtor()
2626
{
2727
var i = new PyFloat(1);
28+
Runtime.Runtime.XIncref(i.Handle);
2829
var ii = new PyFloat(i.Handle);
2930
Assert.AreEqual(i.Handle, ii.Handle);
3031
}

src/embed_tests/TestPyInt.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ public void TestCtorSByte()
8686
public void TestCtorPtr()
8787
{
8888
var i = new PyInt(5);
89+
Runtime.Runtime.XIncref(i.Handle);
8990
var a = new PyInt(i.Handle);
9091
Assert.AreEqual(5, a.ToInt32());
9192
}
@@ -94,6 +95,7 @@ public void TestCtorPtr()
9495
public void TestCtorPyObject()
9596
{
9697
var i = new PyInt(5);
98+
Runtime.Runtime.XIncref(i.Handle);
9799
var a = new PyInt(i);
98100
Assert.AreEqual(5, a.ToInt32());
99101
}

src/embed_tests/TestPyLong.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ public void TestCtorDouble()
102102
public void TestCtorPtr()
103103
{
104104
var i = new PyLong(5);
105+
Runtime.Runtime.XIncref(i.Handle);
105106
var a = new PyLong(i.Handle);
106107
Assert.AreEqual(5, a.ToInt32());
107108
}
@@ -110,6 +111,7 @@ public void TestCtorPtr()
110111
public void TestCtorPyObject()
111112
{
112113
var i = new PyLong(5);
114+
Runtime.Runtime.XIncref(i.Handle);
113115
var a = new PyLong(i);
114116
Assert.AreEqual(5, a.ToInt32());
115117
}

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy