Skip to content
michael.caisse.io michael.caisse.io

CIB Guide Chapter 1 of 8

Nexus

Components declare what they offer and what they extend. The compiler wires the firmware together — no registration code, no init order to maintain, no dispatch tables.

Headers
include/nexus/, include/cib/
Namespace
cib
Size
710 lines
Skeleton
cib::top
Used by
every cib library

The idea

The usual way a firmware project grows: a driver appears, and somewhere a central init() gains a line calling it. Then an ordering constraint. Then an #ifdef because the variant without the display shouldn’t call it at all. The central file becomes a map of every module in the build, and no module can be added or dropped without editing it.

The nexus inverts that. A component says I extend this service; nothing says call this component. Adding a component to the project list is the whole integration step, and the wiring is computed by the compiler.

Extension point service
A type. Anyone may extend it; it doesn’t know who.
Unit of code component
A type with a config. Stateless, never instantiated.
The whole build project
A component at the top, usually just a list of others.
The machine nexus
Gathers every extension and builds each service once, at compile time.
COMPONENTS core exports<say_message> greeter extend<say_message>(…) lazy_dog extend<say_message>(&fn) no component references another NEXUS (compile time) gather by service fold into builder build() once BUILT SERVICE say_message void (*)() calls all three init() publishes it to cib::service<say_message>
Everything left of the built service happens in the compiler. What ships is one function that calls three.

Quick start

the whole framework in one file
#include <cib/cib.hpp>

// 1. a service: an extension point
struct say_message : public callback::service<> {};

// 2. a component that exports it
struct core {
    constexpr static auto config = cib::config(cib::exports<say_message>);
};

// 3. components that extend it -- neither knows the other exists
struct greeter {
    constexpr static auto config =
        cib::config(cib::extend<say_message>([] { puts("Hello, world!"); }));
};
struct lazy_dog {
    static auto talk() -> void { puts("The quick brown fox…"); }
    constexpr static auto config =
        cib::config(cib::extend<say_message>(&talk));
};

// 4. the project composes them
struct hello_world {
    constexpr static auto config =
        cib::components<core, greeter, lazy_dog>;
};

cib::nexus<hello_world> nexus{};

int main() {
    nexus.init();                 // publish services to the global handles
    cib::service<say_message>();  // runs both extensions
}

Dropping lazy_dog from the build means deleting it from one list. Adding a third greeter means adding it to that list. No other file changes.

Vocabulary

PieceWhat it is
cib::config(items…)Bundle config items into one
cib::exports<Services…>Declare that these services exist
cib::extend<Service>(fns…)Add functionality, by type
cib::extend<"name">(fns…)Add functionality, by name
cib::components<Cs…>Compose other components
cib::nexus<Project>The built project
nexus.init()Publish every service to its global handle
cib::service<T>The global handle — a function pointer
cib::constexpr_condition<"n">(p)Include items only if p() at compile time
cib::runtime_condition<"n">(p)Include, gated at run time (§8)

A config item is anything with extends_tuple() and get_exports() — which is why config, components, exports, extend and the conditionals all nest inside one another freely.

cib::config(…) is only needed to combine items. A component with one thing to say can assign it bare:

test/cib/readme_hello_world.cpp
struct core { constexpr static auto config = cib::exports<say_message>; };
struct ext  { constexpr static auto config = cib::extend<say_message>([]{ … }); };

Services

a callback service is a fan-out of void(Args…)
struct say_message : public callback::service<> {};              // void()
struct send_byte   : public callback::service<std::uint8_t> {};  // void(uint8_t)

A service meta supplies builder_t, interface_t and uninitialized(). callback::service<Args…> gives interface_t = void (*)(Args…) and an uninitialized() that panics. Other libraries bring their own metas — flow::service<"name"> and msg::service<…>, whose interface_t is a pointer, hence the cib::service<S>->handle(…) arrow syntax.

The builder’s add(…) must be pure — the nexus folds builder = builder.add(args…) at compile time — and its build<BuilderValue, Nexus>() receives the nexus type, injected, so builders and extension callables can be written generically over it.

Naming a service

A name member enables cib::extend<"name"> and nexus.service<"name">, which lets a component extend a service whose type it can’t see.

use this exact spelling
struct named_svc : public callback::service<> {
    constexpr static auto name = +stdx::ct_format<"named_svc">();
};

Extending

lambdas, function pointers, several at once
struct comp {
    static auto handler() -> void;          // may be defined in a .cpp

    constexpr static auto config = cib::config(
        cib::extend<say_message>([] { … }),            // captureless lambda
        cib::extend<say_message>(&handler),            // function pointer
        cib::extend<say_message>([] { … }, [] { … })); // several at once
};
  • Lambdas must be captureless — they have to be constant expressions.
  • &fn works for a function declared in a header and defined in a translation unit, so extensions need not be header-only.
  • Any number of components may extend the same service, and a component may extend a service another component exports. That is the entire point.

