From 6dc8d43b38c6bc581bfa6003970e086197c99b51 Mon Sep 17 00:00:00 2001 From: Rintaro Ishizaki Date: Mon, 22 Apr 2024 13:50:35 -0700 Subject: [PATCH 1/4] Merge 'AtomicBool' into _SwiftSyntaxCShims Headers are now in 'swiftsyntax' subdirectory to avoid file name conflicts with libc. e.g. '#include <_stdio.h>' referenced from C++ compiler code which might pick up '_SwiftSyntaxCShims/include/_stdio.h' --- Package.swift | 6 +--- .../SwiftCompilerPlugin/CompilerPlugin.swift | 26 +++++++-------- Sources/SwiftSyntax/SyntaxArena.swift | 2 +- .../_AtomicBool/src/SwiftSyntaxAtomicBool.c | 27 --------------- .../include/module.modulemap | 4 ++- .../include/swiftsyntax/AtomicBool.h} | 21 +++++++++--- .../include/{_stdio.h => swiftsyntax/errno.h} | 23 ++++--------- .../include/swiftsyntax/stdio.h | 33 +++++++++++++++++++ 8 files changed, 73 insertions(+), 69 deletions(-) delete mode 100644 Sources/_AtomicBool/src/SwiftSyntaxAtomicBool.c rename Sources/{_AtomicBool/include/SwiftSyntaxAtomicBool.h => _SwiftSyntaxCShims/include/swiftsyntax/AtomicBool.h} (59%) rename Sources/_SwiftSyntaxCShims/include/{_stdio.h => swiftsyntax/errno.h} (68%) create mode 100644 Sources/_SwiftSyntaxCShims/include/swiftsyntax/stdio.h diff --git a/Package.swift b/Package.swift index 122590d6982..a1064f39077 100644 --- a/Package.swift +++ b/Package.swift @@ -34,10 +34,6 @@ let package = Package( name: "_SwiftSyntaxCShims" ), - .target( - name: "_AtomicBool" - ), - .target( name: "_InstructionCounter" ), @@ -131,7 +127,7 @@ let package = Package( .target( name: "SwiftSyntax", - dependencies: ["_AtomicBool", "SwiftSyntax509", "SwiftSyntax510", "SwiftSyntax600"], + dependencies: ["_SwiftSyntaxCShims", "SwiftSyntax509", "SwiftSyntax510", "SwiftSyntax600"], exclude: ["CMakeLists.txt"], swiftSettings: swiftSyntaxSwiftSettings ), diff --git a/Sources/SwiftCompilerPlugin/CompilerPlugin.swift b/Sources/SwiftCompilerPlugin/CompilerPlugin.swift index bf3c5b11367..1fb68e260ea 100644 --- a/Sources/SwiftCompilerPlugin/CompilerPlugin.swift +++ b/Sources/SwiftCompilerPlugin/CompilerPlugin.swift @@ -136,35 +136,31 @@ 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 { - let stdin = _ss_stdin() - let stdout = _ss_stdout() - let stderr = _ss_stderr() - // Duplicate the `stdin` file descriptor, which we will then use for // receiving messages from the plugin host. - let inputFD = dup(fileno(stdin)) + let inputFD = dup(fileno(_stdin)) guard inputFD >= 0 else { - internalError("Could not duplicate `stdin`: \(describe(errno: _ss_errno())).") + 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: _ss_errno())).") + 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)) + let outputFD = dup(fileno(_stdout)) guard outputFD >= 0 else { - internalError("Could not dup `stdout`: \(describe(errno: _ss_errno())).") + 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: _ss_errno())).") + guard dup2(fileno(_stderr), fileno(_stdout)) >= 0 else { + internalError("Could not dup2 `stdout` to `stderr`: \(describe(errno: _errno)).") } #if canImport(ucrt) @@ -194,7 +190,7 @@ extension CompilerPlugin { // Private function to report internal errors and then exit. fileprivate static func internalError(_ message: String) -> Never { - fputs("Internal Error: \(message)\n", _ss_stderr()) + fputs("Internal Error: \(message)\n", _stderr) exit(1) } } @@ -245,7 +241,7 @@ private func _write(_ fd: CInt, contentsOf buffer: UnsafeRawBufferPointer) throw let endPtr = ptr.advanced(by: buffer.count) while ptr != endPtr { switch write(fd, ptr, numericCast(endPtr - ptr)) { - case -1: throw IOError.writeFailed(errno: _ss_errno()) + case -1: throw IOError.writeFailed(errno: _errno) case 0: throw IOError.writeFailed(errno: 0) /* unreachable */ case let n: ptr += Int(n) } @@ -260,7 +256,7 @@ private func _read(_ fd: CInt, into buffer: UnsafeMutableRawBufferPointer) throw let endPtr = ptr.advanced(by: buffer.count) while ptr != endPtr { switch read(fd, ptr, numericCast(endPtr - ptr)) { - case -1: throw IOError.readFailed(errno: _ss_errno()) + case -1: throw IOError.readFailed(errno: _errno) case 0: throw IOError.readReachedEndOfInput case let n: ptr += Int(n) } diff --git a/Sources/SwiftSyntax/SyntaxArena.swift b/Sources/SwiftSyntax/SyntaxArena.swift index af924ddea07..d9f57930b24 100644 --- a/Sources/SwiftSyntax/SyntaxArena.swift +++ b/Sources/SwiftSyntax/SyntaxArena.swift @@ -24,7 +24,7 @@ fileprivate struct AtomicBool { } } #else -import _AtomicBool +import _SwiftSyntaxCShims #endif /// A syntax arena owns the memory for all syntax nodes within it. diff --git a/Sources/_AtomicBool/src/SwiftSyntaxAtomicBool.c b/Sources/_AtomicBool/src/SwiftSyntaxAtomicBool.c deleted file mode 100644 index 7d1e0d93667..00000000000 --- a/Sources/_AtomicBool/src/SwiftSyntaxAtomicBool.c +++ /dev/null @@ -1,27 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2023 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 -// -//===----------------------------------------------------------------------===// - -#include "SwiftSyntaxAtomicBool.h" - -AtomicBool atomic_bool_create(bool initialValue) { - AtomicBool atomic; - atomic.value = initialValue; - return atomic; -} - -bool atomic_bool_get(AtomicBool *atomic) { - return atomic->value; -} - -void atomic_bool_set(AtomicBool *atomic, bool newValue) { - atomic->value = newValue; -} diff --git a/Sources/_SwiftSyntaxCShims/include/module.modulemap b/Sources/_SwiftSyntaxCShims/include/module.modulemap index db118d217dd..409e034e8ad 100644 --- a/Sources/_SwiftSyntaxCShims/include/module.modulemap +++ b/Sources/_SwiftSyntaxCShims/include/module.modulemap @@ -1,4 +1,6 @@ module _SwiftSyntaxCShims { - header "_stdio.h" + header "swiftsyntax/AtomicBool.h" + header "swiftsyntax/errno.h" + header "swiftsyntax/stdio.h" export * } diff --git a/Sources/_AtomicBool/include/SwiftSyntaxAtomicBool.h b/Sources/_SwiftSyntaxCShims/include/swiftsyntax/AtomicBool.h similarity index 59% rename from Sources/_AtomicBool/include/SwiftSyntaxAtomicBool.h rename to Sources/_SwiftSyntaxCShims/include/swiftsyntax/AtomicBool.h index 28d45c06d96..68990e87aec 100644 --- a/Sources/_AtomicBool/include/SwiftSyntaxAtomicBool.h +++ b/Sources/_SwiftSyntaxCShims/include/swiftsyntax/AtomicBool.h @@ -2,7 +2,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors +// Copyright (c) 2014 - 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 @@ -10,6 +10,9 @@ // //===----------------------------------------------------------------------===// +#ifndef SWIFTSYNTAX_ATOMICBOOL_H +#define SWIFTSYNTAX_ATOMICBOOL_H + #include typedef struct { @@ -17,10 +20,20 @@ typedef struct { } AtomicBool; __attribute__((swift_name("AtomicBool.init(initialValue:)"))) -AtomicBool atomic_bool_create(bool initialValue); +static inline AtomicBool atomic_bool_create(bool initialValue) { + AtomicBool atomic; + atomic.value = initialValue; + return atomic; +} __attribute__((swift_name("getter:AtomicBool.value(self:)"))) -bool atomic_bool_get(AtomicBool *atomic); +static inline bool atomic_bool_get(AtomicBool *atomic) { + return atomic->value; +} __attribute__((swift_name("setter:AtomicBool.value(self:_:)"))) -void atomic_bool_set(AtomicBool *atomic, bool newValue); +static inline void atomic_bool_set(AtomicBool *atomic, bool newValue) { + atomic->value = newValue; +} + +#endif // SWIFTSYNTAX_ATOMICBOOL_H diff --git a/Sources/_SwiftSyntaxCShims/include/_stdio.h b/Sources/_SwiftSyntaxCShims/include/swiftsyntax/errno.h similarity index 68% rename from Sources/_SwiftSyntaxCShims/include/_stdio.h rename to Sources/_SwiftSyntaxCShims/include/swiftsyntax/errno.h index 17c5e5b34dc..589fde7eaed 100644 --- a/Sources/_SwiftSyntaxCShims/include/_stdio.h +++ b/Sources/_SwiftSyntaxCShims/include/swiftsyntax/errno.h @@ -10,23 +10,14 @@ // //===----------------------------------------------------------------------===// -#include -#include - -typedef FILE *_ss_ptr_FILE; +#ifndef SWIFTSYNTAX_ERRNO_H +#define SWIFTSYNTAX_ERRNO_H -static _ss_ptr_FILE _ss_stdout(void) { - return stdout; -} - -static _ss_ptr_FILE _ss_stdin(void) { - return stdin; -} - -static _ss_ptr_FILE _ss_stderr(void) { - return stderr; -} +#include -static int _ss_errno(void) { +__attribute__((swift_name("getter:_errno()"))) +static inline int swiftsyntax_errno(void) { return errno; } + +#endif // SWIFTSYNTAX_ERRNO_H diff --git a/Sources/_SwiftSyntaxCShims/include/swiftsyntax/stdio.h b/Sources/_SwiftSyntaxCShims/include/swiftsyntax/stdio.h new file mode 100644 index 00000000000..a7f24c3dfee --- /dev/null +++ b/Sources/_SwiftSyntaxCShims/include/swiftsyntax/stdio.h @@ -0,0 +1,33 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +#ifndef SWIFTSYNTAX_STDIO_H +#define SWIFTSYNTAX_STDIO_H + +#include + +__attribute__((swift_name("getter:_stdout()"))) +static inline FILE *swiftsyntax_stdout(void) { + return stdout; +} + +__attribute__((swift_name("getter:_stdin()"))) +static inline FILE *swiftsyntax_stdin(void) { + return stdin; +} + +__attribute__((swift_name("getter:_stderr()"))) +static inline FILE *swiftsyntax_stderr(void) { + return stderr; +} + +#endif // SWIFTSYNTAX_STDIO_H From 3a5ff0e40eb5a99f8d9e5dc2ad99e1414d49cfa5 Mon Sep 17 00:00:00 2001 From: Rintaro Ishizaki Date: Mon, 22 Apr 2024 15:02:24 -0700 Subject: [PATCH 2/4] Move PluginMessageConnection to PluginMessageHandling So that other modules (e.g. swift-plugin-server can use it) Renamed to 'StandardIOMessageConnection' --- Package.swift | 3 +- Sources/SwiftCompilerPlugin/CMakeLists.txt | 1 - .../SwiftCompilerPlugin/CompilerPlugin.swift | 169 +---------------- .../CMakeLists.txt | 2 + .../StandardIOMessageConnection.swift | 177 ++++++++++++++++++ 5 files changed, 185 insertions(+), 167 deletions(-) create mode 100644 Sources/SwiftCompilerPluginMessageHandling/StandardIOMessageConnection.swift diff --git a/Package.swift b/Package.swift index a1064f39077..abed0f30023 100644 --- a/Package.swift +++ b/Package.swift @@ -73,7 +73,7 @@ let package = Package( .target( name: "SwiftCompilerPlugin", - dependencies: ["SwiftCompilerPluginMessageHandling", "SwiftSyntaxMacros", "_SwiftSyntaxCShims"], + dependencies: ["SwiftCompilerPluginMessageHandling", "SwiftSyntaxMacros"], exclude: ["CMakeLists.txt"] ), @@ -87,6 +87,7 @@ let package = Package( .target( name: "SwiftCompilerPluginMessageHandling", dependencies: [ + "_SwiftSyntaxCShims", "SwiftDiagnostics", "SwiftOperators", "SwiftParser", diff --git a/Sources/SwiftCompilerPlugin/CMakeLists.txt b/Sources/SwiftCompilerPlugin/CMakeLists.txt index 4552f3ef02d..20c7d9a7872 100644 --- a/Sources/SwiftCompilerPlugin/CMakeLists.txt +++ b/Sources/SwiftCompilerPlugin/CMakeLists.txt @@ -14,5 +14,4 @@ add_swift_syntax_library(SwiftCompilerPlugin target_link_swift_syntax_libraries(SwiftCompilerPlugin PUBLIC SwiftSyntaxMacros SwiftCompilerPluginMessageHandling - _SwiftSyntaxCShims ) diff --git a/Sources/SwiftCompilerPlugin/CompilerPlugin.swift b/Sources/SwiftCompilerPlugin/CompilerPlugin.swift index 1fb68e260ea..431b8ad2f3b 100644 --- a/Sources/SwiftCompilerPlugin/CompilerPlugin.swift +++ b/Sources/SwiftCompilerPlugin/CompilerPlugin.swift @@ -9,31 +9,13 @@ // 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 #if swift(>=6.0) -private import _SwiftSyntaxCShims public import SwiftSyntaxMacros @_spi(PluginMessage) private import SwiftCompilerPluginMessageHandling -#if canImport(Darwin) -private import Darwin -#elseif canImport(Glibc) -private import Glibc -#elseif canImport(ucrt) -private import ucrt -#endif #else -import _SwiftSyntaxCShims import SwiftSyntaxMacros @_spi(PluginMessage) import SwiftCompilerPluginMessageHandling -#if canImport(Darwin) -import Darwin -#elseif canImport(Glibc) -import Glibc -#elseif canImport(ucrt) -import ucrt -#endif #endif // @@ -122,61 +104,12 @@ struct MacroProviderAdapter: PluginProvider { } } -#if canImport(ucrt) -private let dup = _dup(_:) -private let fileno = _fileno(_:) -private let dup2 = _dup2(_:_:) -private let close = _close(_:) -private let read = _read(_:_:_:) -private let write = _write(_:_:_:) -#endif - extension CompilerPlugin { - /// Main entry point of the plugin — sets up a communication channel with - /// the plugin host and runs the main message loop. + /// Main entry point of the plugin — sets up a standard I/O 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)).") - } - - #if canImport(ucrt) - // Set I/O to binary mode. Avoid CRLF translation, and Ctrl+Z (0x1A) as EOF. - _ = _setmode(inputFD, _O_BINARY) - _ = _setmode(outputFD, _O_BINARY) - #endif - - // Open a message channel for communicating with the plugin host. - let connection = PluginHostConnection( - inputStream: inputFD, - outputStream: outputFD - ) - - // Handle messages from the host until the input stream is closed, - // indicating that we're done. + let connection = try StandardIOMessageConnection() let provider = MacroProviderAdapter(plugin: Self()) let impl = CompilerPluginMessageListener(connection: connection, provider: provider) do { @@ -184,101 +117,7 @@ extension CompilerPlugin { } 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) - } -} - -internal struct PluginHostConnection: MessageConnection { - // File descriptor for input from the host. - fileprivate let inputStream: CInt - // File descriptor for output to the host. - fileprivate let outputStream: CInt - - func sendMessage(_ message: TX) throws { - // Encode the message as JSON. - let payload = try JSON.encode(message) - - // Write the header (a 64-bit length field in little endian byte order). - let count = payload.count - var header = UInt64(count).littleEndian - try withUnsafeBytes(of: &header) { try _write(outputStream, contentsOf: $0) } - - // Write the JSON payload. - try payload.withUnsafeBytes { try _write(outputStream, contentsOf: $0) } - } - - func waitForNextMessage(_ ty: RX.Type) throws -> RX? { - // Read the header (a 64-bit length field in little endian byte order). - var header: UInt64 = 0 - do { - try withUnsafeMutableBytes(of: &header) { try _read(inputStream, into: $0) } - } catch IOError.readReachedEndOfInput { - // Connection closed. - return nil - } - - // Read the JSON payload. - let count = Int(UInt64(littleEndian: header)) - let data = UnsafeMutableRawBufferPointer.allocate(byteCount: count, alignment: 1) - defer { data.deallocate() } - try _read(inputStream, into: data) - - // Decode and return the message. - return try JSON.decode(ty, from: UnsafeBufferPointer(data.bindMemory(to: UInt8.self))) - } -} - -/// Write the buffer to the file descriptor. Throws an error on failure. -private func _write(_ fd: CInt, contentsOf buffer: UnsafeRawBufferPointer) throws { - guard var ptr = buffer.baseAddress else { return } - let endPtr = ptr.advanced(by: buffer.count) - while ptr != endPtr { - switch write(fd, ptr, numericCast(endPtr - ptr)) { - case -1: throw IOError.writeFailed(errno: _errno) - case 0: throw IOError.writeFailed(errno: 0) /* unreachable */ - case let n: ptr += Int(n) + fatalError("Internal Error: \(error)") } } } - -/// Fill the buffer from the file descriptor. Throws an error on failure. -/// If the file descriptor reached the end-of-file before filling up the entire -/// buffer, throws IOError.readReachedEndOfInput -private func _read(_ fd: CInt, into buffer: UnsafeMutableRawBufferPointer) throws { - guard var ptr = buffer.baseAddress else { return } - let endPtr = ptr.advanced(by: buffer.count) - while ptr != endPtr { - switch read(fd, ptr, numericCast(endPtr - ptr)) { - case -1: throw IOError.readFailed(errno: _errno) - case 0: throw IOError.readReachedEndOfInput - case let n: ptr += Int(n) - } - } -} - -private enum IOError: Error, CustomStringConvertible { - case readReachedEndOfInput - case readFailed(errno: CInt) - case writeFailed(errno: CInt) - - var description: String { - switch self { - case .readReachedEndOfInput: "read(2) reached end-of-file" - case .readFailed(let errno): "read(2) failed: \(describe(errno: errno))" - case .writeFailed(let errno): "write(2) failed: \(describe(errno: errno))" - } - } -} - -// Private function to construct an error message from an `errno` code. -private func describe(errno: CInt) -> String { - if let cStr = strerror(errno) { return String(cString: cStr) } - return String(describing: errno) -} diff --git a/Sources/SwiftCompilerPluginMessageHandling/CMakeLists.txt b/Sources/SwiftCompilerPluginMessageHandling/CMakeLists.txt index 5252fb645fe..0c6ecea6f7a 100644 --- a/Sources/SwiftCompilerPluginMessageHandling/CMakeLists.txt +++ b/Sources/SwiftCompilerPluginMessageHandling/CMakeLists.txt @@ -17,9 +17,11 @@ add_swift_syntax_library(SwiftCompilerPluginMessageHandling JSON/JSON.swift JSON/JSONDecoding.swift JSON/JSONEncoding.swift + StandardIOMessageConnection.swift ) target_link_swift_syntax_libraries(SwiftCompilerPluginMessageHandling PUBLIC + _SwiftSyntaxCShims SwiftSyntax SwiftBasicFormat SwiftDiagnostics diff --git a/Sources/SwiftCompilerPluginMessageHandling/StandardIOMessageConnection.swift b/Sources/SwiftCompilerPluginMessageHandling/StandardIOMessageConnection.swift new file mode 100644 index 00000000000..2318c451810 --- /dev/null +++ b/Sources/SwiftCompilerPluginMessageHandling/StandardIOMessageConnection.swift @@ -0,0 +1,177 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2024 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 +// +//===----------------------------------------------------------------------===// + +#if swift(>=6.0) +private import _SwiftSyntaxCShims +#if canImport(Darwin) +private import Darwin +#elseif canImport(Glibc) +private import Glibc +#elseif canImport(ucrt) +private import ucrt +#endif +#else +import _SwiftSyntaxCShims +#if canImport(Darwin) +import Darwin +#elseif canImport(Glibc) +import Glibc +#elseif canImport(ucrt) +import ucrt +#endif +#endif + +#if canImport(ucrt) +private let dup = _dup(_:) +private let fileno = _fileno(_:) +private let dup2 = _dup2(_:_:) +private let close = _close(_:) +private let read = _read(_:_:_:) +private let write = _write(_:_:_:) +#endif + +/// Concrete 'MessageConnection' type using Standard I/O. +/// +/// When creating, `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 attempts 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. +/// +/// Each message is serialized to UTF-8 encoded JSON text, prefixed with a +/// 8 byte header which is the byte size of the JSON payload serialized to a +/// little-endian 64bit unsigned integer. +@_spi(PluginMessage) +public struct StandardIOMessageConnection: MessageConnection { + private var inputFileDescriptor: CInt + private var outputFileDescriptor: CInt + + public init() 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 { + throw IOError.systemError(function: "dup(fileno(stdin))", 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 { + throw IOError.systemError(function: "close(fileno(stdin))", 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 { + throw IOError.systemError(function: "dup(fileno(stdout))", 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 { + throw IOError.systemError(function: "dup2(fileno(stderr), fileno(stdout))", errno: _errno) + } + + #if canImport(ucrt) + // Set I/O to binary mode. Avoid CRLF translation, and Ctrl+Z (0x1A) as EOF. + _ = _setmode(inputFD, _O_BINARY) + _ = _setmode(outputFD, _O_BINARY) + #endif + + self.inputFileDescriptor = inputFD + self.outputFileDescriptor = outputFD + } + + /// Write the buffer to the file descriptor. Throws an error on failure. + private func _write(contentsOf buffer: UnsafeRawBufferPointer) throws { + guard var ptr = buffer.baseAddress else { return } + let endPtr = ptr.advanced(by: buffer.count) + while ptr != endPtr { + switch write(outputFileDescriptor, ptr, numericCast(endPtr - ptr)) { + case -1: throw IOError.systemError(function: "write(_:_:_:)", errno: _errno) + case 0: throw IOError.systemError(function: "write", errno: 0) /* unreachable */ + case let n: ptr += Int(n) + } + } + } + + /// Fill the buffer from the file descriptor. Throws an error on failure. + /// If the file descriptor reached the end-of-file before filling up the entire + /// buffer, throws IOError.readReachedEndOfInput + private func _read(into buffer: UnsafeMutableRawBufferPointer) throws { + guard var ptr = buffer.baseAddress else { return } + let endPtr = ptr.advanced(by: buffer.count) + while ptr != endPtr { + switch read(inputFileDescriptor, ptr, numericCast(endPtr - ptr)) { + case -1: throw IOError.systemError(function: "read(_:_:_:)", errno: _errno) + case 0: throw IOError.readReachedEndOfInput + case let n: ptr += Int(n) + } + } + } + + public func sendMessage(_ message: TX) throws { + // Encode the message as JSON. + let payload = try JSON.encode(message) + + // Write the header (a 64-bit length field in little endian byte order). + let count = payload.count + var header = UInt64(count).littleEndian + try withUnsafeBytes(of: &header) { try _write(contentsOf: $0) } + + // Write the JSON payload. + try payload.withUnsafeBytes { try _write(contentsOf: $0) } + } + + public func waitForNextMessage(_ type: RX.Type) throws -> RX? { + // Read the header (a 64-bit length field in little endian byte order). + var header: UInt64 = 0 + do { + try withUnsafeMutableBytes(of: &header) { try _read(into: $0) } + } catch IOError.readReachedEndOfInput { + // Connection closed. + return nil + } + + // Read the JSON payload. + let count = Int(UInt64(littleEndian: header)) + let data = UnsafeMutableRawBufferPointer.allocate(byteCount: count, alignment: 1) + defer { data.deallocate() } + try _read(into: data) + + // Decode and return the message. + return try JSON.decode(type, from: UnsafeBufferPointer(data.bindMemory(to: UInt8.self))) + } +} + +private enum IOError: Error, CustomStringConvertible { + case readReachedEndOfInput + case systemError(function: String, errno: CInt) + + var description: String { + switch self { + case .readReachedEndOfInput: + return "read(2) reached end-of-file" + case .systemError(let function, let errno): + return "\(function) failed: \(describe(errno: errno))" + } + } +} + +// Private function to construct an error message from an `errno` code. +private func describe(errno: CInt) -> String { + if let cStr = strerror(errno) { return String(cString: cStr) } + return String(describing: errno) +} From e53be25ecb662bc39962aeafcf19948f08c940c7 Mon Sep 17 00:00:00 2001 From: Rintaro Ishizaki Date: Thu, 25 Apr 2024 11:50:51 -0700 Subject: [PATCH 3/4] StandardIOMessageConnection to have memberwise initializer --- .../StandardIOMessageConnection.swift | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/Sources/SwiftCompilerPluginMessageHandling/StandardIOMessageConnection.swift b/Sources/SwiftCompilerPluginMessageHandling/StandardIOMessageConnection.swift index 2318c451810..5e1bf2bc3d6 100644 --- a/Sources/SwiftCompilerPluginMessageHandling/StandardIOMessageConnection.swift +++ b/Sources/SwiftCompilerPluginMessageHandling/StandardIOMessageConnection.swift @@ -41,21 +41,26 @@ private let write = _write(_:_:_:) /// Concrete 'MessageConnection' type using Standard I/O. /// -/// When creating, `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 attempts 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. -/// /// Each message is serialized to UTF-8 encoded JSON text, prefixed with a /// 8 byte header which is the byte size of the JSON payload serialized to a /// little-endian 64bit unsigned integer. @_spi(PluginMessage) public struct StandardIOMessageConnection: MessageConnection { - private var inputFileDescriptor: CInt - private var outputFileDescriptor: CInt + private let inputFileDescriptor: CInt + private let outputFileDescriptor: CInt + + public init(inputFileDescriptor: CInt, outputFileDescriptor: CInt) { + self.inputFileDescriptor = inputFileDescriptor + self.outputFileDescriptor = outputFileDescriptor + } + /// Convenience initializer for normal executable plugins. Upon creation: + /// - Redirect `stdout` to `stderr` so that print statements from the plugin + /// are treated as plain-text output + /// - Close `stdin` so that any attempts by the plugin logic to read from + /// console result in errors instead of blocking the process + /// - Duplicate the original `stdin` and `stdout` for use as messaging + /// pipes, and are not directly used by the plugin logic public init() throws { // Duplicate the `stdin` file descriptor, which we will then use for // receiving messages from the plugin host. @@ -90,8 +95,7 @@ public struct StandardIOMessageConnection: MessageConnection { _ = _setmode(outputFD, _O_BINARY) #endif - self.inputFileDescriptor = inputFD - self.outputFileDescriptor = outputFD + self.init(inputFileDescriptor: inputFD, outputFileDescriptor: outputFD) } /// Write the buffer to the file descriptor. Throws an error on failure. From f0b5d1642f0ff8adf647bcb134193729df0d8799 Mon Sep 17 00:00:00 2001 From: Rintaro Ishizaki Date: Thu, 25 Apr 2024 11:10:19 -0700 Subject: [PATCH 4/4] Instead of importing platform SDK modules, just use C shims --- .../CMakeLists.txt | 8 +++-- .../JSON/JSONDecoding.swift | 16 ++------- .../StandardIOMessageConnection.swift | 16 +-------- .../include/module.modulemap | 1 + .../include/swiftsyntax/_includes.h | 35 +++++++++++++++++++ 5 files changed, 45 insertions(+), 31 deletions(-) create mode 100644 Sources/_SwiftSyntaxCShims/include/swiftsyntax/_includes.h diff --git a/Sources/SwiftCompilerPluginMessageHandling/CMakeLists.txt b/Sources/SwiftCompilerPluginMessageHandling/CMakeLists.txt index 0c6ecea6f7a..c6093117fc2 100644 --- a/Sources/SwiftCompilerPluginMessageHandling/CMakeLists.txt +++ b/Sources/SwiftCompilerPluginMessageHandling/CMakeLists.txt @@ -21,11 +21,15 @@ add_swift_syntax_library(SwiftCompilerPluginMessageHandling ) target_link_swift_syntax_libraries(SwiftCompilerPluginMessageHandling PUBLIC - _SwiftSyntaxCShims SwiftSyntax SwiftBasicFormat SwiftDiagnostics SwiftParser SwiftSyntaxMacros SwiftSyntaxMacroExpansion - SwiftOperators) + SwiftOperators +) + +target_link_swift_syntax_libraries(SwiftCompilerPluginMessageHandling PRIVATE + _SwiftSyntaxCShims +) diff --git a/Sources/SwiftCompilerPluginMessageHandling/JSON/JSONDecoding.swift b/Sources/SwiftCompilerPluginMessageHandling/JSON/JSONDecoding.swift index 041348d658f..c820da8fda3 100644 --- a/Sources/SwiftCompilerPluginMessageHandling/JSON/JSONDecoding.swift +++ b/Sources/SwiftCompilerPluginMessageHandling/JSON/JSONDecoding.swift @@ -11,21 +11,9 @@ //===----------------------------------------------------------------------===// #if swift(>=6.0) -#if canImport(Darwin) -private import Darwin -#elseif canImport(Glibc) -private import Glibc -#elseif canImport(ucrt) -private import ucrt -#endif +private import _SwiftSyntaxCShims #else -#if canImport(Darwin) -import Darwin -#elseif canImport(Glibc) -import Glibc -#elseif canImport(ucrt) -import ucrt -#endif +@_implementationOnly import _SwiftSyntaxCShims #endif func decodeFromJSON(json: UnsafeBufferPointer) throws -> T { diff --git a/Sources/SwiftCompilerPluginMessageHandling/StandardIOMessageConnection.swift b/Sources/SwiftCompilerPluginMessageHandling/StandardIOMessageConnection.swift index 5e1bf2bc3d6..df5a10ecdd7 100644 --- a/Sources/SwiftCompilerPluginMessageHandling/StandardIOMessageConnection.swift +++ b/Sources/SwiftCompilerPluginMessageHandling/StandardIOMessageConnection.swift @@ -12,22 +12,8 @@ #if swift(>=6.0) private import _SwiftSyntaxCShims -#if canImport(Darwin) -private import Darwin -#elseif canImport(Glibc) -private import Glibc -#elseif canImport(ucrt) -private import ucrt -#endif #else -import _SwiftSyntaxCShims -#if canImport(Darwin) -import Darwin -#elseif canImport(Glibc) -import Glibc -#elseif canImport(ucrt) -import ucrt -#endif +@_implementationOnly import _SwiftSyntaxCShims #endif #if canImport(ucrt) diff --git a/Sources/_SwiftSyntaxCShims/include/module.modulemap b/Sources/_SwiftSyntaxCShims/include/module.modulemap index 409e034e8ad..7c669d9a817 100644 --- a/Sources/_SwiftSyntaxCShims/include/module.modulemap +++ b/Sources/_SwiftSyntaxCShims/include/module.modulemap @@ -1,4 +1,5 @@ module _SwiftSyntaxCShims { + header "swiftsyntax/_includes.h" header "swiftsyntax/AtomicBool.h" header "swiftsyntax/errno.h" header "swiftsyntax/stdio.h" diff --git a/Sources/_SwiftSyntaxCShims/include/swiftsyntax/_includes.h b/Sources/_SwiftSyntaxCShims/include/swiftsyntax/_includes.h new file mode 100644 index 00000000000..f094155d15d --- /dev/null +++ b/Sources/_SwiftSyntaxCShims/include/swiftsyntax/_includes.h @@ -0,0 +1,35 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// +// +// This file is for, instead of importing platform modules in Swift file, like: +// +// #if canImport(Darwin) +// import Darwin +// #elseif canImport(Glibc) +// import Glibc +// ... +// +// Just include them here using C facilities, so that Swift module can just: +// +// import _SwiftSyntaxCShims +// +//===----------------------------------------------------------------------===// + +#if defined(_WIN32) +#include +#elif defined(__unix__) || defined(__APPLE__) +#include +#endif + +#include +#include +#include