sc_interface in SystemC — The Interface Method Call Pattern
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.
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
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:
- Inherit
virtualfromsc_interface. All interface classes in a hierarchy must use virtual inheritance to prevent duplicate base class subobjects when a channel implements multiple interfaces. Forgettingvirtualhere will cause cryptic compile errors or linker problems. - Always declare a virtual destructor. Without it, deleting a pointer-to-interface leaks the derived channel object. Many compilers will warn; some won't. Always add it.
- Use the
_ifsuffix. The SystemC community convention ismem_if,bus_if,dma_if. It clearly signals "this is an interface, not a channel or module" when reading unfamiliar code. - Split read and write interfaces. A read-only initiator should hold
sc_port<mem_read_if>, notsc_port<mem_if>. Splitting interfaces enforces access control at compile time and documents intent clearly. - No data members. An interface that holds state is a channel, not an interface. Keep the contract pure.
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.
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
sc_interfaceis the abstract base class of every SystemC channel. It carries no domain methods — only two kernel hooks:register_port()(called on port binding) anddefault_event()(called onsensitive << port).- Define a custom interface by inheriting
public virtual sc_interfaceand declaring pure virtual methods. Always add a virtual destructor. Never add data members — an interface that holds state is a channel, not an interface. - Use
virtualinheritance in all interface declarations. Without it, a channel that implements two interfaces gets duplicatesc_interfacesubobjects and the compiler rejects the binding. - Use the
_ifsuffix convention and split read and write into separate interfaces so a read-only initiator cannot accidentally callwrite(). - The three interface-level mistakes: missing virtual destructor, forgetting
virtualinheritance, and putting data in the interface. Port binding, channel implementation, and the complete wiring are covered in Articles 02–04 of this series.
Channels & Interfaces — 4-part series