Surprising Failures Putting a C Array Inside an Objective-C Struct Property
Asked Answered
S

2

5

I know Objective-C doesn't allow you to use C arrays as property types. I get the compiler error I expect in that case.

But I'm surprised at the behavior I'm seeing with regards to C arrays inside struct properties:

  • No compilation errors or warnings.
  • Unexpected address of the address itself (gdb's info malloc doesn't know about it, not sure if it's uninitialized memory or what. But I would expect a crash or apparently-working albeit with memory corruption).
  • Assignment becomes a no-op.

I boiled it down this example code:

#import <Foundation/Foundation.h>

#ifndef sizeofA
    #define sizeofA(array) (sizeof(array)/sizeof(array[0]))
#endif

@interface IncludeCArrayDirectly : NSObject // Doesn't even compile
// Uncomment below to see the compilation error for yourself.
//@property(nonatomic, assign) int8_t f[9]; // ERROR: Property cannot have array or function type 'int8_t [9]'
@end

@interface IncludeCArrayInStruct : NSObject // Compiles (no warning) and runs but is amazingly broken.
@property(nonatomic, assign) int normalProperty;
@property(nonatomic, assign) struct { int f[9]; } p;
- (void*)normalPropertysAddress;
@end

@interface IncludeCArrayInIvar : NSObject {  // Totally works.
    @public
    int normalIvar;
    int8_t f[9];
}
@end

int main(int argc, const char *argv[]) {
    @autoreleasepool {
        {{
            IncludeCArrayInStruct *a = [IncludeCArrayInStruct new];

            // Notice a.p.f's address is off in 0x7fffxxxx-land:
            printf("&a = %p, &a.normalProperty = %p, a.p.f = %p\n",
                   a, [a normalPropertysAddress], a.p.f);

            printf("a.p.f[4] BEFORE %d\n", a.p.f[4]);
            a.p.f[4] = 42;
            printf("a.p.f[4] AFTER %d\n", a.p.f[4]);
            assert(a.p.f[4] == 0); // Surprise! Assertion passes. Assignment above is a no-op.

            // Dump all of a.p.f just to take a better look:
            for (unsigned i = 0; i < sizeofA(a.p.f); i++) {
                printf("a.p.f[%d] == %d\n", i, a.p.f[i]);
            }
        }}
        {{
            IncludeCArrayInIvar *b = [IncludeCArrayInIvar new];

            // All these addresses are about what you'd expect:
            printf("&b = %p, &b.normalIvar = %p, b.f = %p\n",
                   b, &b->normalIvar, b->f);

            printf("b->f[4] BEFORE %d\n", b->f[4]);
            b->f[4] = 42;
            printf("a->f[4] AFTER %d\n", b->f[4]);
            assert(b->f[4] == 42); // No surprise here, above assignment worked.

            // Dump all of b.f just to take a better look:
            for (unsigned i = 0; i < sizeofA(b->f); i++) {
                printf("b->f[%d] == %d\n", i, b->f[i]);
            }
        }}

    }
    return 0;
}


@implementation IncludeCArrayDirectly
@end

@implementation IncludeCArrayInStruct
- (void*)normalPropertysAddress {
    return &_normalProperty;
}
@end

@implementation IncludeCArrayInIvar
@end

Any explanations to my puzzle points above?

Synesthesia answered 25/7, 2013 at 23:51 Comment(0)
L
9

struct objects are always copied by value, not by reference. This means that when your struct is returned via an accessor method, that returned object is a copy of the one in your object instance. I suspect this comes from C where it makes no difference in the scenario of a standalone function that also shares that return type:

struct sample
{
    int arr[4];
};

struct sample FunctionThatReturnsSample(void)
{
    static struct sample s = { { 0, 1, 2, 3 } };
    return s;
}

int main(void)
{
    FunctionThatReturnsSample().arr[3] = 4;

    printf("%d\n", FunctionThatReturnsSample().arr[3]);
    // still prints "3"
}
Leprous answered 26/7, 2013 at 0:14 Comment(1)
Ah, having the property return a copy of the struct explains the apparent assignment no-op. Thanks!Synesthesia
P
0

While this may not directly answer your question, the answer is most likely found here: objc-accessors.mm

A quick peek at it reveals the mechanisms by which Objective-C properties are synthesized, and this (somewhat troubling) header comment seems relevant.

// This entry point was designed wrong.  When used as a getter, src needs to be locked so that
// if simultaneously used for a setter then there would be contention on src.
// So we need two locks - one of which will be contended.
void objc_copyStruct(void *dest, const void *src, ptrdiff_t size, BOOL atomic, BOOL hasStrong)
Pocahontas answered 26/7, 2013 at 0:5 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.