Chapter 11: Advanced Core Semantics

sc_object, Names, and Hierarchy

How SystemC builds the object tree, assigns hierarchical names, registers children, and why construction order matters.

How to Read This Lesson

These core semantics are where experienced SystemC engineers earn their calm. We will name the scheduler rule, then show how the source enforces it.

sc_object, Names, and Hierarchy

Every visible SystemC component is part of an object tree rooted in the kernel. Modules, ports, exports, primitive channels, processes, and events owned by objects all inherit from or rely on sc_core::sc_object.

This matters because hierarchy is not just for pretty printing. It controls default names, full names (name()), sensitivity lookup, report context, tracing paths (VCD), CCI parameter paths, and the way tools inspect a model during elaboration and simulation.

Under the Hood: C++ Implementation in Accellera SystemC

How does the SystemC kernel know who your parent is when you instantiate an sc_object? The magic happens inside sc_simcontext via a hierarchy stack.

  1. sc_object_manager: The sc_simcontext owns an sc_object_manager which maintains the master table of every sc_object in existence, mapping string names to pointers to guarantee name uniqueness.
  2. The Active Module Stack: When a module's constructor (SC_CTOR) begins execution, the kernel pushes that module onto an internal stack (hierarchy_push). When the constructor finishes, it pops it (hierarchy_pop).
  3. Implicit Parenting: When you create an sc_port or a nested sc_module inside that constructor, the sc_object base constructor inspects the top of the sc_simcontext hierarchy stack. It automatically registers itself as a child of whatever module is currently on top. This is why you never have to pass this to child components to build the tree.

Source and LRM Trail

Advanced core behavior should always be checked against Docs/LRMs/SystemC_LRM_1666-2023.pdf before source details. For implementation, read .codex-src/systemc/src/sysc/kernel and .codex-src/systemc/src/sysc/communication, especially the scheduler, events, object hierarchy, writer policy, report handler, and async update path.

The LRM View on Elaboration and Hierarchy

SystemC operates in phases. The first phase is elaboration, where the structural model is built. During elaboration, C++ constructors execute. The IEEE 1666 LRM specifies that the SystemC kernel tracks the current construction context (using the active module). When a new sc_object (like a port, signal, or child module) is instantiated, it automatically registers itself as a child of the currently active module.

That is why this pattern works:

// Correct hierarchical instantiation
SC_MODULE(Uart) {
    sc_core::sc_in<bool> clk{"clk"}; // Becomes a child of Uart
    sc_core::sc_out<bool> irq{"irq"};
 
    SC_CTOR(Uart) {
        // SC_METHOD also registers itself as a child process of Uart
        SC_METHOD(tick);
        sensitive << clk.pos();
    }
 
    void tick() {}
};

If Uart is instantiated at the top level with the name "my_uart", the ports automatically receive the hierarchical names "my_uart.clk" and "my_uart.irq".

Construction Order and Object Lifetimes

A critical rule mandated by the LRM: Do structural construction before simulation starts. Do not create ports, exports, or ordinary modules after elaboration (e.g., inside end_of_elaboration, start_of_simulation, or process threads) and expect the design to behave correctly.

The Most Common Lifetime Bug

The most common hierarchy bug is constructing something as a local stack variable inside a constructor:

SC_CTOR(Top) {
    Uart uart{"uart"}; // ERROR: Destroyed when constructor returns!
}

The object registers itself with the kernel briefly, then C++ destroys it at the end of the scope, leaving a dangling pointer in the kernel's object hierarchy. Always use class members or dynamically allocate (new) structural components.

Explicit vs. Generated Names

Avoid creating important modules or ports with generated names (sc_core::sc_gen_unique_name) in production virtual platforms. Stable names become part of:

  • CCI configuration parameter paths
  • VCD trace paths
  • Debug messages
  • Waveform bookmarks

Complete Example: Traversing the Hierarchy

The following complete sc_main example demonstrates how to build a valid hierarchy, how object names are assigned, and how to programmatically traverse the sc_object tree using standard LRM APIs.

#include <systemc>
#include <iostream>
#include <string>
 
// A simple leaf module
SC_MODULE(Peripheral) {
    sc_core::sc_in<bool> clk{"clk"};
    
    SC_CTOR(Peripheral) {
        SC_METHOD(logic);
        sensitive << clk.pos();
    }
    void logic() {}
};
 
// A top-level module containing children
SC_MODULE(SystemTop) {
    sc_core::sc_signal<bool> sys_clk{"sys_clk"};
    Peripheral* uart;
    Peripheral* spi;
 
    SC_CTOR(SystemTop) {
        // Child objects dynamically allocated. Their lifetimes must 
        // persist throughout simulation.
        uart = new Peripheral("uart");
        spi = new Peripheral("spi");
 
        // Bindings
        uart->clk(sys_clk);
        spi->clk(sys_clk);
    }
 
    ~SystemTop() {
        delete uart;
        delete spi;
    }
};
 
// Recursive function to print the object tree
void print_hierarchy(sc_core::sc_object* obj, int depth = 0) {
    if (!obj) return;
 
    std::string indent(depth * 2, ' ');
    std::cout << indent << "- " << obj->name() 
              << " (kind: " << obj->kind() << ")\n";
 
    // Recursively visit all children
    const std::vector<sc_core::sc_object*>& children = obj->get_child_objects();
    for (auto* child : children) {
        print_hierarchy(child, depth + 1);
    }
}
 
int sc_main(int argc, char* argv[]) {
    // Instantiate the top-level module
    SystemTop top("top_module");
 
    // The kernel provides sc_get_top_level_objects() to inspect 
    // the root of the hierarchy after elaboration.
    std::cout << "--- SystemC Object Hierarchy ---\n";
    const std::vector<sc_core::sc_object*>& roots = sc_core::sc_get_top_level_objects();
    for (auto* root : roots) {
        print_hierarchy(root);
    }
    std::cout << "--------------------------------\n\n";
 
    // Start simulation (not strictly necessary here as we just want to see elaboration results)
    sc_core::sc_start(1, sc_core::SC_MS);
    
    return 0;
}

Explanation of the Output

If you run the above code, you will see output similar to this:

--- SystemC Object Hierarchy ---
- top_module (kind: sc_module)
  - top_module.sys_clk (kind: sc_signal)
  - top_module.uart (kind: sc_module)
    - top_module.uart.clk (kind: sc_in)
    - top_module.uart.logic (kind: sc_method_process)
  - top_module.spi (kind: sc_module)
    - top_module.spi.clk (kind: sc_in)
    - top_module.spi.logic (kind: sc_method_process)
--------------------------------

Notice how every sc_object's name() includes its full hierarchical path. The kind() method (from the LRM) returns a string identifying the type of the object, which is very useful for introspection tools and custom tracing setups.

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.

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.

Comments and Corrections