File: PluginManager.swift

package info (click to toggle)
swiftlang 6.2.3-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 2,856,264 kB
  • sloc: cpp: 9,995,718; ansic: 2,234,019; asm: 1,092,167; python: 313,940; objc: 82,726; f90: 80,126; lisp: 38,373; pascal: 25,580; sh: 20,378; ml: 5,058; perl: 4,751; makefile: 4,725; awk: 3,535; javascript: 3,018; xml: 918; fortran: 664; cs: 573; ruby: 396
file content (186 lines) | stat: -rw-r--r-- 8,284 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift open source project
//
// Copyright (c) 2025 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See http://swift.org/LICENSE.txt for license information
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

import SWBLibc

@globalActor public actor PluginExtensionSystemActor: GlobalActor {
    public static let shared = PluginExtensionSystemActor()
}

/// A generic area of extensibility.
///
/// Plugins hook into specific extension points to add additional behavior; the extension points themselves are defined by clients.
@PluginExtensionSystemActor public protocol ExtensionPoint: Sendable {
    /// The protocol describing the API required of extensions.
    associatedtype ExtensionProtocol: Sendable

    /// The name of the extension point.
    static var name: String { get }
}

/// A very basic Swift plugin manager.
///
/// This manager works by using a *very* minimal API between the service and the plugins, only the PluginManager itself is passed to the plugins (as an opaque pointer). All actual plugging then can happen using real Swift APIs between the plugin manager and the plugins.
///
/// This mechanism is *NOT* intended to be used to build plugins which have a stable API -- all plugins must be built with exactly the Swift Build the are intended to plug in to. The only thing this allows is for dynamically loaded the plugin in order to manage distributing parts independently.
@PluginExtensionSystemActor public final class PluginManager: Sendable {
    private struct Plugin: CommonPlugin {
        /// The identifier of the plugin.
        @_spi(Testing) public let identifier: String

        /// The path to the plugin. If the plugin is a bundle, this is the path to the bundle directory. Otherwise, it's the path to the plugin library.
        public let path: Path
    }

    /// The set of all plugins we've successfully loaded (CFBundleIdentifier for bundles if they have one, filenames otherwise), mapping to the paths to those plugins (for debugging convenience).
    ///
    /// This is primarily used to avoid loading the same plugin twice.
    private var loadedPlugins = [String: Path]()

    /// Diagnostics produced during plugin loading.
    public private(set) var loadingDiagnostics: [Diagnostic] = []

    /// The set of registered extension points.
    private var extensionPoints: [String: any ExtensionPoint] = [:]

    private var extensions: [Ref<any ExtensionPoint>: [Any]] = [:]

    private let skipLoadingPluginIdentifiers: Set<String>

    /// Create the plugin manager.
    ///
    /// Clients are expected to register all of the available extension points, then load the plugins.
    public init(skipLoadingPluginIdentifiers: Set<String>) {
        self.skipLoadingPluginIdentifiers = skipLoadingPluginIdentifiers
    }

    public var pluginsByIdentifier: [String: any CommonPlugin] {
        Dictionary(uniqueKeysWithValues: loadedPlugins.map { ($0.key, Plugin(identifier: $0.key, path: $0.value)) })
    }

