Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[cxx-interop] Estend _SwiftifyImport with basic std::span support #78352

Merged
merged 1 commit into from
Jan 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 121 additions & 5 deletions lib/Macros/Sources/SwiftMacros/SwiftifyImportMacro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import SwiftSyntaxMacros

protocol ParamInfo: CustomStringConvertible {
var description: String { get }
var original: ExprSyntax { get }
var original: SyntaxProtocol { get }
var pointerIndex: Int { get }
var nonescaping: Bool { get set }

Expand All @@ -16,12 +16,31 @@ protocol ParamInfo: CustomStringConvertible {
) -> BoundsCheckedThunkBuilder
}

struct CxxSpan: ParamInfo {
var pointerIndex: Int
var nonescaping: Bool
var original: SyntaxProtocol
var typeMappings: [String: String]

var description: String {
return "std::span(pointer: \(pointerIndex), nonescaping: \(nonescaping))"
}

func getBoundsCheckedThunkBuilder(
_ base: BoundsCheckedThunkBuilder, _ funcDecl: FunctionDeclSyntax,
_ variant: Variant
) -> BoundsCheckedThunkBuilder {
CxxSpanThunkBuilder(base: base, index: pointerIndex - 1, signature: funcDecl.signature,
typeMappings: typeMappings, node: original)
}
}

struct CountedBy: ParamInfo {
var pointerIndex: Int
var count: ExprSyntax
var sizedBy: Bool
var nonescaping: Bool
var original: ExprSyntax
var original: SyntaxProtocol

var description: String {
if sizedBy {
Expand All @@ -43,11 +62,12 @@ struct CountedBy: ParamInfo {
nonescaping: nonescaping, isSizedBy: sizedBy)
}
}

struct EndedBy: ParamInfo {
var pointerIndex: Int
var endIndex: Int
var nonescaping: Bool
var original: ExprSyntax
var original: SyntaxProtocol

var description: String {
return ".endedBy(start: \(pointerIndex), end: \(endIndex), nonescaping: \(nonescaping))"
Expand Down Expand Up @@ -196,6 +216,7 @@ func getParam(_ signature: FunctionSignatureSyntax, _ paramIndex: Int) -> Functi
return params[params.startIndex]
}
}

func getParam(_ funcDecl: FunctionDeclSyntax, _ paramIndex: Int) -> FunctionParameterSyntax {
return getParam(funcDecl.signature, paramIndex)
}
Expand Down Expand Up @@ -256,6 +277,43 @@ struct FunctionCallBuilder: BoundsCheckedThunkBuilder {
}
}

struct CxxSpanThunkBuilder: BoundsCheckedThunkBuilder {
public let base: BoundsCheckedThunkBuilder
public let index: Int
public let signature: FunctionSignatureSyntax
public let typeMappings: [String: String]
public let node: SyntaxProtocol

func buildBoundsChecks(_ variant: Variant) throws -> [CodeBlockItemSyntax.Item] {
return []
}

func buildFunctionSignature(_ argTypes: [Int: TypeSyntax?], _ variant: Variant) throws
-> FunctionSignatureSyntax {
var types = argTypes
let param = getParam(signature, index)
let typeName = try getTypeName(param.type).text;
guard let desugaredType = typeMappings[typeName] else {
throw DiagnosticError(
"unable to desugar type with name '\(typeName)'", node: node)
}

let parsedDesugaredType = try TypeSyntax("\(raw: desugaredType)")
types[index] = TypeSyntax(IdentifierTypeSyntax(name: "Span",
genericArgumentClause: parsedDesugaredType.as(IdentifierTypeSyntax.self)!.genericArgumentClause))
return try base.buildFunctionSignature(types, variant)
}

func buildFunctionCall(_ pointerArgs: [Int: ExprSyntax], _ variant: Variant) throws -> ExprSyntax {
var args = pointerArgs
let param = getParam(signature, index)
let typeName = try getTypeName(param.type).text;
assert(args[index] == nil)
args[index] = ExprSyntax("\(raw: typeName)(\(raw: param.secondName ?? param.firstName))")
return try base.buildFunctionCall(args, variant)
}
}

protocol PointerBoundsThunkBuilder: BoundsCheckedThunkBuilder {
var name: TokenSyntax { get }
var nullable: Bool { get }
Expand Down Expand Up @@ -460,7 +518,8 @@ func getParameterIndexForDeclRef(
/// Depends on bounds, escapability and lifetime information for each pointer.
/// Intended to map to C attributes like __counted_by, __ended_by and __no_escape,
/// for automatic application by ClangImporter when the C declaration is annotated
/// appropriately.
/// appropriately. Moreover, it can wrap C++ APIs using unsafe C++ types like
/// std::span with APIs that use their safer Swift equivalents.
public struct SwiftifyImportMacro: PeerMacro {
static func parseEnumName(_ enumConstructorExpr: FunctionCallExprSyntax) throws -> String {
guard let calledExpr = enumConstructorExpr.calledExpression.as(MemberAccessExprSyntax.self)
Expand Down Expand Up @@ -557,6 +616,54 @@ public struct SwiftifyImportMacro: PeerMacro {
return pointerParamIndex
}

static func parseTypeMappingParam(_ paramAST: LabeledExprSyntax?) throws -> [String: String]? {
guard let unwrappedParamAST = paramAST else {
return nil
}
let paramExpr = unwrappedParamAST.expression
guard let dictExpr = paramExpr.as(DictionaryExprSyntax.self) else {
return nil
}
var dict : [String: String] = [:]
switch dictExpr.content {
case .colon(_):
return dict
case .elements(let types):
for element in types {
guard let key = element.key.as(StringLiteralExprSyntax.self) else {
throw DiagnosticError("expected a string literal, got '\(element.key)'", node: element.key)
}
guard let value = element.value.as(StringLiteralExprSyntax.self) else {
throw DiagnosticError("expected a string literal, got '\(element.value)'", node: element.value)
}
dict[key.representedLiteralValue!] = value.representedLiteralValue!
}
default:
throw DiagnosticError("unknown dictionary literal", node: dictExpr)
}
return dict
}

static func parseCxxSpanParams(
_ signature: FunctionSignatureSyntax,
_ typeMappings: [String: String]?
) throws -> [ParamInfo] {
guard let typeMappings else {
return []
}
var result : [ParamInfo] = []
for (idx, param) in signature.parameterClause.parameters.enumerated() {
let typeName = try getTypeName(param.type).text;
if let desugaredType = typeMappings[typeName] {
if desugaredType.starts(with: "span") {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this will cause an issue if any type alias from C/C++ where the desugared type starts with "span", right? It's probably a good idea to add a cxxSpan option to the _SwiftifyInfo enum.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I wanted to update this code snippet. I don't think we need an enum for this, but I'll make the name matching more precise.

Copy link
Contributor Author

@Xazax-hun Xazax-hun Jan 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the meantime, I am working on a follow-up PR. The content of the string we are matching against depends on how we generate the typeMapping in ClangImporter. Would you be OK if this part would stay as is and I only changed it in the follow up where this starts to work end to end? Alternatively, I can backport some code to this PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The follow-up PR where this logic changes: #78422

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That works for me!

result.append(CxxSpan(pointerIndex: idx + 1, nonescaping: false,
original: param, typeMappings: typeMappings))
}
}
}
return result
}

static func parseMacroParam(
_ paramAST: LabeledExprSyntax, _ signature: FunctionSignatureSyntax,
nonescapingPointers: inout Set<Int>
Expand Down Expand Up @@ -651,11 +758,20 @@ public struct SwiftifyImportMacro: PeerMacro {
}

let argumentList = node.arguments!.as(LabeledExprListSyntax.self)!
var arguments = Array<LabeledExprSyntax>(argumentList)
let typeMappings = try parseTypeMappingParam(arguments.last)
if typeMappings != nil {
arguments = arguments.dropLast()
}
var nonescapingPointers = Set<Int>()
var parsedArgs = try argumentList.compactMap {
var parsedArgs = try arguments.compactMap {
try parseMacroParam($0, funcDecl.signature, nonescapingPointers: &nonescapingPointers)
}
parsedArgs.append(contentsOf: try parseCxxSpanParams(funcDecl.signature, typeMappings))
setNonescapingPointers(&parsedArgs, nonescapingPointers)
parsedArgs = parsedArgs.filter {
!($0 is CxxSpan) || ($0 as! CxxSpan).nonescaping
}
try checkArgs(parsedArgs, funcDecl)
let baseBuilder = FunctionCallBuilder(funcDecl)

Expand Down
6 changes: 4 additions & 2 deletions stdlib/public/core/SwiftifyImport.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,17 +32,19 @@ public enum _SwiftifyInfo {
case nonescaping(pointer: Int)
}

/// Generates a bounds safe wrapper for function with Unsafe[Mutable][Raw]Pointer[?] arguments.
/// Generates a safe wrapper for function with Unsafe[Mutable][Raw]Pointer[?] or std::span arguments.
/// Intended to be automatically attached to function declarations imported by ClangImporter.
/// The wrapper function will replace Unsafe[Mutable][Raw]Pointer[?] parameters with
/// [Mutable][Raw]Span[?] or Unsafe[Mutable][Raw]BufferPointer[?] if they have bounds information
/// attached. Where possible "count" parameters will be elided from the wrapper signature, instead
/// fetching the count from the buffer pointer. In these cases the bounds check is also skipped.
/// It will replace some std::span arguments with Swift's Span type when sufficient information is
/// available.
///
/// Currently not supported: return pointers, nested pointers, pointee "count" parameters, endedBy.
///
/// Parameter paramInfo: information about how the function uses the pointer passed to it. The
/// safety of the generated wrapper function depends on this info being extensive and accurate.
@attached(peer, names: overloaded)
public macro _SwiftifyImport(_ paramInfo: _SwiftifyInfo...) =
public macro _SwiftifyImport(_ paramInfo: _SwiftifyInfo..., typeMappings: [String: String] = [:]) =
#externalMacro(module: "SwiftMacros", type: "SwiftifyImportMacro")
17 changes: 17 additions & 0 deletions test/Macros/SwiftifyImport/CxxSpan/NoEscapeSpan.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// REQUIRES: swift_swift_parser
// REQUIRES: swift_feature_Span

// 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

public struct SpanOfInt {
hnrklssn marked this conversation as resolved.
Show resolved Hide resolved
init(_ x: Span<CInt>) {}
}

@_SwiftifyImport(.nonescaping(pointer: 1), typeMappings: ["SpanOfInt" : "span<CInt>"])
func myFunc(_ span: SpanOfInt, _ secondSpan: SpanOfInt) {
}

// CHECK: @_alwaysEmitIntoClient
// CHECK-NEXT: func myFunc(_ span: Span<CInt>, _ secondSpan: SpanOfInt) {
// CHECK-NEXT: return myFunc(SpanOfInt(span), secondSpan)
// CHECK-NEXT: }