Skip to content

Commit b0bb995

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 ef9d2b7 commit b0bb995

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
}
@@ -256,6 +277,43 @@ struct FunctionCallBuilder: BoundsCheckedThunkBuilder {
256277
}
257278
}
258279

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

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

653760
let argumentList = node.arguments!.as(LabeledExprListSyntax.self)!
761+
var arguments = Array<LabeledExprSyntax>(argumentList)
762+
let typeMappings = try parseTypeMappingParam(arguments.last)
763+
if typeMappings != nil {
764+
arguments = arguments.dropLast()
765+
}
654766
var nonescapingPointers = Set<Int>()
655-
var parsedArgs = try argumentList.compactMap {
767+
var parsedArgs = try arguments.compactMap {
656768
try parseMacroParam($0, funcDecl.signature, nonescapingPointers: &nonescapingPointers)
657769
}
770+
parsedArgs.append(contentsOf: try parseCxxSpanParams(funcDecl.signature, typeMappings))
658771
setNonescapingPointers(&parsedArgs, nonescapingPointers)
772+
parsedArgs = parsedArgs.filter {
773+
!($0 is CxxSpan) || ($0 as! CxxSpan).nonescaping
774+
}
659775
try checkArgs(parsedArgs, funcDecl)
660776
let baseBuilder = FunctionCallBuilder(funcDecl)
661777

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)