diff --git a/src/embed_tests/TestGILState.cs b/src/embed_tests/TestGILState.cs
index bf6f02dc6..1f454c1b2 100644
--- a/src/embed_tests/TestGILState.cs
+++ b/src/embed_tests/TestGILState.cs
@@ -1,5 +1,7 @@
namespace Python.EmbeddingTest
{
+ using System;
+ using System.Threading;
using NUnit.Framework;
using Python.Runtime;
@@ -18,6 +20,57 @@ public void CanDisposeMultipleTimes()
}
}
+ ///
+ /// The thread's PyThreadState must survive between GIL scopes. Native extensions
+ /// built with pybind11 (e.g. matplotlib >= 3.10 _path/ft2font) cache the pointer
+ /// per OS thread and crash with an access violation if a later scope runs after
+ /// the thread state was deleted. threading.local values live in the thread state
+ /// dictionary, so they survive a second scope only if the thread state did.
+ ///
+ [Test]
+ public void ThreadStateIsPreservedBetweenGILScopes()
+ {
+ var result = 0;
+ Exception error = null;
+ var thread = new Thread(() =>
+ {
+ try
+ {
+ PyModule scope;
+ using (Py.GIL())
+ {
+ scope = Py.CreateScope();
+ scope.Exec("import threading\nlocal = threading.local()\nlocal.value = 42");
+ }
+ using (Py.GIL())
+ using (scope)
+ {
+ using var value = scope.Eval("getattr(local, 'value', -1)");
+ result = value.As();
+ }
+ }
+ catch (Exception e)
+ {
+ error = e;
+ }
+ });
+ // the fixture initializes the engine with the GIL held on this thread:
+ // release it so the worker thread can acquire it
+ var ts = PythonEngine.BeginAllowThreads();
+ try
+ {
+ thread.Start();
+ thread.Join();
+ }
+ finally
+ {
+ PythonEngine.EndAllowThreads(ts);
+ }
+
+ Assert.IsNull(error);
+ Assert.AreEqual(42, result);
+ }
+
[OneTimeSetUp]
public void SetUp()
{
diff --git a/src/perf_tests/Python.PerformanceTests.csproj b/src/perf_tests/Python.PerformanceTests.csproj
index 2655a5c10..6d38f74bc 100644
--- a/src/perf_tests/Python.PerformanceTests.csproj
+++ b/src/perf_tests/Python.PerformanceTests.csproj
@@ -13,7 +13,7 @@
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
compile
@@ -25,7 +25,7 @@
-
+
diff --git a/src/runtime/Properties/AssemblyInfo.cs b/src/runtime/Properties/AssemblyInfo.cs
index 9514b7ab6..ab4fddd1d 100644
--- a/src/runtime/Properties/AssemblyInfo.cs
+++ b/src/runtime/Properties/AssemblyInfo.cs
@@ -4,5 +4,5 @@
[assembly: InternalsVisibleTo("Python.EmbeddingTest, PublicKey=00240000048000009400000006020000002400005253413100040000110000005ffd8f49fb44ab0641b3fd8d55e749f716e6dd901032295db641eb98ee46063cbe0d4a1d121ef0bc2af95f8a7438d7a80a3531316e6b75c2dae92fb05a99f03bf7e0c03980e1c3cfb74ba690aca2f3339ef329313bcc5dccced125a4ffdc4531dcef914602cd5878dc5fbb4d4c73ddfbc133f840231343e013762884d6143189")]
[assembly: InternalsVisibleTo("Python.Test, PublicKey=00240000048000009400000006020000002400005253413100040000110000005ffd8f49fb44ab0641b3fd8d55e749f716e6dd901032295db641eb98ee46063cbe0d4a1d121ef0bc2af95f8a7438d7a80a3531316e6b75c2dae92fb05a99f03bf7e0c03980e1c3cfb74ba690aca2f3339ef329313bcc5dccced125a4ffdc4531dcef914602cd5878dc5fbb4d4c73ddfbc133f840231343e013762884d6143189")]
-[assembly: AssemblyVersion("2.0.61")]
-[assembly: AssemblyFileVersion("2.0.61")]
+[assembly: AssemblyVersion("2.0.62")]
+[assembly: AssemblyFileVersion("2.0.62")]
diff --git a/src/runtime/Py.cs b/src/runtime/Py.cs
index 824cb9d15..65e6999bc 100644
--- a/src/runtime/Py.cs
+++ b/src/runtime/Py.cs
@@ -28,12 +28,28 @@ public void Dispose()
public class GILState : IDisposable
{
+ // Tracks the runtime run for which this thread's PyThreadState has been pinned.
+ // Native extensions built with pybind11 (e.g. matplotlib >= 3.10 _path/ft2font)
+ // cache the PyThreadState pointer per OS thread and reuse it later; if the
+ // outermost PyGILState_Release deletes the thread state, that cached pointer
+ // dangles and the next native GIL acquire crashes with an access violation.
+ // Pinning: one extra, never-released PyGILState_Ensure per thread keeps the
+ // gilstate counter >= 1 so the thread state lives until engine shutdown.
+ [ThreadStatic] private static int _pinnedOnRun;
+
private readonly PyGILState state;
private bool isDisposed;
internal GILState()
{
state = PythonEngine.AcquireLock();
+
+ var run = Runtime.GetRun();
+ if (_pinnedOnRun != run)
+ {
+ _pinnedOnRun = run;
+ Runtime.PyGILState_Ensure();
+ }
}
public virtual void Dispose()
diff --git a/src/runtime/Python.Runtime.csproj b/src/runtime/Python.Runtime.csproj
index 66f746d08..359e42944 100644
--- a/src/runtime/Python.Runtime.csproj
+++ b/src/runtime/Python.Runtime.csproj
@@ -5,7 +5,7 @@
Python.Runtime
Python.Runtime
QuantConnect.pythonnet
- 2.0.61
+ 2.0.62
false
LICENSE
https://github.com/pythonnet/pythonnet