Skip to content

Commit 48f81c8

Browse files
committed
[Swiftify] Add _SwiftifyImportProtocol for safe overloads for protocols
The existing _SwiftifyImport macro is a peer macro, limiting it to only emitting function wrappers in the same scope as the original function. Protocols cannot contain function implementations, so these need to be placed in a separate protocol extension instead. _SwiftifyImportProtocol is an extension macro rather than a peer macro, to enable this functionality. Rather than operating on a single function, like _SwiftifyImport, _SwiftifyImportProtocol takes information about multiple methods and creates a single protocol extension with all wrappers in a one-shot operation. rdar://144335990
1 parent 2b7adbc commit 48f81c8

File tree

7 files changed

+246
-0
lines changed

7 files changed

+246
-0
lines changed

lib/Macros/Sources/SwiftMacros/SwiftifyImportMacro.swift

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1730,6 +1730,106 @@ public struct SwiftifyImportMacro: PeerMacro {
17301730
}
17311731
}
17321732

1733+
func parseProtocolMacroParam(
1734+
_ paramAST: LabeledExprSyntax,
1735+
methods: [String: FunctionDeclSyntax]
1736+
) throws -> (FunctionDeclSyntax, [ExprSyntax]) {
1737+
let paramExpr = paramAST.expression
1738+
guard let enumConstructorExpr = paramExpr.as(FunctionCallExprSyntax.self) else {
1739+
throw DiagnosticError(
1740+
"expected _SwiftifyProtocolMethodInfo enum literal as argument, got '\(paramExpr)'", node: paramExpr)
1741+
}
1742+
let enumName = try parseEnumName(paramExpr)
1743+
if enumName != "method" {
1744+
throw DiagnosticError(
1745+
"expected 'method', got '\(enumName)'",
1746+
node: enumConstructorExpr)
1747+
}
1748+
let argumentList = enumConstructorExpr.arguments
1749+
let methodSignatureArg = try getArgumentByName(argumentList, "signature")
1750+
guard let methodSignatureStringLit = methodSignatureArg.as(StringLiteralExprSyntax.self) else {
1751+
throw DiagnosticError(
1752+
"expected string literal for 'signature' parameter, got \(methodSignatureArg)", node: methodSignatureArg)
1753+
}
1754+
let methodSignature = methodSignatureStringLit.representedLiteralValue!
1755+
guard let methodSyntax = methods[methodSignature] else {
1756+
var notes: [Note] = []
1757+
var name = methodSignature
1758+
if let methodSyntax = DeclSyntax("\(raw: methodSignature)").as(FunctionDeclSyntax.self) {
1759+
name = methodSyntax.name.trimmed.text
1760+
}
1761+
for (tmp, method) in methods where method.name.trimmed.text == name {
1762+
notes.append(Note(node: Syntax(method.name), message: MacroExpansionNoteMessage("did you mean '\(method.trimmed.description)'?")))
1763+
}
1764+
throw DiagnosticError(
1765+
"method with signature '\(methodSignature)' not found in protocol", node: methodSignatureArg, notes: notes)
1766+
}
1767+
let paramInfoArg = try getArgumentByName(argumentList, "paramInfo")
1768+
guard let paramInfoArgList = paramInfoArg.as(ArrayExprSyntax.self) else {
1769+
throw DiagnosticError("expected array literal for 'paramInfo' parameter, got \(paramInfoArg)", node: paramInfoArg)
1770+
}
1771+
return (methodSyntax, paramInfoArgList.elements.map { ExprSyntax($0.expression) })
1772+
}
1773+
1774+
/// Similar to SwiftifyImportMacro, but for providing overloads to methods in
1775+
/// protocols using an extension, rather than in the same scope as the original.
1776+
public struct SwiftifyImportProtocolMacro: ExtensionMacro {
1777+
public static func expansion(
1778+
of node: AttributeSyntax,
1779+
attachedTo declaration: some DeclGroupSyntax,
1780+
providingExtensionsOf type: some TypeSyntaxProtocol,
1781+
conformingTo protocols: [TypeSyntax],
1782+
in context: some MacroExpansionContext
1783+
) throws -> [ExtensionDeclSyntax] {
1784+
do {
1785+
guard let protocolDecl = declaration.as(ProtocolDeclSyntax.self) else {
1786+
throw DiagnosticError("@_SwiftifyImportProtocol only works on protocols", node: declaration)
1787+
}
1788+
let argumentList = node.arguments!.as(LabeledExprListSyntax.self)!
1789+
var arguments = [LabeledExprSyntax](argumentList)
1790+
let typeMappings = try parseTypeMappingParam(arguments.last)
1791+
if typeMappings != nil {
1792+
arguments = arguments.dropLast()
1793+
}
1794+
let spanAvailability = try parseSpanAvailabilityParam(arguments.last)
1795+
if spanAvailability != nil {
1796+
arguments = arguments.dropLast()
1797+
}
1798+
1799+
var methods: [String: FunctionDeclSyntax] = [:]
1800+
for member in protocolDecl.memberBlock.members {
1801+
guard let methodDecl = member.decl.as(FunctionDeclSyntax.self) else {
1802+
continue
1803+
}
1804+
let trimmedDecl = methodDecl.with(\.body, nil)
1805+
.with(\.attributes, [])
1806+
.trimmed
1807+
methods[trimmedDecl.description] = methodDecl
1808+
}
1809+
let overloads = try arguments.map {
1810+
let (method, args) = try parseProtocolMacroParam($0, methods: methods)
1811+
let function = try constructOverloadFunction(
1812+
forDecl: method, leadingTrivia: Trivia(), args: args,
1813+
spanAvailability: spanAvailability,
1814+
typeMappings: typeMappings)
1815+
return MemberBlockItemSyntax(decl: function)
1816+
}
1817+
1818+
return [ExtensionDeclSyntax(extensionKeyword: .identifier("extension"), extendedType: type,
1819+
memberBlock: MemberBlockSyntax(leftBrace: .leftBraceToken(),
1820+
members: MemberBlockItemListSyntax(overloads),
1821+
rightBrace: .rightBraceToken())
1822+
)]
1823+
} catch let error as DiagnosticError {
1824+
context.diagnose(
1825+
Diagnostic(
1826+
node: error.node, message: MacroExpansionErrorMessage(error.description),
1827+
notes: error.notes))
1828+
return []
1829+
}
1830+
}
1831+
}
1832+
17331833
// MARK: syntax utils
17341834
extension TypeSyntaxProtocol {
17351835
public var isSwiftCoreModule: Bool {

stdlib/public/core/SwiftifyImport.swift

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,30 @@ public macro _SwiftifyImport(_ paramInfo: _SwiftifyInfo...,
6666
#externalMacro(module: "SwiftMacros", type: "SwiftifyImportMacro")
6767
#endif
6868

69+
/// Allows annotating pointer parameters in a protocol method using the `@_SwiftifyImportProtocol` macro.
70+
///
71+
/// This is not marked @available, because _SwiftifyImportProtocolMethod is available for any target.
72+
/// Instances of _SwiftifyProtocolMethodInfo should ONLY be passed as arguments directly to
73+
/// _SwiftifyImportProtocolMethod, so they should not affect linkage since there are never any instances
74+
/// at runtime.
75+
public enum _SwiftifyProtocolMethodInfo {
76+
case method(signature: String, paramInfo: [_SwiftifyInfo])
77+
}
78+
79+
/// Like _SwiftifyImport, but since protocols cannot contain function implementations they need to
80+
/// be placed in a separate extension instead. Unlike _SwiftifyImport, which applies to a single
81+
/// function, this macro supports feeding info about multiple methods and generating safe overloads
82+
/// for all of them at once.
83+
#if hasFeature(Macros)
84+
@attached(extension, names: arbitrary)
85+
public macro _SwiftifyImportProtocol(
86+
_ methodInfo: _SwiftifyProtocolMethodInfo...,
87+
spanAvailability: String? = nil,
88+
typeMappings: [String: String] = [:]
89+
) =
90+
#externalMacro(module: "SwiftMacros", type: "SwiftifyImportProtocolMacro")
91+
#endif
92+
6993
/// Unsafely discard any lifetime dependency on the `dependent` argument. Return
7094
/// a value identical to `dependent` with a lifetime dependency on the caller's
7195
/// borrow scope of the `source` argument.
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
// REQUIRES: swift_swift_parser
2+
// REQUIRES: swift_feature_Lifetimes
3+
4+
// RUN: %empty-directory(%t)
5+
// RUN: split-file %s %t
6+
7+
// RUN: %target-swift-frontend %t/test.swift -emit-module -plugin-path %swift-plugin-dir -enable-experimental-feature Lifetimes -verify
8+
// RUN: %target-swift-frontend %t/test.swift -typecheck -plugin-path %swift-plugin-dir -enable-experimental-feature Lifetimes -dump-macro-expansions 2> %t/expansions.out
9+
// RUN: %diff %t/expansions.out %t/expansions.expected
10+
11+
//--- test.swift
12+
@_SwiftifyImportProtocol(.method(signature: "func myFunc(_ ptr: UnsafePointer<CInt>, _ len: CInt)", paramInfo: [.countedBy(pointer: .param(1), count: "len")]))
13+
protocol SimpleProtocol {
14+
func myFunc(_ ptr: UnsafePointer<CInt>, _ len: CInt)
15+
}
16+
17+
@_SwiftifyImportProtocol(.method(signature: "func foo(_ ptr: UnsafePointer<CInt>, _ len: CInt)", paramInfo: [.countedBy(pointer: .param(1), count: "len"), .nonescaping(pointer: .param(1))]),
18+
.method(signature: "func bar(_ len: CInt) -> UnsafePointer<CInt>", paramInfo: [.countedBy(pointer: .return, count: "len"), .nonescaping(pointer: .return), .lifetimeDependence(dependsOn: .self, pointer: .return, type: .borrow)]))
19+
protocol SpanProtocol {
20+
func foo(_ ptr: UnsafePointer<CInt>, _ len: CInt)
21+
func bar(_ len: CInt) -> UnsafePointer<CInt>
22+
}
23+
24+
@_SwiftifyImportProtocol(.method(signature: "func foo(_ ptr: UnsafePointer<CInt>, _ len: CInt)", paramInfo: [.countedBy(pointer: .param(1), count: "len"), .nonescaping(pointer: .param(1))]),
25+
.method(signature: "func bar(_ ptr: UnsafePointer<CInt>, _ len: CInt)", paramInfo: [.countedBy(pointer: .param(1), count: "len")]))
26+
protocol MixedProtocol {
27+
/// Some doc comment
28+
func foo(_ ptr: UnsafePointer<CInt>, _ len: CInt)
29+
func bar(_ ptr: UnsafePointer<CInt>, _ len: CInt)
30+
}
31+
32+
@_SwiftifyImportProtocol(.method(signature: "func foo(_ ptr: UnsafePointer<CInt>, _ len1: CInt)", paramInfo: [.countedBy(pointer: .param(1), count: "len1")]),
33+
.method(signature: "func foo(bar: UnsafePointer<CInt>, _ len2: CInt)", paramInfo: [.countedBy(pointer: .param(1), count: "len2")]))
34+
protocol OverloadedProtocol {
35+
func foo(_ ptr: UnsafePointer<CInt>, _ len1: CInt)
36+
func foo(bar: UnsafePointer<CInt>, _ len2: CInt)
37+
func foo()
38+
}
39+
40+
//--- expansions.expected
41+
@__swiftmacro_4test14SimpleProtocol015_SwiftifyImportC0fMe_.swift
42+
------------------------------
43+
extension SimpleProtocol {
44+
/// This is an auto-generated wrapper for safer interop
45+
@_alwaysEmitIntoClient @_disfavoredOverload
46+
func myFunc(_ ptr: UnsafeBufferPointer<CInt>) {
47+
let len = CInt(exactly: ptr.count)!
48+
return unsafe myFunc(ptr.baseAddress!, len)
49+
}
50+
}
51+
------------------------------
52+
@__swiftmacro_4test12SpanProtocol015_SwiftifyImportC0fMe_.swift
53+
------------------------------
54+
extension SpanProtocol {
55+
/// This is an auto-generated wrapper for safer interop
56+
@_alwaysEmitIntoClient @_disfavoredOverload
57+
func foo(_ ptr: Span<CInt>) {
58+
let len = CInt(exactly: ptr.count)!
59+
return unsafe ptr.withUnsafeBufferPointer { _ptrPtr in
60+
return unsafe foo(_ptrPtr.baseAddress!, len)
61+
}
62+
}
63+
/// This is an auto-generated wrapper for safer interop
64+
@_alwaysEmitIntoClient @_lifetime(borrow self) @_disfavoredOverload
65+
func bar(_ len: CInt) -> Span<CInt> {
66+
return unsafe _swiftifyOverrideLifetime(Span<CInt>(_unsafeStart: unsafe bar(len), count: Int(len)), copying: ())
67+
}
68+
}
69+
------------------------------
70+
@__swiftmacro_4test13MixedProtocol015_SwiftifyImportC0fMe_.swift
71+
------------------------------
72+
extension MixedProtocol {
73+
/// This is an auto-generated wrapper for safer interop
74+
@_alwaysEmitIntoClient @_disfavoredOverload
75+
/// Some doc comment
76+
func foo(_ ptr: Span<CInt>) {
77+
let len = CInt(exactly: ptr.count)!
78+
return unsafe ptr.withUnsafeBufferPointer { _ptrPtr in
79+
return unsafe foo(_ptrPtr.baseAddress!, len)
80+
}
81+
}
82+
/// This is an auto-generated wrapper for safer interop
83+
@_alwaysEmitIntoClient @_disfavoredOverload
84+
func bar(_ ptr: UnsafeBufferPointer<CInt>) {
85+
let len = CInt(exactly: ptr.count)!
86+
return unsafe bar(ptr.baseAddress!, len)
87+
}
88+
}
89+
------------------------------
90+
@__swiftmacro_4test18OverloadedProtocol015_SwiftifyImportC0fMe_.swift
91+
------------------------------
92+
extension OverloadedProtocol {
93+
/// This is an auto-generated wrapper for safer interop
94+
@_alwaysEmitIntoClient @_disfavoredOverload
95+
func foo(_ ptr: UnsafeBufferPointer<CInt>) {
96+
let len1 = CInt(exactly: ptr.count)!
97+
return unsafe foo(ptr.baseAddress!, len1)
98+
}
99+
/// This is an auto-generated wrapper for safer interop
100+
@_alwaysEmitIntoClient @_disfavoredOverload
101+
func foo(bar: UnsafeBufferPointer<CInt>) {
102+
let len2 = CInt(exactly: bar.count)!
103+
return unsafe foo(bar: bar.baseAddress!, len2)
104+
}
105+
}
106+
------------------------------

test/abi/Inputs/macOS/arm64/stdlib/baseline

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8937,6 +8937,10 @@ _$ss27_BidirectionalCollectionBoxCMu
89378937
_$ss27_BidirectionalCollectionBoxCfD
89388938
_$ss27_BidirectionalCollectionBoxCfd
89398939
_$ss27_BidirectionalCollectionBoxCy7ElementQzs09_AnyIndexC0_pcig
8940+
_$ss27_SwiftifyProtocolMethodInfoO6methodyABSS_Says01_aD0OGtcABmFWC
8941+
_$ss27_SwiftifyProtocolMethodInfoOMa
8942+
_$ss27_SwiftifyProtocolMethodInfoOMn
8943+
_$ss27_SwiftifyProtocolMethodInfoON
89408944
_$ss27_allocateUninitializedArrayySayxG_BptBwlF
89418945
_$ss27_bridgeAnythingToObjectiveCyyXlxlF
89428946
_$ss27_debuggerTestingCheckExpectyySS_SStF

test/abi/Inputs/macOS/arm64/stdlib/baseline-asserts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8949,6 +8949,10 @@ _$ss27_BidirectionalCollectionBoxCMu
89498949
_$ss27_BidirectionalCollectionBoxCfD
89508950
_$ss27_BidirectionalCollectionBoxCfd
89518951
_$ss27_BidirectionalCollectionBoxCy7ElementQzs09_AnyIndexC0_pcig
8952+
_$ss27_SwiftifyProtocolMethodInfoO6methodyABSS_Says01_aD0OGtcABmFWC
8953+
_$ss27_SwiftifyProtocolMethodInfoOMa
8954+
_$ss27_SwiftifyProtocolMethodInfoOMn
8955+
_$ss27_SwiftifyProtocolMethodInfoON
89528956
_$ss27_allocateUninitializedArrayySayxG_BptBwlF
89538957
_$ss27_bridgeAnythingToObjectiveCyyXlxlF
89548958
_$ss27_debuggerTestingCheckExpectyySS_SStF

test/abi/Inputs/macOS/x86_64/stdlib/baseline

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8962,6 +8962,10 @@ _$ss27_BidirectionalCollectionBoxCMu
89628962
_$ss27_BidirectionalCollectionBoxCfD
89638963
_$ss27_BidirectionalCollectionBoxCfd
89648964
_$ss27_BidirectionalCollectionBoxCy7ElementQzs09_AnyIndexC0_pcig
8965+
_$ss27_SwiftifyProtocolMethodInfoO6methodyABSS_Says01_aD0OGtcABmFWC
8966+
_$ss27_SwiftifyProtocolMethodInfoOMa
8967+
_$ss27_SwiftifyProtocolMethodInfoOMn
8968+
_$ss27_SwiftifyProtocolMethodInfoON
89658969
_$ss27_allocateUninitializedArrayySayxG_BptBwlF
89668970
_$ss27_bridgeAnythingToObjectiveCyyXlxlF
89678971
_$ss27_debuggerTestingCheckExpectyySS_SStF

test/abi/Inputs/macOS/x86_64/stdlib/baseline-asserts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8974,6 +8974,10 @@ _$ss27_BidirectionalCollectionBoxCMu
89748974
_$ss27_BidirectionalCollectionBoxCfD
89758975
_$ss27_BidirectionalCollectionBoxCfd
89768976
_$ss27_BidirectionalCollectionBoxCy7ElementQzs09_AnyIndexC0_pcig
8977+
_$ss27_SwiftifyProtocolMethodInfoO6methodyABSS_Says01_aD0OGtcABmFWC
8978+
_$ss27_SwiftifyProtocolMethodInfoOMa
8979+
_$ss27_SwiftifyProtocolMethodInfoOMn
8980+
_$ss27_SwiftifyProtocolMethodInfoON
89778981
_$ss27_allocateUninitializedArrayySayxG_BptBwlF
89788982
_$ss27_bridgeAnythingToObjectiveCyyXlxlF
89798983
_$ss27_debuggerTestingCheckExpectyySS_SStF

0 commit comments

Comments
 (0)