Filling a Swift array

I'm wrapping a C++ API for the RPLIDAR SDK, for use by Swift. I've got an Objective-C++ class wrapping their C++ class, and I'd like to find the fastest (i.e. fewest copies) way of making the following call in Swift:

var measurements = [RPLidarMeasurement](repeating: RPLidarMeasurement(), count: 8192)
measurements.reserveCapacity(8192)
try! lidar.grabScanData(&measurements)

The wrapper method should be semantically equivalent to this, but I don’t know how to massage the types to make Swift happy, with the least amount of massaging:

- (BOOL)
grabScanData: (NSMutableData*) ioData error: (NSError**) outError
{
	uint32_t nodeCount = 8192;
	if (ioData.length < nodeCount)
	{
		//  Set outError
		return false;
	}
	
	u_result result = self.driver->grabScanData((rplidar_response_measurement_node_t*) ioData.bytes, nodeCount);
	if IS_FAIL(result)
	{
		//  Set outError
		return false;
	}
	
	return true;
}

RPLidarMeasurement looks like this, defined in the Objective-C(++) wrapper header (there's no C++ in the header):

typedef struct RPLidarMeasurement {
	uint8_t			sync_quality;			// syncbit:1;syncbit_inverse:1;quality:6;
	uint16_t		angle_q6_checkbit;		// check_bit:1;angle_q6:15;
	uint16_t		distance_q2;
} __attribute__((packed)) RPLidarMeasurement;

Importantly, I want to pass the size of array (the number of elements) to the call to grabScanData. It's defined as:

u_result grabScanDataHq(RPLidarMeasurement * nodebuffer, size_t & count, uint32_t timeout = DEFAULT_TIMEOUT);

I don’t mind having a bunch of pointer manipulation, but I'd like to hide it from the Swift clients, and avoid copying, if possible.

Update: As I continue to work with this, I think the only thing I can do is create a class collection type for the measurement data, because I want it referenced all over the place and it's hard to ensure Swift doesn't copy the Array type.

var measurements = [RPLidarMeasurement](repeating: RPLidarMeasurement(), count: 8192)
measurements.reserveCapacity(8192)

The first line here is allocating the array, and doing a O(N) process of initializing 8192 instances of RPLidarMeasurement. The second line is a no-op (since 8192 capacity has not already been reserved, but also used.

Swift evolution proposal SE-0245 (Add an Array Initializer with Access to Uninitialized Storage) has been implemented in Swift 5.1, and does exactly what you're looking for.

Using it, your code would look something like this:

var measurements = Array<RPLidarMeasurement>(unsafeUninitializedCapacity: 8192) { buffer, initializedCount in
    try! lidar.grabScanData(measurements)
}

As described in the addendum, you can implement this yourself pre-Swift 5.1 with this extension:

extension Array {
    public init(
        unsafeUninitializedCapacity: Int,
        initializingWith initializer: (
            _ buffer: inout UnsafeMutableBufferPointer<Element>,
            _ initializedCount: inout Int
        ) throws -> Void
    ) rethrows {
        var buffer = UnsafeMutableBufferPointer<Element>
            .allocate(capacity: unsafeUninitializedCapacity)
        defer { buffer.deallocate() }
        
        var count = 0
        do {
            try initializer(&buffer, &count)
        } catch {
            buffer.baseAddress!.deinitialize(count: count)
            throw error
        }
        self = Array(buffer[0..<count])
    }
}

However, since the Array initializer causes a copy, this won't give you the same performance as the Swift 5.1 implementation, which can leverage the internal Array._allocateUninitialized API

1 Like

Ah, that reserveCapacity() call was leftover (I edited my post many times to reflect my exploration of the implementation).

Thanks for the info about Swift 5.1. I may be willing to require that for this API. Since the RPLIDAR SDK is open source and they publish the wire protocol, I'm thinking it may be advantageous to write a pure-Swift library to talk to it. I may also create a more specific container for the data, since each scan results in a different number of scan points; associating the pre-allocated buffer with the current scan point count makes sense.