Indexing flexible arrays in imported C-structs

I am having difficulties in indexing the flexible array field of a C struct imported into Swift:

struct Packet {
   unsigned long size;
   char bytes [];
};
Packet Details
// Packet.h

#ifndef Packet_h
#define Packet_h

struct Packet {
    unsigned long size;
    char bytes [];    // flexible array member
};

typedef struct Packet Packet;

Packet * Packet_allocate (unsigned long size);
void     Packet_print (Packet *);

#endif
// Packet.c

Packet * Packet_allocate (unsigned long size) {
    Packet w;
    Packet * ptr = malloc (sizeof (Packet) + size * sizeof (w.bytes [0]));
    assert (ptr);
    ptr->size = size;
    ptr->bytes [0] = 2;
    ptr->bytes [size - 1] = 3;
    return ptr;
}

void Packet_print (Packet * ptr) {
    printf ("Packet: size: %lu\n", ptr->size);
    for (int x = 0; x < ptr->size; ++x) {
        printf ("\tbytes [%d]: %d\n", x, ((int) (ptr->bytes [x])));
    }
}

This code provides the indexing functionality. (Originally provided by @scanon, with a warning.)

Packet+Index.swift
// Packet+Index.swift

extension Packet: RandomAccessCollection, MutableCollection {
  
  public var startIndex: Int { 0 }
  public var endIndex  : Int { Int (self.size) }
  
  public subscript (index: Int) -> UInt8 {
    @_transparent
    get {
        precondition (indices.contains (index))
        return withUnsafeBytes (of: self) {
            $0 [MemoryLayout <Self>.size + index]
        }
    }
      
    @_transparent
    set {
        precondition (indices.contains (index))
        withUnsafeMutableBytes (of: &self) {
            $0 [MemoryLayout <Self>.size + index] = newValue
        }
    }
  }
}

I get a Thread 1: Fatal error when I run the following code.

static func indexPacket () {
    let u : UnsafeMutablePointer <Packet> = Packet_allocate (UInt (16))
    
    Packet_print (u);

    var packet = u.pointee
    
    print (packet [0])                     // <-- Thread 1: Fatal error
    print (packet [Int (packet.size) - 1]) // <-- Thread 1: Fatal error
    
    packet [0] = 32                        // <-- Thread 1: Fatal error
    packet [Int(packet.size) - 1] = 64     // <-- Thread 1: Fatal error

    print ()
    Packet_print (u);
}

The crash occurs in this code:

extension Packet: RandomAccessCollection, MutableCollection {
  
  public var startIndex: Int { 0 }
  public var endIndex  : Int { Int (self.size) }
  
  public subscript (index: Int) -> UInt8 {
    @_transparent
    get {
        precondition (indices.contains (index))
        return withUnsafeBytes (of: self) {
            $0 [MemoryLayout <Self>.size + index] // <-- Thread 1: Fatal error
        }
    }
      
    @_transparent
    set {
        precondition (indices.contains (index))
        withUnsafeMutableBytes (of: &self) {
            $0 [MemoryLayout <Self>.size + index] = newValue // <-- Thread 1: Fatal error
        }
    }
  }
}

Although, thinking in C mode, I understand the above code, I am not sure whether it is correct or not.

Can anyone help please?

Thank you.

I would guess you're operating on a temporary "copy" of your struct the Swift compiler made at some point while skipping the extra bytes. Try printing the address of self and the address that was allocated and see if they're the same.

I suppose you have an UnsafePointer<Packet> somewhere. I'd try to use directly this pointer as the base address instead of relying on self or &self which are borrowed and inout variables, both of which are allowed to be copies.

1 Like

Thank you for the hints, @michelf

I am now starting to believe that indexing a flexible-array using the above method is not currently possible in Swift. It appears as if the compiler is truncating the struct upon seeing the flexible-array declaration in the C-struct.

struct Packet {
   unsigned long size;
   char bytes []; // flexible array
};

Time to find out if this is something intentional or something that was overlooked. :thinking:

Yes, flexible array members work extremely badly in Swift. If you want them to work you need to hold them extremely carefully. You can do it, but I'd encourage you to wrap the type in a class that holds a pointer whose size you manage yourself, and then use that as your abstraction type in Swift. In your case, rather than using Packet directly, you can use:

struct SwiftPacket {
    private var storage: Storage

    init(count: UInt) {
        self.storage = Storage(count: count)
    }
}

extension SwiftPacket {
    fileprivate final class Storage {
        private let ptr: UnsafeMutablePointer<Packet>

        init(count: UInt) {
            self.ptr = Packet_allocate(count)
        }

        init(copying original: Storage) {
            self.ptr = Packet_allocate(original.size)
            for i in 0..<Int(original.size) {
                self[i] = original[i]
            }
        }

        var size: CUnsignedLong {
            ptr.pointee.size
        }

        subscript(index: Int) -> UInt8 {
            get {
                (UnsafeRawPointer(ptr) + MemoryLayout<Packet>.size + index).load(as: UInt8.self)
            }
            set {
                (UnsafeMutableRawPointer(ptr) + MemoryLayout<Packet>.size + index).storeBytes(of: newValue, as: UInt8.self)
            }
        }

        deinit {
            Packet_free(ptr)
        }
    }
}

extension SwiftPacket: RandomAccessCollection, MutableCollection {
    var count: Int {
        Int(self.storage.size)
    }

    var startIndex: Int { 0 }
    var endIndex: Int { count }

    subscript(index: Int) -> UInt8 {
        get {
            precondition(indices.contains(index))
            return storage[index]
        }
        set {
            precondition(indices.contains(index))
            if !isKnownUniquelyReferenced(&storage) {
                storage = Storage(copying: storage)
            }
            storage[index] = newValue
        }
    }
}

While you'll have to take care with the pointer sizing dance, this gives you a Swift-native CoW equivalent of the original pointer type, with an entirely safe user interface. I think this kind of wrapper is pretty neat!

3 Likes