Skip to content

Commit 1c0984f

Browse files
authored
Merge branch 'master' into repr
2 parents cf1f1dc + add127a commit 1c0984f

20 files changed

+943
-256
lines changed

AUTHORS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
- ([@civilx64](https://github.com/civilx64))
5757
- ([@GSPP](https://github.com/GSPP))
5858
- ([@omnicognate](https://github.com/omnicognate))
59+
- ([@OneBlue](https://github.com/OneBlue))
5960
- ([@rico-chet](https://github.com/rico-chet))
6061
- ([@rmadsen-ks](https://github.com/rmadsen-ks))
6162
- ([@stonebig](https://github.com/stonebig))

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ This document follows the conventions laid out in [Keep a CHANGELOG][].
77

88
## [unreleased][]
99

10+
- Look for installed Windows 10 sdk's during installation instead of relying on specific versions.
11+
1012
### Added
1113

1214
- Added support for embedding python into dotnet core 2.0 (NetStandard 2.0)
@@ -21,11 +23,13 @@ This document follows the conventions laid out in [Keep a CHANGELOG][].
2123
- Catches exceptions thrown in C# iterators (yield returns) and rethrows them in python ([#475][i475])([#693][p693])
2224
- Implemented GetDynamicMemberNames() for PyObject to allow dynamic object members to be visible in the debugger ([#443][i443])([#690][p690])
2325
- Incorporated reference-style links to issues and pull requests in the CHANGELOG ([#608][i608])
26+
- Added PyObject finalizer support, Python objects referred by C# can be auto collect now ([#692][p692]).
2427
- Added detailed comments about aproaches and dangers to handle multi-app-domains ([#625][p625])
2528
- Python 3.7 support, builds and testing added. Defaults changed from Python 3.6 to 3.7 ([#698][p698])
2629
- Added support for C# types to provide `__repr__` ([#680][p680])
2730

2831
### Changed
32+
- PythonException included C# call stack
2933

3034
- Reattach python exception traceback information (#545)
3135
- 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])

setup.py

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
kits_root = "SOFTWARE\\Microsoft\\Windows Kits\\Installed Roots"
4040
kits_suffix = os.path.join("bin", ARCH)
4141

42-
WIN_SDK_KEYS = (
42+
WIN_SDK_KEYS = [
4343
RegKey(sdk_name="Windows Kit 10.0", key=kits_root,
4444
value_name="KitsRoot10", suffix=os.path.join("bin", "10.0.16299.0", ARCH)),
4545

@@ -69,7 +69,7 @@
6969

7070
RegKey(sdk_name="Windows SDK 6.0A", key=sdks_root.format("6.0A\\WinSDK"),
7171
value_name="InstallationFolder", suffix=""),
72-
)
72+
]
7373

7474
VS_KEYS = (
7575
RegKey(sdk_name="MSBuild 15", key=vs_root.format("15.0"),
@@ -145,6 +145,29 @@ def _update_xlat_devtools():
145145
elif DEVTOOLS == "Mono":
146146
DEVTOOLS = "dotnet"
147147

148+
def _collect_installed_windows_kits_v10(winreg):
149+
"""Adds the installed Windows 10 kits to WIN_SDK_KEYS """
150+
global WIN_SDK_KEYS
151+
installed_kits = []
152+
153+
with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, kits_root, 0, winreg.KEY_READ) as key:
154+
i = 0
155+
while True:
156+
try:
157+
installed_kits.append(winreg.EnumKey(key, i))
158+
i += 1
159+
except WindowsError:
160+
break
161+
162+
def make_reg_key(version):
163+
return RegKey(sdk_name="Windows Kit 10.0", key=kits_root,
164+
value_name="KitsRoot10", suffix=os.path.join("bin", version, ARCH))
165+
166+
WIN_SDK_KEYS += [make_reg_key(e) for e in installed_kits if e.startswith('10.')]
167+
168+
# Make sure this function won't be called again
169+
_collect_installed_windows_kits_v10 = (lambda:None)
170+
148171
class BuildExtPythonnet(build_ext.build_ext):
149172
user_options = build_ext.build_ext.user_options + [
150173
('xplat', None, None)
@@ -367,6 +390,8 @@ def _find_msbuild_tool(self, tool="msbuild.exe", use_windows_sdk=False):
367390
except ImportError: # PY3
368391
import winreg
369392

393+
_collect_installed_windows_kits_v10(winreg)
394+
370395
keys_to_check = WIN_SDK_KEYS if use_windows_sdk else VS_KEYS
371396
hklm = winreg.HKEY_LOCAL_MACHINE
372397
for rkey in keys_to_check:

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)