Visitar URL original
Fix MethodBinding/OverloadMapper memory leak (#691) by greateggsgreg · Pull Request #2719 · pythonnet/pythonnet · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Next Next commit
Fix MethodBinding/OverloadMapper memory leak (#691)
MethodBinding and OverloadMapper held PyObject `target` references that
were not disposed during tp_clear, leaving Python-side refcount drops to
wait on the multi-hop .NET finalizer chain. They also shared the same
C# PyObject instance across mp_subscript/Overloads paths, so freeing one
could free the underlying Python object out from under the others.

- ExtensionType: add virtual OnClear() hook called from tp_clear before
  the GCHandle is released, letting subclasses eagerly drop owned
  Python references.
- MethodBinding/OverloadMapper: override OnClear to dispose `target`.
  (`targetType` is intentionally not disposed since Python types are
  long-lived and tracked by other caches.)
- Take an independent INCREF'd PyObject copy at every site that hands a
  shared target into a new MethodBinding or OverloadMapper, so each
  wrapper owns its own reference.

Result: the three _does_not_leak_memory tests drop from ~485 MB delta
to ~10 KB delta on Python 3.14.
  • Loading branch information
greateggsgreg authored and filmor committed May 11, 2026
commit a305127d13b546c8a2ab73b058daed42a72fa873
10 changes: 10 additions & 0 deletions src/runtime/Types/ExtensionType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,18 @@ public unsafe static void tp_dealloc(NewReference lastRef)
DecrefTypeAndFree(lastRef.Steal());
}

/// <summary>
/// Called during tp_clear before the GCHandle is released.
/// Override to eagerly dispose Python object references (PyObject fields)
/// held by the subclass, preventing the multi-hop .NET finalizer chain
/// from delaying Python-side refcount decrements.
/// </summary>
protected virtual void OnClear() { }

Comment thread
filmor marked this conversation as resolved.
public static int tp_clear(BorrowedReference ob)
{
(GetManagedObject(ob) as ExtensionType)?.OnClear();

var weakrefs = Runtime.PyObject_GetWeakRefList(ob);
if (weakrefs != null)
{
Expand Down
14 changes: 12 additions & 2 deletions src/runtime/Types/MethodBinding.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,10 @@ public static NewReference mp_subscript(BorrowedReference tp, BorrowedReference
}

MethodObject overloaded = self.m.WithOverloads(overloads);
var mb = new MethodBinding(overloaded, self.target, self.targetType);
var mb = new MethodBinding(
overloaded,
self.target is null ? null : new PyObject(self.target.Reference),
self.targetType is null ? null : new PyType(self.targetType.Reference));
return mb.Alloc();
}

Expand Down Expand Up @@ -141,7 +144,8 @@ public static NewReference tp_getattro(BorrowedReference ob, BorrowedReference k
// FIXME: deprecate __overloads__ soon...
case "__overloads__":
case "Overloads":
var om = new OverloadMapper(self.m, self.target);
var om = new OverloadMapper(self.m,
self.target is null ? null : new PyObject(self.target.Reference));
return om.Alloc();
case "__signature__" when Runtime.InspectModule is not null:
var sig = self.Signature;
Expand Down Expand Up @@ -281,5 +285,11 @@ public static NewReference tp_repr(BorrowedReference ob)
string name = self.m.name;
return Runtime.PyString_FromString($"<{type} method '{name}'>");
}

protected override void OnClear()
{
target?.Dispose();
target = null;
}
}
}
13 changes: 11 additions & 2 deletions src/runtime/Types/OverloadMapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ namespace Python.Runtime
internal class OverloadMapper : ExtensionType
{
private readonly MethodObject m;
private readonly PyObject? target;
private PyObject? target;

public OverloadMapper(MethodObject m, PyObject? target)
{
Expand Down Expand Up @@ -42,7 +42,10 @@ public static NewReference mp_subscript(BorrowedReference tp, BorrowedReference
return Exceptions.RaiseTypeError(e);
}

var mb = new MethodBinding(self.m, self.target) { info = mi };
var mb = new MethodBinding(
self.m,
self.target is null ? null : new PyObject(self.target.Reference))
{ info = mi };
return mb.Alloc();
}

Expand All @@ -54,5 +57,11 @@ public static NewReference tp_repr(BorrowedReference op)
var self = (OverloadMapper)GetManagedObject(op)!;
return self.m.GetDocString();
}

protected override void OnClear()
{
target?.Dispose();
target = null;
}
}
}