X Tutup
Skip to content
This repository was archived by the owner on Jul 22, 2023. It is now read-only.

Commit 5db2f27

Browse files
authored
Merge branch 'master' into py37
2 parents 89dff06 + 784190a commit 5db2f27

18 files changed

+799
-64
lines changed

AUTHORS.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
- Alexandre Catarino([@AlexCatarino](https://github.com/AlexCatarino))
1616
- Arvid JB ([@ArvidJB](https://github.com/ArvidJB))
17+
- Benoît Hudson ([@benoithudson](https://github.com/benoithudson))
1718
- Bradley Friedman ([@leith-bartrich](https://github.com/leith-bartrich))
1819
- Callum Noble ([@callumnoble](https://github.com/callumnoble))
1920
- Christian Heimes ([@tiran](https://github.com/tiran))
@@ -22,10 +23,12 @@
2223
- Daniel Fernandez ([@fdanny](https://github.com/fdanny))
2324
- Daniel Santana ([@dgsantana](https://github.com/dgsantana))
2425
- Dave Hirschfeld ([@dhirschfeld](https://github.com/dhirschfeld))
26+
- David Lassonde ([@lassond](https://github.com/lassond))
2527
- David Lechner ([@dlech](https://github.com/dlech))
2628
- Dmitriy Se ([@dmitriyse](https://github.com/dmitriyse))
2729
- He-chien Tsai ([@t3476](https://github.com/t3476))
2830
-   Ivan Cronyn ([@cronan](https://github.com/cronan))
31+
- Jan Krivanek ([@jakrivan](https://github.com/jakrivan))
2932
-   Jeff Reback ([@jreback](https://github.com/jreback))
3033
- Joe Frayne ([@jfrayne](https://github.com/jfrayne))
3134
- John Burnett ([@johnburnett](https://github.com/johnburnett))
@@ -39,6 +42,7 @@
3942
- Sam Winstanley ([@swinstanley](https://github.com/swinstanley))
4043
- Sean Freitag ([@cowboygneox](https://github.com/cowboygneox))
4144
- Serge Weinstock ([@sweinst](https://github.com/sweinst))
45+
- Viktoria Kovescses ([@vkovec](https://github.com/vkovec))
4246
- Ville M. Vainio ([@vivainio](https://github.com/vivainio))
4347
- Virgil Dupras ([@hsoft](https://github.com/hsoft))
4448
- Wenguang Yang ([@yagweb](https://github.com/yagweb))

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,15 @@ This document follows the conventions laid out in [Keep a CHANGELOG][].
1919
- Catches exceptions thrown in C# iterators (yield returns) and rethrows them in python ([#475][i475])([#693][p693])
2020
- Implemented GetDynamicMemberNames() for PyObject to allow dynamic object members to be visible in the debugger ([#443][i443])([#690][p690])
2121
- Incorporated reference-style links to issues and pull requests in the CHANGELOG ([#608][i608])
22+
- Added detailed comments about aproaches and dangers to handle multi-app-domains ([#625][p625])
2223

2324
### Changed
2425

2526
### Fixed
2627

2728
- Fixed Visual Studio 2017 compat ([#434][i434]) for setup.py
29+
- Fixed crashes when integrating pythonnet in Unity3d ([#714][i714]),
30+
related to unloading the Application Domain
2831
- Fixed crash on exit of the Python interpreter if a python class
2932
derived from a .NET class has a `__namespace__` or `__assembly__`
3033
attribute ([#481][i481])
@@ -683,4 +686,5 @@ This document follows the conventions laid out in [Keep a CHANGELOG][].
683686
[p225]: https://github.com/pythonnet/pythonnet/pull/225
684687
[p78]: https://github.com/pythonnet/pythonnet/pull/78
685688
[p163]: https://github.com/pythonnet/pythonnet/pull/163
689+
[p625]: https://github.com/pythonnet/pythonnet/pull/625
686690
[i131]: https://github.com/pythonnet/pythonnet/issues/131

src/embed_tests/Python.EmbeddingTest.15.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
<BaseDefineConstants>XPLAT</BaseDefineConstants>
3030
<DefineConstants>$(DefineConstants);$(CustomDefineConstants);$(BaseDefineConstants);</DefineConstants>
3131
<DefineConstants Condition="'$(TargetFramework)'=='netcoreapp2.0'">$(DefineConstants);NETCOREAPP</DefineConstants>
32+
<DefineConstants Condition="'$(TargetFramework)'=='netstandard2.0'">$(DefineConstants);NETSTANDARD</DefineConstants>
3233
<DefineConstants Condition="'$(BuildingInsideVisualStudio)' == 'true' AND '$(CustomDefineConstants)' != '' AND $(Configuration.Contains('Debug'))">$(DefineConstants);TRACE;DEBUG</DefineConstants>
3334
<FrameworkPathOverride Condition="'$(TargetFramework)'=='net40' AND $(Configuration.Contains('Mono'))">$(NuGetPackageRoot)\microsoft.targetingpack.netframework.v4.5\1.0.1\lib\net45\</FrameworkPathOverride>
3435
</PropertyGroup>

src/embed_tests/Python.EmbeddingTest.csproj

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@
8686
<Compile Include="pyrunstring.cs" />
8787
<Compile Include="TestConverter.cs" />
8888
<Compile Include="TestCustomMarshal.cs" />
89+
<Compile Include="TestDomainReload.cs" />
8990
<Compile Include="TestExample.cs" />
9091
<Compile Include="TestPyAnsiString.cs" />
9192
<Compile Include="TestPyFloat.cs" />
@@ -103,6 +104,7 @@
103104
<Compile Include="TestPyWith.cs" />
104105
<Compile Include="TestRuntime.cs" />
105106
<Compile Include="TestPyScope.cs" />
107+
<Compile Include="TestTypeManager.cs" />
106108
</ItemGroup>
107109
<ItemGroup>
108110
<ProjectReference Include="..\runtime\Python.Runtime.csproj">
@@ -122,4 +124,4 @@
122124
<Copy SourceFiles="$(TargetAssembly)" DestinationFolder="$(PythonBuildDir)" />
123125
<!--Copy SourceFiles="$(TargetAssemblyPdb)" Condition="Exists('$(TargetAssemblyPdb)')" DestinationFolder="$(PythonBuildDir)" /-->
124126
</Target>
125-
</Project>
127+
</Project>
Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
using System;
2+
using System.CodeDom.Compiler;
3+
using System.Reflection;
4+
using NUnit.Framework;
5+
using Python.Runtime;
6+
7+
//
8+
// This test case is disabled on .NET Standard because it doesn't have all the
9+
// APIs we use. We could work around that, but .NET Core doesn't implement
10+
// domain creation, so it's not worth it.
11+
//
12+
// Unfortunately this means no continuous integration testing for this case.
13+
//
14+
#if !NETSTANDARD && !NETCOREAPP
15+
namespace Python.EmbeddingTest
16+
{
17+
class TestDomainReload
18+
{
19+
/// <summary>
20+
/// Test that the python runtime can survive a C# domain reload without crashing.
21+
///
22+
/// At the time this test was written, there was a very annoying
23+
/// seemingly random crash bug when integrating pythonnet into Unity.
24+
///
25+
/// The repro steps that David Lassonde, Viktoria Kovecses and
26+
/// Benoit Hudson eventually worked out:
27+
/// 1. Write a HelloWorld.cs script that uses Python.Runtime to access
28+
/// some C# data from python: C# calls python, which calls C#.
29+
/// 2. Execute the script (e.g. make it a MenuItem and click it).
30+
/// 3. Touch HelloWorld.cs on disk, forcing Unity to recompile scripts.
31+
/// 4. Wait several seconds for Unity to be done recompiling and
32+
/// reloading the C# domain.
33+
/// 5. Make python run the gc (e.g. by calling gc.collect()).
34+
///
35+
/// The reason:
36+
/// A. In step 2, Python.Runtime registers a bunch of new types with
37+
/// their tp_traverse slot pointing to managed code, and allocates
38+
/// some objects of those types.
39+
/// B. In step 4, Unity unloads the C# domain. That frees the managed
40+
/// code. But at the time of the crash investigation, pythonnet
41+
/// leaked the python side of the objects allocated in step 1.
42+
/// C. In step 5, python sees some pythonnet objects in its gc list of
43+
/// potentially-leaked objects. It calls tp_traverse on those objects.
44+
/// But tp_traverse was freed in step 3 => CRASH.
45+
///
46+
/// This test distills what's going on without needing Unity around (we'd see
47+
/// similar behaviour if we were using pythonnet on a .NET web server that did
48+
/// a hot reload).
49+
/// </summary>
50+
[Test]
51+
public static void DomainReloadAndGC()
52+
{
53+
// We're set up to run in the directory that includes the bin directory.
54+
System.IO.Directory.SetCurrentDirectory(AppDomain.CurrentDomain.BaseDirectory);
55+
56+
Assembly pythonRunner1 = BuildAssembly("test1");
57+
RunAssemblyAndUnload(pythonRunner1, "test1");
58+
59+
// Verify that python is not initialized even though we ran it.
60+
Assert.That(Runtime.Runtime.Py_IsInitialized(), Is.Zero);
61+
62+
// This caused a crash because objects allocated in pythonRunner1
63+
// still existed in memory, but the code to do python GC on those
64+
// objects is gone.
65+
Assembly pythonRunner2 = BuildAssembly("test2");
66+
RunAssemblyAndUnload(pythonRunner2, "test2");
67+
}
68+
69+
//
70+
// The code we'll test. All that really matters is
71+
// using GIL { Python.Exec(pyScript); }
72+
// but the rest is useful for debugging.
73+
//
74+
// What matters in the python code is gc.collect and clr.AddReference.
75+
//
76+
// Note that the language version is 2.0, so no $"foo{bar}" syntax.
77+
//
78+
const string TestCode = @"
79+
using Python.Runtime;
80+
using System;
81+
class PythonRunner {
82+
public static void RunPython() {
83+
AppDomain.CurrentDomain.DomainUnload += OnDomainUnload;
84+
string name = AppDomain.CurrentDomain.FriendlyName;
85+
Console.WriteLine(string.Format(""[{0} in .NET] In PythonRunner.RunPython"", name));
86+
using (Py.GIL()) {
87+
try {
88+
var pyScript = string.Format(""import clr\n""
89+
+ ""print('[{0} in python] imported clr')\n""
90+
+ ""clr.AddReference('System')\n""
91+
+ ""print('[{0} in python] allocated a clr object')\n""
92+
+ ""import gc\n""
93+
+ ""gc.collect()\n""
94+
+ ""print('[{0} in python] collected garbage')\n"",
95+
name);
96+
PythonEngine.Exec(pyScript);
97+
} catch(Exception e) {
98+
Console.WriteLine(string.Format(""[{0} in .NET] Caught exception: {1}"", name, e));
99+
}
100+
}
101+
}
102+
static void OnDomainUnload(object sender, EventArgs e) {
103+
System.Console.WriteLine(string.Format(""[{0} in .NET] unloading"", AppDomain.CurrentDomain.FriendlyName));
104+
}
105+
}";
106+
107+
108+
/// <summary>
109+
/// Build an assembly out of the source code above.
110+
///
111+
/// This creates a file <paramref name="assemblyName"/>.dll in order
112+
/// to support the statement "proxy.theAssembly = assembly" below.
113+
/// That statement needs a file, can't run via memory.
114+
/// </summary>
115+
static Assembly BuildAssembly(string assemblyName)
116+
{
117+
var provider = CodeDomProvider.CreateProvider("CSharp");
118+
119+
var compilerparams = new CompilerParameters();
120+
compilerparams.ReferencedAssemblies.Add("Python.Runtime.dll");
121+
compilerparams.GenerateExecutable = false;
122+
compilerparams.GenerateInMemory = false;
123+
compilerparams.IncludeDebugInformation = false;
124+
compilerparams.OutputAssembly = assemblyName;
125+
126+
var results = provider.CompileAssemblyFromSource(compilerparams, TestCode);
127+
if (results.Errors.HasErrors)
128+
{
129+
var errors = new System.Text.StringBuilder("Compiler Errors:\n");
130+
foreach (CompilerError error in results.Errors)
131+
{
132+
errors.AppendFormat("Line {0},{1}\t: {2}\n",
133+
error.Line, error.Column, error.ErrorText);
134+
}
135+
throw new Exception(errors.ToString());
136+
}
137+
else
138+
{
139+
return results.CompiledAssembly;
140+
}
141+
}
142+
143+
/// <summary>
144+
/// This is a magic incantation required to run code in an application
145+
/// domain other than the current one.
146+
/// </summary>
147+
class Proxy : MarshalByRefObject
148+
{
149+
Assembly theAssembly = null;
150+
151+
public void InitAssembly(string assemblyPath)
152+
{
153+
theAssembly = Assembly.LoadFile(System.IO.Path.GetFullPath(assemblyPath));
154+
}
155+
156+
public void RunPython()
157+
{
158+
Console.WriteLine("[Proxy] Entering RunPython");
159+
160+
// Call into the new assembly. Will execute Python code
161+
var pythonrunner = theAssembly.GetType("PythonRunner");
162+
var runPythonMethod = pythonrunner.GetMethod("RunPython");
163+
runPythonMethod.Invoke(null, new object[] { });
164+
165+
Console.WriteLine("[Proxy] Leaving RunPython");
166+
}
167+
}
168+
169+
/// <summary>
170+
/// Create a domain, run the assembly in it (the RunPython function),
171+
/// and unload the domain.
172+
/// </summary>
173+
static void RunAssemblyAndUnload(Assembly assembly, string assemblyName)
174+
{
175+
Console.WriteLine($"[Program.Main] === creating domain for assembly {assembly.FullName}");
176+
177+
// Create the domain. Make sure to set PrivateBinPath to a relative
178+
// path from the CWD (namely, 'bin').
179+
// See https://stackoverflow.com/questions/24760543/createinstanceandunwrap-in-another-domain
180+
var currentDomain = AppDomain.CurrentDomain;
181+
var domainsetup = new AppDomainSetup()
182+
{
183+
ApplicationBase = currentDomain.SetupInformation.ApplicationBase,
184+
ConfigurationFile = currentDomain.SetupInformation.ConfigurationFile,
185+
LoaderOptimization = LoaderOptimization.SingleDomain,
186+
PrivateBinPath = "."
187+
};
188+
var domain = AppDomain.CreateDomain(
189+
$"My Domain {assemblyName}",
190+
currentDomain.Evidence,
191+
domainsetup);
192+
193+
// Create a Proxy object in the new domain, where we want the
194+
// assembly (and Python .NET) to reside
195+
Type type = typeof(Proxy);
196+
System.IO.Directory.SetCurrentDirectory(AppDomain.CurrentDomain.BaseDirectory);
197+
var theProxy = (Proxy)domain.CreateInstanceAndUnwrap(
198+
type.Assembly.FullName,
199+
type.FullName);
200+
201+
// From now on use the Proxy to call into the new assembly
202+
theProxy.InitAssembly(assemblyName);
203+
theProxy.RunPython();
204+
205+
Console.WriteLine($"[Program.Main] Before Domain Unload on {assembly.FullName}");
206+
AppDomain.Unload(domain);
207+
Console.WriteLine($"[Program.Main] After Domain Unload on {assembly.FullName}");
208+
209+
// Validate that the assembly does not exist anymore
210+
try
211+
{
212+
Console.WriteLine($"[Program.Main] The Proxy object is valid ({theProxy}). Unexpected domain unload behavior");
213+
}
214+
catch (Exception)
215+
{
216+
Console.WriteLine("[Program.Main] The Proxy object is not valid anymore, domain unload complete.");
217+
}
218+
}
219+
220+
/// <summary>
221+
/// Resolves the assembly. Why doesn't this just work normally?
222+
/// </summary>
223+
static Assembly ResolveAssembly(object sender, ResolveEventArgs args)
224+
{
225+
var loadedAssemblies = AppDomain.CurrentDomain.GetAssemblies();
226+
227+
foreach (var assembly in loadedAssemblies)
228+
{
229+
if (assembly.FullName == args.Name)
230+
{
231+
return assembly;
232+
}
233+
}
234+
235+
return null;
236+
}
237+
}
238+
}
239+
#endif

src/embed_tests/TestRuntime.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,26 @@ namespace Python.EmbeddingTest
66
{
77
public class TestRuntime
88
{
9+
/// <summary>
10+
/// Test the cache of the information from the platform module.
11+
///
12+
/// Test fails on platforms we haven't implemented yet.
13+
/// </summary>
14+
[Test]
15+
public static void PlatformCache()
16+
{
17+
Runtime.Runtime.Initialize();
18+
19+
Assert.That(Runtime.Runtime.Machine, Is.Not.EqualTo(Runtime.Runtime.MachineType.Other));
20+
Assert.That(!string.IsNullOrEmpty(Runtime.Runtime.MachineName));
21+
22+
Assert.That(Runtime.Runtime.OperatingSystem, Is.Not.EqualTo(Runtime.Runtime.OperatingSystemType.Other));
23+
Assert.That(!string.IsNullOrEmpty(Runtime.Runtime.OperatingSystemName));
24+
25+
// Don't shut down the runtime: if the python engine was initialized
26+
// but not shut down by another test, we'd end up in a bad state.
27+
}
28+
929
[Test]
1030
public static void Py_IsInitializedValue()
1131
{

0 commit comments

Comments
 (0)
X Tutup