Stop importing unannotated C++ APIs returning SWIFT_SHARED_REFERENECE types into Swift

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)>

1 Like

@Alex_L do you have opinions?

@cxx-interop-workgroup