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.
config. Stateless, never instantiated.Quick start
#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
| Piece | What 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:
struct core { constexpr static auto config = cib::exports<say_message>; };
struct ext { constexpr static auto config = cib::extend<say_message>([]{ … }); }; Services
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.
struct named_svc : public callback::service<> {
constexpr static auto name = +stdx::ct_format<"named_svc">();
}; Extending
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.
&fnworks 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
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? | No | Yes |
| Takes arguments? | No | Yes |
| Usable where? | Where the nexus object is visible | Anywhere, including other components |
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>.
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).
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.
EarlyRuntimeInit— immediately after the C++ runtime is stable. Logging, clocks, the host system.RuntimeInit— general component initialization.RuntimeStart— enable interrupts, start threads; be ready for external events.MainLoop— repeated in an infinite loop.
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
| Library | Coupling | How it meets the nexus |
|---|---|---|
| flow | service 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. |
| msg | service meta | Message handler services; callbacks are added with cib::extend and support runtime_condition. |
| interrupt | service meta | The interrupt manager is built through the nexus; ISRs are extensions. |
| seq | service meta | Sequencer services, same pattern. |
| log | used by | cib::top logs each phase transition with CIB_INFO. |
| stdx | link | ct_string, tuple, panic. The nexus is mostly tuple algebra. |
Pitfalls
| Message or symptom | Cause | Fix |
|---|---|---|
Trying to extend a service (X) that is not exported | No component exports X | Add cib::exports<X> somewhere |
Trying to invoke a service (n) that is not exported | nexus.service<"n">() matches no name | Check the name and that the service declares one |
no member named 'config' in 'C' | Component without constexpr static auto config | Add 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 match | cib::service<T>(args) after init(); check the name spelling (§4.1) |
too few arguments to function call | nexus.service<T>() on a parameterised service | Same |
call to deleted function 'make_runtime_conditional' | runtime_condition over a callback service | Test the flag inside the lambda (§8.2) |
Callback service has more than one callback requiring a move-only arg | Two extensions on a callback::service<T> with move-only T | Only one extension may take it |
constraints not satisfied for alias template 'builder_t' | Exporting something that isn’t a builder_meta | Supply uninitialized(), or don’t export it (§4) |
| Nothing happens, exit code 0 | Service used before init(), default panic handler is a no-op | Call init(); install a panic handler (§7) |
| Extensions run in an unexpected order | Order is unspecified | Use a flow service |