See More

//===-- PythonLibrary.swift -----------------------------------*- swift -*-===// // // This source file is part of the Swift.org open source project // // Copyright (c) 2014 - 2018 Apple Inc. and the Swift project authors // Licensed under Apache License v2.0 with Runtime Library Exception // // See https://swift.org/LICENSE.txt for license information // See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors // //===----------------------------------------------------------------------===// // // This file implements the logic for dynamically loading Python at runtime. // //===----------------------------------------------------------------------===// #if canImport(Darwin) import Darwin #elseif canImport(Glibc) import Glibc #elseif canImport(Musl) import Musl #elseif os(Windows) import CRT import WinSDK #endif //===----------------------------------------------------------------------===// // The `PythonLibrary` struct that loads Python symbols at runtime. //===----------------------------------------------------------------------===// public struct PythonLibrary { public enum Error: Swift.Error, Equatable, CustomStringConvertible { case pythonLibraryNotFound public var description: String { switch self { case .pythonLibraryNotFound: return """ Python library not found. Set the \(Environment.library.key) \ environment variable with the path to a Python library. """ } } } 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 } let pythonLibraryHandle = self.loadPythonLibrary() guard self.isPythonLibraryLoaded(at: pythonLibraryHandle) else { throw Error.pythonLibraryNotFound } self.isPythonLibraryLoaded = true self._pythonLibraryHandle = pythonLibraryHandle } private static let isLegacyPython: Bool = { let isLegacyPython = PythonLibrary.loadSymbol(PythonLibrary.pythonLibraryHandle, PythonLibrary.pythonLegacySymbolName) != nil if isLegacyPython { PythonLibrary.log("Loaded legacy Python library, using legacy symbols...") } 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) } } // Methods of `PythonLibrary` required to load the Python library. 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 = [""] private static var librarySearchPaths = ["", "/opt/homebrew/Frameworks/", "/usr/local/Frameworks/"] private static var libraryVersionSeparator = "." #elseif os(Linux) private static var libraryNames = ["libpython:", "libpython:m"] private static var libraryPathExtensions = [".so"] private static var librarySearchPaths = [""] private static var libraryVersionSeparator = "." #elseif os(Windows) private static var libraryNames = ["python:"] private static var libraryPathExtensions = [".dll"] private static var librarySearchPaths = [""] private static var libraryVersionSeparator = "" #endif private static let libraryPaths: [String] = { var libraryPaths: [String] = [] for librarySearchPath in librarySearchPaths { for libraryName in libraryNames { for libraryPathExtension in libraryPathExtensions { let libraryPath = librarySearchPath + libraryName + libraryPathExtension libraryPaths.append(libraryPath) } } } return libraryPaths }() private static func loadSymbol( _ libraryHandle: UnsafeMutableRawPointer?, _ name: String) -> UnsafeMutableRawPointer? { #if os(Windows) guard let libraryHandle = libraryHandle else { return nil } let moduleHandle = libraryHandle .assumingMemoryBound(to: HINSTANCE__.self) let moduleSymbol = GetProcAddress(moduleHandle, name) return unsafeBitCast(moduleSymbol, to: UnsafeMutableRawPointer?.self) #else 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() { pythonLibraryHandle = self.defaultLibraryHandle } else if let pythonLibraryPath = Environment.library.value { pythonLibraryHandle = self.loadPythonLibrary(at: pythonLibraryPath) } else { pythonLibraryHandle = self.findAndLoadExternalPythonLibrary() } return pythonLibraryHandle } private static func findAndLoadExternalPythonLibrary() -> UnsafeMutableRawPointer? { for majorVersion in supportedMajorVersions { for minorVersion in supportedMinorVersions { for libraryPath in libraryPaths { let version = PythonVersion(major: majorVersion, minor: minorVersion) guard let pythonLibraryHandle = loadPythonLibrary( at: libraryPath, version: version) else { continue } return pythonLibraryHandle } } } 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, requiredMajorVersion != version.major { return nil } } let libraryVersionString = versionString .split(separator: PythonVersion.versionSeparator) .joined(separator: libraryVersionSeparator) let path = path.split(separator: libraryPathVersionCharacter) .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) let pythonLibraryHandle = UnsafeMutableRawPointer(LoadLibraryA(path)) #else // Must be RTLD_GLOBAL because subsequent .so files from the imported python // modules may depend on this .so file. let pythonLibraryHandle = dlopen(path, RTLD_LAZY | RTLD_GLOBAL) if pythonLibraryHandle == nil { self.log("Failed to load library at '\(path)'.") if let errorCString = dlerror() { let errorString = String(cString: errorCString) self.log("Reason for failure: \(errorString)") } } #endif if pythonLibraryHandle != nil { self.log("Library at '\(path)' was successfully loaded.") } return pythonLibraryHandle } } // Methods of `PythonLibrary` required to set a given Python version or library path. extension PythonLibrary { private static func enforceNonLoadedPythonLibrary(function: String = #function) { precondition(!self.isPythonLibraryLoaded, """ Error: \(function) should not be called after any Python library \ has already been loaded. """) } /// Use the Python library with the specified version. /// - Parameters: /// - major: Major version or nil to use any Python version. /// - minor: Minor version or nil to use any minor version. public static func useVersion(_ major: Int?, _ minor: Int? = nil) { self.enforceNonLoadedPythonLibrary() 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?) { self.enforceNonLoadedPythonLibrary() PythonLibrary.Environment.library.set(path ?? "") } } // `PythonVersion` struct that defines a given Python version. 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 \ without the major version. """) self.major = major self.minor = minor } var versionString: String { guard let major = major else { return "" } var versionString = String(major) if let minor = minor { versionString += "\(PythonVersion.versionSeparator)\(minor)" } return versionString } } } // `PythonLibrary.Environment` enum used to read and set environment variables. 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) #else setenv(key, value, 1) #endif } } } // Methods of `PythonLibrary` used for logging messages. extension PythonLibrary { private static func log(_ message: String) { guard Environment.loaderLogging.value != nil else { return } fputs(message + "\n", stderr) } }