When importing CoreFoundation (or CF) functions into swift, the ownership of the returned CF object (returned as owned
/unowned
) is decided based on whether the "name" of the function has "create" or "copy" string. Swift/C++ interop also inherited this behavior for functions returning SWIFT_SHARED_REFERENECE
types. C++ functions which have "create" or "copy" string in their name are treated as returning owned
values whereas all other functions, static, non-static methods (regardless of their name) are treated as returning unowned
values.
While CoreFoundation had strong naming convention that made this rule a good heuristic, C++ doesn't have a reliable naming convention around this. Using CF's naming convention for C++ is an important source of memory unsafety and confusion in importing C++ functions returning SWIFT_SHARED_REFERENECE
. The following example illustrates the problems of having these naming conventions in C++. The Swift compiler makes wrong inference of the ownership of the returned value for both createTree
and makeTree
which led to a use-after-free and a memory leak.
Example:
C++
#include <swift/bridging>
struct Tree {
public:
static Tree* _Nonnull makeTree() {
Tree *t = new Tree();
t->increaseRefCount();
return t;
}
void increaseRefCount() {
refcount+=1;
}
void decreaseRefCount() {
assert(refcount >= 0)
refcount-=1;
if (refcount==0) delete this;
}
private:
int refcount = 0;
} SWIFT_SHARED_REFERENCE(retain_tree, release_tree);
void retain_tree(Tree * _Nonnull tr){ tr->increaseRefCount();};
void release_tree(Tree * _Nonnull tr){tr->decreaseRefCount();};
Tree* _Nonnull createTree() {
return new Tree();
}
Swift
// Example illustrating use after free
let tree1 = createTree()
let tree2 = tree1
print(tree2)
// Desugared code with autogenerated reference counting operations
let tree1 = createTree() // returns owned
let tree2 = tree1
retain_tree(tree2)
print(tree2)
release_tree(tree2) // frees the shared object
release_tree(tree1) // use after free
// Example illustrating leak
let tree = Tree::makeTree()
print(tree)
// Desugared code with autogenerated reference counting operations
let tree = Tree::makeTree() // returns unowned
retain_tree(tree) // Swift does a defensive retain as it considers makeTree as returning unowned
print(tree)
release_tree(tree) // leaks the shared object
Related: Github Issue
Until recently, there wasn't a way to override the create/copy rule manually for C++ APIs. Recently, in PR, we added SWIFT_RETURNS_RETAINED
and SWIFT_RETURNS_UNRETAINED
annotations for C++ methods and functions returning SWIFT_SHARED_REFERENCE
types to specify the ownership convention of the returned value when importing these into swift.
In this pitch, I want to propose to stop importing unannotated C++ APIs returning SWIFT_SHARED_REFERENECE
types into Swift. Since there is no good universally applicable heuristic to determine which functions are returning owned or unowned, this model would enforce that the ownership is explicitly specified and documented by the programmer. In the future, it is possible to aid the programmer through static analysis that can infer the annotations and suggest them as fix-its.
Since this would be a source breaking change, this would be guarded by a new interop version. To preserve source compatibility, in the existing versions of interop, I propose to add a Swift compiler warning and fixit for C++ APIs returning SWIFT_SHARED_REFERENECE
that are not annotated with SWIFT_RETURNS_{RETAINED/UNRETAINED}
. We propose to support these new annotations in APINotes as well, so that it can be used to annotate types imported from libraries whose sources cannot be modified.
We plan to stop importing all kinds of unannotated C++ APIs that return SWIFT_SHARED_REFERENECE
types. This would include operators, function templates, function pointers and class templates with function call operators like std::function
. To support this we would allow the SWIFT_RETURNS_{RETAINED/UNRETAINED}
attributes on type aliases. For example:
using OwnedFunc = SWIFT_RETURNS_RETAINED std::function<SharedThing *(int)>