Skip to content

Add @PointerBounds macro #76969

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 13 commits into from
Nov 11, 2024
Merged

Conversation

hnrklssn
Copy link
Contributor

@PointerBounds is a macro intended to be applied by ClangImporter when importing functions with pointer parameters from C headers. By leveraging C attributes we can get insight into bounds, esapability, and (eventually) lifetimes of pointers, allowing us to map them to safe(r) and more ergonomic types than UnsafePointer.

This initial macro implementation supports CountedBy and Sizedby, but not yet EndedBy. It can generate function overloads with and without an explicit count parameter, as well as with UnsafeBufferPointer or Span (if marked nonescaping), and any of their combinations. It supports nullable/optional pointers, and both mutable and immutable pointers. It supports arbitrary count expressions. These are passed to the macro as a string literal since any parameters referred to in the count expression will not have been declared yet when parsing the macro.

It does not support indirect pointers or inout parameters. It supports functions with return values, but returned pointers can not be bounds checked yet.

Bounds checked pointers must be of type Unsafe[Mutable]Pointer[?]. Count expressions must conform to the BinaryInteger protocol, and have an initializer with signature "init(exactly: Int) -> T?" (or accept a supertype of Int).

rdar://137628612

@hnrklssn hnrklssn requested a review from a team as a code owner October 10, 2024 22:51
@hnrklssn
Copy link
Contributor Author

@swift-ci Please smoke test

