Cliff Ribaudo
Cliff Ribaudo

Reputation: 9039

Swift 5 Create 3D Array of Doubles And Pass To C Function

I need to call a legacy C function (from swift) that expects a 3D array of Doubles as an argument. I am fairly new to Swift and have begun converting a large ObjC and C code base written for iOS and Mac over to Swift. The C code does a lot of complex astronomical math and for which Swift is just too cumbersome. I will not convert those, but I need to use them from Swift

The C function is declared like this and the .H file is visible to swift:

void readSWEDayData(double dData[DATA_ROWS_PER_DAY][NUM_PLANET_ELEMENTS][NUM_ELEMENTS_PER_PLANET]);

The Constants used in the declaration are defined to be:

DATA_ROWS_PER_DAY = 1
NUM_PLANET_ELEMENTS = 35
NUM_ELEMENTS_PER_PLANET = 4

I am struggling with declaring the array of doubles in a way that Swift will allow to be passed to the C function. I've tried several approaches.

First Approach:

I declare the array and call it like so:

var data = Array(repeating: Double(EPHEMERIS_NA), count:Int(DATA_ROWS_PER_DAY * NUM_PLANET_ELEMENTS * NUM_ELEMENTS_PER_PLANET))
readSWEDayData(&data)

I get this error: Cannot convert value of type 'UnsafeMutablePointer' to expected argument type 'UnsafeMutablePointer<((Double, Double, Double, Double),...

Second Approach:

If I declare the array this way:

var data = [(Double, Double, Double, Double)](repeating: (EPHEMERIS_NA, EPHEMERIS_NA, EPHEMERIS_NA, EPHEMERIS_NA), count: Int(NUM_PLANET_ELEMENTS))
readSWEDayData(&data)

I get this error: Cannot convert value of type 'UnsafeMutablePointer<(Double, Double, Double, Double)>' to expected argument type 'UnsafeMutablePointer<((Double, Double, Double, Double),

So, how the heck does one declare a 3D Array in Swift of a specific size so that it can be passed to a C Function?

Upvotes: 2

Views: 296

Answers (3)

ingconti
ingconti

Reputation: 11646

My two cents for others..Hoping will help. I got a similar problem, but hope can save time for other. I had to pass down:

  • path (from String to char *)
  • title (from String to char *)
  • columns ([String] to array of char *)
  • a counter

to sum up I had to call "C" function:

bool OpenXLSXManager_saveIn(const char * cFullPath, const char * sheetName, char *const columnTitles[], double *const values[], int columnCount);

  1. I started from excellent: // https://oleb.net/blog/2016/10/swift-array-of-c-strings/

  2. expanded a bit:

     public func withArrayOfCStringsAndValues<R>(
     _ args: [String],
     _ values: [[Double]],
    
     _ body: ([UnsafeMutablePointer<CChar>?]  , [UnsafeMutablePointer<Double>?]  ) -> R ) -> R {
    
     var cStrings = args.map { strdup($0) }
     cStrings.append(nil)
    
     let cValuesArrr = values.map { (numbers: [Double]) -> UnsafeMutablePointer<Double> in
    
         let pointer = UnsafeMutablePointer<Double>.allocate(capacity: numbers.count)
         for (index, value) in numbers.enumerated() {
             pointer.advanced(by: index).pointee = value
         }
    
         return pointer
     }
    
     defer {
         cStrings.forEach { free($0) }
    
         for pointer in cValuesArrr{
             pointer.deallocate()
         }
     }
    
     return body(cStrings, cValuesArrr)
    

    }

  3. so I can call:

func passDown(

    filePath: String,
    sheetName:
    String,
    colNames: [String],
    values: [[Double]]
) -> Bool 

{ let columnCount = Int32(colNames.count) return withArrayOfCStringsAndValues(colNames, values) {

    columnTitles, values in
    
    let retval = OpenXLSXManager_saveIn(filePath, sheetName, columnTitles, values, columnCount)
    return retval
}

}

(SORRY for formatting, S.O. formatter has BIG issues ..)

Upvotes: 0

Cliff Ribaudo
Cliff Ribaudo

Reputation: 9039

