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

1
.gitignore vendored
View file

@ -1,5 +1,6 @@
.idea/ .idea/
build/ build/
ideas/
pkg/ pkg/

View file

@ -39,10 +39,13 @@ FORCE:
$(PKGCONFIG): libooc.pc.in FORCE | $(BUILD_DIR); sed -e 's|@prefix@|$(PREFIX)|g' -e 's|@VERSION@|$(VERSION)|g' $< > $@ $(PKGCONFIG): libooc.pc.in FORCE | $(BUILD_DIR); sed -e 's|@prefix@|$(PREFIX)|g' -e 's|@VERSION@|$(VERSION)|g' $< > $@
example: shared 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) EXAMPLE_SRCS=example/animal.c example/dog.c example/cat.c example/garfield.c example/snoopy.c
$(CC) $(CPPFLAGS) $(CFLAGS) $(WARNINGS) $(LDFLAGS) -Iexample tests/safety.c example/animal.c example/dog.c example/cat.c $(LIB_STATIC) -o $@ 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 example-test: example
LD_LIBRARY_PATH=$(BUILD_DIR) $(BUILD_DIR)/example LD_LIBRARY_PATH=$(BUILD_DIR) $(BUILD_DIR)/example

View file

@ -45,17 +45,69 @@ If installed under `/usr/local` and your pkg-config does not search there:
## Example ## 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 test # runs example/main.c
make safety-test # runs the assertion suite in tests/ make safety-test # runs the assertion suite in tests/
Objects are reference-counted with `ooc_retain()` and `ooc_release()`.
The public installed header is: The public installed header is:
#include <ooc/ooc.h> #include <ooc/ooc.h>
Application-specific classes such as `Animal`, `Dog` and `Cat` are not installed by the library. 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 ## Fields by name
A class publishes the fields that may be reached dynamically as a NULL-terminated 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 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. 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 `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 unknown field name or a NULL argument, so a bad name is reported instead of
writing out of bounds. 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 Assigning a field the pointer it already holds frees nothing. The destructor
still frees whatever the field holds when the object is released. 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 ## Type checks
`ooc_is_a()` walks the inheritance chain from the object's runtime type, so it `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 */ 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 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. 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 own fields through the struct definition, where the members are in view and no
accessor is needed. 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 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 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 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 include/ooc/ooc.h Public API
src/ooc.c Library implementation 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 Makefile Build/install rules
libooc.pc.in pkg-config template libooc.pc.in pkg-config template

View file

@ -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 * Split out of cat_new() so a subclass of Cat can build this part itself, the
* allocated through ooc_new() with a reference count of one, so the caller owns * same reason animal_init() exists for Animal. Garfield does exactly that: it
* it and must release it. animal_init() fills in the base part, including the * calls this, then installs its own vtable and adds its own members.
* `_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.
* *
* The vtable is overwritten after animal_init() rather than before, since that * The vtable is overwritten after animal_init() rather than before, since that
* call installs Animal's table and this one has to win. * 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 * `_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 * cannot write it with ooc_set(), so this function and any method of Cat are the
* Cat are the only places the value can change -- which is the point of marking * only places the value can change -- which is the point of marking it with an
* it with an underscore. * underscore.
* *
* Each step can fail, and both failures release the object, which is safe * The colour is copied before the base part is built, so a failed copy cannot
* because the destructor is already registered and frees whatever is present: * leave a half-initialised object behind: on failure nothing has been written
* after a failed animal_init() there is no name and no colour to free, and after * at all and the caller may simply release the zeroed storage. Failure leaves
* a failed colour copy the name has to be freed, which is exactly what the * the cat unchanged.
* destructor does. Returns NULL if any step fails, leaving nothing to release. *
* 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) 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) if (!cat)
return NULL; return NULL;
/* if (cat_init(cat, name, age, colour) != 0) {
* 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) {
ooc_release(cat); ooc_release(cat);
return NULL; return NULL;
} }

View file

@ -64,4 +64,22 @@ extern const ooc_class Cat_class;
*/ */
Cat *cat_new(const char *name, int age, const char *colour); 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 */ #endif /* CAT_H */

View file

