How can I implement a {mutable}span property for my inline storage type?

Currently, the platforms I'm targeting, do not support InlineArray. So, I wrote a simple type that stores some values inline:

// Simplified, there are normally some other stuff here.
struct InlineArraySize4<T> {
    var _storage: (T, T, T, T)

    init(repeating value: T) {
        self._storage = (value, value, value, value)
    }
}

Great, but one problem. I have no idea how to implement {mutable}span for my type. For heap storage, it's easy. Just pass the pointer to the span and you're done. But for inline storage, uhhh... where is the pointer exactly?

After a bit of digging (specifically InlineArray and CollectionOfOne impl), I found that the pointer comes from the Builtin.addressOfBorrow function, which is bad news for me. And I also found about addressability but I'm going to gloss over it to keep this short. But you can look to this forum post about it for more information which is how I found out about it.

After a bit of fiddling around, this is the solution that I've found which I don't know the safety of but works under optimisation:

struct InlineArraySize4<T> {
    // CollectionOfOne is @_addressableForDependencies so it _should_ make this type indirectly AFD, too.
    // The atttribute normally requires an experimental feature.
    private var _storage: CollectionOfOne<(T, T, T, T)>

    init(repeating value: T) {
        self._storage = .init((value, value, value, value))
    }

    public var span: Span<T> {
        @_lifetime(borrow self)
        borrowing get {
            // I am just stealing the span from CollectionOfOne and "repurposing" it.
            // This should normally be undefined behaviour but it seems like the parameter
            // is the span's underlying stored pointer.
            let address = _storage.span.withUnsafeBufferPointer(\.self)
            let castedAddress = UnsafeRawPointer(address.baseAddress!)
                .bindMemory(to: T.self, capacity: 4)

            let span = Span<T>(_unsafeStart: castedAddress, count: 4)
            // I don't know if this is neccecarry or not.
            return _overrideLifetime(span, borrowing: self)
        }
    }

    // Similar with MutableSpan...
}

An issue that I found is that if I try to get the pointer in an auxiliary function and try to use the pointer, under optimisation, it crashes/causes undefined behaviour.

var _address: UnsafePointer<T> {
    let address = _storage.span.withUnsafeBufferPointer(\.self)
    return UnsafeRawPointer(address.baseAddress!)
                .bindMemory(to: T.self, capacity: 4)
}

public var span: Span<T> {
    @_lifetime(borrow self)
    borrowing get {
        let span = Span<T>(_unsafeStart: _address, count: 4)
        // I don't know if this is neccecarry or not.
        return _overrideLifetime(span, borrowing: self)
    }
}

Is there a better implementation for what I'm trying to do?

Also, I have few little extra questions:

  • Would the behaviour change if I implemented it like this instead?
    private var _spanHelper: CollectionOfOne<Void>
    private var _storage: (T, T, T, T)
    
  • How much of an issue will occur with Span since size and stride are not the same?
  • Can I safely use the "addressability" and "builtinmodule" experimental features without affecting users (this is supposed to be in a library)?

A post was just posted before me that explains it. Omg, the timing :sob:.

Ok since the main question seems to be answered I'm changing the main question(s):

  • Will the Span cause me issues since the size and stride are not the same.
  • Will the users be affected if the required experimental features are enabled since this is in a library

No.

No. Any such behavior would be a bug (and unfortunately does happen once in a while around experimental features).

1 Like

Thank you!!

This has been sitting in my drafts folder for a while and I just edited an posted it, sorry.

1 Like