Skip to content

Commit 41d41e4

Browse files
authored
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
1 parent ab55975 commit 41d41e4

9 files changed

Lines changed: 272 additions & 87 deletions

File tree

Sources/Containerization/LinuxContainer.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ public final class LinuxContainer: Container, Sendable {
5454
var dns: DNS? = nil
5555
}
5656

57-
@SendableProperty
57+
@SendablePropertyUnchecked
5858
private var state: State
5959

6060
private let config: Mutex<Configuration>

Sources/SendableProperty/SendableProperty.swift

Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -14,33 +14,7 @@
1414
// limitations under the License.
1515
//===----------------------------------------------------------------------===//
1616

17-
// `Synchronization` will be automatically imported with `SendableProperty`.
18-
@_exported import Synchronization
19-
2017
// A declaration of the `@SendableProperty` macro.
2118
@attached(peer, names: arbitrary)
2219
@attached(accessor)
2320
public macro SendableProperty() = #externalMacro(module: "SendablePropertyMacros", type: "SendablePropertyMacro")
24-
25-
/// A synchronization primitive that protects shared mutable state via mutual exclusion.
26-
public final class Synchronized<T>: Sendable {
27-
private let lock: Mutex<State>
28-
29-
private struct State: @unchecked Sendable {
30-
var value: T
31-
}
32-
33-
/// Creates a new instance.
34-
/// - Parameter value: The initial value.
35-
public init(_ value: T) {
36-
self.lock = Mutex(State(value: value))
37-
}
38-
39-
/// Calls the given closure after acquiring the lock and returns its value.
40-
/// - Parameter body: The body of code to execute while the lock is held.
41-
public func withLock<R>(_ body: (inout T) throws -> R) rethrows -> R {
42-
try lock.withLock { state in
43-
try body(&state.value)
44-
}
45-
}
46-
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
//===----------------------------------------------------------------------===//
2+
// Copyright © 2025 Apple Inc. and the Containerization project authors. All rights reserved.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// https://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
//===----------------------------------------------------------------------===//
16+
17+
// A declaration of the `@SendablePropertyUnchecked` macro.
18+
@attached(peer, names: arbitrary)
19+
@attached(accessor)
20+
public macro SendablePropertyUnchecked() = #externalMacro(module: "SendablePropertyMacros", type: "SendablePropertyMacroUnchecked")
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
//===----------------------------------------------------------------------===//
2+
// Copyright © 2025 Apple Inc. and the Containerization project authors. All rights reserved.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// https://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
//===----------------------------------------------------------------------===//
16+
17+
// `Synchronization` will be automatically imported with `SendableProperty`.
18+
@_exported import Synchronization
19+
20+
/// A synchronization primitive that protects shared mutable state via mutual exclusion.
21+
public final class Synchronized<T>: Sendable {
22+
private let lock: Mutex<State>
23+
24+
private struct State: @unchecked Sendable {
25+
var value: T
26+
}
27+
28+
/// Creates a new instance.
29+
/// - Parameter value: The initial value.
30+
public init(_ value: T) {
31+
self.lock = Mutex(State(value: value))
32+
}
33+
34+
/// Calls the given closure after acquiring the lock and returns its value.
35+
/// - Parameter body: The body of code to execute while the lock is held.
36+
public func withLock<R>(_ body: (inout T) throws -> R) rethrows -> R {
37+
try lock.withLock { state in
38+
try body(&state.value)
39+
}
40+
}
41+
}

Sources/SendablePropertyMacros/SendablePropertyError.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,13 @@
1818
enum SendablePropertyError: CustomStringConvertible, Error {
1919
case unexpectedError
2020
case onlyApplicableToVar
21+
case notApplicableToType
2122

2223
var description: String {
2324
switch self {
24-
case .unexpectedError: return "@SendableProperty encountered an unexpected error"
25-
case .onlyApplicableToVar: return "@SendableProperty can only be applied to a variable"
25+
case .unexpectedError: return "The macro encountered an unexpected error"
26+
case .onlyApplicableToVar: return "The macro can only be applied to a variable"
27+
case .notApplicableToType: return "The macro can't be applied to a variable of this type"
2628
}
2729
}
2830
}

Sources/SendablePropertyMacros/SendablePropertyMacro.swift

Lines changed: 33 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,36 @@ import SwiftSyntax
2121
import SwiftSyntaxBuilder
2222
import SwiftSyntaxMacros
2323

24-
/// A macro that allows to make a property thread-safe keeping the `Sendable` conformance of the type.
24+
/// A macro that allows to make a property of a supported type thread-safe keeping the `Sendable` conformance of the type.
2525
public struct SendablePropertyMacro: PeerMacro {
26-
private static func peerPropertyName(for propertyName: String) -> String {
27-
"_" + propertyName
26+
private static let allowedTypes: Set<String> = [
27+
"Int", "UInt", "Int16", "UInt16", "Int32", "UInt32", "Int64", "UInt64", "Float", "Double", "Bool", "UnsafeRawPointer", "UnsafeMutableRawPointer", "UnsafePointer",
28+
"UnsafeMutablePointer",
29+
]
30+
31+
private static func checkPropertyType(in declaration: some DeclSyntaxProtocol) throws {
32+
guard let varDecl = declaration.as(VariableDeclSyntax.self),
33+
let binding = varDecl.bindings.first,
34+
let typeAnnotation = binding.typeAnnotation,
35+
let id = typeAnnotation.type.as(IdentifierTypeSyntax.self)
36+
else {
37+
// Nothing to check.
38+
return
39+
}
40+
41+
var typeName = id.name.text
42+
// Allow optionals of the allowed types.
43+
if typeName.prefix(9) == "Optional<" && typeName.suffix(1) == ">" {
44+
typeName = String(typeName.dropFirst(9).dropLast(1))
45+
}
46+
// Allow generics of the allowed types.
47+
if typeName.contains("<") {
48+
typeName = String(typeName.prefix { $0 != "<" })
49+
}
50+
51+
guard allowedTypes.contains(typeName) else {
52+
throw SendablePropertyError.notApplicableToType
53+
}
2854
}
2955

3056
/// 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 {
3561
public static func expansion(
3662
of node: SwiftSyntax.AttributeSyntax, providingPeersOf declaration: some SwiftSyntax.DeclSyntaxProtocol, in context: some SwiftSyntaxMacros.MacroExpansionContext
3763
) throws -> [SwiftSyntax.DeclSyntax] {
38-
guard let varDecl = declaration.as(VariableDeclSyntax.self),
39-
let binding = varDecl.bindings.first,
40-
let pattern = binding.pattern.as(IdentifierPatternSyntax.self)
41-
else {
42-
throw SendablePropertyError.onlyApplicableToVar
43-
}
44-
45-
let propertyName = pattern.identifier.text
46-
let hasInitializer = binding.initializer != nil
47-
let initializerValue = binding.initializer?.value.description ?? "nil"
48-
49-
var genericTypeAnnotation = ""
50-
if let typeAnnotation = binding.typeAnnotation {
51-
let typeName = typeAnnotation.type.description.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
52-
genericTypeAnnotation = "<\(typeName)\(hasInitializer ? "" : "?")>"
53-
}
54-
55-
let accessLevel = varDecl.modifiers.first(where: { ["open", "public", "internal", "fileprivate", "private"].contains($0.name.text) })?.name.text ?? "internal"
56-
57-
// Create a peer property
58-
let peerPropertyName = self.peerPropertyName(for: propertyName)
59-
let peerProperty: DeclSyntax =
60-
"""
61-
\(raw: accessLevel) let \(raw: peerPropertyName) = Synchronized\(raw: genericTypeAnnotation)(\(raw: initializerValue))
62-
"""
63-
return [peerProperty]
64+
try checkPropertyType(in: declaration)
65+
return try SendablePropertyMacroUnchecked.expansion(of: node, providingPeersOf: declaration, in: context)
6466
}
6567
}
6668

@@ -73,32 +75,7 @@ extension SendablePropertyMacro: AccessorMacro {
7375
public static func expansion(
7476
of node: SwiftSyntax.AttributeSyntax, providingAccessorsOf declaration: some SwiftSyntax.DeclSyntaxProtocol, in context: some SwiftSyntaxMacros.MacroExpansionContext
7577
) throws -> [SwiftSyntax.AccessorDeclSyntax] {
76-
guard let varDecl = declaration.as(VariableDeclSyntax.self),
77-
let binding = varDecl.bindings.first,
78-
let pattern = binding.pattern.as(IdentifierPatternSyntax.self)
79-
else {
80-
throw SendablePropertyError.onlyApplicableToVar
81-
}
82-
83-
let propertyName = pattern.identifier.text
84-
let hasInitializer = binding.initializer != nil
85-
86-
// Replace the property with an accessor
87-
let peerPropertyName = Self.peerPropertyName(for: propertyName)
88-
89-
let accessorGetter: AccessorDeclSyntax =
90-
"""
91-
get {
92-
\(raw: peerPropertyName).withLock { $0\(raw: hasInitializer ? "" : "!") }
93-
}
94-
"""
95-
let accessorSetter: AccessorDeclSyntax =
96-
"""
97-
set {
98-
\(raw: peerPropertyName).withLock { $0 = newValue }
99-
}
100-
"""
101-
102-
return [accessorGetter, accessorSetter]
78+
try checkPropertyType(in: declaration)
79+
return try SendablePropertyMacroUnchecked.expansion(of: node, providingAccessorsOf: declaration, in: context)
10380
}
10481
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
//===----------------------------------------------------------------------===//
2+
// Copyright © 2025 Apple Inc. and the Containerization project authors. All rights reserved.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// https://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
//===----------------------------------------------------------------------===//
16+
17+
import Foundation
18+
import SwiftCompilerPlugin
19+
import SwiftParser
20+
import SwiftSyntax
21+
import SwiftSyntaxBuilder
22+
import SwiftSyntaxMacros
23+
24+
/// 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.
25+
public struct SendablePropertyMacroUnchecked: PeerMacro {
26+
private static func peerPropertyName(for propertyName: String) -> String {
27+
"_" + propertyName
28+
}
29+
30+
/// 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.
31+
/// - Parameters:
32+
/// - node: The given attribute node.
33+
/// - declaration: The given declaration.
34+
/// - context: The macro expansion context.
35+
public static func expansion(
36+
of node: SwiftSyntax.AttributeSyntax, providingPeersOf declaration: some SwiftSyntax.DeclSyntaxProtocol, in context: some SwiftSyntaxMacros.MacroExpansionContext
37+
) throws -> [SwiftSyntax.DeclSyntax] {
38+
guard let varDecl = declaration.as(VariableDeclSyntax.self),
39+
let binding = varDecl.bindings.first,
40+
let pattern = binding.pattern.as(IdentifierPatternSyntax.self)
41+
else {
42+
throw SendablePropertyError.onlyApplicableToVar
43+
}
44+
45+
let propertyName = pattern.identifier.text
46+
let hasInitializer = binding.initializer != nil
47+
let initializerValue = binding.initializer?.value.description ?? "nil"
48+
49+
var genericTypeAnnotation = ""
50+
if let typeAnnotation = binding.typeAnnotation {
51+
let typeName = typeAnnotation.type.description.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
52+
genericTypeAnnotation = "<\(typeName)\(hasInitializer ? "" : "?")>"
53+
}
54+
55+
let accessLevel = varDecl.modifiers.first(where: { ["open", "public", "internal", "fileprivate", "private"].contains($0.name.text) })?.name.text ?? "internal"
56+
57+
// Create a peer property
58+
let peerPropertyName = self.peerPropertyName(for: propertyName)
59+
let peerProperty: DeclSyntax =
60+
"""
61+
\(raw: accessLevel) let \(raw: peerPropertyName) = Synchronized\(raw: genericTypeAnnotation)(\(raw: initializerValue))
62+
"""
63+
return [peerProperty]
64+
}
65+
}
66+
67+
extension SendablePropertyMacroUnchecked: AccessorMacro {
68+
/// The macro expansion that adds `Sendable`-conforming accessors to the given declaration of a variable.
69+
/// - Parameters:
70+
/// - node: The given attribute node.
71+
/// - declaration: The given declaration.
72+
/// - context: The macro expansion context.
73+
public static func expansion(
74+
of node: SwiftSyntax.AttributeSyntax, providingAccessorsOf declaration: some SwiftSyntax.DeclSyntaxProtocol, in context: some SwiftSyntaxMacros.MacroExpansionContext
75+
) throws -> [SwiftSyntax.AccessorDeclSyntax] {
76+
guard let varDecl = declaration.as(VariableDeclSyntax.self),
77+
let binding = varDecl.bindings.first,
78+
let pattern = binding.pattern.as(IdentifierPatternSyntax.self)
79+
else {
80+
throw SendablePropertyError.onlyApplicableToVar
81+
}
82+
83+
let propertyName = pattern.identifier.text
84+
let hasInitializer = binding.initializer != nil
85+
86+
// Replace the property with an accessor
87+
let peerPropertyName = Self.peerPropertyName(for: propertyName)
88+
89+
let accessorGetter: AccessorDeclSyntax =
90+
"""
91+
get {
92+
\(raw: peerPropertyName).withLock { $0\(raw: hasInitializer ? "" : "!") }
93+
}
94+
"""
95+
let accessorSetter: AccessorDeclSyntax =
96+
"""
97+
set {
98+
\(raw: peerPropertyName).withLock { $0 = newValue }
99+
}
100+
"""
101+
102+
return [accessorGetter, accessorSetter]
103+
}
104+
}

Sources/SendablePropertyMacros/SendablePropertyPlugin.swift

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,11 @@
1717
import SwiftCompilerPlugin
1818
import SwiftSyntaxMacros
1919

20-
/// A plugin that registers the `SendablePropertyMacro`.
20+
/// A plugin that registers the `SendablePropertyMacroUnchecked` and `SendablePropertyMacro`.
2121
@main
2222
struct SendablePropertyPlugin: CompilerPlugin {
2323
let providingMacros: [Macro.Type] = [
24-
SendablePropertyMacro.self
24+
SendablePropertyMacroUnchecked.self,
25+
SendablePropertyMacro.self,
2526
]
2627
}

0 commit comments

Comments
 (0)