docs/agents/cpp-standards.md
fl:: namespace instead of std::fl/type_traits.hfl::vector<T> for dynamic arrays. The implementation is in src/fl/stl/vector.h.fl::net Namespace ConventionFiles in src/fl/net/ follow a two-level namespace pattern:
fl::net
fl::net::OTAfl::net::<module> sub-namespace
fl::net::ota::Servicefl::net::<module>
fl::net::http::*, fl::net::ble::*When moving a type into a sub-namespace, drop the module prefix from the name since the namespace already provides context:
OTAService → fl::net::ota::ServiceBleStatusInfo → fl::net::ble::StatusInfoBleTransportState → fl::net::ble::TransportState| File | Primary type (fl::net) | Sub-namespace (fl::net::<mod>) |
|---|---|---|
ota.h | OTA | ota::Service |
rpc.h | (none — transport aliases) | rpc::StreamClient, rpc::StreamServer |
http.h | (none — facade) | http::Server, http::Response, http::fetch_get, … |
ble.h | (none — collection) | ble::TransportState, ble::StatusInfo, ble::createTransport, … |
An API object is a public-facing wrapper header that lives alongside a directory of the same name containing implementation details. Users only ever #include the API header.
File layout:
src/fl/stl/fixed_point.h ← API object (public interface)
src/fl/stl/fixed_point/ ← implementation directory
s16x16.h ← concrete type
base.h ← shared internals
...
Rules:
foo.h is the public API; foo/ holds everything behind it.fixed_point_impl<IntBits, FracBits, Sign>) selects the right concrete type at compile time. Invalid combinations fail via an undefined primary template.fl::sin(), fl::floor()) are SFINAE-gated to the wrapper type, giving users a natural calling convention.s0x32 * s16x16 → s16x16) are defined at the bottom, after all types are visible.Exemplar: src/fl/stl/fixed_point.h wrapping src/fl/stl/fixed_point/
src/platforms/ (e.g., int.h, io_arduino.h) that route to platform-specific implementations via coarse-to-fine detection. See src/platforms/README.md for details.src/platforms/**): Header files typically do NOT need platform guards (e.g., #ifdef ESP32). Only the .cpp implementation files require guards. When the .cpp file is guarded from compilation, the header won't be included. This approach provides better IDE code assistance and IntelliSense support.
header.h: No platform guards (clean interface)header.cpp: Has platform guards (e.g., #ifdef ESP32 ... #endif)#ifdef ESP32 to both header and implementation files (degrades IDE experience).cpp.hpp files)A .cpp.hpp file is a compile-time component router that assembles a complete feature from sparse, per-platform fragments with null/no-op fallbacks.
Key properties:
.cpp.hpp file examines platform defines and #includes the right fragments. It composes a complete system from whatever pieces the current platform offers.Why .cpp.hpp instead of .cpp or .h?
.cpp) — so it can't be a normal .h (would cause multiple-definition linker errors).#included from exactly one translation unit (like .hpp) — not compiled on its own.// IWYU pragma: private to enforce single inclusion.Naming convention (future standard):
component.impl.cpp.hpp — The router file. Contains the #if/#elif dispatch logic that selects which platform fragment to include. Included from exactly one translation unit.component_<platform>.impl.hpp — Platform-specific implementation fragments that the router includes (e.g., coroutine_esp32.impl.hpp, coroutine_wasm.impl.hpp).The names make roles explicit: .impl.cpp.hpp = "implementation router, include me once." _<platform>.impl.hpp = "platform fragment, the router includes me."
Structure of a .impl.cpp.hpp router:
// IWYU pragma: private
// Platform detection
#include "platforms/arm/is_arm.h"
#include "platforms/esp/is_esp.h"
#include "platforms/wasm/is_wasm.h"
#if defined(FL_IS_WASM)
#include "platforms/wasm/feature_wasm.impl.hpp"
#elif defined(FASTLED_STUB_IMPL)
#include "platforms/stub/feature_stub.impl.hpp"
#elif defined(FL_IS_ESP32)
#include "platforms/esp/32/feature_esp32.impl.hpp"
#else
// Fallback: null/no-op implementation
#include "platforms/shared/feature_null.impl.hpp"
#endif
Current exemplar: src/platforms/coroutine.impl.cpp.hpp.
fl::span<T> has implicit conversion constructors - you don't need explicit fl::span<T>(...) wrapping in function calls. Example:
verifyPixels8bit(output, leds) (implicit conversion)verifyPixels8bit(output, fl::span<const CRGB>(leds, 3)) (unnecessary explicit wrapping)fl::span<T> or fl::span<const T> for function parameters and return types unless a copy of the source data is required:
fl::span<const uint8_t> getData() (zero-copy view)void process(fl::span<const CRGB> pixels) (accepts arrays, vectors, etc.)std::vector<uint8_t> getData() (unnecessary copy)fl::span<const T> for read-only views to prevent accidental modificationPlatform Identification Naming Convention:
FL_IS_<PLATFORM><_OPTIONAL_VARIANT>FL_IS_STM32, FL_IS_STM32_F1, FL_IS_STM32_H7, FL_IS_ESP_32S3FASTLED_STM32_F1, FASTLED_STM32 (missing FL_IS_ prefix)FL_STM32_F1, IS_STM32_F1 (incorrect prefix pattern)Detection and Usage:
FL_IS_ARM, FL_IS_STM32, FL_IS_ESP32 and their variantsFASTLED_STM32_HAS_TIM5, FASTLED_STM32_DMA_CHANNEL_BASED (not platform IDs)#ifdef FL_IS_STM32 or #ifndef FL_IS_STM32#if defined(FL_IS_STM32) or #if !defined(FL_IS_STM32)#define FL_IS_STM32 (no value)#if FL_IS_STM32 or #if FL_IS_STM32 == 1#define FL_IS_STM32 1 (do not assign values)FASTLED_USE_PROGMEM, FASTLED_ALLOW_INTERRUPTS)FASTLED_STM32_GPIO_MAX_FREQ_MHZ 100, FASTLED_STM32_DMA_TOTAL_CHANNELS 14)#define FASTLED_USE_PROGMEM 1 or #define FASTLED_USE_PROGMEM 0#define FASTLED_STM32_GPIO_MAX_FREQ_MHZ 100#if FASTLED_USE_PROGMEM or #if FASTLED_USE_PROGMEM == 1#define FASTLED_USE_PROGMEM (missing value - ambiguous default behavior)fl/stl/compiler_control.hFL_DBG("message" << var) for debug prints (easily stripped in release builds)FL_WARN("message" << var) for warnings (persist into release builds)fl::printf, fl::print, fl::println - prefer FL_DBG/FL_WARN macros instead<< operator, NOT printf-style formatting__cxa_guard Conflicts__cxa_guard_* function calls. If Teensy's <new.h> is included after the compiler sees the static, the signatures conflict..cpp.hpp files): Include <new.h> early on Teensy 3.x to declare the guard functions before use:
// Teensy 3.x compatibility: Include new.h before function-local statics
#if defined(__MK20DX128__) || defined(__MK20DX256__) || defined(__MK64FX512__) || defined(__MK66FX1M0__)
#include <new.h>
#endif
src/fl/async.cpp.hpp (includes <new.h> early to prevent conflicts)__guard* signature.cpp file
src/platforms/shared/spi_hw_1.{h,cpp} for the correct patternci/lint_cpp/test_no_static_in_headers.py for critical directories (src/platforms/shared/, src/fl/, src/fx/)// okay static in header comment if absolutely necessary (use sparingly)int mCount;, fl::string mName;, bool mIsEnabled;int count;, fl::string name;, bool isEnabled;