The board is data.
The compiler is the validator.
Describe your hardware as constexpr tables. Firn's consteval engine joins every cross-reference, rejects every inconsistency with an error that names the offender, and C++26 reflection generates your typed, zero-overhead device API from the same tables. No code generator. No DSL. Nothing to drift.
C++26 reflection (P2996) — GCC 16 trunk today
inline constexpr Pin kPins[] = {
{ "LedRed", "P0", 13,
Direction::OUT, Signal::DIGITAL,
true, Pull::NONE },
{ "Button3", "P0", 11,
Direction::IN, Signal::DIGITAL,
false, Pull::UP,
0, 1, 0.0, 1.0, "ButtonTopic" },
};
inline constexpr Sensor kSensors[] = {
{ "AmbientLight", "I2C_7", 0x55 },
}; // bus is a keyref
dev.LedRed.write(true);
// active-low handled; one store
double lux = dev.Light.getPhysical();
dev.AmbientLight.lux();
// bus resolved at compile time
dev.Button3.write(true);
// ERROR: write() requires dir != IN
tests/fail/unknown_bus.cpp:8:23:
error: static assertion failed:
sensor 'AmbientLight' references
unknown bus 'I2C_9'
8 | static_assert(kDiag.ok(), kDiag);
| ~~~~~~~~^~
Panel C is verbatim g++-16 output. That's a real compiler message. That's the whole idea. A wrong board doesn't boot wrong — it doesn't compile.
Why it holds
Three claims, each with a checked artifact behind it
Validated by the compiler
Cross-references between tables — a sensor naming its bus, an
encoder naming its chain bits, an LCD role naming its pin — are
consteval joins. Uniqueness, pin conflicts, capacities,
orphaned encoder phases: all checked before anything runs. A
violation is a compile error that names the offender.
21 compile-fail regression tests, every one a named-offender diagnostic
Generated by reflection
C++26 define_aggregate walks the tables and emits a
struct whose member names come from the data:
dev.LedRed, dev.AmbientLight. Add a row
and a typed handle appears — no hand-written handle list can
drift.
live run: the whole device surface from one board.hpp
Zero overhead, proven
The reflection-generated C→C++ ISR bridge thunk compiles to the same instructions as a hand-written thunk and as a direct call. The test suite diffs the disassembly instruction-for-instruction on every run.
asm diff in CI: reflection == hand-written == direct
Try it
Break the board. The compiler catches it.
Click a highlighted value to sabotage the topology
— and watch the build answer. Every diagnostic below is the verbatim
message from Firn's compile-fail regression suite; fix the board and the
generated firn::dev surface comes back.
inline constexpr Pin kPins[] = {
{ "LedRed", "P0", 13,
Direction::OUT, /*active-low*/ },
{ "LedGreen", , 0,
Direction::OUT, /*active-low*/ },
{ "Button3", "P0", ,
Direction::IN, Pull::UP },
};
inline constexpr Bus kBuses[] = {
{ "I2C_7", 7, 400 },
};
inline constexpr Sensor kSensors[] = {
{ "AmbientLight", , 0x55 },
};
The squiggle marks what you broke; the message names it. Wrong usage fails the same way — writing to an input pin or reading analog from a digital pin is a compile error too. .
Zero overhead, verbatim
The assembly is the argument
A vendor C driver's interrupt calls a typed C++ method through
firn::bridge::Thunk — one cast, zero cost. From the test
suite's disassembly diff (all -O2):
── reflection thunk Gpio<0x500>::set ──
0: endbr64
4: movl $0x501,0x0(%rip)
e: ret
── hand-written thunk ──
0: endbr64
4: movl $0x501,0x0(%rip)
e: ret
── direct dev.set() ──
10: endbr64
14: movl $0x501,0x0(%rip)
1e: ret
=> reflection thunk == hand-written == direct; bridge adds 0 instructions.
Everything consteval folds to constants; none of it reaches flash. Proven on a Cortex-M0+ with a GCC-16 arm-none-eabi cross — see the case study.
Lineage
Twenty-five years in the making. Compiled at last.
One idea — describe the hardware as data, let a machine turn it into typed, validated handles — carried by one person since 2001.
Reactor (GekkoWare)
hwConf.xml + XSD keyref integrity on PREEMPT_RT Linux. The rigor — but it needed a PC. Still in production in 2026.
SnowFlake
peripherals.json + Python codegen on Cortex-M and µOS++. Reached the microcontroller — but traded the rigor away.
Firn
constexpr tables, consteval joins, reflection-generated handles. The rigor, the binding, and the footprint — at once.