Wiring bench

Toggle components in and out of the project and watch what the nexus builds. This is the integration step in cib: a component is in the build, or it isn’t, and nothing else changes.

what the nexus builds

Each service collects extensions from whichever components are present — and a missing export is a compile error, not a silent no-op.

Components in the project

project
services
build

Note what doesn’t appear: any list of components to call, anywhere. Each service’s contents are derived from the set of components in the build.

Invoking

nexus.service<T>()cib::service<T>
Needs init() first?NoYes
Takes arguments?NoYes
Usable where?Where the nexus object is visibleAnywhere, including other components
anything with parameters goes through the global handle
nexus.init();
cib::service<send_byte>(0x42);        // a plain function pointer
cib::invoke_service<"named_svc">();   // by name

init() is also what lets one component call another’s service without knowing the nexus type — the mechanism behind all cross-library wiring. flow::run<F> is just cib::service<F>.

make panics loud
struct my_panic {
    template <stdx::ct_string Why, typename... Ts>
    static auto panic(Ts &&...) noexcept -> void {
        std::printf("PANIC: %s\n", std::string_view{Why}.data());
        std::abort();
    }
};
template <> inline auto stdx::panic_handler<> = my_panic{};
// -> PANIC: Attempting to run callback before it is initialized

Conditions

Compile time

A false constexpr_condition removes the extension from the build entirely — the code isn’t in the image (verified both ways).

variant configuration without #ifdef
constexpr static auto when_v_is_42 =
    cib::constexpr_condition<"when_v_is_42">([] { return V == 42; });

constexpr static auto config =
    cib::config(when_v_is_42(cib::extend<TestCallback<0>>([] { … })));

Run time

runtime_condition keeps the extension in the build and gates it per call — but it works only over things that can carry a condition internally.

Two more limits follow from that rewrite: it reconstructs each argument as Arg{}, so conditional extension arguments must be stateless; and it rebuilds via the service type, so a runtime condition cannot wrap a by-name cib::extend<"Name"> (no type named 'service_type' in 'name_extend'). Condition predicates must be default-constructible — a capturing lambda trips a static_assert carrying no message at all.

cib::top

A ready-made main for firmware: four flow services, run in order, then a loop forever.

  1. EarlyRuntimeInit — immediately after the C++ runtime is stable. Logging, clocks, the host system.
  2. RuntimeInit — general component initialization.
  3. RuntimeStart — enable interrupts, start threads; be ready for external events.
  4. MainLoop — repeated in an infinite loop.
the entire application skeleton
struct my_project { constexpr static auto config = cib::components<>; };

cib::top<my_project> top{};
int main() { top.main(); }   // [[noreturn]]

top exports all four services itself — so don’t export them again — wraps your project as a component, calls init(), then runs them. Your components extend them with cib::extend<cib::MainLoop>(*ACTION); since they’re flow::service<>, you use flow’s step and ordering DSL rather than bare lambdas, which is how you get a defined order where it matters.

Rest of cib

LibraryCouplingHow it meets the nexus
flowservice meta flow::service<"name">. flow::run<F> is cib::service<F>. The four cib::top services are flows. Use a flow when extension order matters.
msgservice meta Message handler services; callbacks are added with cib::extend and support runtime_condition.
interruptservice meta The interrupt manager is built through the nexus; ISRs are extensions.
seqservice meta Sequencer services, same pattern.
logused by cib::top logs each phase transition with CIB_INFO.
stdxlink ct_string, tuple, panic. The nexus is mostly tuple algebra.

Pitfalls

Message or symptomCauseFix
Trying to extend a service (X) that is not exportedNo component exports XAdd cib::exports<X> somewhere
Trying to invoke a service (n) that is not exportednexus.service<"n">() matches no nameCheck the name and that the service declares one
no member named 'config' in 'C'Component without constexpr static auto configAdd one — cib::config() if it has nothing to say
no matching member function for call to 'service'Arguments passed to nexus.service<T>(), or a name that didn’t matchcib::service<T>(args) after init(); check the name spelling (§4.1)
too few arguments to function callnexus.service<T>() on a parameterised serviceSame
call to deleted function 'make_runtime_conditional'runtime_condition over a callback serviceTest the flag inside the lambda (§8.2)
Callback service has more than one callback requiring a move-only argTwo extensions on a callback::service<T> with move-only TOnly one extension may take it
constraints not satisfied for alias template 'builder_t'Exporting something that isn’t a builder_metaSupply uninitialized(), or don’t export it (§4)
Nothing happens, exit code 0Service used before init(), default panic handler is a no-opCall init(); install a panic handler (§7)
Extensions run in an unexpected orderOrder is unspecifiedUse a flow service