Skip to content

Commit e1fc9bd

Browse files
author
Gabor Horvath
committed
[cxx-interop] Estend _SwiftifyImport with basic std::span support
This is a preliminary PR to transform nonescaping std::span parameters to Swift's Span type in safe wrappers. To hook this up with ClangImporter, we will need generalize the noescape attribute to non-pointer types (PR is already in review). To transform potentially escaping spans and spans in the return position, a follow-up PR will add lifetime annotation support. This is a building block towards rdar://139074571.
1 parent 5cac7db commit e1fc9bd

File tree

3 files changed

+142
-7
lines changed

3 files changed

+142
-7
lines changed

lib/Macros/Sources/SwiftMacros/SwiftifyImportMacro.swift

Lines changed: 121 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import SwiftSyntaxMacros
66

77
protocol ParamInfo: CustomStringConvertible {
88
var description: String { get }
9-
var original: ExprSyntax { get }
9+
var original: SyntaxProtocol { get }
1010
var pointerIndex: Int { get }
1111
var nonescaping: Bool { get set }
1212

@@ -16,12 +16,31 @@ protocol ParamInfo: CustomStringConvertible {
1616
) -> BoundsCheckedThunkBuilder
1717
}
1818

19+
struct CxxSpan: ParamInfo {
20+
var pointerIndex: Int
21+
var nonescaping: Bool
22+
var original: SyntaxProtocol
23+
var typeMappings: [String: String]
24+
25+
var description: String {
26+
return "std::span(pointer: \(pointerIndex), nonescaping: \(nonescaping))"
27+
}
28+
29+
func getBoundsCheckedThunkBuilder(
30+
_ base: BoundsCheckedThunkBuilder, _ funcDecl: FunctionDeclSyntax,
31+
_ variant: Variant
32+
) -> BoundsCheckedThunkBuilder {
33+
CxxSpanThunkBuilder(base: base, index: pointerIndex - 1, signature: funcDecl.signature,
34+
typeMappings: typeMappings, node: original)
35+
}
36+
}
37+
1938
struct CountedBy: ParamInfo {
2039
var pointerIndex: Int
2140
var count: ExprSyntax
2241
var sizedBy: Bool
2342
var nonescaping: Bool
24-
var original: ExprSyntax
43+
var original: SyntaxProtocol
2544

2645
var description: String {
2746
if sizedBy {
@@ -43,11 +62,12 @@ struct CountedBy: ParamInfo {
4362
nonescaping: nonescaping, isSizedBy: sizedBy)
4463
}
4564
}
65+
4666
struct EndedBy: ParamInfo {
4767
var pointerIndex: Int
4868
var endIndex: Int
4969
var nonescaping: Bool
50-
var original: ExprSyntax
70+
var original: SyntaxProtocol
5171

5272
var description: String {
5373
return ".endedBy(start: \(pointerIndex), end: \(endIndex), nonescaping: \(nonescaping))"
@@ -196,6 +216,7 @@ func getParam(_ signature: FunctionSignatureSyntax, _ paramIndex: Int) -> Functi
196216
return params[params.startIndex]
197217
}
198218
}
219+
199220
func getParam(_ funcDecl: FunctionDeclSyntax, _ paramIndex: Int) -> FunctionParameterSyntax {
200221
return getParam(funcDecl.signature, paramIndex)
201222
}
@@ -257,6 +278,43 @@ struct FunctionCallBuilder: BoundsCheckedThunkBuilder {
257278
}
258279
}
259280

