Memory Management
ApplePy bridges two memory management systems:
- Swift: Automatic Reference Counting (ARC)
- Python: Manual reference counting (Py_INCREF / Py_DECREF)
How It Works
When Python wraps a Swift object, neither runtime "knows" about the other's refcount. Instead:
- Python creates a wrapper:
tp_newallocates aPyObject - Swift object stored:
tp_initcallsUnmanaged.passRetained()→ ARC count +1 - Python uses the object: Python refcount tracks Python references
- Python deallocates:
tp_dealloccallsUnmanaged.release()→ ARC count -1
The key invariant: the Unmanaged.passRetained() in tp_init is balanced by Unmanaged.release() in tp_dealloc.
PyBridged Protocol
protocol PyBridged {
associatedtype Box: AnyObject
static func box(_ value: Self) -> Box
static func unbox(_ box: Box) -> Self
}
- Structs: Boxed in
PyObjectBox<T>(heap-allocated wrapper with copy semantics) - Classes: Box IS the class itself (zero-overhead)
PyBridge Helpers
// tp_init: store a Swift value
PyBridge.store(myCounter, in: pyObject)
// Method wrapper: access the Swift value
let counter = PyBridge.load(Counter.self, from: pyObject)
// tp_dealloc: release
PyBridge.release(Counter.self, from: pyObject)
Debug Assertions
In DEBUG builds, ApplePy checks:
- Python refcount ≥ 1 when accessing (assertPyAlive)
- Swift pointer is non-nil when loading (assertSwiftAlive)
GIL Considerations
All Python object access must happen while holding the GIL:
PythonHandle.withGIL { py in
// Safe to use Python objects here
let result = myValue.intoPython(py: py)
}
Use allowThreads for CPU-bound Swift work:
Common Pitfalls
- Don't store
PyObjectPtracross GIL releases — the object might be deallocated - Don't share
PyObjectPtracross threads without acquiring the GIL - Watch for cycles: Swift object → Python wrapper → Swift object. Break with weak references.