Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 16 additions & 3 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ jobs:
instance: macos-15
suffix: -macos-aarch64-none

python: ["3.10", "3.11", "3.12", "3.13", "3.14"]
python: ["3.10", "3.11", "3.12", "3.13", "3.14", "3.14t"]

exclude:
# Fails with initfs_encoding error
Expand All @@ -67,6 +67,17 @@ jobs:
platform: x86
python: '3.13'

# Free-threaded Python on Windows is not yet supported by pythonnet's
# native build chain; restrict 3.14t to Linux and macOS for now.
- os:
category: windows
platform: x86
python: '3.14t'
- os:
category: windows
platform: x64
python: '3.14t'

env:
NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages
steps:
Expand Down Expand Up @@ -99,7 +110,8 @@ jobs:
run: uv sync --managed-python

- name: Embedding tests (Mono/.NET Framework)
if: always()
# Mono does not yet support free-threaded Python.
if: ${{ always() && matrix.python != '3.14t' }}
run: dotnet test --runtime any-${{ matrix.os.platform }} --framework net472 --logger "console;verbosity=detailed" src/embed_tests/
env:
MONO_THREADS_SUSPEND: preemptive # https://github.com/mono/mono/issues/21466
Expand All @@ -113,7 +125,8 @@ jobs:
run: pytest --runtime coreclr

- name: Python Tests (Mono)
if: always()
# Mono does not yet support free-threaded Python.
if: ${{ always() && matrix.python != '3.14t' }}
run: pytest --runtime mono

- name: Python Tests (.NET Framework)
Expand Down
12 changes: 9 additions & 3 deletions src/runtime/ClassManager.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
Expand Down Expand Up @@ -33,7 +34,12 @@ internal class ClassManager
BindingFlags.Public |
BindingFlags.NonPublic;

internal static Dictionary<MaybeType, ReflectedClrType> cache = new(capacity: 128);
// cache: fully-initialised types (lock-free reads).
// _inProgressCache: partial types visible only to the lock-holding builder
// for self-referential definitions.
internal static ConcurrentDictionary<MaybeType, ReflectedClrType> cache = new();
internal static readonly ConcurrentDictionary<MaybeType, ReflectedClrType> _inProgressCache = new();
internal static readonly object _cacheCreateLock = new();
private static readonly Type dtype;

private ClassManager()
Expand Down Expand Up @@ -103,13 +109,13 @@ internal static ClassManagerState SaveRuntimeData()
return new()
{
Contexts = contexts,
Cache = cache,
Cache = new Dictionary<MaybeType, ReflectedClrType>(cache),
};
}

