2017-11-02

SZLib Super

Something missing from SZLib is a way for subclasses to call the implementation of a method they’ve overridden. For example, in Objective-C:

@interface ClassA : NSObject

- (void)printMe;

@end

@interface ClassB : ClassA
@end

@interface ClassC : ClassB
@end

@implementation ClassA

- (void)printMe {
    printf("ClassA");
}

@end

@implementation ClassB
@end

@implementation ClassC

- (void)printMe {
    [super printMe];
    printf("ClassC");
}

@end

int main(int argc, const char *argv[]) {
    @autoreleasepool {
        ClassC *c = [[ClassC alloc] init];
        [c printMe];
    }

    return 0;
}

The output of this program is ClassAClassC. The call to super in ClassC is smart enough to realize that ClassB doesn’t implement the method and will fall back to the implementation in ClassA. super can be used recursively to move up through the class hierarchy despite the type of the object being acted upon not changing.

I’ve been trying to come up with a way of supporting something like this in SZLib, but it turns out that it’s tricky to implement tidily without compiler support.

First, I moved the callbacks struct out of the objects that I create and into their metaclasses. Here’s the callback struct from SZObject:

typedef struct {
    int (*equals)(SZTypeRef, SZTypeRef);
    int (*compare)(SZTypeRef, SZTypeRef);
    unsigned long (*hash)(SZTypeRef);
    SZTypeRef (*copy)(SZTypeRef);
    void (*dealloc)(SZTypeRef);
    char *(*description)(SZTypeRef);
} SZObjectCallbacks;

Previously I’d pass this into the object’s initializer:

SZTypeRef SZObjectInit(SZTypeRef ref, const SZObjectCallbacks *callbacks);

Aside from being awkward, this meant that each object could only reference the single callback struct passed to it in its initializer. There was no way to work back through the class hierarchy and examine the callbacks of any superclasses. The workaround I used was for classes to expose their callback functions and allow subclasses to call them (so SZSomeObject had a function __SZSomeObjectEquals() exposed in its header, which subclasses of SZSomeObject could call in their own equals implementation). This too was awkward, as it exposed the internals of the superclasses and required that subclasses knew more than they really needed to about the implementation of their superclass.

Callbacks are now passed to the allocator, not the initializer, as part of the metaclass:

typedef struct __SZMetaClass {
    const struct __SZMetaClass *parentClass;
    const char *className;
    const SZObjectCallbacks *callbacks;
} SZMetaClass;

SZTypeRef SZObjectAllocate(size_t size, const SZMetaClass *metaclass);

Each metaclass has a pointer to its superclass, instantly giving us a hierarchy we can walk.

Now that we have a hierarchy we still need to be able to:

  • Call the superclass’ implementation of a given function from an overriding function in a subclass;
  • Walk back up the hierarchy if we find that a superclass doesn’t implement a given function until we find a class that does;
  • Allow recursive calls of the function so that each subclass in the hierachy can call its superclass’ implementation.

This is last point in particular is harder than it sounds. The obvious approach fails miserably:

void SZSomeObjectEquals(SZTypeRef ref1, SZTypeRef ref2) {
    SZObjectRef obj1 = (SZObjectRef)ref1;
    SZObjectRef obj2 = (SZObjectRef)ref2;

    // Attempt to call "super" implementation of "equals"
    return obj1->metaclass->parentClass->equals(obj1, obj2);
}

Accessing the parentClass in this way gives us the same metaclass each time, so if the superclass’ implementation of equals calls its own superclass it’ll get stuck in a loop. We’d need to pass the metaclass as an argument to the function so that we can update it on each successive call:

void SZSomeObjectEquals(SZTypeRef ref1, SZTypeRef ref2) {
    SZObjectRef obj1 = (SZObjectRef)ref1;
    SZObjectRef obj2 = (SZObjectRef)ref2;

    // Call the immediate superclass' implementation of "equals"
    return obj1->metaclass->parentClass->equals(obj1, obj2, obj->metaclass->parentClass);
}

That implies a public API that doesn’t include a metaclass parameter vs an internal API that expects a metaclass parameter.

We also need to deal with the possibility that some classes have opted not to override the function, so we end up with something like this in SZObject:

int SZObjectEqualsUsingMetaClass(SZTypeRef ref1, SZTypeRef ref2, const SZMetaClass *metaclass) {
    const SZObjectRef obj1 = (SZObjectRef)ref1;
    const SZObjectRef obj2 = (SZObjectRef)ref2;

    // Hunt for the next valid implementation of "equals" in the superclass hierarchy
    while (metaclass && metaclass->parentClass && !metaclass->callbacks->equals) {
        metaclass = metaclass->parentClass;
    }

    // Call the implementation of "equals" that we found
    return metaclass->callbacks->equals(obj1, obj2, metaclass);
}

We’d use that like this:

void SZSomeObjectEquals(SZSomeObject obj1, SZSomeObject obj2) {

    // Call the "super" implementation of "equals"
    return SZObjectEqualsUsingMetaClass(obj1, obj2, obj.object->metaclass->parentClass);
}

Our callback struct has to accept the metaclass as a parameter in each callback function:

typedef struct {
    int (*equals)(SZTypeRef, SZTypeRef, const SZMetaClass *);
    int (*compare)(SZTypeRef, SZTypeRef, const SZMetaClass *);
    unsigned long (*hash)(SZTypeRef, const SZMetaClass *);
    SZTypeRef (*copy)(SZTypeRef, const SZMetaClass *);
    void (*dealloc)(SZTypeRef, const SZMetaClass *);
    char *(*description)(SZTypeRef, const SZMetaClass *);
} SZObjectCallbacks;

This is still a little awkward:

  • I’d rather hide the metaclass stuff somehow rather than force each overridden function to deal with it;
  • There needs to be a function to lookup the correct callback for each callback;
  • Callbacks need to be looked up each time the function is called.

The overhead of the lookup isn’t significant so I’m not worried about that, at least with the shallow class hierarchies I’m building. The proliferation of additional functions is unfortunate but I don’t see a way around it in C (I came up with a macro that would generate lookup functions automatically, but it transpires that the C preprocessor doesn’t allow the token pasting operator to inject struct fields (obj->metaclass->##name## is forbidden) which killed that idea).

In spite of the limitations this system works well enough, and achieves the initial goals:

  • Superclasses can stop exposing their implementations of the various callbacks;
  • Subclasses can call the standard …UsingMetaClass() functions instead of trying to figure out what their superclasses are doing;
  • Subclasses can specify NULL as their callback function and the system will gracefully fall back to the superclass’ implementation.