Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Sources/Containerization/LinuxContainer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import ContainerizationOCI
import Foundation
import Logging
import SendableProperty
import Synchronization

import struct ContainerizationOS.Terminal

Expand Down
4 changes: 2 additions & 2 deletions Sources/ContainerizationOCI/Client/RegistryClient+Fetch.swift
Original file line number Diff line number Diff line change
Expand Up @@ -177,10 +177,10 @@ extension RegistryClient {
while var buf = try await itr.next() {
let readBytes = Int64(buf.readableBytes)
received += readBytes
let written = try await writer.write(contentsOf: buf)
await progress?([
ProgressEvent(event: "add-size", value: readBytes)
ProgressEvent(event: "add-size", value: written)
])
let written = try await writer.write(contentsOf: buf)
guard written == readBytes else {
throw ContainerizationError(.internalError, message: "Could not write \(readBytes) bytes to file \(file)")
}
Expand Down
26 changes: 24 additions & 2 deletions Sources/SendableProperty/SendableProperty.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,32 @@
// limitations under the License.
//===----------------------------------------------------------------------===//

// `Synchronization` will be automatically imported with `SendableProperty`
@_exported import Synchronization
// `Foundation` will be automatically imported with `SendableProperty`.
@_exported import Foundation

// A declaration of the `@SendableProperty` macro.
@attached(peer, names: arbitrary)
@attached(accessor)
public macro SendableProperty() = #externalMacro(module: "SendablePropertyMacros", type: "SendablePropertyMacro")

/// A synchronization primitive that protects shared mutable state via mutual exclusion.
public final class Synchronized<T>: @unchecked Sendable {
private let lock = NSLock()
private var value: T

/// Creates a new instance.
/// - Parameter value: The initial value.
public init(_ value: T) {
self.value = value
}

/// Calls the given closure after acquiring the lock and returns its value.
/// - Parameter body: The body of code to execute while the lock is held.
public func withLock<R>(_ body: (inout T) throws -> R) rethrows -> R {
lock.lock()
defer {
lock.unlock()
}
return try body(&value)
}
}
16 changes: 4 additions & 12 deletions Sources/SendablePropertyMacros/SendablePropertyMacro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import SwiftParser
import SwiftSyntax
import SwiftSyntaxBuilder
import SwiftSyntaxMacros
import Synchronization

/// A macro that allows to make a property thread-safe keeping the `Sendable` conformance of the type.
public struct SendablePropertyMacro: PeerMacro {
Expand Down Expand Up @@ -54,12 +53,13 @@ public struct SendablePropertyMacro: PeerMacro {
genericTypeAnnotation = "<\(typeName)\(hasInitializer ? "" : "?")>"
}

let accessLevel = varDecl.modifiers.first(where: { ["open", "public", "internal", "fileprivate", "private"].contains($0.name.text) })?.name.text ?? "internal"

// Create a peer property
let peerPropertyName = self.peerPropertyName(for: propertyName)
// `Mutex` (requires macOS 15) and `OSAllocationUnfairLock` (requires macOS 13, unsupported on Linux) are more effective than `NSLock`.
let peerProperty: DeclSyntax =
"""
private let \(raw: peerPropertyName) = Mutex\(raw: genericTypeAnnotation)(\(raw: initializerValue))
\(raw: accessLevel) let \(raw: peerPropertyName) = Synchronized\(raw: genericTypeAnnotation)(\(raw: initializerValue))
"""
return [peerProperty]
}
Expand Down Expand Up @@ -93,18 +93,10 @@ extension SendablePropertyMacro: AccessorMacro {
\(raw: peerPropertyName).withLock { $0\(raw: hasInitializer ? "" : "!") }
}
"""
// The `Sending` class is used as a temporary workaround for the error: "'inout sending' parameter '$0' cannot be task-isolated at end of function."
let accessorSetter: AccessorDeclSyntax =
"""
set {
class Sending<T>: @unchecked Sendable {
let wrappedValue: T
init(_ value: T) {
wrappedValue = value
}
}
let newValue = Sending(newValue)
\(raw: peerPropertyName).withLock { $0 = newValue.wrappedValue }
\(raw: peerPropertyName).withLock { $0 = newValue }
}
"""

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ public class ImageStoreTests: ContainsAuth {
return
}
let imageReference = "ghcr.io/apple/containerization/dockermanifestimage:0.0.2"
let busyboxImage = try await self.store.pull(reference: imageReference, auth: Self.authentication)
let busyboxImage = try await self.store.pull(reference: imageReference, auth: authentication)

let got = try await self.store.get(reference: imageReference)
#expect(got.descriptor == busyboxImage.descriptor)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,20 +52,13 @@ final class SendablePropertyMacrosTests: XCTestCase {
}
}
set {
class Sending<T>: @unchecked Sendable {
let wrappedValue: T
init(_ value: T) {
wrappedValue = value
}
}
let newValue = Sending(newValue)
_value.withLock {
$0 = newValue.wrappedValue
$0 = newValue
}
}
}

private let _value = Mutex<Int?>(nil)
internal let _value = Synchronized<Int?>(nil)
}
""",
macros: testMacros
Expand Down Expand Up @@ -94,20 +87,13 @@ final class SendablePropertyMacrosTests: XCTestCase {
}
}
set {
class Sending<T>: @unchecked Sendable {
let wrappedValue: T
init(_ value: T) {
wrappedValue = value
}
}
let newValue = Sending(newValue)
_value.withLock {
$0 = newValue.wrappedValue
$0 = newValue
}
}
}

private let _value = Mutex(0)
internal let _value = Synchronized(0)
}
""",
macros: testMacros
Expand Down Expand Up @@ -136,20 +122,13 @@ final class SendablePropertyMacrosTests: XCTestCase {
}
}
set {
class Sending<T>: @unchecked Sendable {
let wrappedValue: T
init(_ value: T) {
wrappedValue = value
}
}
let newValue = Sending(newValue)
_value.withLock {
$0 = newValue.wrappedValue
$0 = newValue
}
}
}

private let _value = Mutex<Int>(0)
internal let _value = Synchronized<Int>(0)
}
""",
macros: testMacros
Expand Down