Strange issue with MemoryLayout.offset(of:)

Hi, I'm currently working on a project which heavily interoperates with C. To ease the work with all the pointers, I have a very useful extension for UnsafePointer (and UnsafeMutablePointer) that looks like this (copied from here):

extension UnsafePointer {
    subscript<T>(_ keyPath: KeyPath<Pointee, T>) -> UnsafePointer<T> {
        let raw = UnsafeRawPointer(self)
        // If a key path is not directly-addressable I consider it programmer error
        let offset = MemoryLayout<Pointee>.offset(of: keyPath)!
        return raw.advanced(by: offset).assumingMemoryBound(to: T.self)
    }
}

extension UnsafeMutablePointer {
    subscript<T>(_ keyPath: KeyPath<Pointee, T>) -> UnsafeMutablePointer<T> {
        let raw = UnsafeMutableRawPointer(self)
        // If a key path is not directly-addressable I consider it programmer error
        let offset = MemoryLayout<Pointee>.offset(of: keyPath)!
        return raw.advanced(by: offset).assumingMemoryBound(to: T.self)
    }
}

This allows me to get a pointer to a specific member of the value stored in the original pointer.
Most of the times it works exactly as it should, but when the KeyPath refers to a member, which is an UnsafePointer as well, then strangely enough the call to MemoryLayout.offset(of:) returns nil which crashes the program. Even stranger is that it doesn't crash, when I don't use the subscript.

If I have e.g. this c struct:

struct Foo {
    int bar;
    int length;
    char * buffer;
};

The program crashes when I do this:

let fooPointer: UnsafeMutablePointer<Foo> = ...

let pointerToBufferPointer = fooPointer[\Foo.buffer]
// should be UnsafeMutablePointer<UnsafeMutablePointer<Int8>>, but crashes because offset is nil in subscript

But it works fine when I do the exact same work as the subscript does myself:

let fooPointer: UnsafeMutablePointer<Foo> = ...

let raw = UnsafeMutableRawPointer(fooPointer)

let bufferPointerOffset = MemoryLayout<Foo>.offset(of: [\Foo.buffer])!

let pointerToBufferPointer = raw.advanced(by: bufferPointerOffset).assumingMemoryBound(to: UnsafeMutablePointer<Int8>.self)

Why doesn't the subscript work, but my code does, even though they are doing the same thing? And why is that only the case, when the value I want is itself a pointer (maybe there are other cases, I didn't test everything yet)?

EDIT:

I tested this a bit more and I didn't get this error with a Swift struct in pure Swift code. Maybe this only occurs with C structs? Or only when the function where I want to do this is passed as a C function pointer to a C function?

What you've described seems fine, and it's working as expected for me (offset is calculated correctly, no crash) in Swift 5.3.1.

You definitely have a pointer in the struct, and not, say, a flexible array member or fixed-size array?

Or only when the function where I want to do this is passed as a C function pointer to a C function?

Can you expand on this bit? I don't quite follow what you mean.

Yes it's defined like in my example above.

To build up on my previous example, I basically have a function in c that takes a function pointer as parameter, which it calls as a callback:

void callCallback(void (* callback)(Foo * foo));

I call it in swift with a closure:

callCallback { (fooPointer: UnsafeMutablePointer<Foo>) in
    // This crashes here:
    // let pointerToBufferPointer = fooPointer[\Foo.buffer]

    // This does work:
    let raw = UnsafeMutableRawPointer(fooPointer)

    let bufferPointerOffset = MemoryLayout<Foo>.offset(of: [\Foo.buffer])!

    let pointerToBufferPointer = raw.advanced(by: bufferPointerOffset).assumingMemoryBound(to: UnsafeMutablePointer<Int8>.self)
}
1 Like

Interesting. Feels like a Swift bug to me then.

I agree, this seems like a bug, especially if the same key path literal is giving you different behavior in different contexts.

Terms of Service

Privacy Policy

Cookie Policy