281+
struct CxxSpanThunkBuilder: BoundsCheckedThunkBuilder {
282+
public let base: BoundsCheckedThunkBuilder
283+
public let index: Int
284+
public let signature: FunctionSignatureSyntax
285+
public let typeMappings: [String: String]
286+
public let node: SyntaxProtocol
287+
288+
func buildBoundsChecks(_ variant: Variant) throws -> [CodeBlockItemSyntax.Item] {
289+
return []
290+
}
291+
292+
func buildFunctionSignature(_ argTypes: [Int: TypeSyntax?], _ variant: Variant) throws
293+
-> FunctionSignatureSyntax {
294+
var types = argTypes
295+
let param = getParam(signature, index)
296+
let typeName = try getTypeName(param.type).text;
297+
guard let desugaredType = typeMappings[typeName] else {
298+
throw DiagnosticError(
299+
"unable to desugar type with name '\(typeName)'", node: node)
300+
}
301+
302+
let parsedDesugaredType = try TypeSyntax("\(raw: desugaredType)")
303+
types[index] = TypeSyntax(IdentifierTypeSyntax(name: "Span",
304+
genericArgumentClause: parsedDesugaredType.as(IdentifierTypeSyntax.self)!.genericArgumentClause))
305+
return try base.buildFunctionSignature(types, variant)
306+
}
307+
308+
func buildFunctionCall(_ pointerArgs: [Int: ExprSyntax], _ variant: Variant) throws -> ExprSyntax {
309+
var args = pointerArgs
310+
let param = getParam(signature, index)
311+
let typeName = try getTypeName(param.type).text;
312+
assert(args[index] == nil)
313+
args[index] = ExprSyntax("\(raw: typeName)(\(raw: param.secondName ?? param.firstName))")
314+
return try base.buildFunctionCall(args, variant)
315+
}
316+
}
317+
260318
protocol PointerBoundsThunkBuilder: BoundsCheckedThunkBuilder {
261319
var name: TokenSyntax { get }
262320
var nullable: Bool { get }
@@ -461,7 +519,8 @@ func getParameterIndexForDeclRef(
461519
/// Depends on bounds, escapability and lifetime information for each pointer.
462520
/// Intended to map to C attributes like __counted_by, __ended_by and __no_escape,
463521
/// for automatic application by ClangImporter when the C declaration is annotated
464-
/// appropriately.
522+
/// appropriately. Moreover, it can wrap C++ APIs using unsafe C++ types like
523+
/// std::span with APIs that use their safer Swift equivalents.
465524
public struct SwiftifyImportMacro: PeerMacro {
466525
static func parseEnumName(_ enumConstructorExpr: FunctionCallExprSyntax) throws -> String {
467526
guard let calledExpr = enumConstructorExpr.calledExpression.as(MemberAccessExprSyntax.self)
@@ -558,6 +617,54 @@ public struct SwiftifyImportMacro: PeerMacro {
558617
return pointerParamIndex
559618
}
560619

620+
static func parseTypeMappingParam(_ paramAST: LabeledExprSyntax?) throws -> [String: String]? {
621+
guard let unwrappedParamAST = paramAST else {
622+
return nil
623+
}
624+
let paramExpr = unwrappedParamAST.expression
625+
guard let dictExpr = paramExpr.as(DictionaryExprSyntax.self) else {
626+
return nil
627+
}
628+
var dict : [String: String] = [:]
629+
switch dictExpr.content {
630+
case .colon(_):
631+
return dict
632+
case .elements(let types):
633+
for element in types {
634+
guard let key = element.key.as(StringLiteralExprSyntax.self) else {
635+
throw DiagnosticError("expected a string literal, got '\(element.key)'", node: element.key)
636+
}
637+
guard let value = element.value.as(StringLiteralExprSyntax.self) else {
638+
throw DiagnosticError("expected a string literal, got '\(element.value)'", node: element.value)
639+
}
640+
dict[key.representedLiteralValue!] = value.representedLiteralValue!
641+
}
642+
default:
643+
throw DiagnosticError("unknown dictionary literal", node: dictExpr)
644+
}
645+
return dict
646+
}
647+
648+
static func parseCxxSpanParams(
649+
_ signature: FunctionSignatureSyntax,
650+
_ typeMappings: [String: String]?
651+
) throws -> [ParamInfo] {
652+
guard let typeMappings else {
653+
return []
654+
}
655+
var result : [ParamInfo] = []
656+
for (idx, param) in signature.parameterClause.parameters.enumerated() {
657+
let typeName = try getTypeName(param.type).text;
658+
if let desugaredType = typeMappings[typeName] {
659+
if desugaredType.starts(with: "span") {
660+
result.append(CxxSpan(pointerIndex: idx + 1, nonescaping: false,
661+
original: param, typeMappings: typeMappings))
662+
}
663+
}
664+
}
665+
return result
666+
}
667+
561668
static func parseMacroParam(
562669
_ paramAST: LabeledExprSyntax, _ signature: FunctionSignatureSyntax,
563670
nonescapingPointers: inout Set<Int>
@@ -652,11 +759,20 @@ public struct SwiftifyImportMacro: PeerMacro {
652759
}
653760

654761
let argumentList = node.arguments!.as(LabeledExprListSyntax.self)!
762+
var arguments = Array<LabeledExprSyntax>(argumentList)
763+
let typeMappings = try parseTypeMappingParam(arguments.last)
764+
if typeMappings != nil {
765+
arguments = arguments.dropLast()
766+
}
655767
var nonescapingPointers = Set<Int>()
656-
var parsedArgs = try argumentList.compactMap {
768+
var parsedArgs = try arguments.compactMap {
657769
try parseMacroParam($0, funcDecl.signature, nonescapingPointers: &nonescapingPointers)
658770
}
771+
parsedArgs.append(contentsOf: try parseCxxSpanParams(funcDecl.signature, typeMappings))
659772
setNonescapingPointers(&parsedArgs, nonescapingPointers)
773+
parsedArgs = parsedArgs.filter {
774+
!($0 is CxxSpan) || ($0 as! CxxSpan).nonescaping
775+
}
660776
try checkArgs(parsedArgs, funcDecl)
661777
let baseBuilder = FunctionCallBuilder(funcDecl)
662778

stdlib/public/core/SwiftifyImport.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,17 +32,19 @@ public enum _SwiftifyInfo {
3232
case nonescaping(pointer: Int)
3333
}
3434

35-
/// Generates a bounds safe wrapper for function with Unsafe[Mutable][Raw]Pointer[?] arguments.
35+
/// Generates a safe wrapper for function with Unsafe[Mutable][Raw]Pointer[?] or std::span arguments.
3636
/// Intended to be automatically attached to function declarations imported by ClangImporter.
3737
/// The wrapper function will replace Unsafe[Mutable][Raw]Pointer[?] parameters with
3838
/// [Mutable][Raw]Span[?] or Unsafe[Mutable][Raw]BufferPointer[?] if they have bounds information
3939
/// attached. Where possible "count" parameters will be elided from the wrapper signature, instead
4040
/// fetching the count from the buffer pointer. In these cases the bounds check is also skipped.
41+
/// It will replace some std::span arguments with Swift's Span type when sufficient information is
42+
/// available.
4143
///
4244
/// Currently not supported: return pointers, nested pointers, pointee "count" parameters, endedBy.
4345
///
4446
/// Parameter paramInfo: information about how the function uses the pointer passed to it. The
4547
/// safety of the generated wrapper function depends on this info being extensive and accurate.
4648
@attached(peer, names: overloaded)
47-
public macro _SwiftifyImport(_ paramInfo: _SwiftifyInfo...) =
49+
public macro _SwiftifyImport(_ paramInfo: _SwiftifyInfo..., typeMappings: [String: String] = [:]) =
4850
#externalMacro(module: "SwiftMacros", type: "SwiftifyImportMacro")
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// REQUIRES: swift_swift_parser
2+
// REQUIRES: swift_feature_Span
3+
4+
// RUN: %target-swift-frontend %s -swift-version 5 -module-name main -disable-availability-checking -typecheck -enable-experimental-feature Span -plugin-path %swift-plugin-dir -dump-macro-expansions 2>&1 | %FileCheck --match-full-lines %s
5+
6+
public struct SpanOfInt {
7+
init(_ x: Span<CInt>) {}
8+
}
9+
10+
@_SwiftifyImport(.nonescaping(pointer: 1), typeMappings: ["SpanOfInt" : "span<CInt>"])
11+
func myFunc(_ span: SpanOfInt, _ secondSpan: SpanOfInt) {
12+
}
13+
14+
// CHECK: @_alwaysEmitIntoClient
15+
// CHECK-NEXT: func myFunc(_ span: Span<CInt>, _ secondSpan: SpanOfInt) {
16+
// CHECK-NEXT: return myFunc(SpanOfInt(span), secondSpan)
17+
// CHECK-NEXT: }

0 commit comments

Comments
 (0)