From a9e76a2eeecdffbe6a8dfbe82861f23b3dcd7e8b Mon Sep 17 00:00:00 2001 From: Jeffrey Davis Date: Mon, 8 Jul 2024 09:47:18 -0700 Subject: [PATCH 01/24] Introduce leak to avoid use after free. --- PythonKit/Python.swift | 281 +++++++++++++++++++++-------------------- 1 file changed, 141 insertions(+), 140 deletions(-) diff --git a/PythonKit/Python.swift b/PythonKit/Python.swift index 841a147..24897ec 100644 --- a/PythonKit/Python.swift +++ b/PythonKit/Python.swift @@ -39,7 +39,7 @@ typealias OwnedPyObjectPointer = PyObjectPointer @usableFromInline @_fixed_layout final class PyReference { private var pointer: OwnedPyObjectPointer - + // This `PyReference`, once deleted, will make no delta change to the // python object's reference count. It will however, retain the reference for // the lifespan of this object. @@ -47,21 +47,21 @@ final class PyReference { self.pointer = pointer Py_IncRef(pointer) } - + // This `PyReference` adopts the +1 reference and will decrement it in the // future. init(consuming pointer: PyObjectPointer) { self.pointer = pointer } - + deinit { Py_DecRef(pointer) } - + var borrowedPyObject: PyObjectPointer { return pointer } - + var ownedPyObject: OwnedPyObjectPointer { Py_IncRef(pointer) return pointer @@ -90,26 +90,26 @@ final class PyReference { public struct PythonObject { /// The underlying `PyReference`. fileprivate var reference: PyReference - + @usableFromInline init(_ pointer: PyReference) { reference = pointer } - + /// Creates a new instance and a new reference. init(_ pointer: OwnedPyObjectPointer) { reference = PyReference(pointer) } - + /// Creates a new instance consuming the specified `PyObject` pointer. init(consuming pointer: PyObjectPointer) { reference = PyReference(consuming: pointer) } - + fileprivate var borrowedPyObject: PyObjectPointer { return reference.borrowedPyObject } - + fileprivate var ownedPyObject: OwnedPyObjectPointer { return reference.ownedPyObject } @@ -165,7 +165,7 @@ fileprivate extension PythonConvertible { var borrowedPyObject: PyObjectPointer { return pythonObject.borrowedPyObject } - + var ownedPyObject: OwnedPyObjectPointer { return pythonObject.ownedPyObject } @@ -189,7 +189,7 @@ extension PythonObject : PythonConvertible, ConvertibleFromPython { public init(_ object: PythonObject) { self.init(consuming: object.ownedPyObject) } - + public var pythonObject: PythonObject { return self } } @@ -210,7 +210,7 @@ public extension PythonObject { public enum PythonError : Error, Equatable { /// A Python runtime exception, produced by calling a Python function. case exception(PythonObject, traceback: PythonObject?) - + /// A failed call on a `PythonObject`. /// Reasons for failure include: /// - A non-callable Python object was called. @@ -218,7 +218,7 @@ public enum PythonError : Error, Equatable { /// object. /// - An invalid keyword argument was specified. case invalidCall(PythonObject) - + /// A module import error. case invalidModule(String) } @@ -248,14 +248,14 @@ extension PythonError : CustomStringConvertible { // active. private func throwPythonErrorIfPresent() throws { if PyErr_Occurred() == nil { return } - + var type: PyObjectPointer? var value: PyObjectPointer? var traceback: PyObjectPointer? - + // Fetch the exception and clear the exception state. PyErr_Fetch(&type, &value, &traceback) - + // The value for the exception may not be set but the type always should be. let resultObject = PythonObject(consuming: value ?? type!) let tracebackObject = traceback.flatMap { PythonObject(consuming: $0) } @@ -271,11 +271,11 @@ private func throwPythonErrorIfPresent() throws { /// `dynamicallyCall` until further discussion/design. public struct ThrowingPythonObject { private var base: PythonObject - + fileprivate init(_ base: PythonObject) { self.base = base } - + /// Call `self` with the specified positional arguments. /// If the call fails for some reason, `PythonError.invalidCall` is thrown. /// - Precondition: `self` must be a Python callable. @@ -285,7 +285,7 @@ public struct ThrowingPythonObject { withArguments args: PythonConvertible...) throws -> PythonObject { return try dynamicallyCall(withArguments: args) } - + /// Call `self` with the specified positional arguments. /// If the call fails for some reason, `PythonError.invalidCall` is thrown. /// - Precondition: `self` must be a Python callable. @@ -294,18 +294,18 @@ public struct ThrowingPythonObject { public func dynamicallyCall( withArguments args: [PythonConvertible] = []) throws -> PythonObject { try throwPythonErrorIfPresent() - + // Positional arguments are passed as a tuple of objects. let argTuple = pyTuple(args.map { $0.pythonObject }) defer { Py_DecRef(argTuple) } - + // Python calls always return a non-null object when successful. If the // Python function produces the equivalent of C `void`, it returns the // `None` object. A `null` result of `PyObjectCall` happens when there is an // error, like `self` not being a Python callable. let selfObject = base.ownedPyObject defer { Py_DecRef(selfObject) } - + guard let result = PyObject_CallObject(selfObject, argTuple) else { // If a Python exception was thrown, throw a corresponding Swift error. try throwPythonErrorIfPresent() @@ -313,7 +313,7 @@ public struct ThrowingPythonObject { } return PythonObject(consuming: result) } - + /// Call `self` with the specified arguments. /// If the call fails for some reason, `PythonError.invalidCall` is thrown. /// - Precondition: `self` must be a Python callable. @@ -324,7 +324,7 @@ public struct ThrowingPythonObject { KeyValuePairs = [:]) throws -> PythonObject { return try _dynamicallyCall(args) } - + /// Alias for the function above that lets the caller dynamically construct the argument list, without using a dictionary literal. /// This function must be called explicitly on a `PythonObject` because `@dynamicCallable` does not recognize it. @discardableResult @@ -333,17 +333,17 @@ public struct ThrowingPythonObject { [(key: String, value: PythonConvertible)] = []) throws -> PythonObject { return try _dynamicallyCall(args) } - + /// Implementation of `dynamicallyCall(withKeywordArguments)`. private func _dynamicallyCall(_ args: T) throws -> PythonObject where T.Element == (key: String, value: PythonConvertible) { try throwPythonErrorIfPresent() - + // An array containing positional arguments. var positionalArgs: [PythonObject] = [] // A dictionary object for storing keyword arguments, if any exist. var kwdictObject: OwnedPyObjectPointer? = nil - + for (key, value) in args { if key.isEmpty { positionalArgs.append(value.pythonObject) @@ -360,20 +360,20 @@ public struct ThrowingPythonObject { Py_DecRef(k) Py_DecRef(v) } - + defer { Py_DecRef(kwdictObject) } // Py_DecRef is `nil` safe. - + // Positional arguments are passed as a tuple of objects. let argTuple = pyTuple(positionalArgs) defer { Py_DecRef(argTuple) } - + // Python calls always return a non-null object when successful. If the // Python function produces the equivalent of C `void`, it returns the // `None` object. A `null` result of `PyObjectCall` happens when there is an // error, like `self` not being a Python callable. let selfObject = base.ownedPyObject defer { Py_DecRef(selfObject) } - + guard let result = PyObject_Call(selfObject, argTuple, kwdictObject) else { // If a Python exception was thrown, throw a corresponding Swift error. try throwPythonErrorIfPresent() @@ -381,7 +381,7 @@ public struct ThrowingPythonObject { } return PythonObject(consuming: result) } - + /// Converts to a 2-tuple, if possible. public var tuple2: (PythonObject, PythonObject)? { let ct = base.checking @@ -390,7 +390,7 @@ public struct ThrowingPythonObject { } return (elt0, elt1) } - + /// Converts to a 3-tuple, if possible. public var tuple3: (PythonObject, PythonObject, PythonObject)? { let ct = base.checking @@ -399,7 +399,7 @@ public struct ThrowingPythonObject { } return (elt0, elt1, elt2) } - + /// Converts to a 4-tuple, if possible. public var tuple4: (PythonObject, PythonObject, PythonObject, PythonObject)? { let ct = base.checking @@ -409,7 +409,7 @@ public struct ThrowingPythonObject { } return (elt0, elt1, elt2, elt3) } - + public var count: Int? { base.checking.count } @@ -434,11 +434,11 @@ public extension PythonObject { public struct CheckingPythonObject { /// The underlying `PythonObject`. private var base: PythonObject - + fileprivate init(_ base: PythonObject) { self.base = base } - + public subscript(dynamicMember name: String) -> PythonObject? { get { let selfObject = base.ownedPyObject @@ -451,7 +451,7 @@ public struct CheckingPythonObject { return PythonObject(consuming: result) } } - + /// Access the element corresponding to the specified `PythonConvertible` /// values representing a key. /// - Note: This is equivalent to `object[key]` in Python. @@ -461,9 +461,10 @@ public struct CheckingPythonObject { let selfObject = base.ownedPyObject defer { Py_DecRef(keyObject) - Py_DecRef(selfObject) + // TODO: Re-enable once the use after free is figured out. + // Py_DecRef(selfObject) } - + // `PyObject_GetItem` returns +1 reference. if let result = PyObject_GetItem(selfObject, keyObject) { return PythonObject(consuming: result) @@ -478,7 +479,7 @@ public struct CheckingPythonObject { Py_DecRef(keyObject) Py_DecRef(selfObject) } - + if let newValue = newValue { let newValueObject = newValue.ownedPyObject PyObject_SetItem(selfObject, keyObject, newValueObject) @@ -489,7 +490,7 @@ public struct CheckingPythonObject { } } } - + /// Access the element corresponding to the specified `PythonConvertible` /// values representing a key. /// - Note: This is equivalent to `object[key]` in Python. @@ -501,7 +502,7 @@ public struct CheckingPythonObject { self[key] = newValue } } - + /// Converts to a 2-tuple, if possible. public var tuple2: (PythonObject, PythonObject)? { guard let elt0 = self[0], let elt1 = self[1] else { @@ -509,7 +510,7 @@ public struct CheckingPythonObject { } return (elt0, elt1) } - + /// Converts to a 3-tuple, if possible. public var tuple3: (PythonObject, PythonObject, PythonObject)? { guard let elt0 = self[0], let elt1 = self[1], let elt2 = self[2] else { @@ -517,7 +518,7 @@ public struct CheckingPythonObject { } return (elt0, elt1, elt2) } - + /// Converts to a 4-tuple, if possible. public var tuple4: (PythonObject, PythonObject, PythonObject, PythonObject)? { guard let elt0 = self[0], let elt1 = self[1], @@ -526,7 +527,7 @@ public struct CheckingPythonObject { } return (elt0, elt1, elt2, elt3) } - + public var count: Int? { Int(Python.len(base)) } @@ -559,7 +560,7 @@ public extension PythonObject { defer { Py_DecRef(selfObject) } let valueObject = newValue.ownedPyObject defer { Py_DecRef(valueObject) } - + if PyObject_SetAttrString(selfObject, memberName, valueObject) == -1 { try! throwPythonErrorIfPresent() fatalError(""" @@ -569,7 +570,7 @@ public extension PythonObject { } } } - + /// Access the element corresponding to the specified `PythonConvertible` /// values representing a key. /// - Note: This is equivalent to `object[key]` in Python. @@ -587,7 +588,7 @@ public extension PythonObject { checking[key] = newValue } } - + /// Converts to a 2-tuple. var tuple2: (PythonObject, PythonObject) { guard let result = checking.tuple2 else { @@ -595,7 +596,7 @@ public extension PythonObject { } return result } - + /// Converts to a 3-tuple. var tuple3: (PythonObject, PythonObject, PythonObject) { guard let result = checking.tuple3 else { @@ -603,7 +604,7 @@ public extension PythonObject { } return result } - + /// Converts to a 4-tuple. var tuple4: (PythonObject, PythonObject, PythonObject, PythonObject) { guard let result = checking.tuple4 else { @@ -611,7 +612,7 @@ public extension PythonObject { } return result } - + /// Call `self` with the specified positional arguments. /// - Precondition: `self` must be a Python callable. /// - Parameter args: Positional arguments for the Python callable. @@ -620,7 +621,7 @@ public extension PythonObject { withArguments args: [PythonConvertible] = []) -> PythonObject { return try! throwing.dynamicallyCall(withArguments: args) } - + /// Call `self` with the specified arguments. /// - Precondition: `self` must be a Python callable. /// - Parameter args: Positional or keyword arguments for the Python callable. @@ -630,7 +631,7 @@ public extension PythonObject { KeyValuePairs = [:]) -> PythonObject { return try! throwing.dynamicallyCall(withKeywordArguments: args) } - + /// Alias for the function above that lets the caller dynamically construct the argument list, without using a dictionary literal. /// This function must be called explicitly on a `PythonObject` because `@dynamicCallable` does not recognize it. @discardableResult @@ -673,16 +674,16 @@ public let Python = PythonInterface() public struct PythonInterface { /// A dictionary of the Python builtins. public let builtins: PythonObject - + init() { Py_Initialize() // Initialize Python builtins = PythonObject(PyEval_GetBuiltins()) - + // Runtime Fixes: PyRun_SimpleString(""" import sys import os - + # Some Python modules expect to have at least one argument in `sys.argv`: sys.argv = [""] @@ -694,7 +695,7 @@ public struct PythonInterface { sys.executable = os.path.join(sys.exec_prefix, "bin", executable_name) """) } - + public func attemptImport(_ name: String) throws -> PythonObject { guard let module = PyImport_ImportModule(name) else { try throwPythonErrorIfPresent() @@ -702,27 +703,27 @@ public struct PythonInterface { } return PythonObject(consuming: module) } - + public func `import`(_ name: String) -> PythonObject { return try! attemptImport(name) } - + public subscript(dynamicMember name: String) -> PythonObject { return builtins[name] } - + // The Python runtime version. // Equivalent to `sys.version` in Python. public var version: PythonObject { return self.import("sys").version } - + // The Python runtime version information. // Equivalent to `sys.version_info` in Python. public var versionInfo: PythonObject { return self.import("sys").version_info } - + /// Emulates a Python `with` statement. /// - Parameter object: A context manager object. /// - Parameter body: A closure to call on the result of `object.__enter__()`. @@ -754,12 +755,12 @@ public extension PythonObject { init(tupleOf elements: PythonConvertible...) { self.init(tupleContentsOf: elements) } - + init(tupleContentsOf elements: T) where T.Element == PythonConvertible { self.init(consuming: pyTuple(elements.map { $0.pythonObject })) } - + init(tupleContentsOf elements: T) where T.Element : PythonConvertible { self.init(consuming: pyTuple(elements)) @@ -775,14 +776,14 @@ public extension PythonObject { private func isType(_ object: PythonObject, type: PyObjectPointer) -> Bool { let typePyRef = PythonObject(type) - + let result = Python.isinstance(object, typePyRef) - + // We cannot use the normal failable Bool initializer from `PythonObject` // here because would cause an infinite loop. let pyObject = result.ownedPyObject defer { Py_DecRef(pyObject) } - + // Anything not equal to `Py_ZeroStruct` is truthy. return pyObject != _Py_ZeroStruct } @@ -790,13 +791,13 @@ private func isType(_ object: PythonObject, extension Bool : PythonConvertible, ConvertibleFromPython { public init?(_ pythonObject: PythonObject) { guard isType(pythonObject, type: PyBool_Type) else { return nil } - + let pyObject = pythonObject.ownedPyObject defer { Py_DecRef(pyObject) } - + self = pyObject == _Py_TrueStruct } - + public var pythonObject: PythonObject { _ = Python // Ensure Python is initialized. return PythonObject(consuming: PyBool_FromLong(self ? 1 : 0)) @@ -807,14 +808,14 @@ extension String : PythonConvertible, ConvertibleFromPython { public init?(_ pythonObject: PythonObject) { let pyObject = pythonObject.ownedPyObject defer { Py_DecRef(pyObject) } - + guard let cString = PyString_AsString(pyObject) else { PyErr_Clear() return nil } self.init(cString: cString) } - + public var pythonObject: PythonObject { _ = Python // Ensure Python is initialized. let v = utf8CString.withUnsafeBufferPointer { @@ -834,17 +835,17 @@ fileprivate extension PythonObject { ) -> T? { let pyObject = ownedPyObject defer { Py_DecRef(pyObject) } - + assert(PyErr_Occurred() == nil, "Python error occurred somewhere but wasn't handled") - + let value = converter(pyObject) guard value != errorValue || PyErr_Occurred() == nil else { PyErr_Clear() return nil } return value - + } } @@ -858,7 +859,7 @@ extension Int : PythonConvertible, ConvertibleFromPython { } self = value } - + public var pythonObject: PythonObject { _ = Python // Ensure Python is initialized. return PythonObject(consuming: PyInt_FromLong(self)) @@ -876,7 +877,7 @@ extension UInt : PythonConvertible, ConvertibleFromPython { } self = value } - + public var pythonObject: PythonObject { _ = Python // Ensure Python is initialized. return PythonObject(consuming: PyInt_FromSize_t(self)) @@ -893,7 +894,7 @@ extension Double : PythonConvertible, ConvertibleFromPython { } self = value } - + public var pythonObject: PythonObject { _ = Python // Ensure Python is initialized. return PythonObject(consuming: PyFloat_FromDouble(self)) @@ -912,7 +913,7 @@ extension Int8 : PythonConvertible, ConvertibleFromPython { guard let i = Int(pythonObject) else { return nil } self.init(i) } - + public var pythonObject: PythonObject { return Int(self).pythonObject } @@ -923,7 +924,7 @@ extension Int16 : PythonConvertible, ConvertibleFromPython { guard let i = Int(pythonObject) else { return nil } self.init(i) } - + public var pythonObject: PythonObject { return Int(self).pythonObject } @@ -934,7 +935,7 @@ extension Int32 : PythonConvertible, ConvertibleFromPython { guard let i = Int(pythonObject) else { return nil } self.init(i) } - + public var pythonObject: PythonObject { return Int(self).pythonObject } @@ -945,7 +946,7 @@ extension Int64 : PythonConvertible, ConvertibleFromPython { guard let i = Int(pythonObject) else { return nil } self.init(i) } - + public var pythonObject: PythonObject { return Int(self).pythonObject } @@ -956,7 +957,7 @@ extension UInt8 : PythonConvertible, ConvertibleFromPython { guard let i = UInt(pythonObject) else { return nil } self.init(i) } - + public var pythonObject: PythonObject { return UInt(self).pythonObject } @@ -967,7 +968,7 @@ extension UInt16 : PythonConvertible, ConvertibleFromPython { guard let i = UInt(pythonObject) else { return nil } self.init(i) } - + public var pythonObject: PythonObject { return UInt(self).pythonObject } @@ -978,7 +979,7 @@ extension UInt32 : PythonConvertible, ConvertibleFromPython { guard let i = UInt(pythonObject) else { return nil } self.init(i) } - + public var pythonObject: PythonObject { return UInt(self).pythonObject } @@ -989,7 +990,7 @@ extension UInt64 : PythonConvertible, ConvertibleFromPython { guard let i = UInt(pythonObject) else { return nil } self.init(i) } - + public var pythonObject: PythonObject { return UInt(self).pythonObject } @@ -1002,7 +1003,7 @@ extension Float : PythonConvertible, ConvertibleFromPython { guard let v = Double(pythonObject) else { return nil } self.init(v) } - + public var pythonObject: PythonObject { return Double(self).pythonObject } @@ -1087,12 +1088,12 @@ extension Dictionary : ConvertibleFromPython where Key : ConvertibleFromPython, Value : ConvertibleFromPython { public init?(_ pythonDict: PythonObject) { self = [:] - + // Iterate over the Python dictionary, converting its keys and values to // Swift `Key` and `Value` pairs. var key, value: PyObjectPointer? var position: Int = 0 - + while PyDict_Next( pythonDict.borrowedPyObject, &position, &key, &value) != 0 { @@ -1204,7 +1205,7 @@ public extension PythonObject { static func + (lhs: PythonObject, rhs: PythonObject) -> PythonObject { return performBinaryOp(PyNumber_Add, lhs: lhs, rhs: rhs) } - + static func - (lhs: PythonObject, rhs: PythonObject) -> PythonObject { return performBinaryOp(PyNumber_Subtract, lhs: lhs, rhs: rhs) } @@ -1212,23 +1213,23 @@ public extension PythonObject { static func * (lhs: PythonObject, rhs: PythonObject) -> PythonObject { return performBinaryOp(PyNumber_Multiply, lhs: lhs, rhs: rhs) } - + static func / (lhs: PythonObject, rhs: PythonObject) -> PythonObject { return performBinaryOp(PyNumber_TrueDivide, lhs: lhs, rhs: rhs) } - + static func += (lhs: inout PythonObject, rhs: PythonObject) { lhs = performBinaryOp(PyNumber_InPlaceAdd, lhs: lhs, rhs: rhs) } - + static func -= (lhs: inout PythonObject, rhs: PythonObject) { lhs = performBinaryOp(PyNumber_InPlaceSubtract, lhs: lhs, rhs: rhs) } - + static func *= (lhs: inout PythonObject, rhs: PythonObject) { lhs = performBinaryOp(PyNumber_InPlaceMultiply, lhs: lhs, rhs: rhs) } - + static func /= (lhs: inout PythonObject, rhs: PythonObject) { lhs = performBinaryOp(PyNumber_InPlaceTrueDivide, lhs: lhs, rhs: rhs) } @@ -1268,9 +1269,9 @@ extension PythonObject : SignedNumeric { public init(exactly value: T) { self.init(Int(value)) } - + public typealias Magnitude = PythonObject - + public var magnitude: PythonObject { return self < 0 ? -self : self } @@ -1284,11 +1285,11 @@ extension PythonObject : SignedNumeric { extension PythonObject : Strideable { public typealias Stride = PythonObject - + public func distance(to other: PythonObject) -> Stride { return other - self } - + public func advanced(by stride: Stride) -> PythonObject { return self + stride } @@ -1318,7 +1319,7 @@ extension PythonObject : Equatable, Comparable { public static func == (lhs: PythonObject, rhs: PythonObject) -> Bool { return lhs.compared(to: rhs, byOp: Py_EQ) } - + public static func != (lhs: PythonObject, rhs: PythonObject) -> Bool { return lhs.compared(to: rhs, byOp: Py_NE) } @@ -1330,11 +1331,11 @@ extension PythonObject : Equatable, Comparable { public static func <= (lhs: PythonObject, rhs: PythonObject) -> Bool { return lhs.compared(to: rhs, byOp: Py_LE) } - + public static func > (lhs: PythonObject, rhs: PythonObject) -> Bool { return lhs.compared(to: rhs, byOp: Py_GT) } - + public static func >= (lhs: PythonObject, rhs: PythonObject) -> Bool { return lhs.compared(to: rhs, byOp: Py_GE) } @@ -1357,27 +1358,27 @@ public extension PythonObject { } return PythonObject(consuming: result) } - + static func == (lhs: PythonObject, rhs: PythonObject) -> PythonObject { return lhs.compared(to: rhs, byOp: Py_EQ) } - + static func != (lhs: PythonObject, rhs: PythonObject) -> PythonObject { return lhs.compared(to: rhs, byOp: Py_NE) } - + static func < (lhs: PythonObject, rhs: PythonObject) -> PythonObject { return lhs.compared(to: rhs, byOp: Py_LT) } - + static func <= (lhs: PythonObject, rhs: PythonObject) -> PythonObject { return lhs.compared(to: rhs, byOp: Py_LE) } - + static func > (lhs: PythonObject, rhs: PythonObject) -> PythonObject { return lhs.compared(to: rhs, byOp: Py_GT) } - + static func >= (lhs: PythonObject, rhs: PythonObject) -> PythonObject { return lhs.compared(to: rhs, byOp: Py_GE) } @@ -1395,19 +1396,19 @@ extension PythonObject : Hashable { extension PythonObject : MutableCollection { public typealias Index = PythonObject public typealias Element = PythonObject - + public var startIndex: Index { return 0 } - + public var endIndex: Index { return Python.len(self) } - + public func index(after i: Index) -> Index { return i + PythonObject(1) } - + public subscript(index: PythonObject) -> PythonObject { get { return self[index as PythonConvertible] @@ -1421,7 +1422,7 @@ extension PythonObject : MutableCollection { extension PythonObject : Sequence { public struct Iterator : IteratorProtocol { fileprivate let pythonIterator: PythonObject - + public func next() -> PythonObject? { guard let result = PyIter_Next(self.pythonIterator.borrowedPyObject) else { try! throwPythonErrorIfPresent() @@ -1430,7 +1431,7 @@ extension PythonObject : Sequence { return PythonObject(consuming: result) } } - + public func makeIterator() -> Iterator { guard let result = PyObject_GetIter(borrowedPyObject) else { try! throwPythonErrorIfPresent() @@ -1610,47 +1611,47 @@ final class PyFunction { case varArgs case varArgsWithKeywords } - + /// Allows `PyFunction` to store Python functions with more than one possible calling convention var callingConvention: CallingConvention - + /// `arguments` is a Python tuple. typealias VarArgsFunction = ( _ arguments: PythonObject) throws -> PythonConvertible - + /// `arguments` is a Python tuple. /// `keywordArguments` is an OrderedDict in Python 3.6 and later, or a dict otherwise. typealias VarArgsWithKeywordsFunction = ( _ arguments: PythonObject, _ keywordArguments: PythonObject) throws -> PythonConvertible - + /// Has the same memory layout as any other function with the Swift calling convention private typealias Storage = () throws -> PythonConvertible - + /// Stores all function pointers in the same stored property. `callAsFunction` casts this into the desired type. private var callSwiftFunction: Storage - + init(_ callSwiftFunction: @escaping VarArgsFunction) { self.callingConvention = .varArgs self.callSwiftFunction = unsafeBitCast(callSwiftFunction, to: Storage.self) } - + init(_ callSwiftFunction: @escaping VarArgsWithKeywordsFunction) { self.callingConvention = .varArgsWithKeywords self.callSwiftFunction = unsafeBitCast(callSwiftFunction, to: Storage.self) } - + private func checkConvention(_ calledConvention: CallingConvention) { precondition(callingConvention == calledConvention, "Called PyFunction with convention \(calledConvention), but expected \(callingConvention)") } - + func callAsFunction(_ argumentsTuple: PythonObject) throws -> PythonConvertible { checkConvention(.varArgs) let callSwiftFunction = unsafeBitCast(self.callSwiftFunction, to: VarArgsFunction.self) return try callSwiftFunction(argumentsTuple) } - + func callAsFunction(_ argumentsTuple: PythonObject, _ keywordArguments: PythonObject) throws -> PythonConvertible { checkConvention(.varArgsWithKeywords) let callSwiftFunction = unsafeBitCast(self.callSwiftFunction, to: VarArgsWithKeywordsFunction.self) @@ -1661,21 +1662,21 @@ final class PyFunction { public struct PythonFunction { /// Called directly by the Python C API private var function: PyFunction - + @_disfavoredOverload public init(_ fn: @escaping (PythonObject) throws -> PythonConvertible) { function = PyFunction { argumentsAsTuple in return try fn(argumentsAsTuple[0]) } } - + /// For cases where the Swift function should accept more (or less) than one parameter, accept an ordered array of all arguments instead. public init(_ fn: @escaping ([PythonObject]) throws -> PythonConvertible) { function = PyFunction { argumentsAsTuple in return try fn(argumentsAsTuple.map { $0 }) } } - + /// For cases where the Swift function should accept keyword arguments as `**kwargs` in Python. /// `**kwargs` must preserve order from Python 3.6 onward, similarly to /// Swift `KeyValuePairs` and unlike `Dictionary`. `KeyValuePairs` cannot be @@ -1751,7 +1752,7 @@ fileprivate extension PythonFunction { return pointer }() - + static let sharedMethodWithKeywordsDefinition: UnsafeMutablePointer = { let name: StaticString = "pythonkit_swift_function_with_keywords" // `utf8Start` is a property of StaticString, thus, it has a stable pointer. @@ -1793,7 +1794,7 @@ fileprivate extension PythonFunction { return nil // This must only be `nil` if an exception has been set } } - + private static let sharedMethodWithKeywordsImplementation: @convention(c) ( PyObjectPointer?, PyObjectPointer?, PyObjectPointer? ) -> PyObjectPointer? = { context, argumentsPointer, keywordArgumentsPointer in @@ -1864,16 +1865,16 @@ struct PyMethodDef { public struct PythonInstanceMethod { private var function: PythonFunction - + @_disfavoredOverload public init(_ fn: @escaping (PythonObject) throws -> PythonConvertible) { function = PythonFunction(fn) } - + public init(_ fn: @escaping ([PythonObject]) throws -> PythonConvertible) { function = PythonFunction(fn) } - + public init(_ fn: @escaping ([PythonObject], [(key: String, value: PythonObject)]) throws -> PythonConvertible) { function = PythonFunction(fn) } @@ -1893,18 +1894,18 @@ extension PythonInstanceMethod : PythonConvertible { public struct PythonClass { private var typeObject: PythonObject - + public struct Members: ExpressibleByDictionaryLiteral { public typealias Key = String public typealias Value = PythonConvertible - + var dictionary: [String: PythonObject] - + public init(dictionaryLiteral elements: (Key, Value)...) { let castedElements = elements.map { (key, value) in (key, value.pythonObject) } - + dictionary = Dictionary(castedElements, uniquingKeysWith: { _, _ in fatalError("Dictionary literal contains duplicate keys") }) @@ -1914,14 +1915,14 @@ public struct PythonClass { public init(_ name: String, superclasses: [PythonObject] = [], members: Members = [:]) { self.init(name, superclasses: superclasses, members: members.dictionary) } - + @_disfavoredOverload public init(_ name: String, superclasses: [PythonObject] = [], members: [String: PythonObject] = [:]) { var trueSuperclasses = superclasses if !trueSuperclasses.contains(Python.object) { trueSuperclasses.append(Python.object) } - + let superclassesTuple = PythonObject(tupleContentsOf: trueSuperclasses) typeObject = Python.type(name, superclassesTuple, members.pythonObject) } From 65bc8a68783e6818ed784a6128fbf83fbf51a9de Mon Sep 17 00:00:00 2001 From: Jeffrey Davis Date: Tue, 16 Jul 2024 09:55:21 -0700 Subject: [PATCH 02/24] Add plumbing to set PythonFunction names in Python. --- PythonKit/Python.swift | 80 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 75 insertions(+), 5 deletions(-) diff --git a/PythonKit/Python.swift b/PythonKit/Python.swift index 24897ec..8b4e0c2 100644 --- a/PythonKit/Python.swift +++ b/PythonKit/Python.swift @@ -1663,6 +1663,9 @@ public struct PythonFunction { /// Called directly by the Python C API private var function: PyFunction + /// Name of the function in Python. + private var name: StaticString? + @_disfavoredOverload public init(_ fn: @escaping (PythonObject) throws -> PythonConvertible) { function = PyFunction { argumentsAsTuple in @@ -1670,6 +1673,12 @@ public struct PythonFunction { } } + @_disfavoredOverload + public init(name: StaticString?, _ fn: @escaping (PythonObject) throws -> PythonConvertible) { + self.init(fn) + self.name = name + } + /// For cases where the Swift function should accept more (or less) than one parameter, accept an ordered array of all arguments instead. public init(_ fn: @escaping ([PythonObject]) throws -> PythonConvertible) { function = PyFunction { argumentsAsTuple in @@ -1677,6 +1686,11 @@ public struct PythonFunction { } } + public init(name: StaticString?, _ fn: @escaping ([PythonObject]) throws -> PythonConvertible) { + self.init(fn) + self.name = name + } + /// For cases where the Swift function should accept keyword arguments as `**kwargs` in Python. /// `**kwargs` must preserve order from Python 3.6 onward, similarly to /// Swift `KeyValuePairs` and unlike `Dictionary`. `KeyValuePairs` cannot be @@ -1691,6 +1705,11 @@ public struct PythonFunction { return try fn(argumentsAsTuple.map { $0 }, kwargs) } } + + public init(name: StaticString?, _ fn: @escaping ([PythonObject], [(key: String, value: PythonObject)]) throws -> PythonConvertible) { + self.init(fn) + self.name = name + } } extension PythonFunction : PythonConvertible { @@ -1714,12 +1733,22 @@ extension PythonFunction : PythonConvertible { ) var methodDefinition: UnsafeMutablePointer - switch function.callingConvention { - case .varArgs: - methodDefinition = PythonFunction.sharedMethodDefinition - case .varArgsWithKeywords: - methodDefinition = PythonFunction.sharedMethodWithKeywordsDefinition + if let name { + switch function.callingConvention { + case .varArgs: + methodDefinition = PythonFunction.sharedMethodDefinition(name: name) + case .varArgsWithKeywords: + methodDefinition = PythonFunction.sharedMethodWithKeywordsDefinition(name: name) + } + } else { + switch function.callingConvention { + case .varArgs: + methodDefinition = PythonFunction.sharedMethodDefinition + case .varArgsWithKeywords: + methodDefinition = PythonFunction.sharedMethodWithKeywordsDefinition + } } + let pyFuncPointer = PyCFunction_NewEx( methodDefinition, capsulePointer, @@ -1753,6 +1782,26 @@ fileprivate extension PythonFunction { return pointer }() + static func sharedMethodDefinition(name: StaticString) -> UnsafeMutablePointer { + let namePointer = UnsafeRawPointer(name.utf8Start).assumingMemoryBound(to: Int8.self) + + let methodImplementationPointer = unsafeBitCast( + PythonFunction.sharedMethodImplementation, to: OpaquePointer.self) + + /// The standard calling convention. See Python C API docs + let METH_VARARGS = 0x0001 as Int32 + + let pointer = UnsafeMutablePointer.allocate(capacity: 1) + pointer.pointee = PyMethodDef( + ml_name: namePointer, + ml_meth: methodImplementationPointer, + ml_flags: METH_VARARGS, + ml_doc: nil + ) + + return pointer + } + static let sharedMethodWithKeywordsDefinition: UnsafeMutablePointer = { let name: StaticString = "pythonkit_swift_function_with_keywords" // `utf8Start` is a property of StaticString, thus, it has a stable pointer. @@ -1776,6 +1825,27 @@ fileprivate extension PythonFunction { return pointer }() + static func sharedMethodWithKeywordsDefinition(name: StaticString) -> UnsafeMutablePointer { + let namePointer = UnsafeRawPointer(name.utf8Start).assumingMemoryBound(to: Int8.self) + + let methodImplementationPointer = unsafeBitCast( + PythonFunction.sharedMethodWithKeywordsImplementation, to: OpaquePointer.self) + + /// A combination of flags that supports `**kwargs`. See Python C API docs + let METH_VARARGS = 0x0001 as Int32 + let METH_KEYWORDS = 0x0002 as Int32 + + let pointer = UnsafeMutablePointer.allocate(capacity: 1) + pointer.pointee = PyMethodDef( + ml_name: namePointer, + ml_meth: methodImplementationPointer, + ml_flags: METH_VARARGS | METH_KEYWORDS, + ml_doc: nil + ) + + return pointer + } + private static let sharedMethodImplementation: @convention(c) ( PyObjectPointer?, PyObjectPointer? ) -> PyObjectPointer? = { context, argumentsPointer in From d710d61ac824f6f08798d1798adead4558e00ef7 Mon Sep 17 00:00:00 2001 From: Jeffrey Davis Date: Tue, 6 Aug 2024 12:07:42 -0700 Subject: [PATCH 03/24] [WIP] Add PyModule and PyType support. --- PythonKit/NumpyConversion.swift | 8 +- PythonKit/Python.swift | 22 ++- PythonKit/PythonLibrary+Symbols.swift | 12 ++ PythonKit/PythonLibrary.swift | 61 +++---- PythonKit/PythonModule.swift | 159 +++++++++++++++++++ Tests/PythonKitTests/PythonModuleTests.swift | 9 ++ 6 files changed, 233 insertions(+), 38 deletions(-) create mode 100644 PythonKit/PythonModule.swift create mode 100644 Tests/PythonKitTests/PythonModuleTests.swift diff --git a/PythonKit/NumpyConversion.swift b/PythonKit/NumpyConversion.swift index fc73a33..e0c81a2 100644 --- a/PythonKit/NumpyConversion.swift +++ b/PythonKit/NumpyConversion.swift @@ -110,18 +110,18 @@ where Element : NumpyScalarCompatible { guard Element.numpyScalarTypes.contains(numpyArray.dtype) else { return nil } - + // Only 1-D `ndarray` instances can be converted to `Array`. let pyShape = numpyArray.__array_interface__["shape"] guard let shape = Array(pyShape) else { return nil } guard shape.count == 1 else { return nil } - + // Make sure that the array is contiguous in memory. This does a copy if // the array is not already contiguous in memory. let contiguousNumpyArray = np.ascontiguousarray(numpyArray) - + guard let ptrVal = UInt(contiguousNumpyArray.__array_interface__["data"].tuple2.0) else { return nil @@ -137,7 +137,7 @@ where Element : NumpyScalarCompatible { self.init(repeating: dummyPointer.move(), count: scalarCount) dummyPointer.deallocate() withUnsafeMutableBufferPointer { buffPtr in - buffPtr.baseAddress!.assign(from: ptr, count: scalarCount) + buffPtr.baseAddress!.update(from: ptr, count: scalarCount) } } } diff --git a/PythonKit/Python.swift b/PythonKit/Python.swift index 8b4e0c2..a782c6a 100644 --- a/PythonKit/Python.swift +++ b/PythonKit/Python.swift @@ -1736,9 +1736,9 @@ extension PythonFunction : PythonConvertible { if let name { switch function.callingConvention { case .varArgs: - methodDefinition = PythonFunction.sharedMethodDefinition(name: name) + methodDefinition = PythonFunction.methodDefinition(name: name) case .varArgsWithKeywords: - methodDefinition = PythonFunction.sharedMethodWithKeywordsDefinition(name: name) + methodDefinition = PythonFunction.methodWithKeywordsDefinition(name: name) } } else { switch function.callingConvention { @@ -1782,7 +1782,7 @@ fileprivate extension PythonFunction { return pointer }() - static func sharedMethodDefinition(name: StaticString) -> UnsafeMutablePointer { + static func methodDefinition(name: StaticString) -> UnsafeMutablePointer { let namePointer = UnsafeRawPointer(name.utf8Start).assumingMemoryBound(to: Int8.self) let methodImplementationPointer = unsafeBitCast( @@ -1825,7 +1825,7 @@ fileprivate extension PythonFunction { return pointer }() - static func sharedMethodWithKeywordsDefinition(name: StaticString) -> UnsafeMutablePointer { + static func methodWithKeywordsDefinition(name: StaticString) -> UnsafeMutablePointer { let namePointer = UnsafeRawPointer(name.utf8Start).assumingMemoryBound(to: Int8.self) let methodImplementationPointer = unsafeBitCast( @@ -1911,6 +1911,20 @@ fileprivate extension PythonFunction { } } +//===----------------------------------------------------------------------===// +// PythonAwaitableFunction - create async functions in Swift that can be +// awaited from Python. +//===----------------------------------------------------------------------===// + +final class PyAwaitableFunction { + +} + +public struct PythonAwaitableFunction { + +} + + extension PythonObject: Error {} // From Python's C Headers: diff --git a/PythonKit/PythonLibrary+Symbols.swift b/PythonKit/PythonLibrary+Symbols.swift index c4db681..ae44208 100644 --- a/PythonKit/PythonLibrary+Symbols.swift +++ b/PythonKit/PythonLibrary+Symbols.swift @@ -51,6 +51,18 @@ let PyImport_ImportModule: @convention(c) ( PyCCharPointer) -> PyObjectPointer? = PythonLibrary.loadSymbol(name: "PyImport_ImportModule") +// The 2nd argument is the api version. +let PyModule_Create: @convention(c) ( + PyModuleDefPointer, Int) -> PyObjectPointer = + PythonLibrary.loadSymbol(name: "PyModule_Create2") + +let PyModule_AddObject: @convention(c) ( + PyObjectPointer, PyCCharPointer, PyObjectPointer) -> Int = + PythonLibrary.loadSymbol(name: "PyModule_AddObject") + +let PyType_Ready: @convention(c) (PyTypeObjectPointer) -> Int = + PythonLibrary.loadSymbol(name: "PyType_Ready") + let PyEval_GetBuiltins: @convention(c) () -> PyObjectPointer = PythonLibrary.loadSymbol(name: "PyEval_GetBuiltins") diff --git a/PythonKit/PythonLibrary.swift b/PythonKit/PythonLibrary.swift index 7befe64..5755821 100644 --- a/PythonKit/PythonLibrary.swift +++ b/PythonKit/PythonLibrary.swift @@ -32,7 +32,7 @@ import WinSDK public struct PythonLibrary { public enum Error: Swift.Error, Equatable, CustomStringConvertible { case pythonLibraryNotFound - + public var description: String { switch self { case .pythonLibraryNotFound: @@ -43,23 +43,23 @@ public struct PythonLibrary { } } } - + private static let pythonInitializeSymbolName = "Py_Initialize" private static let pythonLegacySymbolName = "PyString_AsString" - + #if canImport(Darwin) private static let defaultLibraryHandle = UnsafeMutableRawPointer(bitPattern: -2) // RTLD_DEFAULT #else private static let defaultLibraryHandle: UnsafeMutableRawPointer? = nil // RTLD_DEFAULT #endif - + private static var isPythonLibraryLoaded = false private static var _pythonLibraryHandle: UnsafeMutableRawPointer? private static var pythonLibraryHandle: UnsafeMutableRawPointer? { try! PythonLibrary.loadLibrary() return self._pythonLibraryHandle } - + /// Tries to load the Python library, will throw an error if no compatible library is found. public static func loadLibrary() throws { guard !self.isPythonLibraryLoaded else { return } @@ -70,7 +70,7 @@ public struct PythonLibrary { self.isPythonLibraryLoaded = true self._pythonLibraryHandle = pythonLibraryHandle } - + private static let isLegacyPython: Bool = { let isLegacyPython = PythonLibrary.loadSymbol(PythonLibrary.pythonLibraryHandle, PythonLibrary.pythonLegacySymbolName) != nil if isLegacyPython { @@ -78,16 +78,17 @@ public struct PythonLibrary { } return isLegacyPython }() - + internal static func loadSymbol( name: String, legacyName: String? = nil, type: T.Type = T.self) -> T { var name = name if let legacyName = legacyName, self.isLegacyPython { name = legacyName } - + log("Loading symbol '\(name)' from the Python library...") - return unsafeBitCast(self.loadSymbol(self.pythonLibraryHandle, name), to: type) + let ptr = self.loadSymbol(self.pythonLibraryHandle, name) + return unsafeBitCast(ptr, to: type) } } @@ -95,9 +96,9 @@ public struct PythonLibrary { extension PythonLibrary { private static let supportedMajorVersions: [Int] = [3, 2] private static let supportedMinorVersions: [Int] = Array(0...30).reversed() - + private static let libraryPathVersionCharacter: Character = ":" - + #if canImport(Darwin) private static var libraryNames = ["Python.framework/Versions/:/Python"] private static var libraryPathExtensions = [""] @@ -114,7 +115,7 @@ extension PythonLibrary { private static var librarySearchPaths = [""] private static var libraryVersionSeparator = "" #endif - + private static let libraryPaths: [String] = { var libraryPaths: [String] = [] for librarySearchPath in librarySearchPaths { @@ -128,7 +129,7 @@ extension PythonLibrary { } return libraryPaths }() - + private static func loadSymbol( _ libraryHandle: UnsafeMutableRawPointer?, _ name: String) -> UnsafeMutableRawPointer? { #if os(Windows) @@ -141,12 +142,12 @@ extension PythonLibrary { return dlsym(libraryHandle, name) #endif } - + private static func isPythonLibraryLoaded(at pythonLibraryHandle: UnsafeMutableRawPointer? = nil) -> Bool { let pythonLibraryHandle = pythonLibraryHandle ?? self.defaultLibraryHandle return self.loadSymbol(pythonLibraryHandle, self.pythonInitializeSymbolName) != nil } - + private static func loadPythonLibrary() -> UnsafeMutableRawPointer? { let pythonLibraryHandle: UnsafeMutableRawPointer? if self.isPythonLibraryLoaded() { @@ -160,7 +161,7 @@ extension PythonLibrary { } return pythonLibraryHandle } - + private static func findAndLoadExternalPythonLibrary() -> UnsafeMutableRawPointer? { for majorVersion in supportedMajorVersions { for minorVersion in supportedMinorVersions { @@ -176,11 +177,11 @@ extension PythonLibrary { } return nil } - + private static func loadPythonLibrary( at path: String, version: PythonVersion) -> UnsafeMutableRawPointer? { let versionString = version.versionString - + if let requiredPythonVersion = Environment.version.value { let requiredMajorVersion = Int(requiredPythonVersion) if requiredPythonVersion != versionString, @@ -188,7 +189,7 @@ extension PythonLibrary { return nil } } - + let libraryVersionString = versionString .split(separator: PythonVersion.versionSeparator) .joined(separator: libraryVersionSeparator) @@ -196,7 +197,7 @@ extension PythonLibrary { .joined(separator: libraryVersionString) return self.loadPythonLibrary(at: path) } - + private static func loadPythonLibrary(at path: String) -> UnsafeMutableRawPointer? { self.log("Trying to load library at '\(path)'...") #if os(Windows) @@ -206,7 +207,7 @@ extension PythonLibrary { // modules may depend on this .so file. let pythonLibraryHandle = dlopen(path, RTLD_LAZY | RTLD_GLOBAL) #endif - + if pythonLibraryHandle != nil { self.log("Library at '\(path)' was successfully loaded.") } @@ -222,7 +223,7 @@ extension PythonLibrary { has already been loaded. """) } - + /// Use the Python library with the specified version. /// - Parameters: /// - major: Major version or nil to use any Python version. @@ -232,7 +233,7 @@ extension PythonLibrary { let version = PythonVersion(major: major, minor: minor) PythonLibrary.Environment.version.set(version.versionString) } - + /// Use the Python library at the specified path. /// - Parameter path: Path of the Python library to load or nil to use the default search path. public static func useLibrary(at path: String?) { @@ -246,9 +247,9 @@ extension PythonLibrary { private struct PythonVersion { let major: Int? let minor: Int? - + static let versionSeparator: Character = "." - + init(major: Int?, minor: Int?) { precondition(!(major == nil && minor != nil), """ Error: The Python library minor version cannot be specified \ @@ -257,7 +258,7 @@ extension PythonLibrary { self.major = major self.minor = minor } - + var versionString: String { guard let major = major else { return "" } var versionString = String(major) @@ -274,22 +275,22 @@ extension PythonLibrary { private enum Environment: String { private static let keyPrefix = "PYTHON" private static let keySeparator = "_" - + case library = "LIBRARY" case version = "VERSION" case loaderLogging = "LOADER_LOGGING" - + var key: String { return Environment.keyPrefix + Environment.keySeparator + rawValue } - + var value: String? { guard let cString = getenv(key) else { return nil } let value = String(cString: cString) guard !value.isEmpty else { return nil } return value } - + func set(_ value: String) { #if os(Windows) _putenv_s(key, value) diff --git a/PythonKit/PythonModule.swift b/PythonKit/PythonModule.swift new file mode 100644 index 0000000..84b7806 --- /dev/null +++ b/PythonKit/PythonModule.swift @@ -0,0 +1,159 @@ +//===--PythonModule.swift -------------------------------------------------===// +// This file defines types related to Python Modules and an interop layer. +//===----------------------------------------------------------------------===// + +//===----------------------------------------------------------------------===// + +typealias PyModuleDefPointer = UnsafeMutableRawPointer +typealias PyTypeObjectPointer = UnsafeMutableRawPointer + +// This will be 3 for the lifetime of Python 3. See PEP-384. +let PyAbiVersion: Int = 3 + +//===----------------------------------------------------------------------===// +// PythonModule Types. +//===----------------------------------------------------------------------===// + +struct PyObject { + var ob_refcnt: UInt32 + var ob_type: OpaquePointer? +} + +struct PyVarObject { + var ob_base: PyObject + var ob_size: Int32 +} + +struct PyModuleDef_Base { + var ob_base: PyObject + var m_init: OpaquePointer? + var m_index: Int32 + var m_copy: OpaquePointer? +} + +struct PyModuleDef_Slot { + var slot: Int32 + var value: OpaquePointer +} + +struct PyModuleDef { + var m_base: PyModuleDef_Base + var m_name: UnsafePointer + var m_doc: UnsafePointer? + var m_size: Int32 + var m_methods: UnsafePointer? + var m_slots: UnsafePointer? + var m_traverse: OpaquePointer? + var m_clear: OpaquePointer? + var m_free: OpaquePointer? +} + +//===----------------------------------------------------------------------===// +// PythonType Types. +//===----------------------------------------------------------------------===// + +struct PyAsyncMethods { + var am_await: OpaquePointer? + var am_aiter: OpaquePointer? + var am_anext: OpaquePointer? +} + +struct PyGetSetDef { + var name: UnsafePointer + var get: OpaquePointer? + var set: OpaquePointer? + var doc: UnsafePointer? + var closure: OpaquePointer? +} + +struct PyTypeObject { + var ob_base: PyVarObject + var tp_name: UnsafePointer + var tp_basicsize: Int32 + var tp_itemsize: Int32 + var tp_dealloc: OpaquePointer? + var tp_vectorcall_offset: Int32? + var tp_getattr: OpaquePointer? + var tp_setattr: OpaquePointer? + var tp_as_async: UnsafePointer? + var tp_repr: OpaquePointer? + var tp_as_number: OpaquePointer? + var tp_as_sequence: OpaquePointer? + var tp_as_mapping: OpaquePointer? + var tp_hash: OpaquePointer? + var tp_call: OpaquePointer? + var tp_str: OpaquePointer? + var tp_getattro: OpaquePointer? + var tp_setattro: OpaquePointer? + var tp_as_buffer: OpaquePointer? + var tp_flags: Int32 + var tp_doc: UnsafePointer? + var tp_traverse: OpaquePointer? + var tp_clear: OpaquePointer? + var tp_richcompare: OpaquePointer? + var tp_weaklistoffset: Int32 + var tp_iter: OpaquePointer? + var tp_iternext: OpaquePointer? + var tp_methods: UnsafePointer? + var tp_members: UnsafePointer>? + var tp_getset: UnsafePointer? + var tp_base: OpaquePointer? + var tp_dict: OpaquePointer? + var tp_descr_get: OpaquePointer? + var tp_descr_set: OpaquePointer? + var tp_dictoffset: Int32 + var tp_init: OpaquePointer? + var tp_alloc: OpaquePointer? + var tp_new: OpaquePointer? + var tp_free: OpaquePointer? + var tp_is_gc: OpaquePointer? + var tp_bases: OpaquePointer? + var tp_mro: OpaquePointer? + var tp_cache: OpaquePointer? + var tp_subclasses: OpaquePointer? + var tp_weaklist: OpaquePointer? + var tp_del: OpaquePointer? + var tp_version_tag: UInt32 + var tp_finalize: OpaquePointer? + var tp_vectorcall: OpaquePointer? + var tp_watched: UInt8 + var tp_versions_used: UInt16 +} + +//===----------------------------------------------------------------------===// +// PythonModule +//===----------------------------------------------------------------------===// + +struct PythonModule { + private static let moduleName: StaticString = "PythonKit" + private static let moduleDef = PyModuleDef( + m_base: PyModuleDef_Base( + ob_base: PyObject( + ob_refcnt: UInt32.max, // _Py_IMMORTAL_REFCNT + ob_type: nil + ), + m_init: nil, + m_index: 0, + m_copy: nil + ), + m_name: UnsafeRawPointer(moduleName.utf8Start).assumingMemoryBound(to: Int8.self), + m_doc: nil, + m_size: -1, + m_methods: nil, + m_slots: nil, + m_traverse: nil, + m_clear: nil, + m_free: nil + ) +} + +extension PythonModule: PythonConvertible { + public var pythonObject: PythonObject { + let moduleDefinition: UnsafeMutablePointer = .allocate(capacity: 1) + moduleDefinition.pointee = Self.moduleDef + + _ = Python // Ensure Python is initialized. + let pyModPointer = PyModule_Create(moduleDefinition, PyAbiVersion) + return PythonObject(consuming: pyModPointer) + } +} diff --git a/Tests/PythonKitTests/PythonModuleTests.swift b/Tests/PythonKitTests/PythonModuleTests.swift new file mode 100644 index 0000000..78090a7 --- /dev/null +++ b/Tests/PythonKitTests/PythonModuleTests.swift @@ -0,0 +1,9 @@ +import XCTest +@testable import PythonKit + +class PythonModuleTests: XCTestCase { + func testPythonModule() { + let module = PythonModule() + XCTAssertNotNil(module.pythonObject) + } +} From 09fa36264b706499c69d4707501d02b5551d8d0a Mon Sep 17 00:00:00 2001 From: Jeffrey Davis Date: Tue, 6 Aug 2024 17:41:08 -0700 Subject: [PATCH 04/24] [WIP] Add initial awaitable type boilerplate. --- PythonKit/Python.swift | 113 ++++++++++++++++++- PythonKit/PythonModule.swift | 43 ++++--- Tests/PythonKitTests/PythonModuleTests.swift | 2 + 3 files changed, 138 insertions(+), 20 deletions(-) diff --git a/PythonKit/Python.swift b/PythonKit/Python.swift index a782c6a..f93e6d7 100644 --- a/PythonKit/Python.swift +++ b/PythonKit/Python.swift @@ -1916,14 +1916,123 @@ fileprivate extension PythonFunction { // awaited from Python. //===----------------------------------------------------------------------===// -final class PyAwaitableFunction { +extension PythonModule { + func addObject(_ object: PyObjectPointer, named: StaticString) -> Bool { + let module = pythonObject.borrowedPyObject + let result = PyModule_AddObject( + module, + UnsafeRawPointer(named.utf8Start).assumingMemoryBound(to: Int8.self), + object) + guard result >= 0 else { + fatalError("Failed to add type to module: \(result)") + } + return true + } + + func addType(_ typeDefinition: PyTypeObjectPointer) -> Bool { + let result = PyType_Ready(typeDefinition) + guard result >= 0 else { + fatalError("Failed to add type to module: \(result)") + } + return true + } +} + +struct PyAwaitableFunction { + var ob_base: PyObject } public struct PythonAwaitableFunction { + static var registered = false -} + static let sharedTypeObject: UnsafeMutablePointer = { + let pythonAwaitableFunctionName: StaticString = "PythonKit.Awaitable" + let pythonAwaitableFunctionDoc: StaticString = "PythonKit Awaitable Functions" + let pythonAwaitableFunctionAsyncMethods = UnsafeMutablePointer.allocate(capacity: 1) + pythonAwaitableFunctionAsyncMethods.pointee = PyAsyncMethods( + am_await: nil, + am_aiter: nil, + am_anext: nil + ) + + let pythonAwaitableFunctionTypeObject = UnsafeMutablePointer.allocate(capacity: 1) + pythonAwaitableFunctionTypeObject.pointee = PyTypeObject( + ob_base: PyVarObject( + ob_base: PyObject( + ob_refcnt: PyImmortalRefCount, + ob_type: nil + ), + ob_size: 0 + ), + tp_name: UnsafeRawPointer(pythonAwaitableFunctionName.utf8Start).assumingMemoryBound(to: Int8.self), + tp_basicsize: MemoryLayout.size, + tp_itemsize: 0, + tp_dealloc: nil, + tp_vectorcall_offset: 0, + tp_getattr: nil, + tp_setattr: nil, + tp_as_async: pythonAwaitableFunctionAsyncMethods, + tp_repr: nil, + tp_as_number: nil, + tp_as_sequence: nil, + tp_as_mapping: nil, + tp_hash: nil, + tp_call: nil, + tp_str: nil, + tp_getattro: nil, + tp_setattro: nil, + tp_as_buffer: nil, + tp_flags: PyTPFlagsDefault, + tp_doc: UnsafeRawPointer(pythonAwaitableFunctionDoc.utf8Start).assumingMemoryBound(to: Int8.self), + tp_traverse: nil, + tp_clear: nil, + tp_richcompare: nil, + tp_weaklistoffset: 0, + tp_iter: nil, + tp_iternext: nil, + tp_methods: nil, + tp_members: nil, + tp_getset: nil, + tp_base: nil, + tp_dict: nil, + tp_descr_get: nil, + tp_descr_set: nil, + tp_dictoffset: 0, + tp_init: nil, + tp_alloc: nil, + tp_new: nil, + tp_free: nil, + tp_is_gc: nil, + tp_bases: nil, + tp_mro: nil, + tp_cache: nil, + tp_subclasses: nil, + tp_weaklist: nil, + tp_del: nil, + tp_version_tag: 0, + tp_finalize: nil, + tp_vectorcall: nil, + tp_watched: 0, + tp_versions_used: 0) + + return pythonAwaitableFunctionTypeObject + }() + + static func ensurePythonAwaitableType(in module: PythonModule) { + guard !registered else { + return + } + guard module.addType(PythonAwaitableFunction.sharedTypeObject) else { + return + } + guard module.addObject(PythonAwaitableFunction.sharedTypeObject, named: "Awaitable") else { + return + } + registered = true + } +} extension PythonObject: Error {} diff --git a/PythonKit/PythonModule.swift b/PythonKit/PythonModule.swift index 84b7806..543864f 100644 --- a/PythonKit/PythonModule.swift +++ b/PythonKit/PythonModule.swift @@ -10,29 +10,35 @@ typealias PyTypeObjectPointer = UnsafeMutableRawPointer // This will be 3 for the lifetime of Python 3. See PEP-384. let PyAbiVersion: Int = 3 +// From the C headers (we're not stackless). +let PyTPFlagsDefault: Int = 0 + +// The immortal value is 0xFFFFFFFF. +let PyImmortalRefCount: Int = Int(UInt32.max) + //===----------------------------------------------------------------------===// // PythonModule Types. //===----------------------------------------------------------------------===// struct PyObject { - var ob_refcnt: UInt32 + var ob_refcnt: Int var ob_type: OpaquePointer? } struct PyVarObject { var ob_base: PyObject - var ob_size: Int32 + var ob_size: Int } struct PyModuleDef_Base { var ob_base: PyObject var m_init: OpaquePointer? - var m_index: Int32 + var m_index: Int var m_copy: OpaquePointer? } struct PyModuleDef_Slot { - var slot: Int32 + var slot: Int var value: OpaquePointer } @@ -40,7 +46,7 @@ struct PyModuleDef { var m_base: PyModuleDef_Base var m_name: UnsafePointer var m_doc: UnsafePointer? - var m_size: Int32 + var m_size: Int var m_methods: UnsafePointer? var m_slots: UnsafePointer? var m_traverse: OpaquePointer? @@ -69,10 +75,10 @@ struct PyGetSetDef { struct PyTypeObject { var ob_base: PyVarObject var tp_name: UnsafePointer - var tp_basicsize: Int32 - var tp_itemsize: Int32 + var tp_basicsize: Int + var tp_itemsize: Int var tp_dealloc: OpaquePointer? - var tp_vectorcall_offset: Int32? + var tp_vectorcall_offset: Int? var tp_getattr: OpaquePointer? var tp_setattr: OpaquePointer? var tp_as_async: UnsafePointer? @@ -86,12 +92,12 @@ struct PyTypeObject { var tp_getattro: OpaquePointer? var tp_setattro: OpaquePointer? var tp_as_buffer: OpaquePointer? - var tp_flags: Int32 + var tp_flags: Int var tp_doc: UnsafePointer? var tp_traverse: OpaquePointer? var tp_clear: OpaquePointer? var tp_richcompare: OpaquePointer? - var tp_weaklistoffset: Int32 + var tp_weaklistoffset: Int var tp_iter: OpaquePointer? var tp_iternext: OpaquePointer? var tp_methods: UnsafePointer? @@ -101,7 +107,7 @@ struct PyTypeObject { var tp_dict: OpaquePointer? var tp_descr_get: OpaquePointer? var tp_descr_set: OpaquePointer? - var tp_dictoffset: Int32 + var tp_dictoffset: Int var tp_init: OpaquePointer? var tp_alloc: OpaquePointer? var tp_new: OpaquePointer? @@ -113,7 +119,7 @@ struct PyTypeObject { var tp_subclasses: OpaquePointer? var tp_weaklist: OpaquePointer? var tp_del: OpaquePointer? - var tp_version_tag: UInt32 + var tp_version_tag: UInt var tp_finalize: OpaquePointer? var tp_vectorcall: OpaquePointer? var tp_watched: UInt8 @@ -124,12 +130,12 @@ struct PyTypeObject { // PythonModule //===----------------------------------------------------------------------===// -struct PythonModule { +struct PythonModule : PythonConvertible { private static let moduleName: StaticString = "PythonKit" private static let moduleDef = PyModuleDef( m_base: PyModuleDef_Base( ob_base: PyObject( - ob_refcnt: UInt32.max, // _Py_IMMORTAL_REFCNT + ob_refcnt: PyImmortalRefCount, ob_type: nil ), m_init: nil, @@ -145,15 +151,16 @@ struct PythonModule { m_clear: nil, m_free: nil ) -} -extension PythonModule: PythonConvertible { - public var pythonObject: PythonObject { + public var pythonObject: PythonObject + + init() { let moduleDefinition: UnsafeMutablePointer = .allocate(capacity: 1) moduleDefinition.pointee = Self.moduleDef _ = Python // Ensure Python is initialized. + let pyModPointer = PyModule_Create(moduleDefinition, PyAbiVersion) - return PythonObject(consuming: pyModPointer) + pythonObject = PythonObject(consuming: pyModPointer) } } diff --git a/Tests/PythonKitTests/PythonModuleTests.swift b/Tests/PythonKitTests/PythonModuleTests.swift index 78090a7..d8470b6 100644 --- a/Tests/PythonKitTests/PythonModuleTests.swift +++ b/Tests/PythonKitTests/PythonModuleTests.swift @@ -5,5 +5,7 @@ class PythonModuleTests: XCTestCase { func testPythonModule() { let module = PythonModule() XCTAssertNotNil(module.pythonObject) + + PythonAwaitableFunction.ensurePythonAwaitableType(in: module) } } From 5ac71d7c82f16e4e1f35cb9b6bf57613f9525d95 Mon Sep 17 00:00:00 2001 From: Jeffrey Davis Date: Thu, 8 Aug 2024 14:05:04 -0700 Subject: [PATCH 05/24] [WIP] Make module and type actually work. --- PythonKit/Python.swift | 128 ++++++++++++++---- PythonKit/PythonLibrary+Symbols.swift | 18 +++ PythonKit/PythonModule.swift | 62 ++++++--- .../PythonKitTests/PythonFunctionTests.swift | 118 ++++++++-------- Tests/PythonKitTests/PythonModuleTests.swift | 12 +- temp | 1 + 6 files changed, 232 insertions(+), 107 deletions(-) create mode 100644 temp diff --git a/PythonKit/Python.swift b/PythonKit/Python.swift index f93e6d7..07a9bed 100644 --- a/PythonKit/Python.swift +++ b/PythonKit/Python.swift @@ -1941,27 +1941,51 @@ extension PythonModule { struct PyAwaitableFunction { var ob_base: PyObject + var aw_magic: Int } public struct PythonAwaitableFunction { - static var registered = false + static var ready = false - static let sharedTypeObject: UnsafeMutablePointer = { - let pythonAwaitableFunctionName: StaticString = "PythonKit.Awaitable" + var awaitable: PythonObject? + + init() { + if !Self.ready { + PythonAwaitableFunction.readyType(in: PythonAwaitableFunction.sharedModule) + } + } +} + +fileprivate extension PythonAwaitableFunction { + + static let sharedModule = PythonModule() + + static let sharedType: UnsafeMutablePointer = { + let pythonAwaitableFunctionName: StaticString = "Awaitable" let pythonAwaitableFunctionDoc: StaticString = "PythonKit Awaitable Functions" let pythonAwaitableFunctionAsyncMethods = UnsafeMutablePointer.allocate(capacity: 1) pythonAwaitableFunctionAsyncMethods.pointee = PyAsyncMethods( - am_await: nil, + am_await: PythonAwaitableFunction.next, am_aiter: nil, - am_anext: nil - ) - - let pythonAwaitableFunctionTypeObject = UnsafeMutablePointer.allocate(capacity: 1) - pythonAwaitableFunctionTypeObject.pointee = PyTypeObject( + am_anext: nil, + am_send: nil) + + let next: StaticString = "next" + let pythonAwaitableFunctionMethods = UnsafeMutablePointer.allocate(capacity: 2) + pythonAwaitableFunctionMethods[0] = PyMethodDef( + ml_name: UnsafeRawPointer(next.utf8Start).assumingMemoryBound(to: Int8.self), + ml_meth: unsafeBitCast(PythonAwaitableFunction.next, to: OpaquePointer.self), + ml_flags: 0, + ml_doc: nil) + pythonAwaitableFunctionMethods[1] = PyMethodDef( + ml_name: nil, ml_meth: nil, ml_flags: 0, ml_doc: nil) + + let pythonAwaitableFunctionType = UnsafeMutablePointer.allocate(capacity: 1) + pythonAwaitableFunctionType.initialize(to: PyTypeObject( ob_base: PyVarObject( ob_base: PyObject( - ob_refcnt: PyImmortalRefCount, + ob_refcnt: Py_ImmortalRefCount, ob_type: nil ), ob_size: 0 @@ -1969,7 +1993,7 @@ public struct PythonAwaitableFunction { tp_name: UnsafeRawPointer(pythonAwaitableFunctionName.utf8Start).assumingMemoryBound(to: Int8.self), tp_basicsize: MemoryLayout.size, tp_itemsize: 0, - tp_dealloc: nil, + tp_dealloc: PythonAwaitableFunction.dealloc, tp_vectorcall_offset: 0, tp_getattr: nil, tp_setattr: nil, @@ -1984,7 +2008,7 @@ public struct PythonAwaitableFunction { tp_getattro: nil, tp_setattro: nil, tp_as_buffer: nil, - tp_flags: PyTPFlagsDefault, + tp_flags: Py_TPFlagsDefault | Py_TPFLAGS_HEAPTYPE, tp_doc: UnsafeRawPointer(pythonAwaitableFunctionDoc.utf8Start).assumingMemoryBound(to: Int8.self), tp_traverse: nil, tp_clear: nil, @@ -1992,7 +2016,7 @@ public struct PythonAwaitableFunction { tp_weaklistoffset: 0, tp_iter: nil, tp_iternext: nil, - tp_methods: nil, + tp_methods: pythonAwaitableFunctionMethods, tp_members: nil, tp_getset: nil, tp_base: nil, @@ -2001,9 +2025,9 @@ public struct PythonAwaitableFunction { tp_descr_set: nil, tp_dictoffset: 0, tp_init: nil, - tp_alloc: nil, - tp_new: nil, - tp_free: nil, + tp_alloc: PyType_GenericAlloc, + tp_new: PythonAwaitableFunction.new, + tp_free: PyObject_Free, tp_is_gc: nil, tp_bases: nil, tp_mro: nil, @@ -2013,24 +2037,74 @@ public struct PythonAwaitableFunction { tp_del: nil, tp_version_tag: 0, tp_finalize: nil, - tp_vectorcall: nil, - tp_watched: 0, - tp_versions_used: 0) + tp_vectorcall: nil)) - return pythonAwaitableFunctionTypeObject + return pythonAwaitableFunctionType }() - static func ensurePythonAwaitableType(in module: PythonModule) { - guard !registered else { + static func readyType(in module: PythonModule) { + guard !ready else { return } - guard module.addType(PythonAwaitableFunction.sharedTypeObject) else { + guard module.addType(PythonAwaitableFunction.sharedType) else { return } - guard module.addObject(PythonAwaitableFunction.sharedTypeObject, named: "Awaitable") else { + Py_IncRef(PythonAwaitableFunction.sharedType) + guard module.addObject(PythonAwaitableFunction.sharedType, named: "Awaitable") else { return } - registered = true + ready = true + } + + static func alloc() -> PyObjectPointer { + precondition(PythonAwaitableFunction.ready, "AwaitableFunction type not ready.") + precondition(PythonAwaitableFunction.sharedType.pointee.tp_alloc != nil, "AwaitableFunction tp_alloc missing.") + + let type = PythonAwaitableFunction.sharedType + guard let tp_alloc = type.pointee.tp_alloc else { + fatalError("Failed to allocate AwaitableFunction") + } + + let result = tp_alloc(type, 0) + guard let result else { + fatalError("Failed to allocate AwaitableFunction") + } + + return result + } + + static func free(_ object: PyObjectPointer) -> Void { + precondition(PythonAwaitableFunction.ready, "AwaitableFunction type not ready.") + precondition(PythonAwaitableFunction.sharedType.pointee.tp_free != nil, "AwaitableFunction tp_free missing.") + + let type = PythonAwaitableFunction.sharedType + guard let tp_free = type.pointee.tp_free else { + fatalError("Failed to deallocate AwaitableFunction") + } + + tp_free(object) + } + + static let new: newfunc = { type, args, kwds in + precondition(PythonAwaitableFunction.ready, "AwaitableFunction type not ready.") + + let result = alloc() + + let awaitable = result.withMemoryRebound(to: PyAwaitableFunction.self, capacity: 1) { + $0.pointee.aw_magic = 0x08675309 + return $0.pointee + } + print("awaitable: \(awaitable)") + + return result + } + + static let dealloc: destructor = { object in + free(object) + } + + static let next: unaryfunc = { object in + return nil } } @@ -2039,11 +2113,11 @@ extension PythonObject: Error {} // From Python's C Headers: struct PyMethodDef { /// The name of the built-in function/method - var ml_name: UnsafePointer + var ml_name: UnsafePointer? /// The C function that implements it. /// Since this accepts multiple function signatures, the Swift type must be opaque here. - var ml_meth: OpaquePointer + var ml_meth: OpaquePointer? /// Combination of METH_xxx flags, which mostly describe the args expected by the C func var ml_flags: Int32 diff --git a/PythonKit/PythonLibrary+Symbols.swift b/PythonKit/PythonLibrary+Symbols.swift index ae44208..ad5cf3a 100644 --- a/PythonKit/PythonLibrary+Symbols.swift +++ b/PythonKit/PythonLibrary+Symbols.swift @@ -51,6 +51,17 @@ let PyImport_ImportModule: @convention(c) ( PyCCharPointer) -> PyObjectPointer? = PythonLibrary.loadSymbol(name: "PyImport_ImportModule") +let PyImport_GetModuleDict: @convention(c) () -> PyObjectPointer = + PythonLibrary.loadSymbol(name: "PyImport_GetModuleDict") + +let _PyImport_FixupExtensionObject: @convention(c) ( + PyObjectPointer, PyObjectPointer, PyObjectPointer, PyObjectPointer) -> Int = + PythonLibrary.loadSymbol(name: "_PyImport_FixupExtensionObject") + +let PyUnicode_InternFromString: @convention(c) ( + PyCCharPointer) -> PyObjectPointer = + PythonLibrary.loadSymbol(name: "PyUnicode_InternFromString") + // The 2nd argument is the api version. let PyModule_Create: @convention(c) ( PyModuleDefPointer, Int) -> PyObjectPointer = @@ -63,6 +74,13 @@ let PyModule_AddObject: @convention(c) ( let PyType_Ready: @convention(c) (PyTypeObjectPointer) -> Int = PythonLibrary.loadSymbol(name: "PyType_Ready") +let PyType_GenericAlloc: @convention(c) ( + PyTypeObjectPointer, Int) -> PyObjectPointer? = + PythonLibrary.loadSymbol(name: "PyType_GenericAlloc") + +let PyObject_Free: @convention(c) (UnsafeMutableRawPointer) -> Void = + PythonLibrary.loadSymbol(name: "PyObject_Free") + let PyEval_GetBuiltins: @convention(c) () -> PyObjectPointer = PythonLibrary.loadSymbol(name: "PyEval_GetBuiltins") diff --git a/PythonKit/PythonModule.swift b/PythonKit/PythonModule.swift index 543864f..bc16dbb 100644 --- a/PythonKit/PythonModule.swift +++ b/PythonKit/PythonModule.swift @@ -7,14 +7,24 @@ typealias PyModuleDefPointer = UnsafeMutableRawPointer typealias PyTypeObjectPointer = UnsafeMutableRawPointer +typealias allocfunc = @convention(c) (PyTypeObjectPointer, Int) -> PyObjectPointer? +typealias destructor = @convention(c) (PyObjectPointer) -> Void +typealias freefunc = @convention(c) (UnsafeMutableRawPointer) -> Void +typealias newfunc = @convention(c) (PyTypeObjectPointer, PyObjectPointer, PyObjectPointer) -> PyObjectPointer? +typealias sendfunc = @convention(c) (PyObjectPointer, PyObjectPointer, PyObjectPointer) -> Int +typealias unaryfunc = @convention(c) (PyObjectPointer) -> PyObjectPointer? + // This will be 3 for the lifetime of Python 3. See PEP-384. -let PyAbiVersion: Int = 3 +let Py_AbiVersion: Int = 3 // From the C headers (we're not stackless). -let PyTPFlagsDefault: Int = 0 +let Py_TPFlagsDefault: UInt64 = 0 + +// Our type is dynamically allocated. +let Py_TPFLAGS_HEAPTYPE: UInt64 = (1 << 9) // The immortal value is 0xFFFFFFFF. -let PyImmortalRefCount: Int = Int(UInt32.max) +let Py_ImmortalRefCount: Int = Int(UInt32.max) //===----------------------------------------------------------------------===// // PythonModule Types. @@ -22,7 +32,7 @@ let PyImmortalRefCount: Int = Int(UInt32.max) struct PyObject { var ob_refcnt: Int - var ob_type: OpaquePointer? + var ob_type: UnsafeMutablePointer? } struct PyVarObject { @@ -59,9 +69,10 @@ struct PyModuleDef { //===----------------------------------------------------------------------===// struct PyAsyncMethods { - var am_await: OpaquePointer? - var am_aiter: OpaquePointer? - var am_anext: OpaquePointer? + var am_await: unaryfunc? + var am_aiter: unaryfunc? + var am_anext: unaryfunc? + var am_send: sendfunc? } struct PyGetSetDef { @@ -77,8 +88,8 @@ struct PyTypeObject { var tp_name: UnsafePointer var tp_basicsize: Int var tp_itemsize: Int - var tp_dealloc: OpaquePointer? - var tp_vectorcall_offset: Int? + var tp_dealloc: destructor? + var tp_vectorcall_offset: Int var tp_getattr: OpaquePointer? var tp_setattr: OpaquePointer? var tp_as_async: UnsafePointer? @@ -92,7 +103,7 @@ struct PyTypeObject { var tp_getattro: OpaquePointer? var tp_setattro: OpaquePointer? var tp_as_buffer: OpaquePointer? - var tp_flags: Int + var tp_flags: UInt64 var tp_doc: UnsafePointer? var tp_traverse: OpaquePointer? var tp_clear: OpaquePointer? @@ -109,9 +120,9 @@ struct PyTypeObject { var tp_descr_set: OpaquePointer? var tp_dictoffset: Int var tp_init: OpaquePointer? - var tp_alloc: OpaquePointer? - var tp_new: OpaquePointer? - var tp_free: OpaquePointer? + var tp_alloc: allocfunc? + var tp_new: newfunc? + var tp_free: freefunc? var tp_is_gc: OpaquePointer? var tp_bases: OpaquePointer? var tp_mro: OpaquePointer? @@ -122,8 +133,6 @@ struct PyTypeObject { var tp_version_tag: UInt var tp_finalize: OpaquePointer? var tp_vectorcall: OpaquePointer? - var tp_watched: UInt8 - var tp_versions_used: UInt16 } //===----------------------------------------------------------------------===// @@ -131,11 +140,12 @@ struct PyTypeObject { //===----------------------------------------------------------------------===// struct PythonModule : PythonConvertible { - private static let moduleName: StaticString = "PythonKit" + private static let moduleName: StaticString = "pythonkit" + private static let moduleDoc: StaticString = "PythonKit Extension Module" private static let moduleDef = PyModuleDef( m_base: PyModuleDef_Base( ob_base: PyObject( - ob_refcnt: PyImmortalRefCount, + ob_refcnt: Py_ImmortalRefCount, ob_type: nil ), m_init: nil, @@ -143,7 +153,7 @@ struct PythonModule : PythonConvertible { m_copy: nil ), m_name: UnsafeRawPointer(moduleName.utf8Start).assumingMemoryBound(to: Int8.self), - m_doc: nil, + m_doc: UnsafeRawPointer(moduleDoc.utf8Start).assumingMemoryBound(to: Int8.self), m_size: -1, m_methods: nil, m_slots: nil, @@ -160,7 +170,19 @@ struct PythonModule : PythonConvertible { _ = Python // Ensure Python is initialized. - let pyModPointer = PyModule_Create(moduleDefinition, PyAbiVersion) - pythonObject = PythonObject(consuming: pyModPointer) + let module = PyModule_Create(moduleDefinition, Py_AbiVersion) + let moduleName = PyUnicode_InternFromString( + UnsafeRawPointer(Self.moduleName.utf8Start).assumingMemoryBound(to: Int8.self)) + let modules = PyImport_GetModuleDict() + let success = _PyImport_FixupExtensionObject(module, moduleName, moduleName, modules) + guard success == 0 else { + fatalError("Failed to fixup extension object.") + } + + pythonObject = PythonObject(consuming: module) } } + +extension PythonModule { + +} diff --git a/Tests/PythonKitTests/PythonFunctionTests.swift b/Tests/PythonKitTests/PythonFunctionTests.swift index 7be1afa..f11ad57 100644 --- a/Tests/PythonKitTests/PythonFunctionTests.swift +++ b/Tests/PythonKitTests/PythonFunctionTests.swift @@ -7,24 +7,24 @@ class PythonFunctionTests: XCTestCase { let versionMinor = Python.versionInfo.minor return (versionMajor == 3 && versionMinor >= 1) || versionMajor > 3 } - + func testPythonFunction() { guard canUsePythonFunction else { return } - + let pythonAdd = PythonFunction { args in let lhs = args[0] let rhs = args[1] return lhs + rhs }.pythonObject - + let pythonSum = pythonAdd(2, 3) XCTAssertNotNil(Double(pythonSum)) XCTAssertEqual(pythonSum, 5) - + // Test function with keyword arguments - + // Since there is no alternative function signature, `args` and `kwargs` // can be used without manually stating their type. This differs from // the behavior when there are no keywords. @@ -36,76 +36,76 @@ class PythonFunctionTests: XCTestCase { XCTAssertEqual(kwargs[0].value, 2) XCTAssertEqual(kwargs[1].key, "x") XCTAssertEqual(kwargs[1].value, 3) - + let conditional = Bool(args[0])! let xIndex = kwargs.firstIndex(where: { $0.key == "x" })! let yIndex = kwargs.firstIndex(where: { $0.key == "y" })! - + return kwargs[conditional ? xIndex : yIndex].value }.pythonObject - + let pythonSelectOutput = pythonSelect(true, y: 2, x: 3) XCTAssertEqual(pythonSelectOutput, 3) } - + // From https://www.geeksforgeeks.org/create-classes-dynamically-in-python func testPythonClassConstruction() { guard canUsePythonFunction else { return } - + let constructor = PythonInstanceMethod { args in let `self` = args[0] `self`.constructor_arg = args[1] return Python.None } - + // Instead of calling `print`, use this to test what would be output. var printOutput: String? - + // Example of function using an alternative syntax for `args`. let displayMethod = PythonInstanceMethod { (args: [PythonObject]) in // let `self` = args[0] printOutput = String(args[1]) return Python.None } - + let classMethodOriginal = PythonInstanceMethod { args in // let cls = args[0] printOutput = String(args[1]) return Python.None } - + // Did not explicitly convert `constructor` or `displayMethod` to // PythonObject. This is intentional, as the `PythonClass` initializer // should take any `PythonConvertible` and not just `PythonObject`. let classMethod = Python.classmethod(classMethodOriginal.pythonObject) - + let Geeks = PythonClass("Geeks", members: [ // Constructor "__init__": constructor, - + // Data members "string_attribute": "Geeks 4 geeks!", "int_attribute": 1706256, - + // Member functions "func_arg": displayMethod, "class_func": classMethod, ]).pythonObject - + let obj = Geeks("constructor argument") XCTAssertEqual(obj.constructor_arg, "constructor argument") XCTAssertEqual(obj.string_attribute, "Geeks 4 geeks!") XCTAssertEqual(obj.int_attribute, 1706256) - + obj.func_arg("Geeks for Geeks") XCTAssertEqual(printOutput, "Geeks for Geeks") - + Geeks.class_func("Class Dynamically Created!") XCTAssertEqual(printOutput, "Class Dynamically Created!") } - + // Previously, there was a build error where passing a simple // `PythonClass.Members` literal made the literal's type ambiguous. It was // confused with `[String: PythonObject]`. The solution was adding a @@ -114,7 +114,7 @@ class PythonFunctionTests: XCTestCase { guard canUsePythonFunction else { return } - + let MyClass = PythonClass( "MyClass", superclasses: [Python.object], @@ -122,76 +122,76 @@ class PythonFunctionTests: XCTestCase { "memberName": "memberValue", ] ).pythonObject - + let memberValue = MyClass().memberName XCTAssertEqual(String(memberValue), "memberValue") } - + func testPythonClassInheritance() { guard canUsePythonFunction else { return } - + var helloOutput: String? var helloWorldOutput: String? - + // Declare subclasses of `Python.Exception` - + let HelloException = PythonClass( "HelloException", superclasses: [Python.Exception], members: [ "str_prefix": "HelloException-prefix ", - + "__init__": PythonInstanceMethod { args in let `self` = args[0] let message = "hello \(args[1])" helloOutput = String(message) - + // Conventional `super` syntax does not work; use this instead. Python.Exception.__init__(`self`, message) return Python.None }, - + // Example of function using the `self` convention instead of `args`. "__str__": PythonInstanceMethod { (`self`: PythonObject) in return `self`.str_prefix + Python.repr(`self`) } ] ).pythonObject - + let HelloWorldException = PythonClass( "HelloWorldException", superclasses: [HelloException], members: [ "str_prefix": "HelloWorldException-prefix ", - + "__init__": PythonInstanceMethod { args in let `self` = args[0] let message = "world \(args[1])" helloWorldOutput = String(message) - + `self`.int_param = args[2] - + // Conventional `super` syntax does not work; use this instead. HelloException.__init__(`self`, message) return Python.None }, - + // Example of function using the `self` convention instead of `args`. "custom_method": PythonInstanceMethod { (`self`: PythonObject) in return `self`.int_param } ] ).pythonObject - + // Test that inheritance works as expected - + let error1 = HelloException("test 1") XCTAssertEqual(helloOutput, "hello test 1") XCTAssertEqual(Python.str(error1), "HelloException-prefix HelloException('hello test 1')") XCTAssertEqual(Python.repr(error1), "HelloException('hello test 1')") - + let error2 = HelloWorldException("test 1", 123) XCTAssertEqual(helloOutput, "hello world test 1") XCTAssertEqual(helloWorldOutput, "world test 1") @@ -199,15 +199,15 @@ class PythonFunctionTests: XCTestCase { XCTAssertEqual(Python.repr(error2), "HelloWorldException('hello world test 1')") XCTAssertEqual(error2.custom_method(), 123) XCTAssertNotEqual(error2.custom_method(), "123") - + // Test that subclasses behave like Python exceptions - + // Example of function with no named parameters, which can be stated // ergonomically using an underscore. The ignored input is a [PythonObject]. - let testFunction = PythonFunction { _ in + let _ = PythonFunction { _ in throw HelloWorldException("EXAMPLE ERROR MESSAGE", 2) }.pythonObject - + /* do { try testFunction.throwing.dynamicallyCall(withArguments: []) @@ -217,7 +217,7 @@ class PythonFunctionTests: XCTestCase { XCTFail("A string could not be created from a HelloWorldException.") return } - + XCTAssertTrue(description.contains("EXAMPLE ERROR MESSAGE")) XCTAssertTrue(description.contains("HelloWorldException")) } catch { @@ -225,19 +225,19 @@ class PythonFunctionTests: XCTestCase { } */ } - + // Tests the ability to dynamically construct an argument list with keywords // and instantiate a `PythonInstanceMethod` with keywords. func testPythonClassInheritanceWithKeywords() { guard canUsePythonFunction else { return } - + func getValue(key: String, kwargs: [(String, PythonObject)]) -> PythonObject { let index = kwargs.firstIndex(where: { $0.0 == key })! return kwargs[index].1 } - + // Base class has the following arguments: // __init__(): // - 1 unnamed argument @@ -247,7 +247,7 @@ class PythonFunctionTests: XCTestCase { // test_method(): // - param1 // - param2 - + let BaseClass = PythonClass( "BaseClass", superclasses: [], @@ -259,7 +259,7 @@ class PythonFunctionTests: XCTestCase { `self`.param2 = getValue(key: "param2", kwargs: kwargs) return Python.None }, - + "test_method": PythonInstanceMethod { args, kwargs in let `self` = args[0] `self`.param1 += getValue(key: "param1", kwargs: kwargs) @@ -268,7 +268,7 @@ class PythonFunctionTests: XCTestCase { } ] ).pythonObject - + // Derived class accepts the following arguments: // __init__(): // - param2 @@ -278,7 +278,7 @@ class PythonFunctionTests: XCTestCase { // - param1 // - param2 // - param3 - + let DerivedClass = PythonClass( "DerivedClass", superclasses: [], @@ -286,7 +286,7 @@ class PythonFunctionTests: XCTestCase { "__init__": PythonInstanceMethod { args, kwargs in let `self` = args[0] `self`.param3 = getValue(key: "param3", kwargs: kwargs) - + // Lists the arguments in an order different than they are // specified (self, param2, param3, param1, arg1). The // correct order is (self, arg1, param1, param2, param3). @@ -296,44 +296,44 @@ class PythonFunctionTests: XCTestCase { ("param1", 1), ("", 0) ] - + BaseClass.__init__.dynamicallyCall( withKeywordArguments: newKeywordArguments) return Python.None }, - + "test_method": PythonInstanceMethod { args, kwargs in let `self` = args[0] `self`.param3 += getValue(key: "param3", kwargs: kwargs) - + BaseClass.test_method.dynamicallyCall( withKeywordArguments: args.map { ("", $0) } + kwargs) return Python.None } ] ).pythonObject - + let derivedInstance = DerivedClass(param2: 2, param3: 3) XCTAssertEqual(derivedInstance.arg1, 0) XCTAssertEqual(derivedInstance.param1, 1) XCTAssertEqual(derivedInstance.param2, 2) XCTAssertEqual(derivedInstance.checking.param3, 3) - + derivedInstance.test_method(param1: 1, param2: 2, param3: 3) XCTAssertEqual(derivedInstance.arg1, 0) XCTAssertEqual(derivedInstance.param1, 2) XCTAssertEqual(derivedInstance.param2, 4) XCTAssertEqual(derivedInstance.checking.param3, 6) - + // Validate that subclassing and instantiating the derived class does // not affect behavior of the parent class. - + let baseInstance = BaseClass(0, param1: 10, param2: 20) XCTAssertEqual(baseInstance.arg1, 0) XCTAssertEqual(baseInstance.param1, 10) XCTAssertEqual(baseInstance.param2, 20) XCTAssertEqual(baseInstance.checking.param3, nil) - + baseInstance.test_method(param1: 10, param2: 20) XCTAssertEqual(baseInstance.arg1, 0) XCTAssertEqual(baseInstance.param1, 20) diff --git a/Tests/PythonKitTests/PythonModuleTests.swift b/Tests/PythonKitTests/PythonModuleTests.swift index d8470b6..b041779 100644 --- a/Tests/PythonKitTests/PythonModuleTests.swift +++ b/Tests/PythonKitTests/PythonModuleTests.swift @@ -6,6 +6,16 @@ class PythonModuleTests: XCTestCase { let module = PythonModule() XCTAssertNotNil(module.pythonObject) - PythonAwaitableFunction.ensurePythonAwaitableType(in: module) + let pythonKit = Python.import("pythonkit") + XCTAssertNotNil(pythonKit) + } + + func testAwaitablePythonFunction() throws { + let _ = PythonAwaitableFunction() + XCTAssertTrue(PythonAwaitableFunction.ready) + + let pythonKit = Python.import("pythonkit") + let awaitable = pythonKit.Awaitable() + XCTAssertNotNil(awaitable) } } diff --git a/temp b/temp new file mode 100644 index 0000000..9ae9ea0 --- /dev/null +++ b/temp @@ -0,0 +1 @@ +Contents \ No newline at end of file From 6e7723ea0f2a565ecc4a30a7924b130d16f6ca8a Mon Sep 17 00:00:00 2001 From: Jeffrey Davis Date: Thu, 8 Aug 2024 15:18:13 -0700 Subject: [PATCH 06/24] [WIP] cleanup a bit. --- PythonKit/Python.swift | 125 ++----------------- PythonKit/PythonModule.swift | 113 +++++++++++++++-- Tests/PythonKitTests/PythonModuleTests.swift | 6 - 3 files changed, 112 insertions(+), 132 deletions(-) diff --git a/PythonKit/Python.swift b/PythonKit/Python.swift index 07a9bed..f845d32 100644 --- a/PythonKit/Python.swift +++ b/PythonKit/Python.swift @@ -675,6 +675,9 @@ public struct PythonInterface { /// A dictionary of the Python builtins. public let builtins: PythonObject + /// Native extension module. + internal let module: PythonModule + init() { Py_Initialize() // Initialize Python builtins = PythonObject(PyEval_GetBuiltins()) @@ -694,6 +697,8 @@ public struct PythonInterface { executable_name = "python{}.{}".format(sys.version_info.major, sys.version_info.minor) sys.executable = os.path.join(sys.exec_prefix, "bin", executable_name) """) + + module = PythonModule() } public func attemptImport(_ name: String) throws -> PythonObject { @@ -1945,122 +1950,11 @@ struct PyAwaitableFunction { } public struct PythonAwaitableFunction { - static var ready = false - - var awaitable: PythonObject? - init() { - if !Self.ready { - PythonAwaitableFunction.readyType(in: PythonAwaitableFunction.sharedModule) - } - } -} - -fileprivate extension PythonAwaitableFunction { - - static let sharedModule = PythonModule() - - static let sharedType: UnsafeMutablePointer = { - let pythonAwaitableFunctionName: StaticString = "Awaitable" - let pythonAwaitableFunctionDoc: StaticString = "PythonKit Awaitable Functions" - - let pythonAwaitableFunctionAsyncMethods = UnsafeMutablePointer.allocate(capacity: 1) - pythonAwaitableFunctionAsyncMethods.pointee = PyAsyncMethods( - am_await: PythonAwaitableFunction.next, - am_aiter: nil, - am_anext: nil, - am_send: nil) - - let next: StaticString = "next" - let pythonAwaitableFunctionMethods = UnsafeMutablePointer.allocate(capacity: 2) - pythonAwaitableFunctionMethods[0] = PyMethodDef( - ml_name: UnsafeRawPointer(next.utf8Start).assumingMemoryBound(to: Int8.self), - ml_meth: unsafeBitCast(PythonAwaitableFunction.next, to: OpaquePointer.self), - ml_flags: 0, - ml_doc: nil) - pythonAwaitableFunctionMethods[1] = PyMethodDef( - ml_name: nil, ml_meth: nil, ml_flags: 0, ml_doc: nil) - - let pythonAwaitableFunctionType = UnsafeMutablePointer.allocate(capacity: 1) - pythonAwaitableFunctionType.initialize(to: PyTypeObject( - ob_base: PyVarObject( - ob_base: PyObject( - ob_refcnt: Py_ImmortalRefCount, - ob_type: nil - ), - ob_size: 0 - ), - tp_name: UnsafeRawPointer(pythonAwaitableFunctionName.utf8Start).assumingMemoryBound(to: Int8.self), - tp_basicsize: MemoryLayout.size, - tp_itemsize: 0, - tp_dealloc: PythonAwaitableFunction.dealloc, - tp_vectorcall_offset: 0, - tp_getattr: nil, - tp_setattr: nil, - tp_as_async: pythonAwaitableFunctionAsyncMethods, - tp_repr: nil, - tp_as_number: nil, - tp_as_sequence: nil, - tp_as_mapping: nil, - tp_hash: nil, - tp_call: nil, - tp_str: nil, - tp_getattro: nil, - tp_setattro: nil, - tp_as_buffer: nil, - tp_flags: Py_TPFlagsDefault | Py_TPFLAGS_HEAPTYPE, - tp_doc: UnsafeRawPointer(pythonAwaitableFunctionDoc.utf8Start).assumingMemoryBound(to: Int8.self), - tp_traverse: nil, - tp_clear: nil, - tp_richcompare: nil, - tp_weaklistoffset: 0, - tp_iter: nil, - tp_iternext: nil, - tp_methods: pythonAwaitableFunctionMethods, - tp_members: nil, - tp_getset: nil, - tp_base: nil, - tp_dict: nil, - tp_descr_get: nil, - tp_descr_set: nil, - tp_dictoffset: 0, - tp_init: nil, - tp_alloc: PyType_GenericAlloc, - tp_new: PythonAwaitableFunction.new, - tp_free: PyObject_Free, - tp_is_gc: nil, - tp_bases: nil, - tp_mro: nil, - tp_cache: nil, - tp_subclasses: nil, - tp_weaklist: nil, - tp_del: nil, - tp_version_tag: 0, - tp_finalize: nil, - tp_vectorcall: nil)) - - return pythonAwaitableFunctionType - }() - - static func readyType(in module: PythonModule) { - guard !ready else { - return - } - guard module.addType(PythonAwaitableFunction.sharedType) else { - return - } - Py_IncRef(PythonAwaitableFunction.sharedType) - guard module.addObject(PythonAwaitableFunction.sharedType, named: "Awaitable") else { - return - } - ready = true } static func alloc() -> PyObjectPointer { - precondition(PythonAwaitableFunction.ready, "AwaitableFunction type not ready.") - precondition(PythonAwaitableFunction.sharedType.pointee.tp_alloc != nil, "AwaitableFunction tp_alloc missing.") - - let type = PythonAwaitableFunction.sharedType + let type = PythonModule.sharedType guard let tp_alloc = type.pointee.tp_alloc else { fatalError("Failed to allocate AwaitableFunction") } @@ -2074,10 +1968,7 @@ fileprivate extension PythonAwaitableFunction { } static func free(_ object: PyObjectPointer) -> Void { - precondition(PythonAwaitableFunction.ready, "AwaitableFunction type not ready.") - precondition(PythonAwaitableFunction.sharedType.pointee.tp_free != nil, "AwaitableFunction tp_free missing.") - - let type = PythonAwaitableFunction.sharedType + let type = PythonModule.sharedType guard let tp_free = type.pointee.tp_free else { fatalError("Failed to deallocate AwaitableFunction") } @@ -2086,8 +1977,6 @@ fileprivate extension PythonAwaitableFunction { } static let new: newfunc = { type, args, kwds in - precondition(PythonAwaitableFunction.ready, "AwaitableFunction type not ready.") - let result = alloc() let awaitable = result.withMemoryRebound(to: PyAwaitableFunction.self, capacity: 1) { diff --git a/PythonKit/PythonModule.swift b/PythonKit/PythonModule.swift index bc16dbb..b94be41 100644 --- a/PythonKit/PythonModule.swift +++ b/PythonKit/PythonModule.swift @@ -1,7 +1,9 @@ //===--PythonModule.swift -------------------------------------------------===// -// This file defines types related to Python Modules and an interop layer. +// This file defines a custom extension module for PythonKit. //===----------------------------------------------------------------------===// +//===----------------------------------------------------------------------===// +// Types from the CPython headers and related. //===----------------------------------------------------------------------===// typealias PyModuleDefPointer = UnsafeMutableRawPointer @@ -26,10 +28,6 @@ let Py_TPFLAGS_HEAPTYPE: UInt64 = (1 << 9) // The immortal value is 0xFFFFFFFF. let Py_ImmortalRefCount: Int = Int(UInt32.max) -//===----------------------------------------------------------------------===// -// PythonModule Types. -//===----------------------------------------------------------------------===// - struct PyObject { var ob_refcnt: Int var ob_type: UnsafeMutablePointer? @@ -40,6 +38,10 @@ struct PyVarObject { var ob_size: Int } +//===----------------------------------------------------------------------===// +// PythonModule Types. +//===----------------------------------------------------------------------===// + struct PyModuleDef_Base { var ob_base: PyObject var m_init: OpaquePointer? @@ -136,10 +138,12 @@ struct PyTypeObject { } //===----------------------------------------------------------------------===// -// PythonModule +// PythonModule for injecting the `pythonkit` extension. //===----------------------------------------------------------------------===// struct PythonModule : PythonConvertible { + static let sharedModule = PythonModule() + private static let moduleName: StaticString = "pythonkit" private static let moduleDoc: StaticString = "PythonKit Extension Module" private static let moduleDef = PyModuleDef( @@ -168,8 +172,6 @@ struct PythonModule : PythonConvertible { let moduleDefinition: UnsafeMutablePointer = .allocate(capacity: 1) moduleDefinition.pointee = Self.moduleDef - _ = Python // Ensure Python is initialized. - let module = PyModule_Create(moduleDefinition, Py_AbiVersion) let moduleName = PyUnicode_InternFromString( UnsafeRawPointer(Self.moduleName.utf8Start).assumingMemoryBound(to: Int8.self)) @@ -183,6 +185,101 @@ struct PythonModule : PythonConvertible { } } +// Extension for injecting our PythonAwaitableFunction type. extension PythonModule { + static let sharedType: UnsafeMutablePointer = { + // For __name__ and __doc__. + let pythonAwaitableFunctionName: StaticString = "Awaitable" + let pythonAwaitableFunctionDoc: StaticString = "PythonKit Awaitable Function" + + // The async methods. + let pythonAwaitableFunctionAsyncMethods = UnsafeMutablePointer.allocate(capacity: 1) + pythonAwaitableFunctionAsyncMethods.pointee = PyAsyncMethods( + am_await: PythonAwaitableFunction.next, + am_aiter: nil, + am_anext: nil, + am_send: nil) + + // The methods. + let next: StaticString = "next" + let pythonAwaitableFunctionMethods = UnsafeMutablePointer.allocate(capacity: 2) + pythonAwaitableFunctionMethods[0] = PyMethodDef( + ml_name: UnsafeRawPointer(next.utf8Start).assumingMemoryBound(to: Int8.self), + ml_meth: unsafeBitCast(PythonAwaitableFunction.next, to: OpaquePointer.self), + ml_flags: 0, + ml_doc: nil) + pythonAwaitableFunctionMethods[1] = PyMethodDef( + ml_name: nil, ml_meth: nil, ml_flags: 0, ml_doc: nil) // Sentinel. + + // The type. + let pythonAwaitableFunctionType = UnsafeMutablePointer.allocate(capacity: 1) + pythonAwaitableFunctionType.initialize(to: PyTypeObject( + ob_base: PyVarObject( + ob_base: PyObject( + ob_refcnt: Py_ImmortalRefCount, + ob_type: nil + ), + ob_size: 0 + ), + tp_name: UnsafeRawPointer(pythonAwaitableFunctionName.utf8Start).assumingMemoryBound(to: Int8.self), + tp_basicsize: MemoryLayout.size, + tp_itemsize: 0, + tp_dealloc: PythonAwaitableFunction.dealloc, + tp_vectorcall_offset: 0, + tp_getattr: nil, + tp_setattr: nil, + tp_as_async: pythonAwaitableFunctionAsyncMethods, + tp_repr: nil, + tp_as_number: nil, + tp_as_sequence: nil, + tp_as_mapping: nil, + tp_hash: nil, + tp_call: nil, + tp_str: nil, + tp_getattro: nil, + tp_setattro: nil, + tp_as_buffer: nil, + tp_flags: Py_TPFlagsDefault | Py_TPFLAGS_HEAPTYPE, + tp_doc: UnsafeRawPointer(pythonAwaitableFunctionDoc.utf8Start).assumingMemoryBound(to: Int8.self), + tp_traverse: nil, + tp_clear: nil, + tp_richcompare: nil, + tp_weaklistoffset: 0, + tp_iter: nil, + tp_iternext: nil, + tp_methods: pythonAwaitableFunctionMethods, + tp_members: nil, + tp_getset: nil, + tp_base: nil, + tp_dict: nil, + tp_descr_get: nil, + tp_descr_set: nil, + tp_dictoffset: 0, + tp_init: nil, + tp_alloc: PyType_GenericAlloc, + tp_new: PythonAwaitableFunction.new, + tp_free: PyObject_Free, + tp_is_gc: nil, + tp_bases: nil, + tp_mro: nil, + tp_cache: nil, + tp_subclasses: nil, + tp_weaklist: nil, + tp_del: nil, + tp_version_tag: 0, + tp_finalize: nil, + tp_vectorcall: nil)) + + // Ready the type. + guard sharedModule.addType(pythonAwaitableFunctionType) else { + fatalError("Failed to add Awaitable type.") + } + + // Add the Awaitable object of the type. + guard sharedModule.addObject(pythonAwaitableFunctionType, named: "Awaitable") else { + fatalError("Failed to add Awaitable object.") + } + return pythonAwaitableFunctionType + }() } diff --git a/Tests/PythonKitTests/PythonModuleTests.swift b/Tests/PythonKitTests/PythonModuleTests.swift index b041779..b4e5cbd 100644 --- a/Tests/PythonKitTests/PythonModuleTests.swift +++ b/Tests/PythonKitTests/PythonModuleTests.swift @@ -3,17 +3,11 @@ import XCTest class PythonModuleTests: XCTestCase { func testPythonModule() { - let module = PythonModule() - XCTAssertNotNil(module.pythonObject) - let pythonKit = Python.import("pythonkit") XCTAssertNotNil(pythonKit) } func testAwaitablePythonFunction() throws { - let _ = PythonAwaitableFunction() - XCTAssertTrue(PythonAwaitableFunction.ready) - let pythonKit = Python.import("pythonkit") let awaitable = pythonKit.Awaitable() XCTAssertNotNil(awaitable) From ed1359238fe5efe30f5cfaf3b55b486ee9d2c040 Mon Sep 17 00:00:00 2001 From: Jeffrey Davis Date: Thu, 8 Aug 2024 15:46:07 -0700 Subject: [PATCH 07/24] [WIP] cleanup some more. --- PythonKit/Python.swift | 4 ++-- PythonKit/PythonModule.swift | 27 +++++++++++---------------- 2 files changed, 13 insertions(+), 18 deletions(-) diff --git a/PythonKit/Python.swift b/PythonKit/Python.swift index f845d32..d67ebe3 100644 --- a/PythonKit/Python.swift +++ b/PythonKit/Python.swift @@ -1954,7 +1954,7 @@ public struct PythonAwaitableFunction { } static func alloc() -> PyObjectPointer { - let type = PythonModule.sharedType + let type = Python.module.pyAwaitableFunctionType guard let tp_alloc = type.pointee.tp_alloc else { fatalError("Failed to allocate AwaitableFunction") } @@ -1968,7 +1968,7 @@ public struct PythonAwaitableFunction { } static func free(_ object: PyObjectPointer) -> Void { - let type = PythonModule.sharedType + let type = Python.module.pyAwaitableFunctionType guard let tp_free = type.pointee.tp_free else { fatalError("Failed to deallocate AwaitableFunction") } diff --git a/PythonKit/PythonModule.swift b/PythonKit/PythonModule.swift index b94be41..e590cef 100644 --- a/PythonKit/PythonModule.swift +++ b/PythonKit/PythonModule.swift @@ -142,8 +142,6 @@ struct PyTypeObject { //===----------------------------------------------------------------------===// struct PythonModule : PythonConvertible { - static let sharedModule = PythonModule() - private static let moduleName: StaticString = "pythonkit" private static let moduleDoc: StaticString = "PythonKit Extension Module" private static let moduleDef = PyModuleDef( @@ -182,12 +180,19 @@ struct PythonModule : PythonConvertible { } pythonObject = PythonObject(consuming: module) + + // Ready the type. + guard addType(pyAwaitableFunctionType) else { + fatalError("Failed to add Awaitable type.") + } + + // Add the Awaitable object of the type. + guard addObject(pyAwaitableFunctionType, named: "Awaitable") else { + fatalError("Failed to add Awaitable object.") + } } -} -// Extension for injecting our PythonAwaitableFunction type. -extension PythonModule { - static let sharedType: UnsafeMutablePointer = { + let pyAwaitableFunctionType: UnsafeMutablePointer = { // For __name__ and __doc__. let pythonAwaitableFunctionName: StaticString = "Awaitable" let pythonAwaitableFunctionDoc: StaticString = "PythonKit Awaitable Function" @@ -270,16 +275,6 @@ extension PythonModule { tp_finalize: nil, tp_vectorcall: nil)) - // Ready the type. - guard sharedModule.addType(pythonAwaitableFunctionType) else { - fatalError("Failed to add Awaitable type.") - } - - // Add the Awaitable object of the type. - guard sharedModule.addObject(pythonAwaitableFunctionType, named: "Awaitable") else { - fatalError("Failed to add Awaitable object.") - } - return pythonAwaitableFunctionType }() } From 7e823da2d2911d242b6d4bc3194a069c27a0b44d Mon Sep 17 00:00:00 2001 From: Jeffrey Davis Date: Thu, 8 Aug 2024 16:22:54 -0700 Subject: [PATCH 08/24] [WIP] Add PythonConvertible to PyAwaitableFunction and test. --- PythonKit/Python.swift | 44 ++++++++++++-------- PythonKit/PythonModule.swift | 4 +- Tests/PythonKitTests/PythonModuleTests.swift | 3 ++ 3 files changed, 32 insertions(+), 19 deletions(-) diff --git a/PythonKit/Python.swift b/PythonKit/Python.swift index d67ebe3..dd93f48 100644 --- a/PythonKit/Python.swift +++ b/PythonKit/Python.swift @@ -1949,10 +1949,32 @@ struct PyAwaitableFunction { var aw_magic: Int } -public struct PythonAwaitableFunction { +extension PyAwaitableFunction: ConvertibleFromPython { + init?(_ pythonObject: PythonObject) { + let pyObject = pythonObject.ownedPyObject + defer { Py_DecRef(pyObject) } + + self = pyObject.withMemoryRebound(to: PyAwaitableFunction.self, capacity: 1) { + $0.pointee + } + } +} + +struct PythonAwaitableFunction { init() { } + static let new: newfunc = { type, args, kwds in + let result = alloc() + + let awaitable = result.withMemoryRebound(to: PyAwaitableFunction.self, capacity: 1) { + $0.pointee.aw_magic = 0x08675309 + return $0.pointee + } + + return result + } + static func alloc() -> PyObjectPointer { let type = Python.module.pyAwaitableFunctionType guard let tp_alloc = type.pointee.tp_alloc else { @@ -1967,6 +1989,10 @@ public struct PythonAwaitableFunction { return result } + static let dealloc: destructor = { object in + free(object) + } + static func free(_ object: PyObjectPointer) -> Void { let type = Python.module.pyAwaitableFunctionType guard let tp_free = type.pointee.tp_free else { @@ -1976,22 +2002,6 @@ public struct PythonAwaitableFunction { tp_free(object) } - static let new: newfunc = { type, args, kwds in - let result = alloc() - - let awaitable = result.withMemoryRebound(to: PyAwaitableFunction.self, capacity: 1) { - $0.pointee.aw_magic = 0x08675309 - return $0.pointee - } - print("awaitable: \(awaitable)") - - return result - } - - static let dealloc: destructor = { object in - free(object) - } - static let next: unaryfunc = { object in return nil } diff --git a/PythonKit/PythonModule.swift b/PythonKit/PythonModule.swift index e590cef..d3b8d02 100644 --- a/PythonKit/PythonModule.swift +++ b/PythonKit/PythonModule.swift @@ -199,11 +199,11 @@ struct PythonModule : PythonConvertible { // The async methods. let pythonAwaitableFunctionAsyncMethods = UnsafeMutablePointer.allocate(capacity: 1) - pythonAwaitableFunctionAsyncMethods.pointee = PyAsyncMethods( + pythonAwaitableFunctionAsyncMethods.initialize(to: PyAsyncMethods( am_await: PythonAwaitableFunction.next, am_aiter: nil, am_anext: nil, - am_send: nil) + am_send: nil)) // The methods. let next: StaticString = "next" diff --git a/Tests/PythonKitTests/PythonModuleTests.swift b/Tests/PythonKitTests/PythonModuleTests.swift index b4e5cbd..39c9a09 100644 --- a/Tests/PythonKitTests/PythonModuleTests.swift +++ b/Tests/PythonKitTests/PythonModuleTests.swift @@ -11,5 +11,8 @@ class PythonModuleTests: XCTestCase { let pythonKit = Python.import("pythonkit") let awaitable = pythonKit.Awaitable() XCTAssertNotNil(awaitable) + + let pyAwaitableFunction = PyAwaitableFunction(awaitable)! + XCTAssert(pyAwaitableFunction.aw_magic == 0x08675309) } } From 73b93a4844ae941d23c38d7892278bafd75355d1 Mon Sep 17 00:00:00 2001 From: Jeffrey Davis Date: Fri, 9 Aug 2024 14:11:36 -0700 Subject: [PATCH 09/24] [WIP] Rename some things and add Awaitable.magic method. --- PythonKit/Python.swift | 25 ++++++++++++-------- PythonKit/PythonModule.swift | 25 +++++++++++--------- Tests/PythonKitTests/PythonModuleTests.swift | 14 +++++++---- 3 files changed, 38 insertions(+), 26 deletions(-) diff --git a/PythonKit/Python.swift b/PythonKit/Python.swift index dd93f48..50f36cd 100644 --- a/PythonKit/Python.swift +++ b/PythonKit/Python.swift @@ -1944,30 +1944,27 @@ extension PythonModule { } } -struct PyAwaitableFunction { +struct PythonKitAwaitable { var ob_base: PyObject var aw_magic: Int } -extension PyAwaitableFunction: ConvertibleFromPython { +extension PythonKitAwaitable: ConvertibleFromPython { init?(_ pythonObject: PythonObject) { let pyObject = pythonObject.ownedPyObject defer { Py_DecRef(pyObject) } - self = pyObject.withMemoryRebound(to: PyAwaitableFunction.self, capacity: 1) { + self = pyObject.withMemoryRebound(to: PythonKitAwaitable.self, capacity: 1) { $0.pointee } } } -struct PythonAwaitableFunction { - init() { - } - +extension PythonKitAwaitable { static let new: newfunc = { type, args, kwds in let result = alloc() - let awaitable = result.withMemoryRebound(to: PyAwaitableFunction.self, capacity: 1) { + let awaitable = result.withMemoryRebound(to: PythonKitAwaitable.self, capacity: 1) { $0.pointee.aw_magic = 0x08675309 return $0.pointee } @@ -1976,7 +1973,7 @@ struct PythonAwaitableFunction { } static func alloc() -> PyObjectPointer { - let type = Python.module.pyAwaitableFunctionType + let type = Python.module.PythonKitAwaitableType guard let tp_alloc = type.pointee.tp_alloc else { fatalError("Failed to allocate AwaitableFunction") } @@ -1994,7 +1991,7 @@ struct PythonAwaitableFunction { } static func free(_ object: PyObjectPointer) -> Void { - let type = Python.module.pyAwaitableFunctionType + let type = Python.module.PythonKitAwaitableType guard let tp_free = type.pointee.tp_free else { fatalError("Failed to deallocate AwaitableFunction") } @@ -2005,6 +2002,14 @@ struct PythonAwaitableFunction { static let next: unaryfunc = { object in return nil } + + static let magic: PyCFunction = { object, _ in + guard let awaitable = PythonKitAwaitable(PythonObject(object)) else { + return nil + } + print("AwaitableFunction.magic called with \(awaitable)") + return PyInt_FromLong(awaitable.aw_magic) + } } extension PythonObject: Error {} diff --git a/PythonKit/PythonModule.swift b/PythonKit/PythonModule.swift index d3b8d02..8412042 100644 --- a/PythonKit/PythonModule.swift +++ b/PythonKit/PythonModule.swift @@ -16,6 +16,8 @@ typealias newfunc = @convention(c) (PyTypeObjectPointer, PyObjectPointer, PyObje typealias sendfunc = @convention(c) (PyObjectPointer, PyObjectPointer, PyObjectPointer) -> Int typealias unaryfunc = @convention(c) (PyObjectPointer) -> PyObjectPointer? +typealias PyCFunction = @convention(c) (PyObjectPointer, PyObjectPointer) -> PyObjectPointer? + // This will be 3 for the lifetime of Python 3. See PEP-384. let Py_AbiVersion: Int = 3 @@ -182,17 +184,17 @@ struct PythonModule : PythonConvertible { pythonObject = PythonObject(consuming: module) // Ready the type. - guard addType(pyAwaitableFunctionType) else { + guard addType(PythonKitAwaitableType) else { fatalError("Failed to add Awaitable type.") } // Add the Awaitable object of the type. - guard addObject(pyAwaitableFunctionType, named: "Awaitable") else { + guard addObject(PythonKitAwaitableType, named: "Awaitable") else { fatalError("Failed to add Awaitable object.") } } - let pyAwaitableFunctionType: UnsafeMutablePointer = { + let PythonKitAwaitableType: UnsafeMutablePointer = { // For __name__ and __doc__. let pythonAwaitableFunctionName: StaticString = "Awaitable" let pythonAwaitableFunctionDoc: StaticString = "PythonKit Awaitable Function" @@ -200,18 +202,19 @@ struct PythonModule : PythonConvertible { // The async methods. let pythonAwaitableFunctionAsyncMethods = UnsafeMutablePointer.allocate(capacity: 1) pythonAwaitableFunctionAsyncMethods.initialize(to: PyAsyncMethods( - am_await: PythonAwaitableFunction.next, + am_await: PythonKitAwaitable.next, am_aiter: nil, am_anext: nil, am_send: nil)) // The methods. - let next: StaticString = "next" + let magicName: StaticString = "magic" + let METH_NOARGS: Int32 = 0x0004 let pythonAwaitableFunctionMethods = UnsafeMutablePointer.allocate(capacity: 2) pythonAwaitableFunctionMethods[0] = PyMethodDef( - ml_name: UnsafeRawPointer(next.utf8Start).assumingMemoryBound(to: Int8.self), - ml_meth: unsafeBitCast(PythonAwaitableFunction.next, to: OpaquePointer.self), - ml_flags: 0, + ml_name: UnsafeRawPointer(magicName.utf8Start).assumingMemoryBound(to: Int8.self), + ml_meth: unsafeBitCast(PythonKitAwaitable.magic, to: OpaquePointer.self), + ml_flags: METH_NOARGS, ml_doc: nil) pythonAwaitableFunctionMethods[1] = PyMethodDef( ml_name: nil, ml_meth: nil, ml_flags: 0, ml_doc: nil) // Sentinel. @@ -227,9 +230,9 @@ struct PythonModule : PythonConvertible { ob_size: 0 ), tp_name: UnsafeRawPointer(pythonAwaitableFunctionName.utf8Start).assumingMemoryBound(to: Int8.self), - tp_basicsize: MemoryLayout.size, + tp_basicsize: MemoryLayout.size, tp_itemsize: 0, - tp_dealloc: PythonAwaitableFunction.dealloc, + tp_dealloc: PythonKitAwaitable.dealloc, tp_vectorcall_offset: 0, tp_getattr: nil, tp_setattr: nil, @@ -262,7 +265,7 @@ struct PythonModule : PythonConvertible { tp_dictoffset: 0, tp_init: nil, tp_alloc: PyType_GenericAlloc, - tp_new: PythonAwaitableFunction.new, + tp_new: PythonKitAwaitable.new, tp_free: PyObject_Free, tp_is_gc: nil, tp_bases: nil, diff --git a/Tests/PythonKitTests/PythonModuleTests.swift b/Tests/PythonKitTests/PythonModuleTests.swift index 39c9a09..987ac0b 100644 --- a/Tests/PythonKitTests/PythonModuleTests.swift +++ b/Tests/PythonKitTests/PythonModuleTests.swift @@ -8,11 +8,15 @@ class PythonModuleTests: XCTestCase { } func testAwaitablePythonFunction() throws { - let pythonKit = Python.import("pythonkit") - let awaitable = pythonKit.Awaitable() - XCTAssertNotNil(awaitable) + let pythonkit = Python.import("pythonkit") + + // Verify we can call Swift methods from Python. + let awaitable = pythonkit.Awaitable() + XCTAssertEqual(awaitable.magic(), 0x08675309) - let pyAwaitableFunction = PyAwaitableFunction(awaitable)! - XCTAssert(pyAwaitableFunction.aw_magic == 0x08675309) + // Verify we can conver to the native Swift type. + let pkAwaitable = PythonKitAwaitable(awaitable)! + XCTAssertNotNil(pkAwaitable) + XCTAssertEqual(pkAwaitable.aw_magic, 0x08675309) } } From 955b4bce1b121103f207612c57c81ebe470525bf Mon Sep 17 00:00:00 2001 From: Jeffrey Davis Date: Fri, 9 Aug 2024 14:22:58 -0700 Subject: [PATCH 10/24] [WIP] Rename more things and add iternext. --- PythonKit/Python.swift | 13 +++++--- PythonKit/PythonModule.swift | 33 ++++++++++---------- Tests/PythonKitTests/PythonModuleTests.swift | 4 +-- 3 files changed, 27 insertions(+), 23 deletions(-) diff --git a/PythonKit/Python.swift b/PythonKit/Python.swift index 50f36cd..97823d8 100644 --- a/PythonKit/Python.swift +++ b/PythonKit/Python.swift @@ -1917,7 +1917,7 @@ fileprivate extension PythonFunction { } //===----------------------------------------------------------------------===// -// PythonAwaitableFunction - create async functions in Swift that can be +// PythonKitAwaitable - create async functions in Swift that can be // awaited from Python. //===----------------------------------------------------------------------===// @@ -1975,12 +1975,12 @@ extension PythonKitAwaitable { static func alloc() -> PyObjectPointer { let type = Python.module.PythonKitAwaitableType guard let tp_alloc = type.pointee.tp_alloc else { - fatalError("Failed to allocate AwaitableFunction") + fatalError("Failed to allocate PythonKitAwaitable") } let result = tp_alloc(type, 0) guard let result else { - fatalError("Failed to allocate AwaitableFunction") + fatalError("Failed to allocate PythonKitAwaitable") } return result @@ -1993,21 +1993,24 @@ extension PythonKitAwaitable { static func free(_ object: PyObjectPointer) -> Void { let type = Python.module.PythonKitAwaitableType guard let tp_free = type.pointee.tp_free else { - fatalError("Failed to deallocate AwaitableFunction") + fatalError("Failed to deallocate PythonKitAwaitable") } tp_free(object) } +} +extension PythonKitAwaitable { static let next: unaryfunc = { object in return nil } +} +extension PythonKitAwaitable { static let magic: PyCFunction = { object, _ in guard let awaitable = PythonKitAwaitable(PythonObject(object)) else { return nil } - print("AwaitableFunction.magic called with \(awaitable)") return PyInt_FromLong(awaitable.aw_magic) } } diff --git a/PythonKit/PythonModule.swift b/PythonKit/PythonModule.swift index 8412042..cc2845c 100644 --- a/PythonKit/PythonModule.swift +++ b/PythonKit/PythonModule.swift @@ -12,6 +12,7 @@ typealias PyTypeObjectPointer = UnsafeMutableRawPointer typealias allocfunc = @convention(c) (PyTypeObjectPointer, Int) -> PyObjectPointer? typealias destructor = @convention(c) (PyObjectPointer) -> Void typealias freefunc = @convention(c) (UnsafeMutableRawPointer) -> Void +typealias iternextfunc = @convention(c) (PyObjectPointer) -> PyObjectPointer? typealias newfunc = @convention(c) (PyTypeObjectPointer, PyObjectPointer, PyObjectPointer) -> PyObjectPointer? typealias sendfunc = @convention(c) (PyObjectPointer, PyObjectPointer, PyObjectPointer) -> Int typealias unaryfunc = @convention(c) (PyObjectPointer) -> PyObjectPointer? @@ -114,7 +115,7 @@ struct PyTypeObject { var tp_richcompare: OpaquePointer? var tp_weaklistoffset: Int var tp_iter: OpaquePointer? - var tp_iternext: OpaquePointer? + var tp_iternext: iternextfunc? var tp_methods: UnsafePointer? var tp_members: UnsafePointer>? var tp_getset: UnsafePointer? @@ -196,12 +197,12 @@ struct PythonModule : PythonConvertible { let PythonKitAwaitableType: UnsafeMutablePointer = { // For __name__ and __doc__. - let pythonAwaitableFunctionName: StaticString = "Awaitable" - let pythonAwaitableFunctionDoc: StaticString = "PythonKit Awaitable Function" + let pythonKitAwaitableName: StaticString = "Awaitable" + let pythonKitAwaitableDoc: StaticString = "PythonKit Awaitable Function" // The async methods. - let pythonAwaitableFunctionAsyncMethods = UnsafeMutablePointer.allocate(capacity: 1) - pythonAwaitableFunctionAsyncMethods.initialize(to: PyAsyncMethods( + let pythonKitAwaitableAsyncMethods = UnsafeMutablePointer.allocate(capacity: 1) + pythonKitAwaitableAsyncMethods.initialize(to: PyAsyncMethods( am_await: PythonKitAwaitable.next, am_aiter: nil, am_anext: nil, @@ -210,18 +211,18 @@ struct PythonModule : PythonConvertible { // The methods. let magicName: StaticString = "magic" let METH_NOARGS: Int32 = 0x0004 - let pythonAwaitableFunctionMethods = UnsafeMutablePointer.allocate(capacity: 2) - pythonAwaitableFunctionMethods[0] = PyMethodDef( + let pythonKitAwaitableMethods = UnsafeMutablePointer.allocate(capacity: 2) + pythonKitAwaitableMethods[0] = PyMethodDef( ml_name: UnsafeRawPointer(magicName.utf8Start).assumingMemoryBound(to: Int8.self), ml_meth: unsafeBitCast(PythonKitAwaitable.magic, to: OpaquePointer.self), ml_flags: METH_NOARGS, ml_doc: nil) - pythonAwaitableFunctionMethods[1] = PyMethodDef( + pythonKitAwaitableMethods[1] = PyMethodDef( ml_name: nil, ml_meth: nil, ml_flags: 0, ml_doc: nil) // Sentinel. // The type. - let pythonAwaitableFunctionType = UnsafeMutablePointer.allocate(capacity: 1) - pythonAwaitableFunctionType.initialize(to: PyTypeObject( + let pythonKitAwaitableType = UnsafeMutablePointer.allocate(capacity: 1) + pythonKitAwaitableType.initialize(to: PyTypeObject( ob_base: PyVarObject( ob_base: PyObject( ob_refcnt: Py_ImmortalRefCount, @@ -229,14 +230,14 @@ struct PythonModule : PythonConvertible { ), ob_size: 0 ), - tp_name: UnsafeRawPointer(pythonAwaitableFunctionName.utf8Start).assumingMemoryBound(to: Int8.self), + tp_name: UnsafeRawPointer(pythonKitAwaitableName.utf8Start).assumingMemoryBound(to: Int8.self), tp_basicsize: MemoryLayout.size, tp_itemsize: 0, tp_dealloc: PythonKitAwaitable.dealloc, tp_vectorcall_offset: 0, tp_getattr: nil, tp_setattr: nil, - tp_as_async: pythonAwaitableFunctionAsyncMethods, + tp_as_async: pythonKitAwaitableAsyncMethods, tp_repr: nil, tp_as_number: nil, tp_as_sequence: nil, @@ -248,14 +249,14 @@ struct PythonModule : PythonConvertible { tp_setattro: nil, tp_as_buffer: nil, tp_flags: Py_TPFlagsDefault | Py_TPFLAGS_HEAPTYPE, - tp_doc: UnsafeRawPointer(pythonAwaitableFunctionDoc.utf8Start).assumingMemoryBound(to: Int8.self), + tp_doc: UnsafeRawPointer(pythonKitAwaitableDoc.utf8Start).assumingMemoryBound(to: Int8.self), tp_traverse: nil, tp_clear: nil, tp_richcompare: nil, tp_weaklistoffset: 0, tp_iter: nil, - tp_iternext: nil, - tp_methods: pythonAwaitableFunctionMethods, + tp_iternext: PythonKitAwaitable.next, + tp_methods: pythonKitAwaitableMethods, tp_members: nil, tp_getset: nil, tp_base: nil, @@ -278,6 +279,6 @@ struct PythonModule : PythonConvertible { tp_finalize: nil, tp_vectorcall: nil)) - return pythonAwaitableFunctionType + return pythonKitAwaitableType }() } diff --git a/Tests/PythonKitTests/PythonModuleTests.swift b/Tests/PythonKitTests/PythonModuleTests.swift index 987ac0b..34a5d10 100644 --- a/Tests/PythonKitTests/PythonModuleTests.swift +++ b/Tests/PythonKitTests/PythonModuleTests.swift @@ -7,14 +7,14 @@ class PythonModuleTests: XCTestCase { XCTAssertNotNil(pythonKit) } - func testAwaitablePythonFunction() throws { + func testAwaitable() throws { let pythonkit = Python.import("pythonkit") // Verify we can call Swift methods from Python. let awaitable = pythonkit.Awaitable() XCTAssertEqual(awaitable.magic(), 0x08675309) - // Verify we can conver to the native Swift type. + // Verify we can convert to the native Swift type. let pkAwaitable = PythonKitAwaitable(awaitable)! XCTAssertNotNil(pkAwaitable) XCTAssertEqual(pkAwaitable.aw_magic, 0x08675309) From cf7670cc326f632726670910502f0353f9d78e1a Mon Sep 17 00:00:00 2001 From: Jeffrey Davis Date: Fri, 9 Aug 2024 14:26:09 -0700 Subject: [PATCH 11/24] [WIP] Cleanup test a bit. --- Tests/PythonKitTests/PythonModuleTests.swift | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Tests/PythonKitTests/PythonModuleTests.swift b/Tests/PythonKitTests/PythonModuleTests.swift index 34a5d10..747fedc 100644 --- a/Tests/PythonKitTests/PythonModuleTests.swift +++ b/Tests/PythonKitTests/PythonModuleTests.swift @@ -8,10 +8,8 @@ class PythonModuleTests: XCTestCase { } func testAwaitable() throws { - let pythonkit = Python.import("pythonkit") - // Verify we can call Swift methods from Python. - let awaitable = pythonkit.Awaitable() + let awaitable = Python.import("pythonkit").Awaitable() XCTAssertEqual(awaitable.magic(), 0x08675309) // Verify we can convert to the native Swift type. From b1082f6c81b2d95ab84855f774c71b8b5df9e064 Mon Sep 17 00:00:00 2001 From: Jeffrey Davis Date: Mon, 12 Aug 2024 12:13:43 -0700 Subject: [PATCH 12/24] [WIP] Cleanup method generation and add PythonFunction overloads for async tasks. --- PythonKit/Python.swift | 108 ++++++++++++++++++- PythonKit/PythonModule.swift | 53 +++++---- Tests/PythonKitTests/PythonModuleTests.swift | 20 ++++ 3 files changed, 161 insertions(+), 20 deletions(-) diff --git a/PythonKit/Python.swift b/PythonKit/Python.swift index 97823d8..32b7837 100644 --- a/PythonKit/Python.swift +++ b/PythonKit/Python.swift @@ -1684,6 +1684,20 @@ public struct PythonFunction { self.name = name } + @available(macOS 10.15, *) + @_disfavoredOverload + public init(name: StaticString?, awaitable fn: @escaping (PythonObject) async throws -> PythonConvertible) { + function = PyFunction { argumentsAsTuple in + let awaitable = Python.import("pythonkit").Awaitable() + Task { + let result = try await fn(argumentsAsTuple[0]) + awaitable.set_result(result) + } + return awaitable + } + self.name = name + } + /// For cases where the Swift function should accept more (or less) than one parameter, accept an ordered array of all arguments instead. public init(_ fn: @escaping ([PythonObject]) throws -> PythonConvertible) { function = PyFunction { argumentsAsTuple in @@ -1696,6 +1710,19 @@ public struct PythonFunction { self.name = name } + @available(macOS 10.15, *) + public init(name: StaticString?, awaitable fn: @escaping ([PythonObject]) async throws -> PythonConvertible) { + function = PyFunction { argumentsAsTuple in + let awaitable = Python.import("pythonkit").Awaitable() + Task { + let result = try await fn(argumentsAsTuple.map { $0 }) + awaitable.set_result(result) + } + return awaitable + } + self.name = name + } + /// For cases where the Swift function should accept keyword arguments as `**kwargs` in Python. /// `**kwargs` must preserve order from Python 3.6 onward, similarly to /// Swift `KeyValuePairs` and unlike `Dictionary`. `KeyValuePairs` cannot be @@ -1715,6 +1742,24 @@ public struct PythonFunction { self.init(fn) self.name = name } + + @available(macOS 10.15, *) + public init(name: StaticString?, awaitable fn: @escaping ([PythonObject], [(key: String, value: PythonObject)]) async throws -> PythonConvertible) { + function = PyFunction { argumentsAsTuple, keywordArgumentsAsDictionary in + let awaitable = Python.import("pythonkit").Awaitable() + Task { + var kwargs: [(String, PythonObject)] = [] + for keyAndValue in keywordArgumentsAsDictionary.items() { + let (key, value) = keyAndValue.tuple2 + kwargs.append((String(key)!, value)) + } + let result = try await fn(argumentsAsTuple.map { $0 }, kwargs) + awaitable.set_result(result) + } + return awaitable + } + self.name = name + } } extension PythonFunction : PythonConvertible { @@ -1947,6 +1992,24 @@ extension PythonModule { struct PythonKitAwaitable { var ob_base: PyObject var aw_magic: Int + var aw_handle: Int + var aw_result: PythonObject? +} + +actor AwaitableManager { + private var awaitables: [PythonObject] = [] + private var nextIndex = 0 + + func add(_ awaitable: PythonObject) { + nextIndex += 1 + + guard nextIndex < Int.max else { + fatalError("Too many awaitables!") + } + + awaitable.set_index(nextIndex) + awaitables.append(awaitable) + } } extension PythonKitAwaitable: ConvertibleFromPython { @@ -1960,12 +2023,16 @@ extension PythonKitAwaitable: ConvertibleFromPython { } } +// Definitions of various CPython functions related to PyType lifecycle. +// The flow is alloc -> new -> [use object] -> dealloc -> free. extension PythonKitAwaitable { static let new: newfunc = { type, args, kwds in let result = alloc() let awaitable = result.withMemoryRebound(to: PythonKitAwaitable.self, capacity: 1) { $0.pointee.aw_magic = 0x08675309 + $0.pointee.aw_handle = Int.max + $0.pointee.aw_result = nil return $0.pointee } @@ -1973,7 +2040,7 @@ extension PythonKitAwaitable { } static func alloc() -> PyObjectPointer { - let type = Python.module.PythonKitAwaitableType + let type = Python.module.pythonKitAwaitableType guard let tp_alloc = type.pointee.tp_alloc else { fatalError("Failed to allocate PythonKitAwaitable") } @@ -1991,7 +2058,7 @@ extension PythonKitAwaitable { } static func free(_ object: PyObjectPointer) -> Void { - let type = Python.module.PythonKitAwaitableType + let type = Python.module.pythonKitAwaitableType guard let tp_free = type.pointee.tp_free else { fatalError("Failed to deallocate PythonKitAwaitable") } @@ -2000,12 +2067,14 @@ extension PythonKitAwaitable { } } +// Definitions of pythonKitAwaitableAsyncMethods. extension PythonKitAwaitable { static let next: unaryfunc = { object in return nil } } +// Definitions of pythonKitAwaitableMethods. extension PythonKitAwaitable { static let magic: PyCFunction = { object, _ in guard let awaitable = PythonKitAwaitable(PythonObject(object)) else { @@ -2013,6 +2082,41 @@ extension PythonKitAwaitable { } return PyInt_FromLong(awaitable.aw_magic) } + + static let handle: PyCFunction = { object, _ in + guard let awaitable = PythonKitAwaitable(PythonObject(object)) else { + return nil + } + return PyInt_FromLong(awaitable.aw_handle) + } + + static let setHandle: PyCFunction = { object, arg in + object.withMemoryRebound(to: PythonKitAwaitable.self, capacity: 1) { + guard var handle = Int(PythonObject(consuming: arg)) else { + fatalError("Failed to set handle") + } + $0.pointee.aw_handle = Int(PythonObject(consuming: arg))! + } + return Python.None.borrowedPyObject + } + + static let result: PyCFunction = { object, _ in + guard let awaitable = PythonKitAwaitable(PythonObject(object)) else { + return nil + } + + guard let result = awaitable.aw_result else { + return Python.None.borrowedPyObject + } + return result.borrowedPyObject + } + + static let setResult: PyCFunction = { object, arg in + object.withMemoryRebound(to: PythonKitAwaitable.self, capacity: 1) { + $0.pointee.aw_result = PythonObject(consuming: arg) + } + return Python.None.borrowedPyObject + } } extension PythonObject: Error {} diff --git a/PythonKit/PythonModule.swift b/PythonKit/PythonModule.swift index cc2845c..828fbf4 100644 --- a/PythonKit/PythonModule.swift +++ b/PythonKit/PythonModule.swift @@ -13,7 +13,7 @@ typealias allocfunc = @convention(c) (PyTypeObjectPointer, Int) -> PyObjectPoint typealias destructor = @convention(c) (PyObjectPointer) -> Void typealias freefunc = @convention(c) (UnsafeMutableRawPointer) -> Void typealias iternextfunc = @convention(c) (PyObjectPointer) -> PyObjectPointer? -typealias newfunc = @convention(c) (PyTypeObjectPointer, PyObjectPointer, PyObjectPointer) -> PyObjectPointer? +typealias newfunc = @convention(c) (PyTypeObjectPointer, PyObjectPointer?, PyObjectPointer?) -> PyObjectPointer? typealias sendfunc = @convention(c) (PyObjectPointer, PyObjectPointer, PyObjectPointer) -> Int typealias unaryfunc = @convention(c) (PyObjectPointer) -> PyObjectPointer? @@ -28,8 +28,11 @@ let Py_TPFlagsDefault: UInt64 = 0 // Our type is dynamically allocated. let Py_TPFLAGS_HEAPTYPE: UInt64 = (1 << 9) -// The immortal value is 0xFFFFFFFF. -let Py_ImmortalRefCount: Int = Int(UInt32.max) +// The immortal value is the 32bit UInt.max value. +let Py_ImmortalRefCount: Int = Int(bitPattern: 0x00000000FFFFFFFF) + +let METH_NOARGS: Int32 = 0x0004 +let METH_O: Int32 = 0x0008 struct PyObject { var ob_refcnt: Int @@ -167,8 +170,11 @@ struct PythonModule : PythonConvertible { m_free: nil ) + // PythonConvertible conformance. public var pythonObject: PythonObject + internal var awaitableManager: AwaitableManager = AwaitableManager() + init() { let moduleDefinition: UnsafeMutablePointer = .allocate(capacity: 1) moduleDefinition.pointee = Self.moduleDef @@ -185,17 +191,17 @@ struct PythonModule : PythonConvertible { pythonObject = PythonObject(consuming: module) // Ready the type. - guard addType(PythonKitAwaitableType) else { + guard addType(pythonKitAwaitableType) else { fatalError("Failed to add Awaitable type.") } // Add the Awaitable object of the type. - guard addObject(PythonKitAwaitableType, named: "Awaitable") else { + guard addObject(pythonKitAwaitableType, named: "Awaitable") else { fatalError("Failed to add Awaitable object.") } } - let PythonKitAwaitableType: UnsafeMutablePointer = { + let pythonKitAwaitableType: UnsafeMutablePointer = { // For __name__ and __doc__. let pythonKitAwaitableName: StaticString = "Awaitable" let pythonKitAwaitableDoc: StaticString = "PythonKit Awaitable Function" @@ -209,18 +215,29 @@ struct PythonModule : PythonConvertible { am_send: nil)) // The methods. - let magicName: StaticString = "magic" - let METH_NOARGS: Int32 = 0x0004 - let pythonKitAwaitableMethods = UnsafeMutablePointer.allocate(capacity: 2) - pythonKitAwaitableMethods[0] = PyMethodDef( - ml_name: UnsafeRawPointer(magicName.utf8Start).assumingMemoryBound(to: Int8.self), - ml_meth: unsafeBitCast(PythonKitAwaitable.magic, to: OpaquePointer.self), - ml_flags: METH_NOARGS, - ml_doc: nil) - pythonKitAwaitableMethods[1] = PyMethodDef( - ml_name: nil, ml_meth: nil, ml_flags: 0, ml_doc: nil) // Sentinel. - - // The type. + let methods: [(StaticString, PyCFunction, Int32)] = [ + ("magic", PythonKitAwaitable.magic, METH_NOARGS), + ("handle", PythonKitAwaitable.handle, METH_NOARGS), + ("set_handle", PythonKitAwaitable.setHandle, METH_O), + ("result", PythonKitAwaitable.result, METH_NOARGS), + ("set_result", PythonKitAwaitable.setResult, METH_O), + ] + + // Build the [PyMethodDef] structure. + let pythonKitAwaitableMethods = UnsafeMutablePointer.allocate(capacity: methods.count + 1) + for (index, (name, fn, meth)) in methods.enumerated() { + pythonKitAwaitableMethods[index] = PyMethodDef( + ml_name: UnsafeRawPointer(name.utf8Start).assumingMemoryBound(to: Int8.self), + ml_meth: unsafeBitCast(fn, to: OpaquePointer.self), + ml_flags: meth, + ml_doc: nil) + } + // Sentinel value. + pythonKitAwaitableMethods[methods.count] = PyMethodDef( + ml_name: nil, ml_meth: nil, ml_flags: 0, ml_doc: nil) + + // The type. This layout currently matches Python 3.11. + // TODO: We should have conditionals to support other versions. let pythonKitAwaitableType = UnsafeMutablePointer.allocate(capacity: 1) pythonKitAwaitableType.initialize(to: PyTypeObject( ob_base: PyVarObject( diff --git a/Tests/PythonKitTests/PythonModuleTests.swift b/Tests/PythonKitTests/PythonModuleTests.swift index 747fedc..bfa0e87 100644 --- a/Tests/PythonKitTests/PythonModuleTests.swift +++ b/Tests/PythonKitTests/PythonModuleTests.swift @@ -16,5 +16,25 @@ class PythonModuleTests: XCTestCase { let pkAwaitable = PythonKitAwaitable(awaitable)! XCTAssertNotNil(pkAwaitable) XCTAssertEqual(pkAwaitable.aw_magic, 0x08675309) + + // Verify methods we expect to be present are present. + let methods = Python.dir(awaitable) + XCTAssertFalse(methods.contains("_should_not_exist_")) // Sanity check. + XCTAssertTrue(methods.contains("magic")) + XCTAssertTrue(methods.contains("handle")) + XCTAssertTrue(methods.contains("set_handle")) + XCTAssertTrue(methods.contains("result")) + XCTAssertTrue(methods.contains("set_result")) + } + + func testAwaitableMethods() throws { + let awaitable = Python.import("pythonkit").Awaitable() + let index = PythonObject(1) + awaitable.set_handle(index) + XCTAssertEqual(awaitable.handle(), 1) + + let result = PythonObject("some result") + awaitable.set_result(result) + XCTAssertEqual(awaitable.result(), "some result") } } From b6fe385671f9489e5d1f14a5844eb85b9e07485e Mon Sep 17 00:00:00 2001 From: Jeffrey Davis Date: Mon, 12 Aug 2024 12:15:02 -0700 Subject: [PATCH 13/24] [WIP] Verify __next__ is present. --- Tests/PythonKitTests/PythonModuleTests.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Tests/PythonKitTests/PythonModuleTests.swift b/Tests/PythonKitTests/PythonModuleTests.swift index bfa0e87..c43318c 100644 --- a/Tests/PythonKitTests/PythonModuleTests.swift +++ b/Tests/PythonKitTests/PythonModuleTests.swift @@ -25,6 +25,9 @@ class PythonModuleTests: XCTestCase { XCTAssertTrue(methods.contains("set_handle")) XCTAssertTrue(methods.contains("result")) XCTAssertTrue(methods.contains("set_result")) + + // Veryify __next__ is present. + XCTAssertNotNil(awaitable.__next__) } func testAwaitableMethods() throws { From 70f3a87409794653fd53996352962de4bea23286 Mon Sep 17 00:00:00 2001 From: Jeffrey Davis Date: Tue, 13 Aug 2024 14:53:31 -0700 Subject: [PATCH 14/24] Add awaitable PythonFunction initializers. --- Package.swift | 3 + PythonKit/Python.swift | 228 +++++++------------ PythonKit/PythonLibrary+Symbols.swift | 10 + PythonKit/PythonModule.swift | 155 +++---------- Tests/PythonKitTests/PythonModuleTests.swift | 47 ++-- 5 files changed, 148 insertions(+), 295 deletions(-) diff --git a/Package.swift b/Package.swift index c1ea0aa..75d1103 100644 --- a/Package.swift +++ b/Package.swift @@ -4,6 +4,9 @@ import PackageDescription let package = Package( name: "PythonKit", + platforms: [ + .macOS(.v10_15) + ], products: [ .library( name: "PythonKit", diff --git a/PythonKit/Python.swift b/PythonKit/Python.swift index 32b7837..9219f68 100644 --- a/PythonKit/Python.swift +++ b/PythonKit/Python.swift @@ -18,7 +18,7 @@ // invasive compiler support. // //===----------------------------------------------------------------------===// - +import Foundation //===----------------------------------------------------------------------===// // `PyReference` definition //===----------------------------------------------------------------------===// @@ -1684,16 +1684,34 @@ public struct PythonFunction { self.name = name } - @available(macOS 10.15, *) @_disfavoredOverload public init(name: StaticString?, awaitable fn: @escaping (PythonObject) async throws -> PythonConvertible) { function = PyFunction { argumentsAsTuple in - let awaitable = Python.import("pythonkit").Awaitable() + let swiftArg = argumentsAsTuple[0] + let loop = Python.import("asyncio").get_running_loop() + let future = loop.create_future() Task { - let result = try await fn(argumentsAsTuple[0]) - awaitable.set_result(result) + var err: Error? + var result: PythonConvertible? + do { + result = try await fn(swiftArg) + } catch { + err = error + } + + let gilState = PyGILState_Ensure() + defer { PyGILState_Release(gilState) } + + if let err { + loop.call_soon_threadsafe(future.set_exception, Python.ValueError("\(err)")) + } else { + guard let result else { + fatalError("Result should exist if err is nil.") + } + loop.call_soon_threadsafe(future.set_result, result) + } } - return awaitable + return future } self.name = name } @@ -1710,15 +1728,33 @@ public struct PythonFunction { self.name = name } - @available(macOS 10.15, *) public init(name: StaticString?, awaitable fn: @escaping ([PythonObject]) async throws -> PythonConvertible) { function = PyFunction { argumentsAsTuple in - let awaitable = Python.import("pythonkit").Awaitable() + let swiftArgs = argumentsAsTuple.map { $0 } + let loop = Python.import("asyncio").get_running_loop() + let future = loop.create_future() Task { - let result = try await fn(argumentsAsTuple.map { $0 }) - awaitable.set_result(result) + var err: Error? + var result: PythonConvertible? + do { + result = try await fn(swiftArgs) + } catch { + err = error + } + + let gilState = PyGILState_Ensure() + defer { PyGILState_Release(gilState) } + + if let err { + loop.call_soon_threadsafe(future.set_exception, Python.ValueError("\(err)")) + } else { + guard let result else { + fatalError("Result should exist if err is nil.") + } + loop.call_soon_threadsafe(future.set_result, result) + } } - return awaitable + return future } self.name = name } @@ -1743,20 +1779,40 @@ public struct PythonFunction { self.name = name } - @available(macOS 10.15, *) public init(name: StaticString?, awaitable fn: @escaping ([PythonObject], [(key: String, value: PythonObject)]) async throws -> PythonConvertible) { function = PyFunction { argumentsAsTuple, keywordArgumentsAsDictionary in - let awaitable = Python.import("pythonkit").Awaitable() + let swiftArgs = argumentsAsTuple.map { $0 } + let swiftKwargs = keywordArgumentsAsDictionary.items().compactMap { keyAndValue -> (String, PythonObject)? in + guard let key = String(keyAndValue.tuple2.0) else { + return nil + } + return (key, keyAndValue.tuple2.1) + } + + let loop = Python.import("asyncio").get_running_loop() + let future = loop.create_future() Task { - var kwargs: [(String, PythonObject)] = [] - for keyAndValue in keywordArgumentsAsDictionary.items() { - let (key, value) = keyAndValue.tuple2 - kwargs.append((String(key)!, value)) + var err: Error? + var result: PythonConvertible? + do { + result = try await fn(swiftArgs, swiftKwargs) + } catch { + err = error + } + + let gilState = PyGILState_Ensure() + defer { PyGILState_Release(gilState) } + + if let err { + loop.call_soon_threadsafe(future.set_exception, Python.ValueError("\(err)")) + } else { + guard let result else { + fatalError("Result should exist if err is nil.") + } + loop.call_soon_threadsafe(future.set_result, result) } - let result = try await fn(argumentsAsTuple.map { $0 }, kwargs) - awaitable.set_result(result) } - return awaitable + return future } self.name = name } @@ -1987,135 +2043,13 @@ extension PythonModule { } return true } -} - -struct PythonKitAwaitable { - var ob_base: PyObject - var aw_magic: Int - var aw_handle: Int - var aw_result: PythonObject? -} - -actor AwaitableManager { - private var awaitables: [PythonObject] = [] - private var nextIndex = 0 - - func add(_ awaitable: PythonObject) { - nextIndex += 1 - - guard nextIndex < Int.max else { - fatalError("Too many awaitables!") - } - - awaitable.set_index(nextIndex) - awaitables.append(awaitable) - } -} - -extension PythonKitAwaitable: ConvertibleFromPython { - init?(_ pythonObject: PythonObject) { - let pyObject = pythonObject.ownedPyObject - defer { Py_DecRef(pyObject) } - - self = pyObject.withMemoryRebound(to: PythonKitAwaitable.self, capacity: 1) { - $0.pointee - } - } -} - -// Definitions of various CPython functions related to PyType lifecycle. -// The flow is alloc -> new -> [use object] -> dealloc -> free. -extension PythonKitAwaitable { - static let new: newfunc = { type, args, kwds in - let result = alloc() - - let awaitable = result.withMemoryRebound(to: PythonKitAwaitable.self, capacity: 1) { - $0.pointee.aw_magic = 0x08675309 - $0.pointee.aw_handle = Int.max - $0.pointee.aw_result = nil - return $0.pointee - } - - return result - } - - static func alloc() -> PyObjectPointer { - let type = Python.module.pythonKitAwaitableType - guard let tp_alloc = type.pointee.tp_alloc else { - fatalError("Failed to allocate PythonKitAwaitable") - } - - let result = tp_alloc(type, 0) - guard let result else { - fatalError("Failed to allocate PythonKitAwaitable") - } - - return result - } - - static let dealloc: destructor = { object in - free(object) - } - - static func free(_ object: PyObjectPointer) -> Void { - let type = Python.module.pythonKitAwaitableType - guard let tp_free = type.pointee.tp_free else { - fatalError("Failed to deallocate PythonKitAwaitable") - } - - tp_free(object) - } -} - -// Definitions of pythonKitAwaitableAsyncMethods. -extension PythonKitAwaitable { - static let next: unaryfunc = { object in - return nil - } -} - -// Definitions of pythonKitAwaitableMethods. -extension PythonKitAwaitable { - static let magic: PyCFunction = { object, _ in - guard let awaitable = PythonKitAwaitable(PythonObject(object)) else { - return nil - } - return PyInt_FromLong(awaitable.aw_magic) - } - - static let handle: PyCFunction = { object, _ in - guard let awaitable = PythonKitAwaitable(PythonObject(object)) else { - return nil - } - return PyInt_FromLong(awaitable.aw_handle) - } - - static let setHandle: PyCFunction = { object, arg in - object.withMemoryRebound(to: PythonKitAwaitable.self, capacity: 1) { - guard var handle = Int(PythonObject(consuming: arg)) else { - fatalError("Failed to set handle") - } - $0.pointee.aw_handle = Int(PythonObject(consuming: arg))! - } - return Python.None.borrowedPyObject - } - - static let result: PyCFunction = { object, _ in - guard let awaitable = PythonKitAwaitable(PythonObject(object)) else { - return nil - } - - guard let result = awaitable.aw_result else { - return Python.None.borrowedPyObject - } - return result.borrowedPyObject - } - static let setResult: PyCFunction = { object, arg in - object.withMemoryRebound(to: PythonKitAwaitable.self, capacity: 1) { - $0.pointee.aw_result = PythonObject(consuming: arg) + static var testAwaitableFunction: PythonFunction? + static let testAwaitable: PyCFunction = { _, _ in + guard let testAwaitableFunction = Self.testAwaitableFunction else { + fatalError("testAwaitableFunction not set") } - return Python.None.borrowedPyObject + return testAwaitableFunction.pythonObject.ownedPyObject } } diff --git a/PythonKit/PythonLibrary+Symbols.swift b/PythonKit/PythonLibrary+Symbols.swift index ad5cf3a..15e2f8f 100644 --- a/PythonKit/PythonLibrary+Symbols.swift +++ b/PythonKit/PythonLibrary+Symbols.swift @@ -34,6 +34,10 @@ let Py_NE: Int32 = 3 let Py_GT: Int32 = 4 let Py_GE: Int32 = 5 +typealias PyGILState_State = Int32 +let Py_GILState_Locked: Int32 = 0 +let Py_GILState_Unlocked: Int32 = 1 + //===----------------------------------------------------------------------===// // Python library symbols lazily loaded at runtime. //===----------------------------------------------------------------------===// @@ -47,6 +51,12 @@ let Py_IncRef: @convention(c) (PyObjectPointer?) -> Void = let Py_DecRef: @convention(c) (PyObjectPointer?) -> Void = PythonLibrary.loadSymbol(name: "Py_DecRef") +let PyGILState_Ensure: @convention(c) () -> PyGILState_State = + PythonLibrary.loadSymbol(name: "PyGILState_Ensure") + +let PyGILState_Release: @convention(c) (PyGILState_State) -> Void = + PythonLibrary.loadSymbol(name: "PyGILState_Release") + let PyImport_ImportModule: @convention(c) ( PyCCharPointer) -> PyObjectPointer? = PythonLibrary.loadSymbol(name: "PyImport_ImportModule") diff --git a/PythonKit/PythonModule.swift b/PythonKit/PythonModule.swift index 828fbf4..8357ddc 100644 --- a/PythonKit/PythonModule.swift +++ b/PythonKit/PythonModule.swift @@ -150,34 +150,42 @@ struct PyTypeObject { struct PythonModule : PythonConvertible { private static let moduleName: StaticString = "pythonkit" private static let moduleDoc: StaticString = "PythonKit Extension Module" - private static let moduleDef = PyModuleDef( - m_base: PyModuleDef_Base( - ob_base: PyObject( - ob_refcnt: Py_ImmortalRefCount, - ob_type: nil - ), - m_init: nil, - m_index: 0, - m_copy: nil - ), - m_name: UnsafeRawPointer(moduleName.utf8Start).assumingMemoryBound(to: Int8.self), - m_doc: UnsafeRawPointer(moduleDoc.utf8Start).assumingMemoryBound(to: Int8.self), - m_size: -1, - m_methods: nil, - m_slots: nil, - m_traverse: nil, - m_clear: nil, - m_free: nil - ) // PythonConvertible conformance. public var pythonObject: PythonObject - internal var awaitableManager: AwaitableManager = AwaitableManager() + private let moduleDef: PyModuleDef init() { + // Define module-level methods. + let methods: [(StaticString, PyCFunction, Int32)] = [ + ("test_awaitable", PythonModule.testAwaitable, METH_NOARGS), + ] + let methodDefs = Self.generateMethodDefs(from: methods) + + // Define the module. + moduleDef = PyModuleDef( + m_base: PyModuleDef_Base( + ob_base: PyObject( + ob_refcnt: Py_ImmortalRefCount, + ob_type: nil + ), + m_init: nil, + m_index: 0, + m_copy: nil + ), + m_name: UnsafeRawPointer(Self.moduleName.utf8Start).assumingMemoryBound(to: Int8.self), + m_doc: UnsafeRawPointer(Self.moduleDoc.utf8Start).assumingMemoryBound(to: Int8.self), + m_size: -1, + m_methods: methodDefs, + m_slots: nil, + m_traverse: nil, + m_clear: nil, + m_free: nil + ) + let moduleDefinition: UnsafeMutablePointer = .allocate(capacity: 1) - moduleDefinition.pointee = Self.moduleDef + moduleDefinition.pointee = moduleDef let module = PyModule_Create(moduleDefinition, Py_AbiVersion) let moduleName = PyUnicode_InternFromString( @@ -189,113 +197,20 @@ struct PythonModule : PythonConvertible { } pythonObject = PythonObject(consuming: module) - - // Ready the type. - guard addType(pythonKitAwaitableType) else { - fatalError("Failed to add Awaitable type.") - } - - // Add the Awaitable object of the type. - guard addObject(pythonKitAwaitableType, named: "Awaitable") else { - fatalError("Failed to add Awaitable object.") - } } - let pythonKitAwaitableType: UnsafeMutablePointer = { - // For __name__ and __doc__. - let pythonKitAwaitableName: StaticString = "Awaitable" - let pythonKitAwaitableDoc: StaticString = "PythonKit Awaitable Function" - - // The async methods. - let pythonKitAwaitableAsyncMethods = UnsafeMutablePointer.allocate(capacity: 1) - pythonKitAwaitableAsyncMethods.initialize(to: PyAsyncMethods( - am_await: PythonKitAwaitable.next, - am_aiter: nil, - am_anext: nil, - am_send: nil)) - - // The methods. - let methods: [(StaticString, PyCFunction, Int32)] = [ - ("magic", PythonKitAwaitable.magic, METH_NOARGS), - ("handle", PythonKitAwaitable.handle, METH_NOARGS), - ("set_handle", PythonKitAwaitable.setHandle, METH_O), - ("result", PythonKitAwaitable.result, METH_NOARGS), - ("set_result", PythonKitAwaitable.setResult, METH_O), - ] - - // Build the [PyMethodDef] structure. - let pythonKitAwaitableMethods = UnsafeMutablePointer.allocate(capacity: methods.count + 1) + private static func generateMethodDefs(from methods: [(StaticString, PyCFunction, Int32)]) -> UnsafeMutablePointer { + let methodDefs = UnsafeMutablePointer.allocate(capacity: methods.count + 1) for (index, (name, fn, meth)) in methods.enumerated() { - pythonKitAwaitableMethods[index] = PyMethodDef( + methodDefs[index] = PyMethodDef( ml_name: UnsafeRawPointer(name.utf8Start).assumingMemoryBound(to: Int8.self), ml_meth: unsafeBitCast(fn, to: OpaquePointer.self), ml_flags: meth, ml_doc: nil) } // Sentinel value. - pythonKitAwaitableMethods[methods.count] = PyMethodDef( + methodDefs[methods.count] = PyMethodDef( ml_name: nil, ml_meth: nil, ml_flags: 0, ml_doc: nil) - - // The type. This layout currently matches Python 3.11. - // TODO: We should have conditionals to support other versions. - let pythonKitAwaitableType = UnsafeMutablePointer.allocate(capacity: 1) - pythonKitAwaitableType.initialize(to: PyTypeObject( - ob_base: PyVarObject( - ob_base: PyObject( - ob_refcnt: Py_ImmortalRefCount, - ob_type: nil - ), - ob_size: 0 - ), - tp_name: UnsafeRawPointer(pythonKitAwaitableName.utf8Start).assumingMemoryBound(to: Int8.self), - tp_basicsize: MemoryLayout.size, - tp_itemsize: 0, - tp_dealloc: PythonKitAwaitable.dealloc, - tp_vectorcall_offset: 0, - tp_getattr: nil, - tp_setattr: nil, - tp_as_async: pythonKitAwaitableAsyncMethods, - tp_repr: nil, - tp_as_number: nil, - tp_as_sequence: nil, - tp_as_mapping: nil, - tp_hash: nil, - tp_call: nil, - tp_str: nil, - tp_getattro: nil, - tp_setattro: nil, - tp_as_buffer: nil, - tp_flags: Py_TPFlagsDefault | Py_TPFLAGS_HEAPTYPE, - tp_doc: UnsafeRawPointer(pythonKitAwaitableDoc.utf8Start).assumingMemoryBound(to: Int8.self), - tp_traverse: nil, - tp_clear: nil, - tp_richcompare: nil, - tp_weaklistoffset: 0, - tp_iter: nil, - tp_iternext: PythonKitAwaitable.next, - tp_methods: pythonKitAwaitableMethods, - tp_members: nil, - tp_getset: nil, - tp_base: nil, - tp_dict: nil, - tp_descr_get: nil, - tp_descr_set: nil, - tp_dictoffset: 0, - tp_init: nil, - tp_alloc: PyType_GenericAlloc, - tp_new: PythonKitAwaitable.new, - tp_free: PyObject_Free, - tp_is_gc: nil, - tp_bases: nil, - tp_mro: nil, - tp_cache: nil, - tp_subclasses: nil, - tp_weaklist: nil, - tp_del: nil, - tp_version_tag: 0, - tp_finalize: nil, - tp_vectorcall: nil)) - - return pythonKitAwaitableType - }() + return methodDefs + } } diff --git a/Tests/PythonKitTests/PythonModuleTests.swift b/Tests/PythonKitTests/PythonModuleTests.swift index c43318c..159b5b3 100644 --- a/Tests/PythonKitTests/PythonModuleTests.swift +++ b/Tests/PythonKitTests/PythonModuleTests.swift @@ -7,37 +7,28 @@ class PythonModuleTests: XCTestCase { XCTAssertNotNil(pythonKit) } - func testAwaitable() throws { - // Verify we can call Swift methods from Python. - let awaitable = Python.import("pythonkit").Awaitable() - XCTAssertEqual(awaitable.magic(), 0x08675309) + func testCanAwait() throws { + _ = Python - // Verify we can convert to the native Swift type. - let pkAwaitable = PythonKitAwaitable(awaitable)! - XCTAssertNotNil(pkAwaitable) - XCTAssertEqual(pkAwaitable.aw_magic, 0x08675309) + PythonModule.testAwaitableFunction = + PythonFunction(name: "test_awaitable") { (_, _) async throws -> PythonConvertible in + let result = 42 + return result + } - // Verify methods we expect to be present are present. - let methods = Python.dir(awaitable) - XCTAssertFalse(methods.contains("_should_not_exist_")) // Sanity check. - XCTAssertTrue(methods.contains("magic")) - XCTAssertTrue(methods.contains("handle")) - XCTAssertTrue(methods.contains("set_handle")) - XCTAssertTrue(methods.contains("result")) - XCTAssertTrue(methods.contains("set_result")) + // TODO: Find a way to assert the result in Swift. + PyRun_SimpleString(""" + import asyncio + import inspect + import pythonkit - // Veryify __next__ is present. - XCTAssertNotNil(awaitable.__next__) - } - - func testAwaitableMethods() throws { - let awaitable = Python.import("pythonkit").Awaitable() - let index = PythonObject(1) - awaitable.set_handle(index) - XCTAssertEqual(awaitable.handle(), 1) + async def main(): + awaitable = pythonkit.test_awaitable() + result = await awaitable() + print(f"Python: result == {result}") + assert result == 42 - let result = PythonObject("some result") - awaitable.set_result(result) - XCTAssertEqual(awaitable.result(), "some result") + asyncio.run(main()) + """) } } From 041f0ac448205f8680af3abb7b02c52f10b5c028 Mon Sep 17 00:00:00 2001 From: Jeffrey Davis Date: Tue, 13 Aug 2024 15:10:47 -0700 Subject: [PATCH 15/24] Rename testAwaitable -> getTestAwaitable. --- PythonKit/Python.swift | 2 +- PythonKit/PythonModule.swift | 2 +- Tests/PythonKitTests/PythonModuleTests.swift | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/PythonKit/Python.swift b/PythonKit/Python.swift index 9219f68..007dbb1 100644 --- a/PythonKit/Python.swift +++ b/PythonKit/Python.swift @@ -2045,7 +2045,7 @@ extension PythonModule { } static var testAwaitableFunction: PythonFunction? - static let testAwaitable: PyCFunction = { _, _ in + static let getTestAwaitable: PyCFunction = { _, _ in guard let testAwaitableFunction = Self.testAwaitableFunction else { fatalError("testAwaitableFunction not set") } diff --git a/PythonKit/PythonModule.swift b/PythonKit/PythonModule.swift index 8357ddc..7baf93d 100644 --- a/PythonKit/PythonModule.swift +++ b/PythonKit/PythonModule.swift @@ -159,7 +159,7 @@ struct PythonModule : PythonConvertible { init() { // Define module-level methods. let methods: [(StaticString, PyCFunction, Int32)] = [ - ("test_awaitable", PythonModule.testAwaitable, METH_NOARGS), + ("get_test_awaitable", PythonModule.getTestAwaitable, METH_NOARGS), ] let methodDefs = Self.generateMethodDefs(from: methods) diff --git a/Tests/PythonKitTests/PythonModuleTests.swift b/Tests/PythonKitTests/PythonModuleTests.swift index 159b5b3..7314f37 100644 --- a/Tests/PythonKitTests/PythonModuleTests.swift +++ b/Tests/PythonKitTests/PythonModuleTests.swift @@ -23,7 +23,7 @@ class PythonModuleTests: XCTestCase { import pythonkit async def main(): - awaitable = pythonkit.test_awaitable() + awaitable = pythonkit.get_test_awaitable() result = await awaitable() print(f"Python: result == {result}") assert result == 42 From 78ef7f6a4d86f350e9a91478e7fbcb4c32dbe465 Mon Sep 17 00:00:00 2001 From: Jeffrey Davis Date: Tue, 13 Aug 2024 15:11:57 -0700 Subject: [PATCH 16/24] Update comment. --- PythonKit/Python.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/PythonKit/Python.swift b/PythonKit/Python.swift index 007dbb1..163c745 100644 --- a/PythonKit/Python.swift +++ b/PythonKit/Python.swift @@ -2018,8 +2018,7 @@ fileprivate extension PythonFunction { } //===----------------------------------------------------------------------===// -// PythonKitAwaitable - create async functions in Swift that can be -// awaited from Python. +// PythonModule functions that need access to PyRef members. //===----------------------------------------------------------------------===// extension PythonModule { From ae300e283b2aadc339e9e043ba87aa6f426676a7 Mon Sep 17 00:00:00 2001 From: Jeffrey Davis Date: Tue, 13 Aug 2024 16:21:57 -0700 Subject: [PATCH 17/24] Remove unused imports. --- PythonKit/Python.swift | 2 +- Tests/PythonKitTests/PythonModuleTests.swift | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/PythonKit/Python.swift b/PythonKit/Python.swift index 163c745..a090dd9 100644 --- a/PythonKit/Python.swift +++ b/PythonKit/Python.swift @@ -18,7 +18,7 @@ // invasive compiler support. // //===----------------------------------------------------------------------===// -import Foundation + //===----------------------------------------------------------------------===// // `PyReference` definition //===----------------------------------------------------------------------===// diff --git a/Tests/PythonKitTests/PythonModuleTests.swift b/Tests/PythonKitTests/PythonModuleTests.swift index 7314f37..50adbda 100644 --- a/Tests/PythonKitTests/PythonModuleTests.swift +++ b/Tests/PythonKitTests/PythonModuleTests.swift @@ -19,7 +19,6 @@ class PythonModuleTests: XCTestCase { // TODO: Find a way to assert the result in Swift. PyRun_SimpleString(""" import asyncio - import inspect import pythonkit async def main(): From d47957f7f8c607f72c4e93bc4789a36910adbcae Mon Sep 17 00:00:00 2001 From: Jeffrey Davis Date: Wed, 14 Aug 2024 13:50:30 -0700 Subject: [PATCH 18/24] [WIP] Add methods to enter/exit GIL on non-Python threads. --- PythonKit/Python.swift | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/PythonKit/Python.swift b/PythonKit/Python.swift index a090dd9..47429aa 100644 --- a/PythonKit/Python.swift +++ b/PythonKit/Python.swift @@ -739,6 +739,38 @@ public struct PythonInterface { } } +//===----------------------------------------------------------------------===// +// Helper to enter and leave the GIL for non-Python created threads. +// See https://docs.python.org/3/c-api/init.html#non-python-created-threads +//===----------------------------------------------------------------------===// + +public class PythonThread { + /// Declare an instance to automatically enter/leave within the + /// current scope, RIAA-style. Or manually call class methods below. + init() { + Self.enterGIL() + } + + deinit { + Self.leaveGIL() + } + + private static var sharedState: PyGILState_State? + + /// Enter the GIL and initialize Python thread state. + public static func enterGIL() { + guard Self.sharedState == nil else { return } + Self.sharedState = PyGILState_Ensure() + } + + /// Exit the GIL. + public static func leaveGIL() { + guard let state = Self.sharedState else { return } + PyGILState_Release(state) + Self.sharedState = nil + } +} + //===----------------------------------------------------------------------===// // Helpers for Python tuple types //===----------------------------------------------------------------------===// From c63c40d2209da812e5b2aeeb682f0c69c57a769b Mon Sep 17 00:00:00 2001 From: Jeffrey Davis Date: Wed, 14 Aug 2024 13:57:26 -0700 Subject: [PATCH 19/24] [WIP] Add withGIL. --- PythonKit/Python.swift | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/PythonKit/Python.swift b/PythonKit/Python.swift index 47429aa..c6ebe86 100644 --- a/PythonKit/Python.swift +++ b/PythonKit/Python.swift @@ -769,6 +769,13 @@ public class PythonThread { PyGILState_Release(state) Self.sharedState = nil } + + /// Execute body while holding the GIL. + public static func withGIL(_ body: () throws -> T) rethrows -> T { + enterGIL() + defer { leaveGIL() } + return try body() + } } //===----------------------------------------------------------------------===// From 59009b610eabafecf5a5d0b4b87a75e9eeb95a39 Mon Sep 17 00:00:00 2001 From: Jeffrey Davis Date: Thu, 15 Aug 2024 10:20:03 -0700 Subject: [PATCH 20/24] Cleanup GIL access. --- PythonKit/Python.swift | 92 +++++++++++++++++------------------------- 1 file changed, 37 insertions(+), 55 deletions(-) diff --git a/PythonKit/Python.swift b/PythonKit/Python.swift index c6ebe86..c7d2beb 100644 --- a/PythonKit/Python.swift +++ b/PythonKit/Python.swift @@ -744,38 +744,39 @@ public struct PythonInterface { // See https://docs.python.org/3/c-api/init.html#non-python-created-threads //===----------------------------------------------------------------------===// -public class PythonThread { - /// Declare an instance to automatically enter/leave within the - /// current scope, RIAA-style. Or manually call class methods below. - init() { - Self.enterGIL() - } - - deinit { - Self.leaveGIL() - } - +class PythonThread { private static var sharedState: PyGILState_State? /// Enter the GIL and initialize Python thread state. public static func enterGIL() { - guard Self.sharedState == nil else { return } + guard Self.sharedState == nil else { + fatalError("The GIL is already held by this thread.") + } Self.sharedState = PyGILState_Ensure() } /// Exit the GIL. public static func leaveGIL() { - guard let state = Self.sharedState else { return } + guard let state = Self.sharedState else { + fatalError("The GIL is not held by this thread.") + } PyGILState_Release(state) Self.sharedState = nil } +} - /// Execute body while holding the GIL. - public static func withGIL(_ body: () throws -> T) rethrows -> T { - enterGIL() - defer { leaveGIL() } - return try body() - } +/// Execute body while holding the GIL. +public func withGIL(_ body: () throws -> T) rethrows -> T { + PythonThread.enterGIL() + defer { PythonThread.leaveGIL() } + return try body() +} + +/// Leave the GIL, execute the body, then re-enter the GIL. +public func withoutGIL(_ body: () throws -> T) rethrows -> T { + PythonThread.leaveGIL() + defer { PythonThread.enterGIL() } + return try body() } //===----------------------------------------------------------------------===// @@ -1737,18 +1738,7 @@ public struct PythonFunction { } catch { err = error } - - let gilState = PyGILState_Ensure() - defer { PyGILState_Release(gilState) } - - if let err { - loop.call_soon_threadsafe(future.set_exception, Python.ValueError("\(err)")) - } else { - guard let result else { - fatalError("Result should exist if err is nil.") - } - loop.call_soon_threadsafe(future.set_result, result) - } + Self.completeFuture(loop, future, result, err) } return future } @@ -1780,18 +1770,7 @@ public struct PythonFunction { } catch { err = error } - - let gilState = PyGILState_Ensure() - defer { PyGILState_Release(gilState) } - - if let err { - loop.call_soon_threadsafe(future.set_exception, Python.ValueError("\(err)")) - } else { - guard let result else { - fatalError("Result should exist if err is nil.") - } - loop.call_soon_threadsafe(future.set_result, result) - } + Self.completeFuture(loop, future, result, err) } return future } @@ -1838,23 +1817,26 @@ public struct PythonFunction { } catch { err = error } - - let gilState = PyGILState_Ensure() - defer { PyGILState_Release(gilState) } - - if let err { - loop.call_soon_threadsafe(future.set_exception, Python.ValueError("\(err)")) - } else { - guard let result else { - fatalError("Result should exist if err is nil.") - } - loop.call_soon_threadsafe(future.set_result, result) - } + Self.completeFuture(loop, future, result, err) } return future } self.name = name } + + private static func completeFuture( + _ loop: PythonObject, _ future: PythonObject, _ result: PythonConvertible?, _ error: Error?) { + withGIL { + if let error { + loop.call_soon_threadsafe(future.set_exception, Python.ValueError("\(error)")) + } else { + guard let result else { + fatalError("Result should exist if error is nil.") + } + loop.call_soon_threadsafe(future.set_result, result) + } + } + } } extension PythonFunction : PythonConvertible { From 1c257a2ae761fd42112b23f8201071e81dfee45c Mon Sep 17 00:00:00 2001 From: Jeffrey Davis Date: Thu, 15 Aug 2024 11:03:00 -0700 Subject: [PATCH 21/24] Fix access specifiers. --- PythonKit/Python.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/PythonKit/Python.swift b/PythonKit/Python.swift index c7d2beb..cccdb5b 100644 --- a/PythonKit/Python.swift +++ b/PythonKit/Python.swift @@ -748,7 +748,7 @@ class PythonThread { private static var sharedState: PyGILState_State? /// Enter the GIL and initialize Python thread state. - public static func enterGIL() { + static func enterGIL() { guard Self.sharedState == nil else { fatalError("The GIL is already held by this thread.") } @@ -756,7 +756,7 @@ class PythonThread { } /// Exit the GIL. - public static func leaveGIL() { + static func leaveGIL() { guard let state = Self.sharedState else { fatalError("The GIL is not held by this thread.") } From be9391c2e0028857fb0742898d6bd78e09994025 Mon Sep 17 00:00:00 2001 From: Jeffrey Davis Date: Thu, 15 Aug 2024 13:20:45 -0700 Subject: [PATCH 22/24] Don't share thread state. --- PythonKit/Python.swift | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/PythonKit/Python.swift b/PythonKit/Python.swift index cccdb5b..74334ba 100644 --- a/PythonKit/Python.swift +++ b/PythonKit/Python.swift @@ -745,37 +745,39 @@ public struct PythonInterface { //===----------------------------------------------------------------------===// class PythonThread { - private static var sharedState: PyGILState_State? + private var state: PyGILState_State? /// Enter the GIL and initialize Python thread state. - static func enterGIL() { - guard Self.sharedState == nil else { + func enterGIL() { + guard state == nil else { fatalError("The GIL is already held by this thread.") } - Self.sharedState = PyGILState_Ensure() + state = PyGILState_Ensure() } /// Exit the GIL. - static func leaveGIL() { - guard let state = Self.sharedState else { + func leaveGIL() { + guard let s = state else { fatalError("The GIL is not held by this thread.") } - PyGILState_Release(state) - Self.sharedState = nil + PyGILState_Release(s) + state = nil } } /// Execute body while holding the GIL. public func withGIL(_ body: () throws -> T) rethrows -> T { - PythonThread.enterGIL() - defer { PythonThread.leaveGIL() } + let thread = PythonThread() + thread.enterGIL() + defer { thread.leaveGIL() } return try body() } /// Leave the GIL, execute the body, then re-enter the GIL. public func withoutGIL(_ body: () throws -> T) rethrows -> T { - PythonThread.leaveGIL() - defer { PythonThread.enterGIL() } + let thread = PythonThread() + thread.leaveGIL() + defer { thread.enterGIL() } return try body() } From 2ae2787e02f9f1acfd1bb40bc56eabde2cb8f5e8 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Wed, 16 Oct 2024 10:14:47 -0300 Subject: [PATCH 23/24] Comment out line causing Py_DecRef crashes in the evals. --- PythonKit/Python.swift | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/PythonKit/Python.swift b/PythonKit/Python.swift index 74334ba..6718673 100644 --- a/PythonKit/Python.swift +++ b/PythonKit/Python.swift @@ -55,7 +55,12 @@ final class PyReference { } deinit { - Py_DecRef(pointer) + // AIQ-230: This is causing crashing in the evals. + // We don't use PythonKit in production, so in the absence of a proper fix + // we're commenting this out. + // + // This change should be reverted if we ever use PythonKit in production. + // Py_DecRef(pointer) } var borrowedPyObject: PyObjectPointer { From a38913bb3e06b4f12227f18bcd95a149783c27ce Mon Sep 17 00:00:00 2001 From: Jeff Date: Fri, 28 Mar 2025 10:14:00 -0700 Subject: [PATCH 24/24] Delete temp file. --- temp | 1 - 1 file changed, 1 deletion(-) delete mode 100644 temp diff --git a/temp b/temp deleted file mode 100644 index 9ae9ea0..0000000 --- a/temp +++ /dev/null @@ -1 +0,0 @@ -Contents \ No newline at end of file