internal static void RestoreRuntimeData(ClassManagerState storage)
{
cache = storage.Cache;
cache = new ConcurrentDictionary<MaybeType, ReflectedClrType>(storage.Cache);
var invalidClasses = new List<KeyValuePair<MaybeType, ReflectedClrType>>();
var contexts = storage.Contexts;
foreach (var pair in cache)
Expand Down
16 changes: 7 additions & 9 deletions src/runtime/DelegateManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ namespace Python.Runtime
internal class DelegateManager
{
private readonly Dictionary<Type,Type> cache = new();
// Reflection.Emit is not thread-safe; serialise cache lookup + DefineType.
private readonly object _emitLock = new();
private readonly Type basetype = typeof(Dispatcher);
private readonly Type arrayType = typeof(object[]);
private readonly Type voidtype = typeof(void);
Expand All @@ -37,18 +39,14 @@ public DelegateManager()
/// </summary>
private Type GetDispatcher(Type dtype)
{
// If a dispatcher type for the given delegate type has already
// been generated, get it from the cache. The cache maps delegate
// types to generated dispatcher types. A possible optimization
// for the future would be to generate dispatcher types based on
// unique signatures rather than delegate types, since multiple
// delegate types with the same sig could use the same dispatcher.

if (cache.TryGetValue(dtype, out Type item))
lock (_emitLock)
{
return item;
return cache.TryGetValue(dtype, out Type item) ? item : BuildDispatcher(dtype);
}
}

private Type BuildDispatcher(Type dtype)
{
string name = $"__{dtype.FullName}Dispatcher";
name = name.Replace('.', '_');
name = name.Replace('+', '_');
Expand Down
16 changes: 11 additions & 5 deletions src/runtime/Finalizer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ public ErrorArgs(Exception error)
[DefaultValue(DefaultThreshold)]
public int Threshold { get; set; } = DefaultThreshold;

bool started;
// volatile: ThrottledCollect on PyObject ctor races with Initialize.
volatile bool started;

[DefaultValue(true)]
public bool Enable { get; set; } = true;
Expand Down Expand Up @@ -113,9 +114,10 @@ internal void ThrottledCollect()
{
if (!started) throw new InvalidOperationException($"{nameof(PythonEngine)} is not initialized");

_throttled = unchecked(this._throttled + 1);
if (!started || !Enable || _throttled < Threshold) return;
_throttled = 0;
if (!Enable || Interlocked.Increment(ref _throttled) < Threshold) return;
// Stale pointers on the queue would crash Py_DecRef during teardown.
if (Runtime._Py_IsFinalizing() == true) return;
Interlocked.Exchange(ref _throttled, 0);
this.Collect();
}

Expand All @@ -136,7 +138,11 @@ internal void AddFinalizedObject(ref IntPtr obj, int run
return;
}

Debug.Assert(Runtime.Refcount(new BorrowedReference(obj)) > 0);
// Skip on FT: stale ob_ref_local read from the finalizer thread can crash.
if (!Native.ABI.IsFreeThreaded)
{
Debug.Assert(Runtime.Refcount(new BorrowedReference(obj)) > 0);
}

#if FINALIZER_CHECK
lock (_queueLock)
Expand Down
12 changes: 8 additions & 4 deletions src/runtime/InternString.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
Expand All @@ -8,8 +9,8 @@ namespace Python.Runtime
{
static partial class InternString
{
private static readonly Dictionary<string, PyString> _string2interns = new();
private static readonly Dictionary<IntPtr, string> _intern2strings = new();
private static readonly ConcurrentDictionary<string, PyString> _string2interns = new();
private static readonly ConcurrentDictionary<IntPtr, string> _intern2strings = new();
const BindingFlags PyIdentifierFieldFlags = BindingFlags.Static | BindingFlags.NonPublic;

static InternString()
Expand Down Expand Up @@ -75,8 +76,11 @@ public static bool TryGetInterned(BorrowedReference op, out string s)

private static void SetIntern(string s, PyString op)
{
_string2interns.Add(s, op);
_intern2strings.Add(op.Reference.DangerousGetAddress(), s);
// Initialize is single-threaded; TryAdd preserves the original
// single-write invariant via Debug.Assert without crashing release.
bool a = _string2interns.TryAdd(s, op);
bool b = _intern2strings.TryAdd(op.Reference.DangerousGetAddress(), s);
Debug.Assert(a && b);
}
}
}
6 changes: 4 additions & 2 deletions src/runtime/Interop.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
Expand Down Expand Up @@ -104,7 +105,8 @@ public enum TypeFlags: long

internal class Interop
{
static readonly Dictionary<MethodInfo, Type> delegateTypes = new();
// Concurrent: type-slot installation can race past TryGetValue.
static readonly ConcurrentDictionary<MethodInfo, Type> delegateTypes = new();

internal static Type GetPrototype(MethodInfo method)
{
Expand All @@ -131,7 +133,7 @@ internal static Type GetPrototype(MethodInfo method)

if (invoke.ReturnType != method.ReturnType) continue;

delegateTypes.Add(method, candidate);
delegateTypes.TryAdd(method, candidate);
return candidate;
}

Expand Down
28 changes: 25 additions & 3 deletions src/runtime/Native/ABI.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,20 @@ namespace Python.Runtime.Native

static class ABI
{
public static int RefCountOffset { get; } = GetRefCountOffset();
public static int ObjectHeadOffset => RefCountOffset;
// GIL builds only. FT splits the refcount; Refcount uses Py_REFCNT.
public static int RefCountOffset { get; private set; }

// Added to generated TypeOffsets. FT PyObject_HEAD is 16 bytes larger.
public static int ObjectHeadOffset { get; private set; }

public static bool IsFreeThreaded { get; private set; }

internal static void Initialize(Version version)
{
IsFreeThreaded = DetectFreeThreaded();
RefCountOffset = IsFreeThreaded ? -1 : ProbeRefCountOffset();
ObjectHeadOffset = IsFreeThreaded ? 16 : RefCountOffset;

string offsetsClassSuffix = string.Format(CultureInfo.InvariantCulture,
"{0}{1}", version.Major, version.Minor);

Expand All @@ -34,7 +43,20 @@ internal static void Initialize(Version version)
TypeOffset.Use(typeOffsets, nativeOffsetsClass == null ? ObjectHeadOffset : 0);
}

static unsafe int GetRefCountOffset()
static bool DetectFreeThreaded()
{
// sys._is_gil_enabled() was added in Python 3.13; absent means GIL build.
using var sys = Runtime.PyImport_ImportModule("sys");
if (sys.IsNull()) { Runtime.PyErr_Clear(); return false; }
using var func = Runtime.PyObject_GetAttrString(sys.Borrow(), "_is_gil_enabled");
if (func.IsNull()) { Runtime.PyErr_Clear(); return false; }
using var args = Runtime.PyTuple_New(0);
using var result = Runtime.PyObject_Call(func.Borrow(), args.Borrow(), default);
if (result.IsNull()) { Runtime.PyErr_Clear(); return false; }
return Runtime.PyObject_IsTrue(result.Borrow()) == 0;
}

static unsafe int ProbeRefCountOffset()
{
using var tempObject = Runtime.PyList_New(0);
IntPtr* tempPtr = (IntPtr*)tempObject.DangerousGetAddress();
Expand Down
31 changes: 22 additions & 9 deletions src/runtime/PythonEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ namespace Python.Runtime
public class PythonEngine : IDisposable
{
private static DelegateManager? delegateManager;
private static bool initialized;
// volatile: read from worker threads, written from Initialize/Shutdown.
private static volatile bool initialized;
private static IntPtr _pythonHome = IntPtr.Zero;
private static IntPtr _programName = IntPtr.Zero;
private static IntPtr _pythonPath = IntPtr.Zero;
Expand Down Expand Up @@ -405,7 +406,9 @@ public static void Shutdown()
/// </summary>
public delegate void ShutdownHandler();

// Lock: ConcurrentStack lacks remove-by-equality; List<> needs serialised mutation.
static readonly List<ShutdownHandler> ShutdownHandlers = new();
static readonly object _shutdownHandlersLock = new();

/// <summary>
/// Add a function to be called when the engine is shut down.
Expand All @@ -422,7 +425,7 @@ public static void Shutdown()
/// </summary>
public static void AddShutdownHandler(ShutdownHandler handler)
{
ShutdownHandlers.Add(handler);
lock (_shutdownHandlersLock) ShutdownHandlers.Add(handler);
}

/// <summary>
Expand All @@ -435,12 +438,15 @@ public static void AddShutdownHandler(ShutdownHandler handler)
/// </summary>
public static void RemoveShutdownHandler(ShutdownHandler handler)
{
for (int index = ShutdownHandlers.Count - 1; index >= 0; --index)
lock (_shutdownHandlersLock)
{
if (ShutdownHandlers[index] == handler)
for (int index = ShutdownHandlers.Count - 1; index >= 0; --index)
{
ShutdownHandlers.RemoveAt(index);
break;
if (ShutdownHandlers[index] == handler)
{
ShutdownHandlers.RemoveAt(index);
break;
}
}
}
}
Expand All @@ -452,10 +458,17 @@ public static void RemoveShutdownHandler(ShutdownHandler handler)
/// </summary>
static void ExecuteShutdownHandlers()
{
while(ShutdownHandlers.Count > 0)
// Invoke unlocked so handlers can re-enter Add/Remove.
while (true)
{
var handler = ShutdownHandlers[ShutdownHandlers.Count - 1];
ShutdownHandlers.RemoveAt(ShutdownHandlers.Count - 1);
ShutdownHandler handler;
lock (_shutdownHandlersLock)
{
if (ShutdownHandlers.Count == 0) return;
int last = ShutdownHandlers.Count - 1;
handler = ShutdownHandlers[last];
ShutdownHandlers.RemoveAt(last);
}
handler();
}
}
Expand Down
Loading
Loading