Static, incremental composition for C++17.
HAPI is a small C++ library for composing components incrementally at the type level.
A component defines a Part<O> that derives from O. A Chain applies those components recursively, while APIOf supplies the final base API.
The result is an ordinary C++ type. HAPI does not require a runtime composition framework.
The following is based on the composition pattern used by HAPI's compile-time tests.
#include <hapi/hapi.h>
using namespace hapi;
template<typename Cfg=Nil>
struct ItemAPI : Cfg {
template<typename Out>
static constexpr void print(Out& out) { out << "/"; }
};
template<typename... OO>
struct ItemDef : APIOf<ItemAPI<>, OO...> {
using Base=APIOf<ItemAPI<>,OO...>;
using Base::Base;
static constexpr const size_t size{sizeof...(OO)};
};
struct A {
template<typename O>
struct Part : O {
using Base=O;
using Base::Base;
template<typename Out>
static constexpr void print(Out& out) {
out << "/A";
Base::print(out);
}
};
};
struct B {
template<typename O>
struct Part : O {
using Base=O;
using Base::Base;
template<typename Out>
static constexpr void print(Out& out) {
out << "/B";
Base::print(out);
}
};
};
constexpr ItemDef<A,B> item{};APIOf<ItemAPI<>,A,B> closes the composition by supplying ItemAPI<> as the base. The resulting type is built through the Part<O> transformations supplied by A and B.
A HAPI component is an open type transformation:
struct A {
template<typename O>
struct Part : O {
using Base=O;
using Base::Base;
};
};The component does not decide what its final base type will be.
A chain can therefore be formed independently:
using C = Chain<A,B>;and applied to a base later:
using T = C::Part<ItemAPI<>>;APIOf provides the convenient form for closing that composition:
using T = APIOf<ItemAPI<>,A,B>;For Chain<A,B>, the recursive Part definition produces the equivalent inheritance structure:
A::Part<
B::Part<
ItemAPI<>
>
>This is the basic mechanism behind HAPI's incremental composition model.
A Chain is itself a component, so a chain can be used as part of another chain.
using AB = Chain<A,B>;
using ABC = Chain<AB,C>;The nested structure remains part of the type:
using T = ABC::Part<ItemAPI<>>;This is useful when larger compositions are assembled from smaller, reusable compositions.
The important point is that the intermediate composition does not have to be flattened into a separate representation before it can be composed again. Chain exposes its own Part<T> and therefore participates in the same composition mechanism as the other components.
Because the composition folds through ordinary inheritance, each component's Part<O> sees the accumulated API of everything composed before it, as O. That gives every component two independent options for any given function:
- contribute a function that did not exist in
O - override a function that already exists in
O, and callBase::to reach the version supplied by the components before it
The print example above already does the second: A::Part and B::Part both override print, and both call Base::print(out) to reach the implementation contributed further down the chain.
A component can just as easily add something new instead:
struct C {
template<typename O>
struct Part : O {
using Base=O;
using Base::Base;
static constexpr int extra() { return 42; }
};
};
using WithC = APIOf<ItemAPI<>,A,B,C>;
static_assert(WithC{}.extra() == 42);extra did not exist in ItemAPI, A, or B. C introduces it, and it becomes part of the resulting type's API exactly as if it had been declared there directly.
Because this is ordinary inheritance, the two cases are not mutually exclusive within a single component: a Part<O> can override some of O's functions while contributing others. The choice is made per function, not per component.
HAPI's compile-time operations operate on Chain structures.
For example, a chain can be mapped using a type transformation:
template<typename O>
struct Identity {
using Type=O;
};
using Input = Chain<A,B>;
using Output = Map<Identity>::Check<Input>;
static_assert(std::is_same_v<Output,Chain<A,B>>);Map rebuilds a Chain from the transformed elements. Nested Chains are traversed by the common Traverse mechanism; which other containers it opens is declared per container with Expand.
A filter can select elements while producing another Chain:
using Input = Chain<A,B>;
using OnlyA = Filter<SameAs<A>>::Check<Input>;
static_assert(std::is_same_v<OnlyA,Chain<A>>);Filter uses the predicate at the element level and concatenates the resulting Chain fragments.
The simplest query is query:
static_assert(query<SameAs<A>,A>);
static_assert(query<SameAs<A>,Chain<A>>);
static_assert(!query<SameAs<B>,Chain<A>>);SameAs<T> is a predicate whose Apply checks std::is_same<T,O>.
Presence can also be checked directly:
using Input = Chain<A,B>;
static_assert(Exists<SameAs<A>,Input>::value);
static_assert(Exists<SameAs<B>,Input>::value);
static_assert(!Exists<SameAs<int>,Input>::value);Exists is the non-failing presence query: it produces a boolean result rather than requiring a successful match.
FindFirst resolves the first matching type in a chain.
using Input = Chain<A,B>;
using Found = FindFirst<SameAs<A>>::Check<Input>;
static_assert(std::is_same_v<Found,A>);A missing match is deliberately a compile-time failure:
// using Missing = FindFirst<SameAs<int>>::Check<Input>;The implementation short-circuits at the first successful match rather than continuing through the remaining elements.
For a non-failing query, FindFirstOr supplies a default:
using Input = Chain<A,B>;
using Found = FindFirstOr<SameAs<A>,Nil>::Check<Input>;
using Missing = FindFirstOr<SameAs<int>,Nil>::Check<Input>;
static_assert(std::is_same_v<Found,A>);
static_assert(std::is_same_v<Missing,Nil>);APIOf exposes the resulting composition through Types.
using Item = ItemDef<A,B>;
static_assert(query<SameAs<A>,typename Item::Types>);
static_assert(query<SameAs<B>,typename Item::Types>);FromTypes<Q> provides a predicate for types that expose a Types member:
using Item = ItemDef<A,B>;
static_assert(
FromTypes<SameAs<A>>::Apply<Item>::value
);
static_assert(
!FromTypes<SameAs<int>>::Apply<Item>::value
);This lets compile-time queries distinguish between the object/type being inspected and its exposed composition structure.
Queries, transforms, FindFirst and the rule checks all need to know one thing about a type: does it hold other components, and which? Chain<OO...> does, and HAPI knows. A wrapper of your own does not, until you say so, once, with an Expand entry:
template<typename... II>
struct Box { // a component that wraps others
template<typename O> struct Part : Chain<II...>::template Part<O> {};
};
namespace hapi {
template<typename... II>
struct Expand<Box<II...>> : Expansion<Chain<II...>, /*queried*/true, /*selected*/true> {};
}
static_assert(query<SameAs<A>, Chain<Box<A>>>); // a query now sees A inside the BoxWithout an entry a type is a leaf: every walk treats it as one whole element. Expansion<Children, Queried, Selected, Validates, Searched> names the children and, for each family of walks, whether it opens the container (all default to false, so an entry lists only what it enables):
| bit | walks that open the container when it is set |
|---|---|
queried |
Any, Exists, query, Requires, Excludes |
selected |
every other Traverse operation: Filter, Map/Transform, Partition, … (false = the element is taken whole) |
validates |
BuildRules / NoCollision: the children are spliced in place, so their rules() run with the enclosing chain as context. The container's own rules(), if it has any, still runs too |
searched |
FindFirst: the container is tested as a whole first, then opened |
The bits are separate because the walks genuinely differ. Chain sets all four. APIOf<API,OO...> sets only validates (its children are Chain<API,OO...>, i.e. APIOf::Types): a nested APIOf is validated in place, but queries and Filter still see it as one element. A container can also want some walks and not others: for instance a component type whose contents queries should see, while Filter must select it whole.
Rules of the road:
- Per exact type, never inferred. Having a
Typesmember does not make a type a container (a generic::Typessplice was tried and reverted: it broke whole-object matching withFromTypes). - A derived type needs its own entry, one line forwarding to its base:
template<typename... OO> struct Expand<D<OO...>> : Expand<B<OO...>> {}; - Declare it before first use, like any specialization (g++ rejects a specialization after the type has been queried; clang does not).
- A hand-written
Traverse<Op,X<...>>specialization still works and wins over the default, but anExpandentry is preferred: it also coversFindFirstand the rule walks, and it lets each family opt in separately. IsContainer<O>tells whether a type has an entry.
Detecting a leaf costs one class template instantiation per element visited (measured with clang++ -ftime-trace); Chain::Map and friends that do not go through Traverse are unaffected.
A chain composes data as well as behaviour. hapi::Slot<Tag, S> is a component that adds one value of type S to the state the components after it built. The state is
one object of static size, with no heap, and a slot is reached by its tag, not by a name or a position, so two slots may have fields of the same name.
#include <hapi/hapi.h>
using namespace hapi;
struct Pos {}; struct Speed {}; // tags: any distinct types
struct PosSlot { int16_t x, y; };
struct SpeedSlot { int16_t x; }; // a field named like PosSlot's: no clash
using State = APIOf<SlotApi, Slot<Pos, PosSlot>, Slot<Speed, SpeedSlot>>::Res;
State s{};
slot<Pos>(s).x = 1; slot<Speed>(s).x = 2;- Compile errors, with their own messages: a tag that is not in the state (
hapi::slot<Tag>: no slot with that tag in this state), and the same tag twice, also across a nestedChainorAPIOf(hapi::Slot: two Parts claim the same tag). A nested composition is the same state, the same size and the same walk order as the flat one. - Nothing about what the slots mean is decided.
Slot<Tag, S, Contract>takes a third parameter, a templateContract<Below, R>whose base carries the state below and the state's own typeR(R::me()is this slot). It is how a user adds members to the state's type, for instance astep():
template<class Below, class R> struct Counted : Below { unsigned steps = 0; void step() { ++steps; } };
using Counting = APIOf<SlotApi, Slot<Pos, PosSlot, Counted>>::Res;each(visitor)walks the slots in chain order (the last-listed first):visitor.layer(Tag::name()), thenS::each(slot, visitor), which the slot's type defines.Tag::name()andS::eachare only looked at wheneachis used, and whatname()returns is the visitor's business.- Cost: the same AVR program as a hand-indexed array (
tests/slots/run.shcompares the flashed bytes); an empty slot and a nested composition add no size (checked on GCC, Clang and MSVC; MSVC removes an empty base from one base only unless__declspec(empty_bases)is applied, which the header does throughHAPI_EMPTY_BASES). A chain of 256 slots takes 1.5 s to compile with g++ (-fsyntax-only), against 0.9 s for 256 plain Parts.
The reference is in docs/REFERENCE.md.
HAPI also provides find<Q>(object).
The current implementation uses the object's Types member to perform a compile-time FindFirst check and then returns the object reference:
ItemDef<A,B> item;
auto& result = find<SameAs<A>>(item);
static_assert(
std::is_same_v<
decltype(result),
ItemDef<A,B>&
>
);The important distinction is that the query is compile-time, while the returned reference is an ordinary runtime reference to the composed object. find does not construct a runtime component registry or perform a dynamic search.
Components can provide rules() to constrain how they may appear in a composition.
This is the actual pattern used by HAPI's compile-time tests:
struct A {
template<typename O>
struct Part : O {
using Base=O;
using Base::Base;
};
};
struct B {
template<typename O>
struct Part : O {
using Base=O;
using Base::Base;
};
template<typename Before,typename After>
static constexpr bool rules() {
static_assert(
query<SameAs<A>,Before>,
"B only makes sense after A"
);
static_assert(
!query<SameAs<B>,After>,
"do not repeat B"
);
static_assert(
!query<SameAs<A>,After>,
"A must be before B"
);
return true;
}
};A valid composition:
constexpr ItemDef<A,B> ok{};An invalid composition can therefore fail during compilation:
// constexpr ItemDef<B> fail_requireA{};
// constexpr ItemDef<B,A> fail_order{};
// constexpr ItemDef<A,B,B> fail_unicity{};APIOf invokes BuildRules when the composition is closed, causing the rules to be evaluated as part of the type construction.
A nested Chain or APIOf is validated in place: its components' rules() see the enclosing chain as Before/After. Any other container is validated the same way only if its Expand entry sets validates; otherwise the rules() of what it holds are not run. Turning it on changes what compiles (a violation hidden inside the container is now reported), so it is opt-in per container. It is meant for wrappers that hold the same item's components; a container that holds other items would mix their components into each other's Before/After and trip rules that only make sense within one item.
HAPI's composition mechanism does not require:
- virtual dispatch
- dynamic allocation
- a runtime component registry
- runtime composition metadata
The goal is not to claim that every generated program is automatically optimal.
The goal is to make composition itself a compile-time property, using ordinary C++ types and inheritance.
Generated code remains subject to the compiler, optimization settings, target architecture, and the implementation of the composed components.
Template-heavy C++ can make compile-time cost an important part of library design.
This was treated as a specific design concern during HAPI's development.
Rather than assuming that static composition would remain inexpensive, HAPI's compile-time operations were measured and compared with established C++ template libraries.
The benchmark compares selected HAPI operations with Boost.Hana.
The measurements use:
g++ -fsyntax-only
and measure compile-time behavior without executing runtime values.
The comparison is not intended as a general performance ranking between the libraries. They have different purposes and different abstractions.
Boost.Hana is used as a well-known C++ metaprogramming reference point.
The purpose of the benchmark is straightforward:
compile-time cost was treated as a design constraint and measured during development.
The benchmark checks how the type-level operations behave as the number of elements grows, including comparisons involving nested tree topology.
HAPI and Boost.Hana are complementary.
Boost.Hana provides a broad framework for heterogeneous compile-time and value-level computation.
HAPI focuses on incremental structural composition through C++ types and recursive inheritance.
The benchmark uses Boost.Hana as a familiar reference point for compile-time behavior. It is not intended to claim that HAPI replaces Hana, or that either library is generally faster.
The comparison was made because compile-time cost matters for a template-based C++ library, and Hana provides a useful established reference for that concern.
The composition mechanism itself never touches hardware. Chain<>'s fold
(hapi/chain.h) is plain recursive inheritance, and hapi/base.h only
branches for AVR/no-STL freestanding toolchains — everywhere else it's
ordinary <cstddef>/<type_traits>/<utility>. OneData, one of the
libraries built on HAPI, states this directly: "Tested on AVR (avr-gcc 7+)
and x86-64" — the same composed types run identically on a hosted target.
The compile-time-cost discipline described above isn't an embedded-only concern either — it benefits any C++17 codebase using heavy template composition, not only firmware.
OneParse, another library built on HAPI, already benchmarks its
runtime parsing throughput against desktop parsing libraries — lexy, PEGTL,
simdjson, rapidjson, and Boost.Spirit.X3 — on JSON fixtures (see
OneParse/benchmark/), independent of any embedded target.
examples/config_loader is the first example
that combines HAPI, OneData, and OneParse together outside the embedded
framing: a small CLI config loader/validator, built as an ordinary
PlatformIO env:native target with no board and no embedded framework.
examples/cutlass_layout composes a real
NVIDIA CuTe (cute::Layout, part of CUTLASS) with HAPI and OneData —
proving the composition model reaches spatial/indexing libraries as
cleanly as temporal ones, host-only, no GPU or nvcc required.
examples/cuda_device_chain goes further:
real nvcc device-side compilation, a new toolchain axis alongside HLS.
HAPI's unmodified Chain<>/APIOf<> composes a __host__ __device__-
annotated component and runs inside a real __global__ kernel — the
generated PTX shows the entire composed object folding to a single
constant store, zero calls. Compiles clean, and now runs for real: a
real CUDA-capable GPU (GeForce GTX 1070, Pascal) replaced the machine's
original one (a GT 710, Kepler, below CUDA's real support floor), and the
same unmodified kernel launches and executes correctly —
host: Ticker after 2x inc() = 4, device: Ticker after 3x inc() = 6,
both as expected. A real CUTLASS SGEMM (examples/00_basic_gemm from
NVIDIA's own repo, plain SIMT, not Tensor-Core) also runs correctly on
the same card, verified against an independently-computed reference
kernel, not just "didn't crash."
examples/cuda_blockchain_spec asks a
sharper question of the same GPU: not "does a bare Chain<> compile
under nvcc," but does hls_blockchain_kernel's actual point — a
protocol specification composed from independently swappable blocks
(Transaction/Hash/Validation/State/Consensus), not raw hashing
throughput — survive a real CUDA target the way it already survived
Bambu and Vitis HLS. It does: host and device paths agree exactly for
both Hash variants (MurmurHash/XorFoldHash, swapped with nothing
else touched), and the PTX shows the entire five-block specification —
real hash-mixing multiply included — folding to seven constant stores,
zero calls. A stronger zero-overhead result than cuda_device_chain's
own toy, over a genuinely meaningful multi-block composition instead of
one increment.
examples/rust_stm32_bridge is the
opposite move from the four examples above: a real embedded
consumer, not another generality proof. Real Rust firmware
(cortex-m-rt/stm32f1xx-hal) calls into unmodified hapi::APIOf<>
compiled by the same real arm-none-eabi-g++ toolchain
focCompose already hardware-verified, on the same
STM32F103C8 (Blue Pill) board — flashed and confirmed via real OpenOCD
register readback, not just "it compiled." Documents the honest
FFI-boundary cost plainly: zero-overhead holds inside the C++ side, a
real, non-inlined call at the language boundary. A second entry point
drives a real HD44780 I2C LCD through the unmodified oneIO::display::I2cLcd
→ Hd44780 → oneBus::I2cGpio → hw::stm32::Stm32I2cCore stack —
composition over an actual peripheral, not a toy counter — with the glass
confirmed by eye.
examples/static_net is a different kind of composition: static networks,
dry, typed, zero-runtime descriptions of dataflow nets (a typelist of parts, wired by index, id or query;
the values a net reads are the slots of a state, hapi/slots.h, and a register is a layer of it), with tinyML as the running example: a 4-input classifier in 42 B and
24 cycles on an 8-bit ATmega328p. It is measured against a table loop and against emlearn on the same
folds, in one build setup; the cycle counts come from simavr and were confirmed on a real Arduino Nano
(100 numbers, no difference), and the realization of a cell (unrolled or as a table and a loop) is a
compile-time choice with a measured size/speed trade. Its README says what was measured and what was not.
HAPI is written for C++17 and is intended for systems where static composition is useful.
The repository includes examples for different environments, including embedded and HLS-oriented experiments. The examples directory currently contains crtp, free, godbolt, rules, std, virt, several HLS examples, four non-embedded cross-library demonstrators (config_loader, cutlass_layout, cuda_device_chain, cuda_blockchain_spec), and a real embedded Rust-to-C++ bridge (rust_stm32_bridge).
single/hapi.h is the whole library in one file (include/hapi/*.h inlined, MIT header kept), for places that take one
file or one URL, such as Compiler Explorer: #include <https://raw.githubusercontent.com/InternetOfPins/HAPI/main/single/hapi.h>.
It is generated, not edited: python3 scripts/amalgamate.py rewrites it, and tests/single_header/run.sh checks that it
is current and that programs built against it are identical, instruction for instruction, to the same programs built against include/.
The composition model can be used for applications such as:
- hardware interfaces
- protocol stacks
- parsers
- input/output components
- processing pipelines
- validation structures
The repository contains HLS experiments, including hls_can_disabler, hls_fir, and hls_smoke.
Selected HAPI compositions have also been tested with PandA-Bambu HLS.
This is an experimental application of the static composition model. It is not a claim that arbitrary HAPI programs are automatically synthesizable.
- Industry Applications — Applications of the composition model.
- Component Architecture — Component anatomy and layer structure.
- API Reference — Core types and advanced usage.
- Compile-time tests — Compile-time validation examples.
- Benchmarks — Compile-time measurements and experiments.
HAPI is the foundation for the One* library family in InternetOfPins:
| Project | Description |
|---|---|
| OneBit | Bit manipulation |
| OneData | Data components |
| OnePin | Pin and port abstractions |
| OneChip | Hardware register components |
| OneBus | Bus protocols |
| OneIO | Physical I/O components |
| OneInput | Composable physical input |
| OneSensor | Sensor components |
| OneItem | Item behavior and presentation |
| OneOutput | Output components |
| OneMenu | Menu system |
| OneParse | Parser components |
HAPI is an experimental C++ composition library under active development.
The central design question is how far incremental, type-level composition through open recursive inheritance can be taken while retaining practical compile-time costs and useful generated programs.
The repository contains the implementation, examples, tests, benchmarks, and experiments used to explore that model.
Made with obsession in the Azores 🇵🇹
By Rui Azevedo · @ruihfazevedo
