Swift 5.10 - changes regarding C++ interop and advancements on multi-platforms

Swift 5.10 is released! Well done Swift team and thanks a lot.

I read the very good release description here Swift.org - Swift 5.10 Released and understand that Swift 5.10 focuses on concurrency checking.

Beyond the focus on concurrency checking, I would like to know if Swift 5.10 has anything advacncements on Cxx interop side and also if there are any changes to know about the Windows version (any information relevant to Linux is also welcome).

Thanks

5 Likes

Thank you for the info !

Looking at the <swift/bridging> header, it looks like there is no update. I'm looking forward for some C++ interoperability updates in the next release, since the reference counting is often unreliable, but I'm still happy we have this feature.

That's disappointing. I was hoping for support for virtual functions and polymorphism. Using C++ without these is practically pointless.

There's been a lot of progress on calling virtual functions on main. It just happened after we branched for the 5.10 release, but it should be usable now. I'd suggest trying it out with a recent toolchain release to see if it's unblocked you at all.

Directly defining subclasses of C++ classes in Swift will be a bigger task, and I don't think it's in the near-term roadmap for the Apple team working on C++ interop. Of course, if people in the community are motivated to work on it, it can happen sooner.

4 Likes

Sounds alarming. Is that really true? Can you provide a reproducible example? Or are you simply referring to programmer errors causing retain cycles?

Yes, but maybe I am too dramatic by calling it unreliable. Let's say we have a cat class:

class cat final {
private:
    std::atomic<size_t> numReferences;

    // fetch_sub returns value before subtraction
    size_t decrement() { return numReferences.fetch_sub(1) - 1; }

    cat(): numReferences(1) { }
    ~cat() { }

public:
    static cat* nonnull create() {
        return new cat();
    }

    void retain() {
        printf("Retained: %zu\n", numReferences.load() + 1);
        numReferences++;
    }

    void release() {
        printf("Released: %zu\n", numReferences.load() - 1);
        if (decrement() == 0) {
            delete this;
        }
    }

    void meow() { printf("meow\n"); }
} SWIFT_SHARED_REFERENCE(retainCat, releaseCat);

cat* nonnull createCat() {
    return cat::create();
}

void retainCat(cat* nonnull aCat) {
    aCat->retain();
}

void releaseCat(cat* nonnull aCat) {
    aCat->release();
}

It's ported to Swift by providing the retainCat and releaseCat functions.

If I create a cat from a global (or from a namespace) C++ function, Swift will only decrease the reference count after exiting the scope, which perfectly suits for my particular use case. The object then gets released, because object was created and returned with reference count of 1, and after exiting the scope it's decremented to 0, so it will be destroyed.

func testFunction() {
    // A cat is created with reference count of 1
    let aCat = createCat();

    // print: Retained: 2
    aCat.meow();
    // print: Released: 1

    // print: Released: 0
    // aCat is destroyed
}

But when I create an object from a class' static C++ method, the Swift will increment the number of references right after receiving the object (which I didn't expect, didn't want and don't have control over) and decrement after exiting the function, leaving aCat with reference count of 1 and thus causing a memory leak.

func testStaticMethod() {
    // A cat is created with reference count of 1
    // print: Retained: 2
    let aCat = cat.create();

    // print: Retained: 3
    aCat.meow();
    // print: Released: 2

    // print: Released: 1
    // aCat is leaked
}

As workaround, I don't use C++'s static methods in Swift code, because it never works the way I want.

Another example, if you get A C++ object from the B C++ object using B's getter, the reference count of A will also be increased after receiving the object, which usually is what we need. But what if B already returns A with increased number of references? This is especially suitable when B is a thread-safe class, and it increases A's number of references inside B's getter's synchronization logic to make sure that A will not be destroyed by another thread while returning it from getter from the current thread. Swift doesn't know it and applies default behavior.

All these problems could've been solved if the methods or functions could provide annotations that allow Swift to understand if it should retain the object after receiving it from a function or method or not, like SWIFT_RETURNS_RETAINED/UNRETAINED would be enough.

Another problem that I faced, is when, in Swift code, I try to obtain a C++ object by accessing an Objective-C object's property - I get a runtime error. It looks like Objective-C tries to call objc_retain function by passing a C++ object, which is definitely wrong. As workaround, I provide a global function that accepts an Objective-C object, calls its getter and returns the value. I've even filed FB13297213 via Feedback Assistant, provided a verbose description and a working example code, and of course I got ghosted since October 2023.

One more problem, the __attribute__((objc_direct_members)) attribute (that is attached to an Objective-C interface) causes compiler to cough and gag and puke with some strange compiler errors if I tried to access its C++ properties via Swift code. I'm not sure if this particular problem was solved, but I uncommented this attribute later after some Xcode updates and it at least compiles, but I still don't access its C++ members in Swift code.

So yeah, I think, I'm just being dramatic, because despite of all these problems, the C++ interoperability works solid, but with many pitfalls and gets you really frustrated if you just started to work with it.

2 Likes