Every SystemC channel you have ever used — sc_signal, sc_fifo, sc_mutex, and every custom channel you write — inherits from a single abstract root: sc_interface. It is the smallest, most important class in the entire SystemC class hierarchy, and yet it is rarely mentioned in introductory material.

Understanding sc_interface is not an academic exercise. It explains why modules do not need to know the concrete type of the channel they use, how you design reusable channel APIs, and why TLM 2.0 transaction-level modelling works the way it does. This article covers the Interface Method Call (IMC) pattern from first principles, walks through building a complete custom interface and channel, and shows you where the pattern fits in production-grade SystemC codebases.

1. What Problem Does sc_interface Solve?

Consider the naive approach to channel communication: a module holds a direct pointer to the concrete channel type.

// Naive — module is tightly coupled to the channel type
SC_MODULE(BusMaster) {
    SimpleRam* ram;   // raw pointer to concrete type

    void run() {
        ram->write(0x1000, 0xDEAD);
        uint32_t val = ram->read(0x1000);
    }
};

This compiles and runs fine — until you want to swap SimpleRam for a FlashModel, a RegisterFile, or a cycle-accurate DdrController. You have to edit BusMaster every time. In a design with dozens of initiators, that is dozens of edits and dozens of recompilations.

The hardware world solved this problem long ago. A bus master on an AHB bus does not know — or care — whether the target is SRAM, flash, or a peripheral register block. It issues read and write transactions to the bus. The bus routes the transaction to whatever target is mapped at that address. The master and target are decoupled by a protocol, not by a direct wire.

The Interface Method Call (IMC) pattern is SystemC's software expression of the same idea. Instead of a raw pointer to a concrete channel, the module holds an sc_port<IF> — a typed reference to an abstract interface. The module calls methods on the interface. The channel implements those methods. At elaboration time, the port is bound to the channel. The module never sees the concrete type.

IMC in one sentence. The module calls through the interface; the channel implements the interface; sc_port connects them. The module is written once and works with any channel that satisfies the interface contract.

2. sc_interface — The Abstract Base

In the SystemC standard library, sc_interface is declared as:

// From <systemc/kernel/sc_interface.h> (simplified)
class sc_interface {
public:
    virtual void             register_port(sc_port_base&, const char*) {}
    virtual const sc_event&  default_event() const;
    virtual                  ~sc_interface() {}
protected:
    sc_interface() {}
private:
    sc_interface(const sc_interface&) = delete;
};

Three things to notice. First, sc_interface itself declares no domain-specific methods — no read(), no write(), nothing about data. It is a pure structural marker. Second, the constructor is protected — you cannot instantiate sc_interface directly; you must subclass it. Third, it is non-copyable — channels are identity objects, not values.

The two methods it does declare are kernel hooks, not user-visible API:

register_port()

The kernel calls register_port(port, if_typename) automatically during elaboration whenever an sc_port is bound to this channel. The default implementation does nothing. Override it when you need to enforce binding constraints — for example, limiting a channel to a single writer:

class SingleWriterBus : public sc_prim_channel, public bus_if {
    int writer_count_ = 0;
public:
    void register_port(sc_port_base& port, const char* if_typename) override {
        // if_typename identifies which interface the port wants
        if (std::string(if_typename) == typeid(bus_write_if).name()) {
            if (++writer_count_ > 1)
                SC_REPORT_ERROR("BUS", "Only one writer allowed");
        }
    }
    // ... implement bus_if methods ...
};

Most channels never need to override register_port(). The default empty body is fine unless you have explicit multi-master constraints.

default_event()

When a process writes sensitive << my_port without specifying a particular event, the kernel calls default_event() on the bound channel to find out what event to use. For sc_signal, the default event is value_changed_event(). For sc_fifo, it is data_written_event().

The base sc_interface implementation throws a runtime error if called — it forces you to think about what your channel's "natural wakeup trigger" is. If your channel has no meaningful default event (e.g. a memory that processes poll explicitly), simply do not override it. If it does, implement it:

class MyChannel : public sc_prim_channel, public my_if {
    sc_event m_data_event;
public:
    // Processes can now write: sensitive << my_port
    // and wake up whenever notify_data() is called
    const sc_event& default_event() const override { return m_data_event; }
private:
    void notify_data() { m_data_event.notify(SC_ZERO_TIME); }
};

Every channel in SystemC — sc_signal, sc_fifo, sc_mutex, sc_semaphore, and anything you write yourself — inherits from sc_interface. It is the common root that allows sc_port to work with any of them without knowing which one it has.

Here is the full picture — who derives from whom and who calls whom at runtime:

// class hierarchy + runtime call flow

CLASS HIERARCHY sc_prim_channel request_update() · name sc_interface register_port() · default_event() mem_read_if virtual read() = 0 mem_write_if virtual write() = 0 mem_if read_if + write_if SimpleRam read() + write() — concrete : sc_prim_channel, mem_if virtual virtual virtual virtual extends extends RUNTIME CALL FLOW Master sc_port<mem_if> mem mem→read() / mem→write() SystemC Kernel elaboration · port binding sensitive<< · event dispatch SimpleRam read(uint32_t addr) write(addr, data) — kernel hooks — register_port(port, type) default_event() → event& override optionally mem→read() / write() register_port() on bind default_event() on sensitive<< user calls kernel calls inherits (virtual) {ErrBits}
Blue = SystemC built-ins  ·  Green = your code  ·  White = concrete implementation

3. Defining a Custom Interface

A custom interface is a C++ abstract class that inherits from sc_interface and declares pure virtual methods for every operation a module should be able to call. The interface contains no data members, no implementation — only the API contract.

