Added garfield and snoopy classes to ./example which are derived from the cat and dog classes.

This commit is contained in:
Johannes Findeisen 2026-10-02 23:38:40 +02:00
commit 32e7d0b337
13 changed files with 1519 additions and 69 deletions

274
example/snoopy.c Normal file
View file

@ -0,0 +1,274 @@
/*
* This file is part of libooc.
* https://xw3.org/hanez/libooc
*
* Copyright 2026 Johannes Findeisen <you@hanez.org>
* Licensed under the terms of the Apache-2.0 license.
* https://opensource.org/license/apache-2-0
*/
/*
* Snoopy -- a subclass of Dog, and so a subclass of a subclass.
*
* The parallel of garfield.c on the other branch of the hierarchy, and the two
* together are what make the hierarchy worth having: Garfield is a Cat,
* Snoopy is a Dog, both are Animals, and a caller holding an Animal * can ask
* ooc_is_a() which one it has and get an answer.
*
* Like every subclass here, Snoopy allocates from its own class record, builds
* the classes above it by calling their constructors rather than duplicating
* them, and registers its own vtable and destructor. What this branch adds is
* the fork: `imagination` and `__flights` exist only on Snoopy, and the chain
* from Snoopy_class leads to Dog_class rather than to Cat_class, so a name
* declared on one branch is unreachable from the other.
*/
#include "snoopy.h"
#include <stddef.h>
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
/*
* Copy `text` onto the heap; the caller owns the result.
*
* A private copy of the same helper animal.c, cat.c, dog.c and garfield.c each
* keep, and identical to all of them. Four copies is past the point where a real
* codebase would publish it instead, but each class owning its own construction
* state is the arrangement the rest of these examples follow, and a divergence
* here would be the thing to notice rather than copy.
*
* Returns NULL if `text` is NULL or the allocation fails. The size is checked
* before the terminator is added, so a string long enough to wrap cannot ask
* malloc() for a short buffer.
*/
static char *dupstr(const char *text)
{
size_t len;
char *copy;
if (!text)
return NULL;
len = strlen(text);
if (len == SIZE_MAX)
return NULL;
++len;
copy = malloc(len);
if (copy)
memcpy(copy, text, len);
return copy;
}
/*
* Virtual destructor: releases everything snoopy_new() allocated.
*
* The library calls the destructor belonging to the object's runtime type and
* stops there, so this one owns the whole hierarchy. Three frees where Dog
* needed two: `imagination` is Snoopy's, `breed` came from dog_init() and
* `name` from animal_init() before that, and neither Dog's destroy() nor
* Animal's ever runs for a Snoopy.
*
* The branch makes no difference to this. A destructor walks the struct
* downwards through whatever it was given, and the only thing that has to be
* right is that it frees each owned allocation exactly once.
*
* All three fields are marked as owned in their own class' table, so ooc_set()
* has already released a previous value where there was one and what is left is
* freed here.
*/
static void destroy(ooc_object *object)
{
Snoopy *snoopy = (Snoopy *)object;
free(snoopy->imagination);
free(snoopy->dog.breed);
free(snoopy->dog.animal.name);
}
/*
* Snoopy's implementation of AnimalVTable::speak.
*
* The signature is Animal's, not Snoopy's, and the cast back down is the same
* no-op at every level, since Animal sits at offset zero however many classes
* are embedded above it.
*
* `__flights` is read directly here, from the struct definition where the member
* is in view. That is the point of a two-underscore name: it keeps callers out
* through the name-based API while leaving the class itself free to use the
* field however it likes. An accessor is what a caller would be given instead.
*
* The fallbacks keep a cleared string from reaching printf(), since a field set
* to NULL is a legitimate state and a vtable entry is called with whatever the
* object currently holds.
*/
static void speak(Animal *animal)
{
Snoopy *snoopy = (Snoopy *)animal;
printf("%s says: Woof! (%s, dreaming of %s, %d flights so far)\n",
snoopy->dog.animal.name ? snoopy->dog.animal.name : "(unnamed)",
snoopy->dog.breed ? snoopy->dog.breed : "(unknown breed)",
snoopy->imagination ? snoopy->imagination : "(nothing much)",
snoopy->__flights);
}
/*
* Snoopy's own vtable, holding Snoopy's speak.
*
* Identical in shape to the tables in animal.c, dog.c and cat.c. Snoopy and Dog
* both declare `speak` and neither can tell the other about it: the slot is
* chosen by the class that owns the table, and a Snoopy reaches this one.
*/
static const AnimalVTable vt = {
.speak = speak,
};
/*
* The fields Snoopy adds on top of the ones above it.
*
* A derived class lists only what it declares itself. Everything else -- "breed"
* from Dog, then "name", "age", "_id" and "__legs" from Animal -- resolves by
* walking up from Snoopy_class through Dog_class to Animal_class.
*
* `imagination` is marked as owned, exactly as Dog's breed is, so replacing it
* through ooc_set() releases the string it held before. `__flights` carries two
* underscores, so ooc_get() withholds it as well as ooc_set() refusing to write
* it, leaving snoopy_flights() as the only route to the value.
*
* The branch is what stops a name leaking sideways: a Snoopy has no "colour"
* and a Garfield has no "imagination", because neither chain contains the class
* that would declare it.
*/
static const ooc_field Snoopy_fields[] = {
{ "imagination", offsetof(Snoopy, imagination),
sizeof(((Snoopy *)0)->imagination), 1 },
{ "__flights", offsetof(Snoopy, __flights),
sizeof(((Snoopy *)0)->__flights), 0 },
{ NULL, 0, 0, 0 },
};
/*
* Snoopy's runtime type record.
*
* `super` is Dog_class, and that pointer is what puts Snoopy on the Dog branch:
* ooc_new() writes this record's address into every object it allocates, and
* field lookup, ooc_set() and ooc_is_a() all start there and walk outwards,
* reaching Dog's and Animal's fields through it.
*
* `size` is sizeof(Snoopy), which must hold Dog and Snoopy's own members. The
* runtime rejects a class whose base is larger, since the base's fields would
* then be written past the end of the allocation.
*
* The record is a file-scope constant, as it is for every class.
*/
const ooc_class Snoopy_class = {
.size = sizeof(Snoopy),
.destroy = destroy,
.super = &Dog_class,
.fields = Snoopy_fields,
};
/*
* Initialise the members Snoopy owns, leaving the object ready to speak as a
* Snoopy.
*
* The third link in the chain of constructors, composed the same way
* garfield_init() is: dog_init() builds the Dog part, which calls animal_init()
* for the Animal part, and Snoopy overwrites the vtable afterwards since
* dog_init() installs Dog's table.
*
* The imagination is copied first, so a failure here cannot leave a partly built
* object behind and the caller may release the zeroed storage without the
* destructor having anything to do.
*
* `__flights` starts at zero and is written here and nowhere else, which is what
* makes the two-underscore rule meaningful: the class decides when it changes,
* and no caller can reach it by name to change it sooner.
*
* Returns 0, or -1 for a NULL object, an already initialised one, or a failed
* copy. Requires zero-initialised members and external synchronization between
* constructors in different threads.
*/
int snoopy_init(Snoopy *snoopy, const char *name, int age, const char *breed,
const char *imagination)
{
char *copy;
if (!snoopy || snoopy->imagination || snoopy->dog.breed ||
snoopy->dog.animal.vtable)
return -1;
copy = dupstr(imagination);
if (!copy)
return -1;
/*
* Let Dog build the part it owns -- which lets Animal build the part it
* owns -- then take over the vtable with Snoopy's own.
*/
if (dog_init(&snoopy->dog, name, age, breed) != 0) {
free(copy);
return -1;
}
snoopy->imagination = copy;
snoopy->dog.animal.vtable = &vt;
return 0;
}
/*
* Create a Snoopy named `name`, of the given `breed` and `imagination`, and
* return it, or NULL.
*
* Two steps, as in dog_new() and cat_new(): allocate through ooc_new() with a
* reference count of one, so the caller owns the result and must release it, and
* then let snoopy_init() build the members. The allocation carries Snoopy's
* destructor from the start, which frees the whole hierarchy, so a failed init
* releases cleanly -- on every failure path above, nothing was written and
* free(NULL) is what the destructor finds.
*
* Returns NULL if the allocation fails or snoopy_init() refuses, in both cases
* leaving nothing to release.
*/
Snoopy *snoopy_new(const char *name, int age, const char *breed,
const char *imagination)
{
Snoopy *snoopy = ooc_new(&Snoopy_class);
if (!snoopy)
return NULL;
if (snoopy_init(snoopy, name, age, breed, imagination) != 0) {
ooc_release(snoopy);
return NULL;
}
return snoopy;
}
/*
* Read back `__flights`, the field ooc_get() withholds.
*
* The only way to learn the value, since nothing outside the class can reach the
* field by name in either direction. Animal exposes `__legs` the same way, and
* the two differ from a single underscore only in that this one cannot be read
* by name at all.
*
* Takes a const object, so a caller with a const pointer can still be told the
* count.
*
* Returns 0 for a NULL object, which is indistinguishable from a real count of
* zero, so a caller that has to tell the two apart should check the pointer
* first.
*/
int snoopy_flights(const Snoopy *snoopy)
{
return snoopy ? snoopy->__flights : 0;
}