Extendible
Synopsis
#include "tscore/Extendible.h"
Extendible
allows Plugins to append additional storage to Core data structures and interface like a map or dictionary. Each additional field is declared during init, so that a custom allocator can malloc one block for the Datatype and its extended fields.
In C++, the template<typename Derived_t, typename Field_t> FieldId
are strongly typed field handles, which allows you to use Extendible in multiple inheritance, and at many levels of inheritance hierarchy, with compile time type safety.
Use Case:
- TSCore
Defines class
Host
asExtendible
- TSPlugin
HealthStatus
Extend the
Host
datatype with field<int> down reason code
. API returns a handle.Use Data and handle to read & write fields.
Description
A data class that inherits from Extendible, uses a CRTP (Curiously Recurring Template Pattern)
so that its static Schema
instance is unique among other Extendible
types. Thus all instances of the type implicitly know memory layout of the fields.
class ExtendibleExample : public ext::Extendible<ExtendibleExample> {
int real_member = 0;
}
The documentation and code refers to the Derived type as the class that is inheriting from an Extendible
.
During system init, code and plugins add fields to the Extendible
’s schema. This will update the Memory Layout of the schema,
and the memory offsets of all fields. The schema does not know the field’s type, but it stores the byte size and creates std::functions of
the type’s constructor, destructor, and serializer. And to avoid corruption, the code asserts that no instances are in use when adding fields.
ext::FieldId<ExtendibleExample,int> fld_my_int;
void PluginInit() {
fld_my_int = ext::fieldAdd(fld_my_int, "my_plugin_int");
}
When an derived class is instantiated, template<> create()
will allocate a block of memory for the derived class and all added
fields. The only memory overhead per instance is an uint16 used as a offset to the start of the extendible block. Then the constructor of the class
is called, followed by the constructors of each extendible field.
ExtendibleExample* alloc_example() {
return ext::create<ExtendibleExample>();
}
Memory Layout
One block of memory is allocated per Extendible
, which include all member variables and extended fields.
Within the block, memory is arranged in the following order:
Derived members (+padding align next field)
Fields (largest to smallest)
Packed Bits
When using inheritance, all base cases arranged from most super to most derived,
then all Extendible
blocks are arranged from most super to most derived.
If the fields are aligned, padding will be inserted where needed.
Strongly Typed Fields
template<typename Derived_t, typename Field_t> FieldId
template<typename Derived_t, typename Field_t> FieldId
is a templated Field_t
reference. One benefit is that all type casting is internal to the Extendible
,
which simplifies the code using it. Also this provides compile errors for common misuses and type mismatches.
// Core code
class Food : public ext::Extendible<Food> {};
class Car : public ext::Extendible<Car> {};
// Example Plugin
ext::FieldId<Food,float> fld_food_weight;
ext::FieldId<Food,time_t> fld_expr_date;
ext::FieldId<Car,float> fld_max_speed;
ext::FieldId<Car,float> fld_car_weight;
PluginInit() {
ext::addField(fld_food_weight, "weight");
ext::addField(fld_expr_date,"expire date");
ext::addField(fld_max_speed,"max_speed");
ext::addField(fld_car_weight,"weight"); // 'weight' is unique within 'Car'
}
PluginFunc() {
Food *banana = ext::create<Food>();
Car *camry = ext::create<Car>();
// Common user errors
float expire_date = ext::get(banana,fld_expr_date);
//^^^
// Compile error: cannot convert time_t to float
float speed = ext::get(banana,fld_max_speed);
// ^^^^^^^^^^^^^
// Compile error: Cannot convert banana to type Extendible<Car>
float weight = ext::get(camry,fld_food_weight);
// ^^^^^^^^^^^^^^^
// Compile error: Cannot convert camry to type Extendible<Food>, even though Car and Food each have a 'weight' field, the FieldId is strongly typed.
}
// Inheritance Example
class Fruit : Food, Extendible<Fruit> {
using super_type = Food;
};
ext::FieldId<Fruit,has_seeds> fld_has_seeds;
Fruit.schema.addField(fld_has_seeds, "has_seeds");
Fruit mango = ext::create<Fruit>();
ext::set(mango, fld_has_seeds) = true; // converts mango to Extendible<Fruit>
ext::set(mango, fld_food_weight) = 2; // converts mango to Extendible<Food>
ext::set(mango, fld_max_speed) = 9;
// ^^^^^^^^^^^^^
// Compile error: Cannot convert mango to type Extendible<Car>
Inheritance
Unfortunately it is non-trivial handle multiple Extendible
super types in the same inheritance
tree. template<> create()
handles allocation and initialization of the entire Derived
class, but it is dependent on each class defining using super_type = *some_super_class*;
so
that it recurse through the classes.
struct A : public Extendible<A> {
uint16_t a = {1};
};
struct B : public A {
using super_type = A;
uint16_t b = {2};
};
struct C : public B, public Extendible<C> {
using super_type = B;
uint16_t c = {3};
};
ext::FieldId<A, atomic<uint16_t>> ext_a_1;
ext::FieldId<C, uint16_t> ext_c_1;
C &x = *(ext::create<C>());
ext::viewFormat(x);
viewFormat()
prints a diagram of the position and size of bytes used within the allocated
memory.
1A | EXT | 2b | ##________##__ |
1A | BASE | 2b | __##__________ |
1B | BASE | 2b | ____##________ |
1C | EXT | 2b | ______##____## |
1C | BASE | 2b | ________##____ |
See src/tscore/unit_tests/test_Extendible.cc for more examples.
Reference
Namespace ext
-
template<typename Derived_t, typename Field_t>
class FieldId The handle used to access a field. These are templated to prevent human error, and branching logic.
- Template Parameters:
Derived_t – The class that you want to extend at runtime.
Field_t – The type of the field.
-
typedef const void *ExtFieldContext
The handle used to access a field through C API. Human error not allowed by convention.
-
template<typename Derived_t>
class Extendible Allocates block of memory, uses
template<typename Derived_t, typename Field_t> FieldId
andSchema
to access slices of memory.- Template Parameters:
Derived_t – The class that you want to extend at runtime.
-
static Schema schema
one schema instance per
Extendible
to define containedFieldDesc
-
template<typename Derived_t>
Extendible *create() To be used in place of new Derived_t(). Allocate a block of memory. Construct the base data. Recursively construct and initialize Derived_t::super_type and its
Extendible
classes.- Template Parameters:
Derived_t – The Derived class to allocate.
-
template<typename Derived_t, typename Field_t>
bool fieldAdd(FieldId<Derived_t, Field_t> &field_id, std::string const &field_name) Declare a new
template<typename Derived_t, typename Field_t> FieldId
for Derived_t.- Template Parameters:
Derived_t – The class that uses this field.
Field_t – The type of the field.
-
template<typename Derived_t, typename Field_t>
bool fieldFind(FieldId<Derived_t, Field_t> &field_id, std::string const &field_name) Find an existing
template<typename Derived_t, typename Field_t> FieldId
for Derived_t.- Template Parameters:
Derived_t – The class that uses this field.
Field_t – The type of the field.
-
template<typename T, typename Derived_t, typename Field_t>
auto const get(T const&, FieldId<Derived_t, Field_t>) Returns T const& value from the field stored in the
Extendible
allocation.- Template Parameters:
T – The class passed in.
Derived_t – The class that uses this field.
Field_t – The type of the field.
-
template<typename T, typename Derived_t, typename Field_t>
T &set(T&, FieldId<Derived_t, Field_t>) Returns T & value from the field stored in
Extendible
allocation.- Template Parameters:
T – The class passed in.
Derived_t – The class that uses this field.
Field_t – The type of the field.
-
template<typename Derived_t>
size_t sizeOf() Recurse through super classes and sum memory needed for allocation.
Depends on usage of super_type in each class.
- Template Parameters:
Derived_t – The class to measure.
-
template<typename Derived_t>
void viewFormat() Recurse through super classes and prints chart of bytes used within the allocation.
Depends on usage of super_type in each class.
- Template Parameters:
Derived_t – The class to analyze.
-
template<typename T>
std::string toString(T const &t) Convert all extendible fields to std::strings (in a YAML-like format) using the
serializeField()
- Template Parameters:
Derived_t – The class to convert to string.
-
template<typename Field_t>
void serializeField(std::ostream &os, Field_t const &f) Converts a single field into a std::string (in a YAML-like format).
Specialize this template or overload the operator<< for your field to convert properly.
This is very useful when debugging.
- Template Parameters:
Derived_t – The field data type.
Namespace ext::details
-
class FieldDesc
Defines a span of memory within the allocation, and holds the constructor, destructor and serializer as std::functions.
Effectively the type-erased version of
template<typename Derived_t, typename Field_t> FieldId
.