-
Notifications
You must be signed in to change notification settings - Fork 440
[Macros] Add executable compiler plugin support library #1359
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
95cba83
[Macros] Add executable compiler plugin support library
rintaro e0898a0
[Macro] Add plugin test
rintaro 7970039
[Plugin] Add unit tests for plugin support library
rintaro e6d7a7c
Fix path resoltion in lit testing
rintaro 4911a66
[CompilerPlugin] Update for conformance macro
rintaro 306eb51
[Plugin] Stop using 'async' for main/message handling methods
rintaro 4f4b873
Update for review
rintaro File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
File renamed without changes.
File renamed without changes.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
import SwiftCompilerPlugin | ||
import SwiftSyntaxMacros | ||
|
||
@main | ||
struct ThePlugin: CompilerPlugin { | ||
var providingMacros: [Macro.Type] = [ | ||
EchoExpressionMacro.self, | ||
MetadataMacro.self, | ||
] | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
import SwiftSyntax | ||
import SwiftSyntaxBuilder | ||
import SwiftSyntaxMacros | ||
|
||
/// Returns the first argument prepending a comment '/* echo */'. | ||
struct EchoExpressionMacro: ExpressionMacro { | ||
static func expansion< | ||
Node: FreestandingMacroExpansionSyntax, | ||
Context: MacroExpansionContext | ||
>( | ||
of node: Node, | ||
in context: Context | ||
) throws -> ExprSyntax { | ||
let expr: ExprSyntax = node.argumentList.first!.expression | ||
return expr.with(\.leadingTrivia, [.blockComment("/* echo */")]) | ||
} | ||
} | ||
|
||
/// Add a static property `__metadata__`. | ||
struct MetadataMacro: MemberMacro { | ||
static func expansion< | ||
Declaration: DeclGroupSyntax, | ||
Context: MacroExpansionContext | ||
>( | ||
of node: SwiftSyntax.AttributeSyntax, | ||
providingMembersOf declaration: Declaration, | ||
in context: Context | ||
) throws -> [DeclSyntax] { | ||
guard let cls = declaration.as(ClassDeclSyntax.self) else { | ||
return [] | ||
} | ||
let className = cls.identifier.trimmedDescription | ||
return [ | ||
""" | ||
static var __metadata__: [String: String] { ["name": "\(raw: className)"] } | ||
""" | ||
] | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,246 @@ | ||
//===----------------------------------------------------------------------===// | ||
// | ||
// This source file is part of the Swift open source project | ||
// | ||
// Copyright (c) 2023 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 | ||
// | ||
//===----------------------------------------------------------------------===// | ||
// NOTE: This basic plugin mechanism is mostly copied from | ||
// https://github.com/apple/swift-package-manager/blob/main/Sources/PackagePlugin/Plugin.swift | ||
|
||
import SwiftSyntaxMacros | ||
|
||
@_implementationOnly import Foundation | ||
#if os(Windows) | ||
@_implementationOnly import ucrt | ||
#endif | ||
|
||
// | ||
// This source file contains the main entry point for compiler plugins. | ||
// A plugin receives messages from the "plugin host" (typically | ||
// 'swift-frontend'), and sends back messages in return based on its actions. | ||
// | ||
// Depending on the platform, plugins are invoked in a sanbox that blocks | ||
// network access and prevents any file system changes. | ||
// | ||
// The host process and the plugin communicate using messages in the form of | ||
// length-prefixed JSON-encoded Swift enums. The host sends messages to the | ||
// plugin through its standard-input pipe, and receives messages through the | ||
// plugin's standard-output pipe. The plugin's standard-error is considered | ||
// to be free-form textual console output. | ||
// | ||
// Within the plugin process, `stdout` is redirected to `stderr` so that print | ||
// statements from the plugin are treated as plain-text output, and `stdin` is | ||
// closed so that any attemps by the plugin logic to read from console result | ||
// in errors instead of blocking the process. The original `stdin` and `stdout` | ||
// are duplicated for use as messaging pipes, and are not directly used by the | ||
// plugin logic. | ||
// | ||
// The exit code of the plugin process indicates whether the plugin invocation | ||
// is considered successful. A failure result should also be accompanied by an | ||
// emitted error diagnostic, so that errors are understandable by the user. | ||
// | ||
// Using standard input and output streams for messaging avoids having to make | ||
// allowances in the sandbox for other channels of communication, and seems a | ||
// more portable approach than many of the alternatives. This is all somewhat | ||
// temporary in any case — in the long term, something like distributed actors | ||
// or something similar can hopefully replace the custom messaging. | ||
// | ||
// Usage: | ||
// struct MyPlugin: CompilerPlugin { | ||
// var providingMacros: [Macros.Type] = [ | ||
// StringifyMacro.self | ||
// ] | ||
public protocol CompilerPlugin { | ||
init() | ||
|
||
var providingMacros: [Macro.Type] { get } | ||
} | ||
|
||
extension CompilerPlugin { | ||
|
||
/// Main entry point of the plugin — sets up a communication channel with | ||
/// the plugin host and runs the main message loop. | ||
public static func main() throws { | ||
// Duplicate the `stdin` file descriptor, which we will then use for | ||
// receiving messages from the plugin host. | ||
let inputFD = dup(fileno(stdin)) | ||
guard inputFD >= 0 else { | ||
internalError("Could not duplicate `stdin`: \(describe(errno: errno)).") | ||
} | ||
|
||
// Having duplicated the original standard-input descriptor, we close | ||
// `stdin` so that attempts by the plugin to read console input (which | ||
// are usually a mistake) return errors instead of blocking. | ||
guard close(fileno(stdin)) >= 0 else { | ||
internalError("Could not close `stdin`: \(describe(errno: errno)).") | ||
} | ||
|
||
// Duplicate the `stdout` file descriptor, which we will then use for | ||
// sending messages to the plugin host. | ||
let outputFD = dup(fileno(stdout)) | ||
guard outputFD >= 0 else { | ||
internalError("Could not dup `stdout`: \(describe(errno: errno)).") | ||
} | ||
|
||
// Having duplicated the original standard-output descriptor, redirect | ||
// `stdout` to `stderr` so that all free-form text output goes there. | ||
guard dup2(fileno(stderr), fileno(stdout)) >= 0 else { | ||
internalError("Could not dup2 `stdout` to `stderr`: \(describe(errno: errno)).") | ||
} | ||
|
||
// Turn off full buffering so printed text appears as soon as possible. | ||
// Windows is much less forgiving than other platforms. If line | ||
// buffering is enabled, we must provide a buffer and the size of the | ||
// buffer. As a result, on Windows, we completely disable all | ||
// buffering, which means that partial writes are possible. | ||
#if os(Windows) | ||
setvbuf(stdout, nil, _IONBF, 0) | ||
#else | ||
setvbuf(stdout, nil, _IOLBF, 0) | ||
#endif | ||
|
||
// Open a message channel for communicating with the plugin host. | ||
pluginHostConnection = PluginHostConnection( | ||
inputStream: FileHandle(fileDescriptor: inputFD), | ||
outputStream: FileHandle(fileDescriptor: outputFD) | ||
) | ||
|
||
// Handle messages from the host until the input stream is closed, | ||
// indicating that we're done. | ||
let instance = Self() | ||
do { | ||
while let message = try pluginHostConnection.waitForNextMessage() { | ||
try instance.handleMessage(message) | ||
} | ||
} catch { | ||
// Emit a diagnostic and indicate failure to the plugin host, | ||
// and exit with an error code. | ||
internalError(String(describing: error)) | ||
} | ||
} | ||
|
||
// Private function to report internal errors and then exit. | ||
fileprivate static func internalError(_ message: String) -> Never { | ||
fputs("Internal Error: \(message)\n", stderr) | ||
exit(1) | ||
} | ||
|
||
// Private function to construct an error message from an `errno` code. | ||
fileprivate static func describe(errno: Int32) -> String { | ||
if let cStr = strerror(errno) { return String(cString: cStr) } | ||
return String(describing: errno) | ||
} | ||
|
||
/// Handles a single message received from the plugin host. | ||
fileprivate func handleMessage(_ message: HostToPluginMessage) throws { | ||
switch message { | ||
case .getCapability: | ||
try pluginHostConnection.sendMessage( | ||
.getCapabilityResult(capability: PluginMessage.capability) | ||
) | ||
break | ||
|
||
case .expandFreestandingMacro(let macro, let discriminator, let expandingSyntax): | ||
try expandFreestandingMacro( | ||
macro: macro, | ||
discriminator: discriminator, | ||
expandingSyntax: expandingSyntax | ||
) | ||
|
||
case .expandAttachedMacro(let macro, let macroRole, let discriminator, let attributeSyntax, let declSyntax, let parentDeclSyntax): | ||
try expandAttachedMacro( | ||
macro: macro, | ||
macroRole: macroRole, | ||
discriminator: discriminator, | ||
attributeSyntax: attributeSyntax, | ||
declSyntax: declSyntax, | ||
parentDeclSyntax: parentDeclSyntax | ||
) | ||
} | ||
} | ||
} | ||
|
||
/// Message channel for bidirectional communication with the plugin host. | ||
internal fileprivate(set) var pluginHostConnection: PluginHostConnection! | ||
|
||
typealias PluginHostConnection = MessageConnection<PluginToHostMessage, HostToPluginMessage> | ||
|
||
internal struct MessageConnection<TX, RX> where TX: Encodable, RX: Decodable { | ||
let inputStream: FileHandle | ||
let outputStream: FileHandle | ||
|
||
func sendMessage(_ message: TX) throws { | ||
// Encode the message as JSON. | ||
let payload = try JSONEncoder().encode(message) | ||
|
||
// Write the header (a 64-bit length field in little endian byte order). | ||
var count = UInt64(payload.count).littleEndian | ||
let header = Swift.withUnsafeBytes(of: &count) { Data($0) } | ||
assert(header.count == 8) | ||
|
||
// Write the header and payload. | ||
try outputStream._write(contentsOf: header) | ||
try outputStream._write(contentsOf: payload) | ||
} | ||
|
||
func waitForNextMessage() throws -> RX? { | ||
// Read the header (a 64-bit length field in little endian byte order). | ||
guard | ||
let header = try inputStream._read(upToCount: 8), | ||
header.count != 0 | ||
else { | ||
return nil | ||
} | ||
guard header.count == 8 else { | ||
throw PluginMessageError.truncatedHeader | ||
} | ||
|
||
// Decode the count. | ||
let count = header.withUnsafeBytes { | ||
UInt64(littleEndian: $0.load(as: UInt64.self)) | ||
} | ||
guard count >= 2 else { | ||
throw PluginMessageError.invalidPayloadSize | ||
} | ||
|
||
// Read the JSON payload. | ||
guard | ||
let payload = try inputStream._read(upToCount: Int(count)), | ||
payload.count == count | ||
else { | ||
throw PluginMessageError.truncatedPayload | ||
} | ||
|
||
// Decode and return the message. | ||
return try JSONDecoder().decode(RX.self, from: payload) | ||
} | ||
|
||
enum PluginMessageError: Swift.Error { | ||
case truncatedHeader | ||
case invalidPayloadSize | ||
case truncatedPayload | ||
} | ||
} | ||
|
||
private extension FileHandle { | ||
func _write(contentsOf data: Data) throws { | ||
if #available(macOS 10.15.4, iOS 13.4, watchOS 6.2, tvOS 13.4, *) { | ||
return try self.write(contentsOf: data) | ||
} else { | ||
return self.write(data) | ||
} | ||
} | ||
|
||
func _read(upToCount count: Int) throws -> Data? { | ||
if #available(macOS 10.15.4, iOS 13.4, watchOS 6.2, tvOS 13.4, *) { | ||
return try self.read(upToCount: count) | ||
} else { | ||
return self.readData(ofLength: 8) | ||
} | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.