From f59561fdfb3d25ecbba312ec3af0fb3f1898be3e Mon Sep 17 00:00:00 2001 From: hanez Date: Fri, 2 Oct 2026 00:08:18 +0200 Subject: [PATCH] Initial commit. --- .gitignore | 5 + LICENSE | 13 ++ Makefile | 78 +++++++++ README.md | 225 ++++++++++++++++++++++++ example/animal.c | 274 +++++++++++++++++++++++++++++ example/animal.h | 107 ++++++++++++ example/cat.c | 215 +++++++++++++++++++++++ example/cat.h | 67 ++++++++ example/dog.c | 192 +++++++++++++++++++++ example/dog.h | 57 ++++++ example/main.c | 271 +++++++++++++++++++++++++++++ include/ooc/ooc.h | 279 ++++++++++++++++++++++++++++++ libooc.pc.in | 9 + src/ooc.c | 428 ++++++++++++++++++++++++++++++++++++++++++++++ tests/safety.c | 296 ++++++++++++++++++++++++++++++++ 15 files changed, 2516 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README.md create mode 100644 example/animal.c create mode 100644 example/animal.h create mode 100644 example/cat.c create mode 100644 example/cat.h create mode 100644 example/dog.c create mode 100644 example/dog.h create mode 100644 example/main.c create mode 100644 include/ooc/ooc.h create mode 100644 libooc.pc.in create mode 100644 src/ooc.c create mode 100644 tests/safety.c diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e8277fd --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.idea/ + +build/ +pkg/ + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..93f9f56 --- /dev/null +++ b/LICENSE @@ -0,0 +1,13 @@ +Copyright 2026 Johannes Findeisen + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..fdb2164 --- /dev/null +++ b/Makefile @@ -0,0 +1,78 @@ +PREFIX ?= /usr/local +LIBDIR ?= $(PREFIX)/lib +INCLUDEDIR ?= $(PREFIX)/include +PKGCONFIGDIR ?= $(LIBDIR)/pkgconfig +CC ?= cc +AR ?= ar +RANLIB ?= ranlib +VERSION ?= 0.1.0 +SOVERSION ?= 0 +CFLAGS ?= -std=c99 -O2 +WARNINGS ?= -Wall -Wextra -Wpedantic +CPPFLAGS ?= -Iinclude +BUILD_DIR=build +PKG_DIR=pkg +OBJ=$(BUILD_DIR)/ooc.o +LIB_STATIC=$(BUILD_DIR)/libooc.a +LIB_SHARED=$(BUILD_DIR)/libooc.so.$(VERSION) +PKGCONFIG=$(BUILD_DIR)/libooc.pc + +.PHONY: all shared static example example-test safety-test install uninstall pkg clean clean-pkg clean-all + +all: shared static +$(BUILD_DIR):; mkdir -p $@ +$(OBJ): src/ooc.c include/ooc/ooc.h | $(BUILD_DIR); $(CC) $(CPPFLAGS) $(CFLAGS) $(WARNINGS) -fPIC -c $< -o $@ +$(LIB_STATIC): $(OBJ); $(AR) rcs $@ $^; $(RANLIB) $@ +$(LIB_SHARED): $(OBJ); $(CC) $(CFLAGS) $(LDFLAGS) -shared -Wl,-soname,libooc.so.$(SOVERSION) -o $@ $^ + +shared: $(LIB_SHARED) + ln -sf $$(basename $(LIB_SHARED)) $(BUILD_DIR)/libooc.so.$(SOVERSION) + ln -sf $$(basename $(LIB_SHARED)) $(BUILD_DIR)/libooc.so + +static: $(LIB_STATIC) +# FORCE, because PREFIX is a make variable rather than a file: without it a +# build/libooc.pc left over from another prefix is never rewritten and the +# staged .pc disagrees with the tree it sits in. +.PHONY: FORCE +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 + +$(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-test: example + LD_LIBRARY_PATH=$(BUILD_DIR) $(BUILD_DIR)/example + +safety-test: $(BUILD_DIR)/safety-test + $(BUILD_DIR)/safety-test + +install: all $(PKGCONFIG) + install -d $(DESTDIR)$(LIBDIR) $(DESTDIR)$(INCLUDEDIR)/ooc $(DESTDIR)$(PKGCONFIGDIR) + install -m 755 $(LIB_SHARED) $(DESTDIR)$(LIBDIR)/ + ln -sf $$(basename $(LIB_SHARED)) $(DESTDIR)$(LIBDIR)/libooc.so.$(SOVERSION) + ln -sf libooc.so.$(SOVERSION) $(DESTDIR)$(LIBDIR)/libooc.so + install -m 644 $(LIB_STATIC) $(DESTDIR)$(LIBDIR)/ + install -m 644 include/ooc/ooc.h $(DESTDIR)$(INCLUDEDIR)/ooc/ + install -m 644 $(PKGCONFIG) $(DESTDIR)$(PKGCONFIGDIR)/ + +# Stage the install tree under ./pkg for packaging, without touching the system. +# Equivalent to: make DESTDIR="$PWD/pkg" PREFIX=/$(PREFIX) install +# The prefix is re-rooted at / with exactly one separator, so an already +# absolute PREFIX does not end up as //usr/local inside the generated .pc file. +pkg: all + $(MAKE) DESTDIR=$(CURDIR)/$(PKG_DIR) PREFIX=/$(patsubst /%,%,$(PREFIX)) install + +uninstall: + rm -f $(DESTDIR)$(LIBDIR)/libooc.so $(DESTDIR)$(LIBDIR)/libooc.so.$(SOVERSION) $(DESTDIR)$(LIBDIR)/$(notdir $(LIB_SHARED)) $(DESTDIR)$(LIBDIR)/$(notdir $(LIB_STATIC)) $(DESTDIR)$(INCLUDEDIR)/ooc/ooc.h $(DESTDIR)$(PKGCONFIGDIR)/libooc.pc + +clean: + rm -rf $(BUILD_DIR) + +clean-pkg: + rm -rf $(PKG_DIR) + +clean-all: clean clean-pkg diff --git a/README.md b/README.md new file mode 100644 index 0000000..823574b --- /dev/null +++ b/README.md @@ -0,0 +1,225 @@ +# libooc + +A tiny C library for object-oriented programming using ordinary C structs and function-pointer vtables. It provides object allocation, virtual destructors, intrusive reference counting, runtime class metadata, name-based field access, and a small interface header. + +## Build + + make + +Build the example: + + make test + +## Install + +The default prefix is `/usr/local`: + + sudo make install + +Or install somewhere else: + + make PREFIX="$HOME/.local" install + +For packaging, `pkg` stages the same tree under `./pkg` without needing root: + + make pkg + make clean-pkg + +That is shorthand for `make DESTDIR="$PWD/pkg" PREFIX=/$(PREFIX) install`, and it honours an overridden prefix: + + make pkg PREFIX=/usr + +## pkg-config + +After installation: + + pkg-config --cflags --libs libooc + +Compile an application with: + + cc -std=c99 -Wall -Wextra -Wpedantic main.c $(pkg-config --cflags --libs libooc) + +If installed under `/usr/local` and your pkg-config does not search there: + + export PKG_CONFIG_PATH=/usr/local/lib/pkgconfig:$PKG_CONFIG_PATH + +## 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()`. + + make test # runs example/main.c + make safety-test # runs the assertion suite in tests/ + +The public installed header is: + + #include + +Application-specific classes such as `Animal`, `Dog` and `Cat` are not installed by the library. + +## Fields by name + +A class publishes the fields that may be reached dynamically as a NULL-terminated +array of `ooc_field`, and names its base class in `ooc_class.super`: + +```c +static const ooc_field Animal_fields[] = { + { "name", offsetof(Animal, name), sizeof(((Animal *)0)->name), 1 }, + { "age", offsetof(Animal, age), sizeof(((Animal *)0)->age), 0 }, + { NULL, 0, 0, 0 }, +}; + +const ooc_class Animal_class = { + .size = sizeof(Animal), + .destroy = destroy, + .super = NULL, + .fields = Animal_fields, +}; +``` + +That is enough to read and write fields through the object alone, without the +struct definition being visible: + +```c +int age = 6; + +ooc_set(dog, "age", &age); +printf("%s is %d\n", *(char **)ooc_get(dog, "name"), *(int *)ooc_get(dog, "age")); +``` + +`ooc_get()` returns the address of the field, so lookup follows the inheritance +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. + +`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. + +The last column of a field descriptor says who owns the value. Zero, the +default, is a value that is only copied around and has to be freed by whoever +allocated it. Non-zero marks a field as a single pointer the class owns, and +`ooc_set()` then hands the field over: the new value is copied in and the old +pointer is freed. Since `ooc_set()` takes the address of the value, a pointer +field is assigned through a pointer to the pointer: + +```c +char *name = copy_of("Bella"); + +ooc_set(dog, "name", &name); /* "Rex" is freed here */ +printf("%s\n", *(char **)ooc_get(dog, "name")); +``` + +Ownership passing means the replacement has to be `malloc()`ed memory and may +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. + +## Type checks + +`ooc_is_a()` walks the inheritance chain from the object's runtime type, so it +answers for a base class as well as for the type itself: + +```c +ooc_is_a(cat, &Cat_class); /* 1 */ +ooc_is_a(cat, &Animal_class); /* 1 -- a Cat is an Animal */ +ooc_is_a(cat, &Dog_class); /* 0 -- siblings are unrelated */ +``` + +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. + +Asking whether an object is *exactly* one type is a different question, and needs +no chain walk. The class record is a public member of `ooc_object`: + +```c +cat->animal.object.class == &Animal_class; /* 0 -- exactly a Cat */ +``` + +## Private fields + +A field whose name starts with an underscore belongs to the class that declares +it. The number of underscores is how much of the field the outside world gets, +and it is the convention that stands in for access specifiers in a language +that has none: + +| Name | `ooc_get()` | `ooc_set()` | +| --- | --- | --- | +| `name` | reads | writes, and releases the old value if owned | +| `_id` | reads | refused | +| `__legs` | refused | refused | +| `has_underscore` | reads | writes | + +Both fields are published in the field table, so the library knows where they +are and can enforce the rest: + +```c +static const ooc_field Animal_fields[] = { + { "name", offsetof(Animal, name), sizeof(((Animal *)0)->name), 1 }, + { "_id", offsetof(Animal, _id), sizeof(((Animal *)0)->_id), 0 }, + { "__legs", offsetof(Animal, __legs), sizeof(((Animal *)0)->__legs), 0 }, + { NULL, 0, 0, 0 }, +}; +``` + +One underscore leaves a field readable but not writable, which is enough for a +class to show its own state without handing out a way to change it: + +```c +ooc_set(dog, "_id", &id); /* refused with -1, the field is unchanged */ +*(int *)ooc_get(dog, "_id"); /* still readable */ +``` + +Two underscores make the field unreachable in both directions. `ooc_get()` +still finds it in the table and then withholds the pointer, so there is no way +in by name, and only the object can change the value: + +```c +ooc_get(dog, "__legs"); /* NULL, whether or not the field exists */ +ooc_set(dog, "__legs", &legs); /* refused with -1 */ +``` + +What a class keeps under such a name it hands out itself, through an accessor +the way a method would: + +```c +int animal_legs(const Animal *animal) +{ + return animal ? animal->__legs : 0; +} +``` + +Both rules follow the name to whichever class in the chain declared it, so a +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. + +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 +not release anything: an owned private field is still owned, so a caller that +frees one behind the class' back leaves the destructor holding a dangling +pointer. + +## Layout + + include/ooc/ooc.h Public API + src/ooc.c Library implementation + example/ Complete example + Makefile Build/install rules + libooc.pc.in pkg-config template + +Class metadata must remain alive and immutable while objects refer to it. +Cyclic inheritance and bases larger than their subclasses are rejected. Dynamic +fields must fit in their declaring class and must not overlap the object header. +Reference counting and field access require external synchronization across +threads. `ooc_retain()` returns NULL if the reference count cannot be incremented. + +## Portability + +The library API is standard C and has no third-party dependencies. The supplied Makefile uses conventional POSIX/Unix build tools and produces both shared and static libraries. Other platforms can use the same source/API with their native build system if their shared-library conventions differ. + +## License + +Apache License 2.0 — see the LICENSE file for details. + +Copyright 2026 Johannes Findeisen diff --git a/example/animal.c b/example/animal.c new file mode 100644 index 0000000..1f9fb38 --- /dev/null +++ b/example/animal.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 + */ + +/* + * 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 +#include +#include +#include +#include +#include + +/* + * 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) +{ + 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 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); +} diff --git a/example/animal.h b/example/animal.h new file mode 100644 index 0000000..df864f9 --- /dev/null +++ b/example/animal.h @@ -0,0 +1,107 @@ +/* + * 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 ANIMAL_H +#define ANIMAL_H + +#include + +typedef struct Animal Animal; +typedef struct AnimalVTable AnimalVTable; + +/* + * Virtual methods inherited by every animal. + * + * The vtable is a plain struct of function pointers, so dispatch is an ordinary + * call through a pointer and needs no support from the compiler. A subclass + * fills in its own table and installs it in the instance, which is how `speak` + * reaches Dog's implementation when the object is only known as an Animal. + * + * Each pointer is called with the object as its first argument, so an + * implementation casts it back to the class it belongs to. + */ +struct AnimalVTable { + void (*speak)(Animal *self); +}; + +/* + * Base class. + * + * Every instance starts with an ooc_object header, which the runtime needs + * for reference counting and the virtual destructor. Subclasses embed this + * struct as their first member, which makes the upcast free. + */ +struct Animal { + ooc_object object; + const AnimalVTable *vtable; + char *name; + int age; + + /* + * Two fields that belong to the class alone, told apart by their leading + * underscores. + * + * `_id` is published, so it can be read by name, and ooc_set() refuses to + * write it. `__legs` is hidden outright: ooc_get() withholds it as well, so + * nothing outside Animal can read or write it by name at all. Both are set + * from the struct definition below, where the members are in view. + */ + int _id; + int __legs; +}; + +/* + * Animal's runtime type record, the value ooc_new() takes. + * + * A subclass names it in its own `super`, which is how field lookup continues + * up the chain and how Dog's objects still resolve "name" and "age". + */ +extern const ooc_class Animal_class; + +/* + * Create an Animal named `name` and return it, or NULL. + * + * Ownership of the result is the caller's, with a reference count of one, so it + * has to reach ooc_release() eventually. Returns NULL if the allocation or the + * copy of the name fails, in which case there is nothing to release. + */ +Animal *animal_new(const char *name, int age); + +/* + * Read back `_id`, the field ooc_get() also returns. + * + * Takes a const object, so it can be called where ooc_get() could not. + */ +int animal_id(const Animal *animal); + +/* + * Read back `__legs`, the field ooc_get() withholds. + * + * This accessor is the only way to learn the value, which is the point of a + * two-underscore name. Returns 0 for a NULL object. + */ +int animal_legs(const Animal *animal); + +/* + * Initialise the members Animal owns, so a subclass constructor can build the + * base part without duplicating it. Requires zero-initialised member storage + * and external synchronization between constructors in different threads. + * Returns 0, or -1 for NULL, an already initialised animal, exhausted positive + * int IDs, or a failed name copy. Failure leaves the animal unchanged. + */ +int animal_init(Animal *animal, const char *name, int age); + +/* + * Dispatch `speak` through the vtable, so the implementation depends on the + * object's runtime type. A NULL object, a NULL vtable and a NULL method are all + * tolerated and do nothing. + */ +void animal_speak(Animal *animal); + +#endif /* ANIMAL_H */ diff --git a/example/cat.c b/example/cat.c new file mode 100644 index 0000000..55efdab --- /dev/null +++ b/example/cat.c @@ -0,0 +1,215 @@ +/* + * 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 + */ + +/* + * Cat -- a second Animal subclass, alongside Dog. + * + * The point of having two is that nothing here is special to Dog: the same three + * steps make any subclass, and a Cat is a different runtime type with its own + * vtable and its own destructor. A caller holding an Animal * for a Cat and one + * for a Dog gets different behaviour out of the same call, which is the whole + * argument for the vtable. + * + * Where Cat differs from Dog is in its members. `colour` is another owned string, + * while `_lives` is an int marked private by its leading underscore, so the + * class owns it and ooc_set() turns a write away even though the caller can + * still read it by name. + */ + +#include "cat.h" + +#include +#include +#include +#include +#include + +/* + * Copy `text` onto the heap; the caller owns the result. + * + * A private copy of Animal's helper, and identical to it. Each class keeps its + * own: exposing dupstr() would mean every subclass reaching into a base class' + * internals to build its members, which is the coupling subclassing is meant to + * remove. The size is checked before the terminator is added, so a string long + * enough to wrap cannot ask malloc() for a short buffer. + * + * Returns NULL if `text` is NULL or the allocation fails. + */ +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 cat_new() allocated. + * + * This replaces Animal's destructor rather than running after it, because the + * library calls the one belonging to the object's runtime type and stops there. + * So a Cat is responsible for Animal's members too: `name` is freed below even + * though animal_init() allocated it, and Animal's own destroy() never runs for + * a Cat. The second free is the half of a derived destructor that is easy to + * forget. + * + * `_lives` needs no freeing, being an int, which is worth noticing: what a + * destructor releases is the allocations a constructor made, not the members. + * + * The order of the two frees does not matter, since the allocations are + * independent, and both are owned: "colour" is marked as owned in the field + * table and "name" in Animal's, so ooc_set() releases a string when it replaces + * one and whatever is left is freed here. + */ +static void destroy(ooc_object *object) +{ + Cat *cat = (Cat *)object; + + free(cat->colour); + free(cat->animal.name); +} + +/* + * Cat's implementation of AnimalVTable::speak. + * + * The signature is Animal's, not Cat's: a vtable entry is called through the + * base class' type, so the implementation casts its parameter back down. That + * cast is safe only because Animal is the first member of Cat, which makes the + * two addresses the same. + * + * The members are read through the cast, including the ones Animal owns, so + * `name` and `_lives` are as available here as they are in Animal's own speak(). + * The fallbacks keep a cleared string from reaching printf(), since a field set + * to NULL is a legitimate state -- ooc_set(dog, "name", NULL) is allowed to + * succeed -- and a vtable entry is called with whatever the object currently + * holds. + */ +static void speak(Animal *animal) +{ + Cat *cat = (Cat *)animal; + + printf("%s says: Meow! (%s, %d lives left)\n", + cat->animal.name ? cat->animal.name : "(unnamed)", + cat->colour ? cat->colour : "(unknown colour)", + cat->_lives); +} + +/* + * Cat's own vtable, identical in shape to Animal's and holding Cat's speak. + * + * A second table with the same layout as Animal's, which is what a vtable buys: + * the slot is chosen by the class that owns the table, not by the type of the + * pointer the caller happens to have. Cat and Dog both override `speak`, and + * neither can tell the other about it. + */ +static const AnimalVTable vt = { + .speak = speak, +}; + +/* + * The fields Cat adds on top of the ones Animal publishes. + * + * A derived class lists only what it declares itself; the rest is inherited + * rather than repeated. Anything missing here -- "name", "age", "_id" and + * "__legs" -- is still reachable through ooc_get() and ooc_set(), because + * Cat_class names Animal_class as its base and lookup walks the chain. + * + * `colour` is marked as owned, exactly as Dog's breed is, so replacing it + * through ooc_set() releases the string it held before. `_lives` carries a + * leading underscore, so ooc_set() refuses to write it while ooc_get() still + * returns it: the underscore rule works the same on a field the subclass + * declared as on one the base declared. + */ +static const ooc_field Cat_fields[] = { + { "colour", offsetof(Cat, colour), sizeof(((Cat *)0)->colour), 1 }, + { "_lives", offsetof(Cat, _lives), sizeof(((Cat *)0)->_lives), 0 }, + { NULL, 0, 0, 0 }, +}; + +/* + * Cat's runtime type record. + * + * `super` is what makes Cat an Animal: field lookup continues from + * Animal_class, so "name", "age", "_id" and "__legs" resolve even though Cat + * does not list them. `size` is sizeof(Cat) rather than sizeof(Animal), since + * the allocation has to hold the colour and the lives count as well. + * + * The record is a file-scope constant, as it is for every class, and ooc_new() + * writes its address into each object it allocates -- which is how a Cat and a + * Dog stay distinguishable while both are passed around as Animal. + */ +const ooc_class Cat_class = { + .size = sizeof(Cat), + .destroy = destroy, + .super = &Animal_class, + .fields = Cat_fields, +}; + +/* + * Create a Cat named `name` of the given `colour` and return it, or NULL. + * + * 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. + * + * 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. + * + * 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. + */ +Cat *cat_new(const char *name, int age, const char *colour) +{ + Cat *cat = ooc_new(&Cat_class); + + 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) { + ooc_release(cat); + return NULL; + } + + return cat; +} diff --git a/example/cat.h b/example/cat.h new file mode 100644 index 0000000..5b46d91 --- /dev/null +++ b/example/cat.h @@ -0,0 +1,67 @@ +/* + * 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 CAT_H +#define CAT_H + +#include "animal.h" + +typedef struct Cat Cat; + +/* + * Derived class. + * + * The embedded Animal comes first, so a Cat pointer can be handed to any + * function expecting an Animal, and Cat adds a colour and a private `_lives` of + * its own. + * + * Two things follow from that ordering. The ooc_object header inside Animal + * stays at offset zero, where the library looks for it, and the upcast is a + * no-op cast because both addresses are the same -- which is what lets speak() + * in cat.c cast its Animal * back to a Cat * without arithmetic. The cost is + * that a Cat may not add a member of its own before the base. + * + * The struct holds no destructor of its own; the one Cat registers lives in + * Cat_class and releases both the colour and Animal's name. + */ +struct Cat { + Animal animal; + char *colour; + + /* + * Private to Cat, and readable by name: one leading underscore stops + * ooc_set() from writing it but leaves ooc_get() alone. Animal's `_id` works + * the same way, so the rule is the one in the library rather than something + * the subclass has to repeat. + */ + int _lives; +}; + +/* + * Cat's runtime type record, the value ooc_new() takes. + * + * Its `super` is Animal_class, so a Cat inherits Animal's fields, and its + * `size` is sizeof(Cat), so the allocation has room for the colour and the + * lives count. + */ +extern const ooc_class Cat_class; + +/* + * Create a Cat named `name` of the given `colour` 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 Cat and answers to Cat's `speak`, whether it is later reached + * through a Cat * or an Animal *. + */ +Cat *cat_new(const char *name, int age, const char *colour); + +#endif /* CAT_H */ diff --git a/example/dog.c b/example/dog.c new file mode 100644 index 0000000..e4323e5 --- /dev/null +++ b/example/dog.c @@ -0,0 +1,192 @@ +/* + * 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 + */ + +/* + * Dog -- an Animal subclass that overrides the virtual `speak` method. + * + * Because Dog embeds an Animal as its first member, a Dog can be passed + * wherever an Animal is expected and still reach Dog's own vtable. + * + * Subclassing here means three things, and this file shows each one. The object + * is allocated from Dog_class, whose `super` points at Animal_class so the + * inherited fields resolve. Animal's members are built by animal_init(), the + * base class' own entry point, rather than duplicated. And a separate vtable + * and destructor are registered for Dog, so the override and the extra + * allocation are cleaned up by the runtime type of the object. + */ + +#include "dog.h" + +#include +#include +#include +#include +#include + +/* + * Copy `text` onto the heap; the caller owns the result. + * + * A private copy of Animal's helper, and identical to it. It is duplicated + * rather than shared because a class keeps its own constructor state: exposing + * dupstr() would mean the derived class reaching into a base class' internals + * to build its members, which is the coupling subclassing is meant to remove. + * + * Returns NULL if `text` is NULL or the allocation fails. + */ +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 dog_new() allocated. + * + * This replaces Animal's destructor rather than running after it, because the + * library calls the one belonging to the object's runtime type and stops + * there. So a Dog is responsible for Animal's members too: `name` is freed + * below even though animal_init() allocated it, and Animal's own destroy() + * never runs for a Dog. The second free is the half of a derived destructor + * that is easy to forget. + * + * The order does not matter, since the two allocations are independent, and + * both are owned: "breed" is marked as owned in the field table, and "name" is + * marked as owned in Animal's, so ooc_set() releases a string when it replaces + * one and what is left here is freed on destruction. + */ +static void destroy(ooc_object *object) +{ + Dog *dog = (Dog *)object; + + free(dog->breed); + free(dog->animal.name); +} + +/* + * Dog's implementation of AnimalVTable::speak. + * + * The signature is Animal's, not Dog's: a vtable entry is called through the + * base class' type, so the implementation casts its parameter back down. That + * cast is the cost of a subclass adding behaviour to an existing method, and + * it is safe only because Animal is the first member of Dog, which makes the + * two addresses the same. + * + * Adding a *new* virtual method is not possible this way, since the vtable is + * the base class' struct: that takes an interface struct of its own, holding + * the vtable pointer this class already has as `vtable`. + */ +static void speak(Animal *animal) +{ + Dog *dog = (Dog *)animal; + + printf("%s says: Woof! (%s)\n", dog->animal.name ? dog->animal.name : "(unnamed)", + dog->breed ? dog->breed : "(unknown breed)"); +} + +/* + * Dog's own vtable, identical in shape to Animal's and holding Dog's speak. + * + * A subclass that overrides nothing reuses the base table as it is, so the + * override is what makes a separate table necessary here. + */ +static const AnimalVTable vt = { + .speak = speak, +}; + +/* + * The fields Dog adds on top of the ones Animal publishes. + * + * A derived class lists only what it declares itself; the rest is inherited + * rather than repeated. Anything missing here -- "name", "age", and Animal's + * two underscore names -- is still reachable through ooc_get() and ooc_set(), + * because Dog_class names Animal_class as its base and lookup walks the chain. + * + * Like Animal's name, `breed` is marked as owned, so replacing it through + * ooc_set() releases the string it held before. The underscores of the + * inherited fields keep their meaning across the boundary: ooc_set(dog, "_id") + * is still refused even though Dog did not declare it. + */ +static const ooc_field Dog_fields[] = { + { "breed", offsetof(Dog, breed), sizeof(((Dog *)0)->breed), 1 }, + { NULL, 0, 0, 0 }, +}; + +/* + * Dog's runtime type record. + * + * `super` is what makes Dog a Dog: field lookup continues from Animal_class, so + * "name", "age", "_id" and "__legs" resolve even though Dog does not list them. + * `size` is sizeof(Dog) rather than sizeof(Animal), since the allocation has to + * hold the breed as well as the base. + */ +const ooc_class Dog_class = { + .size = sizeof(Dog), + .destroy = destroy, + .super = &Animal_class, + .fields = Dog_fields, +}; + +/* + * Create a Dog named `name` of the given `breed` and return it, or NULL. + * + * 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. + * + * 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. + */ +Dog *dog_new(const char *name, int age, const char *breed) +{ + Dog *dog = ooc_new(&Dog_class); + + 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) { + ooc_release(dog); + return NULL; + } + + return dog; +} diff --git a/example/dog.h b/example/dog.h new file mode 100644 index 0000000..cb0b48f --- /dev/null +++ b/example/dog.h @@ -0,0 +1,57 @@ +/* + * 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 DOG_H +#define DOG_H + +#include "animal.h" + +typedef struct Dog Dog; + +/* + * Derived class. + * + * The embedded Animal comes first, so a Dog pointer can be handed to any + * function expecting an Animal, and Dog adds a breed of its own. + * + * Two things follow from that ordering. The ooc_object header inside Animal + * stays at offset zero, where the library looks for it, and the upcast is a + * no-op cast because both addresses are the same -- which is what lets speak() + * in dog.c cast its Animal * back to a Dog * without arithmetic. The cost is + * that a Dog may not add a member of its own before the base. + * + * The struct holds no destructor of its own; the one Dog registers lives in + * Dog_class and releases both the breed and Animal's name. + */ +struct Dog { + Animal animal; + char *breed; +}; + +/* + * Dog's runtime type record, the value ooc_new() takes. + * + * Its `super` is Animal_class, so a Dog inherits Animal's fields, and its + * `size` is sizeof(Dog), so the allocation has room for the breed. + */ +extern const ooc_class Dog_class; + +/* + * Create a Dog named `name` of the given `breed` 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 Dog and answers to Dog's `speak`, whether it is later reached + * through a Dog * or an Animal *. + */ +Dog *dog_new(const char *name, int age, const char *breed); + +#endif /* DOG_H */ diff --git a/example/main.c b/example/main.c new file mode 100644 index 0000000..406fa9d --- /dev/null +++ b/example/main.c @@ -0,0 +1,271 @@ +/* + * 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 + */ + +/* + * Walks through the libooc life cycle twice over, once per subclass. + * + * 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 + * the virtual destructor. The Cat walk then repeats the same calls against a + * 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. + */ + +#include "cat.h" +#include "dog.h" + +#include +#include +#include +#include +#include + +/* Copy `text` onto the heap, because a field that owns its value needs heap. */ +static char *copy_of(const char *text) +{ + size_t len = strlen(text); + char *copy = malloc(len + 1); + ++len; + + if (copy) + memcpy(copy, text, len); + + return copy; +} + +/* + * The Dog walk. + * + * One object taken through the whole life cycle: created, shared through a base + * class reference, written and read by field name, spoken through, and + * released. Every reference the object collects has to be given back, so both + * `dog` and `animal` are released on the way out, including on the error paths. + * + * Returns 0 when the walk completes and -1 if a step the example relies on was + * refused. + */ +static int visit_dog(void) +{ + Dog *dog; + Animal *animal; + char *name; + int birthday = 6; + int weight = 45; + + dog = dog_new("Rex", 5, "German Shepherd"); + if (!dog) { + fprintf(stderr, "failed to create dog\n"); + return -1; + } + + /* The count is now two, so both references have to be released. */ + animal = ooc_retain((Animal *)dog); + if (!animal) { + ooc_release(dog); + return -1; + } + + /* + * Fields are reachable by name alone, with no struct definition in sight: + * "breed" is Dog's own field, "name" and "age" are inherited from Animal. + * + * ooc_get() hands back the address of the field, so a string field is read + * in two steps -- the slot first, the string it points to second. + */ + printf("%s is a %d year old %s.\n", + *(char **)ooc_get(dog, "name"), + *(int *)ooc_get(dog, "age"), + *(char **)ooc_get(dog, "breed")); + + /* ooc_set() copies a value into the named field. */ + if (ooc_set(dog, "age", &birthday) != 0) { + fprintf(stderr, "failed to set age\n"); + goto fail; + } + + printf("%s just turned %d.\n", *(char **)ooc_get(dog, "name"), + *(int *)ooc_get(dog, "age")); + + /* + * "name" is a field the class owns, so assigning a new one hands it over: + * the replacement is copied in and the string it displaces is released + * here, leaving nothing to free by hand. + * + * ooc_set() takes the address of the value, so a pointer field is assigned + * through a pointer to the pointer -- and the new value has to be heap + * memory rather than a literal, because the object would otherwise own + * something free() cannot release. + */ + name = copy_of("Bella"); + if (!name || ooc_set(dog, "name", &name) != 0) { + fprintf(stderr, "failed to set name\n"); + free(name); + goto fail; + } + + printf("%s was renamed.\n", *(char **)ooc_get(dog, "name")); + + /* An unknown name is refused rather than written out of bounds. */ + if (ooc_set(dog, "weight", &weight) != 0) + printf("no such field: weight\n"); + + /* + * A name starting with an underscore belongs to the class that declared + * it, so the setter turns it away. The field is still readable by name, + * which is what lets a class show its own state without handing out a way + * to change it -- note that "_id" is inherited from Animal, and the + * refusal follows the name to whichever class in the chain declared it. + */ + if (ooc_set(dog, "_id", &weight) != 0) + printf("_id is private, and reads back as %d\n", + *(int *)ooc_get(dog, "_id")); + + /* + * Two leading underscores go further and are not readable either: + * ooc_get() finds the field and then withholds it, so there is no way in + * from here at all. An accessor is the only way to the value, and both + * "_id" and "__legs" now report the same through the class instead. + */ + printf("__legs is readable by name: %s\n", + ooc_get(dog, "__legs") ? "yes" : "no"); + printf("__legs is writable by name: %s\n", + ooc_set(dog, "__legs", &weight) == 0 ? "yes" : "no"); + printf("and the class still reports %d legs, id %d.\n", + animal_legs((Animal *)dog), animal_id((Animal *)dog)); + + animal_speak(animal); + + ooc_release(dog); + ooc_release(animal); + + return 0; + +fail: + ooc_release(animal); + ooc_release(dog); + return -1; +} + +/* + * The Cat walk. + * + * The same calls as visit_dog(), against a second subclass, and that is the + * point of having it: nothing below is Cat-specific. Field lookup walks the + * chain to Animal for "name" and "age" and stops at Cat for "colour", the + * owned-field and private-field rules behave identically, and the destructor + * releases both strings because Cat registered its own. + * + * The runtime type is asked about explicitly at the end, in both directions. + * ooc_is_a() answers yes for the cat and for the Animal it derives from and no + * for its sibling, and the exact type comes from the object's own class + * member. + * + * Returns 0 when the walk completes and -1 if a step the example relies on was + * refused. + */ +static int visit_cat(void) +{ + Cat *cat; + Animal *animal; + char *colour; + int weight = 45; + + cat = cat_new("Mia", 3, "tabby"); + if (!cat) { + fprintf(stderr, "failed to create cat\n"); + return -1; + } + + /* A second reference again, this time on an object of another type. */ + animal = ooc_retain((Animal *)cat); + if (!animal) { + ooc_release(cat); + return -1; + } + + printf("%s is a %d year old %s with %d lives.\n", + *(char **)ooc_get(cat, "name"), + *(int *)ooc_get(cat, "age"), + *(char **)ooc_get(cat, "colour"), + *(int *)ooc_get(cat, "_lives")); + + /* + * "colour" is owned, exactly as Dog's breed is, so a replacement releases + * the string that was there. Same call, same rules, a field the subclass + * declared for itself. + */ + colour = copy_of("calico"); + if (!colour || ooc_set(cat, "colour", &colour) != 0) { + fprintf(stderr, "failed to set colour\n"); + free(colour); + goto fail; + } + + printf("%s is now a %s.\n", *(char **)ooc_get(cat, "name"), + *(char **)ooc_get(cat, "colour")); + + /* + * The underscore rules do not care which class in the chain declared the + * field. Cat's own `_lives` is readable and not writable, the same as + * Animal's `_id` reached through a Dog, and Animal's hidden `__legs` is + * still out of reach from here. + */ + if (ooc_set(cat, "_lives", &weight) != 0) + printf("_lives is private, and reads back as %d\n", + *(int *)ooc_get(cat, "_lives")); + printf("__legs is readable from a cat: %s\n", + ooc_get(cat, "__legs") ? "yes" : "no"); + printf("and the class still reports %d legs.\n", animal_legs(animal)); + + /* + * The runtime type is what the object carries, not what the cast says. + * + * ooc_is_a() walks the inheritance chain, so a cat answers yes for Cat and + * for the Animal it derives from, and no for its sibling Dog. Asking about + * a base class is the useful direction: it is how a caller holding an + * Animal * asks "is this one of mine" without knowing the answer in + * advance. + */ + printf("a cat is a Cat: %s, an Animal: %s, a Dog: %s\n", + ooc_is_a(cat, &Cat_class) ? "yes" : "no", + ooc_is_a(cat, &Animal_class) ? "yes" : "no", + ooc_is_a(cat, &Dog_class) ? "yes" : "no"); + + /* + * Asking "exactly which type" is a different question, and needs no chain + * walk: the class record is a public member of the object, so a single + * comparison answers it. This is also what a switch over a tag would use. + */ + printf("and it is exactly an Animal: %s\n", + cat->animal.object.class == &Animal_class ? "yes" : "no"); + + /* One call site, and the vtable decides which speak() runs. */ + animal_speak(animal); + + ooc_release(cat); + ooc_release(animal); + + return 0; + +fail: + ooc_release(animal); + ooc_release(cat); + return -1; +} + +int main(void) +{ + int status = visit_dog(); + + if (status == 0) + status = visit_cat(); + + return status != 0; +} diff --git a/include/ooc/ooc.h b/include/ooc/ooc.h new file mode 100644 index 0000000..54a163d --- /dev/null +++ b/include/ooc/ooc.h @@ -0,0 +1,279 @@ +/* + * 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 OOC_H +#define OOC_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif +typedef struct ooc_object ooc_object; + +typedef struct ooc_class ooc_class; + +typedef struct ooc_field ooc_field; + +/* + * Handle for an interface object, holding the address of its vtable. + * + * The library does not read this struct: it is the convention for pointing at + * a vtable of a known shape when the vtable's own type is not the caller's to + * see. An object offering an interface embeds this as its first member, and a + * caller casts the object to ooc_interface * to reach the vtable it names. + * `vtable` is const because the table it points to is shared by every instance + * of the class, not owned by any one of them. + */ +typedef struct ooc_interface { const void *vtable; } ooc_interface; + +/* + * Header stored at the very start of every object. + * + * `class` is the runtime type used for the virtual destructor and for dynamic + * field lookup, `refs` is the intrusive reference count. + * + * Because the library reaches both through this header, it has to be the first + * member of every object. A subclass embeds its base struct first rather than + * the header directly, which is what makes an upcast between them a no-op cast: + * both addresses are the same. Nothing else in the struct may precede it. + */ +struct ooc_object { + const ooc_class *class; + size_t refs; +}; + +/* + * Description of a single field that ooc_get() and ooc_set() can reach. + * + * A class publishes its own fields as a NULL-terminated array of these. + * `offset` and `size` locate the storage inside the object, so classes fill + * them in with offsetof() and sizeof() instead of hand-computed numbers. Fields + * must be nonempty, follow the object header, and fit in the declaring class. + * Invalid descriptors are refused by both accessors. Owned fields must hold + * exactly one object pointer compatible with free(). + * + * `owned` marks a field as a single pointer the class owns. ooc_set() then + * hands the field over to its new value instead of overwriting it blindly, + * and releases the pointer that was there before. It stays zero for a value + * that is merely copied around, such as an int or a pointer somebody else + * keeps hold of. A zero `owned` is the default, so a table that lists nothing + * but a name, an offset and a size is unchanged. + * + * A name starting with an underscore marks the field as belonging to the class + * that declares it, which is how a class keeps a member to itself in a language + * that has no access specifiers. One underscore hides the field from ooc_set(), + * so it can be read but not written by name; two hide it from both ooc_set() + * and ooc_get(), leaving the object the only thing that can reach the value. + */ +struct ooc_field { + const char *name; + size_t offset; + size_t size; + int owned; +}; + +/* + * Runtime type metadata. + * + * `size` is the size of the whole struct, subclasses included, and is what + * ooc_new() allocates; `destroy` is the virtual destructor ooc_release() calls + * once the last reference is gone, and is where a class releases whatever its + * constructor allocated. A NULL `destroy` is allowed and frees the object + * without further ado, which suits a class that owns nothing. + * + * `super` points at the base class, or NULL for a root class; field lookup + * follows it, so a derived class inherits the fields of its ancestors. A NULL + * `fields` array means the class publishes nothing dynamically. + * + * A class record is normally a file-scope constant. It is read but never + * written by the library. The record, its ancestors, field tables and names + * must remain alive and immutable until every referring object is released. + * The inheritance chain must be acyclic and each base must fit in its subclass. + */ +struct ooc_class { + size_t size; + void (*destroy)(ooc_object *); + const ooc_class *super; + const ooc_field *fields; +}; + +/* + * Create a zero-initialised instance of `class` and hand it to the caller. + * + * The object comes back with a reference count of one, so it is the caller's + * to release, and every member behind the header is zero, so a subclass only + * has to fill in what it cares about. No constructor runs: a subclass that + * allocates members of its own does that itself, and releases the half-built + * object if a later step fails. + * + * `class` must describe the class being instantiated and not one of its bases, + * since its `size` is what gets allocated. + * + * Returns NULL if `class` is NULL, is too small to hold an object header, + * has a cyclic inheritance chain or a base that does not fit, or allocation + * fails. There is nothing to release in that case. + */ +void *ooc_new(const ooc_class *class); + +/* + * Take an additional reference on `object` and return it unchanged. + * + * The object survives until the last reference is released, which is what lets + * a caller pass an object on without copying it. The returned pointer is + * `object` itself, so a reference can be handed out in one expression. + * + * NULL is passed through and counted as nothing, so an optional object may be + * retained unconditionally. Returns NULL without changing the count if it is + * zero (destruction in progress) or SIZE_MAX (no additional reference fits). + * Callers must check the result before treating it as a new reference. + * Reference counting and field access require external synchronization when + * an object is shared between threads. + * + * Only objects from ooc_new() are meant to be counted this way: a struct that + * merely embeds an ooc_object header has no allocation behind it to be freed + * with. + */ +void *ooc_retain(void *object); + +/* + * Drop one reference on `object` and free it once the last one is gone. + * + * The object's runtime destructor runs first, while the object's storage is + * still intact, and is responsible for releasing everything the subclass + * allocated; the library frees the object's own storage afterwards and nothing + * else. A class that allocates members therefore needs a `destroy` of its own, + * or those members leak. + * + * Destruction is not recursive and does not walk the class chain: the runtime + * type's destructor is the only one that runs, so a subclass frees what it + * inherited from its base itself. + * + * NULL and a zero count during destruction are ignored. Dead pointers cannot + * be validated here, so releasing an object that other references still point at frees it under a live + * pointer, and releasing one that is already gone is a use-after-free. + */ +void ooc_release(void *object); + +/* + * Drop one reference on `object` -- a synonym for ooc_release(). + * + * Spelled the way an operator delete would read, for callers who think in + * new/delete terms. It differs from ooc_release() in name only, so it does not + * destroy the object unconditionally but waits for the last reference to go, + * and NULL is ignored. + */ +void ooc_delete(void *object); + +/* + * Report whether `object` is an instance of `class` or of a subclass of it. + * + * The `super` chain is walked from the runtime type of the object, so a Cat + * answers true for Cat_class, for Animal_class and for anything in between, + * while a Dog answers false for Cat_class. This is how a caller holding an + * Animal * finds out what an object really is, since an upcast in C is + * unchecked, and how it asks "is this one of mine" about a base class. + * + * To ask whether an object is *exactly* one type rather than a subtype of it, + * compare the object's public `class` member directly: + * + * obj->class == &Animal_class + * + * A NULL object or a NULL `class` answers false. + */ +int ooc_is_a(const void *object, const ooc_class *class); + +/* + * Return a pointer to the storage of the named field, so callers can read or + * write a field they only know by name. + * + * The object is all that is needed: its runtime class says where the field + * sits, so the struct definition need not be visible to the caller. + * + * Lookup starts at the object's own class and continues up the inheritance + * chain, so a Dog resolves "name" through Animal. A field a subclass declares + * shadows a same-named field of its base, and because lookup starts at the + * runtime type, the same name can reach two different fields depending on the + * object -- the runtime type decides, not the static type of the pointer the + * object arrived as. + * + * The returned pointer is the field's real storage and is writable, so it + * stays valid for as long as the object does; the object therefore has to be a + * non-const object. A pointer field is read in two steps: the slot comes back + * first, the value it points to second. + * + * A field whose name starts with two underscores is out of reach: the lookup + * finds it, but the pointer is withheld and NULL comes back instead, so the + * value stays inside the class that declared it. One leading underscore does + * not hide a field, it only stops ooc_set() writing one. + * + * Writing through the returned pointer bypasses ooc_set(), so a field that + * declares itself owned is replaced through ooc_set() instead, or the value it + * held is not released. It also bypasses the underscore rule for a field that + * can be read in the first place, since the rule lives in ooc_set() rather than + * in the pointer returned here. + * + * Returns NULL if the object is NULL, the name is NULL, no class in the chain + * declares that field, the descriptor is invalid, or the field is hidden + * behind two leading underscores. + */ +void *ooc_get(void *object, const char *field); + +/* + * Copy the value pointed to by `value` into the named field. + * + * `value` is the address of the value, exactly like the source argument of + * memmove(), so overlapping source storage is allowed. A pointer field is + * assigned through a pointer to the pointer: + * ooc_set(dog, "name", &name). It must point to at least as many bytes as the + * field occupies, normally a variable of the field's declared type, and + * nothing beyond those bytes is read. + * + * The copy is all-or-nothing: a NULL argument, an unknown field name, or a + * field of size zero is reported instead of written. + * + * A field that declares itself owned behaves like an assignment in C++: the + * new value takes over and the old one goes away. ooc_set() copies the + * replacement in first and then calls free() on the displaced pointer, so + * source storage in the old allocation is readable until the copy finishes. + * A field that is assigned the very same pointer again is left alone, making + * it a no-op rather than a double free. + * + * Because ownership moves, the replacement must be NULL or memory from malloc() + * and must point to the start of that allocation (never into the old allocation), + * and once assigned no other field may be given the same pointer, or the same + * allocation would be freed twice. A field that does not declare itself owned + * is written blind and whatever it held stays the caller's to release. The + * destructor still releases the value an owned field holds when the object is + * destroyed. + * + * A field whose name starts with an underscore belongs to the class that + * declares it and is not written here: the call is refused with -1. That is + * the whole of a private member in a language without access specifiers. A name + * that only contains an underscore further along is an ordinary field. + * + * Two leading underscores are the stronger form and are refused here as well; + * those fields are also kept from ooc_get(), so nothing outside the class can + * read or write them. Either way the refusal follows the name to whichever + * class in the chain declares it, so a subclass cannot write a base class' + * private field either. A class reaches its own fields through the struct + * definition, where the members are in view and no accessor is needed. + * + * The descriptor of an owned field has to cover exactly one pointer. A wider + * or narrower one is reported with -1 rather than partly released. + * + * Returns 0 on success and -1 when the write is refused, which covers every + * case named above. + */ +int ooc_set(void *object, const char *field, const void *value); + +#ifdef __cplusplus +} +#endif +#endif diff --git a/libooc.pc.in b/libooc.pc.in new file mode 100644 index 0000000..1523193 --- /dev/null +++ b/libooc.pc.in @@ -0,0 +1,9 @@ +prefix=@prefix@ +exec_prefix=${prefix} +libdir=${exec_prefix}/lib +includedir=${prefix}/include +Name: libooc +Description: A tiny C library for object-oriented programming using ordinary C structs and function-pointer vtables. +Version: @VERSION@ +Libs: -L${libdir} -looc +Cflags: -I${includedir} diff --git a/src/ooc.c b/src/ooc.c new file mode 100644 index 0000000..656ba4a --- /dev/null +++ b/src/ooc.c @@ -0,0 +1,428 @@ +/* + * 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 + */ + +/* + * libooc -- a very small object-orientation layer for plain C. + * + * Provides allocation, virtual destruction, intrusive reference counting and + * name-based field access for objects that embed an `ooc_object` header and + * describe themselves with a runtime `ooc_class` record. + */ + +#include "ooc/ooc.h" + +#include +#include +#include + +/* + * Report whether a class record and its ancestors describe a usable hierarchy. + * + * Everything the runtime does with a class record -- allocating from its size, + * calling its destructor, walking its inheritance chain, resolving a field + * against it -- reads the record and the records it points at, so a malformed + * one is a memory-safety problem rather than a wrong answer. The checks here + * are what a caller of the API cannot be trusted to have got right, since a + * class record is hand-written data like any other. Metadata is expected to + * stay alive and immutable for as long as its objects exist, so this can only + * look at the shape of it. + * + * Three things are rejected. A NULL class has nothing to describe. A cycle in + * the `super` chain would make every walk of it loop forever, so the chain is + * checked with the tortoise-and-hare method before it is followed: one pointer + * steps once per iteration and the other twice, and a chain that loops brings + * them together. Finally every class on the chain has to be able to hold an + * object header, and a derived class may not be smaller than its base -- a base + * that does not fit inside the derived object would have its fields written + * past the end of the allocation. + * + * Returns 1 for a sound hierarchy and 0 otherwise. A chain that is merely + * broken in some other way, a class whose `size` does not match its struct for + * instance, cannot be detected from the record alone and is not checked. + */ +static int valid_class(const ooc_class *class) +{ + const ooc_class *slow = class; + const ooc_class *fast = class; + + if (!class) + return 0; + + /* Reject cycles before walking the chain. */ + while (fast && fast->super) { + slow = slow->super; + fast = fast->super->super; + if (slow == fast) + return 0; + } + + for (; class; class = class->super) { + if (class->size < sizeof(ooc_object) || + (class->super && class->super->size > class->size)) + return 0; + } + return 1; +} + +/* + * Report whether a field descriptor stays inside the object of `class`. + * + * A descriptor is the one piece of a class record that is turned into a real + * address -- `object + offset` -- and the width it claims is turned into a + * copy, so a descriptor whose numbers do not describe the struct gives out a + * pointer into unrelated memory and reads or writes through it. The descriptor + * is checked against the class it was found in, which is the object that will + * be addressed: a field inherited from a base is bounded by the derived class, + * which is the larger of the two. + * + * The rules are that the field occupies some bytes, sits at or after the end of + * the object header -- an offset of 0 would let a name-based write overwrite the + * runtime type and the reference count, and with them the object's own + * bookkeeping -- and ends within the object. That last test is written as a + * subtraction rather than as `offset + size <= class->size`, because adding + * first wraps around for a pair of large values and a wrapped sum would pass + * the check it was meant to fail. A field marked owned has to be exactly one + * pointer wide, since that is the width ooc_set() frees, and freeing part of a + * wider field or more than one of its pointers would corrupt the heap. + * + * Returns 1 for a descriptor that is safe to address and 0 otherwise. Note that + * a rejected field makes its *name* unusable: lookup stops at the first match + * and reports it as unknown, rather than continuing to an ancestor that happens + * to declare the same name. + */ +static int valid_field(const ooc_class *class, const ooc_field *field) +{ + /* Subtraction avoids overflow in offset + size. Protect the header too. */ + return field->size != 0 && field->offset >= sizeof(ooc_object) && + field->offset <= class->size && + field->size <= class->size - field->offset && + (!field->owned || field->size == sizeof(void *)); +} + +/* + * Create a zero-initialised instance of `class`. + * + * The object gets a header -- its runtime type and a reference count of one -- + * and calloc() zeroes everything behind it, so a subclass only has to fill in + * the members it actually cares about. No constructor runs, so a subclass that + * allocates members of its own does that itself, and releases the half-built + * object again if a later step fails. + * + * `class` has to describe the subclass and not one of its bases: `size` is + * what gets allocated, so a class reporting the size of its base hands out an + * object too small for the members declared below it. + * + * The new object starts out with a reference count of one, so ownership is + * handed to the caller, which must eventually pass it to `ooc_release`. + * + * Returns NULL if `class` is missing, is too small to hold an object header, + * or if the allocation fails. There is nothing to release in that case, since + * no object came into being. + */ +void *ooc_new(const ooc_class *class) +{ + ooc_object *object; + + if (!valid_class(class)) + return NULL; + + object = calloc(1, class->size); + if (!object) + return NULL; + + object->class = class; + object->refs = 1; + + return object; +} + +/* + * Take an additional reference on `object`. + * + * The count sits in the header at the very start of every object, so a + * reference can be taken through a pointer to any of its base classes: the + * upcast costs nothing and the count lands on the same header either way. The + * count is a number of references rather than of owners, which is what lets an + * object outlive the code that created it and die with the last holder. + * + * The object is returned unchanged, so a reference can be handed on in a + * single expression. Count zero or SIZE_MAX returns NULL without incrementing. + * NULL is passed through, so optional objects may be + * retained unconditionally. + * + * Only objects that came out of `ooc_new` are meant to be counted this way; a + * struct that merely embeds the header has no allocation behind it to survive + * or be freed with. + */ +void *ooc_retain(void *object) +{ + ooc_object *self = object; + + if (!self || self->refs == 0 || self->refs == SIZE_MAX) + return NULL; + + ++self->refs; + return object; +} + +/* + * Drop one reference on `object`. + * + * Once the last reference is gone the class' virtual destructor runs -- it is + * responsible for releasing everything the subclass allocated -- and the + * object itself is freed. NULL is ignored. + * + * Destruction is the counterpart of the reference `ooc_new` hands out, so every + * reference taken by `ooc_new` or `ooc_retain` wants one release back. The + * balance is the caller's to keep and cannot be checked from here: releasing an + * object whose other references are still in use frees it under a live pointer, + * and releasing one that is already gone is a use-after-free. + * + * Releasing is not recursive and does not climb the class chain by itself. The + * destructor of the runtime type is the one that runs, and a subclass that + * inherits members from a base has to free those itself, as the example's Dog + * does for Animal's name. + */ +void ooc_release(void *object) +{ + ooc_object *self = object; + + if (!self || self->refs == 0) + return; + + if (--self->refs == 0) { + if (self->class && self->class->destroy) + self->class->destroy(self); + + free(self); + } +} + +/* + * Drop one reference on `object`. + * + * Synonym for `ooc_release`, spelled the way an operator delete would read. + * + * The name is the only difference, and it is the caller's shorthand: this + * destroys nothing on its own but drops a reference, so the object goes away + * when the last one does, exactly as `ooc_release` behaves. NULL is ignored. + */ +void ooc_delete(void *object) +{ + ooc_release(object); +} + +/* + * Report whether `object` is an instance of `class` or of any subclass of it. + * + * The search walks the `super` chain from the runtime type of the object, so a + * Dog answers true for Animal_class as well as for Dog_class. That is the + * useful direction to answer in: an upcast in C is unchecked, so this is how a + * caller holding an Animal * finds out what the object really is, and asking + * about a base class is how it asks "is this one of mine". + * + * The converse test -- is this *exactly* this type -- is deliberately not what + * this function does. A class record is public in ooc_object, so a caller who + * needs the exact type compares obj->class against the record itself and gets + * the answer without a chain walk. + * + * A NULL object or a NULL class answers false, and a class whose chain is + * unsound is refused by the same validation ooc_new() applies, so a cyclic + * hierarchy cannot make this loop. The result is a plain yes-or-no. + */ +int ooc_is_a(const void *object, const ooc_class *class) +{ + const ooc_object *self = object; + const ooc_class *step; + + if (!self || !class) + return 0; + + if (!valid_class(self->class) || !valid_class(class)) + return 0; + + for (step = self->class; step; step = step->super) + if (step == class) + return 1; + + return 0; +} + +/* + * Find a field by name, starting at `class` and continuing up the inheritance + * chain. + * + * Fields declared by a class shadow same-named fields of its ancestors, and + * because the search starts at the runtime type of the object, the same name + * can reach two different fields depending on which class is asked. The + * descriptor that comes back belongs to the class and is const, so it stays + * valid for as long as that class does. + * + * This is a strcmp() per field along the chain, which is the point of a + * reflection layer and wasted effort wherever the struct definition is known + * at compile time and the member can simply be named. + * + * Returns NULL if either argument is NULL or no class in the chain knows the + * name. + */ +static const ooc_field *find_field(const ooc_class *class, const char *name) +{ + if (!name || !valid_class(class)) + return NULL; + + for (; class; class = class->super) { + const ooc_field *field; + + for (field = class->fields; field && field->name; field++) + if (strcmp(field->name, name) == 0) + return valid_field(class, field) ? field : NULL; + } + + return NULL; +} + +/* + * Report whether a field name is reserved for the class that declares it. + * + * A leading underscore marks a field as internal to the class, so callers + * outside it can look but not touch: the underscore is the convention that + * stands in for a private member in a language without access specifiers. The + * rule is a leading underscore, so both `_count` and `__count` are reserved, + * while a name that merely contains one further along is not. + */ +static int is_private_field(const char *name) +{ + return name && name[0] == '_'; +} + +/* + * Report whether a field name is out of reach even to read. + * + * Two leading underscores are the stronger form: a name reserved to the class + * on both sides, which ooc_get() will not hand out and ooc_set() will not + * write. Reading is refused along with writing, so what a class keeps under + * such a name stays inside it and the object is the only thing that can reach + * the value. + */ +static int is_hidden_field(const char *name) +{ + return name && name[0] == '_' && name[1] == '_'; +} + +/* + * Return the address of the named field's storage. + * + * The object is all that is needed: its runtime class says where the field + * sits, so a caller that has never seen the struct definition can still reach + * the field. Lookup starts at the object's own class and continues up the + * inheritance chain, so a field a subclass declares shadows a same-named field + * of its base -- the runtime type of the object decides, not the static type + * of the pointer it arrived as. + * + * The result is the field's real storage and not a copy of it, so it stays + * valid for as long as the object does and may be written through. A pointer + * field is therefore read in two steps: the slot comes back first, the value + * it points to second. Writing through the returned pointer sidesteps + * ooc_set(), so an owned field is replaced through ooc_set() instead, or the + * value it held is not released. + * + * A field whose name starts with two underscores is out of reach here: the + * lookup still finds it, but the pointer is withheld and NULL comes back + * instead, so the value stays inside the class that declared it. A single + * leading underscore does not hide a field, it only stops ooc_set() writing + * one. + * + * Returns NULL if the object is NULL, the name is NULL, no class in the chain + * declares the field, or the field is hidden behind two leading underscores. + */ +void *ooc_get(void *object, const char *field) +{ + const ooc_field *descriptor; + + if (!object) + return NULL; + + descriptor = find_field(((ooc_object *)object)->class, field); + if (!descriptor || is_hidden_field(descriptor->name)) + return NULL; + + return (char *)object + descriptor->offset; +} + +/* + * Copy the value pointed to by `value` into the named field. + * + * `value` is the address of the value, as with memcpy(), so a pointer field is + * assigned through a pointer to the pointer and a name is replaced by + * ooc_set(dog, "name", &name). Nothing is read out of `value` beyond the bytes + * the field occupies, which is why the caller's variable has to be of the + * field's declared type. + * + * A field that declares itself owned is handed over rather than overwritten: + * the copy lands first, so source storage in the old allocation stays readable + * until copying finishes, and the displaced pointer is freed afterwards + * unless the field is being assigned the very same pointer again. A field that + * does not declare itself owned is written blind, and whatever its old value + * was stays the caller's to release. + * + * A field whose name starts with an underscore belongs to the class that + * declares it, and writing it by name is refused, whether the name is reached + * through this call or was found in a base class. Reading it stays possible, + * so ooc_get() still reaches a private field. + * + * Returns 0 on success and -1 when the write is refused: a NULL object or + * value, a name no class in the chain knows, a name reserved to the class + * declaring it, a field of size zero, or an owned field whose descriptor does + * not cover exactly one pointer. + */ +int ooc_set(void *object, const char *field, const void *value) +{ + const ooc_field *descriptor; + + if (!object || !value) + return -1; + + descriptor = find_field(((ooc_object *)object)->class, field); + if (!descriptor || descriptor->size == 0) + return -1; + + if (is_private_field(descriptor->name)) + return -1; + + if (descriptor->owned) { + void *old; + void *replacement; + + /* + * An owned field is one pointer, so a descriptor covering more or + * less is refused here instead of having part of a wider field freed. + */ + if (descriptor->size != sizeof(old)) + return -1; + + /* + * The declared type of the field is not known here, so both pointers + * travel through memcpy() rather than through a cast. + */ + memcpy(&old, (char *)object + descriptor->offset, sizeof(old)); + memcpy(&replacement, value, sizeof(replacement)); + + memcpy((char *)object + descriptor->offset, &replacement, sizeof(replacement)); + + /* Assigning the same pointer back replaces nothing. */ + if (old != replacement) + free(old); + + return 0; + } + + memmove((char *)object + descriptor->offset, value, descriptor->size); + + return 0; +} diff --git a/tests/safety.c b/tests/safety.c new file mode 100644 index 0000000..358c357 --- /dev/null +++ b/tests/safety.c @@ -0,0 +1,296 @@ +/* + * 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 + */ + +/* + * libooc safety regression tests. + * + * 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 + * invalid memory accesses, invalid frees, and leaks. + */ + +#include +#include "cat.h" +#include "dog.h" + +#include +#include +#include +#include + +/* + * Object used by check_fields(). The byte array is copied by value; the owned + * pointer is freed on replacement and destruction. Both follow the header. + */ +struct sample { + ooc_object object; + char bytes[8]; + void *owned; +}; + +/* Counts destructor calls, so a test can tell a release from a no-op. */ +static int destroyed; + +/* + * Destructor for the sample object. Verifies that a zero reference count during + * destruction prevents retention and makes a nested release a no-op, then + * frees the owned allocation and records the destructor call. + */ +static void destroy(ooc_object *object) +{ + struct sample *self = (struct sample *)object; + + /* A destructor must not resurrect or recursively free the object. */ + assert(ooc_retain(object) == NULL); + ooc_release(object); + free(self->owned); + ++destroyed; +} + +/* + * Check field validation, overlapping copies, ownership, and reference counts. + * + * The table contains two valid descriptors and six invalid ones; the rejected + * names also include an absent field. Both accessors must reject fields in the + * header, out-of-bounds or overflowing ranges, zero-sized fields, and an owned + * field with the wrong pointer width. NULL names and source values are checked. + * + * Value copies cover identical and partially overlapping source storage; + * memcpy() would have undefined behavior for these overlapping ranges. Owned + * assignments cover the first allocation, assigning the slot back to itself, + * and replacement with a second allocation. Memory checkers can detect invalid + * frees or leaks that the return-value assertions alone cannot establish. + * + * Retaining at SIZE_MAX must fail without changing the count. After resetting + * the count to one, a successful retain raises it to two: the first release + * keeps the object alive and the second calls destroy() exactly once. + */ +static void check_fields(void) +{ + const ooc_field fields[] = { + { "bytes", offsetof(struct sample, bytes), 8, 0 }, + { "owned", offsetof(struct sample, owned), sizeof(void *), 1 }, + { "header", 0, sizeof(void *), 0 }, + { "past", sizeof(struct sample), 1, 0 }, + { "wrap", SIZE_MAX, 2, 0 }, + { "huge", offsetof(struct sample, bytes), SIZE_MAX, 0 }, + { "empty", offsetof(struct sample, bytes), 0, 0 }, + { "bad_owned", offsetof(struct sample, bytes), 1, 1 }, + { NULL, 0, 0, 0 } + }; + 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); + char initial[8] = "abcdefg"; + void *replacement = malloc(8); + void *second = malloc(8); + size_t i; + + assert(self && replacement && second); + assert(ooc_set(self, "bytes", initial) == 0); + assert(ooc_set(self, "bytes", self->bytes) == 0); + /* Source partially overlaps the destination but fits in the object. */ + assert(ooc_set(self, "bytes", self->bytes + 1) == 0); + assert(memcmp(self->bytes, "bcdefg\0", 7) == 0); + for (i = 0; i < sizeof(invalid) / sizeof(invalid[0]); ++i) { + assert(ooc_get(self, invalid[i]) == NULL); + assert(ooc_set(self, invalid[i], initial) == -1); + } + assert(ooc_get(self, NULL) == NULL); + assert(ooc_set(self, "bytes", NULL) == -1); + assert(ooc_set(self, "owned", &replacement) == 0); + assert(ooc_set(self, "owned", ooc_get(self, "owned")) == 0); + assert(ooc_set(self, "owned", &second) == 0); + assert(self->owned == second); + self->object.refs = SIZE_MAX; + assert(ooc_retain(self) == NULL); + assert(self->object.refs == SIZE_MAX); + self->object.refs = 1; + assert(ooc_retain(self) == self); + ooc_release(self); + assert(destroyed == 0); + ooc_release(self); + assert(destroyed == 1); +} + +/* + * Check rejection of NULL or undersized classes, a base larger than its + * subclass, a two-class cycle, and a self-cycle. Field lookup through the + * two-class cycle must also return NULL instead of looping indefinitely, and + * ooc_is_a() through a self-cycle must answer false instead of looping, since + * it walks the chain. + * + * Stack metadata is changed only between calls, while no allocated objects + * refer to it. A synthetic stack header exercises narrowly defined guards: + * a NULL class cannot match ooc_is_a(), retaining a zero count fails, and + * releasing a zero count or NULL does nothing. These checks do not imply that + * arbitrary stack objects or invalid pointers are supported by the runtime. + */ +static void check_classes(void) +{ + ooc_class first = { sizeof(struct sample), NULL, NULL, NULL }; + ooc_class second = { sizeof(struct sample), NULL, &first, NULL }; + ooc_object fake = { NULL, 0 }; + + assert(ooc_new(NULL) == NULL); + first.size = sizeof(ooc_object) - 1; + assert(ooc_new(&first) == NULL); + first.size = sizeof(struct sample) + 1; + assert(ooc_new(&second) == NULL); + first.size = sizeof(struct sample); + first.super = &second; + assert(ooc_new(&first) == NULL); + fake.class = &first; + assert(ooc_get(&fake, "missing") == NULL); + first.super = &first; + assert(ooc_new(&first) == NULL); + /* A self-referential chain must answer rather than loop, now that a type + check walks it. */ + assert(!ooc_is_a(&fake, &first)); + fake.class = NULL; + assert(!ooc_is_a(&fake, NULL)); + assert(ooc_retain(&fake) == NULL); + ooc_release(&fake); + ooc_release(NULL); +} + +/* + * Check inherited field lookup and access restrictions on a constructed Dog: + * age resolves to Animal's storage, _id refuses writes, and __legs refuses + * reads. NULL and repeated animal_init() calls must fail, leaving the existing + * name unchanged on repeated initialization. + * + * Setting the owned name and breed to NULL must succeed. Speaking afterwards + * exercises NULL-safe output, then releasing the dog exercises cleanup. The + * final constructor checks reject a NULL name and a NULL breed; the latter + * fails after base initialization and must clean up the partially built dog. + * Memory checkers verify that these paths do not leak or free invalid storage. + */ +static void check_example(void) +{ + Dog *dog = dog_new("Rex", 5, "Shepherd"); + char *empty = NULL; + int value = 9; + assert(dog); + assert(ooc_get(dog, "age") == &dog->animal.age); + assert(ooc_set(dog, "_id", &value) == -1); + assert(ooc_get(dog, "__legs") == NULL); + assert(animal_init(NULL, "name", 1) == -1); + assert(animal_init(&dog->animal, "again", 1) == -1); + assert(strcmp(dog->animal.name, "Rex") == 0); + assert(ooc_set(dog, "name", &empty) == 0); + assert(ooc_set(dog, "breed", &empty) == 0); + animal_speak(&dog->animal); + ooc_release(dog); + assert(animal_new(NULL, 0) == NULL); + assert(dog_new("name", 0, NULL) == NULL); +} + +/* + * Check a Cat and a Dog living side by side, which is the case the second + * subclass exists to cover. + * + * A Cat resolves "colour" to its own storage while "age" still resolves to + * Animal's, and its own "_lives" refuses writes while remaining readable, so + * the underscore rules are checked on a field the subclass declared rather than + * one it inherited. The hidden "__legs" stays out of reach from either object. + * + * Replacing the owned colour with a second allocation must release the first; + * only a memory checker can see that the string cat_new() allocated is gone + * rather than leaked. The replacement is heap memory, since a stack buffer + * handed to an owned field would be freed by the destructor. + * + * ooc_is_a() answers for a base class as well as for the runtime type, so a Cat + * is a Cat and an Animal but not a Dog, while the exact type is read from the + * object's own class member. Both objects are then spoken through the same + * Animal pointer, where the printed lines are the evidence that each reached + * its own vtable. Clearing both owned strings to NULL makes the speak + * implementations fall back instead of passing NULL to printf(), and releasing + * both objects afterwards must free what is left without a double free. + * + * The final constructor check rejects a NULL colour, which fails after base + * initialization and must clean up the partially built cat. + */ +static void check_subclasses(void) +{ + Dog *dog = dog_new("Rex", 5, "Shepherd"); + Cat *cat = cat_new("Mia", 3, "tabby"); + Animal *dog_view; + Animal *cat_view; + char *colour = malloc(sizeof("calico")); + char *empty = NULL; + int value = 3; + + assert(dog && cat && colour); + memcpy(colour, "calico", sizeof("calico")); + + /* A subclass field and an inherited one, both resolved to real storage. */ + assert(ooc_get(cat, "colour") == &cat->colour); + assert(ooc_get(cat, "age") == &cat->animal.age); + assert(ooc_get(cat, "__legs") == NULL); + assert(ooc_get(dog, "__legs") == NULL); + + /* The subclass's own private field: readable, refused to write, unchanged. */ + assert(*(int *)ooc_get(cat, "_lives") == 9); + assert(ooc_set(cat, "_lives", &value) == -1); + assert(*(int *)ooc_get(cat, "_lives") == 9); + + /* Owned replacement on a subclass field releases the first allocation. */ + assert(ooc_set(cat, "colour", &colour) == 0); + assert(strcmp(cat->colour, "calico") == 0); + assert(ooc_get(cat, "colour") == &cat->colour); + + /* Subtype test walks the chain; the exact type comes from `class`. */ + assert(ooc_is_a(cat, &Cat_class)); + assert(ooc_is_a(cat, &Animal_class)); + assert(!ooc_is_a(cat, &Dog_class)); + assert(ooc_is_a(dog, &Dog_class)); + assert(ooc_is_a(dog, &Animal_class)); + assert(!ooc_is_a(dog, &Cat_class)); + assert(cat->animal.object.class == &Cat_class); + assert(dog->animal.object.class != &Animal_class); + + /* One call site, two vtables: each object answers in its own voice. */ + dog_view = ooc_retain((Animal *)dog); + cat_view = ooc_retain((Animal *)cat); + assert(dog_view && cat_view); + animal_speak(dog_view); + animal_speak(cat_view); + + /* NULL-safe speak output, then cleanup of both objects. */ + assert(ooc_set(cat, "colour", &empty) == 0); + assert(ooc_set(cat, "name", &empty) == 0); + assert(ooc_set(dog, "breed", &empty) == 0); + animal_speak(cat_view); + ooc_release(cat_view); + ooc_release(cat); + ooc_release(dog_view); + ooc_release(dog); + + assert(cat_new("name", 0, NULL) == NULL); +} + +/* + * 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. + */ +int main(void) +{ + check_fields(); + check_classes(); + check_example(); + check_subclasses(); + return 0; +}