Skip to content

Commit 03fd1c9

Browse files
committed
Squashed commit of the following:
commit 814f2c171d50578f5d003ea021b84fe7b6c75f6b Author: Geordie J <[email protected]> Date: Tue Aug 25 14:07:29 2020 +0200 Minor refactor commit 40c5b2e9c75379c4e3ad41c60eb3e7c2e8005b7d Author: Geordie J <[email protected]> Date: Tue Aug 25 12:08:44 2020 +0200 Clean up commit b99098bc4d1405b94d9060950bd039c40fb5102e Author: Geordie J <[email protected]> Date: Mon Aug 24 23:05:38 2020 +0200 Don't `import Python` commit 384b21b667cc5fb2aca0f3d54aea40d919815b7b Author: Geordie J <[email protected]> Date: Mon Aug 24 22:32:07 2020 +0200 Add support for defining Swift functions to be called from Python
1 parent 86ce684 commit 03fd1c9

File tree

2 files changed

+147
-0
lines changed

2 files changed

+147
-0
lines changed

PythonKit/Python.swift

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1400,6 +1400,7 @@ extension PythonObject : Sequence {
14001400
}
14011401
}
14021402

1403+
14031404
//===----------------------------------------------------------------------===//
14041405
// `ExpressibleByLiteral` conformances
14051406
//===----------------------------------------------------------------------===//
@@ -1517,3 +1518,139 @@ public struct PythonBytes : PythonConvertible, ConvertibleFromPython, Hashable {
15171518
}
15181519
}
15191520
}
1521+
1522+
//===----------------------------------------------------------------------===//
1523+
// PythonFunction - create functions in Swift that can be called from Python
1524+
//===----------------------------------------------------------------------===//
1525+
1526+
/// Create functions in Swift that can be called from Python
1527+
///
1528+
/// Example:
1529+
///
1530+
/// The Python code `map(lambda(x: x * 2), [10, 12, 14])` would be written as:
1531+
///
1532+
/// Python.map(PythonFunction { x in x * 2 }, [10, 12, 14]) // [20, 24, 28]
1533+
///
1534+
final public class PythonFunction {
1535+
/// Called directly by the Python C API
1536+
private let callSwiftFunction: (_ argumentsTuple: PythonObject) throws -> PythonConvertible
1537+
1538+
public init(_ fn: @escaping (PythonObject) throws -> PythonConvertible) {
1539+
self.callSwiftFunction = { argumentsAsTuple in
1540+
return try fn(argumentsAsTuple[0])
1541+
}
1542+
}
1543+
1544+
/// For cases where the Swift function should accept more (or less) than one parameter, accept an ordered array of all arguments instead
1545+
public init(_ fn: @escaping ([PythonObject]) throws -> PythonConvertible) {
1546+
self.callSwiftFunction = { argumentsAsTuple in
1547+
return try fn(argumentsAsTuple.map { $0 })
1548+
}
1549+
}
1550+
}
1551+
1552+
extension PythonFunction : PythonConvertible {
1553+
public var pythonObject: PythonObject {
1554+
_ = Python // Ensure Python is initialized.
1555+
1556+
// FIXME: Memory management issue:
1557+
// It is necessary to pass a retained reference to `PythonFunction` so that it
1558+
// outlives the `PyReference` of the PyCFunction we create below. If we don't,
1559+
// Python tries to access what then has become a garbage pointer when it cleans
1560+
// up the CFunction. This means the entire `PythonFunction` currently leaks.
1561+
let selfPointer = Unmanaged.passRetained(self).toOpaque()
1562+
1563+
let fnPointer = PyCFunction_New(
1564+
PythonFunction.sharedMethodDefinition,
1565+
selfPointer
1566+
)
1567+
1568+
// FIXME: Another potential memory management issue.
1569+
// I can't see how to stop these functions from being prematurely
1570+
// garbage collected unless we untrack it from the Python GC.
1571+
PyObject_GC_UnTrack(fnPointer)
1572+
1573+
return PythonObject(fnPointer)
1574+
}
1575+
}
1576+
1577+
// The pointers here technically constitute a leak, but no more than
1578+
// a static string or a static struct definition at top level.
1579+
fileprivate extension PythonFunction {
1580+
static let sharedFunctionName: UnsafePointer<Int8> = {
1581+
let name = "pythonkit_swift_function"
1582+
let cString = name.utf8CString
1583+
let copy = UnsafeMutableBufferPointer<Int8>.allocate(capacity: cString.count)
1584+
_ = copy.initialize(from: cString)
1585+
return UnsafePointer(copy.baseAddress!)
1586+
}()
1587+
1588+
static let sharedMethodDefinition: UnsafeMutablePointer<PyMethodDef> = {
1589+
/// The standard calling convention. See Python C API docs
1590+
let METH_VARARGS = 1 as Int32
1591+
1592+
let pointer = UnsafeMutablePointer<PyMethodDef>.allocate(capacity: 1)
1593+
pointer.pointee = PyMethodDef(
1594+
ml_name: PythonFunction.sharedFunctionName,
1595+
ml_meth: PythonFunction.sharedMethodImplementation,
1596+
ml_flags: METH_VARARGS,
1597+
ml_doc: nil
1598+
)
1599+
1600+
return pointer
1601+
}()
1602+
1603+
private static let sharedMethodImplementation: @convention(c) (PyObjectPointer?, PyObjectPointer?) -> PyObjectPointer? = { context, argumentsPointer in
1604+
guard let argumentsPointer = argumentsPointer, let selfPointer = context else {
1605+
return nil
1606+
}
1607+
1608+
let `self` = Unmanaged<PythonFunction>.fromOpaque(selfPointer).takeUnretainedValue()
1609+
1610+
do {
1611+
let argumentsAsTuple = PythonObject(consuming: argumentsPointer)
1612+
return try self.callSwiftFunction(argumentsAsTuple).ownedPyObject
1613+
} catch {
1614+
PythonFunction.setPythonError(swiftError: error)
1615+
return nil // This must only be `nil` if an exception has been set
1616+
}
1617+
}
1618+
1619+
private static func setPythonError(swiftError: Error) {
1620+
if let pythonObject = swiftError as? PythonObject {
1621+
if Bool(Python.isinstance(pythonObject, Python.BaseException))! {
1622+
// We are an instance of an Exception class type. Set the exception class to the object's type:
1623+
PyErr_SetString(Python.type(pythonObject).ownedPyObject, pythonObject.description)
1624+
} else {
1625+
// Assume an actual class type was thrown (rather than an instance)
1626+
// Crashes if it was neither a subclass of BaseException nor an instance of one.
1627+
//
1628+
// We *could* check to see whether `pythonObject` is a class here and fall back
1629+
// to the default case of setting a generic Exception, below, but we also want
1630+
// people to write valid code.
1631+
PyErr_SetString(pythonObject.ownedPyObject, pythonObject.description)
1632+
}
1633+
} else {
1634+
// Make a generic Python Exception based on the Swift Error:
1635+
PyErr_SetString(Python.Exception.ownedPyObject, "\(type(of: swiftError)) raised in Swift: \(swiftError)")
1636+
}
1637+
}
1638+
}
1639+
1640+
extension PythonObject : Error {}
1641+
1642+
// From Python's C Headers:
1643+
struct PyMethodDef {
1644+
/// The name of the built-in function/method
1645+
public var ml_name: UnsafePointer<Int8>
1646+
1647+
/// The C function that implements it
1648+
public var ml_meth: @convention(c) (PyObjectPointer?, PyObjectPointer?) -> PyObjectPointer?
1649+
1650+
/// Combination of METH_xxx flags, which mostly describe the args expected by the C func
1651+
public var ml_flags: Int32
1652+
1653+
/// The __doc__ attribute, or NULL
1654+
public var ml_doc: UnsafePointer<Int8>?
1655+
}
1656+
>>>>>>> Squashed commit of the following:

PythonKit/PythonLibrary+Symbols.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
@usableFromInline
2222
typealias PyObjectPointer = UnsafeMutableRawPointer
23+
typealias PyMethodDefPointer = UnsafeMutableRawPointer
2324
typealias PyCCharPointer = UnsafePointer<Int8>
2425
typealias PyBinaryOperation =
2526
@convention(c) (PyObjectPointer?, PyObjectPointer?) -> PyObjectPointer?
@@ -56,6 +57,15 @@ let PyEval_GetBuiltins: @convention(c) () -> PyObjectPointer =
5657
let PyRun_SimpleString: @convention(c) (PyCCharPointer) -> Void =
5758
PythonLibrary.loadSymbol(name: "PyRun_SimpleString")
5859

60+
let PyCFunction_New: @convention(c) (PyMethodDefPointer, UnsafeMutableRawPointer) -> PyObjectPointer =
61+
PythonLibrary.loadSymbol(name: "PyCFunction_New")
62+
63+
let PyObject_GC_UnTrack: @convention(c) (PyObjectPointer) -> Void =
64+
PythonLibrary.loadSymbol(name: "PyObject_GC_UnTrack")
65+
66+
let PyErr_SetString: @convention(c) (PyObjectPointer, UnsafePointer<CChar>?) -> Void =
67+
PythonLibrary.loadSymbol(name: "PyErr_SetString")
68+
5969
let PyErr_Occurred: @convention(c) () -> PyObjectPointer? =
6070
PythonLibrary.loadSymbol(name: "PyErr_Occurred")
6171

0 commit comments

Comments
 (0)