Skip to content

Commit 3d98a84

Browse files
committed
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.
1 parent f477389 commit 3d98a84

File tree

8 files changed

+277
-7
lines changed

8 files changed

+277
-7
lines changed

Sources/CSystem/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ target_include_directories(CSystem INTERFACE
1414

1515
install(FILES
1616
include/CSystemLinux.h
17+
include/CSystemShared.h
1718
include/CSystemWindows.h
1819
include/module.modulemap
1920
DESTINATION include/CSystem)
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
extern int csystem_posix_pipe2(int fildes[2], int flag);
2+
extern int csystem_posix_dup3(int fildes, int fildes2, int flag);

Sources/CSystem/include/module.modulemap

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
module CSystem {
2+
header "CSystemShared.h"
23
header "CSystemLinux.h"
34
header "CSystemWASI.h"
45
header "CSystemWindows.h"

Sources/CSystem/shims.c

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,40 @@
77
See https://swift.org/LICENSE.txt for license information
88
*/
99

10-
#ifdef __linux__
10+
#if defined(__FreeBSD__) || defined(__OpenBSD__)
11+
#define __BSD_VISIBLE
12+
#include <unistd.h>
13+
#endif
1114

15+
#ifdef __linux__
16+
#define _GNU_SOURCE
1217
#include <CSystemLinux.h>
13-
1418
#endif
1519

1620
#if defined(_WIN32)
1721
#include <CSystemWindows.h>
1822
#endif
23+
24+
#include <errno.h>
25+
26+
#if !defined(_WIN32) && !defined(__wasi__) && !defined(__APPLE__)
27+
#define HAVE_PIPE2_DUP3
28+
#endif
29+
30+
// Wrappers are required because _GNU_SOURCE causes a conflict with other imports when defined in CSystemLinux.h
31+
extern int csystem_posix_pipe2(int fildes[2], int flag) {
32+
#ifdef HAVE_PIPE2_DUP3
33+
return pipe2(fildes, flag);
34+
#else
35+
errno = ENOSYS;
36+
return -1;
37+
#endif
38+
}
39+
extern int csystem_posix_dup3(int fildes, int fildes2, int flag) {
40+
#ifdef HAVE_PIPE2_DUP3
41+
return dup3(fildes, fildes2, flag);
42+
#else
43+
errno = ENOSYS;
44+
return -1;
45+
#endif
46+
}

Sources/System/FileDescriptor.swift

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,141 @@ extension FileDescriptor {
324324
#endif
325325
}
326326

327+
/// Options that specify behavior for a newly-created pipe.
328+
@frozen
329+
@available(Windows, unavailable)
330+
@available(macOS, unavailable)
331+
@available(iOS, unavailable)
332+
@available(tvOS, unavailable)
333+
@available(watchOS, unavailable)
334+
@available(visionOS, unavailable)
335+
public struct PipeOptions: OptionSet, Sendable, Hashable, Codable {
336+
/// The raw C options.
337+
@_alwaysEmitIntoClient
338+
public var rawValue: CInt
339+
340+
/// Create a strongly-typed options value from raw C options.
341+
@_alwaysEmitIntoClient
342+
public init(rawValue: CInt) { self.rawValue = rawValue }
343+
344+
#if !os(Windows)
345+
/// Indicates that all
346+
/// subsequent input and output operations on the pipe's file descriptors will be nonblocking.
347+
///
348+
/// The corresponding C constant is `O_NONBLOCK`.
349+
@_alwaysEmitIntoClient
350+
public static var nonBlocking: OpenOptions { .init(rawValue: _O_NONBLOCK) }
351+
352+
@_alwaysEmitIntoClient
353+
@available(*, unavailable, renamed: "nonBlocking")
354+
public static var O_NONBLOCK: OpenOptions { nonBlocking }
355+
356+
/// Indicates that executing a program closes the file.
357+
///
358+
/// Normally, file descriptors remain open
359+
/// across calls to the `exec(2)` family of functions.
360+
/// If you specify this option,
361+
/// the file descriptor is closed when replacing this process
362+
/// with another process.
363+
///
364+
/// The state of the file
365+
/// descriptor flags can be inspected using `F_GETFD`,
366+
/// as described in the `fcntl(2)` man page.
367+
///
368+
/// The corresponding C constant is `O_CLOEXEC`.
369+
@_alwaysEmitIntoClient
370+
public static var closeOnExec: OpenOptions { .init(rawValue: _O_CLOEXEC) }
371+
372+
@_alwaysEmitIntoClient
373+
@available(*, unavailable, renamed: "closeOnExec")
374+
public static var O_CLOEXEC: OpenOptions { closeOnExec }
375+
376+
#if !os(WASI) && !os(Linux) && !os(Android)
377+
/// Indicates that forking a program closes the file.
378+
///
379+
/// Normally, file descriptors remain open
380+
/// across calls to the `fork(2)` function.
381+
/// If you specify this option,
382+
/// the file descriptor is closed when forking this process
383+
/// into another process.
384+
///
385+
/// The state of the file
386+
/// descriptor flags can be inspected using `F_GETFD`,
387+
/// as described in the `fcntl(2)` man page.
388+
///
389+
/// The corresponding C constant is `O_CLOFORK`.
390+
@_alwaysEmitIntoClient
391+
public static var closeOnFork: OpenOptions { .init(rawValue: _O_CLOFORK) }
392+
393+
@_alwaysEmitIntoClient
394+
@available(*, unavailable, renamed: "closeOnFork")
395+
public static var O_CLOFORK: OpenOptions { closeOnFork }
396+
#endif
397+
#endif
398+
}
399+
400+
/// Options that specify behavior for a duplicated file descriptor.
401+
@frozen
402+
@available(Windows, unavailable)
403+
@available(macOS, unavailable)
404+
@available(iOS, unavailable)
405+
@available(tvOS, unavailable)
406+
@available(watchOS, unavailable)
407+
@available(visionOS, unavailable)
408+
public struct DuplicateOptions: OptionSet, Sendable, Hashable, Codable {
409+
/// The raw C options.
410+
@_alwaysEmitIntoClient
411+
public var rawValue: CInt
412+
413+
/// Create a strongly-typed options value from raw C options.
414+
@_alwaysEmitIntoClient
415+
public init(rawValue: CInt) { self.rawValue = rawValue }
416+
417+
#if !os(Windows)
418+
/// Indicates that executing a program closes the file.
419+
///
420+
/// Normally, file descriptors remain open
421+
/// across calls to the `exec(2)` family of functions.
422+
/// If you specify this option,
423+
/// the file descriptor is closed when replacing this process
424+
/// with another process.
425+
///
426+
/// The state of the file
427+
/// descriptor flags can be inspected using `F_GETFD`,
428+
/// as described in the `fcntl(2)` man page.
429+
///
430+
/// The corresponding C constant is `O_CLOEXEC`.
431+
@_alwaysEmitIntoClient
432+
public static var closeOnExec: OpenOptions { .init(rawValue: _O_CLOEXEC) }
433+
434+
@_alwaysEmitIntoClient
435+
@available(*, unavailable, renamed: "closeOnExec")
436+
public static var O_CLOEXEC: OpenOptions { closeOnExec }
437+
438+
#if !os(WASI) && !os(Linux) && !os(Android)
439+
/// Indicates that forking a program closes the file.
440+
///
441+
/// Normally, file descriptors remain open
442+
/// across calls to the `fork(2)` function.
443+
/// If you specify this option,
444+
/// the file descriptor is closed when forking this process
445+
/// into another process.
446+
///
447+
/// The state of the file
448+
/// descriptor flags can be inspected using `F_GETFD`,
449+
/// as described in the `fcntl(2)` man page.
450+
///
451+
/// The corresponding C constant is `O_CLOFORK`.
452+
@_alwaysEmitIntoClient
453+
public static var closeOnFork: OpenOptions { .init(rawValue: _O_CLOFORK) }
454+
455+
@_alwaysEmitIntoClient
456+
@available(*, unavailable, renamed: "closeOnFork")
457+
public static var O_CLOFORK: OpenOptions { closeOnFork }
458+
#endif
459+
#endif
460+
}
461+
327462
/// Options for specifying what a file descriptor's offset is relative to.
328463
@frozen
329464
@available(System 0.0.1, *)

Sources/System/FileOperations.swift

Lines changed: 83 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -403,17 +403,62 @@ extension FileDescriptor {
403403
as target: FileDescriptor? = nil,
404404
retryOnInterrupt: Bool = true
405405
) throws -> FileDescriptor {
406-
try _duplicate(as: target, retryOnInterrupt: retryOnInterrupt).get()
406+
try _duplicate(as: target, options: [], retryOnInterrupt: retryOnInterrupt).get()
407+
}
408+
409+
/// Duplicates this file descriptor and return the newly created copy.
410+
///
411+
/// - Parameters:
412+
/// - `target`: The desired target file descriptor.
413+
/// - `options`: The behavior for creating the target file descriptor.
414+
/// - retryOnInterrupt: Whether to retry the write operation
415+
/// if it throws ``Errno/interrupted``. The default is `true`.
416+
/// Pass `false` to try only once and throw an error upon interruption.
417+
/// - Returns: The new file descriptor.
418+
///
419+
/// If the `target` descriptor is already in use, then it is first
420+
/// deallocated as if a close(2) call had been done first.
421+
///
422+
/// File descriptors are merely references to some underlying system resource.
423+
/// The system does not distinguish between the original and the new file
424+
/// descriptor in any way. For example, read, write and seek operations on
425+
/// one of them also affect the logical file position in the other, and
426+
/// append mode, non-blocking I/O and asynchronous I/O options are shared
427+
/// between the references. If a separate pointer into the file is desired,
428+
/// a different object reference to the file must be obtained by issuing an
429+
/// additional call to `open`.
430+
///
431+
/// However, each file descriptor maintains its own close-on-exec flag.
432+
///
433+
///
434+
/// The corresponding C function is `dup3`.
435+
@_alwaysEmitIntoClient
436+
@available(Windows, unavailable)
437+
@available(macOS, unavailable)
438+
@available(iOS, unavailable)
439+
@available(tvOS, unavailable)
440+
@available(watchOS, unavailable)
441+
@available(visionOS, unavailable)
442+
public func duplicate(
443+
as target: FileDescriptor,
444+
options: DuplicateOptions,
445+
retryOnInterrupt: Bool = true
446+
) throws -> FileDescriptor {
447+
try _duplicate(as: target, options: options, retryOnInterrupt: retryOnInterrupt).get()
407448
}
408449

409450
@available(System 0.0.2, *)
410451
@usableFromInline
411452
internal func _duplicate(
412453
as target: FileDescriptor?,
454+
options: DuplicateOptions,
413455
retryOnInterrupt: Bool
414456
) throws -> Result<FileDescriptor, Errno> {
415457
valueOrErrno(retryOnInterrupt: retryOnInterrupt) {
416458
if let target = target {
459+
if !options.isEmpty {
460+
return system_dup3(self.rawValue, target.rawValue, options.rawValue)
461+
}
417462
return system_dup2(self.rawValue, target.rawValue)
418463
}
419464
return system_dup(self.rawValue)
@@ -431,6 +476,12 @@ extension FileDescriptor {
431476
public func dup2() throws -> FileDescriptor {
432477
fatalError("Not implemented")
433478
}
479+
480+
@_alwaysEmitIntoClient
481+
@available(*, unavailable, renamed: "duplicate")
482+
public func dup3() throws -> FileDescriptor {
483+
fatalError("Not implemented")
484+
}
434485
}
435486
#endif
436487

@@ -445,21 +496,49 @@ extension FileDescriptor {
445496
@_alwaysEmitIntoClient
446497
@available(System 1.1.0, *)
447498
public static func pipe() throws -> (readEnd: FileDescriptor, writeEnd: FileDescriptor) {
448-
try _pipe().get()
499+
try _pipe(options: []).get()
500+
}
501+
502+
/// Creates a unidirectional data channel, which can be used for interprocess communication.
503+
///
504+
/// - Parameters:
505+
/// - options: The behavior for creating the pipe.
506+
///
507+
/// - Returns: The pair of file descriptors.
508+
///
509+
/// The corresponding C function is `pipe2`.
510+
@_alwaysEmitIntoClient
511+
@available(Windows, unavailable)
512+
@available(macOS, unavailable)
513+
@available(iOS, unavailable)
514+
@available(tvOS, unavailable)
515+
@available(watchOS, unavailable)
516+
@available(visionOS, unavailable)
517+
public static func pipe(options: PipeOptions) throws -> (readEnd: FileDescriptor, writeEnd: FileDescriptor) {
518+
try _pipe(options: options).get()
449519
}
450520

451521
@available(System 1.1.0, *)
452522
@usableFromInline
453-
internal static func _pipe() -> Result<(readEnd: FileDescriptor, writeEnd: FileDescriptor), Errno> {
523+
internal static func _pipe(options: PipeOptions) -> Result<(readEnd: FileDescriptor, writeEnd: FileDescriptor), Errno> {
454524
var fds: (Int32, Int32) = (-1, -1)
455525
return withUnsafeMutablePointer(to: &fds) { pointer in
456526
pointer.withMemoryRebound(to: Int32.self, capacity: 2) { fds in
457527
valueOrErrno(retryOnInterrupt: false) {
458-
system_pipe(fds)
528+
if !options.isEmpty {
529+
return system_pipe2(fds, options.rawValue)
530+
}
531+
return system_pipe(fds)
459532
}.map { _ in (.init(rawValue: fds[0]), .init(rawValue: fds[1])) }
460533
}
461534
}
462535
}
536+
537+
@_alwaysEmitIntoClient
538+
@available(*, unavailable, renamed: "pipe")
539+
public func pipe2() throws -> FileDescriptor {
540+
fatalError("Not implemented")
541+
}
463542
}
464543
#endif
465544

Sources/System/Internals/Constants.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -621,6 +621,15 @@ internal var _O_SYMLINK: CInt { O_SYMLINK }
621621
#if !os(Windows)
622622
@_alwaysEmitIntoClient
623623
internal var _O_CLOEXEC: CInt { O_CLOEXEC }
624+
625+
@_alwaysEmitIntoClient
626+
internal var _O_CLOFORK: CInt {
627+
#if !os(WASI) && !os(Linux) && !os(Android) && !canImport(Darwin)
628+
O_CLOFORK
629+
#else
630+
0
631+
#endif
632+
}
624633
#endif
625634

626635
@_alwaysEmitIntoClient

Sources/System/Internals/Syscalls.swift

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import Glibc
1414
#elseif canImport(Musl)
1515
import Musl
1616
#elseif canImport(WASILibc)
17-
import CSystem
1817
import WASILibc
1918
#elseif os(Windows)
2019
import ucrt
@@ -24,6 +23,8 @@ import Android
2423
#error("Unsupported Platform")
2524
#endif
2625

26+
import CSystem
27+
2728
// Interacting with the mocking system, tracing, etc., is a potentially significant
2829
// amount of code size, so we hand outline that code for every syscall
2930

@@ -139,6 +140,13 @@ internal func system_dup2(_ fd: Int32, _ fd2: Int32) -> Int32 {
139140
#endif
140141
return dup2(fd, fd2)
141142
}
143+
144+
internal func system_dup3(_ fd: Int32, _ fd2: Int32, _ oflag: Int32) -> Int32 {
145+
#if ENABLE_MOCKING
146+
if mockingEnabled { return _mock(fd, fd2, oflag) }
147+
#endif
148+
return csystem_posix_dup3(fd, fd2, oflag)
149+
}
142150
#endif
143151

144152
#if !os(WASI)
@@ -148,6 +156,13 @@ internal func system_pipe(_ fds: UnsafeMutablePointer<Int32>) -> CInt {
148156
#endif
149157
return pipe(fds)
150158
}
159+
160+
internal func system_pipe2(_ fds: UnsafeMutablePointer<Int32>, _ oflag: Int32) -> CInt {
161+
#if ENABLE_MOCKING
162+
if mockingEnabled { return _mock(fds, oflag) }
163+
#endif
164+
return csystem_posix_pipe2(fds, oflag)
165+
}
151166
#endif
152167

153168
internal func system_ftruncate(_ fd: Int32, _ length: off_t) -> Int32 {

0 commit comments

Comments
 (0)