Skip to content

Opaquepointer interop #78315

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

Merged
merged 5 commits 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
56 changes: 48 additions & 8 deletions lib/Macros/Sources/SwiftMacros/SwiftifyImportMacro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -131,11 +131,22 @@ func getPointerMutability(text: String) -> Mutability? {
case "UnsafeMutablePointer": return .Mutable
case "UnsafeRawPointer": return .Immutable
case "UnsafeMutableRawPointer": return .Mutable
case "OpaquePointer": return .Immutable
default:
return nil
}
}

func isRawPointerType(text: String) -> Bool {
switch text {
case "UnsafeRawPointer": return true
case "UnsafeMutableRawPointer": return true
case "OpaquePointer": return true
default:
return false
}
}

func getSafePointerName(mut: Mutability, generateSpan: Bool, isRaw: Bool) -> TokenSyntax {
switch (mut, generateSpan, isRaw) {
case (.Immutable, true, true): return "RawSpan"
Expand All @@ -160,9 +171,13 @@ func transformType(_ prev: TypeSyntax, _ variant: Variant, _ isSizedBy: Bool) th
}
let name = try getTypeName(prev)
let text = name.text
if !isSizedBy && (text == "UnsafeRawPointer" || text == "UnsafeMutableRawPointer") {
let isRaw = isRawPointerType(text: text)
if isRaw && !isSizedBy {
throw DiagnosticError("raw pointers only supported for SizedBy", node: name)
}
if !isRaw && isSizedBy {
throw DiagnosticError("SizedBy only supported for raw pointers", node: name)
}

guard let kind: Mutability = getPointerMutability(text: text) else {
throw DiagnosticError(
Expand Down Expand Up @@ -331,7 +346,7 @@ struct CountedOrSizedPointerThunkBuilder: PointerBoundsThunkBuilder {
var args = argOverrides
let argExpr = ExprSyntax("\(unwrappedName).baseAddress")
assert(args[index] == nil)
args[index] = unwrapIfNonnullable(argExpr)
args[index] = try castPointerToOpaquePointer(unwrapIfNonnullable(argExpr))
let call = try base.buildFunctionCall(args, variant)
let ptrRef = unwrapIfNullable(ExprSyntax(DeclReferenceExprSyntax(baseName: name)))

Expand All @@ -353,7 +368,26 @@ struct CountedOrSizedPointerThunkBuilder: PointerBoundsThunkBuilder {
return ExprSyntax("\(name).\(raw: countName)")
}

func getPointerArg() -> ExprSyntax {
func peelOptionalType(_ type: TypeSyntax) -> TypeSyntax {
if let optType = type.as(OptionalTypeSyntax.self) {
return optType.wrappedType
}
if let impOptType = type.as(ImplicitlyUnwrappedOptionalTypeSyntax.self) {
return impOptType.wrappedType
}
return type
}

func castPointerToOpaquePointer(_ baseAddress: ExprSyntax) throws -> ExprSyntax {
let i = try getParameterIndexForParamName(signature.parameterClause.parameters, name)
let type = peelOptionalType(getParam(signature, i).type)
if type.canRepresentBasicType(type: OpaquePointer.self) {
return ExprSyntax("OpaquePointer(\(baseAddress))")
}
return baseAddress
}

func getPointerArg() throws -> ExprSyntax {
if nullable {
return ExprSyntax("\(name)?.baseAddress")
}
Expand Down Expand Up @@ -391,7 +425,7 @@ struct CountedOrSizedPointerThunkBuilder: PointerBoundsThunkBuilder {
return unwrappedCall
}

args[index] = getPointerArg()
args[index] = try castPointerToOpaquePointer(getPointerArg())
return try base.buildFunctionCall(args, variant)
}
}
Expand Down Expand Up @@ -440,22 +474,28 @@ func getOptionalArgumentByName(_ argumentList: LabeledExprListSyntax, _ name: St
})?.expression
}

func getParameterIndexForDeclRef(
_ parameterList: FunctionParameterListSyntax, _ ref: DeclReferenceExprSyntax
func getParameterIndexForParamName(
_ parameterList: FunctionParameterListSyntax, _ tok: TokenSyntax
) throws -> Int {
let name = ref.baseName.text
let name = tok.text
guard
let index = parameterList.enumerated().first(where: {
(_: Int, param: FunctionParameterSyntax) in
let paramenterName = param.secondName ?? param.firstName
return paramenterName.trimmed.text == name
})?.offset
else {
throw DiagnosticError("no parameter with name '\(name)' in '\(parameterList)'", node: ref)
throw DiagnosticError("no parameter with name '\(name)' in '\(parameterList)'", node: tok)
}
return index
}

func getParameterIndexForDeclRef(
_ parameterList: FunctionParameterListSyntax, _ ref: DeclReferenceExprSyntax
) throws -> Int {
return try getParameterIndexForParamName((parameterList), ref.baseName)
}

/// A macro that adds safe(r) wrappers for functions with unsafe pointer types.
/// Depends on bounds, escapability and lifetime information for each pointer.
/// Intended to map to C attributes like __counted_by, __ended_by and __no_escape,
Expand Down
4 changes: 4 additions & 0 deletions test/Interop/C/swiftify-import/Inputs/module.modulemap
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,8 @@ module CountedByClang {
header "counted-by.h"
export *
}
module SizedByClang {
header "sized-by.h"
export *
}

21 changes: 21 additions & 0 deletions test/Interop/C/swiftify-import/Inputs/sized-by.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#pragma once

#define __sized_by(x) __attribute__((__sized_by__(x)))

void simple(int len, void * __sized_by(len) p);

void swiftAttr(int len, void *p) __attribute__((
swift_attr("@_SwiftifyImport(.sizedBy(pointer: 2, size: \"len\"))")));

void shared(int len, void * __sized_by(len) p1, void * __sized_by(len) p2);

void complexExpr(int len, int offset, void * __sized_by(len - offset) p);

void nullUnspecified(int len, void * __sized_by(len) _Null_unspecified p);

void nonnull(int len, void * __sized_by(len) _Nonnull p);

void nullable(int len, void * __sized_by(len) _Nullable p);

typedef struct foo opaque_t;
void opaque(int len, opaque_t * __sized_by(len) p);
49 changes: 11 additions & 38 deletions test/Interop/C/swiftify-import/counted-by.swift
Original file line number Diff line number Diff line change
@@ -1,46 +1,19 @@
// REQUIRES: swift_feature_SafeInteropWrappers

// RUN: %target-swift-ide-test -print-module -module-to-print=CountedByClang -plugin-path %swift-plugin-dir -I %S/Inputs -source-filename=x -enable-experimental-feature SafeInteropWrappers | %FileCheck %s

// swift-ide-test doesn't currently typecheck the macro expansions, so run the compiler as well
// RUN: %empty-directory(%t)
// RUN: %target-swift-frontend -emit-module -disable-availability-checking -plugin-path %swift-plugin-dir -o %t/CountedBy.swiftmodule -I %S/Inputs -enable-experimental-feature SafeInteropWrappers %s
// RUN: %target-swift-frontend -emit-module -plugin-path %swift-plugin-dir -o %t/CountedBy.swiftmodule -I %S/Inputs -enable-experimental-feature SafeInteropWrappers %s

// Check that ClangImporter correctly infers and expands @_SwiftifyImport macros for functions with __counted_by parameters.

import CountedByClang

@inlinable
public func callSimple(_ p: UnsafeMutableBufferPointer<CInt>) {
simple(p)
}

// Check that macros from swift_attr work the same as inferred macros.
@inlinable
public func callSwiftAttr(_ p: UnsafeMutableBufferPointer<CInt>) {
swiftAttr(p)
}

@inlinable
public func callShared(_ len: CInt, _ p1: UnsafeMutableBufferPointer<CInt>, _ p2: UnsafeMutableBufferPointer<CInt>) {
shared(len, p1, p2)
}

@inlinable
public func callComplexExpr(_ len: CInt, _ offset: CInt, _ p: UnsafeMutableBufferPointer<CInt>) {
complexExpr(len, offset, p)
}


@inlinable
public func callNullUnspecified(_ p: UnsafeMutableBufferPointer<CInt>) {
nullUnspecified(p)
}

@inlinable
public func callNonnull(_ p: UnsafeMutableBufferPointer<CInt>) {
nonnull(p)
}

@inlinable
public func callNullable(_ p: UnsafeMutableBufferPointer<CInt>?) {
nullable(p)
}

// CHECK: @_alwaysEmitIntoClient public func complexExpr(_ len: Int{{.*}}, _ offset: Int{{.*}}, _ p: UnsafeMutableBufferPointer<Int{{.*}}>)
// CHECK-NEXT: @_alwaysEmitIntoClient public func nonnull(_ p: UnsafeMutableBufferPointer<Int{{.*}}>)
// CHECK-NEXT: @_alwaysEmitIntoClient public func nullUnspecified(_ p: UnsafeMutableBufferPointer<Int{{.*}}>)
// CHECK-NEXT: @_alwaysEmitIntoClient public func nullable(_ p: UnsafeMutableBufferPointer<Int{{.*}}>?)
// CHECK-NEXT: @_alwaysEmitIntoClient public func shared(_ len: Int{{.*}}, _ p1: UnsafeMutableBufferPointer<Int{{.*}}>, _ p2: UnsafeMutableBufferPointer<Int{{.*}}>)
// CHECK-NEXT: @_alwaysEmitIntoClient public func simple(_ p: UnsafeMutableBufferPointer<Int{{.*}}>)
// CHECK-NEXT: @_alwaysEmitIntoClient public func swiftAttr(_ p: UnsafeMutableBufferPointer<Int{{.*}}>)
20 changes: 20 additions & 0 deletions test/Interop/C/swiftify-import/sized-by.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// REQUIRES: swift_feature_SafeInteropWrappers

// RUN: %target-swift-ide-test -print-module -module-to-print=SizedByClang -plugin-path %swift-plugin-dir -I %S/Inputs -source-filename=x -enable-experimental-feature SafeInteropWrappers | %FileCheck %s

// swift-ide-test doesn't currently typecheck the macro expansions, so run the compiler as well
// RUN: %empty-directory(%t)
// RUN: %target-swift-frontend -emit-module -plugin-path %swift-plugin-dir -o %t/CountedBy.swiftmodule -I %S/Inputs -enable-experimental-feature SafeInteropWrappers %s

// Check that ClangImporter correctly infers and expands @_SwiftifyImport macros for functions with __sized_by parameters.
import SizedByClang

// CHECK: @_alwaysEmitIntoClient public func complexExpr(_ len: Int{{.*}}, _ offset: Int{{.*}}, _ p: UnsafeMutableRawBufferPointer)
// CHECK-NEXT: @_alwaysEmitIntoClient public func nonnull(_ p: UnsafeMutableRawBufferPointer)
// CHECK-NEXT: @_alwaysEmitIntoClient public func nullUnspecified(_ p: UnsafeMutableRawBufferPointer)
// CHECK-NEXT: @_alwaysEmitIntoClient public func nullable(_ p: UnsafeMutableRawBufferPointer?)
// CHECK-NEXT: @_alwaysEmitIntoClient public func opaque(_ p: UnsafeRawBufferPointer)
// CHECK-NEXT: @_alwaysEmitIntoClient public func shared(_ len: Int{{.*}}, _ p1: UnsafeMutableRawBufferPointer, _ p2: UnsafeMutableRawBufferPointer)
// CHECK-NEXT: @_alwaysEmitIntoClient public func simple(_ p: UnsafeMutableRawBufferPointer)
// CHECK-NEXT: @_alwaysEmitIntoClient public func swiftAttr(_ p: UnsafeMutableRawBufferPointer)

10 changes: 10 additions & 0 deletions test/Macros/SwiftifyImport/MacroErrors/NullableSpan.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// REQUIRES: swift_swift_parser
// REQUIRES: swift_feature_Span

// RUN: %target-typecheck-verify-swift -swift-version 5 -module-name main -disable-availability-checking -typecheck -plugin-path %swift-plugin-dir -verify -enable-experimental-feature Span

// XFAIL: *
// expanded form errors with "type 'Span' does not conform to protocol 'Escapable'" because Optional doesn't support ~Escapable yet
@_SwiftifyImport(.countedBy(pointer: 1, count: "len"), .nonescaping(pointer: 1))
func nullableSpan(_ ptr: UnsafePointer<CInt>?, _ len: CInt) {
}
19 changes: 19 additions & 0 deletions test/Macros/SwiftifyImport/MacroErrors/Pointee.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// REQUIRES: swift_swift_parser

// RUN: %target-typecheck-verify-swift -swift-version 5 -module-name main -disable-availability-checking -typecheck -plugin-path %swift-plugin-dir -verify

// expected-error@+2{{SizedBy only supported for raw pointers}}
@_SwiftifyImport(.sizedBy(pointer: 1, size: "size"))
func myFunc(_ ptr: UnsafePointer<Int>, _ size: CInt) {
}

// expected-error@+2{{raw pointers only supported for SizedBy}}
@_SwiftifyImport(.countedBy(pointer: 1, count: "count"))
func myFunc(_ ptr: UnsafeRawPointer, _ count: CInt) {
}

// expected-error@+2{{raw pointers only supported for SizedBy}}
@_SwiftifyImport(.countedBy(pointer: 1, count: "count"))
func myFunc(_ ptr: OpaquePointer, _ count: CInt) {
}

71 changes: 71 additions & 0 deletions test/Macros/SwiftifyImport/SizedBy/Opaque.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// REQUIRES: swift_swift_parser
// REQUIRES: swift_feature_Span

// RUN: %target-swift-frontend %s -swift-version 5 -module-name main -disable-availability-checking -typecheck -plugin-path %swift-plugin-dir -dump-macro-expansions -enable-experimental-feature Span -verify 2>&1 | %FileCheck --match-full-lines %s

@_SwiftifyImport(.sizedBy(pointer: 1, size: "size"))
func nonnullUnsafeRawBufferPointer(_ ptr: OpaquePointer, _ size: CInt) {
}

@_SwiftifyImport(.sizedBy(pointer: 1, size: "size"))
func nullableUnsafeRawBufferPointer(_ ptr: OpaquePointer?, _ size: CInt) {
}

@_SwiftifyImport(.sizedBy(pointer: 1, size: "size"))
func impNullableUnsafeRawBufferPointer(_ ptr: OpaquePointer!, _ size: CInt) {
}

@_SwiftifyImport(.sizedBy(pointer: 1, size: "size"), .nonescaping(pointer: 1))
func nonnullSpan(_ ptr: OpaquePointer, _ size: CInt) {
}

// expected-note@+2{{in expansion of macro '_SwiftifyImport' on global function 'nullableSpan' here}}
// Cannot refer to source location for the error: "type 'RawSpan' does not conform to protocol 'Escapable'" (which is currently necessary for Optional)
@_SwiftifyImport(.sizedBy(pointer: 1, size: "size"), .nonescaping(pointer: 1))
func nullableSpan(_ ptr: OpaquePointer?, _ size: CInt) {
}

@_SwiftifyImport(.sizedBy(pointer: 1, size: "size"), .nonescaping(pointer: 1))
func impNullableSpan(_ ptr: OpaquePointer!, _ size: CInt) {
}

// CHECK: @_alwaysEmitIntoClient
// CHECK-NEXT: func nonnullUnsafeRawBufferPointer(_ ptr: UnsafeRawBufferPointer) {
// CHECK-NEXT: return nonnullUnsafeRawBufferPointer(OpaquePointer(ptr.baseAddress!), CInt(exactly: ptr.count)!)
// CHECK-NEXT: }

// CHECK: @_alwaysEmitIntoClient
// CHECK-NEXT: func nullableUnsafeRawBufferPointer(_ ptr: UnsafeRawBufferPointer?) {
// CHECK-NEXT: return nullableUnsafeRawBufferPointer(OpaquePointer(ptr?.baseAddress), CInt(exactly: ptr?.count ?? 0)!)
// CHECK-NEXT: }

// CHECK: @_alwaysEmitIntoClient
// CHECK-NEXT: func impNullableUnsafeRawBufferPointer(_ ptr: UnsafeRawBufferPointer) {
// CHECK-NEXT: return impNullableUnsafeRawBufferPointer(OpaquePointer(ptr.baseAddress!), CInt(exactly: ptr.count)!)
// CHECK-NEXT: }

// CHECK: @_alwaysEmitIntoClient
// CHECK-NEXT: func nonnullSpan(_ ptr: RawSpan) {
// CHECK-NEXT: return ptr.withUnsafeBytes { _ptrPtr in
// CHECK-NEXT: return nonnullSpan(OpaquePointer(_ptrPtr.baseAddress!), CInt(exactly: ptr.byteCount)!)
// CHECK-NEXT: }
// CHECK-NEXT: }

// CHECK: @_alwaysEmitIntoClient
// CHECK-NEXT: func nullableSpan(_ ptr: RawSpan?) {
// CHECK-NEXT: return if ptr == nil {
// CHECK-NEXT: nullableSpan(nil, CInt(exactly: ptr?.byteCount ?? 0)!)
// CHECK-NEXT: } else {
// CHECK-NEXT: ptr!.withUnsafeBytes { _ptrPtr in
// CHECK-NEXT: return nullableSpan(OpaquePointer(_ptrPtr.baseAddress), CInt(exactly: ptr?.byteCount ?? 0)!)
// CHECK-NEXT: }
// CHECK-NEXT: }
// CHECK-NEXT: }

// CHECK: @_alwaysEmitIntoClient
// CHECK-NEXT: func impNullableSpan(_ ptr: RawSpan) {
// CHECK-NEXT: return ptr.withUnsafeBytes { _ptrPtr in
// CHECK-NEXT: return impNullableSpan(OpaquePointer(_ptrPtr.baseAddress!), CInt(exactly: ptr.byteCount)!)
// CHECK-NEXT: }
// CHECK-NEXT: }