Skip to content

Commit f6adb06

Browse files
author
Gabor Horvath
committed
Document features around safe interoperability
This PR adds a new top-level page describing how to interact with C++ from strict memory safe Swift.
1 parent bedf6b2 commit f6adb06

File tree

2 files changed

+317
-0
lines changed

2 files changed

+317
-0
lines changed

_data/documentation.yaml

+6
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,12 @@
4848
description: |
4949
Swift has support for bidirectional interoperability with C++.
5050
A great variety of C++ APIs can be called directly from Swift, and select Swift APIs can be used from C++.
51+
- title: Mixing Strict Memory Safe Swift and C++
52+
url: /documentation/cxx-interop/safe-interop
53+
description: |
54+
Strict memory safe Swift makes code more auditable for memory safety errors by explicitly delineating potentially
55+
unsafe code from safe code. Using the right annotations we can safely interact with C++ from Swift without the need
56+
to sprinkle `unsafe` before every C++ construct.
5157
- title: Value and Reference types
5258
url: /documentation/articles/value-and-reference-types.html
5359
description: |
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,311 @@
1+
---
2+
layout: page
3+
title: Mixing Strict Memory Safe Swift and C++
4+
official_url: https://swift.org/documentation/cxx-interop/safe-interop/
5+
redirect_from:
6+
- /documentation/cxx-interop/safe-interop.html
7+
---
8+
9+
## Table of Contents
10+
{:.no_toc}
11+
12+
* TOC
13+
{:toc}
14+
15+
## Introduction
16+
17+
Swift's [strict memory safety mode](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0458-strict-memory-safety.md)
18+
is a new feature in Swift 6.2 to make code easier to audit for memory safety.
19+
This document describes how to ergonomically interact with Swift by importing
20+
C++ construct safely. All of the features in this document work in regular Swift
21+
but they provide more value in strict memory safe mode.
22+
23+
* * *
24+
25+
<div class="info" markdown="1">
26+
C++ interoperability is an actively evolving feature of Swift.
27+
Future releases of Swift might change how Swift and C++
28+
interoperate,
29+
as the Swift community gathers feedback from real world adoption of C++
30+
interoperability in mixed Swift and C++ codebases.
31+
Please provide the feedback that you have on the
32+
[Swift Forums](https://forums.swift.org/c/development/c-interoperability/), or
33+
by filing an [issue on GitHub](https://github.com/swiftlang/swift/issues/new/choose).
34+
Future changes to the design or functionality of C++ interoperability will not
35+
break code in existing codebases [by default](#source-stability-guarantees-for-mixed-language-codebases).
36+
</div>
37+
38+
## Overview
39+
40+
Swift provides memory safety with a combination of language affordances and runtime checking.
41+
However, Swift also deliberately includes some unsafe constructs, such as the `UnsafePointer` and `UnsafeMutablePointer`
42+
types in the standard library.
43+
Swift occasionally needs additional information that is not present in the C++ type and API declarations
44+
to safely interface with them. This document describes how such code needs to be annotated.
45+
46+
### Annotating foreign types
47+
48+
Types imported from C++ are considered foreign to Swift. Many of these types are considered safe,
49+
including the built-in integral types like `int`, some standard library types like `std::string`,
50+
and aggregate types built from other safe types.
51+
52+
On the other hand, some C++ types are imported as unsafe by default. Consider the following C++ type
53+
and APIs:
54+
55+
```c++
56+
class StringRef {
57+
public:
58+
...
59+
private:
60+
const char* ptr;
61+
size_t len;
62+
};
63+
64+
std::string normalize(const std::string& path);
65+
66+
StringRef fileName(const std::string& normalizedPath);
67+
```
68+
69+
Let's try to use them from Swift with strict memory safety enabled:
70+
71+
```swift
72+
func getFileName(_ path: borrowing std.string) -> StringRef {
73+
let normalizedPath = normalize(path)
74+
return fileName(normalizedPath)
75+
}
76+
```
77+
78+
Building this code will emit a warning that the `fileName` call is unsafe because
79+
it references the unsafe type `StringRef`. Swift considers `StringRef` unsafe because
80+
it has a pointer member. Types like `StringRef` can dangle, so we need to take extra
81+
care using them, making sure the referenced buffer outlives the `StringRef` object.
82+
83+
Swift's [non-escapable types](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0446-non-escapable.md)
84+
can also have lifetime dependencies, just like `StringRef`. However, the Swift compiler
85+
can track these dependencies and enforce safety at compile time. To import `StringRef`
86+
as a safe type we need to mark it as a non-escapable type, we can annotate the class
87+
definition:
88+
89+
```c++
90+
class SWIFT_NONESCAPABLE StringRef { ... };
91+
```
92+
93+
Now the Swift compiler imports `StringRef` as a safe type and no longer
94+
emits a warning about using an unsafe type.
95+
96+
### Annotating APIs
97+
98+
Building the code again will emit a new diagnostic for the `fileName` function about
99+
missing lifetime annotations. Functions returning non-escapable types need annotations
100+
to describe their lifetime contracts via [lifetimebound](https://clang.llvm.org/docs/AttributeReference.html#id11)
101+
and [lifetime_capture_by](https://clang.llvm.org/docs/AttributeReference.html#lifetime-capture-by) annotations.
102+
103+
```c++
104+
StringRef fileName(const std::string& normalizedPath [[clang::lifetimebound]]);
105+
```
106+
107+
Adding this annotation to `fileName` indicates that the returned `StringRef` value has the
108+
same lifetime as the argument of the `fileName` function.
109+
110+
Building the project again reveals a lifetime error in the Swift function:
111+
112+
```swift
113+
func getFileName(_ path: borrowing std.string) -> StringRef {
114+
let normalizedPath = normalize(path)
115+
// error: lifetime-dependent value escapes local scope
116+
// note: depends on `normalizedPath`
117+
return fileName(normalizedPath)
118+
}
119+
```
120+
121+
The value returned by `fileName` will dangle after the lifetime of `normalizedPath` ends.
122+
We can fix this error by pushing the task of normalizing a path to the callee:
123+
124+
```swift
125+
// Path needs to be normalized.
126+
func getFileName(_ path: borrowing std.string) -> StringRef {
127+
return fileName(normalizedPath)
128+
}
129+
```
130+
131+
Or we could return an `Escapable` value like `std.string` instead of a dangling `StringRef`:
132+
133+
```swift
134+
func getFileName(_ path: borrowing std.string) -> std.string {
135+
let normalizedPath = normalize(path)
136+
let ref = fileName(normalizedPath)
137+
return ref.toString()
138+
}
139+
```
140+
141+
After annotating the C++ code, the Swift compiler can enforce the lifetime
142+
contracts helping us to write code that is free of memory safety errors.
143+
144+
## Escapability annotations in detail
145+
146+
Currently, unannotated types are imported as `Escapable` to maintain backward
147+
compatibility. This might change in the future under a new interoperability version.
148+
We have already seen that we can import a type as `~Escapable` to Swift by adding
149+
the `SWIFT_NONESCAPABLE` annotation:
150+
151+
```c++
152+
struct SWIFT_NONESCAPABLE View {
153+
View() : member(nullptr) {}
154+
View(const int *p) : member(p) {}
155+
View(const View&) = default;
156+
private:
157+
const int *member;
158+
};
159+
```
160+
161+
Moreover, we can explicitly mark types as `Escapable` using the `SWIFT_ESCAPABLE`
162+
annotation:
163+
164+
```c++
165+
struct SWIFT_ESCAPABLE Owner {
166+
...
167+
};
168+
```
169+
170+
The main reason for explicitly annotating a type as `SWIFT_ESCAPABLE` is to make sure
171+
it is considered as a safe type when used from Swift. Functions returning escapable
172+
types do not need lifetime annotations.
173+
174+
Escapability annotations can also be attached to types via APINotes:
175+
176+
```
177+
Tags:
178+
- Name: NonEscapableType
179+
SwiftEscapable: false
180+
- Name: EscapableType
181+
SwiftEscapable: true
182+
```
183+
184+
In case of template instantiations the escapability of a type can depend on the
185+
template arguments:
186+
187+
```c++
188+
MyList<View> f();
189+
MyList<Owner> g();
190+
```
191+
192+
In this example, `MyList<View>` should be imported as `~Escapable` while `MyList<Owner>`
193+
should be imported as `Escapable`. This can be achieved via conditional escapability
194+
annotations:
195+
196+
```
197+
template<typename T>
198+
struct SWIFT_ESCAPABLE_IF(T) MyList {
199+
...
200+
};
201+
```
202+
203+
## Lifetime annotations in detail
204+
205+
The `lifetimebound` attribute can be used to annotate code in various scenarios.
206+
On a constructor, it describes the lifetime of the created object:
207+
208+
```c++
209+
struct SWIFT_NONESCAPABLE View {
210+
View(const int *p [[clang::lifetimebound]]) : member(p) {}
211+
private:
212+
const int *member;
213+
};
214+
```
215+
216+
In case the attribute is after the method signature, the returned object has
217+
the same lifetime as the `this` object.
218+
219+
```c++
220+
struct Owner {
221+
int data;
222+
223+
View handOutView() const [[clang::lifetimebound]] {
224+
return View(&data);
225+
}
226+
};
227+
```
228+
229+
In case the attribute is applied to a subset of the formal parameters, the return
230+
value might depend on the corresponding arguments:
231+
232+
```c++
233+
View getView(const Owner& owner [[clang::lifetimebound]]) {
234+
return View(&owner.data);
235+
}
236+
237+
View getViewFromFirst(const Owner& owner [[clang::lifetimebound]], const Owner& owner2) {
238+
return View(&owner.data);
239+
}
240+
241+
View getViewFromEither(View view1 [[clang::lifetimebound]], View view2 [[clang::lifetimebound]]) {
242+
if (coinFlip)
243+
return view1;
244+
else
245+
return view2;
246+
}
247+
```
248+
249+
Occasionally, a function might return a non-escapable type that in fact has no dependency on any other values.
250+
These types might point to static data or might represent an empty sequence or lack of data.
251+
Such functions need to be annotated with `SWIFT_RETURNS_INDEPENDENT_VALUE`:
252+
253+
```c++
254+
View returnsEmpty() SWIFT_RETURNS_INDEPENDENT_VALUE {
255+
return View();
256+
}
257+
```
258+
259+
Notably, the default constructor of a type is always assumed to create an independent value.
260+
261+
We can also annotate `lifetimebound` APIs via APINotes. The `-1` index represents the this position.
262+
263+
```
264+
Tags:
265+
- Name: MyClass
266+
Methods:
267+
- Name: annotateThis
268+
Parameters:
269+
- Position: -1
270+
Lifetimebound: true
271+
- Name: methodToAnnotate
272+
Parameters:
273+
- Position: 0
274+
Lifetimebound: true
275+
```
276+
277+
Note that APINotes have some limitations around C++, they do not support overloaded functions.
278+
279+
We can use `lifetime_capture_by` annotations for output arguments.
280+
281+
```c++
282+
void copyView(View view1 [[clang::lifetime_capture_by(view2)]], View &view2) {
283+
view2 = view1;
284+
}
285+
286+
struct SWIFT_NONESCAPABLE CaptureView {
287+
CaptureView() : view(nullptr) {}
288+
CaptureView(View p [[clang::lifetimebound]]) : view(p) {}
289+
290+
void captureView(View v [[clang::lifetime_capture_by(this)]]) {
291+
view = v;
292+
}
293+
294+
void handOut(View &v) const [[clang::lifetime_capture_by(v)]] {
295+
v = view;
296+
}
297+
298+
View view;
299+
};
300+
```
301+
302+
All of the non-escapable inputs need lifetime annotations for a function to be
303+
considered safe. If an input never escapes from the called function we can use
304+
the `noescape` annotation.
305+
306+
```c++
307+
void is_palindrome(std::span<int> s [[clang::noescape]]);
308+
```
309+
310+
## Convenience overloads for annotated spans and pointers
311+

0 commit comments

Comments
 (0)