Safely Mixing Swift and C/C++
Table of Contents
- Introduction
- Overview
- Escapability Annotations in Detail
- Lifetime Annotations in Detail
- Safe Overloads for Annotated Spans and Pointers
Introduction
This document describes the additional memory safety features that Swift provides when interoperating with C and C++. It describes the ways to make Swift aware of certain memory safety aspects of your codebase, as well as how Swift uses that knowledge to prevent common memory safety issues in your code.
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, or by filing an issue on GitHub. Future changes to the design or functionality of C++ interoperability will not break code in existing codebases by default.
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.
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 so that it can be used in strict safety mode.
Annotating Foreign Types
Normally, C++ types imported into Swift are assumed to not impact Swift’s memory
safety, and can be used without safety-related restrictions. 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
explains this in more detail.) Under the strict memory safety mode, the compiler will flip the polarity of its safety assumptions.
It will treat all types that are not known to be safe as unsafe, and emit diagnostics for usage of unsafe types without the unsafe
keyword.
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 strict safety mode is on or off. When the strict safety
mode is on, the compiler diagnostics can serve as a guide on how to properly annotate C++ types, and also help ensure
that the code doesn’t use unsafe APIs anywhere. When the strict safety mode is turned off, it is still
recommended to adopt these annotation wherever appropriate, especially on C++ types that are potentially
dependent on the lifetimes of other objects.
Under the strict safety mode, built-in numeric types like int
, some standard library types like std::string
,
and aggregate types built from other safe types are considered safe. All other unannotated types
are considered unsafe.
Let’s see what happens when we are trying to use an unannotated type in strict safety mode. Consider the following C++ type and APIs:
struct 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:
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. Pointers in types like StringRef
can dangle, so we need to take extra
care that the buffer they point to outlives the StringRef
object they are encapsulated by.
Swift’s non-escapable types
can have lifetime dependencies. 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 by annotating the class definition:
struct SWIFT_NONESCAPABLE StringRef { ... };
Now the Swift compiler imports StringRef
as a safe type and no longer
emits a warning about using an unsafe type.
Some containers and protocols do not yet support non-escapable types in Swift 6.2.
Annotating C++ APIs
Building the code again will emit a new diagnostic for the fileName
function about
missing lifetime annotations. C and C++ functions that return non-escapable types need annotations
to describe their lifetime contracts via lifetimebound
and lifetime_capture_by annotations.
Not all versions of C and C++ support the [[clang::lifetimebound]]
attribute syntax. Convenience macros for
lifetime annotations using the GNU style attribute syntax are available in the lifetimebound.h
header, and we’ll
be using them throughout this document.
StringRef fileName(const std::string &normalizedPath __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 that calls fileName
:
func getFileName(_ path: borrowing std.string) -> StringRef {
let normalizedPath = normalize(path)
return fileName(normalizedPath)
// error: lifetime-dependent value escapes local scope
// note: depends on `normalizedPath`
}
The value returned by fileName
will dangle after the lifetime of
normalizedPath
ends. We can fix this error by making the caller of
getFileName
normalize the path prior to calling getFileName
:
// Path needs to be normalized.
func getFileName(_ normalizedPath: borrowing std.string) -> StringRef {
return fileName(normalizedPath)
}
Alternatively, we could construct and return an Escapable
value of type such
as std.string
instead of a dangling StringRef
:
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 those lifetime contracts and help us to write code that is free of memory safety errors.
Escapability Annotations in Detail
Under the strict safety mode, even though compiler warns us when importing
unannotated types, they are still imported as if they are Escapable
to
maintain backward compatibility. This behavior 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:
struct SWIFT_NONESCAPABLE View {
View(const int *p) : member(p) {}
private:
const int *member;
};
Moreover, we can explicitly mark types as Escapable
using the SWIFT_ESCAPABLE
annotation to express that they are not lifetime dependent on any other values:
struct SWIFT_ESCAPABLE Owner { ... };
The main reason for explicitly annotating a type as SWIFT_ESCAPABLE
is to make sure
it is considered 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 API Notes:
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:
MyList<View> f();
MyList<Owner> g();
In this example, MyList<View>
should be imported as ~Escapable
while MyList<Owner>
should be imported as Escapable
. This can be achieved via conditional escapability
annotations:
template<typename T>
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:
template<typename F, typename S>
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:
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 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.
Annotating the parameters of a constructor describes the lifetime of the created object:
struct SWIFT_NONESCAPABLE View {
View(const int *p __lifetimebound) : member(p) {}
...
};
In this example, the object initialized by the View
constructor has the same
lifetime as the argument p
of the constructor.
When the attribute is written after the method signature, the returned object has
the same lifetime as the implicit this
parameter.
struct Owner {
int data;
View handOutView() const __lifetimebound {
return View(&data);
}
};
For 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:
View getOneOfTheViews(const Owner &owner1 __lifetimebound,
const Owner &owner2,
View view1 __lifetimebound,
View view2 __lifetimebound) {
if (coinFlip)
return View(&owner1.data);
if (coinFlip)
return view1;
else
return view2;
}
Here, the returned View
’s lifetime depends on owner
, view1
, and view2
but not 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
:
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 attach lifetimebound
annotations to C and C++ APIs using API Notes. 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 API Notes have some limitations around C++, they do not support overloaded functions.
While lifetimebound
always describes the lifetime dependencies of the return value (or
the constructed object in case of constructors), we can use can use the lifetime_capture_by
annotation to describe the lifetime of other output values, like output/inout arguments
or globals.
void copyView(View view1 __lifetime_capture_by(view2), View &view2) {
view2 = view1;
}
Here, view2
inherits the lifetime dependencies of view1
, so callers of copyView
must ensure that whatever they pass as view2
does not outlive view1
.
We can use this annotation to specify that a parameter’s lifetime dependencies are captured by the implicit this
object, or
conversely, that an inout argument captures the lifetime dependencies of this
:
struct SWIFT_NONESCAPABLE CaptureView {
private:
View containedView;
public:
void captureView(View v __lifetime_capture_by(this)) {
containedView = v;
}
void handOut(View &v) const __lifetime_capture_by(v) {
v = containedView;
}
};
All of the non-escapable arguments 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:
void is_palindrome(std::span<int> s __noescape);
The lifetime annotations presented in this sections are powerful, but there are still lifetime contracts that they cannot express. APIs with such contracts can still be used from Swift, but they are imported as unsafe APIs, so that developers are aware that they need to take extra care when using these APIs to avoid memory safety violations.
Safe Overloads for Annotated Spans and Pointers
C and C++ APIs often feature parameters that denote a span of memory.
For example, some might have two parameters where one points to a memory buffer
and the other designates the buffer’s size; others might use the
std::span
type from the
C++ standard library. When such APIs are appropriately annotated, the Swift
compiler can bridge those span-like parameters to Swift’s
Span
and MutableSpan
types, and the user with a safe and convenient interface to those imported APIs.
These interfaces are in addition to the interfaces generated without annotations:
adding additional annotations to C or C++ APIs will not affect Swift code currently
relying on the plain interface. Adding additional information may alter the signature
of any existing safe overload however, since only 1 safe overload per imported function is generated.
At the time of writing, the features described in this section
are behind an experimental feature flag on the Swift 6.2 release branch.
To enable these features, pass -enable-experimental-feature SafeInteropWrappers
to the Swift compiler.
Safe Overloads for C++ std::span
std::span section" href="#safe-overloads-for-c-stdspan">
APIs taking or returning C++’s std::span
with sufficient lifetime
annotations will automatically get safe overloads take or return Swift
Span
and MutableSpan
types.
The following table summarizes the generated convenience overloads:
using IntSpan = std::span<const int>;
using IntVec = std::vector<int>;
C++ API | Generated Swift overload |
---|---|
void takeSpan(IntSpan x __noescape); |
func takeSpan(_ x: Span<Int32>) |
IntSpan changeSpan(IntSpan x __lifetimebound); |
@lifetime(x) func changeSpan(_ x: Span<Int32>) -> Span<Int32> |
IntSpan changeSpan(IntVec &x __lifetimebound); |
@lifetime(x) func changeSpan(_ x: borrowing IntVec) -> Span<Int32> |
IntSpan Owner::getSpan() __lifetimebound; |
@lifetime(self) func getSpan() -> Span<Int32> |
These transformations only support top-level std::span
s. The compiler
currently does not transform nested std::span
s. A std::span<T>
of a non-const
type T
is transformed to MutableSpan<T>
on the Swift side.
Safe Overloads for Pointers
If an API uses raw pointers rather than std::span
- perhaps because it’s written in C or Objective-C,
or because it’s an older C++ API that doesn’t want to break backwards compatibility -
it can still receive the same interop safety as std::span
. This added bounds safety doesn’t break
source compatiblity, nor does it affect ABI. Instead it leverages bounds annotations to express the
pointer bounds in terms of other parameters in the function signature.
Annotating Pointers with Bounds Annotations
The most common bounds annotation is __counted_by
. You can apply __counted_by
to pointer parameters
and return values to indicate the number of elements that the pointer points to, like this:
int calculate_sum(const int * __counted_by(len) values __noescape, int len);
In this example, the function signature on the C side hasn’t changed, but the __counted_by(len)
annotation communicates that the values
and len
parameters are related - specifically, values
should point to a buffer of at least len
int
values. When you annotate a function with a bounds
annotation like this, the compiler will generate a bounds safe overload: in addition to the imported
func calculate_sum(_ values: UnsafePointer<CInt>, _ len: CInt) -> CInt
signature, you will also
get the an overload with the func calculate_sum(_ values: Span<CInt>) -> CInt
signature.
Note that, like for std::span
, the __noescape
annotation is necessary to get a safe wrapper using Span
.
This signature is not only more ergonomic to work with - since the generated overload does the unpacking
of base pointer and count for you - but it’s also bounds safe. For example, if your API contains parameters
that share a count, the bounds safe overload will check that they all correspond.
void sum_vectors(const int * __counted_by(len) a __noescape,
const int * __counted_by(len) b __noescape,
int * __counted_by(len) out __noescape,
int len);
This safe overload will trap if a.count != b.count || b.count != out.count
:
func sum_vectors(_ a: Span<CInt>,
_ b: Span<CInt>,
_ out: MutableSpan<CInt>)
If the count of the Span
parameters is larger than the C function expects and you intentionally want to use
only a part of it, you can create a slice using extracting(_:)
.
If your API has more complex bounds you can express those with an arithmetic expression, like so:
int transpose_matrix(int * __counted_by(columns * rows) values __noescape, int columns, int rows);
In this case the columns
and rows
parameters can’t be elided:
func transpose_matrix(_ values: MutableSpan<CInt>, _ columns: CInt, _ rows: CInt)
This is because there’s no way to factor out columns
and rows
given only values.count
.
Instead a bounds check is inserted to verify that columns * rows == values.count
.
When your C/C++ API uses opaque pointers, void pointers or otherwise pointers to unsized types,
the buffer size can’t be described in terms of the number of elements. Instead you can annotate
these pointers with __sized_by
. This bounds annotation behaves like __counted_by
, but takes
a parameter describing the number of bytes in the buffer rather than the number of elements.
Where __counted_by
maps to Span<T>
, __sized_by
will map to
RawSpan
instead.
You can access the __counted_by
and __sized_by
macro definitions by including the ptrcheck.h
header.
For more information about these annotations, see Clang’s bounds safety documentation.
If the C code base is compiled with -fbounds-safety
, bounds safety is enforced on the C side as well -
otherwise it is only enforced at the interop boundary.
Lifetime Annotations for Pointers
Like with std::span
, pointers with bounds annotations can have their safe overloads map to
Span
/MutableSpan
when annotated with the appropriate lifetime annotations from the lifetimebound.h
header.
Unlike std::span
, pointers with bounds annotations also get a bounds safe overload when they lack lifetime annotations:
C API | Generated Swift overload |
|
|
|
|
|
|
|
|
The UnsafeBufferPointer
overloads provide the same bounds safety as their Span
equvalents
(NB: UnsafeBufferPointer
is not bounds checked on memory access in release builds, but the generated
UnsafeBufferPointer
overloads contain bounds checks in the same cases as the Span
overloads, even in release builds),
but without lifetime safety. If lifetime information is available the generated safe overload will always
choose to use Span
- no UnsafeBufferPointer
overload will be generated in this case. This means
that existing callers are not affected by annotating an API with __counted_by
, but callers using the
safe overload after adding __counted_by
will be affected if __noescape
is also added later on, or
if another parameter is then also annotated with __counted_by
.
To prevent source breaking changes, make sure to fully annotate the bounds and lifetimes of an API when
adding any bounds or lifetime annotations.
Bounds Annotations using API Notes
In cases where you don’t want to modify the imported headers, bounds annotations can be applied using API Notes. Given the following header:
void foo(int *p, int len);
void *bar(int size);
We can provide bounds annotations in our API note file like this:
Functions:
- Name: foo
Parameters:
- Position: 0
BoundsSafety:
Kind: counted_by
BoundedBy: "len"
- Name: bar
BoundsSafety:
Kind: sized_by
BoundedBy: "size"
Limitations
Bounds annotations are not supported for nested pointers: only the outermost pointer can be transformed.
lifetime_capture_by
is currently not taken into account when generating safe overloads.
Bounds annotations on global variables or struct fields are ignored: only parameters and return values are considered.
Bounds annotations, while supported in Objective-C code bases, are not currently supported in Objective-C class method signatures.