9 Replies
      Latest reply: Aug 29, 2017 4:14 AM by eskimo RSS
      ole Level 1 Level 1 (20 points)

        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.

        • Re: Populating a Swift array in a C function callback
          Jens Level 3 Level 3 (230 points)

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

          • Re: Populating a Swift array in a C function callback
            LCS Level 4 Level 4 (595 points)

            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).

              • Re: Populating a Swift array in a C function callback
                ole Level 1 Level 1 (20 points)

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

                • Re: Populating a Swift array in a C function callback
                  ole Level 1 Level 1 (20 points)

                  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().

                    • Re: Populating a Swift array in a C function callback
                      eskimo Apple Staff Apple Staff (7,530 points)

                      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"

                        • Re: Populating a Swift array in a C function callback
                          ole Level 1 Level 1 (20 points)

                          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.

                          • Re: Populating a Swift array in a C function callback
                            eskimo Apple Staff Apple Staff (7,530 points)

                            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"

                              • Re: Populating a Swift array in a C function callback
                                eskimo Apple Staff Apple Staff (7,530 points)

                                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"