Home > OS >  Creating an NSArray from a C Array
Creating an NSArray from a C Array

Time:09-30

Newbie to Objective-C and Swift here. I'm creating an NSArray from a C float array with the following code:

float* output_f = output.data_ptr<float>();
NSMutableArray *results = [NSMutableArray arrayWithCapacity: 1360*1060]
for (int i = 0; i < 1360 * 1016; i  ) {
        [results insertObject:@(output_f[i]) atIndex:i];
    }

However since there are over a million samples to be inserted this is slow and is becoming a bottle-neck in my application. Is there a quicker way to create an NSArray from a C array without copying the elements one-by-one?

CodePudding user response:

There's no need to go through Obj-C. Assuming that output_f appears in an include file that's included via your bridging header, Swift will see its type as UnsafeMutablePointer<CFloat> (CFloat is just a typealias for Float, named to clarify that it corresponds to the C type).

Assuming you also make the number of floats in the array available, lets say included somewhere in your bridged header files is:

extern float* output_f;
extern int output_f_count;

Then on the Swift-side, you can use them like this:

let outputFloats = UnsafeMutableBufferPointer<CFloat>(
    start: output_f, 
    count: Int(output_f_count))

The cast of output_f_count to Int is necessary because Swift interprets C's int as CInt (aka Int32).

You can use UnsafeMutablePointer much like array, but there's no copying. It's just aliases the C data in Swift.

If you want to make sure you don't mutate the data, you can create an UnsafeBufferPointer instead, but you'll need to cast the pointer.

let outputFloats = UnsafeBufferPointer<CFloat>(
    start: UnsafePointer(output_f), 
    count: Int(output_f_count))

Since there's no copying, both of those options are very fast. However, they are pointers. If Swift modifies the contents, the C code will see the changed data, and vice-versa. That may or may not be a good thing, depending on your use case, but you definitely want to be aware of it.

If you want to make a copy, you can make a Swift Array very easily like this:

let outputFloatsArray = [CFloat](outputFloats)

Now you have you Swift-side copy in an Array.

As a very closely related thing, if in a C header, output_f were declared as an actual array like this,

extern float output_f[1360*1060];

Then Swift doesn't see a pointer. It sees, believe it or not, a tuple... a great big ugly tuple with a crap-load of CFloat members, which has the benefit of being a value type, but is hard to work with directly because you can't index into it. Fortunately you can work around that:

withUnsafeBytes(of: output_f) 
{
    let outputFloats = $0.bindMemory(to: CFloat.self)

    // Now within the scope of this closure you can use outputFloats
    // just as before.
}
  • Note: You can also use the pointer directly without going through the buffer pointer types, and because you avoid bounds-checking that way, it is a tiny bit faster, but just a very tiny bit, it's more awkward, and well... you lose the error catching benefits of bounds-checking. Plus the buffer pointer types provide all the RandomAccessCollection methods like map, filter, forEach, etc...

Update:

In comments OP said that he had tried this approach but got EXEC_BAD_ACCESS while dereferencing them. Missing is the context of what is happening between obtaining the pointer from output and its being available to Swift.

Given the clue from earlier that it's actually C , I think output is probably std::vector<float>, and its probably going out of scope before Swift does anything with the pointers, so its destructor is being called, which of course, deletes it's internal data pointer. In that case Swift is accessing memory that is no longer valid.

There are two ways to address this. The first in to make sure that output is not cleaned up until after Swift is done with it's data. The other option, is to copy the data in C.

const int capacity = 1360*1060;
float* p = output.data_ptr<float>();

// static_cast because the above template syntax indicates 
// this is actually C  , not C.
float* output_f = static_cast<float*>(calloc(capacity, sizeof(float)));
memcpy(output_f, p, capacity * sizeof(float));

Now output can be cleaned up before Swift accesses output_f. Also this makes the copy that was original asked about much faster that using NSArray. Assuming the C code doesn't use output_f after this, Swift can just take ownership of it. In that case, Swift needs to be sure to call free(outout_f) when it's done.

If the Swift code doesn't care about it being in an actual array, the Unsafe...BufferPointer types will do the job.

However, if an actual Array is desired, this will be yet another copy, and copying the same data twice just to get it in a Swift Array doesn't make sense if it can be avoided. How to avoid it depends on whether C (or Obj-C) is calling Swift, or Swift is calling Obj-C. I'm going to assume that it's Swift calling C. So let's assume that Swift is calling some C function get_floats() defined like this:

extern "C" *float get_floats()
{
    const int capacity = 1360*1060;
    float* p = output.data_ptr<float>();

    // static_cast because the above template syntax indicates 
    // this is actually C  , not C.
    float* output_f = static_cast<float*>(
        calloc(capacity, sizeof(float))
    );
    memcpy(output_f, p, capacity * sizeof(float));

    // Maybe do other work including disposing of `output`

    return output_f;
}

You want to change the interface so that a pre-allocated pointer is provided as a parameter, along with its capacity.

extern "C" void get_floats(float *output_f, int capacity)
{
    float* p = output.data_ptr<float>();

    memcpy(output_f, p, capacity * sizeof(float));

    // Maybe do other work including disposing of `output`

    // can use return for something else now -- maybe error code?
}

On the Swift side, you could allocate pointers, but since you want it in an Array anyway:

var outputFloats = [Array](repeating: 0, count: 1360*1060)

outputFloats.withUnsafeMutableBuffer {
    get_floats($0.baseAddress, CInt($0.count))
}

// Now the array is populated with the contents of the C array.

One last thing. The above code makes an assumption that output.data_ptr() points to at least capacity number of floats. Are you sure this is true? Assuming output is std::vector, it would be better to change the memcpy call to:

    let floatsToCopy = std::min(capacity, output.size())
    memcpy(output_f, p, floatsToCopy * sizeof(float));

That ensures that you're not reading garbage from the end of real data if it's actually less than capacity. Then you can return floatsToCopy; from get_floats.

Then on the Swift side, it looks like this:

var outputFloats = [Array](repeating: 0, count: 1360*1060)

let floatsCopied = outputFloats.withUnsafeMutableBuffer {
    get_floats($0.baseAddress, CInt($0.count))
}

outputFloats.removeLast(
    outputFloats.count - Int(floatsCopied), 
    keepingCapacity: true)

You don't actually have to use the keepingCapacity parameter, but doing so allows you to re-use the array without having to pay for more memory allocations. Just refill out to full capacity before calling get_floats again with the same array. Plus unless your peak memory usage is an issue, keepingCapacity: true is likely faster, and at least no worse, than the default, because without it, Array might choose to reallocate to the smaller size, which internally is an allocation, a copy, and a free, and the whole point was to avoid a copy... but the dynamic memory allocation is the really slow part. Given CPU caches and the way instruction pipelines work, you can do a lot of sequential copying in the time it takes to do a single memory allocation.

CodePudding user response:

According to the comments section your final goal is to read C-array data in Swift. Provided you know the length of the array, you can return it from an Objective-C function as a pointer:

- (float *)cArray {
    float *arr = (float *)malloc(sizeof(float) * 4);
    for (int i = 0; i < 4;   i) {
        arr[i] = i;
    }
    return arr;
}

And just read it from an UnsafePointer in Swift:

let ptr = TDWObject().cArray()

(0 ..< 4).forEach {
    print(ptr.advanced(by: $0).pointee)
}

Don't forget to deallocate the pointer when you are done with it:

ptr.deallocate()
  • Related