@@ -0,0 +1,11 @@
add_swift_target_library(swift_PointerBounds ${SWIFT_STDLIB_LIBRARY_BUILD_TYPES} IS_STDLIB
Copy link
Member

Choose a reason for hiding this comment

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

We shouldn't create a new target here. The actual macro declaration and the types it depend on will go into the standard library so it's always available. In the short term, we can put them into the test cases themselves or (better) a module within the test directory that we build and feed in to the test command.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Wouldn't placing it in the test dir rather than conditionally making it part of the stdlib make it harder to create pre-release versions for testing on real codebases?

// CHECK-NEXT: }
// CHECK-NEXT:
// CHECK-NEXT: func myFunc(_ ptr: UnsafeBufferPointer<CInt>, _ size: CInt, _ count: CInt) {
// CHECK-NEXT: let _ptrCount: some BinaryInteger = size * count
Copy link
Member

Choose a reason for hiding this comment

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

I'd rather not have _ptrCount typed to some BinaryInteger, which is erasing information when we could be more precise. Maybe just leave off the type annotation and let the compiler figure it out?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I did this because the expression could be any type, and in the case that it does have the wrong type, the type error here makes it clearer that the error is because of the user annotation rather than a bug in the macro. Maybe there's a neater way to do something like this?

let _ptrCount = size * count
let _assertPtrCountBinaryIntegerType: some BinaryInteger = _ptrCount // unused, only for asserting type

Copy link
Member

Choose a reason for hiding this comment

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

I guess if we have a function like this we could form a call to it:

@_alwaysEmitIntoClient 
func _assertBinaryInteger<T: BinaryInteger>(_: T) { }

... but the result is basically the same, so what you've done will work. My main concern is that the opaque type we're creating here will have some runtime impact that doesn't get optimized away.

@hnrklssn hnrklssn force-pushed the pointer-bounds-macro branch from 55744d9 to 20ad631 Compare October 15, 2024 00:43
@hnrklssn
Copy link
Contributor Author

Managed to get things to build and run properly now. Will start addressing comments tomorrow.

@hnrklssn
Copy link
Contributor Author

@swift-ci Please smoke test


struct CountedBy: ParamInfo {
var pointerIndex: Int
var count: ExprSyntax
Copy link
Member

Choose a reason for hiding this comment

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

Should this be normalized to a count index? That would be more consistent with the other parameters that are resolved as indices.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

How would you then represent count expressions with e.g. addition or multiplication?

Copy link
Member

Choose a reason for hiding this comment

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

To get there, we'd have to "parse" the C/C++ expression and map it into something. We can represent that here with an expression in terms of Swift if we need to.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Indeed. So far I've punted that issue, because the basic operations that cover most use cases have the same syntax in Swift. I imagine a macro syntax like this: .mul(.param(2), .param(3)). This has the upside of only being able to represent expressions we support, and downside of requiring us to add explicit support for every type of expression. It would also let us add an enum case representing the return value of a function, so malloc could be annotated with @PointerBounds(.sizedByOrNull(pointer: .retVal, size: .param(1))).

Comment on lines 48 to 59
// Stub interfaces for testing until Span lands
public struct Span<T> {
public var count: Int
var ptr: UnsafeBufferPointer<T>
public func withUnsafeBufferPointer<R>(_ body: (UnsafeBufferPointer<T>) throws -> R) rethrows -> R {
return try body(ptr)
}
}
public protocol RawSpan {
var byteCount: Int { get }
func withUnsafeBytes<R>(_ body: (UnsafeRawBufferPointer) throws -> R) rethrows -> R
}
Copy link
Member

Choose a reason for hiding this comment

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

At this point, I expect Span will land before this PR, now that it's been accepted.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Great! I'll keep it for now for my own testing, seeing as Span hasn't landed, but I'll make sure to remove it before merging this.

// CHECK-NEXT: }
// CHECK-NEXT:
// CHECK-NEXT: func myFunc(_ ptr: UnsafeBufferPointer<CInt>, _ size: CInt, _ count: CInt) {
// CHECK-NEXT: let _ptrCount: some BinaryInteger = size * count
Copy link
Member

Choose a reason for hiding this comment

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

I guess if we have a function like this we could form a call to it:

@_alwaysEmitIntoClient 
func _assertBinaryInteger<T: BinaryInteger>(_: T) { }

... but the result is basically the same, so what you've done will work. My main concern is that the opaque type we're creating here will have some runtime impact that doesn't get optimized away.

@hnrklssn
Copy link
Contributor Author

depends on swiftlang/swift-syntax#2886

@DougGregor
Copy link
Member

depends on swiftlang/swift-syntax#2886

I'd like to break this dependency on the swift-syntax change, which I feel needs more discussion independently of this PR. Can you bring in whatever bits from swiftlang/swift-syntax#2886 that you need here so we can land this PR without any changes to swift-syntax? Once we do get to an answer there, we can move over to the corresponding API.

@hnrklssn
Copy link
Contributor Author

hnrklssn commented Nov 5, 2024

@swift-ci Please smoke test

5 similar comments
@hnrklssn
Copy link
Contributor Author

hnrklssn commented Nov 6, 2024

@swift-ci Please smoke test

@hnrklssn
Copy link
Contributor Author

hnrklssn commented Nov 6, 2024

@swift-ci Please smoke test

@hnrklssn
Copy link
Contributor Author

hnrklssn commented Nov 7, 2024

@swift-ci Please smoke test

@hnrklssn
Copy link
Contributor Author

hnrklssn commented Nov 7, 2024

@swift-ci Please smoke test

@DougGregor
Copy link
Member

@swift-ci Please smoke test

@hnrklssn
Copy link
Contributor Author

hnrklssn commented Nov 7, 2024

/home/build-user/swift/lib/Macros/Sources/SwiftMacros/DebugDescriptionMacro.swift:244:7: warning: default will never be executed
      @unknown default:
      ^
/home/build-user/swift/lib/Macros/Sources/SwiftMacros/DebugDescriptionMacro.swift:440:5: warning: default will never be executed
    @unknown default:
    ^
/home/build-user/swift/lib/Macros/Sources/SwiftMacros/OptionSetMacro.swift:122:12: error: cannot convert return expression of type '(StructDeclSyntax, EnumDeclSyntax, GenericArgumentSyntax.Argument)' to return type '(StructDeclSyntax, EnumDeclSyntax, TypeSyntax)?'
    return (structDecl, optionsEnum, rawType)
           ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
                                              as! (StructDeclSyntax, EnumDeclSyntax, TypeSyntax)
/home/build-user/swift/lib/Macros/Sources/SwiftMacros/DistributedResolvableMacro.swift:145:39: error: value of type 'SameTypeRequirementSyntax.LeftType' has no member 'isActorSystem'
           where sameTypeReq.leftType.isActorSystem:
                 ~~~~~~~~~~~~~~~~~~~~ ^~~~~~~~~~~~~
/home/build-user/swift/lib/Macros/Sources/SwiftMacros/DistributedResolvableMacro.swift:146:40: error: cannot assign value of type 'SameTypeRequirementSyntax.RightType' to type 'TypeSyntax?'
        specificActorSystemRequirement = sameTypeReq.rightType.trimmed
                                       ^

@DougGregor this doesn't look related to this patch at all. Is it a failure in main?

@DougGregor
Copy link
Member

I suspect you need to rebase your swift and update your swift-syntax

@PointerBounds is a macro intended to be applied by ClangImporter when
importing functions with pointer parameters from C headers. By
leveraging C attributes we can get insight into bounds, esapability, and
(eventually) lifetimes of pointers, allowing us to map them to safe(r)
and more ergonomic types than UnsafePointer.

This initial macro implementation supports CountedBy and Sizedby, but
not yet EndedBy. It can generate function overloads with and without an
explicit count parameter, as well as with UnsafeBufferPointer or Span
(if marked nonescaping), and any of their combinations. It supports
nullable/optional pointers, and both mutable and immutable pointers.
It supports arbitrary count expressions. These are passed to the macro
as a string literal since any parameters referred to in the count
expression will not have been declared yet when parsing the macro.

It does not support indirect pointers or inout parameters. It supports
functions with return values, but returned pointers can not be bounds
checked yet.

Bounds checked pointers must be of type Unsafe[Mutable]Pointer[?]<T>
or Unsafe[Mutable]RawPointer[?]. Count expressions must conform to
the BinaryInteger protocol, and have an initializer with signature
"init(exactly: Int) -> T?" (or accept a supertype of Int).

rdar://137628612
@hnrklssn hnrklssn force-pushed the pointer-bounds-macro branch from 65898f3 to fed9049 Compare November 7, 2024 23:58
@hnrklssn
Copy link
Contributor Author

hnrklssn commented Nov 7, 2024

@swift-ci Please smoke test

1 similar comment
@hnrklssn
Copy link
Contributor Author

hnrklssn commented Nov 8, 2024

@swift-ci Please smoke test

Copy link
Member

@DougGregor DougGregor left a comment

Choose a reason for hiding this comment

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

Bah, Python lint issue due to the long name

@DougGregor
Copy link
Member

@swift-ci please smoke test

@DougGregor
Copy link
Member

@hnrklssn theSpan PR landed before this :)

error: 'Span' is ambiguous for type lookup in this context

@hnrklssn
Copy link
Contributor Author

hnrklssn commented Nov 8, 2024

@swift-ci Please smoke test

@DougGregor
Copy link
Member

@swift-ci please smoke test Windows

}

// CHECK: @_alwaysEmitIntoClient
// CHECK-NEXT: func myFunc(_ ptr: MutableSpan<CInt>) {
Copy link
Member

Choose a reason for hiding this comment

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

MutableSpan isn't actually defined anywhere. Not sure why the Windows tests are crashing here, but at this point it isn't expected to really work.

Copy link
Member

@DougGregor DougGregor left a comment

Choose a reason for hiding this comment

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

XFAIL'ing tests that need more standard library support to work properly

@DougGregor
Copy link
Member

@swift-ci please smoke test

@@ -1,6 +1,6 @@
// REQUIRES: swift_swift_parser
// REQUIRES: pointer_bounds

// XFAIL: OS=windows-msvc
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@DougGregor if MutableSpan is unavailable, shouldn't we XFAIL this on all platforms?

Copy link
Member

Choose a reason for hiding this comment

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

I suspect that the macro is being expanded but nothing ever type-checks it, so it's "working" for now but will fail with subsequent changes. We can disable then.

@hnrklssn hnrklssn merged commit 0678829 into swiftlang:main Nov 11, 2024
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants