Skip to content

lince_defaults

Reference implementations of the I* interfaces, used by the standalone build. An SMP2 wrapper, a Python binding, or a custom test harness can replace any of them by calling the matching Emulator::set_* setter.

Depends on lince_interfaces only — there are no dependencies on the core, bus, or peripherals.

Source layout

src/defaults/
├── CMakeLists.txt
└── src/
    ├── stdout_logger.cpp
    ├── stdout_char_device.cpp
    ├── debug_publisher.cpp
    └── null_fault_injector.cpp

The headers are intentionally co-located with the .cpp files (one per implementation) because no other module is supposed to consume them by type — consumers see them through the I* interfaces.

StdoutLogger

ILogger implementation that writes to stderr via fprintf. Supports four severity levels (Debug, Info, Warn, Error) and a configurable threshold.

class StdoutLogger final : public ILogger {
public:
    explicit StdoutLogger(LogLevel threshold = LogLevel::Info);
    void log(LogLevel, std::string_view) override;
    void set_threshold(LogLevel);
private:
    LogLevel threshold_;
};

The CLI sets the threshold to Debug when --verbose is passed.

StdoutCharDevice

ICharacterDevice implementation that writes TX bytes via putchar (effectively unbuffered — the implementation calls fflush(stdout) after each byte to keep RTEMS UART output interleaved with logs in predictable order). RX bytes come from getchar() in non-blocking mode (O_NONBLOCK on STDIN_FILENO).

class StdoutCharDevice final : public ICharacterDevice {
public:
    StdoutCharDevice();
    void transmit(std::uint8_t) override;
    bool receive(std::uint8_t&) override;
};

NullFaultInjector

IFaultInjector no-op implementation. Returns "no fault, hardware healthy" for every call. The hooks exist in the core (read paths, register accesses) so that a Level-3 implementation can be plugged in later without refactoring.

class NullFaultInjector final : public IFaultInjector {
public:
    bool inject_register_fault(CoreId, std::uint8_t /*reg*/) override
                                                              { return false; }
    bool inject_memory_fault(PhysAddr, AccessSize) override   { return false; }
    void publish(IPublisher&) override {}
};

Why have a no-op interface at all? Two reasons:

  1. The call sites in the core are part of the design — adding them later would mean changing every read path. Better to wire the interface in once and noop the implementation.
  2. SMP2 wrappers may surface fault-injection as model operations; the no-op default lets the standalone build ignore them entirely.

DebugPublisher

IPublisher implementation that dumps every registered observable field to stderr as JSON when its dump() method is called. Used by the CLI's --verbose mode and by integration tests that want to snapshot peripheral state.

class DebugPublisher final : public IPublisher {
public:
    void register_field(std::string_view name, FieldType,
                        FieldGetter, FieldSetter) override;
    void dump() const;
};

A future SMP2 wrapper replaces this with Smp::IPublication, which has the same shape (named fields, typed getters/setters).

Why no header per class?

Consumers of lince_defaults see only the I* interfaces — there is no need to expose StdoutLogger as a type. The implementation is chosen via:

emu->set_logger(std::make_unique<StdoutLogger>(LogLevel::Debug));

…which is what lince_app/main.cpp does. Library users in other contexts substitute their own implementations and never include any header from lince_defaults at all.