Uninitialized Void Pointers and C API in Swift 5

In all of the Swift manual memory management docs and writing I've come across, I've yet to see examples of dealing with C void pointers. I'm attempting to interact with an API nearly identical to C binary search trees — the imported signatures are the same as tsearch, etc., so I've been testing with with those — but only half successfully.

In plain C, the below creates the tree at root and places item within it:

void *root;
tsearch(&item, &root, &compare_method);

The Swift signature for this function is tsearch(UnsafeRawPointer!, UnsafeMutablePointer<UnsafeMutableRawPointer?>!, (UnsafeRawPointer?, UnsafeRawPointer?) -> Int32!), and to satisfy all compiler warnings I've called it like so:

var root: OpaquePointer?
withUnsafeMutablePointer(to: &root) {
    $0.withMemoryRebound(to: UnsafeMutableRawPointer?.self, capacity: 1) { rootPointer in
        let nodePointer = UnsafeMutablePointer<Item>.allocate(capacity: 1)
        nodePointer.initialize(to: item)
        tsearch(UnsafeRawPointer(nodePointer), rootPointer, tSearchCompareCallback)
    }
}

My tSearchCompareCallback is confirmed as receiving raw pointers that are loading to their types as expected (I've tested with String, structs, objects, you name it, and have added numerous at a time without a problem).

The issue is any further attempts via tfind or twalk to access root, whether as a class property or simply within the same func or block, fail. In taking many approaches, almost all resulting in either "Fatal error: UnsafeMutablePointer.initialize overlapping range" or "EXC_BAD_ACCESS", I'm assuming that despite seemingly being able to populate a search tree I am actually off base with using an OpaquePointer.

Is there a standard Swift way to call a C function expecting an uninitialized void * parameter?

I would do this:

struct Item : Comparable {
  var x: Int

  static func < (lhs: Item, rhs: Item) -> Bool {
    return lhs.x < rhs.x
  }
}

let tSearchCompareCallback : @convention(c) (UnsafeRawPointer?, UnsafeRawPointer?) -> Int32 = {
  let i1 = $0!.load(as: Item.self)
  let i2 = $1!.load(as: Item.self)
  if i1 < i2 {
    return -1
  }
  if i1 > i2 {
    return 1
  }
  return 0
}

func run(item: Item) {
  var root: UnsafeMutableRawPointer?
  withUnsafeMutablePointer(to: &root) { (rootPointer) in
    let nodePointer = UnsafeMutablePointer<Item>.allocate(capacity: 1)
    nodePointer.initialize(to: item)
    tsearch(nodePointer, rootPointer, tSearchCompareCallback)
  }
}

Although I think the C function should really be declared:
void tsearch(const void*, void const **, int(const void*, const void*));

You've still made this too hard. It shouldn't really be worse than the C version.

func run(item: Item) {
  var root: UnsafeMutableRawPointer? = …
  var mutableItem = item
  // now that you've found the value, get it back out
  // EDIT: THIS IS WRONG; IT LEADS TO A DANGLING POINTER TO THE STACK
  if let foundPtr = tsearch(&mutableItem, &root, tSearchCompareCallback) {
    let result = foundPtr.load(as: Item.self)
    // …
  }
}

The only reason to do the mutableItem thing is because of what Andy said: the function should be declared to operate on const values.

@jrose This API stores pointers to user allocated data in the tree. So if you ignore for a moment that I wrapped everything in a run function so that root doesn't escape (nor is it correctly deleted), then it looks like the call to tsearch will escape a pointer to your Swift local variable, considering that the item's pointer will be inserted in the tree when it isn't found.

Oh, bleah, you're right, of course. I got it mixed up with tfind, convinced myself that "oh, tsearch is going to mutate the tree; that's why it takes the tree pointer non-const", and then forgot to re-examine the initial assumptions.

The lesson is, try to avoid working directly with pointers on the Swift side. You can often avoid withUnsafeMutablePointer as @jrose pointed out, and if at all possible avoid withMemoryRebound by defining variables of the correct type on the Swift side, and copying back to another type if needed. You can also avoid declaring pointers-to-pointers on the Swift side by using the UnsafeRawPointer.load(as: SomePointerType.self) API.

Below I pasted a more complete example. There may be logic bugs or leaks since I didn't test it. And we're approaching the question "why would want a wrapper around tsearch when the whole thing's much easier and more efficient in pure Swift". But hopefully this clears up the interoperability confusion...

import Darwin

struct Item : Comparable {
  var x: Int

  static func < (lhs: Item, rhs: Item) -> Bool {
    return lhs.x < rhs.x
  }
}

class Tree {
  var root: UnsafeMutableRawPointer? = nil

  private static let itemCompareCallback : @convention(c) (UnsafeRawPointer?, UnsafeRawPointer?) -> Int32 = {
    let i1 = $0!.load(as: Item.self)
    let i2 = $1!.load(as: Item.self)
    if i1 < i2 {
      return -1
    }
    else if i1 > i2 {
      return 1
    }
    return 0
  }

  private static func getItemPointer(nodePointer: UnsafeRawPointer) -> UnsafeMutablePointer<Item> {
    // The first word in the tree node is a pointer to item.
    return nodePointer.load(as: UnsafeMutablePointer<Item>.self)
  }

  deinit {
    while (root != nil) {
      let itemPointer = Tree.getItemPointer(nodePointer: root!)
      tdelete(itemPointer, &root, Tree.itemCompareCallback)
      itemPointer.deallocate()
    }
  }

  func insert(_ item: Item) {
    let itemPointer = UnsafeMutablePointer<Item>.allocate(capacity: 1)
    itemPointer.initialize(to: item)
    let nodePointer = tsearch(itemPointer, &root, Tree.itemCompareCallback)!
    if (Tree.getItemPointer(nodePointer: nodePointer) != itemPointer) {
      itemPointer.deallocate()
    }
  }

  func contains(_ item: Item) -> Bool {
    var item = item
    return tfind(&item, &root, Tree.itemCompareCallback) != nil
  }
}

var tree = Tree()
tree.insert(Item(x:0))
tree.insert(Item(x:0))
print(tree.contains(Item(x:0)))
print(tree.contains(Item(x:3)))
1 Like
Terms of Service

Privacy Policy

Cookie Policy