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.

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.

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.

Comments and Corrections