Initial commit.

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

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

@ -0,0 +1,279 @@
/*
* This file is part of libooc.
* https://xw3.org/hanez/libooc
*
* Copyright 2026 Johannes Findeisen <you@hanez.org>
* Licensed under the terms of the Apache-2.0 license.
* https://opensource.org/license/apache-2-0
*/
#ifndef OOC_H
#define OOC_H
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef struct ooc_object ooc_object;
typedef struct ooc_class ooc_class;
typedef struct ooc_field ooc_field;
/*
* Handle for an interface object, holding the address of its vtable.
*
* The library does not read this struct: it is the convention for pointing at
* a vtable of a known shape when the vtable's own type is not the caller's to
* see. An object offering an interface embeds this as its first member, and a
* caller casts the object to ooc_interface * to reach the vtable it names.
* `vtable` is const because the table it points to is shared by every instance
* of the class, not owned by any one of them.
*/
typedef struct ooc_interface { const void *vtable; } ooc_interface;
/*
* Header stored at the very start of every object.
*
* `class` is the runtime type used for the virtual destructor and for dynamic
* field lookup, `refs` is the intrusive reference count.
*
* Because the library reaches both through this header, it has to be the first
* member of every object. A subclass embeds its base struct first rather than
* the header directly, which is what makes an upcast between them a no-op cast:
* both addresses are the same. Nothing else in the struct may precede it.
*/
struct ooc_object {
const ooc_class *class;
size_t refs;
};
/*
* Description of a single field that ooc_get() and ooc_set() can reach.
*
* A class publishes its own fields as a NULL-terminated array of these.
* `offset` and `size` locate the storage inside the object, so classes fill
* them in with offsetof() and sizeof() instead of hand-computed numbers. Fields
* must be nonempty, follow the object header, and fit in the declaring class.
* Invalid descriptors are refused by both accessors. Owned fields must hold
* exactly one object pointer compatible with free().
*
* `owned` marks a field as a single pointer the class owns. ooc_set() then
* hands the field over to its new value instead of overwriting it blindly,
* and releases the pointer that was there before. It stays zero for a value
* that is merely copied around, such as an int or a pointer somebody else
* keeps hold of. A zero `owned` is the default, so a table that lists nothing
* but a name, an offset and a size is unchanged.
*
* A name starting with an underscore marks the field as belonging to the class
* that declares it, which is how a class keeps a member to itself in a language
* that has no access specifiers. One underscore hides the field from ooc_set(),
* so it can be read but not written by name; two hide it from both ooc_set()
* and ooc_get(), leaving the object the only thing that can reach the value.
*/
struct ooc_field {
const char *name;
size_t offset;
size_t size;
int owned;
};
/*
* Runtime type metadata.
*
* `size` is the size of the whole struct, subclasses included, and is what
* ooc_new() allocates; `destroy` is the virtual destructor ooc_release() calls
* once the last reference is gone, and is where a class releases whatever its
* constructor allocated. A NULL `destroy` is allowed and frees the object
* without further ado, which suits a class that owns nothing.
*
* `super` points at the base class, or NULL for a root class; field lookup
* follows it, so a derived class inherits the fields of its ancestors. A NULL
* `fields` array means the class publishes nothing dynamically.
*
* A class record is normally a file-scope constant. It is read but never
* written by the library. The record, its ancestors, field tables and names
* must remain alive and immutable until every referring object is released.
* The inheritance chain must be acyclic and each base must fit in its subclass.
*/
struct ooc_class {
size_t size;
void (*destroy)(ooc_object *);
const ooc_class *super;
const ooc_field *fields;
};
/*
* Create a zero-initialised instance of `class` and hand it to the caller.
*
* The object comes back with a reference count of one, so it is the caller's
* to release, and every member behind the header is zero, so a subclass only
* has to fill in what it cares about. No constructor runs: a subclass that
* allocates members of its own does that itself, and releases the half-built
* object if a later step fails.
*
* `class` must describe the class being instantiated and not one of its bases,
* since its `size` is what gets allocated.
*
* Returns NULL if `class` is NULL, is too small to hold an object header,
* has a cyclic inheritance chain or a base that does not fit, or allocation
* fails. There is nothing to release in that case.
*/
void *ooc_new(const ooc_class *class);
/*
* Take an additional reference on `object` and return it unchanged.
*
* The object survives until the last reference is released, which is what lets
* a caller pass an object on without copying it. The returned pointer is
* `object` itself, so a reference can be handed out in one expression.
*
* NULL is passed through and counted as nothing, so an optional object may be
* retained unconditionally. Returns NULL without changing the count if it is
* zero (destruction in progress) or SIZE_MAX (no additional reference fits).
* Callers must check the result before treating it as a new reference.
* Reference counting and field access require external synchronization when
* an object is shared between threads.
*
* Only objects from ooc_new() are meant to be counted this way: a struct that
* merely embeds an ooc_object header has no allocation behind it to be freed
* with.
*/
void *ooc_retain(void *object);
/*
* Drop one reference on `object` and free it once the last one is gone.
*
* The object's runtime destructor runs first, while the object's storage is
* still intact, and is responsible for releasing everything the subclass
* allocated; the library frees the object's own storage afterwards and nothing
* else. A class that allocates members therefore needs a `destroy` of its own,
* or those members leak.
*
* Destruction is not recursive and does not walk the class chain: the runtime
* type's destructor is the only one that runs, so a subclass frees what it
* inherited from its base itself.
*
* NULL and a zero count during destruction are ignored. Dead pointers cannot
* be validated here, so releasing an object that other references still point at frees it under a live
* pointer, and releasing one that is already gone is a use-after-free.
*/
void ooc_release(void *object);
/*
* Drop one reference on `object` -- a synonym for ooc_release().
*
* Spelled the way an operator delete would read, for callers who think in
* new/delete terms. It differs from ooc_release() in name only, so it does not
* destroy the object unconditionally but waits for the last reference to go,
* and NULL is ignored.
*/
void ooc_delete(void *object);
/*
* Report whether `object` is an instance of `class` or of a subclass of it.
*
* The `super` chain is walked from the runtime type of the object, so a Cat
* answers true for Cat_class, for Animal_class and for anything in between,
* while a Dog answers false for Cat_class. This is how a caller holding an
* Animal * finds out what an object really is, since an upcast in C is
* unchecked, and how it asks "is this one of mine" about a base class.
*
* To ask whether an object is *exactly* one type rather than a subtype of it,
* compare the object's public `class` member directly:
*
* obj->class == &Animal_class
*
* A NULL object or a NULL `class` answers false.
*/
int ooc_is_a(const void *object, const ooc_class *class);
/*
* Return a pointer to the storage of the named field, so callers can read or
* write a field they only know by name.
*
* The object is all that is needed: its runtime class says where the field
* sits, so the struct definition need not be visible to the caller.
*
* Lookup starts at the object's own class and continues up the inheritance
* chain, so a Dog resolves "name" through Animal. A field a subclass declares
* shadows a same-named field of its base, and because lookup starts at the
* runtime type, the same name can reach two different fields depending on the
* object -- the runtime type decides, not the static type of the pointer the
* object arrived as.
*
* The returned pointer is the field's real storage and is writable, so it
* stays valid for as long as the object does; the object therefore has to be a
* non-const object. A pointer field is read in two steps: the slot comes back
* first, the value it points to second.
*
* A field whose name starts with two underscores is out of reach: the lookup
* finds it, but the pointer is withheld and NULL comes back instead, so the
* value stays inside the class that declared it. One leading underscore does
* not hide a field, it only stops ooc_set() writing one.
*
* Writing through the returned pointer bypasses ooc_set(), so a field that
* declares itself owned is replaced through ooc_set() instead, or the value it
* held is not released. It also bypasses the underscore rule for a field that
* can be read in the first place, since the rule lives in ooc_set() rather than
* in the pointer returned here.
*
* Returns NULL if the object is NULL, the name is NULL, no class in the chain
* declares that field, the descriptor is invalid, or the field is hidden
* behind two leading underscores.
*/
void *ooc_get(void *object, const char *field);
/*
* Copy the value pointed to by `value` into the named field.
*
* `value` is the address of the value, exactly like the source argument of
* memmove(), so overlapping source storage is allowed. A pointer field is
* assigned through a pointer to the pointer:
* ooc_set(dog, "name", &name). It must point to at least as many bytes as the
* field occupies, normally a variable of the field's declared type, and
* nothing beyond those bytes is read.
*
* The copy is all-or-nothing: a NULL argument, an unknown field name, or a
* field of size zero is reported instead of written.
*
* A field that declares itself owned behaves like an assignment in C++: the
* new value takes over and the old one goes away. ooc_set() copies the
* replacement in first and then calls free() on the displaced pointer, so
* source storage in the old allocation is readable until the copy finishes.
* A field that is assigned the very same pointer again is left alone, making
* it a no-op rather than a double free.
*
* Because ownership moves, the replacement must be NULL or memory from malloc()
* and must point to the start of that allocation (never into the old allocation),
* and once assigned no other field may be given the same pointer, or the same
* allocation would be freed twice. A field that does not declare itself owned
* is written blind and whatever it held stays the caller's to release. The
* destructor still releases the value an owned field holds when the object is
* destroyed.
*
* A field whose name starts with an underscore belongs to the class that
* declares it and is not written here: the call is refused with -1. That is
* the whole of a private member in a language without access specifiers. A name
* that only contains an underscore further along is an ordinary field.
*
* Two leading underscores are the stronger form and are refused here as well;
* those fields are also kept from ooc_get(), so nothing outside the class can
* read or write them. Either way the refusal follows the name to whichever
* class in the chain declares it, so a subclass cannot write a base class'
* private field either. A class reaches its own fields through the struct
* definition, where the members are in view and no accessor is needed.
*
* The descriptor of an owned field has to cover exactly one pointer. A wider
* or narrower one is reported with -1 rather than partly released.
*
* Returns 0 on success and -1 when the write is refused, which covers every
* case named above.
*/
int ooc_set(void *object, const char *field, const void *value);
#ifdef __cplusplus
}
#endif
#endif