Importing C-arrays without dealing with tuples

I have accidentally discovered something - something good I believe.

A way of importing C-arrays into Swift, which makes it possible to access the elements of an array by indexing, eliminating the need to deal with potentially large tuples. :slight_smile:

I was trying to import the following C-array into Swift, but I immediately ran into a stumbling block.

struct Packet {
    unsigned long size;
    char bytes [1];  // this style of trailing array is no longer valid
};

Only the first byte of the array is visible in Swift, because Swift seems to always expect something like one of these:

struct FriendlyPacket {
    unsigned long size;
    char * bytes ; 
};

struct FixedSizePacket {
    char bytes [8192];
};

Following @scanon's advice below, I used a flexible array instead:

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

This time, however, I was promptly greeted with a new problem: Swift can't see the bytes field at all!

Luckily, there is a simple solution: use a thin adaptation layer.

adaptor: Packet -> FriendlyPacket

// In C

// Case 1 - flexible array
Packet p = ...
FriendlyPacket fp = {p.size, p.bytes};

// Case 2 - fixed-size array
FixedSizePacket p2 = ...
FriendlyPacket fp2 = {sizeof (p2.bytes), p2.bytes};

// Back in Swift
Access elements of bytes in a FriendlyPacket simply by indexing, without having to deal with tuples!

Details Here
// Packet.h

#ifndef Packet_h
#define Packet_h

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

typedef struct Packet Packet;

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

struct FriendlyPacket {
    unsigned long size;
    char * bytes;
};

typedef struct FriendlyPacket FriendlyPacket;

FriendlyPacket Packet_adapt (Packet *);

#endif /* Packet_h */
// Packet.c

#include <stdlib.h>
#include <stdio.h>
#include <assert.h>
#include "Packet.h"

//
// --------------------------------------------------------
//
Packet * Packet_allocate (unsigned long size) {
    Packet * ptr = malloc (sizeof (Packet) + size * sizeof (char));
    assert (ptr);
    ptr->size = size;
    ptr->bytes [0] = 5;
    ptr->bytes [size - 1] = 7;
    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])));
    }
}

FriendlyPacket Packet_adapt (Packet * ptr) {
    FriendlyPacket u = {ptr->size, ptr->bytes};
    return u;
}

// Test.swift

@main
struct T1 {
    static func main () async {
        let u : UnsafeMutablePointer <Packet> = Packet_allocate (UInt (5))
        Packet_print (u);
        
        // Can't modify u, because only the first byte is visible in Swift. (or not visible at all in the case the flexible array.)
        
        // Adapt it to a form Swift can recognize
        let fp = Packet_adapt (u);
        
        print ("FriendlyPacket: size: \(fp.size)")
        for i in 0..<fp.size {
            print ("\tbytes [\(i)]:\(fp.bytes [Int (i)])")
        }
        
        // Now can modify it
        for i in 0..<fp.size {
            fp.bytes [Int (i)] *= 3
        }

        Packet_print (u);
    }
}

I submit this for your perusal.

Thank you.

Edit: use flexible array.

2 Likes

This old-style "trailing array member declared as array[1]" is invalid C, and leads directly to undefined behavior. The correct way to do this in standard C is with char bytes[], which has been in the language since C99.

FWIW, when working with a C struct that really wants to be a collection and doesn't have any other fields, I would probably skip the adaptor type and do something like the following (untested and written in the browser, so don't copy this without some inspection):

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

/// ------

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
      }
    }
  }
}

For more complicated cases, an adaptor type can make good sense.

2 Likes

Thank you, @scanon.

I have modified the original post to take your advice into account.

BREAKING NEWS

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

Thanks to @lukasa, there exists an elegant and edifying solution for indexing flexible arrays.

There is no longer any need for the thin adaptation layer for flexible arrays! :joy: