pointersobjective-c-runtimemacos-carbonzig

Convert CFArray to pointer


I've been using the objective-c runtime with Zig:

const carbon = @cImport({
    @cInclude("Carbon/Carbon.h");
});

const objc = @cImport({
    @cInclude("objc/runtime.h");
});

I wrote a wrapper Window around the objective-c runtime window type.

const Window = packed struct {
    inner: ?*const anyopaque,
    ...
};

Now, given a CFArray of ?*const anyopaque, I want to extract the pointer [*]const ?*const anyopaque and @ptrCast it to [*]const Window, assuming such a cast is well-formed. How can I get the underlying pointer of the CFArray?

I looked at the documentation for CFArray, but I couldn't find anything. I also tried this, in objective-c:

CFArrayRef a = CFArrayCreate(nil, (void const *[]){@1, @2}, 2, nil);
NSLog("%p\n", &a[0]);

but it doesn't work either:

main.m:6:21: error: subscript of pointer to incomplete type 'const struct __CFArray'
    NSLog("%p\n", &a[0]);
                   ~^
/Library/Developer/CommandLineTools/SDKs/MacOSX13.sdk/System/Library/Frameworks/CoreFoundation.framework/Headers/CFArray.h:108:47: note: forward declaration of 'struct __CFArray'
typedef const struct CF_BRIDGED_TYPE(NSArray) __CFArray * CFArrayRef;
                                              ^
1 error generated.

Solution

  • There is no "underlying pointer of the CFArray." CFArray does not promise that its contents are contiguous, and they are not always. If you need contiguous memory, you'll need to copy the values using CFArrayGetValues.

    That's the whole answer, but if you want the gory details, read on.

    Apple has been somewhat inconsistent in releasing Core Foundation source code, but you can inspect an older version (~2015) in CFArray.c. Its default internals at the time looked like this:

    struct __CFArray {
        CFRuntimeBase _base;
        CFIndex _count;     /* number of objects */
        CFIndex _mutations;
        int32_t _mutInProgress;
        __strong void *_store;           /* can be NULL when MutableDeque */
    };
    

    The _store is a __CFArrayDeque:

    struct __CFArrayDeque {
        uintptr_t _leftIdx;
        uintptr_t _capacity;
        /* struct __CFArrayBucket buckets follow here */
    };
    

    And a __CFArrayBucket looks like this:

    struct __CFArrayBucket {
        const void *_item;
    };
    

    And so, using this, you could look some number of bytes past the header, and grab a pointer to stuff. This is how that was implemented in 2015 (but may not be this way now, and may be different on different platforms):

    CF_INLINE struct __CFArrayBucket *__CFArrayGetBucketsPtr(CFArrayRef array) {
        switch (__CFArrayGetType(array)) {
        case __kCFArrayImmutable:
        return (struct __CFArrayBucket *)((uint8_t *)array + __CFArrayGetSizeOfType(((CFRuntimeBase *)array)->_cfinfo[CF_INFO_BITS]));
        case __kCFArrayDeque: {
        struct __CFArrayDeque *deque = (struct __CFArrayDeque *)array->_store;
            return (struct __CFArrayBucket *)((uint8_t *)deque + sizeof(struct __CFArrayDeque) + deque->_leftIdx * sizeof(struct __CFArrayBucket));
        }
        }
        return NULL;
    }
    

    Anything you build is going to be version dependent, because Apple sometimes changes the internal implementation of these types. But it's worse than that. Remember that I said "default internals." This will work (for a given version of the OS) if you know that the CFArray is really a CFArray. But it may not be due to toll-free bridging (which is likely in the case you're describing).

    The first line of CFArrayGetValues is:

    CF_OBJC_FUNCDISPATCHV(CFArrayGetTypeID(), void, (NSArray *)array, getObjects:(id *)values range:NSMakeRange(range.location, range.length));
    

    CF_OBJC_FUNCDISPATCHV() is a function that checks if this happens to actually be an NSArray, or a subclass, or even something that is pretending to be an NSArray, which is a thing that happens all the time. If so, the request is handled by ObjC, not by this function. That type can be implemented any way it likes, and does not have to be contiguous memory. It could, for example, be backed by a DispatchData, which can stitch together existing memory buffers without copying them.

    CF_OBJC_FUNCDISPATCHV() is quite magical, and can interpose itself into the function call, so that the called method can directly return to the caller, ignoring the rest of this function.

    Anyway, this is a long, involved explanation of the fact that CFArray is not just a thin wrapper around a C array or C++ vector. It really is an object, with all the polymorphism that implies.