Initial commit.
This commit is contained in:
commit
f59561fdfb
15 changed files with 2516 additions and 0 deletions
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
|
||||
Loading…
Add table
Add a link
Reference in a new issue