Skip to content

Commit 049f699

Browse files
committed
Use PyCapsule to wrap the function closure.
1 parent d7d8d43 commit 049f699

File tree

2 files changed

+27
-19
lines changed

2 files changed

+27
-19
lines changed

PythonKit/Python.swift

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1543,42 +1543,41 @@ final class PyFunction {
15431543

15441544
public struct PythonFunction {
15451545
/// Called directly by the Python C API
1546-
private var function: Unmanaged<PyFunction>
1546+
private var function: PyFunction
15471547

15481548
public init(_ fn: @escaping (PythonObject) throws -> PythonConvertible) {
1549-
let function = PyFunction { argumentsAsTuple in
1549+
function = PyFunction { argumentsAsTuple in
15501550
return try fn(argumentsAsTuple[0])
15511551
}
1552-
self.function = Unmanaged.passRetained(function)
15531552
}
15541553

15551554
/// For cases where the Swift function should accept more (or less) than one parameter, accept an ordered array of all arguments instead
15561555
public init(_ fn: @escaping ([PythonObject]) throws -> PythonConvertible) {
1557-
let function = PyFunction { argumentsAsTuple in
1556+
function = PyFunction { argumentsAsTuple in
15581557
return try fn(argumentsAsTuple.map { $0 })
15591558
}
1560-
self.function = Unmanaged.passRetained(function)
15611559
}
15621560

1563-
// FIXME: Memory management issue:
1564-
// It is necessary to pass a retained reference to `PythonFunction` so that it
1565-
// outlives the `PyReference` of the PyCFunction we create below. If we don't,
1566-
// Python tries to access what then has become a garbage pointer when it cleans
1567-
// up the CFunction. This means the entire `PythonFunction` currently leaks.
1568-
public func deallocate() {
1569-
function.release()
1570-
}
15711561
}
15721562

15731563
extension PythonFunction : PythonConvertible {
15741564
public var pythonObject: PythonObject {
1575-
_ = Python // Ensure Python is initialized.
1565+
// Ensure Python is initialized, and check for version match.
1566+
let versionMajor = Python.versionInfo.major
1567+
let versionMinor = Python.versionInfo.minor
1568+
guard (versionMajor == 3 && versionMinor >= 1) || versionMajor > 3 else {
1569+
fatalError("PythonFunction only supports Python 3.1 and above.")
1570+
}
15761571

1577-
let funcPointer = function.toOpaque()
1572+
let funcPointer = Unmanaged.passRetained(function).toOpaque()
1573+
let capsulePointer = PyCapsule_New(funcPointer, nil, { capsulePointer in
1574+
let funcPointer = PyCapsule_GetPointer(capsulePointer, nil)
1575+
Unmanaged<PyFunction>.fromOpaque(funcPointer).release()
1576+
})
15781577

15791578
let pyFuncPointer = PyCFunction_New(
15801579
PythonFunction.sharedMethodDefinition,
1581-
funcPointer
1580+
capsulePointer
15821581
)
15831582

15841583
return PythonObject(consuming: pyFuncPointer)
@@ -1609,11 +1608,12 @@ fileprivate extension PythonFunction {
16091608
}()
16101609

16111610
private static let sharedMethodImplementation: @convention(c) (PyObjectPointer?, PyObjectPointer?) -> PyObjectPointer? = { context, argumentsPointer in
1612-
guard let argumentsPointer = argumentsPointer, let selfPointer = context else {
1611+
guard let argumentsPointer = argumentsPointer, let capsulePointer = context else {
16131612
return nil
16141613
}
16151614

1616-
let function = Unmanaged<PyFunction>.fromOpaque(selfPointer).takeUnretainedValue()
1615+
let funcPointer = PyCapsule_GetPointer(capsulePointer, nil)
1616+
let function = Unmanaged<PyFunction>.fromOpaque(funcPointer).takeUnretainedValue()
16171617

16181618
do {
16191619
let argumentsAsTuple = PythonObject(consuming: argumentsPointer)

PythonKit/PythonLibrary+Symbols.swift

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@ typealias PyCCharPointer = UnsafePointer<Int8>
2525
typealias PyBinaryOperation =
2626
@convention(c) (PyObjectPointer?, PyObjectPointer?) -> PyObjectPointer?
2727
typealias PyUnaryOperation =
28-
@convention(c) (PyObjectPointer?) -> PyObjectPointer?
28+
@convention(c) (PyObjectPointer?) -> PyObjectPointer?
29+
typealias PyCapsuleDestructor =
30+
@convention(c) (PyObjectPointer?) -> Void
2931

3032
let Py_LT: Int32 = 0
3133
let Py_LE: Int32 = 1
@@ -60,6 +62,12 @@ let PyRun_SimpleString: @convention(c) (PyCCharPointer) -> Void =
6062
let PyCFunction_New: @convention(c) (PyMethodDefPointer, UnsafeMutableRawPointer) -> PyObjectPointer =
6163
PythonLibrary.loadSymbol(name: "PyCFunction_New")
6264

65+
let PyCapsule_New: @convention(c) (UnsafeMutableRawPointer, UnsafePointer<CChar>?, PyCapsuleDestructor) -> PyObjectPointer =
66+
PythonLibrary.loadSymbol(name: "PyCapsule_New")
67+
68+
let PyCapsule_GetPointer: @convention(c) (PyObjectPointer?, UnsafePointer<CChar>?) -> UnsafeMutableRawPointer =
69+
PythonLibrary.loadSymbol(name: "PyCapsule_GetPointer")
70+
6371
let PyErr_SetString: @convention(c) (PyObjectPointer, UnsafePointer<CChar>?) -> Void =
6472
PythonLibrary.loadSymbol(name: "PyErr_SetString")
6573

0 commit comments

Comments
 (0)