Safe and efficient C++ interoperability via non-escapable types and lifetimes

Introduction

Safely interacting with unsafe code is challenging. The C++ interoperability layer has limited safeguards in place to mitigate some sources of unsafety including hiding some APIs (like methods returning iterators) and exposing them with an “Unsafe” suffix. Unfortunately, these are based on simple syntactic heuristics that are incomplete (like identifying methods that return pointer like objects) and therefore do not provide sufficient safety guarantees and do not provide a way to safely access those APIs.

let vec = getVecOfInt();
let begin = vec.__beginUnsafe(); // Last use of vec, it might be freed.
let val = begin.pointee; // Potential use after free.

Building on the ongoing work to express lifetimes in Swift, we propose using annotations to express lifetime information for C++ APIs. Then, the compiler can know that begin depends on vec and it should not be cleaned up before begin ‘s last use.

Moreover, the interoperability layer introduces defensive copies to extend the lifetime of collections while they are being iterated on. This has a performance cost and can result in a less ergonomic experience interacting with C++ code.

var v = getVectorOfString()
for e in v { // Creating a deep copy of the vector.
...
}

With sufficient annotations on the C++ side we can safely remove the defensive copy when iterating on a C++ container.

Escapability

Non-escapable types provide the ability to create types whose instances cannot escape out of the context in which they were created with no runtime overhead. We want to introduce C++ annotations to let users import types as not escapable. For example, view types in C++ like iterators are good candidates to be imported as non-escapable types. Templates pose a challenge as std::vector<int> is safe to escape from a function while std::vector<std::span<int>> might not be. We plan to introduce C++ annotations to help express conditional escapability similar to conditional conformances in Swift: extension Container: Escapable where T: Escapable { } .

We plan to import a targeted set of types such as C++ iterators as not escapable. By default, most types would still be imported to Swift as escapable for backward compatibility and ergonomic reasons. Once Swift has better support for non-escapable types (like the new container protocols), we will reconsider changing the defaults for a subset of types under a new interop version.

Lifetime dependency annotations

Clang supports the [[clang::lifetimebound]] attribute to annotate lifetime dependencies between a return value and some input arguments. This annotation does not support all the scenarios that Swift lifetime dependencies can express. However, Clang already has some diagnostics to find some lifetime related errors in annotated code. The Clang community is actively improving these diagnostics and proposed extensions to these annotations. We propose importing lifetimebound annotations as Swift dependencies. Users annotating their C++ code can benefit from additional checking in both Swift and C++.

SpanOfInts MyContainer::getBuffer() [[clang::lifetimebound]];
// Is imported as:
@lifetime(self)
func getBuffer() -> SpanOfInts

We plan to recognize certain idioms in the importer and import reference returning methods like operator* , operator[] as projections with the correct lifetime dependecies. As a result, users would no longer get an implicit copy of the object behind the reference when calling these methods. We expect this to improve the performance and resolve some current problems when objects are sliced unintentionally.

Moreover, we plan to add or infer lifetimebound annotations for the C++ Standard Library so commonly used methods like vector::front work as expected out of the box.

In the future, we plan to add new annotations on the C++ side that closely match the semantics of how lifetime dependencies are specified on the Swift side.

Safe interoperability mode

Swift has an optional strict memory safety mode that provides more guarantees than the existing Safe-by-Default model. We plan supporting this effort by importing some user specified types without escapability annotations as @unsafe . This makes sure that only audited and annotated APIs can be called from strict memory safe Swift. This is a good fit for safety critical code but comes at the cost of bigger annotation burden. For more details see the post “Optional Strict Memory Safety for Swift”.

Annotation inference

We plan to introduce heuristics in the future that will help reduce the annotation burden. The goal is to find a good balance between usability and safety. The inference will make a set of assumptions like the C++ code is not casting constness away and the lifetime dependencies of the outputs of a function call can only depend on on its inputs.

Roadmap

Here is a rough roadmap (subject to change) for implementing the features laid out in this proposal. We also link to PRs where they are available.

  • Add Escapable and ~Escapable annotations to C++.
  • Import [[clang::lifetimebound]] annotations as Swift lifetime dependencies
  • Introduce a safe mode for interoperability
  • Add or infer lifetimebound annotations to the STL
  • Add a way to express objects without lifetime dependencies (e.g., API returning a string_view to a literal).
  • Introduce a way to distinguish between scoped and inherited lifetime dependency (vector::operator[] vs span::operator[]).
  • Import reference returning operator[] and operator* using projections
  • Make iterator types non-escapable

We also plan to work on the following features later:

  • More complex lifetime annotations
  • Conditional escapability annotations
  • Inference of escapability for certain types
  • Inference of lifetime annotations for certain APIs
  • Better support for output arguments

Let us know what you think.

20 Likes

I have a question, which is more of an implementation detail. I like the proposal here very much, but I wondered if the non-escapable types could be based on applying the already-existing [[clang::lifetimebound]] attribute but for types instead of methods/parameters, rather than as some brand new concept.

I think reusing [[clang::lifetimebound]] for types would be a viable approach, there are two reasons why we do not advocate for that:

  • It would be a hard sell to upstream a this change to Clang, because this attribute on a type would only make sense for Swift interop. They prefer contributions that benefit the C++ community.
  • Non-escapable types are a concept in Swift, and we wanted to reuse the vocabulary Swift developers use for the same concept in C++.