Initial commit.

This commit is contained in:
Johannes Findeisen 2026-10-02 00:08:18 +02:00
commit f59561fdfb
15 changed files with 2516 additions and 0 deletions

5
.gitignore vendored Normal file
View file

@ -0,0 +1,5 @@
.idea/
build/
pkg/

13
LICENSE Normal file
View file

@ -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.

78
Makefile Normal file
View file

@ -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

225
README.md Normal file
View file

@ -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 <ooc/ooc.h>
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

274
example/animal.c Normal file
View file

@ -0,0 +1,274 @@
/*
* This file is part of libooc.
* https://xw3.org/hanez/libooc
*
* Copyright 2026 Johannes Findeisen <you@hanez.org>
* Licensed under the terms of the Apache-2.0 license.
* https://opensource.org/license/apache-2-0
*/
/*
* Animal -- the base class of the example.
*
* It owns a copy of its name, hands ownership of every instance to the
* caller, and forwards `speak` to the vtable so that subclasses can override
* the behaviour.
*/
#include "animal.h"
#include <limits.h>
#include <stddef.h>
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
/*
* Copy `text` onto the heap; the caller owns the result.
*
* A class that holds a string allocates it once here and hands the copy to the
* field, which is why the destructor has something to free. The copy is
* independent of `text`, so a name built from a stack buffer or a literal
* outlives the call.
*
* Returns NULL if `text` is NULL or the allocation fails, which the callers
* here treat as a failed construction. The terminator is copied along, so the
* result is a C string and nothing has to be added to it.
*/
static char *dupstr(const char *text)
{
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);
}

107
example/animal.h Normal file
View file

@ -0,0 +1,107 @@
/*
* This file is part of libooc.
* https://xw3.org/hanez/libooc
*
* Copyright 2026 Johannes Findeisen <you@hanez.org>
* Licensed under the terms of the Apache-2.0 license.
* https://opensource.org/license/apache-2-0
*/
#ifndef ANIMAL_H
#define ANIMAL_H
#include <ooc/ooc.h>
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 */

215
example/cat.c Normal file
View file

@ -0,0 +1,215 @@
/*
* This file is part of libooc.
* https://xw3.org/hanez/libooc
*
* Copyright 2026 Johannes Findeisen <you@hanez.org>
* Licensed under the terms of the Apache-2.0 license.
* https://opensource.org/license/apache-2-0
*/
/*
* 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 <stddef.h>
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
/*
* Copy `text` onto the heap; the caller owns the result.
*
* A private copy of 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;
}

67
example/cat.h Normal file
View file

@ -0,0 +1,67 @@
/*
* This file is part of libooc.
* https://xw3.org/hanez/libooc
*
* Copyright 2026 Johannes Findeisen <you@hanez.org>
* Licensed under the terms of the Apache-2.0 license.
* https://opensource.org/license/apache-2.0
*/
#ifndef 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 */

192
example/dog.c Normal file
View file

@ -0,0 +1,192 @@
/*
* This file is part of libooc.
* https://xw3.org/hanez/libooc
*
* Copyright 2026 Johannes Findeisen <you@hanez.org>
* Licensed under the terms of the Apache-2.0 license.
* https://opensource.org/license/apache-2-0
*/
/*
* 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 <stddef.h>
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
/*
* Copy `text` onto the heap; the caller owns the result.
*
* A private copy of 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;
}

57
example/dog.h Normal file
View file

@ -0,0 +1,57 @@
/*
* This file is part of libooc.
* https://xw3.org/hanez/libooc
*
* Copyright 2026 Johannes Findeisen <you@hanez.org>
* Licensed under the terms of the Apache-2.0 license.
* https://opensource.org/license/apache-2-0
*/
#ifndef 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 */

271
example/main.c Normal file
View file

@ -0,0 +1,271 @@
/*
* This file is part of libooc.
* https://xw3.org/hanez/libooc
*
* Copyright 2026 Johannes Findeisen <you@hanez.org>
* Licensed under the terms of the Apache-2.0 license.
* https://opensource.org/license/apache-2.0
*/
/*
* 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 <ooc/ooc.h>
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
/* 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;
}

279
include/ooc/ooc.h Normal file
View file

@ -0,0 +1,279 @@
/*
* This file is part of libooc.
* https://xw3.org/hanez/libooc
*
* Copyright 2026 Johannes Findeisen <you@hanez.org>
* Licensed under the terms of the Apache-2.0 license.
* https://opensource.org/license/apache-2-0
*/
#ifndef OOC_H
#define OOC_H
#include <stddef.h>
#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

9
libooc.pc.in Normal file
View file

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

428
src/ooc.c Normal file
View file

@ -0,0 +1,428 @@
/*
* This file is part of libooc.
* https://xw3.org/hanez/libooc
*
* Copyright 2026 Johannes Findeisen <you@hanez.org>
* Licensed under the terms of the Apache-2.0 license.
* https://opensource.org/license/apache-2-0
*/
/*
* 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 <stdint.h>
#include <stdlib.h>
#include <string.h>
/*
* 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;
}

296
tests/safety.c Normal file
View file

@ -0,0 +1,296 @@
/*
* This file is part of libooc.
* https://xw3.org/hanez/libooc
*
* Copyright 2026 Johannes Findeisen <you@hanez.org>
* Licensed under the terms of the Apache-2.0 license.
* https://opensource.org/license/apache-2-0
*/
/*
* 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 <ooc/ooc.h>
#include "cat.h"
#include "dog.h"
#include <assert.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
/*
* 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;
}