From 41d41e4ffe5c625e1fe01da454879423f5333eea Mon Sep 17 00:00:00 2001 From: Dmitry Kovba Date: Mon, 14 Jul 2025 15:08:08 -0700 Subject: [PATCH] Check for supported types in the `@SendableProperty` macro (#212) - The `@SendableProperty` macro now checks for supported types to prevent a misuse with unsupported types like structs, dictionaries, and arrays - The new `@SendablePropertyUnchecked` can be used with classes and enums --- Sources/Containerization/LinuxContainer.swift | 2 +- .../SendableProperty/SendableProperty.swift | 26 ----- .../SendablePropertyUnchecked.swift | 20 ++++ Sources/SendableProperty/Synchronized.swift | 41 +++++++ .../SendablePropertyError.swift | 6 +- .../SendablePropertyMacro.swift | 89 ++++++--------- .../SendablePropertyMacroUnchecked.swift | 104 ++++++++++++++++++ .../SendablePropertyPlugin.swift | 5 +- .../SendablePropertyTests.swift | 66 +++++++++++ 9 files changed, 272 insertions(+), 87 deletions(-) create mode 100644 Sources/SendableProperty/SendablePropertyUnchecked.swift create mode 100644 Sources/SendableProperty/Synchronized.swift create mode 100644 Sources/SendablePropertyMacros/SendablePropertyMacroUnchecked.swift diff --git a/Sources/Containerization/LinuxContainer.swift b/Sources/Containerization/LinuxContainer.swift index 33801d12..ceddc97a 100644 --- a/Sources/Containerization/LinuxContainer.swift +++ b/Sources/Containerization/LinuxContainer.swift @@ -54,7 +54,7 @@ public final class LinuxContainer: Container, Sendable { var dns: DNS? = nil } - @SendableProperty + @SendablePropertyUnchecked private var state: State private let config: Mutex diff --git a/Sources/SendableProperty/SendableProperty.swift b/Sources/SendableProperty/SendableProperty.swift index 162fad22..bf8d60e5 100644 --- a/Sources/SendableProperty/SendableProperty.swift +++ b/Sources/SendableProperty/SendableProperty.swift @@ -14,33 +14,7 @@ // limitations under the License. //===----------------------------------------------------------------------===// -// `Synchronization` will be automatically imported with `SendableProperty`. -@_exported import Synchronization - // 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: Sendable { - private let lock: Mutex - - private struct State: @unchecked Sendable { - var value: T - } - - /// Creates a new instance. - /// - Parameter value: The initial value. - public init(_ value: T) { - self.lock = Mutex(State(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(_ body: (inout T) throws -> R) rethrows -> R { - try lock.withLock { state in - try body(&state.value) - } - } -} diff --git a/Sources/SendableProperty/SendablePropertyUnchecked.swift b/Sources/SendableProperty/SendablePropertyUnchecked.swift new file mode 100644 index 00000000..8c859b92 --- /dev/null +++ b/Sources/SendableProperty/SendablePropertyUnchecked.swift @@ -0,0 +1,20 @@ +//===----------------------------------------------------------------------===// +// Copyright © 2025 Apple Inc. and the Containerization project authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//===----------------------------------------------------------------------===// + +// A declaration of the `@SendablePropertyUnchecked` macro. +@attached(peer, names: arbitrary) +@attached(accessor) +public macro SendablePropertyUnchecked() = #externalMacro(module: "SendablePropertyMacros", type: "SendablePropertyMacroUnchecked") diff --git a/Sources/SendableProperty/Synchronized.swift b/Sources/SendableProperty/Synchronized.swift new file mode 100644 index 00000000..bbe5c42f --- /dev/null +++ b/Sources/SendableProperty/Synchronized.swift @@ -0,0 +1,41 @@ +//===----------------------------------------------------------------------===// +// Copyright © 2025 Apple Inc. and the Containerization project authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//===----------------------------------------------------------------------===// + +// `Synchronization` will be automatically imported with `SendableProperty`. +@_exported import Synchronization + +/// A synchronization primitive that protects shared mutable state via mutual exclusion. +public final class Synchronized: Sendable { + private let lock: Mutex + + private struct State: @unchecked Sendable { + var value: T + } + + /// Creates a new instance. + /// - Parameter value: The initial value. + public init(_ value: T) { + self.lock = Mutex(State(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(_ body: (inout T) throws -> R) rethrows -> R { + try lock.withLock { state in + try body(&state.value) + } + } +} diff --git a/Sources/SendablePropertyMacros/SendablePropertyError.swift b/Sources/SendablePropertyMacros/SendablePropertyError.swift index 29042fe8..41ca6e2d 100644 --- a/Sources/SendablePropertyMacros/SendablePropertyError.swift +++ b/Sources/SendablePropertyMacros/SendablePropertyError.swift @@ -18,11 +18,13 @@ enum SendablePropertyError: CustomStringConvertible, Error { case unexpectedError case onlyApplicableToVar + case notApplicableToType var description: String { switch self { - case .unexpectedError: return "@SendableProperty encountered an unexpected error" - case .onlyApplicableToVar: return "@SendableProperty can only be applied to a variable" + case .unexpectedError: return "The macro encountered an unexpected error" + case .onlyApplicableToVar: return "The macro can only be applied to a variable" + case .notApplicableToType: return "The macro can't be applied to a variable of this type" } } } diff --git a/Sources/SendablePropertyMacros/SendablePropertyMacro.swift b/Sources/SendablePropertyMacros/SendablePropertyMacro.swift index 78b5f3fb..2a2667a2 100644 --- a/Sources/SendablePropertyMacros/SendablePropertyMacro.swift +++ b/Sources/SendablePropertyMacros/SendablePropertyMacro.swift @@ -21,10 +21,36 @@ import SwiftSyntax import SwiftSyntaxBuilder import SwiftSyntaxMacros -/// A macro that allows to make a property thread-safe keeping the `Sendable` conformance of the type. +/// A macro that allows to make a property of a supported type thread-safe keeping the `Sendable` conformance of the type. public struct SendablePropertyMacro: PeerMacro { - private static func peerPropertyName(for propertyName: String) -> String { - "_" + propertyName + private static let allowedTypes: Set = [ + "Int", "UInt", "Int16", "UInt16", "Int32", "UInt32", "Int64", "UInt64", "Float", "Double", "Bool", "UnsafeRawPointer", "UnsafeMutableRawPointer", "UnsafePointer", + "UnsafeMutablePointer", + ] + + private static func checkPropertyType(in declaration: some DeclSyntaxProtocol) throws { + guard let varDecl = declaration.as(VariableDeclSyntax.self), + let binding = varDecl.bindings.first, + let typeAnnotation = binding.typeAnnotation, + let id = typeAnnotation.type.as(IdentifierTypeSyntax.self) + else { + // Nothing to check. + return + } + + var typeName = id.name.text + // Allow optionals of the allowed types. + if typeName.prefix(9) == "Optional<" && typeName.suffix(1) == ">" { + typeName = String(typeName.dropFirst(9).dropLast(1)) + } + // Allow generics of the allowed types. + if typeName.contains("<") { + typeName = String(typeName.prefix { $0 != "<" }) + } + + guard allowedTypes.contains(typeName) else { + throw SendablePropertyError.notApplicableToType + } } /// The macro expansion that introduces a `Sendable`-conforming "peer" declaration for a thread-safe storage for the value of the given declaration of a variable. @@ -35,32 +61,8 @@ public struct SendablePropertyMacro: PeerMacro { public static func expansion( of node: SwiftSyntax.AttributeSyntax, providingPeersOf declaration: some SwiftSyntax.DeclSyntaxProtocol, in context: some SwiftSyntaxMacros.MacroExpansionContext ) throws -> [SwiftSyntax.DeclSyntax] { - guard let varDecl = declaration.as(VariableDeclSyntax.self), - let binding = varDecl.bindings.first, - let pattern = binding.pattern.as(IdentifierPatternSyntax.self) - else { - throw SendablePropertyError.onlyApplicableToVar - } - - let propertyName = pattern.identifier.text - let hasInitializer = binding.initializer != nil - let initializerValue = binding.initializer?.value.description ?? "nil" - - var genericTypeAnnotation = "" - if let typeAnnotation = binding.typeAnnotation { - let typeName = typeAnnotation.type.description.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) - 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) - let peerProperty: DeclSyntax = - """ - \(raw: accessLevel) let \(raw: peerPropertyName) = Synchronized\(raw: genericTypeAnnotation)(\(raw: initializerValue)) - """ - return [peerProperty] + try checkPropertyType(in: declaration) + return try SendablePropertyMacroUnchecked.expansion(of: node, providingPeersOf: declaration, in: context) } } @@ -73,32 +75,7 @@ extension SendablePropertyMacro: AccessorMacro { public static func expansion( of node: SwiftSyntax.AttributeSyntax, providingAccessorsOf declaration: some SwiftSyntax.DeclSyntaxProtocol, in context: some SwiftSyntaxMacros.MacroExpansionContext ) throws -> [SwiftSyntax.AccessorDeclSyntax] { - guard let varDecl = declaration.as(VariableDeclSyntax.self), - let binding = varDecl.bindings.first, - let pattern = binding.pattern.as(IdentifierPatternSyntax.self) - else { - throw SendablePropertyError.onlyApplicableToVar - } - - let propertyName = pattern.identifier.text - let hasInitializer = binding.initializer != nil - - // Replace the property with an accessor - let peerPropertyName = Self.peerPropertyName(for: propertyName) - - let accessorGetter: AccessorDeclSyntax = - """ - get { - \(raw: peerPropertyName).withLock { $0\(raw: hasInitializer ? "" : "!") } - } - """ - let accessorSetter: AccessorDeclSyntax = - """ - set { - \(raw: peerPropertyName).withLock { $0 = newValue } - } - """ - - return [accessorGetter, accessorSetter] + try checkPropertyType(in: declaration) + return try SendablePropertyMacroUnchecked.expansion(of: node, providingAccessorsOf: declaration, in: context) } } diff --git a/Sources/SendablePropertyMacros/SendablePropertyMacroUnchecked.swift b/Sources/SendablePropertyMacros/SendablePropertyMacroUnchecked.swift new file mode 100644 index 00000000..0330575a --- /dev/null +++ b/Sources/SendablePropertyMacros/SendablePropertyMacroUnchecked.swift @@ -0,0 +1,104 @@ +//===----------------------------------------------------------------------===// +// Copyright © 2025 Apple Inc. and the Containerization project authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//===----------------------------------------------------------------------===// + +import Foundation +import SwiftCompilerPlugin +import SwiftParser +import SwiftSyntax +import SwiftSyntaxBuilder +import SwiftSyntaxMacros + +/// A macro that allows to make a property of a custom type thread-safe keeping the `Sendable` conformance of the type. This macro can be used with classes and enums. Avoid using it with structs, arrays, and dictionaries. +public struct SendablePropertyMacroUnchecked: PeerMacro { + private static func peerPropertyName(for propertyName: String) -> String { + "_" + propertyName + } + + /// The macro expansion that introduces a `Sendable`-conforming "peer" declaration for a thread-safe storage for the value of the given declaration of a variable. + /// - Parameters: + /// - node: The given attribute node. + /// - declaration: The given declaration. + /// - context: The macro expansion context. + public static func expansion( + of node: SwiftSyntax.AttributeSyntax, providingPeersOf declaration: some SwiftSyntax.DeclSyntaxProtocol, in context: some SwiftSyntaxMacros.MacroExpansionContext + ) throws -> [SwiftSyntax.DeclSyntax] { + guard let varDecl = declaration.as(VariableDeclSyntax.self), + let binding = varDecl.bindings.first, + let pattern = binding.pattern.as(IdentifierPatternSyntax.self) + else { + throw SendablePropertyError.onlyApplicableToVar + } + + let propertyName = pattern.identifier.text + let hasInitializer = binding.initializer != nil + let initializerValue = binding.initializer?.value.description ?? "nil" + + var genericTypeAnnotation = "" + if let typeAnnotation = binding.typeAnnotation { + let typeName = typeAnnotation.type.description.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) + 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) + let peerProperty: DeclSyntax = + """ + \(raw: accessLevel) let \(raw: peerPropertyName) = Synchronized\(raw: genericTypeAnnotation)(\(raw: initializerValue)) + """ + return [peerProperty] + } +} + +extension SendablePropertyMacroUnchecked: AccessorMacro { + /// The macro expansion that adds `Sendable`-conforming accessors to the given declaration of a variable. + /// - Parameters: + /// - node: The given attribute node. + /// - declaration: The given declaration. + /// - context: The macro expansion context. + public static func expansion( + of node: SwiftSyntax.AttributeSyntax, providingAccessorsOf declaration: some SwiftSyntax.DeclSyntaxProtocol, in context: some SwiftSyntaxMacros.MacroExpansionContext + ) throws -> [SwiftSyntax.AccessorDeclSyntax] { + guard let varDecl = declaration.as(VariableDeclSyntax.self), + let binding = varDecl.bindings.first, + let pattern = binding.pattern.as(IdentifierPatternSyntax.self) + else { + throw SendablePropertyError.onlyApplicableToVar + } + + let propertyName = pattern.identifier.text + let hasInitializer = binding.initializer != nil + + // Replace the property with an accessor + let peerPropertyName = Self.peerPropertyName(for: propertyName) + + let accessorGetter: AccessorDeclSyntax = + """ + get { + \(raw: peerPropertyName).withLock { $0\(raw: hasInitializer ? "" : "!") } + } + """ + let accessorSetter: AccessorDeclSyntax = + """ + set { + \(raw: peerPropertyName).withLock { $0 = newValue } + } + """ + + return [accessorGetter, accessorSetter] + } +} diff --git a/Sources/SendablePropertyMacros/SendablePropertyPlugin.swift b/Sources/SendablePropertyMacros/SendablePropertyPlugin.swift index 6920eecc..bbf069c8 100644 --- a/Sources/SendablePropertyMacros/SendablePropertyPlugin.swift +++ b/Sources/SendablePropertyMacros/SendablePropertyPlugin.swift @@ -17,10 +17,11 @@ import SwiftCompilerPlugin import SwiftSyntaxMacros -/// A plugin that registers the `SendablePropertyMacro`. +/// A plugin that registers the `SendablePropertyMacroUnchecked` and `SendablePropertyMacro`. @main struct SendablePropertyPlugin: CompilerPlugin { let providingMacros: [Macro.Type] = [ - SendablePropertyMacro.self + SendablePropertyMacroUnchecked.self, + SendablePropertyMacro.self, ] } diff --git a/Tests/SendablePropertyTests/SendablePropertyTests.swift b/Tests/SendablePropertyTests/SendablePropertyTests.swift index 1962a90d..b36df490 100644 --- a/Tests/SendablePropertyTests/SendablePropertyTests.swift +++ b/Tests/SendablePropertyTests/SendablePropertyTests.swift @@ -76,4 +76,70 @@ final class SendablePropertyTests: XCTestCase { } dispatchGroup.wait() } + + func testMacroWithSupportedTypes() throws { + final class TestMacro: Sendable { + @SendableProperty + var int: Int + @SendableProperty + var uint: UInt + @SendableProperty + var int16: Int16 + @SendableProperty + var uint16: UInt16 + @SendableProperty + var int32: Int32 + @SendableProperty + var uint32: UInt32 + @SendableProperty + var int64: Int64 + @SendableProperty + var uint64: UInt64 + @SendableProperty + var float: Float + @SendableProperty + var double: Double + @SendableProperty + var bool: Bool + @SendableProperty + var unsafeRawPoiner: UnsafeRawPointer + @SendableProperty + var unsafeMutableRawPointer: UnsafeMutableRawPointer + @SendableProperty + var unsafePoiner: UnsafePointer + @SendableProperty + var unsafeMutablePointer: UnsafeMutablePointer + + @SendableProperty + var intOpt: Int? + @SendableProperty + var uintOpt: UInt? + @SendableProperty + var int16Opt: Int16? + @SendableProperty + var uint16Opt: UInt16? + @SendableProperty + var int32Opt: Int32? + @SendableProperty + var uint32Opt: UInt32? + @SendableProperty + var int64Opt: Int64? + @SendableProperty + var uint64Opt: UInt64? + @SendableProperty + var floaOptt: Float? + @SendableProperty + var doubleOpt: Double? + @SendableProperty + var boolOpt: Bool? + @SendableProperty + var unsafeRawPoinerOpt: UnsafeRawPointer? + @SendableProperty + var unsafeMutableRawPointerOpt: UnsafeMutableRawPointer? + @SendableProperty + var unsafePoinerOpt: UnsafePointer? + @SendableProperty + var unsafeMutablePointerOpt: UnsafeMutablePointer? + } + } }