#include <systemc.h>
#include <cstdint>

// ── Read-only memory interface ────────────────────────────────
class mem_read_if : public virtual sc_interface {
public:
    virtual uint32_t read(uint32_t addr) = 0;
    virtual ~mem_read_if() {}
};

// ── Write-only memory interface ───────────────────────────────
class mem_write_if : public virtual sc_interface {
public:
    virtual void write(uint32_t addr, uint32_t data) = 0;
    virtual ~mem_write_if() {}
};

// ── Combined read/write interface — for read-write initiators ─
class mem_if : public virtual mem_read_if,
               public virtual mem_write_if {
public:
    virtual ~mem_if() {}
};

Several design rules apply here and they are worth internalising:

Virtual inheritance is mandatory. When a channel class like SimpleRam inherits from both mem_read_if and mem_write_if, and both of those inherit from sc_interface, you must use virtual inheritance in the interface declarations. Without it, C++ creates two separate sc_interface subobjects in SimpleRam, causing ambiguous base class errors. The public virtual sc_interface syntax in each interface class collapses them into one shared subobject.
What comes next in the series.

You now have the interface contract. Three more pieces complete the picture:

  • Article 02 — sc_port and sc_export: how a module connects to a channel through a typed port, and how a module exposes an interface through an export.
  • Article 03 — sc_prim_channel: how a channel registers with the kernel and implements the evaluate-update protocol for deferred semantics.
  • Article 04 — IMC: the Complete Picture: interface + channel + port wired together in a full working example, and how TLM 2.0 uses the same pattern.

4. Common Pitfalls

A module that needs to call methods on a channel declares an sc_port templated on the interface type. The port type parameter tells the SystemC kernel which interface the bound channel must implement — it is a compile-time contract.

SC_MODULE(Master) {
    // Read/write port — channel must implement mem_if
    sc_port<mem_if> mem_port;

    SC_CTOR(Master) { SC_THREAD(run); }

    void run() {
        // Write through the port — calls into whatever channel is bound
        mem_port->write(0x0000, 0xCAFE);
        mem_port->write(0x0004, 0xBABE);

        // Read through the port
        uint32_t v0 = mem_port->read(0x0000);
        uint32_t v1 = mem_port->read(0x0004);

        cout << "[MASTER] read back: 0x" << hex << v0
             << ", 0x" << v1 << endl;
    }
};

// A read-only observer — only needs mem_read_if
SC_MODULE(Monitor) {
    sc_port<mem_read_if> rd_port;   // cannot call write() — compile error if tried

    SC_CTOR(Monitor) { SC_THREAD(sample); }

    void sample() {
        wait(50, SC_NS);
        uint32_t val = rd_port->read(0x0000);
        cout << "[MON]   sampled: 0x" << hex << val << endl;
    }
};

The -> operator on sc_port is overloaded to return a pointer to the bound interface object. At elaboration time, after sc_main has bound ports to channels, this pointer is set. Before elaboration completes, calling through an unbound port is a fatal runtime error — the kernel checks this.

Pitfall 1 — Forgetting the virtual destructor

This is the most common mistake and it produces no compile error. If your interface has no virtual destructor and someone calls delete ptr through a base-class pointer, only the base destructor runs — the derived channel object leaks. Some compilers and static analysis tools will warn (-Wnon-virtual-dtor in GCC/Clang); many will not.

// WRONG — memory leak if channel is deleted through this pointer
class bad_if : public virtual sc_interface {
public:
    virtual uint32_t read(uint32_t addr) = 0;
    // no virtual destructor!
};

// CORRECT
class good_if : public virtual sc_interface {
public:
    virtual uint32_t read(uint32_t addr) = 0;
    virtual ~good_if() {}   // always add this
};

Pitfall 2 — Putting data members or logic in the interface

An interface with state is no longer an interface — it is a partially-implemented channel. Any data you put in the interface is shared structurally across all implementations, which creates confusion about ownership, initialisation, and reset. Keep interfaces pure: pure virtual methods, virtual destructor, nothing else.

// WRONG — state in the interface
class mem_if_bad : public virtual sc_interface {
public:
    uint32_t base_addr = 0;   // NO — data belongs in the channel
    virtual uint32_t read(uint32_t addr) = 0;
    virtual ~mem_if_bad() {}
};

Pitfall 3 — Forgetting virtual inheritance

If mem_read_if and mem_write_if each inherit non-virtually from sc_interface, then a class like SimpleRam that inherits from both has two separate sc_interface base subobjects. The compiler will reject port binding because it cannot unambiguously convert SimpleRam* to sc_interface*.

// WRONG — non-virtual inheritance causes ambiguous base
class mem_read_if  : public sc_interface { /* ... */ };  // missing virtual
class mem_write_if : public sc_interface { /* ... */ };  // missing virtual

// CORRECT — both use virtual inheritance
class mem_read_if  : public virtual sc_interface { /* ... */ };
class mem_write_if : public virtual sc_interface { /* ... */ };

Summary


Channels & Interfaces — 4-part series

01 sc_interface — The Interface Contract ← here 02 sc_prim_channel — Implementing a Channel 03 sc_port & sc_export — Connecting Modules 04 IMC — The Complete Picture

📬 Get new articles in your inbox

Deep dives on SystemC, C++, and embedded systems — no spam, unsubscribe any time.

No spam, unsubscribe any time. Privacy Policy

Aditya Gaurav

Aditya Gaurav

Embedded systems engineer specializing in SystemC, ARM architecture, and C/C++ internals. Writing deep technical dives for VLSI and embedded engineers.

Part of the SystemC Foundations guide Browse all SystemC guides →