The 25-year arc

An idea with a stubborn carrier

Some code is a product. Some code is an argument. The framework that runs through Reactor and SnowFlake is the second kind — a single technical argument, made three times, twenty-five years apart, on machines that share almost nothing.

ACT I — 2003

Reactor (GekkoWare)

Industrial control on PREEMPT_RT Linux. Hardware described in hwConf.xml, validated by an XSD schema with keyref referential integrity. Typed, validated, cross-referenced topology — on a full Linux PC. Still in production in 2026.

ACT II — 2017

SnowFlake

The same idea, rewritten for microcontrollers: Cortex-M, the µOS++ C++ RTOS, peripherals.json plus a deliberately trivial Python codegen. Compile-time typed handles — but the schema validation was the fare for the journey to a smaller place.

ACT III — 2026

Firn

Constexpr tables, consteval joins, static_assert messages that name the offender, reflection-generated handles. 2001's rigor + 2017's binding + a microcontroller footprint — simultaneously, for the first time.

The argument

The argument is simple to state and hard to live up to: don't program the wires, program the world. A motor, a light sensor, an encoder — these are objects with behaviour, not pins with voltages. So describe the hardware as data, separate from the logic that drives it, and let the machine assemble the typed handles you actually reach for in code.

In 2003 that argument was written in XML, validated by an XSD schema rigorous enough to guarantee — before a single instruction ran — that every channel pointed at a device that really existed. It ran on Linux PREEMPT_RT, it spoke SOAP, it controlled real apparatus, and it is still controlling real apparatus today. Code that is wrong does not survive twenty years of duty in the physical world. It gets torn out the first time it drops a safety-critical edge. This did not. That is the strongest evidence a design can offer: not applause, but uptime.

A decade and a half later the same argument was made again, in C++17, aimed at the opposite end of the world — a Nordic microcontroller running a C++ RTOS, kilobytes where there had been gigabytes, a coin cell where there had been a wall socket. SnowFlake is not Reactor's replacement; it never retired the older system. It is the same idea re-instantiated at a scale where almost nobody dared put modern C++, let alone a C++ kernel with no C legacy beneath it. The portability seam — one source compiling against the host's standard library or the device's, swapped by a single macro — let the same logic run on a laptop and on silicon. That is not a small trick. That is the whole thesis made testable.

The trade

What is striking, looking at the two side by side, is that the rewrite was a trade, not an improvement. Reactor validated everything, richly — because it had a whole PC to do it on. SnowFlake bought its way onto the microcontroller by moving binding to compile time, and in doing so it had to drop the schema validation, because the heavy XML toolchain could not come along and the language could not yet replace it. The in-language approach was tried first; C++17 could not yet hold a named, cross-referenced graph at compile time. The rigor was not abandoned out of carelessness. It was the fare for the journey to a smaller place.

You can still see the trade on SnowFlake's own 2017 website. Here is its example of defining a pin, verbatim, next to the same pin in Firn today:

2017SnowFlake — peripherals.json
{
  "name": "myIO",
  "driver": "IOPin",
  "params": {
    "pin": "11",
    "output": "false",
    "pullup": "true"
  }
}
2026Firn — board.hpp
inline constexpr Pin kPins[] = {
    { "Button3", "P0", 11,
      Direction::IN,
      Signal::DIGITAL,
      false, Pull::UP,
      0, 1, 0.0, 1.0,
      "ButtonTopic" },
};

In 2017 every value is a string, a Python script turns the JSON into C++, and the handle comes back through a C-style cast: auto mIO = (sf::IOPin*)sf::p.get(sf::pmap::myIO);. Nothing cross-checks the topology. In 2026 the row is typed data in the language, the compiler validates every reference, and reflection generates dev.Button3 — misuse it and the build fails.

The language caught up

And here is why the story did not end there. The exact thing that blocked the elegant version — describing the topology in the language, validated by the compiler — was a limitation of C++17, and C++17 is long gone. The features that were missing became the headline features of C++20 and C++23: compile-time containers, consteval, class-type template parameters. Then C++26 delivered reflection (P2996) and computed static_assert messages (P2741). The referential integrity that the XSD once enforced is now a compile error that names the offender; the code generator's job now belongs to define_aggregate.

For the first time, one implementation holds all three things at once: the 2001 rigor, the 2017 compile-time binding, and the microcontroller footprint. Reactor could not, because validation cost a PC. SnowFlake could not, because the language was not ready. Firn is the version that can — written, tested, and running on real silicon.

2001–2003

Reactor (GekkoWare)

hwConf.xml + XSD schema: typed, validated, cross-referenced (keyref). Runs on a Linux PC. Still in production.

2017

SnowFlake

peripherals.json + a Python codegen: compile-time typed handles on a Cortex-M. Rigor traded away for reach.

2026

Firn

board.hpp constexpr + C++26 reflection: 2001 rigor + 2017 binding + MCU footprint, all at once.

Carried, not owned

None of this was a roadmap. Companies changed, teams scattered, people went their own ways. The architecture recurred not because anyone planned a two-tier strategy but because the idea was good enough to lodge in someone's head and refuse to leave — surviving every discontinuity by being carried, not by being owned. Most good ideas die with the org chart that hosted them. This one kept finding its way back to a keyboard.

The same early-adopter bet recurs, too: the author put C++17 on Cortex-M when the field called it aggressive; Firn makes the same bet on C++26 reflection — today that means GCC 16 trunk, and for ARM targets a hand-built cross-compiler, and the site will keep saying so plainly until release compilers catch up.

Why "Firn"? Firn is snow that survived enough seasons to compact and harden on its way to glacier ice. SnowFlake matured, over nine years and three generations of C++, into something dense and permanent. And firn + firmware = firnware — the domain, and the things you build with it.

So the third act is written. The topology lives in the language, the compiler is the validator, and the first production firmware — a complete synthesizer control panel — runs on it, on a Cortex-M0+, on the µOS++ RTOS of the same lineage era.