Source Deep Dive: TLM Socket Internals
How simple initiator and target sockets wrap interfaces, callbacks, binding, and transport dispatch.
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.
TLM sockets are convenient because they hide a lot of interface plumbing. To understand them, we must peel back the convenience layer. According to the IEEE 1666-2023 LRM Section 12, a socket is structurally composed of an sc_port and an sc_export bound to forward and backward transport interfaces.
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.
The Interfaces Underneath
TLM defines core transport interfaces like tlm_fw_transport_if and tlm_bw_transport_if. A utility target socket (simple_target_socket) implements the forward interface internally and dispatches calls to your registered member function.
To see exactly what the socket does under the hood, here is a complete, fully compilable example where the target manually implements the raw tlm_fw_transport_if and uses standard sc_export to receive transactions, completely bypassing simple_target_socket for the target side!
#include <systemc>
#include <tlm>
#include <tlm_utils/simple_initiator_socket.h>
using namespace sc_core;
// 1. The Raw Core Interface Target (What simple_target_socket hides)
class RawMemoryTarget : public sc_module, public tlm::tlm_fw_transport_if<> {
public:
// Expose the interface outward
sc_export<tlm::tlm_fw_transport_if<>> target_export{"target_export"};
SC_CTOR(RawMemoryTarget) {
// Bind the export to 'this' module, which implements the interface
target_export.bind(*this);
}
// --- Implement tlm_fw_transport_if ---
void b_transport(tlm::tlm_generic_payload& trans, sc_time& delay) override {
std::cout << "[RawTarget] b_transport called with address 0x"
<< std::hex << trans.get_address() << "\n";
trans.set_response_status(tlm::TLM_OK_RESPONSE);
}
tlm::tlm_sync_enum nb_transport_fw(tlm::tlm_generic_payload& trans,
tlm::tlm_phase& phase, sc_time& delay) override {
return tlm::TLM_COMPLETED;
}
bool get_direct_mem_ptr(tlm::tlm_generic_payload& trans,
tlm::tlm_dmi& dmi_data) override {
return false;
}
unsigned int transport_dbg(tlm::tlm_generic_payload& trans) override {
return 0;
}
};
// 2. Standard Utility Initiator
SC_MODULE(CpuInitiator) {
tlm_utils::simple_initiator_socket<CpuInitiator> socket{"socket"};
SC_CTOR(CpuInitiator) { SC_THREAD(run); }
void run() {
tlm::tlm_generic_payload trans;
sc_time delay = SC_ZERO_TIME;
trans.set_command(tlm::TLM_WRITE_COMMAND);
trans.set_address(0x1000);
// operator-> on the socket accesses the bound tlm_fw_transport_if!
socket->b_transport(trans, delay);
}
};
int sc_main(int argc, char* argv[]) {
CpuInitiator cpu("cpu");
RawMemoryTarget mem("mem");
// The initiator socket contains an sc_port which binds to the sc_export
cpu.socket.bind(mem.target_export);
sc_start();
return 0;
}When you use target.register_b_transport(...), the utility socket creates a small internal channel object exactly like the RawMemoryTarget above, storing your object pointer and member function pointer, and dispatches the raw b_transport interface call into your callback.
Initiator Socket
The initiator socket acts like a typed access point to the target's transport interface. When you call socket->b_transport(...), that operator call reaches the bound target interface. The initiator does not need to know whether the target is a raw module implementing the interface or a utility socket doing callback dispatch.
Generic Payload Lifetime
Sockets dispatch payload references. They do not magically copy all transaction data. That means payload lifetime and extension ownership matter.
For blocking transport, a stack payload is fine (as shown in the example). For non-blocking transport with deferred completion, the transaction may outlive the call. Then you need a disciplined ownership strategy, often with a memory manager.
Why Socket Internals Matter
When a TLM model fails, the bug is often one of these:
- socket not bound
- callback not registered
- payload reused too early
- response status not set
- delay not updated consistently
Understanding sockets as standard sc_port binding plus callback dispatch makes those bugs much easier to diagnose.
Exhaustive Deep Dive: IEEE 1666-2023 LRM and Accellera Socket Architecture
Sockets are often taught as a magical connection point, but they are strictly defined by IEEE 1666-2023 LRM Section 16 (TLM Sockets) as structural groupings of standard SystemC sc_port and sc_export objects.
LRM Section 16.1: Socket Structure
The LRM dictates that every TLM-2.0 socket must contain two communication paths to support the core transport interfaces defined in Section 12.2:
- A Forward Path: To send requests from initiator to target using
tlm_fw_transport_if. - A Backward Path: To send responses from target to initiator using
tlm_bw_transport_if.
LRM Clause 16.1.1 defines tlm_initiator_socket. It mandates that an initiator socket has-a sc_port for the forward path and has-a sc_export for the backward path.
LRM Clause 16.1.2 defines tlm_target_socket, which reverses this: it has-a sc_export for the forward path (to receive requests) and has-a sc_port for the backward path (to send responses).
Inside tlm_core: The Base Socket Classes
If you look inside src/tlm_core/tlm_2/tlm_sockets/tlm_initiator_socket.h, the class tlm_base_initiator_socket explicitly implements this structural requirement:
template <unsigned int BUSWIDTH, typename FW_IF, typename BW_IF, int N, sc_core::sc_port_policy POL>
class tlm_base_initiator_socket : public sc_core::sc_port<FW_IF, N, POL> {
protected:
sc_core::sc_export<BW_IF> m_bw;
public:
virtual sc_core::sc_export<BW_IF>& get_base_export() { return m_bw; }
virtual sc_core::sc_port_b<FW_IF>& get_base_port() { return *this; }
};Notice that the initiator socket inherits directly from sc_port. That is why you can call socket->b_transport(...). The overloaded operator-> belongs to the underlying sc_port, which forwards the call to the bound sc_export on the target.
How socket.bind() Actually Works
When you write cpu.socket.bind(mem.socket), you are invoking the bind() method defined in tlm_initiator_socket.h.
virtual void bind(base_target_socket_type& s) {
// 1. Forward path: Bind the initiator's sc_port to the target's sc_export
(this->get_base_port())(s.get_base_export());
// 2. Backward path: Bind the target's sc_port to the initiator's sc_export
(s.get_base_port())(this->get_base_export());
}A socket bind is literally just a macro-like helper that performs two standard SystemC port-to-export bindings simultaneously. Because it relies on standard sc_port binding, the elaboration rules, sc_port_registry, and complete_binding() algorithms (discussed in earlier tutorials) execute exactly the same way.
The Utility Sockets (simple_target_socket)
While tlm_target_socket provides the raw ports, developers rarely want to write an entire module inheriting from tlm_fw_transport_if (as shown in the code above). LRM Section 16.2 provides convenience sockets.
In src/tlm_utils/simple_target_socket.h, the simple_target_socket instantiates an internal helper class: fw_process.
This fw_process inherits from tlm_fw_transport_if.
When you call socket.register_b_transport(this, &MyModule::my_b_transport), the socket stores your C++ member function pointer inside the fw_process.
The socket then binds its own sc_export to this internal fw_process object.
During simulation:
- The initiator calls
socket->b_transport(...). - The virtual function call lands in the target's
fw_process::b_transport. - The
fw_processextracts the stored C++ member function pointer and executes it on your module instance.
Multi-Sockets and the Hierarchical Bind Matrix
If you look into src/tlm_utils/multi_passthrough_initiator_socket.h, the complexity multiplies. A multi-socket maintains an array or vector of internal sc_ports and sc_exports. When b_transport is invoked, it uses the transaction's address (often passed through an interconnect) or an explicit socket index to select which specific sc_port array element to dispatch through.
By reading the source code of the sockets, the illusion of TLM magic fades. A socket is not a proprietary high-speed simulation pipeline; it is just an elegant C++ wrapper that enforces the LRM's two-way port/export binding rules and automates callback dispatch.
Comments and Corrections