|
| 1 | +//===----------------------------------------------------------------------===// |
| 2 | +// |
| 3 | +// This source file is part of the Swift open source project |
| 4 | +// |
| 5 | +// Copyright (c) 2023 Apple Inc. and the Swift project authors |
| 6 | +// Licensed under Apache License v2.0 with Runtime Library Exception |
| 7 | +// |
| 8 | +// See http://swift.org/LICENSE.txt for license information |
| 9 | +// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors |
| 10 | +// |
| 11 | +//===----------------------------------------------------------------------===// |
| 12 | +// NOTE: This basic plugin mechanism is mostly copied from |
| 13 | +// https://github.com/apple/swift-package-manager/blob/main/Sources/PackagePlugin/Plugin.swift |
| 14 | + |
| 15 | +import SwiftSyntaxMacros |
| 16 | + |
| 17 | +@_implementationOnly import Foundation |
| 18 | +#if os(Windows) |
| 19 | +@_implementationOnly import ucrt |
| 20 | +#endif |
| 21 | + |
| 22 | +// |
| 23 | +// This source file contains the main entry point for compiler plugins. |
| 24 | +// A plugin receives messages from the "plugin host" (typically |
| 25 | +// 'swift-frontend'), and sends back messages in return based on its actions. |
| 26 | +// |
| 27 | +// Depending on the platform, plugins are invoked in a sanbox that blocks |
| 28 | +// network access and prevents any file system changes. |
| 29 | +// |
| 30 | +// The host process and the plugin communicate using messages in the form of |
| 31 | +// length-prefixed JSON-encoded Swift enums. The host sends messages to the |
| 32 | +// plugin through its standard-input pipe, and receives messages through the |
| 33 | +// plugin's standard-output pipe. The plugin's standard-error is considered |
| 34 | +// to be free-form textual console output. |
| 35 | +// |
| 36 | +// Within the plugin process, `stdout` is redirected to `stderr` so that print |
| 37 | +// statements from the plugin are treated as plain-text output, and `stdin` is |
| 38 | +// closed so that any attemps by the plugin logic to read from console result |
| 39 | +// in errors instead of blocking the process. The original `stdin` and `stdout` |
| 40 | +// are duplicated for use as messaging pipes, and are not directly used by the |
| 41 | +// plugin logic. |
| 42 | +// |
| 43 | +// The exit code of the plugin process indicates whether the plugin invocation |
| 44 | +// is considered successful. A failure result should also be accompanied by an |
| 45 | +// emitted error diagnostic, so that errors are understandable by the user. |
| 46 | +// |
| 47 | +// Using standard input and output streams for messaging avoids having to make |
| 48 | +// allowances in the sandbox for other channels of communication, and seems a |
| 49 | +// more portable approach than many of the alternatives. This is all somewhat |
| 50 | +// temporary in any case — in the long term, something like distributed actors |
| 51 | +// or something similar can hopefully replace the custom messaging. |
| 52 | +// |
| 53 | +// Usage: |
| 54 | +// struct MyPlugin: CompilerPlugin { |
| 55 | +// var providingMacros: [Macros.Type] = [ |
| 56 | +// StringifyMacro.self |
| 57 | +// ] |
| 58 | +public protocol CompilerPlugin { |
| 59 | + init() |
| 60 | + |
| 61 | + var providingMacros: [Macro.Type] { get } |
| 62 | +} |
| 63 | + |
| 64 | +extension CompilerPlugin { |
| 65 | + |
| 66 | + /// Main entry point of the plugin — sets up a communication channel with |
| 67 | + /// the plugin host and runs the main message loop. |
| 68 | + public static func main() throws { |
| 69 | + // Duplicate the `stdin` file descriptor, which we will then use for |
| 70 | + // receiving messages from the plugin host. |
| 71 | + let inputFD = dup(fileno(stdin)) |
| 72 | + guard inputFD >= 0 else { |
| 73 | + internalError("Could not duplicate `stdin`: \(describe(errno: errno)).") |
| 74 | + } |
| 75 | + |
| 76 | + // Having duplicated the original standard-input descriptor, we close |
| 77 | + // `stdin` so that attempts by the plugin to read console input (which |
| 78 | + // are usually a mistake) return errors instead of blocking. |
| 79 | + guard close(fileno(stdin)) >= 0 else { |
| 80 | + internalError("Could not close `stdin`: \(describe(errno: errno)).") |
| 81 | + } |
| 82 | + |
| 83 | + // Duplicate the `stdout` file descriptor, which we will then use for |
| 84 | + // sending messages to the plugin host. |
| 85 | + let outputFD = dup(fileno(stdout)) |
| 86 | + guard outputFD >= 0 else { |
| 87 | + internalError("Could not dup `stdout`: \(describe(errno: errno)).") |
| 88 | + } |
| 89 | + |
| 90 | + // Having duplicated the original standard-output descriptor, redirect |
| 91 | + // `stdout` to `stderr` so that all free-form text output goes there. |
| 92 | + guard dup2(fileno(stderr), fileno(stdout)) >= 0 else { |
| 93 | + internalError("Could not dup2 `stdout` to `stderr`: \(describe(errno: errno)).") |
| 94 | + } |
| 95 | + |
| 96 | + // Turn off full buffering so printed text appears as soon as possible. |
| 97 | + // Windows is much less forgiving than other platforms. If line |
| 98 | + // buffering is enabled, we must provide a buffer and the size of the |
| 99 | + // buffer. As a result, on Windows, we completely disable all |
| 100 | + // buffering, which means that partial writes are possible. |
| 101 | + #if os(Windows) |
| 102 | + setvbuf(stdout, nil, _IONBF, 0) |
| 103 | + #else |
| 104 | + setvbuf(stdout, nil, _IOLBF, 0) |
| 105 | + #endif |
| 106 | + |
| 107 | + // Open a message channel for communicating with the plugin host. |
| 108 | + pluginHostConnection = PluginHostConnection( |
| 109 | + inputStream: FileHandle(fileDescriptor: inputFD), |
| 110 | + outputStream: FileHandle(fileDescriptor: outputFD) |
| 111 | + ) |
| 112 | + |
| 113 | + // Handle messages from the host until the input stream is closed, |
| 114 | + // indicating that we're done. |
| 115 | + let instance = Self() |
| 116 | + do { |
| 117 | + while let message = try pluginHostConnection.waitForNextMessage() { |
| 118 | + try instance.handleMessage(message) |
| 119 | + } |
| 120 | + } catch { |
| 121 | + // Emit a diagnostic and indicate failure to the plugin host, |
| 122 | + // and exit with an error code. |
| 123 | + internalError(String(describing: error)) |
| 124 | + } |
| 125 | + } |
| 126 | + |
| 127 | + // Private function to report internal errors and then exit. |
| 128 | + fileprivate static func internalError(_ message: String) -> Never { |
| 129 | + fputs("Internal Error: \(message)\n", stderr) |
| 130 | + exit(1) |
| 131 | + } |
| 132 | + |
| 133 | + // Private function to construct an error message from an `errno` code. |
| 134 | + fileprivate static func describe(errno: Int32) -> String { |
| 135 | + if let cStr = strerror(errno) { return String(cString: cStr) } |
| 136 | + return String(describing: errno) |
| 137 | + } |
| 138 | + |
| 139 | + /// Handles a single message received from the plugin host. |
| 140 | + fileprivate func handleMessage(_ message: HostToPluginMessage) throws { |
| 141 | + switch message { |
| 142 | + case .getCapability: |
| 143 | + try pluginHostConnection.sendMessage( |
| 144 | + .getCapabilityResult(capability: PluginMessage.capability) |
| 145 | + ) |
| 146 | + break |
| 147 | + |
| 148 | + case .expandFreestandingMacro(let macro, let discriminator, let expandingSyntax): |
| 149 | + try expandFreestandingMacro( |
| 150 | + macro: macro, |
| 151 | + discriminator: discriminator, |
| 152 | + expandingSyntax: expandingSyntax |
| 153 | + ) |
| 154 | + |
| 155 | + case .expandAttachedMacro(let macro, let macroRole, let discriminator, let attributeSyntax, let declSyntax, let parentDeclSyntax): |
| 156 | + try expandAttachedMacro( |
| 157 | + macro: macro, |
| 158 | + macroRole: macroRole, |
| 159 | + discriminator: discriminator, |
| 160 | + attributeSyntax: attributeSyntax, |
| 161 | + declSyntax: declSyntax, |
| 162 | + parentDeclSyntax: parentDeclSyntax |
| 163 | + ) |
| 164 | + } |
| 165 | + } |
| 166 | +} |
| 167 | + |
| 168 | +/// Message channel for bidirectional communication with the plugin host. |
| 169 | +internal fileprivate(set) var pluginHostConnection: PluginHostConnection! |
| 170 | + |
| 171 | +typealias PluginHostConnection = MessageConnection<PluginToHostMessage, HostToPluginMessage> |
| 172 | + |
| 173 | +internal struct MessageConnection<TX, RX> where TX: Encodable, RX: Decodable { |
| 174 | + let inputStream: FileHandle |
| 175 | + let outputStream: FileHandle |
| 176 | + |
| 177 | + func sendMessage(_ message: TX) throws { |
| 178 | + // Encode the message as JSON. |
| 179 | + let payload = try JSONEncoder().encode(message) |
| 180 | + |
| 181 | + // Write the header (a 64-bit length field in little endian byte order). |
| 182 | + var count = UInt64(payload.count).littleEndian |
| 183 | + let header = Swift.withUnsafeBytes(of: &count) { Data($0) } |
| 184 | + assert(header.count == 8) |
| 185 | + |
| 186 | + // Write the header and payload. |
| 187 | + try outputStream._write(contentsOf: header) |
| 188 | + try outputStream._write(contentsOf: payload) |
| 189 | + } |
| 190 | + |
| 191 | + func waitForNextMessage() throws -> RX? { |
| 192 | + // Read the header (a 64-bit length field in little endian byte order). |
| 193 | + guard |
| 194 | + let header = try inputStream._read(upToCount: 8), |
| 195 | + header.count != 0 |
| 196 | + else { |
| 197 | + return nil |
| 198 | + } |
| 199 | + guard header.count == 8 else { |
| 200 | + throw PluginMessageError.truncatedHeader |
| 201 | + } |
| 202 | + |
| 203 | + // Decode the count. |
| 204 | + let count = header.withUnsafeBytes { |
| 205 | + UInt64(littleEndian: $0.load(as: UInt64.self)) |
| 206 | + } |
| 207 | + guard count >= 2 else { |
| 208 | + throw PluginMessageError.invalidPayloadSize |
| 209 | + } |
| 210 | + |
| 211 | + // Read the JSON payload. |
| 212 | + guard |
| 213 | + let payload = try inputStream._read(upToCount: Int(count)), |
| 214 | + payload.count == count |
| 215 | + else { |
| 216 | + throw PluginMessageError.truncatedPayload |
| 217 | + } |
| 218 | + |
| 219 | + // Decode and return the message. |
| 220 | + return try JSONDecoder().decode(RX.self, from: payload) |
| 221 | + } |
| 222 | + |
| 223 | + enum PluginMessageError: Swift.Error { |
| 224 | + case truncatedHeader |
| 225 | + case invalidPayloadSize |
| 226 | + case truncatedPayload |
| 227 | + } |
| 228 | +} |
| 229 | + |
| 230 | +private extension FileHandle { |
| 231 | + func _write(contentsOf data: Data) throws { |
| 232 | + if #available(macOS 10.15.4, iOS 13.4, watchOS 6.2, tvOS 13.4, *) { |
| 233 | + return try self.write(contentsOf: data) |
| 234 | + } else { |
| 235 | + return self.write(data) |
| 236 | + } |
| 237 | + } |
| 238 | + |
| 239 | + func _read(upToCount count: Int) throws -> Data? { |
| 240 | + if #available(macOS 10.15.4, iOS 13.4, watchOS 6.2, tvOS 13.4, *) { |
| 241 | + return try self.read(upToCount: count) |
| 242 | + } else { |
| 243 | + return self.readData(ofLength: 8) |
| 244 | + } |
| 245 | + } |
| 246 | +} |
0 commit comments