Chapter 5: Source Internals

Source Deep Dive: Port Binding and Interface Dispatch

How sc_port, sc_export, sc_interface, and channels connect modules during elaboration.

How to Read This Lesson

This is a source-reading lesson. We will use the Accellera implementation as a microscope, while keeping the LRM as the portability contract.

Port binding is where SystemC turns a C++ object hierarchy into a connected model graph. The implementation must answer what interface is required, what object provides it, and if the binding is complete.

Source and LRM Trail

This lesson is deliberately source-facing. Use Docs/LRMs/SystemC_LRM_1666-2023.pdf to decide what must be portable, then use .codex-src/systemc/src/sysc and .codex-src/systemc/src/tlm_core to see one reference implementation. Treat private members as explanatory, not as APIs your models should depend on.

Interfaces, Ports, and Channels

An interface derives from sc_interface. A port requires an interface. A channel implements the interface. Binding must happen before simulation time begins, allowing the kernel to validate the structural integrity.

Here is a complete, compilable example illustrating how an sc_port resolves a C++ virtual function call to an sc_interface implemented by an sc_export mapping to a channel.

#include <systemc>
#include <iostream>
 
using namespace sc_core;
 
// 1. The Interface Contract
struct BusIf : public sc_interface {
  virtual uint32_t read(uint64_t address) = 0;
  virtual void write(uint64_t address, uint32_t data) = 0;
};
 
// 2. The Channel Implementation
class MemoryChannel : public sc_module, public BusIf {
public:
  SC_CTOR(MemoryChannel) {}
 
  uint32_t read(uint64_t address) override {
    std::cout << "Memory handling READ at " << std::hex << address << "\n";
    return 0xCAFE;
  }
  void write(uint64_t address, uint32_t data) override {
    std::cout << "Memory handling WRITE at " << std::hex << address << " data: " << data << "\n";
  }
};
 
// 3. The Subsystem Exposing the Channel via sc_export
SC_MODULE(MemorySubsystem) {
  sc_export<BusIf> target{"target"};
  MemoryChannel memory{"memory"};
 
  SC_CTOR(MemorySubsystem) {
    // Export binds to the internal channel implementation
    target.bind(memory);
  }
};
 
// 4. The Initiator Using the Channel via sc_port
SC_MODULE(CpuModel) {
  sc_port<BusIf> bus{"bus"};
 
  SC_CTOR(CpuModel) { SC_THREAD(run); }
 
  void run() {
    // The operator-> provides access to the bound interface
    // The call becomes a virtual function call on the channel implementation
    bus->write(0x1000, 0x1234);
    uint32_t val = bus->read(0x1000);
  }
};
 
int sc_main(int argc, char* argv[]) {
  CpuModel cpu("cpu");
  MemorySubsystem subsystem("subsystem");
 
  // Elaboration phase: Structural binding
  cpu.bus.bind(subsystem.target);
 
  sc_start(); // Simulation phase
  return 0;
}

Binding During Elaboration

Binding happens before simulation. That matters because the kernel can validate topology before any process runs. At the end of elaboration, the kernel checks that required ports are bound and that binding policies are satisfied.

Calling Through a Port

Once bound, the module can call through the port using operator->. The useful abstraction: structural binding is checked during elaboration, while actual behavior is ordinary C++ interface dispatch (virtual function calls) during simulation.

How This Relates to TLM Sockets

TLM sockets are higher-level wrappers around the exact same ideas. An initiator socket contains a port to a forward transport interface. A target socket exposes an export for that interface and dispatches calls to registered callbacks. The magic is packaging, not a different universe.

Standard and Source Deep Dive: Port Binding

Port binding is the topological glue of a SystemC model. The IEEE 1666-2023 LRM Sections 4.2.1 (Elaboration) and Section 6.11-6.13 (Ports, Exports, Interfaces) rigidly define how structural connections are made and verified.

The LRM Mandates: Interfaces, Ports, and Exports

LRM Clause 6.11 (sc_interface): Every interface in SystemC must derive from the abstract base class sc_interface. Its primary requirement is a virtual register_port() method, which allows the kernel to track which ports are bound to which channel.

LRM Clause 6.12 (sc_port): A port specifies the interface it requires. A port can be bound to a channel, to another port (hierarchical port-to-port binding), or to an export.

LRM Clause 6.13 (sc_export): An export specifies an interface that a module provides from its internal implementation up to its parent module.

The LRM dictates that binding occurs strictly during Elaboration. Once sc_start() is called, the hierarchy is locked. You cannot dynamically instantiate modules or bind ports during simulation time.

Inside the Accellera Source: sc_port_b and sc_port_registry

In src/sysc/communication/sc_port.h/cpp, all specialized sc_port<IF> classes derive from a non-template base class sc_port_b. When you declare sc_port<BusIf> bus{"bus"};, the constructor ultimately calls sc_simcontext::get_port_registry()->insert(this).

The sc_port_registry (located in src/sysc/kernel/sc_simcontext.cpp) is the global list of every port in the simulation.

When you write cpu.bus.bind(subsystem.target); in your C++ code, you are invoking the bind() method on sc_port. However, this does not immediately resolve the C++ pointer! Instead, the port simply stores a generic pointer to the bound object in an internal array (because a port can be bound to multiple channels if the port's N parameter is > 1).

The Elaboration Phase: complete_binding()

The real magic happens when sc_start() is called. Before simulation begins, sc_start() invokes sc_simcontext::elaborate(), which ultimately calls sc_port_registry::complete_binding().

If you trace sysc/kernel/sc_simcontext.cpp, you will see complete_binding() iterate over every single port in the design. For each port:

  1. It traverses the binding tree. If Port A is bound to Port B, and Port B is bound to Channel C, it recursively walks from A -> B -> C to find the actual sc_interface implementation.
  2. Type Checking: It uses C++ RTTI (dynamic_cast) to verify that the target object actually implements the interface required by the port.
    // Abstract representation of the kernel's check:
    sc_interface* target_if = dynamic_cast<sc_interface*>(bound_object);
    if (!target_if) { SC_REPORT_ERROR("Port binding failed: interface mismatch"); }
  3. It resolves the final interface pointer and stores it directly inside the port's m_interface pointer array.

Zero-Overhead Simulation Dispatch

Why delay pointer resolution until complete_binding()? Because once elaboration finishes, the port has an absolute, direct C++ pointer to the implementing channel.

In src/sysc/communication/sc_port.h, the overloaded operator-> is extraordinarily simple:

template <class IF>
inline IF* sc_port<IF>::operator -> () {
    return m_interface;
}

During simulation, when a thread executes bus->write(0x10, data);, there are no map lookups, no string comparisons, and no routing tables. It is exactly equivalent to a direct C++ virtual function call on the channel object.

This guarantees that the structural abstraction of SystemC modules and ports imposes zero performance overhead during the simulation phase. The structural validation is paid for entirely during the one-time elaboration phase.

Comments and Corrections