Initial commit.
This commit is contained in:
commit
f59561fdfb
15 changed files with 2516 additions and 0 deletions
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
build/
|
||||||
|
pkg/
|
||||||
|
|
||||||
13
LICENSE
Normal file
13
LICENSE
Normal 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
78
Makefile
Normal 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
225
README.md
Normal 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
274
example/animal.c
Normal 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
107
example/animal.h
Normal 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
215
example/cat.c
Normal 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
67
example/cat.h
Normal 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
192
example/dog.c
Normal 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
57
example/dog.h
Normal 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
271
example/main.c
Normal 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
279
include/ooc/ooc.h
Normal 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
9
libooc.pc.in
Normal 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
428
src/ooc.c
Normal 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
296
tests/safety.c
Normal 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;
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue