Introduction: There are a lot of analysis articles about the objective-C weak mechanism implemented by various communities, but there have been very few analysis articles about the ABI since Swift was released, which seems to be an area not covered by many iOS developers… This article analyzes how Swift implements weak mechanism from the source level.

The preparatory work

Due to the large amount of Swift source code, we strongly recommend that the Repo Clone down, combined with the source code together to see this article.

$ git clone https://github.com/apple/swift.git
Copy the code

Swift uses CMake as a build tool. If you want to open it with Xcode, you need to install LLVM first, and then use CMake -g to generate Xcode projects.

We are just doing source analysis here, I will directly use Visual Studio Code with C/C++ plug-in, also support symbol jump, find references. As a bonus note, the type hierarchy of the C++ code in Swift stdlib is complex and can be quite taxing to read without IDE assistance.

The body of the

Now let’s enter the source code analysis stage, first let’s look at the object in Swift (class instance) its memory layout is what.

HeapObject

We know that Objective-C represents an object in Runtime by objc_Object, and these types define the structure of the object’s head in memory. Similarly, there is a similar structure in Swift, that is, the HeapObject. Let’s look at its definition:

struct HeapObject {
  /// This is always a valid pointer to a metadata object.
  HeapMetadata const *metadata;

  SWIFT_HEAPOBJECT_NON_OBJC_MEMBERS;

  HeapObject() = default;

  // Initialize a HeapObject header as appropriate for a newly-allocated object.
  constexpr HeapObject(HeapMetadata const *newMetadata) 
    : metadata(newMetadata)
    , refCounts(InlineRefCounts::Initialized)
  { }
  
  // Initialize a HeapObject header for an immortal object
  constexpr HeapObject(HeapMetadata const *newMetadata,
                       InlineRefCounts::Immortal_t immortal)
  : metadata(newMetadata)
  , refCounts(InlineRefCounts::Immortal)
  { }

};
Copy the code

As you can see, the first field of the HeapObject isa HeapMetadata object. This object does a similar job as isa_t, describing the type of the object (equivalent to the result of type(of:)), but Swift does not use it in many cases. Such as static method distribution and so on.

Next up is SWIFT_HEAPOBJECT_NON_OBJC_MEMBERS, which is a macro definition that expands to:

RefCounts<InlineRefCountBits> refCounts;
Copy the code

This is a fairly important thing, related to reference counting, weak references, and unowned references, as well as being one of the more complex structures in Swift objects (the rest of the Swift objects in this article are guided by types, or instances of class).

Well, it’s not that complicated, we know that in Objective-C Runtime there are a lot of applications of union structures, for example isa_t has pointer and nonpointer, and they all take up the same amount of memory, The advantage of this is that you can use memory more efficiently, especially if you’re using a lot of it, which can significantly reduce runtime overhead. Similar techniques exist in the JVM, such as the Mark Word for object headers. Of course, this technique is also heavily used in the Swift ABI.

RefCountsType and Side Table

The RefCounts type is mentioned above, so let’s see what it is.

Let’s take a look at the definition:

template <typename RefCountBits>
class RefCounts {
  std::atomic<RefCountBits> refCounts;
  
  // ...
  
};
Copy the code

This is the memory layout for RefCounts, and I’ve omitted all method and type definitions here. You can think of RefCounts as a thread-safe wrapper. The template parameter RefCountBits specifies the actual internal type. There are two types in the Swift ABI:

typedef RefCounts<InlineRefCountBits> InlineRefCounts;
typedef RefCounts<SideTableRefCountBits> SideTableRefCounts;
Copy the code

The former is used in HeapObject, while the latter is used in HeapObject TableEntry (SideTable). I will talk about these two types one by one later.

In general, Swift objects do not use Side tables. Once an object is referenced by weak or unowned, it is allocated a Side Table.

InlineRefCountBits

Definition:

typedef RefCountBitsT<RefCountIsInline> InlineRefCountBits;

template <RefCountInlinedness refcountIsInline>
class RefCountBitsT {

  friend class RefCountBitsT<RefCountIsInline>;
  friend class RefCountBitsT<RefCountNotInline>;
  
  static const RefCountInlinedness Inlinedness = refcountIsInline;

  typedef typename RefCountBitsInt<refcountIsInline, sizeof(void*)>::Type
    BitsType;
  typedef typename RefCountBitsInt<refcountIsInline, sizeof(void*)>::SignedType
    SignedBitsType;
  typedef RefCountBitOffsets<sizeof(BitsType)>
    Offsets;

  BitsType bits;
  
  // ...
  
};
Copy the code

After template substitution, InlineRefCountBits is actually a uint64_t, a bunch of related types that are designed to make code more readable (or less readable, ha ha ha) through template metaprogramming.

Let’s simulate the object reference count +1:

  1. Calling the SIL interfaceswift::swift_retain:
HeapObject *swift::swift_retain(HeapObject *object) {
  return _swift_retain(object);
}

static HeapObject *_swift_retain_(HeapObject *object) {
  SWIFT_RT_TRACK_INVOCATION(object, swift_retain);
  if (isValidPointerForNativeRetain(object))
    object->refCounts.increment(1);
  return object;
}

auto swift::_swift_retain = _swift_retain_;
Copy the code
  1. callRefCountsincrementMethods:
void increment(uint32_t inc = 1) {
  // 3. Read the InlineRefCountBits object atomically (that is, a uint64_t).
  auto oldbits = refCounts.load(SWIFT_MEMORY_ORDER_CONSUME);
  RefCountBits newbits;
  do {
    newbits = oldbits;
    / / 4. Call InlineRefCountBits incrementStrongExtraRefCount method
    // Make a series of operations on the uint64_t.
    bool fast = newbits.incrementStrongExtraRefCount(inc);
    // If there is no weak or unowned reference, it is usually not entered.
    if(SWIFT_UNLIKELY(! fast)) {if (oldbits.isImmortal())
        return;
      return incrementSlow(oldbits, inc);
    }
    // 5. Set the uint64_t back to the uint64_t using CAS.
  } while(! refCounts.compare_exchange_weak(oldbits, newbits,std::memory_order_relaxed));
}
Copy the code

This completes a retain operation.

SideTableRefCountBits

If there is no weak, unowned reference, let’s see what happens if we add an weak reference.

  1. Calling the SIL interfaceswift::swift_weakAssign(Temporarily omit this piece of logic, it belongs to the logic of the referenced, we now analyze the referenced)
  2. callRefCounts<InlineRefCountBits>::formWeakReferenceAdd a weak reference:
template <>
HeapObjectSideTableEntry* RefCounts<InlineRefCountBits>::formWeakReference()
{
  // Assign a Side Table.
  auto side = allocateSideTable(true);
  if (side)
    // Add a weak reference.
    return side->incrementWeak();
  else
    return nullptr;
}
Copy the code

AllocateSideTable implementation

template <>
HeapObjectSideTableEntry* RefCounts<InlineRefCountBits>::allocateSideTable(bool failIfDeiniting)
{
  auto oldbits = refCounts.load(SWIFT_MEMORY_ORDER_CONSUME);
  
  // Return a Side Table if it already exists or is being destructed.
  if (oldbits.hasSideTable()) {
    return oldbits.getSideTable();
  } 
  else if (failIfDeiniting && oldbits.getIsDeiniting()) {
    return nullptr;
  }

  // Allocate the Side Table object.
  HeapObjectSideTableEntry *side = new HeapObjectSideTableEntry(getHeapObject());
  
  auto newbits = InlineRefCountBits(side);
  
  do {
    if (oldbits.hasSideTable()) {
      // At this point, another thread may create the Side Table, delete the allocated by that thread, and return.
      auto result = oldbits.getSideTable();
      delete side;
      return result;
    }
    else if (failIfDeiniting && oldbits.getIsDeiniting()) {
      return nullptr;
    }
    
    // Initialize the Side Table with the current InlineRefCountBits.
    side->initRefCounts(oldbits);
    // Perform CAS.
  } while (! refCounts.compare_exchange_weak(oldbits, newbits,
                                             std::memory_order_release,
                                             std::memory_order_relaxed));
  return side;
}
Copy the code

Remember that the RefCounts in HeapObject are actually a wrapper for InlineRefCountBits? After constructing the Side Table above, the InlineRefCountBits in the object is not the original reference count, but a pointer to the Side Table. However, since they are actually uint64_t, we need a method to distinguish them. The constructor for InlineRefCountBits can be used to distinguish between them:

LLVM_ATTRIBUTE_ALWAYS_INLINE
  RefCountBitsT(HeapObjectSideTableEntry* side)
    : bits((reinterpret_cast<BitsType>(side) >> Offsets::SideTableUnusedLowBits)
           | (BitsType(1) << Offsets::UseSlowRCShift)
           | (BitsType(1) << Offsets::SideTableMarkShift))
  {
    assert(refcountIsInline);
  }
Copy the code

In fact, the most common method is to replace the useless bits of the pointer address with identifying bits.

By the way, take a look at the structure of Side Table:

class HeapObjectSideTableEntry {
  // FIXME: does object need to be atomic?
  std::atomic<HeapObject*> object;
  SideTableRefCounts refCounts;

  public:
  HeapObjectSideTableEntry(HeapObject *newObject)
    : object(newObject), refCounts()
  { }

  // ...

};
Copy the code

What happens when you increase the reference count? Let’s look at the RefCounts:: Increment method

void increment(uint32_t inc = 1) {
  auto oldbits = refCounts.load(SWIFT_MEMORY_ORDER_CONSUME);
  RefCountBits newbits;
  do {
    newbits = oldbits;
    bool fast = newbits.incrementStrongExtraRefCount(inc);
    // Enter the branch this time.
    if(SWIFT_UNLIKELY(! fast)) {if (oldbits.isImmortal())
        return;
      returnincrementSlow(oldbits, inc); }}while(! refCounts.compare_exchange_weak(oldbits, newbits,std::memory_order_relaxed));
}
Copy the code
template <typename RefCountBits>
void RefCounts<RefCountBits>::incrementSlow(RefCountBits oldbits,
                                            uint32_t n) {
  if (oldbits.isImmortal()) {
    return;
  }
  else if (oldbits.hasSideTable()) {
    auto side = oldbits.getSideTable();
    // --> then call here.
    side->incrementStrong(n);
  }
  else{ swift::swift_abortRetainOverflow(); }}Copy the code
void HeapObjectSideTableEntry::incrementStrong(uint32_t inc) {
  // Finally, refCounts is a refCounts 
      
        object.
      
  refCounts.increment(inc);
}
Copy the code

SideTableRefCountBits = InlineRefCountBits; SideTableRefCountBits = InlineRefCountBits; SideTableRefCountBits = InlineRefCountBits;

class SideTableRefCountBits : public RefCountBitsT<RefCountNotInline>
{
  uint32_t weakBits;
  
  // ...
  
};
Copy the code

The subtotal

I don’t know if this is confusing, but it took me a little bit of time to analyze it at first.

One type of RefCounts is inline and used in the HeapObject as a uint64_t. It can be used as a reference count or pointer to a Side Table.

SideTable is a type of structure called HeapObjectSideTableEntry, which also contains RefCounts. Inside is SideTableRefCountBits. Basically the uint64_t plus a uint32_t for storing weak references.

WeakReference

All the above are the logic involved in the referenced object, and the logic of the reference side is a little simpler, mainly through the WeakReference class to achieve, relatively simple, we will simply go over it.

Weak variable in the Swift after silgen will become Swift: : swift_weakAssign calls, then to WeakReference: : nativeAssign:

void nativeAssign(HeapObject *newObject) {
  if (newObject) {
    assert(objectUsesNativeSwiftReferenceCounting(newObject) &&
           "weak assign native with non-native new object");
  }
  
  // let the referenced construct the Side Table.
  auto newSide =
    newObject ? newObject->refCounts.formWeakReference() : nullptr;
  auto newBits = WeakReferenceBits(newSide);

  // CAS.
  auto oldBits = nativeValue.load(std::memory_order_relaxed);
  nativeValue.store(newBits, std::memory_order_relaxed);

  assert(oldBits.isNativeOrNull() &&
         "weak assign native with non-native old object");
  // Destroy the weak reference to the original object.
  destroyOldNativeBits(oldBits);
}
Copy the code

Weak references are easier to access:

HeapObject *nativeLoadStrongFromBits(WeakReferenceBits bits) {
  auto side = bits.getNativeOrNull();
  return side ? side->tryRetain() : nullptr;
}
Copy the code

If the referenced object is released, why can I access the Side Table directly? In fact, the life cycle of the Side Table in the Swift ABI is separated from the object. When the strong reference count is 0, only the HeapObject is released.

The Side Table can only be released if all weak references are released or the relevant variables are set to nil.

void HeapObjectSideTableEntry::decrementWeak() {
  // FIXME: assertions
  // FIXME: optimize barriers
  bool cleanup = refCounts.decrementWeakShouldCleanUp();
  if(! cleanup)return;

  // Weak ref count is now zero. Delete the side table entry.
  // FREED -> DEAD
  assert(refCounts.getUnownedCount() == 0);
  delete this;
}
Copy the code

So even if weak references are used, there is no guarantee that all the relevant memory will be freed, because the Side Table will exist as long as the weak variable is not explicitly set nil. The ABI can also be improved by destroying its own weak reference if it finds that the referenced object has been freed while accessing a weak reference variable to avoid repeating meaningless CAS operations later. Of course ABI doesn’t do this optimization, we can do it in Swift code. 🙂

conclusion

The Swift weak reference mechanism is implemented in objective-C Runtime. The object Side Table is used to maintain the reference count. The difference is that Objective-C objects do not have Side Table Pointers in their memory layout, instead maintaining the relationship between objects and Side tables through a global StripedMap, which is not as efficient as Swift. In addition, the Objective-C Runtime zeroed out all __weak variables on object release, whereas Swift does not.

Overall, Swift’s implementation is slightly simpler (although the code is more complex and the Swift team pursues higher abstractions). This is the first time to analyze the Swift ABI. This article is for reference only. Thank you!