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.
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:
- 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_interfaceimplementation. - 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"); } - It resolves the final interface pointer and stores it directly inside the port's
m_interfacepointer 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