libooc/example/animal.c

274 lines
9.1 KiB
C
Raw Normal View History

2026-10-02 00:08:18 +02:00
/*
* 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
*/
/*
* Animal -- the base class of the example.
*
* It owns a copy of its name, hands ownership of every instance to the
* caller, and forwards `speak` to the vtable so that subclasses can override
* the behaviour.
*/
#include "animal.h"
#include <limits.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 class that holds a string allocates it once here and hands the copy to the
* field, which is why the destructor has something to free. The copy is
* independent of `text`, so a name built from a stack buffer or a literal
* outlives the call.
*
* Returns NULL if `text` is NULL or the allocation fails, which the callers
* here treat as a failed construction. The terminator is copied along, so the
* result is a C string and nothing has to be added to it.
*/
static char *dupstr(const char *text)
{
2026-10-02 00:33:43 +02:00
size_t len;
char *copy;
2026-10-02 00:08:18 +02:00
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 what the constructor allocated.
*
* ooc_release() calls this with the object's storage still intact and frees the
* object itself afterwards, so the members are readable here and only the
* members need releasing. `name` is the one allocation Animal owns, and the
* "name" field is marked as owned, so ooc_set() frees an old string when it is
* replaced and the value that is left is freed here.
*
* The cast is free because the ooc_object header is the first member of Animal,
* which is what makes the address a valid Animal * in the first place. A
* subclass overrides this through its own `destroy`, and then it is also
* responsible for the members it added.
*/
static void destroy(ooc_object *object)
{
Animal *animal = (Animal *)object;
free(animal->name);
}
/*
* Default implementation of AnimalVTable::speak.
*
* Animal is the base class, so this is what a subclass that does not care to
* override gets: it prints the name, which is why the name has to be a real
* string rather than a pointer into somewhere else.
*
* The parameter is an Animal * rather than the runtime type, so an override
* casts it back to its own class -- see speak() in dog.c.
*/
static void speak(Animal *animal)
{
printf("%s makes a sound.\n", animal->name ? animal->name : "(unnamed)");
}
/*
* Animal's own vtable.
*
* One table per class, shared by every instance, which is why the field in the
* struct is a pointer to a const table rather than the table itself. A subclass
* installs its own, so an instance of the derived class reaches the derived
* `speak` through the same call.
*/
static const AnimalVTable vt = {
.speak = speak,
};
/*
* The fields ooc_get() and ooc_set() may reach by name.
*
* offsetof() and sizeof() keep the table in step with the struct, so a changed
* member type or position needs no edit here. The last column marks a field
* the class owns: `name` is a string on the heap, so ooc_set() releases the
* old one when a new name is assigned, while `age` is copied as it stands.
*
* The two underscore names are published so the library can enforce what they
* mean. "_id" is readable and refuses to be written, and "__legs" is readable
* by neither, which is why the class hands it out through animal_legs().
*/
static const ooc_field Animal_fields[] = {
{ "name", offsetof(Animal, name), sizeof(((Animal *)0)->name), 1 },
{ "age", offsetof(Animal, age), sizeof(((Animal *)0)->age), 0 },
{ "_id", offsetof(Animal, _id), sizeof(((Animal *)0)->_id), 0 },
{ "__legs", offsetof(Animal, __legs), sizeof(((Animal *)0)->__legs), 0 },
{ NULL, 0, 0, 0 },
};
/*
* Animal's runtime type record.
*
* `size` is sizeof(Animal) and not the size of a base, because ooc_new()
* allocates exactly this much and the derived classes that embed Animal need
* room for their own members on top. `destroy` is the destructor above,
* `super` is NULL because Animal is a root class, and `fields` is the table
* that gives ooc_get() and ooc_set() something to resolve.
*
* The record is a file-scope constant, which is what ooc_new() writes into
* every object it allocates.
*/
const ooc_class Animal_class = {
.size = sizeof(Animal),
.destroy = destroy,
.super = NULL,
.fields = Animal_fields,
};
/*
* Hand out the next serial number.
*
* File-scope so the numbers keep climbing across calls, and private to this
* file so no other class can hand them out or reset them. It is the only thing
* that decides what `_id` becomes.
*/
static int next_id = 1;
/*
* Initialise the members Animal owns.
*
* Separate from animal_new() so a subclass constructor can build the base part
* instead of duplicating it: dog_new() calls this and then installs its own
* vtable and its own members. The `vtable` it sets is Animal's, which a
* subclass is expected to overwrite.
*
* The caller supplies storage that is already an object, ooc_new() having
* allocated it, so there is nothing here to release: the name is copied into a
* field the object now owns and the destructor frees it. The two underscore
* fields are written here and nowhere else, which is what keeps them to the
* class. The field table publishes both so the library knows where they are,
* and the leading underscores tell ooc_set() and ooc_get() to leave them alone.
*
* Requires zero-initialised members and externally synchronized constructors.
* Returns -1 for NULL, repeated initialisation, exhausted IDs or a failed name
* copy. Failure leaves the object unchanged and still owned by the caller.
*/
int animal_init(Animal *animal, const char *name, int age)
{
char *copy;
if (!animal || animal->vtable || animal->name || next_id == 0)
return -1;
copy = dupstr(name);
if (!copy)
return -1;
animal->vtable = &vt;
animal->name = copy;
animal->age = age;
animal->_id = next_id;
next_id = next_id == INT_MAX ? 0 : next_id + 1;
animal->__legs = 4;
return 0;
}
/*
* Read back `_id`, the field a caller may also reach with ooc_get().
*
* An accessor alongside the by-name route rather than instead of it, and the
* difference is the const: this one does not require a writable object, so it
* can be called on a const Animal *.
*
* Returns 0 for a NULL object, which is indistinguishable from a real id of
* zero, so a caller that has to tell the two apart should check the pointer
* first. Ids start at 1.
*/
int animal_id(const Animal *animal)
{
return animal ? animal->_id : 0;
}
/*
* Read back `__legs`, the field nothing outside the class can reach by name.
*
* ooc_get(dog, "__legs") returns NULL, so this accessor is the only way to
* learn the value -- which is the whole point of a two-underscore name. The
* class decides what a caller is told, and could as well return something
* derived from `__legs` instead of the field itself.
*
* Returns 0 for a NULL object, as above.
*/
int animal_legs(const Animal *animal)
{
return animal ? animal->__legs : 0;
}
/*
* Create an Animal named `name` and return it, or NULL.
*
* The object is allocated through ooc_new() and comes back with a reference
* count of one, so the caller owns it and must pass it to ooc_release()
* eventually. animal_init() fills in the members, and a failure there releases
* the half-built object, since the destructor is already in place and finds
* `name` still NULL to free.
*
* This is the constructor a caller normally uses; animal_init() is what a
* subclass uses instead, because it is building the base part of a larger
* object rather than a whole one.
*
* Returns NULL if the allocation fails or the name could not be copied. In
* both cases nothing is left to release.
*/
Animal *animal_new(const char *name, int age)
{
Animal *animal = ooc_new(&Animal_class);
if (!animal)
return NULL;
if (animal_init(animal, name, age) != 0) {
ooc_release(animal);
return NULL;
}
return animal;
}
/*
* Dispatch `speak` through the vtable.
*
* The call is virtual: which implementation runs is decided by the vtable the
* object carries, not by the static type of the pointer handed in. A Dog and an
* Animal pointer to the same object therefore print different things.
*
* The three checks are what keep the call safe on a partial object. A NULL
* animal is skipped, a NULL vtable means the object was never initialised, and
* a NULL `speak` means a vtable that does not implement the method. All three
* are tolerated silently, since a class is not obliged to implement anything.
*/
void animal_speak(Animal *animal)
{
if (animal && animal->vtable && animal->vtable->speak)
animal->vtable->speak(animal);
}