Because Swift 5 lacks support for interoperability with C language multi-dimensional Arrays of fixed size except via tuples of explicitly declared structure (See Sweeper's answer above) and which is something I wish to avoid to keep my code flexible for future changes to the C Library being used, I opted to write a wrapper for the C function and make it appear to Swift as a 1 dimensional array.

This was necessary because the Constants used in the C Code change when readSWEDayData increases the array sizes to support additional elements and tuple declarations like this:

let Double4x35 = Tuple35<(Double, Double, Double, Double)>

will DEFINITELY break in a way that will be hard to find:

So my C wrapper function looks like so:

void readSWEDayDataForSwift(double *dData) {
    readSWEDayData((double (*)[NUM_PLANET_ELEMENTS][NUM_ELEMENTS_PER_PLANET])dData);
}

Making it easy to call it from Swift like so:

    var data = Array(repeating: Double(EPHEMERIS_NA), count:Int(DATA_ROWS_PER_DAY * NUM_PLANET_ELEMENTS * NUM_ELEMENTS_PER_PLANET))

I was surprised that this far into Swift's evolution there is no better way to do this!

Upvotes: 0

Sweeper
Sweeper

Reputation: 271965

The function needs an UnsafeMutablePointer to a 35-tuple of things, where each of those things are 4-tuples of Doubles. Yes, C arrays translate to tuples in Swift, because Swift doesn't have fixed size arrays. You could do:

var giantTuple = (
    (EPHEMERIS_NA, EPHEMERIS_NA, EPHEMERIS_NA, EPHEMERIS_NA),
    (EPHEMERIS_NA, EPHEMERIS_NA, EPHEMERIS_NA, EPHEMERIS_NA),
    (EPHEMERIS_NA, EPHEMERIS_NA, EPHEMERIS_NA, EPHEMERIS_NA),
    // 32 more times...
)
readSWEDayData(&giantTuple)

But I don't think you'd like that. You can create an array, and use some pointer magic to convert that to a tuple, as discussed in this Swift Forums post. In fact, that post is highly relevant to your situation.

To save some typing, we can write some type aliases first:

typealias Tuple35<T> = (T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T,T)
typealias Double4x35 = Tuple35<(Double, Double, Double, Double)>

Then we can do:

var giantTuple = Array(repeating: (EPHEMERIS_NA, EPHEMERIS_NA, EPHEMERIS_NA, EPHEMERIS_NA), count: NUM_PLANET_ELEMENTS).withUnsafeBytes { p in
    p.bindMemory(to: Double4x35.self)[0]
}

readSWEDayData(&giantTuple)

This works because tuples and arrays have essentially the same "layout" in memory.

Note that I "cheated" a little bit here, since DATA_ROWS_PER_DAY is 1, you can just create one such giantTuple, and get a pointer to it. However, if it is greater than 1, you'd have to do something like:

var giantTuples = Array(repeating:
                        Array(repeating: (EPHEMERIS_NA, EPHEMERIS_NA, EPHEMERIS_NA, EPHEMERIS_NA), count: NUM_PLANET_ELEMENTS).withUnsafeBytes { p in
                            p.bindMemory(to: Double4x35.self)[0]
                        },
                        count: DATA_ROWS_PER_DAY)

readSWEDayData(&giantTuples)

To convert from the giant tuple back to an array, you can do something like this:

// converting the first giantTuples in "giantTuples" as an example
let arrayOf4Tuples = asCollection(giantTuples[0], Array.init)
let finalArray = arrayOf4Tuples.map { asCollection($0, Array.init) }

// these are adapted from the Swift forum thread
// you'll need two of these, because you have 2 types of tuples
// yes, working with C arrays is hard :(
func asCollection<T, E>(_ tuple: Tuple35<E>, _ perform: (UnsafeBufferPointer<E>)->T) -> T {
  return withUnsafeBytes(of: tuple) { ptr in
    let buffer = ptr.bindMemory(to: (E.self))
    return perform(buffer)
  }
}
func asCollection<T, E>(_ tuple: (E, E, E, E), _ perform: (UnsafeBufferPointer<E>)->T) -> T {
  return withUnsafeBytes(of: tuple) { ptr in
    let buffer = ptr.bindMemory(to: (E.self))
    return perform(buffer)
  }
}

Upvotes: 1

Related Questions