Skip to content

Commit 3f670dc

Browse files
committed
[Macros] Add executable compiler plugin support library
'SwiftCompilerPlugin' is a helper library to write an executable compiler plugin. Usage: import SwiftCompilerPlugin import SwiftSyntaxMacros @main struct MyPlugin: CompilerPlugin { var providingMacros: [Macro.Type] = [MyMacro.self] } struct MyMacro: ExpressionMacro { ... }
1 parent 324166c commit 3f670dc

File tree

6 files changed

+926
-0
lines changed

6 files changed

+926
-0
lines changed

Package.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ let package = Package(
4343
.library(name: "SwiftSyntaxParser", type: .static, targets: ["SwiftSyntaxParser"]),
4444
.library(name: "SwiftSyntaxBuilder", type: .static, targets: ["SwiftSyntaxBuilder"]),
4545
.library(name: "SwiftSyntaxMacros", type: .static, targets: ["SwiftSyntaxMacros"]),
46+
.library(name: "SwiftCompilerPlugin", type: .static, targets: ["SwiftCompilerPlugin"]),
4647
.library(name: "SwiftRefactor", type: .static, targets: ["SwiftRefactor"]),
4748
],
4849
targets: [
@@ -122,6 +123,12 @@ let package = Package(
122123
"CMakeLists.txt"
123124
]
124125
),
126+
.target(
127+
name: "SwiftCompilerPlugin",
128+
dependencies: [
129+
"SwiftSyntax", "SwiftParser", "SwiftDiagnostics", "SwiftSyntaxMacros", "SwiftOperators",
130+
]
131+
),
125132
.target(
126133
name: "SwiftRefactor",
127134
dependencies: [
Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
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() async 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+
// Handle messages from the host until the input stream is closed,
113+
// indicating that we're done.
114+
let instance = Self()
115+
do {
116+
while let message = try pluginHostConnection.waitForNextMessage() {
117+
try await instance.handleMessage(message)
118+
}
119+
} catch {
120+
// Emit a diagnostic and indicate failure to the plugin host,
121+
// and exit with an error code.
122+
internalError(String(describing: error))
123+
}
124+
}
125+
126+
// Private function to report internal errors and then exit.
127+
fileprivate static func internalError(_ message: String) -> Never {
128+
fputs("Internal Error: \(message)\n", stderr)
129+
exit(1)
130+
}
131+
132+
// Private function to construct an error message from an `errno` code.
133+
fileprivate static func describe(errno: Int32) -> String {
134+
if let cStr = strerror(errno) { return String(cString: cStr) }
135+
return String(describing: errno)
136+
}
137+
138+
/// Handles a single message received from the plugin host.
139+
fileprivate func handleMessage(_ message: HostToPluginMessage) async throws {
140+
switch message {
141+
case .getCapability:
142+
try pluginHostConnection.sendMessage(
143+
.getCapabilityResult(capability: PluginMessage.capability))
144+
break
145+
146+
case .expandFreestandingMacro(let macro, let discriminator, let expandingSyntax):
147+
try expandFreestandingMacro(
148+
macro: macro,
149+
discriminator: discriminator,
150+
expandingSyntax: expandingSyntax)
151+
152+
case .expandAttachedMacro(let macro, let macroRole, let discriminator, let customAttributeSyntax, let declSyntax, let parentDeclSyntax):
153+
try expandAttachedMacro(
154+
macro: macro,
155+
macroRole: macroRole,
156+
discriminator: discriminator,
157+
customAttributeSyntax: customAttributeSyntax,
158+
declSyntax: declSyntax,
159+
parentDeclSyntax: parentDeclSyntax)
160+
}
161+
}
162+
}
163+
164+
/// Message channel for bidirectional communication with the plugin host.
165+
internal fileprivate(set) var pluginHostConnection: PluginHostConnection!
166+
167+
typealias PluginHostConnection = MessageConnection<PluginToHostMessage, HostToPluginMessage>
168+
169+
internal struct MessageConnection<TX,RX> where TX: Encodable, RX: Decodable {
170+
let inputStream: FileHandle
171+
let outputStream: FileHandle
172+
173+
func sendMessage(_ message: TX) throws {
174+
// Encode the message as JSON.
175+
let payload = try JSONEncoder().encode(message)
176+
177+
// Write the header (a 64-bit length field in little endian byte order).
178+
var count = UInt64(payload.count).littleEndian
179+
let header = Swift.withUnsafeBytes(of: &count) { Data($0) }
180+
assert(header.count == 8)
181+
182+
// Write the header and payload.
183+
try outputStream._write(contentsOf: header)
184+
try outputStream._write(contentsOf: payload)
185+
}
186+
187+
func waitForNextMessage() throws -> RX? {
188+
// Read the header (a 64-bit length field in little endian byte order).
189+
guard
190+
let header = try inputStream._read(upToCount: 8),
191+
header.count != 0
192+
else {
193+
return nil
194+
}
195+
guard header.count == 8 else {
196+
throw PluginMessageError.truncatedHeader
197+
}
198+
199+
// Decode the count.
200+
let count = header.withUnsafeBytes{
201+
UInt64(littleEndian: $0.load(as: UInt64.self))
202+
}
203+
guard count >= 2 else {
204+
throw PluginMessageError.invalidPayloadSize
205+
}
206+
207+
// Read the JSON payload.
208+
guard
209+
let payload = try inputStream._read(upToCount: Int(count)),
210+
payload.count == count
211+
else {
212+
throw PluginMessageError.truncatedPayload
213+
}
214+
215+
// Decode and return the message.
216+
return try JSONDecoder().decode(RX.self, from: payload)
217+
}
218+
219+
enum PluginMessageError: Swift.Error {
220+
case truncatedHeader
221+
case invalidPayloadSize
222+
case truncatedPayload
223+
}
224+
}
225+
226+
private extension FileHandle {
227+
func _write(contentsOf data: Data) throws {
228+
if #available(macOS 10.15.4, iOS 13.4, watchOS 6.2, tvOS 13.4, *) {
229+
return try self.write(contentsOf: data)
230+
} else {
231+
return self.write(data)
232+
}
233+
}
234+
235+
func _read(upToCount count: Int) throws -> Data? {
236+
if #available(macOS 10.15.4, iOS 13.4, watchOS 6.2, tvOS 13.4, *) {
237+
return try self.read(upToCount: count)
238+
} else {
239+
return self.readData(ofLength: 8)
240+
}
241+
}
242+
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
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+
13+
import SwiftDiagnostics
14+
import SwiftSyntax
15+
16+
/// Errors in macro handing.
17+
enum MacroExpansionError: String {
18+
case macroTypeNotFound = "macro expanding type not found"
19+
case freestandingMacroSyntaxIsNotMacro = "macro syntax couldn't be parsed"
20+
}
21+
22+
extension MacroExpansionError: DiagnosticMessage {
23+
var message: String {
24+
self.rawValue
25+
}
26+
var diagnosticID: SwiftDiagnostics.MessageID {
27+
.init(domain: "\(type(of: self))", id: "\(self)")
28+
}
29+
var severity: SwiftDiagnostics.DiagnosticSeverity {
30+
.error
31+
}
32+
}
33+
34+
extension PluginMessage.Diagnostic.Severity {
35+
init(from syntaxDiagSeverity: SwiftDiagnostics.DiagnosticSeverity) {
36+
switch syntaxDiagSeverity {
37+
case .error: self = .error
38+
case .warning: self = .warning
39+
case .note: self = .note
40+
}
41+
}
42+
}
43+
44+
extension PluginMessage.Diagnostic {
45+
init(from syntaxDiag: SwiftDiagnostics.Diagnostic, in sourceManager: SourceManager) {
46+
guard let position = sourceManager.position(
47+
of: syntaxDiag.node, at: .afterLeadingTrivia) else {
48+
fatalError("unknown diagnostic node")
49+
}
50+
51+
self.position = .init(fileName: position.fileName, offset: position.utf8Offset)
52+
self.severity = .init(from: syntaxDiag.diagMessage.severity)
53+
self.message = syntaxDiag.message
54+
55+
self.highlights = syntaxDiag.highlights.map {
56+
guard let range = sourceManager.range(of: $0) else {
57+
fatalError("highlight node is not known")
58+
}
59+
return .init(fileName: range.fileName,
60+
startOffset: range.startUTF8Offset,
61+
endOffset: range.endUTF8Offset)
62+
}
63+
64+
self.notes = syntaxDiag.notes.map {
65+
guard let pos = sourceManager.position(of: $0.node, at: .afterLeadingTrivia) else {
66+
fatalError("note node is not known")
67+
}
68+
let position = PluginMessage.Diagnostic.Position(
69+
fileName: pos.fileName, offset: pos.utf8Offset)
70+
return .init(position: position, message: $0.message)
71+
}
72+
73+
self.fixIts = syntaxDiag.fixIts.map {
74+
PluginMessage.Diagnostic.FixIt(
75+
message: $0.message.message,
76+
changes: $0.changes.changes.map {
77+
let range: SourceManager.SourceRange?
78+
let text: String
79+
switch $0 {
80+
case .replace(let oldNode, let newNode):
81+
range = sourceManager.range(
82+
of: oldNode, from: .afterLeadingTrivia, to: .beforeTrailingTrivia)
83+
text = newNode.trimmedDescription
84+
case .replaceLeadingTrivia(let token, let newTrivia):
85+
range = sourceManager.range(
86+
of: Syntax(token), from: .beforeLeadingTrivia, to: .afterLeadingTrivia)
87+
text = newTrivia.description
88+
case .replaceTrailingTrivia(let token, let newTrivia):
89+
range = sourceManager.range(
90+
of: Syntax(token), from: .beforeTrailingTrivia, to: .afterTrailingTrivia)
91+
text = newTrivia.description
92+
}
93+
guard let range = range else {
94+
fatalError("unknown")
95+
}
96+
return .init(
97+
range: PositionRange(fileName: range.fileName,
98+
startOffset: range.startUTF8Offset,
99+
endOffset: range.endUTF8Offset),
100+
newText: text)
101+
})
102+
}
103+
}
104+
}

0 commit comments

Comments
 (0)