Files
sourcekit-lsp/Sources/SwiftSourceKitPlugin/Plugin.swift
T
Rintaro Ishizaki 4b2fa3193a Allow injection of a pre-initialized sourcekitd connection
Introduce `SourceKitDCore` as the protocol boundary between dylib
lifecycle management and the high-level `SourceKitD` API. Its single
lifecycle entry point, `initializeService(api:notificationCallback:)`,
receives the already-loaded `sourcekitd_api_functions_t` from
`SourceKitD.init(core:)`.

`SourceKitDCoreImpl` is the standard implementation: `init` opens the
dylib; `initializeService` registers any plugin paths, calls
`api.initialize()`, and wires the notification handler; `deinit` calls
`shutdown()` and closes the handle. Pre-initialized conformances
implement `initializeService` as a no-op.

Wire a `sourcekitdCoreInjector` hook through `Hooks` so an embedding
host can return a pre-initialized `SourceKitDCore` for a given toolchain
path, preventing `sourcekitd_initialize()` from being called a second
time.

Declare `SourceKitDCoreForPlugin` at its use sites so each call site
can express the exact deinit behavior it needs: `dlclose` for handles
acquired via `RTLD_NOLOAD`, and `leak` for externally-owned handles.
2026-05-07 09:58:47 -07:00

322 lines
11 KiB
Swift

//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2024 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
//
//===----------------------------------------------------------------------===//
public import Csourcekitd
import Foundation
@_spi(SourceKitLSP) import SKLogging
import SourceKitD
import SwiftExtensions
import SwiftSourceKitPluginCommon
import ToolsProtocolsSwiftExtensions
private func useNewAPI(for dict: SKDRequestDictionaryReader) -> Bool {
guard let opts: SKDRequestDictionaryReader = dict[dict.sourcekitd.keys.codeCompleteOptions],
opts[dict.sourcekitd.keys.useNewAPI] == 1
else {
return false
}
return true
}
final class RequestHandler: Sendable {
enum HandleRequestResult {
/// `handleRequest` will call `receiver`.
case requestHandled
/// `handleRequest` will not call `receiver` and a request response should be produced by sourcekitd (not the plugin).
case handleInSourceKitD
}
let requestHandlingQueue = AsyncQueue<Serial>()
let sourcekitd: SourceKitD
let completionProvider: CompletionProvider
init(params: sourcekitd_api_plugin_initialize_params_t, completionResultsBufferKind: UInt64, sourcekitd: SourceKitD) {
let ideInspectionInstance = sourcekitd.servicePluginApi.plugin_initialize_get_swift_ide_inspection_instance(params)
self.sourcekitd = sourcekitd
self.completionProvider = CompletionProvider(
completionResultsBufferKind: completionResultsBufferKind,
opaqueIDEInspectionInstance: OpaqueIDEInspectionInstance(ideInspectionInstance),
sourcekitd: sourcekitd
)
}
func handleRequest(
_ dict: SKDRequestDictionaryReader,
handle: RequestHandle?,
receiver: @Sendable @escaping (SKDResponse) -> Void
) -> HandleRequestResult {
func produceResult(
body: @escaping @Sendable () async throws -> SKDResponseDictionaryBuilder
) -> HandleRequestResult {
withLoggingScope("request-\((handle?.numericValue ?? 0) % 100)") {
let start = Date()
logger.debug(
"""
Plugin received sourcekitd request (handle: \(handle?.numericValue ?? -1))
\(dict.description)
"""
)
requestHandlingQueue.async {
let response: SKDResponse
do {
response = try await body().response
} catch {
response = SKDResponse.from(error: error, sourcekitd: self.sourcekitd)
}
logger.debug(
"""
Finished (took \(Date().timeIntervalSince(start))s)
\(response.description)
"""
)
receiver(response)
}
return .requestHandled
}
}
func sourcekitdProducesResult(body: @escaping @Sendable () async -> Void) -> HandleRequestResult {
requestHandlingQueue.async {
await body()
}
return .handleInSourceKitD
}
switch dict[sourcekitd.keys.request] as sourcekitd_api_uid_t? {
case sourcekitd.requests.editorOpen:
return sourcekitdProducesResult {
await self.completionProvider.handleDocumentOpen(dict)
}
case sourcekitd.requests.editorReplaceText:
return sourcekitdProducesResult {
await self.completionProvider.handleDocumentEdit(dict)
}
case sourcekitd.requests.editorClose:
return sourcekitdProducesResult {
await self.completionProvider.handleDocumentClose(dict)
}
case sourcekitd.requests.codeCompleteOpen:
guard useNewAPI(for: dict) else {
return .handleInSourceKitD
}
return produceResult {
try await self.completionProvider.handleCompleteOpen(dict, handle: handle)
}
case sourcekitd.requests.codeCompleteUpdate:
guard useNewAPI(for: dict) else {
return .handleInSourceKitD
}
return produceResult {
try await self.completionProvider.handleCompleteUpdate(dict)
}
case sourcekitd.requests.codeCompleteClose:
guard useNewAPI(for: dict) else {
return .handleInSourceKitD
}
return produceResult {
try await self.completionProvider.handleCompleteClose(dict)
}
case sourcekitd.requests.codeCompleteDocumentation:
return produceResult {
try await self.completionProvider.handleCompletionDocumentation(dict)
}
case sourcekitd.requests.codeCompleteDiagnostic:
return produceResult {
try await self.completionProvider.handleCompletionDiagnostic(dict)
}
case sourcekitd.requests.codeCompleteSetPopularAPI:
guard useNewAPI(for: dict) else {
return .handleInSourceKitD
}
return produceResult {
await self.completionProvider.handleSetPopularAPI(dict)
}
case sourcekitd.requests.dependencyUpdated:
return sourcekitdProducesResult {
await self.completionProvider.handleDependencyUpdated()
}
default:
return .handleInSourceKitD
}
}
func cancel(_ handle: RequestHandle) {
logger.debug("Cancelling request with handle \(handle.numericValue)")
self.completionProvider.cancel(handle: handle)
}
}
/// Legacy plugin initialization logic in which sourcekitd does not inform the plugin about the sourcekitd path it was
/// loaded from.
@_cdecl("sourcekitd_plugin_initialize")
public func sourcekitd_plugin_initialize(_ params: sourcekitd_api_plugin_initialize_params_t) {
LoggingScope.configureDefaultLoggingSubsystem("org.swift.sourcekit-lsp.service-plugin")
logger.fault("sourcekitd_plugin_initialize has been removed in favor of sourcekitd_plugin_initialize_2")
}
#if canImport(Darwin)
private extension SourceKitD {
/// When a plugin is initialized, it gets passed the library it was loaded from to `sourcekitd_plugin_initialize_2`.
///
/// Since the plugin wants to interact with sourcekitd in-process, it needs to load `sourcekitdInProc`. This function
/// loads `sourcekitdInProc` relative to the parent library path, if it exists, or `sourcekitd` if `sourcekitdInProc`
/// doesn't exist (eg. on Linux where `sourcekitd` is already in-process).
static func inProcLibrary(relativeTo parentLibraryPath: URL) throws -> SourceKitD {
var frameworkUrl = parentLibraryPath
// Remove path components until we reach the `sourcekitd.framework` directory. The plugin might have been loaded
// from an XPC service, in which case `parentLibraryPath` is
// `sourcekitd.framework/XPCServices/SourceKitService.xpc/Contents/MacOS/SourceKitService`.
while frameworkUrl.pathExtension != "framework" {
guard frameworkUrl.pathComponents.count > 1 else {
struct NoFrameworkPathError: Error, CustomStringConvertible {
var parentLibraryPath: URL
var description: String { "Could not find .framework directory relative to '\(parentLibraryPath)'" }
}
throw NoFrameworkPathError(parentLibraryPath: parentLibraryPath)
}
frameworkUrl.deleteLastPathComponent()
}
frameworkUrl.deleteLastPathComponent()
let inProcUrl =
frameworkUrl
.appending(components: "sourcekitdInProc.framework", "sourcekitdInProc")
if FileManager.default.fileExists(at: inProcUrl) {
return try SourceKitD(core: SourceKitDCoreForPlugin(dylibPath: inProcUrl))
}
let sourcekitdUrl =
frameworkUrl
.appending(components: "sourcekitd.framework", "sourcekitd")
return try SourceKitD(core: SourceKitDCoreForPlugin(dylibPath: sourcekitdUrl))
}
}
#endif
@_cdecl("sourcekitd_plugin_initialize_2")
public func sourcekitd_plugin_initialize_2(
_ params: sourcekitd_api_plugin_initialize_params_t,
_ parentLibraryPath: UnsafePointer<CChar>
) {
LoggingScope.configureDefaultLoggingSubsystem("org.swift.sourcekit-lsp.service-plugin")
let parentLibraryPath = String(cString: parentLibraryPath)
#if canImport(Darwin)
if parentLibraryPath == "SOURCEKIT_LSP_PLUGIN_PARENT_LIBRARY_RTLD_DEFAULT" {
SourceKitD.forPlugin = try! SourceKitD(
core: SourceKitDCoreForPlugin(dlhandle: .rtldDefault, path: URL(string: "rtld-default://")!)
)
} else {
SourceKitD.forPlugin = try! SourceKitD.inProcLibrary(relativeTo: URL(fileURLWithPath: parentLibraryPath))
}
#else
// On other platforms, sourcekitd is always in process, so we can load it straight away.
SourceKitD.forPlugin = try! SourceKitD(
core: SourceKitDCoreForPlugin(dylibPath: URL(fileURLWithPath: parentLibraryPath))
)
#endif
let sourcekitd = SourceKitD.forPlugin
let completionResultsBufferKind = sourcekitd.pluginApi.plugin_initialize_custom_buffer_start(params)
let isClientOnly = sourcekitd.pluginApi.plugin_initialize_is_client_only(params)
let uidFromCString = sourcekitd.pluginApi.plugin_initialize_uid_get_from_cstr(params)
let uidGetCString = sourcekitd.pluginApi.plugin_initialize_uid_get_string_ptr(params)
// Depending on linking and loading configuration, we may need to chain the global UID handlers back to the UID
// handlers in the caller. The extra hop should not matter, since we cache the results.
if unsafeBitCast(uidFromCString, to: UnsafeRawPointer.self)
!= unsafeBitCast(sourcekitd.api.uid_get_from_cstr, to: UnsafeRawPointer.self)
{
sourcekitd.api.set_uid_handlers(uidFromCString, uidGetCString)
}
sourcekitd.pluginApi.plugin_initialize_register_custom_buffer(
params,
completionResultsBufferKind,
CompletionResultsArray.arrayFuncs.rawValue
)
if isClientOnly {
return
}
let requestHandler = RequestHandler(
params: params,
completionResultsBufferKind: completionResultsBufferKind,
sourcekitd: sourcekitd
)
sourcekitd.servicePluginApi.plugin_initialize_register_cancellation_handler(params) { handle in
if let handle = RequestHandle(handle) {
requestHandler.cancel(handle)
}
}
sourcekitd.servicePluginApi.plugin_initialize_register_cancellable_request_handler(params) {
(request, handle, receiver) -> Bool in
guard let receiver, let request, let dict = SKDRequestDictionaryReader(request, sourcekitd: sourcekitd) else {
return false
}
let handle = RequestHandle(handle)
let handledRequest = requestHandler.handleRequest(dict, handle: handle) { receiver($0.underlyingValueRetained()) }
switch handledRequest {
case .requestHandled: return true
case .handleInSourceKitD: return false
}
}
}
private final class SourceKitDCoreForPlugin: SourceKitDCore, Sendable {
let dlHandle: DLHandle
let path: URL
private let ownsHandle: Bool
init(dylibPath: URL) throws {
#if os(Windows)
let dlopenModes: DLOpenFlags = []
#else
let dlopenModes: DLOpenFlags = [.lazy, .local, .noLoad]
#endif
self.dlHandle = try dlopen(dylibPath.filePath, mode: dlopenModes)
self.path = dylibPath
self.ownsHandle = true
}
init(dlhandle: DLHandle, path: URL) {
self.dlHandle = dlhandle
self.path = path
self.ownsHandle = false
}
deinit {
if ownsHandle {
try? dlHandle.close()
} else {
dlHandle.leak()
}
}
func initializeService(
api: sourcekitd_api_functions_t,
notificationCallback: @escaping @Sendable (sourcekitd_api_response_t) -> Void
) {
// Borrowed handle — sourcekitd is already initialized.
}
}