Example of SWIFT_SHARED_REFERENCE and std::enable_shared_from_this used together?

Hi. Given Swift 6.1 and c++20, can someone show an example how to properly use SWIFT_SHARED_REFERENCE macro? (I am struggling to understand what kind of parameters this macro accepts) Let's have a concrete example:

#include <swift/bridging>
using TcpSessionPtr = std::shared_ptr<TcpSession>;

class TcpSession final : public std::enable_shared_from_this<TcpSession> {
    // skipping code
    TcpSession(asio::strand<asio::any_io_executor>& strand, const TcpConfig& config) {}
    ~TcpSession() {}
    
    TcpSessionPtr static shared(asio::strand<asio::any_io_executor>& strand, const TcpConfig& config) {
        return std::make_shared<TcpSession>(strand, config);
    }
}

Can I make Swift import TcpSessionPtr as a class TcpSession?

(I do not think the shared static method will be called directly from Swift, only its result is expected to be passed to a Swift function)

Hopefully it makes sense and thanks in advance!

You'll have to have a refCount field (ideally of type std::atomic) on your TcpSession class, then add SWIFT_SHARED_REFERENCE after the ending class bracket, like this:

} SWIFT_SHARED_REFERENCE(retainSession, releaseSession);

Those retain and release functions could be defined like this:

inline void retainSession(TcpSession* obj) {
    obj->refCount++;
}

inline void releaseSession(TcpSession* obj) {
    if (obj->refCount > 0) {
         obj->refCount--;
    }
    if (obj->refCount == 0) {
        delete obj;
    }
}

Also, you'll probably need to add SWIFT_RETURNS_RETAINED or SWIFT_RETURNS_UNRETAINED to the shared static function after the argument list:

TcpSessionPtr static shared(asio::strand<asio::any_io_executor>& strand, const TcpConfig& config) SWIFT_RETURNS_RETAINED {

If you use SWIFT_RETURNS_RETAINED, make sure that your refCount starts at 1. Otherwise, it can start at 0. If you need to create your class instance from Swift, you can call the .shared function:

let session = TcpSession.shared(arg1, arg2)

Or, you can create it from C++ and then access the shared_ptr from Swift instead. However you need.

I think this should work fine with std::shared_ptr, but I'm curious to see how it works.

1 Like

Thanks. This gave a bit more clarity to the docs. After some experimenting, I think I have managed to create a basic example with the shared_ptr. Here is a repo I created: https://github.com/RussBaz/swift-cpp-interop I am still figuring it out, however.

I have found something surprising though - my class is released twice but retained only once. Needs more researching.

I think I could have skipped SWIFT_SHARED_REFERENCE entirely and just use the shared_ptr. But then it will be exposed as a pointer to the swift side, and I was not sure about the ergonomics of it.

Actually on second thought, if you are using std::shared_ptr then you shouldn't have to make the object a SWIFT_SHARED_REFERENCE. Depending on how you are using it in Swift, it may work fine to just be a std::shared_ptr.

If you need to access .pointee from Swift, though, be careful- there's a bug or unintended behavior where that creates a copy instead of borrowing.

Is this is a correct implementation of releaseSession? There’s a hazard where one thread can suspend at any point before the second if statement, another thread can run releaseSession to completion, decrementing obj->refCount to 0 and deleting obj, and then the first thread resumes and tries to dereference obj which is now a dangling pointer. This would indicate an overrelease bug in the client, but the whole purpose of checking if refCount > 0 seems to be to handle that. (That comparison also implies that refCount is a signed integer, which also seems to be a decision to handle overrelease.)

1 Like

I'm going to say this probably isn't the best. I'm currently using this implementation on my app but it's pretty much not concurrent in that different threads aren't messing with my shared references. So, if there's a suggestion for a better release method, let me know...

I think this is a thread-safe SWIFT_SHARED_REFERENCE retain and release implementation:

class SwiftShared {
    // Main class content here . . .

    std::atomic<int> referenceCount = 0;
};

void retainShared(SwiftShared * _Nonnull obj)
{
    obj->referenceCount++;
}

void releaseShared(SwiftShared * _Nonnull obj)
{
    if (--obj->referenceCount == 0) { // `--` performs atomic read-modify-write operation
        delete obj;
    }
}

The atomic decrement operator performs an atomic read-modify-write operation.

This is the approach the JUCE library takes in their ReferenceCountedObject implementation.

This could be implemented generically, like Russ' generic SwiftSharedBase so you wouldn't need to add the atomic member and reference count implementation per class.

1 Like

Nice, this looks good @TimArt ! I might use it in my project as well :slight_smile: