The Virtual Interface Bridge
The generator and its mailbox, built in Section B, live entirely inside class-based, procedural code — no module, no hierarchy, no physical connection to anything. The FIFO DUT and its pins exist only inside a module's static hierarchy. Before a driver or monitor class can touch a single pin, something has to bridge those two worlds. This page is that bridge, on its own, before either component is built.
Why a plain interface handle doesn't work here
An interface instance, like fifo_if_inst in a testbench-top module, lives at a fixed point in the module hierarchy — exactly like a module instance does. A class, by contrast, has no hierarchical position at all; it's constructed and destroyed dynamically, wherever new() happens to be called. A class simply cannot declare a property of a plain fifo_if type and have it mean anything — there's no static hierarchy for that reference to resolve against.
virtual: an interface handle a class can actually hold
class driver;
virtual fifo_if vif;
// ...
endclass
The virtual keyword turns fifo_if into a handle type — conceptually similar to a class handle — that a class can store as an ordinary property, with no fixed hierarchical position of its own. It starts out null, exactly like an unconstructed object handle, until something explicitly assigns it to a real interface instance that does exist somewhere in the module hierarchy.
That null starting value isn't just a formality — a component that tries to drive or read through vif before anything has assigned it (say, a driver whose constructor argument was accidentally omitted) hits a null-handle dereference at the moment it's used, not at elaboration time: simulation reports a fatal runtime error and stops right there, rather than silently doing nothing or defaulting to some harmless value. The fix is always the same one this page already builds — make sure a real interface instance reaches the handle before run() ever executes.
Assigning it, the only way available before uvm_config_db exists
module tb_top;
bit clk, rst_n;
always #5 clk = ~clk;
fifo_if fifo_if_inst (.clk(clk), .rst_n(rst_n));
sync_fifo dut (
.clk(clk), .rst_n(rst_n),
.wr_en(fifo_if_inst.wr_en), .wr_data(fifo_if_inst.wr_data), .full(fifo_if_inst.full),
.rd_en(fifo_if_inst.rd_en), .rd_data(fifo_if_inst.rd_data), .empty(fifo_if_inst.empty)
);
initial begin
rst_n = 0;
#20 rst_n = 1;
// fifo_if_inst is a real interface instance, sitting right here in tb_top's
// own hierarchy — the driver's constructor is handed a virtual handle to it
driver drv = new(fifo_if_inst, gen2drv);
end
endmodule
new(fifo_if_inst, gen2drv) passes the real interface instance directly as a constructor argument — SystemVerilog implicitly converts it to the virtual fifo_if type the constructor expects. This is the only mechanism available to this topic: no lookup-by-name, no indirection, just a handle passed explicitly, once, at construction time.
uvm_config_db exists to closeEvery component that needs vif — driver, monitor, and anything else built later — needs it threaded through its own constructor by hand, from whoever constructs it, all the way from tb_top. That's manageable with two components. It stops being manageable the moment components nest several layers deep inside an environment, which is precisely the pain UVM's Resource DB & Config DB solves — a single set() call in tb_top, retrievable from anywhere in an arbitrarily deep tree, with no constructor threading at all. Worth remembering this exact moment when that page's motivation is described later.
What's next
With a way to reach real pins established, the next page builds the first component that actually uses it: the driver.