Skip to content

Commit 55a8427

Browse files
authored
Merge pull request #16 from iCharlesHu/charles/mutex-fallback
Fix build on macOS 13 while maintaining minimal compatibility
2 parents bd0cc4f + 808f059 commit 55a8427

17 files changed

+295
-39
lines changed

Sources/Subprocess/API.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
//===----------------------------------------------------------------------===//
1111

1212
#if canImport(System)
13-
import System
13+
@preconcurrency import System
1414
#else
1515
@preconcurrency import SystemPackage
1616
#endif

Sources/Subprocess/AsyncBufferSequence.swift

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,22 +10,21 @@
1010
//===----------------------------------------------------------------------===//
1111

1212
#if canImport(System)
13-
import System
13+
@preconcurrency import System
1414
#else
1515
@preconcurrency import SystemPackage
1616
#endif
1717

1818
#if SubprocessSpan
1919
@available(SubprocessSpan, *)
2020
#endif
21-
internal struct AsyncBufferSequence: AsyncSequence, Sendable {
22-
internal typealias Failure = any Swift.Error
23-
24-
internal typealias Element = SequenceOutput.Buffer
21+
public struct AsyncBufferSequence: AsyncSequence, Sendable {
22+
public typealias Failure = any Swift.Error
23+
public typealias Element = SequenceOutput.Buffer
2524

2625
@_nonSendable
27-
internal struct Iterator: AsyncIteratorProtocol {
28-
internal typealias Element = SequenceOutput.Buffer
26+
public struct Iterator: AsyncIteratorProtocol {
27+
public typealias Element = SequenceOutput.Buffer
2928

3029
private let fileDescriptor: TrackedFileDescriptor
3130
private var buffer: [UInt8]
@@ -39,7 +38,7 @@ internal struct AsyncBufferSequence: AsyncSequence, Sendable {
3938
self.finished = false
4039
}
4140

42-
internal mutating func next() async throws -> SequenceOutput.Buffer? {
41+
public mutating func next() async throws -> SequenceOutput.Buffer? {
4342
let data = try await self.fileDescriptor.wrapped.readChunk(
4443
upToLength: readBufferSize
4544
)
@@ -58,7 +57,7 @@ internal struct AsyncBufferSequence: AsyncSequence, Sendable {
5857
self.fileDescriptor = fileDescriptor
5958
}
6059

61-
internal func makeAsyncIterator() -> Iterator {
60+
public func makeAsyncIterator() -> Iterator {
6261
return Iterator(fileDescriptor: self.fileDescriptor)
6362
}
6463
}

Sources/Subprocess/Atomic.swift

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2025 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
//
10+
//===----------------------------------------------------------------------===//
11+
12+
#if canImport(Synchronization)
13+
import Synchronization
14+
#else
15+
16+
#if canImport(os)
17+
internal import os
18+
#if canImport(C.os.lock)
19+
internal import C.os.lock
20+
#endif
21+
#elseif canImport(Bionic)
22+
@preconcurrency import Bionic
23+
#elseif canImport(Glibc)
24+
@preconcurrency import Glibc
25+
#elseif canImport(Musl)
26+
@preconcurrency import Musl
27+
#elseif canImport(WinSDK)
28+
import WinSDK
29+
#endif // canImport(os)
30+
31+
#endif // canImport(Synchronization)
32+
33+
internal struct AtomicBox: Sendable, ~Copyable {
34+
internal typealias BitwiseXorFunc = (OutputConsumptionState) -> OutputConsumptionState
35+
36+
private let storage: @Sendable () -> BitwiseXorFunc
37+
38+
internal init() {
39+
#if canImport(Synchronization)
40+
guard #available(macOS 15, *) else {
41+
fatalError("Unexpected configuration")
42+
}
43+
let box = Atomic(UInt8(0))
44+
self.storage = {
45+
return { input in
46+
return box._bitwiseXor(input)
47+
}
48+
}
49+
#else
50+
let state = LockedState(OutputConsumptionState(rawValue: 0))
51+
self.storage = {
52+
return state._bitwiseXor
53+
}
54+
#endif
55+
}
56+
57+
internal func bitwiseXor(
58+
_ operand: OutputConsumptionState
59+
) -> OutputConsumptionState {
60+
return self.storage()(operand)
61+
}
62+
}
63+
64+
#if canImport(Synchronization)
65+
@available(macOS 15, *)
66+
extension Atomic where Value == UInt8 {
67+
borrowing func _bitwiseXor(
68+
_ operand: OutputConsumptionState
69+
) -> OutputConsumptionState {
70+
let newState = self.bitwiseXor(
71+
operand.rawValue,
72+
ordering: .relaxed
73+
).newValue
74+
return OutputConsumptionState(rawValue: newState)
75+
}
76+
77+
init(_ initialValue: OutputConsumptionState) {
78+
self = Atomic(initialValue.rawValue)
79+
}
80+
}
81+
#else
82+
// Fallback to LockedState if `Synchronization` is not available
83+
extension LockedState where State == OutputConsumptionState {
84+
init(_ initialValue: OutputConsumptionState) {
85+
self.init(initialState: initialValue)
86+
}
87+
88+
func _bitwiseXor(
89+
_ operand: OutputConsumptionState
90+
) -> OutputConsumptionState {
91+
return self.withLock { state in
92+
state = OutputConsumptionState(rawValue: state.rawValue ^ operand.rawValue)
93+
return state
94+
}
95+
}
96+
}
97+
98+
// MARK: - LockState
99+
internal struct LockedState<State>: ~Copyable {
100+
101+
// Internal implementation for a cheap lock to aid sharing code across platforms
102+
private struct _Lock {
103+
#if canImport(os)
104+
typealias Primitive = os_unfair_lock
105+
#elseif os(FreeBSD) || os(OpenBSD)
106+
typealias Primitive = pthread_mutex_t?
107+
#elseif canImport(Bionic) || canImport(Glibc) || canImport(Musl)
108+
typealias Primitive = pthread_mutex_t
109+
#elseif canImport(WinSDK)
110+
typealias Primitive = SRWLOCK
111+
#elseif os(WASI)
112+
// WASI is single-threaded, so we don't need a lock.
113+
typealias Primitive = Void
114+
#endif
115+
116+
typealias PlatformLock = UnsafeMutablePointer<Primitive>
117+
var _platformLock: PlatformLock
118+
119+
fileprivate static func initialize(_ platformLock: PlatformLock) {
120+
#if canImport(os)
121+
platformLock.initialize(to: os_unfair_lock())
122+
#elseif canImport(Bionic) || canImport(Glibc) || canImport(Musl)
123+
pthread_mutex_init(platformLock, nil)
124+
#elseif canImport(WinSDK)
125+
InitializeSRWLock(platformLock)
126+
#elseif os(WASI)
127+
// no-op
128+
#else
129+
#error("LockedState._Lock.initialize is unimplemented on this platform")
130+
#endif
131+
}
132+
133+
fileprivate static func deinitialize(_ platformLock: PlatformLock) {
134+
#if canImport(Bionic) || canImport(Glibc) || canImport(Musl)
135+
pthread_mutex_destroy(platformLock)
136+
#endif
137+
platformLock.deinitialize(count: 1)
138+
}
139+
140+
static fileprivate func lock(_ platformLock: PlatformLock) {
141+
#if canImport(os)
142+
os_unfair_lock_lock(platformLock)
143+
#elseif canImport(Bionic) || canImport(Glibc) || canImport(Musl)
144+
pthread_mutex_lock(platformLock)
145+
#elseif canImport(WinSDK)
146+
AcquireSRWLockExclusive(platformLock)
147+
#elseif os(WASI)
148+
// no-op
149+
#else
150+
#error("LockedState._Lock.lock is unimplemented on this platform")
151+
#endif
152+
}
153+
154+
static fileprivate func unlock(_ platformLock: PlatformLock) {
155+
#if canImport(os)
156+
os_unfair_lock_unlock(platformLock)
157+
#elseif canImport(Bionic) || canImport(Glibc) || canImport(Musl)
158+
pthread_mutex_unlock(platformLock)
159+
#elseif canImport(WinSDK)
160+
ReleaseSRWLockExclusive(platformLock)
161+
#elseif os(WASI)
162+
// no-op
163+
#else
164+
#error("LockedState._Lock.unlock is unimplemented on this platform")
165+
#endif
166+
}
167+
}
168+
169+
private class _Buffer: ManagedBuffer<State, _Lock.Primitive> {
170+
deinit {
171+
withUnsafeMutablePointerToElements {
172+
_Lock.deinitialize($0)
173+
}
174+
}
175+
}
176+
177+
private let _buffer: ManagedBuffer<State, _Lock.Primitive>
178+
179+
package init(initialState: State) {
180+
_buffer = _Buffer.create(
181+
minimumCapacity: 1,
182+
makingHeaderWith: { buf in
183+
buf.withUnsafeMutablePointerToElements {
184+
_Lock.initialize($0)
185+
}
186+
return initialState
187+
}
188+
)
189+
}
190+
191+
package func withLock<T>(_ body: @Sendable (inout State) throws -> T) rethrows -> T {
192+
try withLockUnchecked(body)
193+
}
194+
195+
package func withLockUnchecked<T>(_ body: (inout State) throws -> T) rethrows -> T {
196+
try _buffer.withUnsafeMutablePointers { state, lock in
197+
_Lock.lock(lock)
198+
defer { _Lock.unlock(lock) }
199+
return try body(&state.pointee)
200+
}
201+
}
202+
203+
// Ensures the managed state outlives the locked scope.
204+
package func withLockExtendingLifetimeOfState<T>(_ body: @Sendable (inout State) throws -> T) rethrows -> T {
205+
try _buffer.withUnsafeMutablePointers { state, lock in
206+
_Lock.lock(lock)
207+
return try withExtendedLifetime(state.pointee) {
208+
defer { _Lock.unlock(lock) }
209+
return try body(&state.pointee)
210+
}
211+
}
212+
}
213+
}
214+
215+
extension LockedState where State == Void {
216+
internal init() {
217+
self.init(initialState: ())
218+
}
219+
220+
internal func withLock<R: Sendable>(_ body: @Sendable () throws -> R) rethrows -> R {
221+
return try withLock { _ in
222+
try body()
223+
}
224+
}
225+
226+
internal func lock() {
227+
_buffer.withUnsafeMutablePointerToElements { lock in
228+
_Lock.lock(lock)
229+
}
230+
}
231+
232+
internal func unlock() {
233+
_buffer.withUnsafeMutablePointerToElements { lock in
234+
_Lock.unlock(lock)
235+
}
236+
}
237+
}
238+
239+
extension LockedState: @unchecked Sendable where State: Sendable {}
240+
241+
#endif

Sources/Subprocess/Configuration.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
//===----------------------------------------------------------------------===//
1111

1212
#if canImport(System)
13-
import System
13+
@preconcurrency import System
1414
#else
1515
@preconcurrency import SystemPackage
1616
#endif
@@ -335,6 +335,7 @@ extension Configuration: CustomStringConvertible, CustomDebugStringConvertible {
335335
)
336336
"""
337337
}
338+
338339
}
339340

340341
// MARK: - Cleanup

Sources/Subprocess/Execution.swift

Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
//===----------------------------------------------------------------------===//
1111

1212
#if canImport(System)
13-
import System
13+
@preconcurrency import System
1414
#else
1515
@preconcurrency import SystemPackage
1616
#endif
@@ -27,8 +27,6 @@ import Musl
2727
import WinSDK
2828
#endif
2929

30-
import Synchronization
31-
3230
/// An object that repersents a subprocess that has been
3331
/// executed. You can use this object to send signals to the
3432
/// child process as well as stream its output and error.
@@ -46,7 +44,8 @@ public final class Execution<
4644
internal let error: Error
4745
internal let outputPipe: CreatedPipe
4846
internal let errorPipe: CreatedPipe
49-
internal let outputConsumptionState: Atomic<OutputConsumptionState.RawValue>
47+
internal let outputConsumptionState: AtomicBox
48+
5049
#if os(Windows)
5150
internal let consoleBehavior: PlatformOptions.ConsoleBehavior
5251

@@ -63,7 +62,7 @@ public final class Execution<
6362
self.error = error
6463
self.outputPipe = outputPipe
6564
self.errorPipe = errorPipe
66-
self.outputConsumptionState = Atomic(0)
65+
self.outputConsumptionState = AtomicBox()
6766
self.consoleBehavior = consoleBehavior
6867
}
6968
#else
@@ -79,7 +78,7 @@ public final class Execution<
7978
self.error = error
8079
self.outputPipe = outputPipe
8180
self.errorPipe = errorPipe
82-
self.outputConsumptionState = Atomic(0)
81+
self.outputConsumptionState = AtomicBox()
8382
}
8483
#endif // os(Windows)
8584
}
@@ -93,13 +92,12 @@ extension Execution where Output == SequenceOutput {
9392
/// Accessing this property will **fatalError** if this property was
9493
/// accessed multiple times. Subprocess communicates with parent process
9594
/// via pipe under the hood and each pipe can only be consumed once.
96-
public var standardOutput: some AsyncSequence<SequenceOutput.Buffer, any Swift.Error> {
95+
public var standardOutput: AsyncBufferSequence {
9796
let consumptionState = self.outputConsumptionState.bitwiseXor(
98-
OutputConsumptionState.standardOutputConsumed.rawValue,
99-
ordering: .relaxed
100-
).newValue
97+
OutputConsumptionState.standardOutputConsumed
98+
)
10199

102-
guard OutputConsumptionState(rawValue: consumptionState).contains(.standardOutputConsumed),
100+
guard consumptionState.contains(.standardOutputConsumed),
103101
let fd = self.outputPipe.readFileDescriptor
104102
else {
105103
fatalError("The standard output has already been consumed")
@@ -117,13 +115,12 @@ extension Execution where Error == SequenceOutput {
117115
/// Accessing this property will **fatalError** if this property was
118116
/// accessed multiple times. Subprocess communicates with parent process
119117
/// via pipe under the hood and each pipe can only be consumed once.
120-
public var standardError: some AsyncSequence<SequenceOutput.Buffer, any Swift.Error> {
118+
public var standardError: AsyncBufferSequence {
121119
let consumptionState = self.outputConsumptionState.bitwiseXor(
122-
OutputConsumptionState.standardErrorConsumed.rawValue,
123-
ordering: .relaxed
124-
).newValue
120+
OutputConsumptionState.standardOutputConsumed
121+
)
125122

126-
guard OutputConsumptionState(rawValue: consumptionState).contains(.standardErrorConsumed),
123+
guard consumptionState.contains(.standardErrorConsumed),
127124
let fd = self.errorPipe.readFileDescriptor
128125
else {
129126
fatalError("The standard output has already been consumed")

0 commit comments

Comments
 (0)