On Oct 23, 2006, at 6:44 PM, David Balmain wrote:

There are costs to this approach.  It requires a fair amount of
boilerplate code. (We might want to consider using a symbol
generator.)  I'm also pretty sure we'll want to write some
bootstrapping code to prep the virtual tables at startup, because it
will be difficult if not impossible to resolve all aspects of
inheritance at compile-time.

Could you give me an example of where this is difficult?

The place where it's hardest is in the definition statements for the class data. You'd like child classes to clone the parent's table, then selectively overwrite the slots that they don't inherit. That's possible with bootstrapping, but I can't think of a way to do that at compile time.

The only way to make compile-time resolution work is to copy-and- paste each member that gets inherited. The bigger the parent class, the more copying and pasting. And if we ever add a method to Obj, from which all classes inherit, we have to manually add that member to every class.

Here's how compile-time resolution with Ferret-style inheritance looks:

    /* in Dog.h */
    typedef struct DOG {
        Animal super;
        Dog_chase_cats_t chase_cats;
    } DOG;
    extern const DOG DOG_CLASSDATA;

    /* in Animal/Dog.c */
    const DOG DOG_CLASSDATA = {
        {
            "Animal::Dog",
            (Animal_speak_t) Dog_speak,
            (Animal_eat_t)   Dog_eat
        },
        Dog_chase_cats
    };

    /* in Animal/Dog/PitBull.c */
    const PIT_BULL PIT_BULL_CLASSDATA = {
        {
            {
                "Animal::PitBull",
                (Animal_speak_t) Dog_speak,
                (Animal_eat_t)   Dog_eat
            },
            Dog_chase_cats
        },
        PitBull_chase_humans
    };

That last one's getting pretty unwieldy, and we've only got a couple members -- imagine what things start to look like with MultiReader. We can't macrofy these, either, because functions get overridden right in the middle of the stuff we'd like to macrofy.

Compile-time resolution with KinoSearch-style inheritance is only marginally less messy.

    /* in Dog.h */
    #define DOG_BASE \
        ANIMAL_BASE \
        Dog_chase_cats_t chase_cats;

    typedef struct DOG {
        DOG_BASE
    } DOG;
    extern const DOG DOG_CLASSDATA;

    /* in Animal/Dog.c */
    const DOG DOG_CLASSDATA = {
                         "Animal::Dog",
        (Animal_speak_t)  Dog_speak,
        (Animal_eat_t)    Dog_eat,
                          Dog_chase_cats
    };

    /* in Animal/Dog/PitBull.c */
    const PIT_BULL PIT_BULL_CLASSDATA = {
                         "Animal::Dog",
        (Animal_speak_t)  Dog_speak,
        (Animal_eat_t)    Dog_eat,
                          Dog_chase_cats,
                          PitBull_chase_humans
    };

The braces go away, which is nice, but we still have all those function names to copy and paste.

With bootstrapping, you can do this instead:

    /* in Animal/Dog/PitBull.c */
    void
    PitBull_bootstrap()
    {
        memcpy(&PITBULL_CLASSDATA, &DOG_CLASSDATA, sizeof(DOG));
        PITBULL_CLASSDATA.chase_humans = PitBull_chase_humans;
    }

That's a lot simpler.

I really
don't like the idea of bootstrapping, simply because it makes it
impossible to clean up the memory allocated during this process when
the application exits which in turn makes it difficult to use valgrind
to track down memory leaks.

I certainly want to be using Valgrind for that, and I don't think I see a conflict there.

If we declare the class tables as global variables, and the bootstrapping performs assignment but no allocation as in the above example, Valgrind won't conflate the globals with memory leaked from malloc.

And even if we do malloc the tables, shouldn't it be possible to clean up everything legit using a cascade of tear-down functions? The trick is that you have to make sure all objects are destroyed before you remove the classdata they rely upon. I think we can pull that off. But I don't think we have to.

As an aside, I wonder if there's a potential benefit for resolving this stuff at compile-time in that the more that you declare as const, the better the compiler does at optimization. I don't think there is, though, because even if all the virtual tables are declared as const, the compiler never knows _which_ const table it will get pointed at during any given method invocation. So it's always going to have to read the table pointer from the object into a register, add the offset for that method to get at the right function pointer in the classdata struct, and jump into code from there.

I'm happy to give up my beloved valgrind for this
project if it is really necessary.

I consider Valgrind absolutely essential for development. KinoSearch has a script ($DIST_ROOT/devel/valgrind_test.plx), which runs the whole test suite under Valgrind and logs the output. It takes 15 minutes instead of 9 seconds to finish, but it's worth it.

Marvin Humphrey
Rectangular Research
http://www.rectangular.com/


Reply via email to