From c69bf50b375a99332ae4881c874e6d3f410aed36 Mon Sep 17 00:00:00 2001 From: Jake Petroules Date: Thu, 2 Oct 2025 11:04:19 -0700 Subject: [PATCH] Add overloads to cover dup3 and pipe2 POSIX APIs This follows the existing pattern of wrapping dup/dup2 and pipe with FileDescriptor.duplicate and FileDescriptor.pipe, with additional overloads for passing the O_CLOEXEC and similar flags. --- Sources/CSystem/CMakeLists.txt | 1 + Sources/CSystem/include/CSystemShared.h | 2 + Sources/CSystem/include/module.modulemap | 1 + Sources/CSystem/shims.c | 32 +++++- Sources/System/FileDescriptor.swift | 135 +++++++++++++++++++++++ Sources/System/FileOperations.swift | 89 ++++++++++++++- Sources/System/Internals/Constants.swift | 9 ++ Sources/System/Internals/Syscalls.swift | 17 ++- 8 files changed, 277 insertions(+), 9 deletions(-) create mode 100644 Sources/CSystem/include/CSystemShared.h diff --git a/Sources/CSystem/CMakeLists.txt b/Sources/CSystem/CMakeLists.txt index faf730e3..36656ae4 100644 --- a/Sources/CSystem/CMakeLists.txt +++ b/Sources/CSystem/CMakeLists.txt @@ -14,6 +14,7 @@ target_include_directories(CSystem INTERFACE install(FILES include/CSystemLinux.h + include/CSystemShared.h include/CSystemWindows.h include/module.modulemap DESTINATION include/CSystem) diff --git a/Sources/CSystem/include/CSystemShared.h b/Sources/CSystem/include/CSystemShared.h new file mode 100644 index 00000000..edcb5cca --- /dev/null +++ b/Sources/CSystem/include/CSystemShared.h @@ -0,0 +1,2 @@ +extern int csystem_posix_pipe2(int fildes[2], int flag); +extern int csystem_posix_dup3(int fildes, int fildes2, int flag); diff --git a/Sources/CSystem/include/module.modulemap b/Sources/CSystem/include/module.modulemap index 6e8b89e9..73b10040 100644 --- a/Sources/CSystem/include/module.modulemap +++ b/Sources/CSystem/include/module.modulemap @@ -1,4 +1,5 @@ module CSystem { + header "CSystemShared.h" header "CSystemLinux.h" header "CSystemWASI.h" header "CSystemWindows.h" diff --git a/Sources/CSystem/shims.c b/Sources/CSystem/shims.c index f492a2ae..69e994e2 100644 --- a/Sources/CSystem/shims.c +++ b/Sources/CSystem/shims.c @@ -7,12 +7,40 @@ See https://swift.org/LICENSE.txt for license information */ -#ifdef __linux__ +#if defined(__FreeBSD__) || defined(__OpenBSD__) +#define __BSD_VISIBLE +#include +#endif +#ifdef __linux__ +#define _GNU_SOURCE #include - #endif #if defined(_WIN32) #include #endif + +#include + +#if !defined(_WIN32) && !defined(__wasi__) && !defined(__APPLE__) +#define HAVE_PIPE2_DUP3 +#endif + +// Wrappers are required because _GNU_SOURCE causes a conflict with other imports when defined in CSystemLinux.h +extern int csystem_posix_pipe2(int fildes[2], int flag) { + #ifdef HAVE_PIPE2_DUP3 + return pipe2(fildes, flag); + #else + errno = ENOSYS; + return -1; + #endif +} +extern int csystem_posix_dup3(int fildes, int fildes2, int flag) { + #ifdef HAVE_PIPE2_DUP3 + return dup3(fildes, fildes2, flag); + #else + errno = ENOSYS; + return -1; + #endif +} diff --git a/Sources/System/FileDescriptor.swift b/Sources/System/FileDescriptor.swift index d5f5883b..86d8fc60 100644 --- a/Sources/System/FileDescriptor.swift +++ b/Sources/System/FileDescriptor.swift @@ -324,6 +324,141 @@ extension FileDescriptor { #endif } + /// Options that specify behavior for a newly-created pipe. + @frozen + @available(Windows, unavailable) + @available(macOS, unavailable) + @available(iOS, unavailable) + @available(tvOS, unavailable) + @available(watchOS, unavailable) + @available(visionOS, unavailable) + public struct PipeOptions: OptionSet, Sendable, Hashable, Codable { + /// The raw C options. + @_alwaysEmitIntoClient + public var rawValue: CInt + + /// Create a strongly-typed options value from raw C options. + @_alwaysEmitIntoClient + public init(rawValue: CInt) { self.rawValue = rawValue } + +#if !os(Windows) + /// Indicates that all + /// subsequent input and output operations on the pipe's file descriptors will be nonblocking. + /// + /// The corresponding C constant is `O_NONBLOCK`. + @_alwaysEmitIntoClient + public static var nonBlocking: OpenOptions { .init(rawValue: _O_NONBLOCK) } + + @_alwaysEmitIntoClient + @available(*, unavailable, renamed: "nonBlocking") + public static var O_NONBLOCK: OpenOptions { nonBlocking } + + /// Indicates that executing a program closes the file. + /// + /// Normally, file descriptors remain open + /// across calls to the `exec(2)` family of functions. + /// If you specify this option, + /// the file descriptor is closed when replacing this process + /// with another process. + /// + /// The state of the file + /// descriptor flags can be inspected using `F_GETFD`, + /// as described in the `fcntl(2)` man page. + /// + /// The corresponding C constant is `O_CLOEXEC`. + @_alwaysEmitIntoClient + public static var closeOnExec: OpenOptions { .init(rawValue: _O_CLOEXEC) } + + @_alwaysEmitIntoClient + @available(*, unavailable, renamed: "closeOnExec") + public static var O_CLOEXEC: OpenOptions { closeOnExec } + +#if !os(WASI) && !os(Linux) && !os(Android) + /// Indicates that forking a program closes the file. + /// + /// Normally, file descriptors remain open + /// across calls to the `fork(2)` function. + /// If you specify this option, + /// the file descriptor is closed when forking this process + /// into another process. + /// + /// The state of the file + /// descriptor flags can be inspected using `F_GETFD`, + /// as described in the `fcntl(2)` man page. + /// + /// The corresponding C constant is `O_CLOFORK`. + @_alwaysEmitIntoClient + public static var closeOnFork: OpenOptions { .init(rawValue: _O_CLOFORK) } + + @_alwaysEmitIntoClient + @available(*, unavailable, renamed: "closeOnFork") + public static var O_CLOFORK: OpenOptions { closeOnFork } +#endif +#endif + } + + /// Options that specify behavior for a duplicated file descriptor. + @frozen + @available(Windows, unavailable) + @available(macOS, unavailable) + @available(iOS, unavailable) + @available(tvOS, unavailable) + @available(watchOS, unavailable) + @available(visionOS, unavailable) + public struct DuplicateOptions: OptionSet, Sendable, Hashable, Codable { + /// The raw C options. + @_alwaysEmitIntoClient + public var rawValue: CInt + + /// Create a strongly-typed options value from raw C options. + @_alwaysEmitIntoClient + public init(rawValue: CInt) { self.rawValue = rawValue } + +#if !os(Windows) + /// Indicates that executing a program closes the file. + /// + /// Normally, file descriptors remain open + /// across calls to the `exec(2)` family of functions. + /// If you specify this option, + /// the file descriptor is closed when replacing this process + /// with another process. + /// + /// The state of the file + /// descriptor flags can be inspected using `F_GETFD`, + /// as described in the `fcntl(2)` man page. + /// + /// The corresponding C constant is `O_CLOEXEC`. + @_alwaysEmitIntoClient + public static var closeOnExec: OpenOptions { .init(rawValue: _O_CLOEXEC) } + + @_alwaysEmitIntoClient + @available(*, unavailable, renamed: "closeOnExec") + public static var O_CLOEXEC: OpenOptions { closeOnExec } + +#if !os(WASI) && !os(Linux) && !os(Android) + /// Indicates that forking a program closes the file. + /// + /// Normally, file descriptors remain open + /// across calls to the `fork(2)` function. + /// If you specify this option, + /// the file descriptor is closed when forking this process + /// into another process. + /// + /// The state of the file + /// descriptor flags can be inspected using `F_GETFD`, + /// as described in the `fcntl(2)` man page. + /// + /// The corresponding C constant is `O_CLOFORK`. + @_alwaysEmitIntoClient + public static var closeOnFork: OpenOptions { .init(rawValue: _O_CLOFORK) } + + @_alwaysEmitIntoClient + @available(*, unavailable, renamed: "closeOnFork") + public static var O_CLOFORK: OpenOptions { closeOnFork } +#endif +#endif + } + /// Options for specifying what a file descriptor's offset is relative to. @frozen @available(System 0.0.1, *) diff --git a/Sources/System/FileOperations.swift b/Sources/System/FileOperations.swift index 9ddc16c3..a1f563c9 100644 --- a/Sources/System/FileOperations.swift +++ b/Sources/System/FileOperations.swift @@ -403,17 +403,61 @@ extension FileDescriptor { as target: FileDescriptor? = nil, retryOnInterrupt: Bool = true ) throws -> FileDescriptor { - try _duplicate(as: target, retryOnInterrupt: retryOnInterrupt).get() + try _duplicate(as: target, options: 0, retryOnInterrupt: retryOnInterrupt).get() + } + + /// Duplicates this file descriptor and return the newly created copy. + /// + /// - Parameters: + /// - `target`: The desired target file descriptor. + /// - `options`: The behavior for creating the target file descriptor. + /// - retryOnInterrupt: Whether to retry the write operation + /// if it throws ``Errno/interrupted``. The default is `true`. + /// Pass `false` to try only once and throw an error upon interruption. + /// - Returns: The new file descriptor. + /// + /// If the `target` descriptor is already in use, then it is first + /// deallocated as if a close(2) call had been done first. + /// + /// File descriptors are merely references to some underlying system resource. + /// The system does not distinguish between the original and the new file + /// descriptor in any way. For example, read, write and seek operations on + /// one of them also affect the logical file position in the other, and + /// append mode, non-blocking I/O and asynchronous I/O options are shared + /// between the references. If a separate pointer into the file is desired, + /// a different object reference to the file must be obtained by issuing an + /// additional call to `open`. + /// + /// However, each file descriptor maintains its own close-on-exec flag. + /// + /// + /// The corresponding C function is `dup3`. + @_alwaysEmitIntoClient + @available(Windows, unavailable) + @available(macOS, unavailable) + @available(iOS, unavailable) + @available(tvOS, unavailable) + @available(watchOS, unavailable) + @available(visionOS, unavailable) + public func duplicate( + as target: FileDescriptor, + options: DuplicateOptions, + retryOnInterrupt: Bool = true + ) throws -> FileDescriptor { + try _duplicate(as: target, options: options.rawValue, retryOnInterrupt: retryOnInterrupt).get() } - @available(System 0.0.2, *) @usableFromInline internal func _duplicate( as target: FileDescriptor?, + options: Int32, retryOnInterrupt: Bool ) throws -> Result { valueOrErrno(retryOnInterrupt: retryOnInterrupt) { if let target = target { + if options != 0 { + return system_dup3(self.rawValue, target.rawValue, options) + } return system_dup2(self.rawValue, target.rawValue) } return system_dup(self.rawValue) @@ -431,6 +475,12 @@ extension FileDescriptor { public func dup2() throws -> FileDescriptor { fatalError("Not implemented") } + + @_alwaysEmitIntoClient + @available(*, unavailable, renamed: "duplicate") + public func dup3() throws -> FileDescriptor { + fatalError("Not implemented") + } } #endif @@ -445,21 +495,48 @@ extension FileDescriptor { @_alwaysEmitIntoClient @available(System 1.1.0, *) public static func pipe() throws -> (readEnd: FileDescriptor, writeEnd: FileDescriptor) { - try _pipe().get() + try _pipe(options: 0).get() + } + + /// Creates a unidirectional data channel, which can be used for interprocess communication. + /// + /// - Parameters: + /// - options: The behavior for creating the pipe. + /// + /// - Returns: The pair of file descriptors. + /// + /// The corresponding C function is `pipe2`. + @_alwaysEmitIntoClient + @available(Windows, unavailable) + @available(macOS, unavailable) + @available(iOS, unavailable) + @available(tvOS, unavailable) + @available(watchOS, unavailable) + @available(visionOS, unavailable) + public static func pipe(options: PipeOptions) throws -> (readEnd: FileDescriptor, writeEnd: FileDescriptor) { + try _pipe(options: options.rawValue).get() } - @available(System 1.1.0, *) @usableFromInline - internal static func _pipe() -> Result<(readEnd: FileDescriptor, writeEnd: FileDescriptor), Errno> { + internal static func _pipe(options: Int32) -> Result<(readEnd: FileDescriptor, writeEnd: FileDescriptor), Errno> { var fds: (Int32, Int32) = (-1, -1) return withUnsafeMutablePointer(to: &fds) { pointer in pointer.withMemoryRebound(to: Int32.self, capacity: 2) { fds in valueOrErrno(retryOnInterrupt: false) { - system_pipe(fds) + if options != 0 { + return system_pipe2(fds, options) + } + return system_pipe(fds) }.map { _ in (.init(rawValue: fds[0]), .init(rawValue: fds[1])) } } } } + + @_alwaysEmitIntoClient + @available(*, unavailable, renamed: "pipe") + public func pipe2() throws -> FileDescriptor { + fatalError("Not implemented") + } } #endif diff --git a/Sources/System/Internals/Constants.swift b/Sources/System/Internals/Constants.swift index d8cbdcbd..eebc8a1e 100644 --- a/Sources/System/Internals/Constants.swift +++ b/Sources/System/Internals/Constants.swift @@ -621,6 +621,15 @@ internal var _O_SYMLINK: CInt { O_SYMLINK } #if !os(Windows) @_alwaysEmitIntoClient internal var _O_CLOEXEC: CInt { O_CLOEXEC } + +@_alwaysEmitIntoClient +internal var _O_CLOFORK: CInt { + #if !os(WASI) && !os(Linux) && !os(Android) && !canImport(Darwin) + O_CLOFORK + #else + 0 + #endif +} #endif @_alwaysEmitIntoClient diff --git a/Sources/System/Internals/Syscalls.swift b/Sources/System/Internals/Syscalls.swift index f6eb5339..28034243 100644 --- a/Sources/System/Internals/Syscalls.swift +++ b/Sources/System/Internals/Syscalls.swift @@ -14,7 +14,6 @@ import Glibc #elseif canImport(Musl) import Musl #elseif canImport(WASILibc) -import CSystem import WASILibc #elseif os(Windows) import ucrt @@ -24,6 +23,8 @@ import Android #error("Unsupported Platform") #endif +import CSystem + // Interacting with the mocking system, tracing, etc., is a potentially significant // amount of code size, so we hand outline that code for every syscall @@ -139,6 +140,13 @@ internal func system_dup2(_ fd: Int32, _ fd2: Int32) -> Int32 { #endif return dup2(fd, fd2) } + +internal func system_dup3(_ fd: Int32, _ fd2: Int32, _ oflag: Int32) -> Int32 { + #if ENABLE_MOCKING + if mockingEnabled { return _mock(fd, fd2, oflag) } + #endif + return csystem_posix_dup3(fd, fd2, oflag) +} #endif #if !os(WASI) @@ -148,6 +156,13 @@ internal func system_pipe(_ fds: UnsafeMutablePointer) -> CInt { #endif return pipe(fds) } + +internal func system_pipe2(_ fds: UnsafeMutablePointer, _ oflag: Int32) -> CInt { +#if ENABLE_MOCKING + if mockingEnabled { return _mock(fds, oflag) } +#endif + return csystem_posix_pipe2(fds, oflag) +} #endif internal func system_ftruncate(_ fd: Int32, _ length: off_t) -> Int32 {