@ -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 * Split out of dog_new() so a subclass of Dog can build this part itself, the
* allocated through ooc_new() with a reference count of one, so the caller owns * same reason animal_init() exists for Animal. Snoopy does exactly that: it
* it and must release it. animal_init() fills in the base part, including the * calls this, then installs its own vtable and adds its own members.
* `_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.
* *
* The vtable is overwritten after animal_init() rather than before, since that * The vtable is overwritten after animal_init() rather than before, since that
* call installs Animal's table and this one has to win. * call installs Animal's table and this one has to win.
* *
* Each step can fail, and both failures release the object, which is safe * The breed is copied before the base part is built, so a failed copy cannot
* because the destructor is already registered and frees whatever is present: * leave a half-initialised object behind: on failure nothing has been written
* after a failed animal_init() there is no name and no breed to free, and after * at all and the caller may simply release the zeroed storage. Failure leaves
* a failed breed copy the name has to be freed, which is exactly what the * the dog unchanged.
* destructor does. Returns NULL if any step fails, leaving nothing to release. *
* 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) 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) if (!dog)
return NULL; return NULL;
/* if (dog_init(dog, name, age, breed) != 0) {
* 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) {
ooc_release(dog); ooc_release(dog);
return NULL; return NULL;
} }

View file

@ -54,4 +54,22 @@ extern const ooc_class Dog_class;
*/ */
Dog *dog_new(const char *name, int age, const char *breed); 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 */ #endif /* DOG_H */

281
example/garfield.c Normal file
View file

@ -0,0 +1,281 @@
/*
* 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
*/
/*
* 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 <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 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;
}

122
example/garfield.h Normal file
View file

@ -0,0 +1,122 @@
/*
* 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
*/
#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 */

View file

@ -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 * 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 * 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 * 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 * source clear: `animal_speak` is a single call site and still reaches Dog's
* implementation for one object and Cat's for the other. * 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 "cat.h"
#include "dog.h" #include "dog.h"
#include "garfield.h"
#include "snoopy.h"
#include <ooc/ooc.h> #include <ooc/ooc.h>
#include <stdio.h> #include <stdio.h>
@ -27,15 +36,32 @@
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
/* 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) static char *copy_of(const char *text)
{ {
size_t len = strlen(text); size_t len;
char *copy = malloc(len + 1); char *copy;
++len;
if (!text)
return NULL;
len = strlen(text);
if (len == SIZE_MAX)
return NULL;
copy = malloc(len + 1);
if (copy) if (copy)
memcpy(copy, text, len); memcpy(copy, text, len + 1);
return copy; return copy;
} }
@ -260,6 +286,302 @@ fail:
return -1; 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 main(void)
{ {
int status = visit_dog(); int status = visit_dog();
@ -267,5 +589,14 @@ int main(void)
if (status == 0) if (status == 0)
status = visit_cat(); status = visit_cat();
if (status == 0)
status = visit_garfield();
if (status == 0)
status = visit_snoopy();
if (status == 0)
status = visit_hierarchy();
return status != 0; return status != 0;
} }

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;
}

111
example/snoopy.h Normal file
View file

@ -0,0 +1,111 @@
/*
* 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
*/
#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 */

View file

@ -12,15 +12,18 @@
* *
* Checks cover rejected metadata and arguments, overlapping field assignments, * Checks cover rejected metadata and arguments, overlapping field assignments,
* owned-pointer replacement, reference-count limits, the example constructors, * owned-pointer replacement, reference-count limits, the example constructors,
* and two subclasses of one base driven through a single call site. Assertions * two subclasses of one base driven through a single call site, and a
* must be enabled: NDEBUG removes checks and API calls inside assert(), leaving * three-level hierarchy whose branches do not see each other. Assertions must be
* an incomplete test run. Use AddressSanitizer or Valgrind as well to detect * 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. * invalid memory accesses, invalid frees, and leaks.
*/ */
#include <ooc/ooc.h> #include <ooc/ooc.h>
#include "cat.h" #include "cat.h"
#include "dog.h" #include "dog.h"
#include "garfield.h"
#include "snoopy.h"
#include <assert.h> #include <assert.h>
#include <stdint.h> #include <stdint.h>
@ -87,6 +90,12 @@ static void check_fields(void)
{ "bad_owned", offsetof(struct sample, bytes), 1, 1 }, { "bad_owned", offsetof(struct sample, bytes), 1, 1 },
{ NULL, 0, 0, 0 } { 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 ooc_class class = { sizeof(struct sample), destroy, NULL, fields };
const char *invalid[] = { "header", "past", "wrap", "huge", "empty", "bad_owned", "absent" }; const char *invalid[] = { "header", "past", "wrap", "huge", "empty", "bad_owned", "absent" };
struct sample *self = ooc_new(&class); 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 * Check a three-level hierarchy, with a subclass of a subclass on each branch.
* 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 * The point of the depth is that lookup crosses two class records, so "colour"
* report additional failures. Compiling with NDEBUG disables the assertion * is found at Cat_class and "name" at Animal_class, and neither is shadowed or
* checks. * 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) int main(void)
{ {
@ -292,5 +442,6 @@ int main(void)
check_classes(); check_classes();
check_example(); check_example();
check_subclasses(); check_subclasses();
check_deep_hierarchy();
return 0; return 0;
} }