    /// Load the plugin at the given path. This is meant for unit tests where a specific plugin is being tested.
    public func loadPlugin(at path: Path) {
        // If we found a bundle, load it and look for a registration function.
        let name = path.basename

        let pluginPath: Path
        let executablePath: Path
        let pluginIdentifier: String
        switch path.fileSuffix {
        case ".bundle":
            pluginPath = path
            let shallow = !localFS.exists(path.join("Contents"))
            let executableBasePath = shallow
                ? path.join(Path(name).basenameWithoutSuffix)
                : path.join("Contents").join("MacOS").join(Path(name).basenameWithoutSuffix)
            executablePath = {
                if let suffix = getEnvironmentVariable("DYLD_IMAGE_SUFFIX")?.nilIfEmpty {
                    let candidate = executableBasePath.appendingFileNameSuffix(suffix)
                    if localFS.exists(candidate) {
                        return candidate
                    }
                }
                return executableBasePath
            }()
            let infoPlistPath = shallow
                ? path.join("Info.plist")
                : path.join("Contents").join("Info.plist")
            if let plist = try? PropertyList.fromPath(infoPlistPath, fs: localFS), let cfBundleIdentifier = plist.dictValue?["CFBundleIdentifier"]?.stringValue {
                pluginIdentifier = cfBundleIdentifier
            }
            else {
                pluginIdentifier = path.basename
            }
        default:
            return
        }

        // If we've already loaded a plugin with this identifier, don't load it again.
        if let existingPluginPath = loadedPlugins[pluginIdentifier] {
            loadingDiagnostics.append(Diagnostic(behavior: .warning, location: .unknown, data: DiagnosticData("Duplicate plugin with identifier '\(pluginIdentifier)' at path: \(pluginPath.str) (already loaded from path \(existingPluginPath.str))")))
            return
        }

        guard !skipLoadingPluginIdentifiers.contains(pluginIdentifier) else {
            return
        }

        let handle: LibraryHandle
        do {
            handle = try Library.open(executablePath)
        } catch {
            loadingDiagnostics.append(Diagnostic(behavior: .error, location: .unknown, data: DiagnosticData("Unable to load plugin: \(pluginPath.str): \(error)")))
            return
        }

        typealias PluginInitializationFunc = @PluginExtensionSystemActor @convention(c) (UnsafeRawPointer) -> Void
        guard let f: PluginInitializationFunc = Library.lookup(handle, "initializePlugin") else {
            loadingDiagnostics.append(Diagnostic(behavior: .error, location: .unknown, data: DiagnosticData("Missing plugin entry point: \(executablePath.str)")))
            return
        }

        // Ask the plugin to register itself.
        f(Unmanaged.passUnretained(self).toOpaque())

        // Remember that we loaded this plugin.
        loadedPlugins[pluginIdentifier] = pluginPath
    }

    /// Load plugins present at the given path.
    public func load(at path: Path) {
        for name in (try? localFS.listdir(path).sorted()) ?? [] {
            self.loadPlugin(at: path.join(name))
        }
    }

    /// Register a new extension point.
    ///
    /// This is intended to be used by clients before any plugin loading happens.
    ///
    /// This is *NOT* thread safe.
    public func registerExtensionPoint<T: ExtensionPoint>(_ extensionPoint: T) {
        extensionPoints[T.self.name] = extensionPoint
    }

    /// Register a new extension for a particular extension point.
    public func register<T: ExtensionPoint>(_ instance: T.ExtensionProtocol, type: T.Type) {
        // Get the extension point.
        guard let extensionPoint = extensionPoints[T.self.name] else {
            fatalError("unknown extension point: \(T.self.name)")
        }

        _register(extensionPoint as! T, instance)
    }

    /// Register a new extension for a particular extension point if the extension point exists. This is only meant for plugin-hosted extension points used in unit tests.
    public func conditionallyRegister<T: ExtensionPoint>(_ instance: T.ExtensionProtocol, type: T.Type) {
        // Get the extension point and return if we can't find it
        guard let extensionPoint = extensionPoints[T.self.name] else {
            return
        }

        _register(extensionPoint as! T, instance)
    }

    private func _register<T: ExtensionPoint>(_ extensionPoint: T, _ instance: T.ExtensionProtocol) {
        extensions[Ref(extensionPoint), default: []].append(instance)
    }

    public func extensions<T: ExtensionPoint>(of extensionPoint: T.Type) -> [T.ExtensionProtocol] {
        extensions.flatMap { key, value in
            if type(of: key.instance) == extensionPoint {
                return value as! [T.ExtensionProtocol]
            }
            return []
        }
    }
}