From f6adb06d2f4dcfda02d8c628db477e414ea6bad7 Mon Sep 17 00:00:00 2001 From: Gabor Horvath Date: Fri, 4 Apr 2025 16:09:23 +0100 Subject: [PATCH 1/2] Document features around safe interoperability This PR adds a new top-level page describing how to interact with C++ from strict memory safe Swift. --- _data/documentation.yaml | 6 + .../cxx-interop/safe-interop/index.md | 311 ++++++++++++++++++ 2 files changed, 317 insertions(+) create mode 100644 documentation/cxx-interop/safe-interop/index.md diff --git a/_data/documentation.yaml b/_data/documentation.yaml index 850ccad66..0f3b775c2 100644 --- a/_data/documentation.yaml +++ b/_data/documentation.yaml @@ -48,6 +48,12 @@ description: | Swift has support for bidirectional interoperability with C++. A great variety of C++ APIs can be called directly from Swift, and select Swift APIs can be used from C++. + - title: Mixing Strict Memory Safe Swift and C++ + url: /documentation/cxx-interop/safe-interop + description: | + Strict memory safe Swift makes code more auditable for memory safety errors by explicitly delineating potentially + unsafe code from safe code. Using the right annotations we can safely interact with C++ from Swift without the need + to sprinkle `unsafe` before every C++ construct. - title: Value and Reference types url: /documentation/articles/value-and-reference-types.html description: | diff --git a/documentation/cxx-interop/safe-interop/index.md b/documentation/cxx-interop/safe-interop/index.md new file mode 100644 index 000000000..75f6b5b26 --- /dev/null +++ b/documentation/cxx-interop/safe-interop/index.md @@ -0,0 +1,311 @@ +--- +layout: page +title: Mixing Strict Memory Safe Swift and C++ +official_url: https://swift.org/documentation/cxx-interop/safe-interop/ +redirect_from: +- /documentation/cxx-interop/safe-interop.html +--- + +## Table of Contents +{:.no_toc} + +* TOC +{:toc} + +## Introduction + +Swift's [strict memory safety mode](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0458-strict-memory-safety.md) +is a new feature in Swift 6.2 to make code easier to audit for memory safety. +This document describes how to ergonomically interact with Swift by importing +C++ construct safely. All of the features in this document work in regular Swift +but they provide more value in strict memory safe mode. + +* * * + +
+C++ interoperability is an actively evolving feature of Swift. +Future releases of Swift might change how Swift and C++ +interoperate, +as the Swift community gathers feedback from real world adoption of C++ +interoperability in mixed Swift and C++ codebases. +Please provide the feedback that you have on the +[Swift Forums](https://forums.swift.org/c/development/c-interoperability/), or +by filing an [issue on GitHub](https://github.com/swiftlang/swift/issues/new/choose). +Future changes to the design or functionality of C++ interoperability will not +break code in existing codebases [by default](#source-stability-guarantees-for-mixed-language-codebases). +
+ +## Overview + +Swift provides memory safety with a combination of language affordances and runtime checking. +However, Swift also deliberately includes some unsafe constructs, such as the `UnsafePointer` and `UnsafeMutablePointer` +types in the standard library. +Swift occasionally needs additional information that is not present in the C++ type and API declarations +to safely interface with them. This document describes how such code needs to be annotated. + +### Annotating foreign types + +Types imported from C++ are considered foreign to Swift. Many of these types are considered safe, +including the built-in integral types like `int`, some standard library types like `std::string`, +and aggregate types built from other safe types. + +On the other hand, some C++ types are imported as unsafe by default. Consider the following C++ type +and APIs: + +```c++ +class StringRef { +public: + ... +private: + const char* ptr; + size_t len; +}; + +std::string normalize(const std::string& path); + +StringRef fileName(const std::string& normalizedPath); +``` + +Let's try to use them from Swift with strict memory safety enabled: + +```swift +func getFileName(_ path: borrowing std.string) -> StringRef { + let normalizedPath = normalize(path) + return fileName(normalizedPath) +} +``` + +Building this code will emit a warning that the `fileName` call is unsafe because +it references the unsafe type `StringRef`. Swift considers `StringRef` unsafe because +it has a pointer member. Types like `StringRef` can dangle, so we need to take extra +care using them, making sure the referenced buffer outlives the `StringRef` object. + +Swift's [non-escapable types](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0446-non-escapable.md) +can also have lifetime dependencies, just like `StringRef`. However, the Swift compiler +can track these dependencies and enforce safety at compile time. To import `StringRef` +as a safe type we need to mark it as a non-escapable type, we can annotate the class +definition: + +```c++ +class SWIFT_NONESCAPABLE StringRef { ... }; +``` + +Now the Swift compiler imports `StringRef` as a safe type and no longer +emits a warning about using an unsafe type. + +### Annotating APIs + +Building the code again will emit a new diagnostic for the `fileName` function about +missing lifetime annotations. Functions returning non-escapable types need annotations +to describe their lifetime contracts via [lifetimebound](https://clang.llvm.org/docs/AttributeReference.html#id11) +and [lifetime_capture_by](https://clang.llvm.org/docs/AttributeReference.html#lifetime-capture-by) annotations. + +```c++ +StringRef fileName(const std::string& normalizedPath [[clang::lifetimebound]]); +``` + +Adding this annotation to `fileName` indicates that the returned `StringRef` value has the +same lifetime as the argument of the `fileName` function. + +Building the project again reveals a lifetime error in the Swift function: + +```swift +func getFileName(_ path: borrowing std.string) -> StringRef { + let normalizedPath = normalize(path) + // error: lifetime-dependent value escapes local scope + // note: depends on `normalizedPath` + return fileName(normalizedPath) +} +``` + +The value returned by `fileName` will dangle after the lifetime of `normalizedPath` ends. +We can fix this error by pushing the task of normalizing a path to the callee: + +```swift +// Path needs to be normalized. +func getFileName(_ path: borrowing std.string) -> StringRef { + return fileName(normalizedPath) +} +``` + +Or we could return an `Escapable` value like `std.string` instead of a dangling `StringRef`: + +```swift +func getFileName(_ path: borrowing std.string) -> std.string { + let normalizedPath = normalize(path) + let ref = fileName(normalizedPath) + return ref.toString() +} +``` + +After annotating the C++ code, the Swift compiler can enforce the lifetime +contracts helping us to write code that is free of memory safety errors. + +## Escapability annotations in detail + +Currently, unannotated types are imported as `Escapable` to maintain backward +compatibility. This might change in the future under a new interoperability version. +We have already seen that we can import a type as `~Escapable` to Swift by adding +the `SWIFT_NONESCAPABLE` annotation: + +```c++ +struct SWIFT_NONESCAPABLE View { + View() : member(nullptr) {} + View(const int *p) : member(p) {} + View(const View&) = default; +private: + const int *member; +}; +``` + +Moreover, we can explicitly mark types as `Escapable` using the `SWIFT_ESCAPABLE` +annotation: + +```c++ +struct SWIFT_ESCAPABLE Owner { + ... +}; +``` + +The main reason for explicitly annotating a type as `SWIFT_ESCAPABLE` is to make sure +it is considered as a safe type when used from Swift. Functions returning escapable +types do not need lifetime annotations. + +Escapability annotations can also be attached to types via APINotes: + +``` +Tags: +- Name: NonEscapableType + SwiftEscapable: false +- Name: EscapableType + SwiftEscapable: true +``` + +In case of template instantiations the escapability of a type can depend on the +template arguments: + +```c++ +MyList f(); +MyList g(); +``` + +In this example, `MyList` should be imported as `~Escapable` while `MyList` +should be imported as `Escapable`. This can be achieved via conditional escapability +annotations: + +``` +template +struct SWIFT_ESCAPABLE_IF(T) MyList { + ... +}; +``` + +## Lifetime annotations in detail + +The `lifetimebound` attribute can be used to annotate code in various scenarios. +On a constructor, it describes the lifetime of the created object: + +```c++ +struct SWIFT_NONESCAPABLE View { + View(const int *p [[clang::lifetimebound]]) : member(p) {} +private: + const int *member; +}; +``` + +In case the attribute is after the method signature, the returned object has +the same lifetime as the `this` object. + +```c++ +struct Owner { + int data; + + View handOutView() const [[clang::lifetimebound]] { + return View(&data); + } +}; +``` + +In case the attribute is applied to a subset of the formal parameters, the return +value might depend on the corresponding arguments: + +```c++ +View getView(const Owner& owner [[clang::lifetimebound]]) { + return View(&owner.data); +} + +View getViewFromFirst(const Owner& owner [[clang::lifetimebound]], const Owner& owner2) { + return View(&owner.data); +} + +View getViewFromEither(View view1 [[clang::lifetimebound]], View view2 [[clang::lifetimebound]]) { + if (coinFlip) + return view1; + else + return view2; +} +``` + +Occasionally, a function might return a non-escapable type that in fact has no dependency on any other values. +These types might point to static data or might represent an empty sequence or lack of data. +Such functions need to be annotated with `SWIFT_RETURNS_INDEPENDENT_VALUE`: + +```c++ +View returnsEmpty() SWIFT_RETURNS_INDEPENDENT_VALUE { + return View(); +} +``` + +Notably, the default constructor of a type is always assumed to create an independent value. + +We can also annotate `lifetimebound` APIs via APINotes. The `-1` index represents the this position. + +``` +Tags: +- Name: MyClass + Methods: + - Name: annotateThis + Parameters: + - Position: -1 + Lifetimebound: true + - Name: methodToAnnotate + Parameters: + - Position: 0 + Lifetimebound: true +``` + +Note that APINotes have some limitations around C++, they do not support overloaded functions. + +We can use `lifetime_capture_by` annotations for output arguments. + +```c++ +void copyView(View view1 [[clang::lifetime_capture_by(view2)]], View &view2) { + view2 = view1; +} + +struct SWIFT_NONESCAPABLE CaptureView { + CaptureView() : view(nullptr) {} + CaptureView(View p [[clang::lifetimebound]]) : view(p) {} + + void captureView(View v [[clang::lifetime_capture_by(this)]]) { + view = v; + } + + void handOut(View &v) const [[clang::lifetime_capture_by(v)]] { + v = view; + } + + View view; +}; +``` + +All of the non-escapable inputs need lifetime annotations for a function to be +considered safe. If an input never escapes from the called function we can use +the `noescape` annotation. + +```c++ +void is_palindrome(std::span s [[clang::noescape]]); +``` + +## Convenience overloads for annotated spans and pointers + From 753f8696bcc6ea7839948030b0dd3b29b824868d Mon Sep 17 00:00:00 2001 From: Gabor Horvath Date: Thu, 10 Apr 2025 13:05:39 +0100 Subject: [PATCH 2/2] Extend the later part of the documentation a bit. --- _data/documentation.yaml | 6 - documentation/cxx-interop/index.md | 6 + .../cxx-interop/safe-interop/index.md | 168 ++++++++++++++---- 3 files changed, 135 insertions(+), 45 deletions(-) diff --git a/_data/documentation.yaml b/_data/documentation.yaml index 0f3b775c2..850ccad66 100644 --- a/_data/documentation.yaml +++ b/_data/documentation.yaml @@ -48,12 +48,6 @@ description: | Swift has support for bidirectional interoperability with C++. A great variety of C++ APIs can be called directly from Swift, and select Swift APIs can be used from C++. - - title: Mixing Strict Memory Safe Swift and C++ - url: /documentation/cxx-interop/safe-interop - description: | - Strict memory safe Swift makes code more auditable for memory safety errors by explicitly delineating potentially - unsafe code from safe code. Using the right annotations we can safely interact with C++ from Swift without the need - to sprinkle `unsafe` before every C++ construct. - title: Value and Reference types url: /documentation/articles/value-and-reference-types.html description: | diff --git a/documentation/cxx-interop/index.md b/documentation/cxx-interop/index.md index dec9d631e..9d2b0b729 100644 --- a/documentation/cxx-interop/index.md +++ b/documentation/cxx-interop/index.md @@ -1331,6 +1331,12 @@ automatically. ## Working with C++ References and View Types in Swift +Swift has a new [safe interoperability](https://www.swift.org/documentation/cxx-interop/safe-interop) +model under development that makes code auditable for memory safety errors by explicitly delineating potentially +unsafe code from safe code and introducing ways to annotate lifetime contracts. We recommend giving this +new mode of interoperability a try as the rest of this section might be subsumed by these new features under +a new interoperability version in the future. + As outlined [earlier](#member-functions-returning-references-are-unsafe-by-default), member functions that return references, pointers, or certain structures/classes that diff --git a/documentation/cxx-interop/safe-interop/index.md b/documentation/cxx-interop/safe-interop/index.md index 75f6b5b26..23d2b5d32 100644 --- a/documentation/cxx-interop/safe-interop/index.md +++ b/documentation/cxx-interop/safe-interop/index.md @@ -14,10 +14,10 @@ redirect_from: ## Introduction -Swift's [strict memory safety mode](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0458-strict-memory-safety.md) -is a new feature in Swift 6.2 to make code easier to audit for memory safety. This document describes how to ergonomically interact with Swift by importing -C++ construct safely. All of the features in this document work in regular Swift +C++ construct safely. Swift's [strict memory safety mode](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0458-strict-memory-safety.md) +is a new feature under development to make code easier to audit for memory safety. +All of the features in this document work in regular Swift but they provide more value in strict memory safe mode. * * * @@ -40,17 +40,28 @@ break code in existing codebases [by default](#source-stability-guarantees-for-m Swift provides memory safety with a combination of language affordances and runtime checking. However, Swift also deliberately includes some unsafe constructs, such as the `UnsafePointer` and `UnsafeMutablePointer` types in the standard library. -Swift occasionally needs additional information that is not present in the C++ type and API declarations +In some cases, Swift needs additional information that is not present in the C++ type and API declarations to safely interface with them. This document describes how such code needs to be annotated. ### Annotating foreign types -Types imported from C++ are considered foreign to Swift. Many of these types are considered safe, -including the built-in integral types like `int`, some standard library types like `std::string`, -and aggregate types built from other safe types. - -On the other hand, some C++ types are imported as unsafe by default. Consider the following C++ type -and APIs: +Types imported from C++ are considered foreign to Swift. +Types imported from C++ are considered foreign to Swift. Normally, most C++ types are imported into Swift +without any restriction. However, a small set of C++ APIs e.g. pointers/references and methods returning +pointers will be imported as unsafe (section [Working with C++ references and view types in Swift](https://www.swift.org/documentation/cxx-interop/#working-with-c-references-and-view-types-in-swift) +explains this in more detail.) Under the strict memory safe mode, the compiler will flip the polarity and +treat all types that are not known to be safe as unsafe, and will diagnose uses of them. In this section, +we will show how to annotate unsafe C++ types so that they can be accessed safely and correctly from Swift. +Note that the features here are agnostic to whether strictly-safe mode is on or off. When the strictly safe +mode is on, the compiler warnings can serve as a guide to properly annotate C++ types and also help ensure +that the code doesn't use unsafe APIs anywhere. When the strictly-memory-safe mode is off, it is still +recommended to adopt these annotation whereever appropriate, especially on C++ types that are potentially +lifetime dependent on other objects. + +Under strictly-memory-safe mode the built-in integral types like `int`, some standard library types like `std::string`, +and aggregate types built from other safe types are considered safe. Whereas all other unannotated types +are considered unsafe. Let's see what happens when we are trying to use an unannotated type in +strictly-safe mode. Consider the following C++ type and APIs: ```c++ class StringRef { @@ -143,28 +154,25 @@ contracts helping us to write code that is free of memory safety errors. ## Escapability annotations in detail -Currently, unannotated types are imported as `Escapable` to maintain backward +Under the strictly-safe mode, even though compiler warns on unannotated types, +they are imported as if they are `Escapable` to maintain backward compatibility. This might change in the future under a new interoperability version. We have already seen that we can import a type as `~Escapable` to Swift by adding the `SWIFT_NONESCAPABLE` annotation: ```c++ struct SWIFT_NONESCAPABLE View { - View() : member(nullptr) {} View(const int *p) : member(p) {} - View(const View&) = default; private: const int *member; }; ``` Moreover, we can explicitly mark types as `Escapable` using the `SWIFT_ESCAPABLE` -annotation: +annotation to express that they are not lifetime dependent on any other values: ```c++ -struct SWIFT_ESCAPABLE Owner { - ... -}; +struct SWIFT_ESCAPABLE Owner { ... }; ``` The main reason for explicitly annotating a type as `SWIFT_ESCAPABLE` is to make sure @@ -193,28 +201,63 @@ In this example, `MyList` should be imported as `~Escapable` while `MyList should be imported as `Escapable`. This can be achieved via conditional escapability annotations: -``` +```c++ template struct SWIFT_ESCAPABLE_IF(T) MyList { ... }; ``` +Here, instantiations of `MyList` are imported as `Escapable` when `T` is substituted +with an `Escapable` type. + +The `SWIFT_ESCAPABLE_IF` macro can take multiple template parameters: + +```c++ +template +struct SWIFT_ESCAPABLE_IF(F, S) MyPair { + F first; + S second; +}; +``` + +`MyPair` instantiations are only imported as `Escapable` if both template arguments +are `Escapable`. + +`Escapable` types cannot have `~Escapable` fields. The following code snippet will +trigger a compiler error: + +```c++ +struct SWIFT_NONESCAPABLE View { ... }; +struct SWIFT_ESCAPABLE Owner { + View v; +}; +``` + +Escapability annotations will not only help the Swift compiler to import C++ types +safely, it will also help discover missing lifetime annotations as all `~Escapable` +parameters and return values need to be annotated in an API to make its use safe in +Swift. + ## Lifetime annotations in detail -The `lifetimebound` attribute can be used to annotate code in various scenarios. -On a constructor, it describes the lifetime of the created object: +The `lifetimebound` attribute on a function parameter or implicit object parameter +indicates that the returned object's lifetime could end when any of the `lifetimebound` +annotated parameters' lifetime ended. +This annotation a constructor describes the lifetime of the created object: ```c++ struct SWIFT_NONESCAPABLE View { View(const int *p [[clang::lifetimebound]]) : member(p) {} -private: - const int *member; + ... }; ``` +In this example, the object initialized by the `View` constructor has the same +lifetime as the input argument of the constructor. + In case the attribute is after the method signature, the returned object has -the same lifetime as the `this` object. +the same lifetime as the implicit `this` parameter. ```c++ struct Owner { @@ -226,19 +269,17 @@ struct Owner { }; ``` -In case the attribute is applied to a subset of the formal parameters, the return +Consider a call site like `View v = o.handOutView()`. The `v` object has the same lifetime +as `o`. + +In case the attribute is applied to a subset of the parameters, the return value might depend on the corresponding arguments: ```c++ -View getView(const Owner& owner [[clang::lifetimebound]]) { - return View(&owner.data); -} - -View getViewFromFirst(const Owner& owner [[clang::lifetimebound]], const Owner& owner2) { - return View(&owner.data); -} - -View getViewFromEither(View view1 [[clang::lifetimebound]], View view2 [[clang::lifetimebound]]) { +View getOneOfTheViews(const Owner& owner1 [[clang::lifetimebound]], const Owner& owner2 + View view1 [[clang::lifetimebound]], View view2 [[clang::lifetimebound]]) { + if (coinFlip) + return View(&owner1.data); if (coinFlip) return view1; else @@ -246,7 +287,10 @@ View getViewFromEither(View view1 [[clang::lifetimebound]], View view2 [[clang:: } ``` -Occasionally, a function might return a non-escapable type that in fact has no dependency on any other values. +Here, the returned `View`'s lifetime depends on `owner`, `view1`, and `view2` but it cannot +depend on `owner2`. + +Occasionally, a function might return a non-escapable type that has no dependency on any other values. These types might point to static data or might represent an empty sequence or lack of data. Such functions need to be annotated with `SWIFT_RETURNS_INDEPENDENT_VALUE`: @@ -276,17 +320,25 @@ Tags: Note that APINotes have some limitations around C++, they do not support overloaded functions. -We can use `lifetime_capture_by` annotations for output arguments. +While `lifetimebound` always describes the lifetime dependencies of the return value (or +the constructed object in case of constructors), we can use can use `lifetime_capture_by` +annotation to descibe the lifetime of other output values, like output/inout arguments +or globals. ```c++ void copyView(View view1 [[clang::lifetime_capture_by(view2)]], View &view2) { view2 = view1; } +``` -struct SWIFT_NONESCAPABLE CaptureView { - CaptureView() : view(nullptr) {} - CaptureView(View p [[clang::lifetimebound]]) : view(p) {} +In this example, `view2` will have get all of the lifetime dependencies of `view1` +after a call to `copyView`. a + +We can annotate dependency captured by the implicit `this` object, or +an inout argument capturing `this`: +```c++ +struct SWIFT_NONESCAPABLE CaptureView { void captureView(View v [[clang::lifetime_capture_by(this)]]) { view = v; } @@ -301,11 +353,49 @@ struct SWIFT_NONESCAPABLE CaptureView { All of the non-escapable inputs need lifetime annotations for a function to be considered safe. If an input never escapes from the called function we can use -the `noescape` annotation. +the `noescape` annotation: ```c++ void is_palindrome(std::span s [[clang::noescape]]); ``` +While the annotations in this section are powerful, they cannot express all of +the lifetime contracts. APIs with inexpressible contracts can be used from Swift, +but they are imported as unsafe APIs and need extra care from the developers +to manually guarantee safety. + ## Convenience overloads for annotated spans and pointers +C++ APIs often using standard library types or other constructs like a +pointer and a size to represent buffers that have Swift equivalents like +Swift's `Span` type. These Swift types have additional requirements and +guarantees. When these properties are properly annotated on the C++ side, +the Swift compiler can introduce safe convenience functions to make +interacting with the C++ APIs as effortless as if they were written in Swift. + +### C++ span support + +APIs taking/returning C++'s `std::span` with sufficient lifetime +annotations will automatically get overloads taking/returning Swift +`Span`. + +The following table summarizes the generated convenience overloads: + +```c++ +using IntSpan = std::span; +using IntVec = std::vector; +``` + +| C++ API | Generated Swift overload | +| --------------------------------------------------------- | -------------------------------------------------------------------- | +| `void takeSpan(IntSpan x [[clang::noescape]]);` | `func takeSpan(_ x: Span)` | +| `IntSpan changeSpan(IntSpan x [[clang::lifetimebound]]);` | `@lifetime(x) func changeSpan(_ x: Span) -> Span` | +| `IntSpan changeSpan(IntVec& x [[clang::lifetimebound]]);` | `@lifetime(x) func changeSpan(_ x: borrowing IntVec) -> Span` | +| `IntSpan Owner::getSpan() [[clang::lifetimebound]];` | `@lifetime(self) func getSpan() -> Span` | + +These transformations only support top level `std::span`s, we do not +transform the nested cases. A `std::span` of a non-const type `T` will +be transformed to `MutableSpan` on the Swift wide. + +### Annotated pointers +