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.
sc_object_manager: Thesc_simcontextowns ansc_object_managerwhich maintains the master table of everysc_objectin existence, mapping string names to pointers to guarantee name uniqueness.- 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). - Implicit Parenting: When you create an
sc_portor a nestedsc_moduleinside that constructor, thesc_objectbase constructor inspects the top of thesc_simcontexthierarchy stack. It automatically registers itself as a child of whatever module is currently on top. This is why you never have to passthisto child components to build the tree.
Standard and source context
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.
Source-reading checkpoint
For hierarchy questions, inspect Accellera sc_module, sc_object, and object-manager code together. Naming is not decoration; it is part of how tools find the model. For bound children, continue into sc_port_registry after the object hierarchy is clear.
Can you answer these clearly?
Keep moving when you can answer each question without looking back at the lesson.
Comments and Corrections