Deleted
Deleted

Reputation: 771

Objc runtime set ivar with value

I'm trying to dynamically create a class with data loaded from JSON. I have successfully created the class, but still stuck at how to add several constants with the values loaded from the JSON.

For example, the JSON below:

"Josh": {
    "age": 10
}

I want a class like

class Josh {
    int age = 10;
}

However, I can only achieve

class Josh {
    int age;
}

with the following code:

Class Josh = objc_allocateClassPair([NSObject class], "Josh", 0);
class_addIvar(Josh, "age", sizeof(int), rint(log2(sizeof(int))), @encode(int));
objc_registerClassPair(Josh);
// Below is the desired result
Josh j = [[Josh alloc] init];
NSLog([j age]); // prints 10

So can anybody answer me, how to add the primitive value 10 to this Ivar I've added? Thank you

Upvotes: 1

Views: 940

Answers (1)

rob mayoff
rob mayoff

Reputation: 385500

What you are doing is almost certainly madness and you shouldn't be messing with it.

That said, the code you wrote doesn't even compile.

So let's fix it.

You can't declare Josh j, because Josh is a variable, not a type. Instead you can use id, which means “any Objective-C object”.

id j = [[Josh alloc] init];

You also can't say [j age] unless you declare an age method somewhere, so stick this somewhere:

@protocol Dummy <NSObject>
- (int)age;
@end

You also can't say NSLog([j age]) because [j age] returns an int, not an NSString *. So do this instead:

NSLog(@"%d", [j age]);

Now you'll find that you get an “unrecognized selector” error, because you didn't add an age method to your Josh class. To do that, you first have to import the Objective-C runtime:

#import <objc/runtime.h>

Then you can add an age method like this:

        Ivar ageIvar = class_getInstanceVariable(Josh, "age");
        ptrdiff_t ageIvarOffset = ivar_getOffset(ageIvar);

        IMP ageImp = imp_implementationWithBlock(^int(id self) {
            void *base = (__bridge void *)self;
            return *(int *)((unsigned char *)base + ageIvarOffset);
        });
        class_addMethod(Josh, @selector(age), ageImp, "v@:");

Here's the complete, compilable, non-crashing program:

#import <Foundation/Foundation.h>
#import <objc/runtime.h>

@protocol Dummy <NSObject>
- (int)age;
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Class Josh = objc_allocateClassPair([NSObject class], "Josh", 0);

        class_addIvar(Josh, "age", sizeof(int), rint(log2(sizeof(int))), @encode(int));

        Ivar ageIvar = class_getInstanceVariable(Josh, "age");
        ptrdiff_t ageIvarOffset = ivar_getOffset(ageIvar);

        IMP ageImp = imp_implementationWithBlock(^int(id self) {
            void *base = (__bridge void *)self;
            return *(int *)((unsigned char *)base + ageIvarOffset);
        });
        class_addMethod(Josh, @selector(age), ageImp, "v@:");

        objc_registerClassPair(Josh);
        // Below is the desired result
        id j = [[Josh alloc] init];
        NSLog(@"%d", [j age]); // prints 10
    }
    return 0;
}

But it still just prints “0“:

2018-06-25 15:07:53.179809-0500 madness[35050:5762222] 0
Program ended with exit code: 0

If you want to initialize age to 10, you need to override init in your Josh class. If you were writing the init method normally, it would look like this:

- (instancetype)init {
    if (self = [super init]) {
        _age = 10;
    }
    return self;
}

But we can only send to super in something the compiler recognizes as a method body, and a block (like we'll pass to imp_implementationWithBlock) doesn't count as a method body. So we have to do the [super init] part the hard way, by calling objc_msgSendSuper. To do that, we have to import another header:

#import <objc/message.h>

Then we can write the block that implements init:

        IMP initImp = imp_implementationWithBlock(^id(id self) {
            struct objc_super superInfo = {
                .receiver = self,
                .super_class = NSObject.self
            };
            id (*msgSendSuper)(struct objc_super *, SEL) = (id (*)(struct objc_super *, SEL))objc_msgSendSuper;
            self = msgSendSuper(&superInfo, @selector(init));
            if (self) {
                void *base = (__bridge void *)self;
                *(int *)((unsigned char *)base + ageIvarOffset) = 10;
            }
            return self;
        });
        class_addMethod(Josh, @selector(init), initImp, "@@:");

Here's the complete program:

#import <Foundation/Foundation.h>
#import <objc/runtime.h>
#import <objc/message.h>

@protocol Dummy <NSObject>
- (int)age;
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Class Josh = objc_allocateClassPair([NSObject class], "Josh", 0);

        class_addIvar(Josh, "age", sizeof(int), rint(log2(sizeof(int))), @encode(int));

        Ivar ageIvar = class_getInstanceVariable(Josh, "age");
        ptrdiff_t ageIvarOffset = ivar_getOffset(ageIvar);

        IMP ageImp = imp_implementationWithBlock(^int(id self) {
            void *base = (__bridge void *)self;
            return *(int *)((unsigned char *)base + ageIvarOffset);
        });
        class_addMethod(Josh, @selector(age), ageImp, "v@:");

        IMP initImp = imp_implementationWithBlock(^id(id self) {
            struct objc_super superInfo = {
                .receiver = self,
                .super_class = NSObject.self
            };
            id (*msgSendSuper)(struct objc_super *, SEL) = (id (*)(struct objc_super *, SEL))objc_msgSendSuper;
            self = msgSendSuper(&superInfo, @selector(init));
            if (self) {
                void *base = (__bridge void *)self;
                *(int *)((unsigned char *)base + ageIvarOffset) = 10;
            }
            return self;
        });
        class_addMethod(Josh, @selector(init), initImp, "@@:");

        objc_registerClassPair(Josh);
        // Below is the desired result
        id j = [[Josh alloc] init];
        NSLog(@"%d", [j age]); // prints 10
    }
    return 0;
}

And here's the output:

2018-06-25 15:31:41.837748-0500 madness[35369:5786038] 10
Program ended with exit code: 0

Upvotes: 3

Related Questions