3

I'm having trouble exploring the results of a Spotlight search in Swift 3 using MDQuery. I expect MDQueryGetResultAtIndex to yield an MDItem, and in C/Objective-C, that assumption works, and I can call MDItemCopyAttribute on it to explore the item. Here, for example, I successfully get the pathname of a found item:

MDItemRef item = (MDItemRef)MDQueryGetResultAtIndex(q,i);
CFStringRef path = MDItemCopyAttribute(item,kMDItemPath);

But in Swift 3, MDQueryGetResultAtIndex returns an UnsafeRawPointer! (it's a pointer-to-void in C). To get past that, I've tried, for example:

if let item = MDQueryGetResultAtIndex(q, 0) {
    let ptr = item.bindMemory(to: MDItem.self, capacity: 1)
    let path = MDItemCopyAttribute(ptr.pointee, kMDItemPath)
}

but that crashes, and logging shows that ptr.pointee is an NSAtom. It is quite evident that my personal UnsafeRawPointer mojo is not working (and to be frank I've always found this confusing).

How would I transform this UnsafeRawPointer into something I can successfully call MDItemCopyAttribute on?

Alternatives

  • I can get over this hump by putting my Objective-C code into an Objective-C helper object and calling it from Swift; but I'd like to write a pure Swift solution.

  • Similarly, I could probably rewrite my code to use the higher-level NSMetadataQuery, and I may well do so; but my original Objective-C code using the lower-level MDQueryRef works fine, so now I'm curious how to turn it directly into Swift.


Complete code for those who would like to try this at home:

let s = "kMDItemDisplayName == \"test\"" // you probably have one somewhere
let q = MDQueryCreate(nil, s as CFString, nil, nil)
MDQueryExecute(q, CFOptionFlags(kMDQuerySynchronous.rawValue))
let ct = MDQueryGetResultCount(q)
if ct > 0 {
    if let item = MDQueryGetResultAtIndex(q, 0) {
        // ... 
    }
}
matt
  • 515,959
  • 87
  • 875
  • 1,141
  • Dumb question, what about `item?.assumingMemoryBound(to: MDItem.self)?.pointee` to get the MDItem? – JAL Apr 18 '17 at 17:48
  • @JAL same result as `bindMemory`. I also tried using them together, just in case. :) – matt Apr 18 '17 at 18:08
  • Ah ok, worth a shot. My guess as to why you're getting the `NSAtom` is because your `ptr` is turning into `UnsafePointer(0x1)`, and getting the `pointee` of that is the equivalent of calling something like `(id)0x1` which is an `__NSAtom`. I'll keep digging. – JAL Apr 18 '17 at 18:10
  • I would *assume* you have to go through `Unmanaged.fromOpaque(...)` – but I have no idea how `MDQuery` works, so cannot test it. – Hamish Apr 18 '17 at 18:16
  • @Hamish I'll try it! I could also post more code, of course. Hang on a sec. – matt Apr 18 '17 at 18:22
  • @Hamish, yep, you got it. I wrote `let realitem = Unmanaged.fromOpaque(item).takeUnretainedValue()` and badda-bing badda-boom. Go ahead and post an answer! Would you mind explaining _why_ this works and my code didn't? I've never understood this dance. – matt Apr 18 '17 at 18:29
  • @Hamish: You are right (was about to write an answer when I saw your comment) . It is the same mechanism as in http://stackoverflow.com/a/33310021/1187415: `let item = Unmanaged.fromOpaque(rawPtr).takeUnretainedValue()` – or `let item = bridge(ptr: rawPtr) as MDItem` using the helper functions from that posting. – Martin R Apr 18 '17 at 18:31
  • Ah @Hamish that's brilliant. Is this because the function `MDQueryGetResultAtIndex` returns an `MDItemRef`, which is really an unmanaged CoreFoundation object that Swift is incorrectly bridging as an actual `MDItem` instance (instead of `Unmanaged`)? Sounds like we should open a radar. – JAL Apr 18 '17 at 18:32
  • Yes, @MartinR, of course I remember that post of yours (it's the one I was only able to upvote once). But isn't binding memory _also_ a way of casting the pointer? Obviously not, in this case; but _why_ not? – matt Apr 18 '17 at 18:34
  • @matt: You cast the pointer and then *dereference* it in `ptr.pointee`, which is the problem. In other words, you interpret the void pointer as a *pointer* to an `MDItemRef` – Martin R Apr 18 '17 at 18:37
  • @MartinR I did that because when I logged `ptr` all I saw was a number, presumably the address. – matt Apr 18 '17 at 18:37
  • @matt The problem is that you're trying to rebind the pointee's memory (i.e the underlying instance) to an `MDItem`, which is a reference type. So you're interpreting (part of) the underlying instance as a pointer to the instance. – Hamish Apr 18 '17 at 18:46
  • @JAL Yes, it's brought into Swift as an opaque pointer to an Core Foundation object (i.e `void*`). I wouldn't say it's incorrectly bridged (apart from the type safety hole, there's nothing really wrong with giving you back an opaque pointer). From what I *understand* from the `MDQueryGetResultAtIndex` docs, it's possible to get back a custom object if you want through supplying a custom creation function – so I don't think it's possible for Swift to bridge it back directly as an `MDItem` (as it might not be). – Hamish Apr 18 '17 at 19:48

1 Answers1

3

The problem in your code

if let item = MDQueryGetResultAtIndex(q, 0) {
    let ptr = item.bindMemory(to: MDItem.self, capacity: 1)
    let path = MDItemCopyAttribute(ptr.pointee, kMDItemPath)
}

is that the UnsafeRawPointer is interpreted as a pointer to an MDItem reference and then dereferenced in ptr.pointee, but the raw pointer is the MDItem reference, so it is dereferenced once too often.

The "shortest" method to convert the raw pointer to an MDItem reference is unsafeBitCast:

let item = unsafeBitCast(rawPtr, to: MDItem.self)

which is the direct analogue of an (Objective-)C cast. You can also use the Unmanaged methods to convert the raw pointer to an unmanaged reference and from there to a managed reference (compare How to cast self to UnsafeMutablePointer<Void> type in swift):

let item = Unmanaged<MDItem>.fromOpaque(rawPtr).takeUnretainedValue()

This looks a bit more complicated but perhaps expresses the intention more clearly. The latter approach works also with (+1) retained references (using takeRetainedValue()).

Self-contained example:

import CoreServices

let queryString = "kMDItemContentType = com.apple.application-bundle"
if let query = MDQueryCreate(kCFAllocatorDefault, queryString as CFString, nil, nil) {
    MDQueryExecute(query, CFOptionFlags(kMDQuerySynchronous.rawValue))

    for i in 0..<MDQueryGetResultCount(query) {
        if let rawPtr = MDQueryGetResultAtIndex(query, i) {
            let item = Unmanaged<MDItem>.fromOpaque(rawPtr).takeUnretainedValue()
            if let path = MDItemCopyAttribute(item, kMDItemPath) as? String {
                print(path)
            }
        }
    }
}
Community
  • 1
  • 1
Martin R
  • 529,903
  • 94
  • 1,240
  • 1,382
  • "that the UnsafeRawPointer is interpreted as a _pointer_ to an MDItem reference and then dereferenced in `ptr.pointee`, but the raw pointer _is_ the MDItem reference" And this is the key fact about when the `fromOpaque` dance is needed that I have never understood. As JAL implies in a comment above, the way a CFType gets bridged is what trips me up here. – matt Apr 18 '17 at 19:27
  • Nice answer Martin and great question matt. Sounds like this API is a perfect example of one that needs an update for Swift. – JAL Apr 18 '17 at 19:54
  • I think I see what @JAL is getting at here. The usual assumption behind a pointer-to-void / UnsafeRawPointer is that we've cast away the type of what's _at the far end_ of the pointer. But when a CFTypeRef is slotted in here, as Martin R says, it _is_ the pointer. That's what I've never appreciated until now. – matt Apr 19 '17 at 00:08