Populating a Swift array in a C function callback

I’d like to add a property to UIBezierPath that returns an array of the path’s elements. So I need to call the CGPathApply() function, which takes a function pointer (a closure in Swift 2.0) and calls the closure for each element in the path. My problem is: how do I get the data about the path elements out of the callback closure and stored in a Swift array? The closure can’t capture any variables (because it corresponds to a C function pointer), so I think I have to pass a pointer to a Swift array as a parameter. Thankfully, CGPathApply() takes a parameter info: UnsafeMutablePointer<Void> and passes it to the closure.


My problem is in line 10: how do I turn the UnsafeMutablePointer<Void> back into a Swift array that I can operate on inside the closure?


extension UIBezierPath {
    var elements: [CGPathElementType] {
        // I'm trying to reserve enough memory for the array. 
        // I don't know how many elements the path has, but this should be good enough for testing.

        var elements = ContiguousArray<CGPathElementType>()
        elements.reserveCapacity(1000)

        CGPathApply(self.CGPath, &elements) { (userInfo: UnsafeMutablePointer<Void>, elementPointer: UnsafePointer<CGPathElement>) -> Void in
            // I don’t know how to convert userInfo back into a Swift array
            var elements = ???
            let elementType = elementPointer.memory.type
            elements.append(elementType)
        }

        return Array(elements)
    }
}


I tried var elements = UnsafeMutablePointer<[CGPathElementType]>(userInfo).memory. This compiles but when I run the code on a bezier path with 5 path elements, the resulting array is empty.

Replies

Isn't the closure supposed to get only one CGPathElement (and it will get called repeatedly, one time for each CGPathElement in the CGPath)?

I think the problem is that the array is being copied and becomes a new seperate array when you append something to it. So the original array is still empty, and that's the only reference you have.


I think you would need to pass a pointer to a pointer to an array (so that you can update the passed pointer to hold a pointer to the new updated array each time).

Yes, exactly. That’s why I want to append the new element to the array in each execution of the closure.

That’s a good point, thanks. I’ll try something like that and report back here with the result.

I got it to work by wrapping the call to CGPathApply in a withUnsafeMutablePointer() call. To be honest, I’m still not totally sure why this works. The whole UnsafePointer stuff in the standard library and how it interacts with value types is still somewhat unclear to me.


Here is the working code:


extension UIBezierPath {
    var elements: [CGPathElementType] {
        var pathElements = [CGPathElementType]()
        withUnsafeMutablePointer(&pathElements) { elementsPointer in
            CGPathApply(CGPath, elementsPointer) { (userInfo, nextElementPointer) in
               // The type of the next path element we want to store in the array
                let nextElement = nextElementPointer.memory.type

                // userInfo is an UnsafeMutablePointer<Void>.
               // Cast it back into an UnsafeMutablePointer<[PathElement]>
                let elementsPointer = UnsafeMutablePointer<[PathElement]>(userInfo)

                // Wrapping everything in withUnsafeMutablePointer() makes this work.
               // nextElement is appended to the array that elementsPointer points to.
                elementsPointer.memory.append(nextElement)
            }
        }
        return pathElements
    }
}


I’d welcome any comments for improvement or clarification what withUnsafeMutablePointer exactly does and why I can now mutate the array inside CGPathApply().

I was intrigued by your question because I often end up using low-level APIs that have C callbacks. In Objective-C I typically get around this by writing my own block-based wrapper for the function. For example:


static void MyPathApplierFunction(void * __nullable info, const CGPathElement * element) {
    CGPathApplierBlock block;

    block = (__bridge CGPathApplierBlock) info;
    block(element);
}

extern void CGPathApplyBlock(CGPathRef __nonnull path, CGPathApplierBlock __nonnull block) {
    CGPathApply(path, (void *) block, MyPathApplierFunction);
}


Now that we have C callbacks in Swift, I figured you could do the same. I came up with two different approaches:


typealias CGPathApplierBlockA = (CGPathElement) -> Void

func CGPathApplyBlockA(path: CGPath, @noescape block: CGPathApplierBlockA) {
   var blockBox = Box(block)
   withUnsafeMutablePointer(&blockBox) { blockBoxPtr in
       CGPathApply(path, blockBoxPtr, { info, element in
           let blockBox2 = UnsafePointer<Box<CGPathApplierBlockA>>(info).memory
           blockBox2.value?(element.memory)
       })
   }
   blockBox.value = nil
}


and:


typealias CGPathApplierBlockB = @convention(block) (CGPathElement) -> Void

func CGPathApplyBlockB(path: CGPath, block: CGPathApplierBlockB) {
   CGPathApply(path, unsafeBitCast(block, UnsafeMutablePointer<Void>.self), { info, element in
       let block2 = unsafeBitCast(info, CGPathApplierBlockB.self)
       block2(element.memory)
   })
}


After talking withs over with the some folks internally I'm going to recommend approach B. It's a bit scary but it's also the most efficient and the most like the Objective-C version.

Share and Enjoy

Quinn "The Eskimo!"
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

Thanks for the ideas! I’m not sure I like the unsafeBitCast version but passing a block in the userInfo param is definitely an intriguing idea. I hadn’t thought of that.

Someone asked me (offline) for a Swift 3 version of this code. Here it is.

typealias CGPathApplierBlock = @convention(block) (CGPathElement) -> Void

extension CGPath {
    func apply(body: CGPathApplierBlock) {
        self.apply(info: unsafeBitCast(body, to: UnsafeMutableRawPointer.self)) { (info, element) in
            let block2 = unsafeBitCast(info, to: CGPathApplierBlock.self)
            block2(element.pointee)
        }
    }
}

ps We finally got around to filing a bug requesting that CG support this directly (r. 28303751). That’s cool for this particular case but I find I need this technique in lots of other places where I call low-level APIs.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

Someone asked me via email whether this technique (that is, approach B, the latest incarnation of which is in my 14 Sep 2016 post) is still best practice. Well, to start, the 2017 OS releases have

CGPathApplyWithBlock
, so this specific example will soon be fading into the past. However, the question is still relevant because the technique is useful in lots of other places.

There’s actually two sub-questions here:

  • Does it still work? — The answer is, yes, it does, at least in Xcode 9.0b6, and I don’t expect it to break any time soon.

  • Is it still the best approach? — I’m kind of equivocal on that front. It’s still the best technique I know of, but I’m not exactly happy with it (or any code that uses

    unsafeBitCast(_:to:)
    for that matter).

The only other approach I know of is shown below.

typealias CGPathApplierBlockC = (CGPathElement) -> Void

extension CGPath {
    func applyC(body: @escaping CGPathApplierBlockC) {
        var body = body
        self.apply(info: &body) { (info, element) in
            let body2 = info!.assumingMemoryBound(to: CGPathApplierBlockC.self).pointee
            body2(element.pointee)
        }
    }
}

I think this is ‘more correct’ than approach B but it has a serious downside, namely, the block must be flagged as escaping. I guess you could use

withoutActuallyEscaping(_:do:)
to get past that, but that’s not a lot better than
unsafeBitCast(_:to:)
.

Regardless of which approach you use, I do have some concrete advice:

  • Put this code in a self-contained wrapper, so you can easily fix it if it breaks, or if best practice changes in the future.

  • If this is related to an Apple API, file a bug requesting an equivalent block-based API to address this problem in the long term. Hey, it worked for

    CGPathApply
    (-:

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"