Logging and Call Monitoring

ml-hpi can generate logging shim classes that intercept every call through an interface, notify a language-specific logger, and delegate to the real implementation. This enables call tracing, latency measurement, protocol checking, and coverage collection without modifying user code.

Log-Level Annotations

Methods and interfaces can carry a log_level annotation that declares the default verbosity of each operation.

In YAML:

ml-hpi:
  interfaces:
  - name: pkg.RegIf
    log_level: debug           # interface-level default
    methods:
    - name: write32
      rtype: void
      attr:
      - log_level: debug       # method-level override
    - name: reset
      rtype: void
      attr:
      - log_level: info

As pragma comments in any language:

virtual void write32(uint64_t addr, uint32_t data) = 0;  // ml-hpi: log_level=debug

Valid levels: off (0), error (1), warning (2), info (3, default), debug (4), trace (5).

Generated Artifacts

The shim generator (--lang shim-cpp, shim-python, shim-sv) produces per interface:

  • Argument packs – a struct/dataclass per method holding all parameter values with a to_string() method.

  • Result packs – a struct/dataclass per non-void method holding the return value.

  • Call context – carries interface/method IDs, path, log level, argument pack, and result pack.

  • Logger interfaceon_enter(ctx) / on_leave(ctx).

  • Logging shim class – implements the interface, delegates to an inner implementation, calls the logger on enter/leave.

The ID table generator (--lang ids) produces constant tables mapping interface and method names to stable numeric IDs.

Usage

$ python -m ml_hpi generate --spec spec.yaml --outdir gen/ \
    --lang cpp,shim-cpp,ids

C++ example:

#include "pkg.hpp"
#include "pkg_shim.hpp"

class MyPrintLogger : public pkg::RegIfLogger {
public:
    void on_enter(const pkg::RegIf_call_context &ctx) override {
        std::cout << "[ENTER] " << ctx.to_string() << std::endl;
    }
    void on_leave(const pkg::RegIf_call_context &ctx) override {
        std::cout << "[LEAVE] " << ctx.to_string() << std::endl;
    }
};

// Wrap an implementation:
MyRegIfImpl impl;
MyPrintLogger logger;
pkg::RegIfLoggingShim shim(&impl, &logger, 4 /* debug threshold */);
shim.write32(0x1000, 0xDEAD);
// Output:
//   [ENTER] RegIf.write32(addr=4096, data=57005)
//   [LEAVE] RegIf.write32(addr=4096, data=57005)

Fan-Out

Each shim holds a single logger. To notify multiple listeners, use a DispatchLogger that forwards to sub-loggers:

class RegIfDispatchLogger : public RegIfLogger {
    std::vector<RegIfLogger *> subs_;
public:
    void add(RegIfLogger *s) { subs_.push_back(s); }
    void on_enter(const RegIf_call_context &ctx) override {
        for (auto *s : subs_) s->on_enter(ctx);
    }
    void on_leave(const RegIf_call_context &ctx) override {
        for (auto *s : subs_) s->on_leave(ctx);
    }
};

Limitations

  • Member accessors (regs(), ports_at()) are passed through without interception. Wrap sub-interfaces with their own shims for full-hierarchy logging.

  • to_string() on argument/result packs is available but not called by the shim itself. Loggers should call it only when they intend to emit output.

  • Thread safety is the logger’s responsibility.

  • Timestamp capture is the logger’s responsibility.