diff --git a/.gitignore b/.gitignore index e8277fd..053d96c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .idea/ build/ +ideas/ pkg/ diff --git a/Makefile b/Makefile index fdb2164..b05287e 100644 --- a/Makefile +++ b/Makefile @@ -39,10 +39,13 @@ FORCE: $(PKGCONFIG): libooc.pc.in FORCE | $(BUILD_DIR); sed -e 's|@prefix@|$(PREFIX)|g' -e 's|@VERSION@|$(VERSION)|g' $< > $@ example: shared - $(CC) $(CPPFLAGS) $(CFLAGS) $(WARNINGS) $(LDFLAGS) -Iinclude -Iexample example/main.c example/animal.c example/dog.c example/cat.c -L$(BUILD_DIR) -Wl,-rpath,'$$ORIGIN' -looc -o $(BUILD_DIR)/example + $(CC) $(CPPFLAGS) $(CFLAGS) $(WARNINGS) $(LDFLAGS) -Iinclude -Iexample example/main.c example/animal.c example/dog.c example/cat.c example/garfield.c example/snoopy.c -L$(BUILD_DIR) -Wl,-rpath,'$$ORIGIN' -looc -o $(BUILD_DIR)/example -$(BUILD_DIR)/safety-test: tests/safety.c example/animal.c example/dog.c example/cat.c example/animal.h example/dog.h example/cat.h $(LIB_STATIC) - $(CC) $(CPPFLAGS) $(CFLAGS) $(WARNINGS) $(LDFLAGS) -Iexample tests/safety.c example/animal.c example/dog.c example/cat.c $(LIB_STATIC) -o $@ +EXAMPLE_SRCS=example/animal.c example/dog.c example/cat.c example/garfield.c example/snoopy.c +EXAMPLE_HDRS=example/animal.h example/dog.h example/cat.h example/garfield.h example/snoopy.h + +$(BUILD_DIR)/safety-test: tests/safety.c $(EXAMPLE_SRCS) $(EXAMPLE_HDRS) $(LIB_STATIC) + $(CC) $(CPPFLAGS) $(CFLAGS) $(WARNINGS) $(LDFLAGS) -Iexample tests/safety.c $(EXAMPLE_SRCS) $(LIB_STATIC) -o $@ example-test: example LD_LIBRARY_PATH=$(BUILD_DIR) $(BUILD_DIR)/example diff --git a/README.md b/README.md index 823574b..dca43f7 100644 --- a/README.md +++ b/README.md @@ -45,17 +45,69 @@ If installed under `/usr/local` and your pkg-config does not search there: ## Example -The `example/` directory contains an `Animal` base class and two derived classes, `Dog` and `Cat`. Both override the virtual `speak` method, and `example/main.c` walks each of them through the same calls, so the output shows one call site reaching two different implementations. Objects are reference-counted with `ooc_retain()` and `ooc_release()`. +The `example/` directory holds a small class hierarchy. `Animal` is the root, +`Dog` and `Cat` derive from it, and `Garfield` and `Snoopy` derive from those: + + Animal + |-- Dog + | `-- Snoopy + `-- Cat + `-- Garfield + +Every class overrides the virtual `speak` method, and `example/main.c` walks each +one through the same calls before putting all four objects through a single +`Animal *`. The output makes the point that is invisible in the source: one call +site reaches four implementations, and the vtable decides which. make test # runs example/main.c make safety-test # runs the assertion suite in tests/ +Objects are reference-counted with `ooc_retain()` and `ooc_release()`. + The public installed header is: #include Application-specific classes such as `Animal`, `Dog` and `Cat` are not installed by the library. +### Constructors down a chain + +Each class publishes an `init` function alongside its constructor, so a subclass +builds the classes above it by calling them rather than repeating what they do: + +```c +int garfield_init(Garfield *garfield, const char *name, int age, + const char *colour, const char *favourite_food); +``` + +`garfield_init()` calls `cat_init()`, which calls `animal_init()`. Each one +installs its own vtable, and the one above overwrites it, so the object ends up +speaking as the most derived class. A subclass of `Garfield` would call +`garfield_init()` in turn, and the composition extends without anything else +changing. + +### Destructors down a chain + +The library calls the destructor belonging to the object's runtime type and stops +there. A derived destructor is therefore responsible for the whole hierarchy, not +only for its own members, and the frees grow with the depth: + +```c +static void destroy(ooc_object *object) +{ + Garfield *garfield = (Garfield *)object; + + free(garfield->favourite_food); /* Garfield's */ + free(garfield->cat.colour); /* Cat's, allocated by cat_init() */ + free(garfield->cat.animal.name); /* Animal's, allocated by animal_init() */ +} +``` + +Cat's own destructor never runs for a Garfield, and Animal's never runs for +either. Omitting a middle free is silent -- the object works perfectly well and +only a memory checker reports the leak, which is why `make safety-test` is worth +running under Valgrind or AddressSanitizer. + ## Fields by name A class publishes the fields that may be reached dynamically as a NULL-terminated @@ -91,6 +143,17 @@ chain: a `Dog` resolves `name` and `age` through `Animal_class`, and a field declared by a subclass shadows a same-named field of its base. Upcasting a pointer does not change this -- the runtime type of the object decides. +Lookup walks as far as the chain goes, and a class inserted in the middle hides +nothing. A `Garfield` resolves its own `favourite_food`, Cat's `colour` and +Animal's `name`, while `imagination` -- declared on the other branch -- resolves +to nothing at all: + +```c +ooc_get(garfield, "colour"); /* Cat's */ +ooc_get(garfield, "name"); /* Animal's, two records up */ +ooc_get(garfield, "imagination"); /* NULL -- Snoopy's field, not in this chain */ +``` + `ooc_set()` copies the field's storage with overlap-safe semantics and returns `-1` for an unknown field name or a NULL argument, so a bad name is reported instead of writing out of bounds. @@ -114,6 +177,11 @@ not be shared with a second field, or the same allocation would be freed twice. Assigning a field the pointer it already holds frees nothing. The destructor still frees whatever the field holds when the object is released. +The rule is the library's rather than the class', so it applies to an inherited +field exactly as to one declared alongside it. Setting a Garfield's inherited +`colour` releases the string Cat's constructor allocated, the same as setting its +own `favourite_food` does. + ## Type checks `ooc_is_a()` walks the inheritance chain from the object's runtime type, so it @@ -125,6 +193,15 @@ ooc_is_a(cat, &Animal_class); /* 1 -- a Cat is an Animal */ ooc_is_a(cat, &Dog_class); /* 0 -- siblings are unrelated */ ``` +The walk covers the whole chain, so depth costs nothing to the caller: + +```c +ooc_is_a(garfield, &Garfield_class); /* 1 */ +ooc_is_a(garfield, &Cat_class); /* 1 -- Garfield -> Cat -> Animal */ +ooc_is_a(garfield, &Animal_class); /* 1 */ +ooc_is_a(garfield, &Snoopy_class); /* 0 -- the other branch */ +``` + This is the useful direction to ask in, because an upcast in C is unchecked: a caller holding an `Animal *` uses it to find out what the object really is. @@ -193,6 +270,20 @@ subclass cannot reach a base class' private field either. A class writes its own fields through the struct definition, where the members are in view and no accessor is needed. +The rules hold at any depth. `Garfield` has a `_meals` of its own while +inheriting Cat's `_lives`, and a caller can read both by name but write neither -- +the rule belongs to the declaration, not to the object: + +```c +ooc_get(garfield, "_meals"); /* readable -- Garfield's */ +ooc_get(garfield, "_lives"); /* readable -- Cat's, two levels up */ +ooc_set(garfield, "_meals", &n); /* refused */ +ooc_set(garfield, "_lives", &n); /* also refused */ +``` + +Private means private to the declaring class, which is why a subclass adds a +field of its own rather than leaning on the base class' private state. + Two things this is not. It does not keep a determined caller from reading a single-underscore field, since `ooc_get()` hands back the field's real storage and C has no access control -- the name is a contract, not a wall. And it does @@ -204,7 +295,8 @@ pointer. include/ooc/ooc.h Public API src/ooc.c Library implementation - example/ Complete example + example/ Complete example: Animal, Dog, Cat, Garfield, Snoopy + tests/safety.c Assertion suite Makefile Build/install rules libooc.pc.in pkg-config template diff --git a/example/cat.c b/example/cat.c index 69b08a2..63f4873 100644 --- a/example/cat.c +++ b/example/cat.c @@ -164,27 +164,64 @@ const ooc_class Cat_class = { }; /* - * Create a Cat named `name` of the given `colour` and return it, or NULL. + * Initialise the members Cat owns, leaving the object ready to speak as a Cat. * - * Three steps, in the order a subclass constructor needs them. The object is - * allocated through ooc_new() with a reference count of one, so the caller owns - * it and must release it. animal_init() fills in the base part, including the - * `_id` and `__legs` that only Animal may write. Then the vtable is replaced - * with Cat's, so the object answers to Cat's speak rather than Animal's. + * Split out of cat_new() so a subclass of Cat can build this part itself, the + * same reason animal_init() exists for Animal. Garfield does exactly that: it + * calls this, then installs its own vtable and adds its own members. * * The vtable is overwritten after animal_init() rather than before, since that * call installs Animal's table and this one has to win. * * `_lives` is set here and nowhere else. A caller can read it with ooc_get() and - * cannot write it with ooc_set(), so this constructor and any future method of - * Cat are the only places the value can change -- which is the point of marking - * it with an underscore. + * cannot write it with ooc_set(), so this function and any method of Cat are the + * only places the value can change -- which is the point of marking it with an + * underscore. * - * Each step can fail, and both failures release the object, which is safe - * because the destructor is already registered and frees whatever is present: - * after a failed animal_init() there is no name and no colour to free, and after - * a failed colour copy the name has to be freed, which is exactly what the - * destructor does. Returns NULL if any step fails, leaving nothing to release. + * The colour is copied before the base part is built, so a failed copy cannot + * leave a half-initialised object behind: on failure nothing has been written + * at all and the caller may simply release the zeroed storage. Failure leaves + * the cat unchanged. + * + * Returns 0, or -1 for a NULL cat, an already initialised one, or a failed copy. + * Requires zero-initialised members and external synchronization between + * constructors in different threads. + */ +int cat_init(Cat *cat, const char *name, int age, const char *colour) +{ + char *copy; + + if (!cat || cat->colour || cat->animal.vtable) + return -1; + + copy = dupstr(colour); + + if (!copy) + return -1; + + if (animal_init(&cat->animal, name, age) != 0) { + free(copy); + return -1; + } + + cat->colour = copy; + cat->animal.vtable = &vt; + cat->_lives = 9; + + return 0; +} + +/* + * Create a Cat named `name` of the given `colour` and return it, or NULL. + * + * Two steps: allocate through ooc_new() with a reference count of one, so the + * caller owns the result and must release it, and then let cat_init() build the + * members. The allocation carries Cat's destructor from the start, so a failed + * cat_init() releases cleanly: nothing was written, and free(NULL) is what the + * destructor finds. + * + * Returns NULL if the allocation fails or cat_init() refuses, in both cases + * leaving nothing to release. */ Cat *cat_new(const char *name, int age, const char *colour) { @@ -193,20 +230,7 @@ Cat *cat_new(const char *name, int age, const char *colour) if (!cat) return NULL; - /* - * Let Animal build the part it owns, including the private `_id`, then - * take over the vtable with Cat's own. - */ - if (animal_init(&cat->animal, name, age) != 0) { - ooc_release(cat); - return NULL; - } - - cat->animal.vtable = &vt; - cat->colour = dupstr(colour); - cat->_lives = 9; - - if (!cat->colour) { + if (cat_init(cat, name, age, colour) != 0) { ooc_release(cat); return NULL; } diff --git a/example/cat.h b/example/cat.h index c662cb3..e24641f 100644 --- a/example/cat.h +++ b/example/cat.h @@ -64,4 +64,22 @@ extern const ooc_class Cat_class; */ Cat *cat_new(const char *name, int age, const char *colour); +/* + * Initialise the members Cat owns, so a subclass constructor can build the Cat + * part of a larger object instead of duplicating it. + * + * This is the second link in the chain of constructors below Animal: a subclass + * of Cat calls this, gets Cat's vtable installed, and then overwrites the + * vtable with its own the same way cat_new() does. + * + * The caller supplies storage that ooc_new() has already allocated and + * zeroed. On failure the object is left exactly as it was found, so the caller + * may release it without a destructor having anything to do. + * + * Returns 0, or -1 for a NULL cat, an already initialised one, or a failed copy + * of the colour. Requires zero-initialised members and external synchronization + * between constructors in different threads. + */ +int cat_init(Cat *cat, const char *name, int age, const char *colour); + #endif /* CAT_H */ diff --git a/example/dog.c b/example/dog.c index f33fbcb..5bebcb5 100644 --- a/example/dog.c +++ b/example/dog.c @@ -147,22 +147,58 @@ const ooc_class Dog_class = { }; /* - * Create a Dog named `name` of the given `breed` and return it, or NULL. + * Initialise the members Dog owns, leaving the object ready to speak as a Dog. * - * Three steps, in the order a subclass constructor needs them. The object is - * allocated through ooc_new() with a reference count of one, so the caller owns - * it and must release it. animal_init() fills in the base part, including the - * `_id` and `__legs` that only Animal may write. Then the vtable is replaced - * with Dog's, so the object answers to Dog's speak rather than Animal's. + * Split out of dog_new() so a subclass of Dog can build this part itself, the + * same reason animal_init() exists for Animal. Snoopy does exactly that: it + * calls this, then installs its own vtable and adds its own members. * * The vtable is overwritten after animal_init() rather than before, since that * call installs Animal's table and this one has to win. * - * Each step can fail, and both failures release the object, which is safe - * because the destructor is already registered and frees whatever is present: - * after a failed animal_init() there is no name and no breed to free, and after - * a failed breed copy the name has to be freed, which is exactly what the - * destructor does. Returns NULL if any step fails, leaving nothing to release. + * The breed is copied before the base part is built, so a failed copy cannot + * leave a half-initialised object behind: on failure nothing has been written + * at all and the caller may simply release the zeroed storage. Failure leaves + * the dog unchanged. + * + * Returns 0, or -1 for a NULL dog, an already initialised one, or a failed copy. + * Requires zero-initialised members and external synchronization between + * constructors in different threads. + */ +int dog_init(Dog *dog, const char *name, int age, const char *breed) +{ + char *copy; + + if (!dog || dog->breed || dog->animal.vtable) + return -1; + + copy = dupstr(breed); + + if (!copy) + return -1; + + if (animal_init(&dog->animal, name, age) != 0) { + free(copy); + return -1; + } + + dog->breed = copy; + dog->animal.vtable = &vt; + + return 0; +} + +/* + * Create a Dog named `name` of the given `breed` and return it, or NULL. + * + * Two steps: allocate through ooc_new() with a reference count of one, so the + * caller owns the result and must release it, and then let dog_init() build the + * members. The allocation carries Dog's destructor from the start, so a failed + * dog_init() releases cleanly: nothing was written, and free(NULL) is what the + * destructor finds. + * + * Returns NULL if the allocation fails or dog_init() refuses, in both cases + * leaving nothing to release. */ Dog *dog_new(const char *name, int age, const char *breed) { @@ -171,19 +207,7 @@ Dog *dog_new(const char *name, int age, const char *breed) if (!dog) return NULL; - /* - * Let Animal build the part it owns, including the private `_id`, then - * take over the vtable with Dog's own. - */ - if (animal_init(&dog->animal, name, age) != 0) { - ooc_release(dog); - return NULL; - } - - dog->animal.vtable = &vt; - dog->breed = dupstr(breed); - - if (!dog->breed) { + if (dog_init(dog, name, age, breed) != 0) { ooc_release(dog); return NULL; } diff --git a/example/dog.h b/example/dog.h index 29bc2be..551102b 100644 --- a/example/dog.h +++ b/example/dog.h @@ -54,4 +54,22 @@ extern const ooc_class Dog_class; */ Dog *dog_new(const char *name, int age, const char *breed); +/* + * Initialise the members Dog owns, so a subclass constructor can build the Dog + * part of a larger object instead of duplicating it. + * + * This is the second link in the chain of constructors below Animal: a subclass + * of Dog calls this, gets Dog's vtable installed, and then overwrites the + * vtable with its own the same way dog_new() does. + * + * The caller supplies storage that ooc_new() has already allocated and + * zeroed. On failure the object is left exactly as it was found, so the caller + * may release it without a destructor having anything to do. + * + * Returns 0, or -1 for a NULL dog, an already initialised one, or a failed copy + * of the breed. Requires zero-initialised members and external synchronization + * between constructors in different threads. + */ +int dog_init(Dog *dog, const char *name, int age, const char *breed); + #endif /* DOG_H */ diff --git a/example/garfield.c b/example/garfield.c new file mode 100644 index 0000000..074aa28 --- /dev/null +++ b/example/garfield.c @@ -0,0 +1,281 @@ +/* + * This file is part of libooc. + * https://xw3.org/hanez/libooc + * + * Copyright 2026 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + */ + +/* + * Garfield -- a subclass of Cat, and so a subclass of a subclass. + * + * Everything a first-level subclass does, this file does again one level down: + * the object is allocated from Garfield_class, whose `super` is Cat_class so + * field lookup continues through Cat to Animal; the members of the classes above + * are built by cat_init() rather than duplicated; and a separate vtable and + * destructor are registered for Garfield, so the override and the extra + * allocation are cleaned up by the object's runtime type. + * + * Adding a level turns out to change none of the rules, which is the point worth + * making with an example. Ownership, the underscore conventions, the virtual + * destructor and the chain walk all behave identically whether the base is two + * levels up or one. What does change is the destructor, and that is the part + * worth reading closely: it now frees three strings instead of two, and forgets + * the middle one at its peril. + */ + +#include "garfield.h" + +#include +#include +#include +#include +#include + +/* + * Copy `text` onto the heap; the caller owns the result. + * + * A private copy of the same helper animal.c, cat.c and dog.c each keep, and + * identical to all of them. By now the duplication is the point being made + * rather than a detail worth hiding: four identical copies is where a real + * codebase would stop and either publish the helper or leave it to the class + * that owns it. The library is too small to have an opinion, and an example is + * clearer for showing what each class is actually responsible for. + * + * 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 garfield_new() allocated. + * + * The library calls the destructor belonging to the object's runtime type and + * stops there, so this one owns the whole hierarchy and not merely Garfield's + * own member. Three frees are needed where Cat needed two: `favourite_food` is + * Garfield's, `colour` was allocated by cat_init() and `name` by animal_init() + * before that, and neither Cat's destroy() nor Animal's ever runs for a + * Garfield. + * + * Omitting the middle free is the mistake a third level invites, and it is + * silent: the object still looks fine, and the leak only shows up in a memory + * checker. Forgetting `name` on the way down would have the same effect one + * level earlier. + * + * The order does not matter, since the three allocations are independent, and + * 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 remains + * here is freed on destruction. + */ +static void destroy(ooc_object *object) +{ + Garfield *garfield = (Garfield *)object; + + free(garfield->favourite_food); + free(garfield->cat.colour); + free(garfield->cat.animal.name); +} + +/* + * Garfield's implementation of AnimalVTable::speak. + * + * The signature is Animal's, not Garfield'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. That is what makes a deep hierarchy cheap: no + * adjustment, no pointer arithmetic, and the cast is valid because the object + * really was a Garfield. + * + * The members come from three different classes in one printf() -- the name from + * Animal, the colour and the lives count from Cat, the food and the meals from + * Garfield -- which is the clearest statement of what inheritance bought: an + * override with access to the whole hierarchy behind it. + * + * The fallbacks keep a cleared string from reaching printf(). A field set to + * NULL is a legitimate state, ooc_set(garfield, "favourite_food", NULL) being + * allowed to succeed, and a vtable entry is called with whatever the object + * currently holds. + */ +static void speak(Animal *animal) +{ + Garfield *garfield = (Garfield *)animal; + + printf("%s says: Meow! (%s, %s, meal %d, %d lives left)\n", + garfield->cat.animal.name ? garfield->cat.animal.name : "(unnamed)", + garfield->cat.colour ? garfield->cat.colour : "(unknown colour)", + garfield->favourite_food ? garfield->favourite_food + : "(no favourite food)", + garfield->_meals, + garfield->cat._lives); +} + +/* + * Garfield's own vtable, holding Garfield's speak. + * + * Identical in shape to the tables in animal.c, cat.c and dog.c. A vtable slot + * is chosen by the class that owns the table, so a Garfield reaches this speak + * and never Cat's, even though Cat declared the same method -- the override + * holds all the way down. + */ +static const AnimalVTable vt = { + .speak = speak, +}; + +/* + * The one field Garfield adds on top of the ones above it. + * + * A derived class lists only what it declares itself. Everything else -- + * "colour" and "_lives" from Cat, "name", "age", "_id" and "__legs" from Animal + * -- resolves by walking up from Garfield_class through Cat_class to + * Animal_class. Inserting a class in the middle hides nothing. + * + * `favourite_food` is marked as owned, exactly as Cat's colour and Dog's breed + * are, so replacing it through ooc_set() releases the string it held before. + * `_meals` carries a leading underscore, so ooc_set() refuses to write it while + * ooc_get() still returns it, and the rule applies to a field this class + * declared rather than one it inherited. + */ +static const ooc_field Garfield_fields[] = { + { "favourite_food", offsetof(Garfield, favourite_food), + sizeof(((Garfield *)0)->favourite_food), 1 }, + { "_meals", offsetof(Garfield, _meals), + sizeof(((Garfield *)0)->_meals), 0 }, + { NULL, 0, 0, 0 }, +}; + +/* + * Garfield's runtime type record. + * + * `super` is Cat_class, and that single pointer is what makes this a subclass of + * a subclass: ooc_new() writes this record's address into every object it + * allocates, and ooc_get(), ooc_set() and ooc_is_a() all start from there and + * walk outwards, reaching Cat's and Animal's fields through it. + * + * `size` is sizeof(Garfield), which must hold Cat and the food on top. 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 Garfield_class = { + .size = sizeof(Garfield), + .destroy = destroy, + .super = &Cat_class, + .fields = Garfield_fields, +}; + +/* + * Initialise the members Garfield owns, leaving the object ready to speak as a + * Garfield. + * + * The third link in the chain of constructors, and it is a strict composition + * of the two below it. cat_init() builds the Cat part -- which calls + * animal_init() for the Animal part -- and Garfield overwrites the vtable + * afterwards, since cat_init() installs Cat's table and Garfield's has to win. + * + * The food is copied first, for the same reason cat_init() copies the colour + * before calling animal_init(): a failure here cannot leave a partly built + * object behind, so the caller may release the zeroed storage without the + * destructor having anything to do. + * + * `_meals` is set here and nowhere else, so it changes only through this + * constructor or a future method of Garfield. A caller can read it by name and + * cannot write it, which is the contract the underscore asks for. + * + * 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 garfield_init(Garfield *garfield, const char *name, int age, + const char *colour, const char *favourite_food) +{ + char *copy; + + if (!garfield || garfield->favourite_food || garfield->cat.colour || + garfield->cat.animal.vtable) + return -1; + + copy = dupstr(favourite_food); + + if (!copy) + return -1; + + /* + * Let Cat build the part it owns -- which lets Animal build the part it + * owns -- then take over the vtable with Garfield's own. + */ + if (cat_init(&garfield->cat, name, age, colour) != 0) { + free(copy); + return -1; + } + + garfield->favourite_food = copy; + garfield->cat.animal.vtable = &vt; + garfield->_meals = 1; + + return 0; +} + +/* + * Create a Garfield named `name`, of the given `colour` and `favourite_food`, + * and return it, or NULL. + * + * Two steps, as in cat_new() and dog_new(): allocate through ooc_new() with a + * reference count of one, so the caller owns the result and must release it, + * and then let garfield_init() build the members. The allocation carries + * Garfield'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 garfield_init() refuses, in both cases + * leaving nothing to release. + */ +Garfield *garfield_new(const char *name, int age, const char *colour, + const char *favourite_food) +{ + Garfield *garfield = ooc_new(&Garfield_class); + + if (!garfield) + return NULL; + + if (garfield_init(garfield, name, age, colour, favourite_food) != 0) { + ooc_release(garfield); + return NULL; + } + + return garfield; +} + +/* + * Read back `_meals`, the field ooc_get() also returns. + * + * Takes a const object, so it can be called where ooc_set() could not -- 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. The count starts at 1. + */ +int garfield_meals(const Garfield *garfield) +{ + return garfield ? garfield->_meals : 0; +} \ No newline at end of file diff --git a/example/garfield.h b/example/garfield.h new file mode 100644 index 0000000..32a2ddc --- /dev/null +++ b/example/garfield.h @@ -0,0 +1,122 @@ +/* + * This file is part of libooc. + * https://xw3.org/hanez/libooc + * + * Copyright 2026 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + */ + +#ifndef GARFIELD_H +#define GARFIELD_H + +#include "cat.h" + +typedef struct Garfield Garfield; + +/* + * A subclass of a subclass, three levels below the root. + * + * The embedded Cat comes first, so the same two properties hold one level down + * from the case dog.h describes: the ooc_object header stays at offset zero + * where the library looks for it, and the upcast from Garfield * to Animal * is + * a no-op cast because every address is the same. The cost is unchanged too -- + * no member may go before the base. + * + * Garfield adds a favourite food of its own and nothing that shadows Cat or + * Animal, so every inherited field keeps the meaning it had. "colour" and + * "_lives" still resolve to Cat's storage, and the underscore rules still apply + * to the class that declared them: a caller can read "_lives" by name and cannot + * write it, even though Garfield sits between the caller and the declaration. + * + * The struct holds no destructor of its own; the one Garfield registers lives in + * Garfield_class and releases all three strings in the hierarchy -- the food, + * Cat's colour and Animal's name. + */ +struct Garfield { + Cat cat; + + /* + * Garfield's own field, and the only one it adds. Marked as owned in the + * field table, so ooc_set() releases the string it displaces exactly as it + * does for a Dog's breed or a Cat's colour. A third level of subclassing + * changes nothing about how ownership behaves. + */ + char *favourite_food; + + /* + * A field nobody but this class may change, counted in meals rather than + * lives. One underscore hides it from ooc_set() and leaves it readable, and + * garfield_meals() is the way a caller asks politely. + * + * Worth noting that `Cat` has a `_lives` of its own and Garfield adds a + * second private field rather than reusing it. Private means private to the + * declaring class, so a subclass cannot lean on the base class' private + * state even from inside the same hierarchy -- which is why the two fields + * coexist under different names. + */ + int _meals; +}; + +/* + * Garfield's runtime type record, the value ooc_new() takes. + * + * Its `super` is Cat_class rather than Animal_class, which is the whole of a + * third level: field lookup starts here, continues at Cat_class for "colour" and + * "_lives", and reaches Animal_class for "name", "age", "_id" and "__legs". The + * chain is walked as far as it goes, so nothing from an ancestor is lost by + * inserting a class in between. + * + * Its `size` is sizeof(Garfield), which has to hold Cat and Garfield's own + * member on top, and the runtime rejects a class smaller than its base. + */ +extern const ooc_class Garfield_class; + +/* + * Create a Garfield named `name`, of the given `colour` and `favourite_food`, + * and return it, or NULL. + * + * The result has a reference count of one, so ownership is the caller's and it + * must reach ooc_release() eventually. Returns NULL if any step of the + * construction fails, leaving nothing to release. + * + * The object is a Garfield and answers to Garfield's `speak` however it is later + * reached -- through a Garfield *, a Cat * or an Animal * alike. + */ +Garfield *garfield_new(const char *name, int age, const char *colour, + const char *favourite_food); + +/* + * Initialise the members Garfield owns, so a subclass of Garfield could build + * this part for itself. + * + * The third link in the chain of constructors: garfield_init() composes + * cat_init(), which composes animal_init(). Each one installs its own vtable and + * the one above overwrites it, so the object ends up speaking as the most + * derived class. + * + * The caller supplies storage that ooc_new() has already allocated and zeroed. + * On failure the object is left exactly as it was found, so the caller may + * release it without a destructor having anything to do. + * + * 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 garfield_init(Garfield *garfield, const char *name, int age, + const char *colour, const char *favourite_food); + +/* + * Read back `_meals`, the field ooc_get() also returns. + * + * The accessor a class provides for a value it keeps to itself. A single + * underscore leaves the field readable by name, so this is a courtesy rather + * than a necessity, unlike animal_legs() for the two-underscore `__legs`. + * + * 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 garfield_meals(const Garfield *garfield); + +#endif /* GARFIELD_H */ \ No newline at end of file diff --git a/example/main.c b/example/main.c index 406fa9d..cc708c6 100644 --- a/example/main.c +++ b/example/main.c @@ -8,7 +8,8 @@ */ /* - * Walks through the libooc life cycle twice over, once per subclass. + * Walks through the libooc life cycle four times, once per class in the + * hierarchy. * * The Dog walk covers the whole API: allocation, a second reference through a * base class, reading and writing fields by name, the private field rules, and @@ -16,10 +17,18 @@ * second subclass, so the output makes the one thing that is not visible in the * source clear: `animal_speak` is a single call site and still reaches Dog's * implementation for one object and Cat's for the other. + * + * Garfield and Snoopy go one level deeper, each on its own branch, and they are + * what turn a list of classes into a hierarchy worth looking at. The last walk + * puts all four objects through one Animal * and asks what each really is, which + * is the question a caller with no static type information can only answer by + * asking. */ #include "cat.h" #include "dog.h" +#include "garfield.h" +#include "snoopy.h" #include #include @@ -27,15 +36,32 @@ #include #include -/* Copy `text` onto the heap, because a field that owns its value needs heap. */ +/* + * Copy `text` onto the heap, because a field that owns its value needs heap. + * + * The example classes have their own private copies of this; it is duplicated + * here rather than shared because main.c is a caller, not part of the hierarchy, + * and has no business reaching into a class' internals for it. + * + * Returns NULL if `text` is NULL or the allocation fails. + */ static char *copy_of(const char *text) { - size_t len = strlen(text); - char *copy = malloc(len + 1); - ++len; + size_t len; + char *copy; + + if (!text) + return NULL; + + len = strlen(text); + + if (len == SIZE_MAX) + return NULL; + + copy = malloc(len + 1); if (copy) - memcpy(copy, text, len); + memcpy(copy, text, len + 1); return copy; } @@ -260,6 +286,302 @@ fail: return -1; } +/* + * The Garfield walk: a subclass of a subclass. + * + * Nothing here is new in kind. The object is created and released the same way + * as a Dog, fields resolve by name the same way, and the ownership and underscore + * rules are the library's rather than the class's. The differences are all + * consequences of the extra level, and they are what the walk is for. + * + * Lookup now crosses two class records instead of one. "favourite_food" and + * "_meals" are Garfield's own, "colour" and "_lives" are found at Cat_class, and + * "name" and "age" are found at Animal_class -- so a name declared two levels up + * is as reachable as one declared immediately above. + * + * The private rules also cross the level unchanged, and that is worth seeing: + * `_meals` belongs to Garfield, `_lives` to Cat, and a caller can read both by + * name but write neither. A subclass sitting between the caller and the + * declaration does not weaken the rule. + * + * Returns 0 when the walk completes and -1 if a step the example relies on was + * refused. + */ +static int visit_garfield(void) +{ + Garfield *garfield; + Cat *cat_view; + Animal *animal; + char *food; + int servings = 7; + + garfield = garfield_new("Garfield", 4, "orange", "lasagna"); + if (!garfield) { + fprintf(stderr, "failed to create garfield\n"); + return -1; + } + + /* + * Upcasting is unchecked in C, so these two casts are just arithmetic that + * happens to be zero: every class in the chain embeds the one above it as + * its first member. A handle to the middle of the chain works exactly like a + * handle to the top or the bottom, which is why the same lookup and the same + * vtable work through any of them. + */ + cat_view = &garfield->cat; + animal = &garfield->cat.animal; + + printf("through a Cat handle, the same object answers as %s.\n", + *(char **)ooc_get(cat_view, "name")); + + printf("%s is a %d year old %s %s who has eaten %d meal%s.\n", + *(char **)ooc_get(garfield, "name"), + *(int *)ooc_get(garfield, "age"), + *(char **)ooc_get(garfield, "colour"), + *(char **)ooc_get(garfield, "favourite_food"), + garfield_meals(garfield), + garfield_meals(garfield) == 1 ? "" : "s"); + + /* + * The owned field is the class's own, so replacing it hands the old string + * over and frees it -- the same rule as a breed or a colour, one level down. + */ + food = copy_of("pizza"); + if (!food || ooc_set(garfield, "favourite_food", &food) != 0) { + fprintf(stderr, "failed to set favourite food\n"); + free(food); + goto fail; + } + + printf("%s now prefers %s.\n", *(char **)ooc_get(garfield, "name"), + *(char **)ooc_get(garfield, "favourite_food")); + + /* Both private fields are readable and neither is writable. */ + if (ooc_set(garfield, "_meals", &servings) != 0) + printf("_meals is private, and reads back as %d\n", + *(int *)ooc_get(garfield, "_meals")); + if (ooc_set(garfield, "_lives", &servings) != 0) + printf("_lives is Cat's, and reads back as %d\n", + *(int *)ooc_get(garfield, "_lives")); + printf("__legs is readable from three levels down: %s\n", + ooc_get(garfield, "__legs") ? "yes" : "no"); + + /* + * The exact type is the deepest class record, reached without a walk, and + * the subtype test walks all three links to say the same thing the long way. + */ + printf("a garfield is exactly a Garfield: %s, a Cat: %s, an Animal: %s\n", + garfield->cat.animal.object.class == &Garfield_class ? "yes" : "no", + ooc_is_a(garfield, &Cat_class) ? "yes" : "no", + ooc_is_a(garfield, &Animal_class) ? "yes" : "no"); + + /* The other branch of the hierarchy is not reachable from here. */ + printf("a garfield is a Snoopy: %s, and has an imagination: %s\n", + ooc_is_a(garfield, &Snoopy_class) ? "yes" : "no", + ooc_get(garfield, "imagination") ? "yes" : "no"); + + /* One call site, and the vtable decides which speak() runs. */ + animal_speak(animal); + + /* + * One reference only, despite the three handles to it: cat_view and animal + * alias the same storage, so releasing any one of them twice would destroy + * the object early. A pointer to a base class is a view, not a new claim. + */ + ooc_release(garfield); + + return 0; + +fail: + ooc_release(garfield); + return -1; +} + +/* + * The Snoopy walk: the other branch, and a hidden field one level down. + * + * The same calls as visit_garfield(), against a Dog subclass rather than a Cat + * one, so the differences are the ones the fork causes. `__flights` carries two + * underscores, which ooc_get() withholds as well as ooc_set() refusing to + * write it, so snoopy_flights() is the only way in -- where Garfield's `_meals` + * is still readable by name. + * + * Returns 0 when the walk completes and -1 if a step the example relies on was + * refused. + */ +static int visit_snoopy(void) +{ + Snoopy *snoopy; + Animal *animal; + char *dream; + int hours = 12; + + snoopy = snoopy_new("Snoopy", 3, "beagle", "flying his red baron"); + if (!snoopy) { + fprintf(stderr, "failed to create snoopy\n"); + return -1; + } + + animal = &snoopy->dog.animal; + + printf("%s is a %d year old %s dreaming of %s, with %d flights.\n", + *(char **)ooc_get(snoopy, "name"), + *(int *)ooc_get(snoopy, "age"), + *(char **)ooc_get(snoopy, "breed"), + *(char **)ooc_get(snoopy, "imagination"), + snoopy_flights(snoopy)); + + /* The owned field again, this time one declared by a Dog subclass. */ + dream = copy_of("a chicken dinner"); + if (!dream || ooc_set(snoopy, "imagination", &dream) != 0) { + fprintf(stderr, "failed to set imagination\n"); + free(dream); + goto fail; + } + + printf("%s is now dreaming of %s.\n", *(char **)ooc_get(snoopy, "name"), + *(char **)ooc_get(snoopy, "imagination")); + + /* + * Two underscores, so there is no way in by name in either direction. The + * class reports the value itself instead, which is the whole point of hiding + * it: the caller is told what it may know and nothing more. + */ + printf("__flights is readable by name: %s, writable: %s\n", + ooc_get(snoopy, "__flights") ? "yes" : "no", + ooc_set(snoopy, "__flights", &hours) == 0 ? "yes" : "no"); + printf("and the class still reports %d flights.\n", + snoopy_flights(snoopy)); + + printf("a snoopy is a Dog: %s, a Cat: %s, a Garfield: %s\n", + ooc_is_a(snoopy, &Dog_class) ? "yes" : "no", + ooc_is_a(snoopy, &Cat_class) ? "yes" : "no", + ooc_is_a(snoopy, &Garfield_class) ? "yes" : "no"); + + animal_speak(animal); + + ooc_release(snoopy); + + return 0; + +fail: + ooc_release(snoopy); + return -1; +} + +/* + * The hierarchy walk: four objects, one pointer type, no static information. + * + * This is what the runtime is for. Each object is created as its own class and + * then handled only as an Animal *, with the pointer's type carrying nothing at + * all about what it points to. The single animal_speak() call below reaches four + * different implementations, and the ooc_is_a() answers come from the objects + * themselves rather than from the call site. + * + * The order is the hierarchy: a root, then a class on each branch, then the two + * subclasses of a subclass. Reading the output top to bottom shows what a + * three-level chain does to a call that knows none of it. + * + * Returns 0 when the walk completes. + */ +static int visit_hierarchy(void) +{ + Animal *rex; + Animal *mia; + Animal *garfield; + Animal *snoopy; + + printf("\n-- one pointer type, four runtime types --\n"); + + /* Each object is created as its own class and only used as an Animal. */ + rex = (Animal *)dog_new("Rex", 5, "German Shepherd"); + mia = (Animal *)cat_new("Mia", 3, "tabby"); + garfield = (Animal *)garfield_new("Garfield", 4, "orange", "lasagna"); + snoopy = (Animal *)snoopy_new("Snoopy", 3, "beagle", "his red baron"); + + if (!rex || !mia || !garfield || !snoopy) { + fprintf(stderr, "failed to create the hierarchy\n"); + + /* Release whatever did get built, since each reference is its own. */ + ooc_release(rex); + ooc_release(mia); + ooc_release(garfield); + ooc_release(snoopy); + + return -1; + } + + /* + * Four calls to one function. Which speak() runs is decided by the vtable + * each object carries, and the pointer type plays no part in it -- the whole + * of the polymorphism is visible right here in the output. + */ + animal_speak(rex); + animal_speak(mia); + animal_speak(garfield); + animal_speak(snoopy); + + /* + * And the runtime type can be recovered from the object alone. This is the + * question a caller with an Animal * and no static information has to ask, + * and the chain walk answers it for a base class as readily as for the exact + * type -- which is the direction that is actually useful. + */ + printf("\n%s is a Dog: %d, a Cat: %d, an Animal: %d\n", + *(char **)ooc_get(rex, "name"), + ooc_is_a(rex, &Dog_class), + ooc_is_a(rex, &Cat_class), + ooc_is_a(rex, &Animal_class)); + + printf("%s is a Dog: %d, a Cat: %d, a Garfield: %d\n", + *(char **)ooc_get(garfield, "name"), + ooc_is_a(garfield, &Dog_class), + ooc_is_a(garfield, &Cat_class), + ooc_is_a(garfield, &Garfield_class)); + + printf("%s is a Snoopy: %d, a Dog: %d, a Cat: %d\n", + *(char **)ooc_get(snoopy, "name"), + ooc_is_a(snoopy, &Snoopy_class), + ooc_is_a(snoopy, &Dog_class), + ooc_is_a(snoopy, &Cat_class)); + + /* + * A field declared on one branch is not reachable from the other. Both + * names are absent from both chains, which is why both calls return NULL and + * why asking for the wrong branch's field is safe rather than corrupting + * memory at a bogus offset. + */ + printf("\n%s has an imagination: %s, and a colour: %s\n", + *(char **)ooc_get(snoopy, "name"), + ooc_get(snoopy, "imagination") ? "yes" : "no", + ooc_get(snoopy, "colour") ? "yes" : "no"); + + printf("%s has a favourite food: %s, and a breed: %s\n", + *(char **)ooc_get(garfield, "name"), + ooc_get(garfield, "favourite_food") ? "yes" : "no", + ooc_get(garfield, "breed") ? "yes" : "no"); + + /* One reference each, so four releases. */ + ooc_release(rex); + ooc_release(mia); + ooc_release(garfield); + ooc_release(snoopy); + + return 0; +} + +/* + * Run the walks in order, stopping at the first failure. + * + * Each walk takes one class through the whole life cycle and returns 0 on + * success or -1 if a step the example relies on was refused, which is enough to + * make a broken expectation visible without aborting. The short-circuit keeps the + * output of a failing run readable: no later walk can be trusted once an earlier + * one has misbehaved. + * + * The exit status is nonzero if any walk failed, so this doubles as the test the + * `test` target runs. Returns 0 when everything succeeded. + */ int main(void) { int status = visit_dog(); @@ -267,5 +589,14 @@ int main(void) if (status == 0) status = visit_cat(); + if (status == 0) + status = visit_garfield(); + + if (status == 0) + status = visit_snoopy(); + + if (status == 0) + status = visit_hierarchy(); + return status != 0; } diff --git a/example/snoopy.c b/example/snoopy.c new file mode 100644 index 0000000..f6319b1 --- /dev/null +++ b/example/snoopy.c @@ -0,0 +1,274 @@ +/* + * This file is part of libooc. + * https://xw3.org/hanez/libooc + * + * Copyright 2026 Johannes Findeisen + * 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 +#include +#include +#include +#include + +/* + * 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; +} \ No newline at end of file diff --git a/example/snoopy.h b/example/snoopy.h new file mode 100644 index 0000000..c77c43b --- /dev/null +++ b/example/snoopy.h @@ -0,0 +1,111 @@ +/* + * This file is part of libooc. + * https://xw3.org/hanez/libooc + * + * Copyright 2026 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + */ + +#ifndef SNOOPY_H +#define SNOOPY_H + +#include "dog.h" + +typedef struct Snoopy Snoopy; + +/* + * A subclass of Dog, and so a subclass of a subclass on the other branch of the + * example hierarchy. + * + * The embedded Dog comes first, which is the same arrangement Garfield uses: the + * ooc_object header stays at offset zero where the library looks for it, and the + * upcast to Animal * is a no-op cast because every address is the same. No member + * may go before the base. + * + * Snoopy adds an imagination and keeps the flying hours to itself. The important + * thing about this branch is not the members but the shape: the hierarchy now + * forks, so Garfield is a Cat and Snoopy is a Dog and neither is anything to do + * with the other beyond their shared Animal. Field lookup walks up and stops at + * the first class that has the name, so "imagination" never resolves on a + * Garfield and "colour" never resolves on a Snoopy. + */ +struct Snoopy { + Dog dog; + + /* + * Snoopy's own field, and the only public one it adds. Marked as owned in + * the field table, so ooc_set() releases the string it displaces exactly as + * it does for a Dog's breed. Nothing about ownership changes with depth. + */ + char *imagination; + + /* + * A private counter, set once and not adjustable from outside. + * + * Two underscores rather than one, which is a difference in degree rather + * than in kind: `__flights` is unreadable as well as unwritable by name, so + * snoopy_flights() is the only way to learn it. Dog has no private field of + * its own and Animal hides `__legs` the same way, so the convention is + * visible at every level of this hierarchy. + */ + int __flights; +}; + +/* + * Snoopy's runtime type record, the value ooc_new() takes. + * + * Its `super` is Dog_class, so lookup continues from there for "breed" and + * onwards to Animal_class for "name", "age", "_id" and "__legs". Its `size` is + * sizeof(Snoopy), which has to hold Dog and Snoopy's own members on top. + */ +extern const ooc_class Snoopy_class; + +/* + * Create a Snoopy named `name`, of the given `breed` and `imagination`, and + * return it, or NULL. + * + * The result has a reference count of one, so ownership is the caller's and it + * must reach ooc_release() eventually. Returns NULL if any step of the + * construction fails, leaving nothing to release. + * + * The object is a Snoopy and answers to Snoopy's `speak` however it is later + * reached -- through a Snoopy *, a Dog * or an Animal * alike. + */ +Snoopy *snoopy_new(const char *name, int age, const char *breed, + const char *imagination); + +/* + * Initialise the members Snoopy owns, so a subclass of Snoopy could build this + * part for itself. + * + * The third link in the chain of constructors: snoopy_init() composes + * dog_init(), which composes animal_init(). Each one installs its own vtable and + * the one above overwrites it, so the object ends up speaking as the most + * derived class. + * + * The caller supplies storage that ooc_new() has already allocated and zeroed. + * On failure the object is left exactly as it was found, so the caller may + * release it without a destructor having anything to do. + * + * 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); + +/* + * Read back `__flights`, the field ooc_get() withholds. + * + * The only route to the value, which is what a two-underscore name asks for: no + * amount of ooc_get() or ooc_set() will reach it, so a caller wanting the count + * has to come through here. Animal exposes `__legs` the same way. + * + * 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. The count starts at 0, since Snoopy has not flown anywhere yet. + */ +int snoopy_flights(const Snoopy *snoopy); + +#endif /* SNOOPY_H */ \ No newline at end of file diff --git a/tests/safety.c b/tests/safety.c index 358c357..aad94f3 100644 --- a/tests/safety.c +++ b/tests/safety.c @@ -12,15 +12,18 @@ * * Checks cover rejected metadata and arguments, overlapping field assignments, * owned-pointer replacement, reference-count limits, the example constructors, - * and two subclasses of one base driven through a single call site. Assertions - * must be enabled: NDEBUG removes checks and API calls inside assert(), leaving - * an incomplete test run. Use AddressSanitizer or Valgrind as well to detect + * two subclasses of one base driven through a single call site, and a + * three-level hierarchy whose branches do not see each other. Assertions must be + * enabled: NDEBUG removes checks and API calls inside assert(), leaving an + * incomplete test run. Use AddressSanitizer or Valgrind as well to detect * invalid memory accesses, invalid frees, and leaks. */ #include #include "cat.h" #include "dog.h" +#include "garfield.h" +#include "snoopy.h" #include #include @@ -87,6 +90,12 @@ static void check_fields(void) { "bad_owned", offsetof(struct sample, bytes), 1, 1 }, { NULL, 0, 0, 0 } }; + /* + * The sample class, sized for the struct and with no base. `destroy` is the + * counting destructor above, so the release at the end of this function can + * be told apart from a no-op, and the field table is the local one that mixes + * valid descriptors with invalid ones. + */ const ooc_class class = { sizeof(struct sample), destroy, NULL, fields }; const char *invalid[] = { "header", "past", "wrap", "huge", "empty", "bad_owned", "absent" }; struct sample *self = ooc_new(&class); @@ -280,11 +289,152 @@ static void check_subclasses(void) } /* - * Run all four groups of checks with assertions enabled. A successful run - * returns 0 and prints the NULL-safe speak output of both example animals. A - * failed assertion aborts instead of returning normally; a memory checker may - * report additional failures. Compiling with NDEBUG disables the assertion - * checks. + * Check a three-level hierarchy, with a subclass of a subclass on each branch. + * + * The point of the depth is that lookup crosses two class records, so "colour" + * is found at Cat_class and "name" at Animal_class, and neither is shadowed or + * lost by Garfield sitting in between. A field declared on the other branch must + * not resolve at all: an absent name is a NULL from ooc_get() and -1 from + * ooc_set(), which is the safe answer rather than a write at a wrong offset. + * + * The underscore rules have to hold across the extra level too. `_meals` and + * `_lives` are both readable and both refuse writes, each through the class that + * declared it, while `__flights` and `__legs` are unreadable as well and only the + * accessors reach them. + * + * Replacing an owned field three levels down has to release the string that was + * there before, and the destructor then has three allocations to free rather than + * one. Only a memory checker can see whether the middle one was freed: a missing + * free at this depth is silent, since the object still works perfectly well. + * That is what the last release below is checking. + * + * Both objects are also spoken through one Animal * each, so the printed lines + * are the evidence that a three-level override reaches its own implementation. + * Repeated initialisation is refused at every level, leaving the existing members + * intact, and the constructor checks reject a NULL argument at each step: the + * food and the imagination fail last, and the colour and the breed fail inside the + * base part, which is a different cleanup path again. + */ +static void check_deep_hierarchy(void) +{ + Garfield *garfield = garfield_new("Garfield", 4, "orange", "lasagna"); + Snoopy *snoopy = snoopy_new("Snoopy", 3, "beagle", "his red baron"); + Animal *garfield_view; + Animal *snoopy_view; + char *food = malloc(sizeof("pizza")); + char *dream = malloc(sizeof("a nap")); + char *empty = NULL; + int value = 7; + + assert(garfield && snoopy && food && dream); + memcpy(food, "pizza", sizeof("pizza")); + memcpy(dream, "a nap", sizeof("a nap")); + + /* Each level's own field, and one found two records up. */ + assert(ooc_get(garfield, "favourite_food") == &garfield->favourite_food); + assert(ooc_get(garfield, "colour") == &garfield->cat.colour); + assert(ooc_get(garfield, "name") == &garfield->cat.animal.name); + assert(ooc_get(snoopy, "imagination") == &snoopy->imagination); + assert(ooc_get(snoopy, "breed") == &snoopy->dog.breed); + assert(ooc_get(snoopy, "name") == &snoopy->dog.animal.name); + + /* The other branch's field is absent from this chain, not misread. */ + assert(ooc_get(garfield, "imagination") == NULL); + assert(ooc_set(garfield, "imagination", dream) == -1); + assert(ooc_get(snoopy, "colour") == NULL); + assert(ooc_set(snoopy, "colour", dream) == -1); + assert(ooc_get(snoopy, "favourite_food") == NULL); + assert(ooc_get(garfield, "breed") == NULL); + + /* Private in the declaring class, readable and unwritable at any depth. */ + assert(*(int *)ooc_get(garfield, "_meals") == 1); + assert(ooc_set(garfield, "_meals", &value) == -1); + assert(*(int *)ooc_get(garfield, "_meals") == 1); + assert(ooc_set(garfield, "_lives", &value) == -1); + assert(*(int *)ooc_get(garfield, "_lives") == 9); + + /* Two underscores withhold the field from ooc_get() as well. */ + assert(ooc_get(garfield, "__legs") == NULL); + assert(ooc_get(snoopy, "__flights") == NULL); + assert(ooc_set(snoopy, "__flights", &value) == -1); + assert(snoopy_flights(snoopy) == 0); + assert(garfield_meals(garfield) == 1); + assert(garfield_meals(NULL) == 0); + assert(snoopy_flights(NULL) == 0); + + /* Owned replacement on the deepest own field, and on an inherited one. */ + assert(ooc_set(garfield, "favourite_food", &food) == 0); + assert(strcmp(garfield->favourite_food, "pizza") == 0); + assert(ooc_set(snoopy, "imagination", &dream) == 0); + assert(strcmp(snoopy->imagination, "a nap") == 0); + + /* + * Subtype tests walk the whole chain, so a three-level object answers for + * every ancestor, while the sibling branch stays unrelated at any depth. + */ + assert(ooc_is_a(garfield, &Garfield_class)); + assert(ooc_is_a(garfield, &Cat_class)); + assert(ooc_is_a(garfield, &Animal_class)); + assert(!ooc_is_a(garfield, &Dog_class)); + assert(!ooc_is_a(garfield, &Snoopy_class)); + assert(ooc_is_a(snoopy, &Snoopy_class)); + assert(ooc_is_a(snoopy, &Dog_class)); + assert(ooc_is_a(snoopy, &Animal_class)); + assert(!ooc_is_a(snoopy, &Cat_class)); + assert(!ooc_is_a(snoopy, &Garfield_class)); + + /* The exact type is the deepest record, with no walk involved. */ + assert(garfield->cat.animal.object.class == &Garfield_class); + assert(snoopy->dog.animal.object.class == &Snoopy_class); + + /* A three-level override, reached through one Animal pointer each. */ + garfield_view = ooc_retain(&garfield->cat.animal); + snoopy_view = ooc_retain(&snoopy->dog.animal); + assert(garfield_view && snoopy_view); + assert(garfield_view->vtable->speak != snoopy_view->vtable->speak); + animal_speak(garfield_view); + animal_speak(snoopy_view); + + /* Repeated initialisation is refused at every level, members intact. */ + assert(garfield_init(garfield, "again", 1, "red", "pizza") == -1); + assert(strcmp(garfield->cat.animal.name, "Garfield") == 0); + assert(snoopy_init(snoopy, "again", 1, "poodle", "food") == -1); + assert(strcmp(snoopy->dog.animal.name, "Snoopy") == 0); + assert(garfield_init(NULL, "name", 0, "colour", "food") == -1); + assert(snoopy_init(NULL, "name", 0, "breed", "dream") == -1); + assert(cat_init(NULL, "name", 0, "colour") == -1); + assert(dog_init(NULL, "name", 0, "breed") == -1); + assert(cat_init(&garfield->cat, "again", 1, "red") == -1); + assert(dog_init(&snoopy->dog, "again", 1, "poodle") == -1); + + /* + * Cleared strings must not reach printf(), and the destructor then frees what + * is left: three allocations here, so a memory checker is the only thing that + * can catch a missing free at the middle level. + */ + assert(ooc_set(garfield, "favourite_food", &empty) == 0); + assert(ooc_set(garfield, "colour", &empty) == 0); + assert(ooc_set(snoopy, "imagination", &empty) == 0); + assert(ooc_set(snoopy, "breed", &empty) == 0); + animal_speak(garfield_view); + ooc_release(garfield_view); + ooc_release(garfield); + ooc_release(snoopy_view); + ooc_release(snoopy); + + /* NULL arguments at each constructor step, from both branches. */ + assert(garfield_new("name", 0, NULL, "food") == NULL); + assert(garfield_new("name", 0, "colour", NULL) == NULL); + assert(snoopy_new("name", 0, NULL, "dream") == NULL); + assert(snoopy_new("name", 0, "breed", NULL) == NULL); +} + +/* + * Run all five groups of checks with assertions enabled. A successful run + * returns 0 and prints the speak output of the example animals, including the + * NULL-safe fallbacks. A failed assertion aborts instead of returning normally; a + * memory checker may report additional failures. Compiling with NDEBUG disables + * the assertion checks. */ int main(void) { @@ -292,5 +442,6 @@ int main(void) check_classes(); check_example(); check_subclasses(); + check_deep_hierarchy(); return 0; }