Part 2 · OOP for Verification · Intermediate
Why Virtual Interfaces Exist
The static instance world vs the dynamic class world, the binding problem, and what a vif actually stores.
Two worlds that cannot see each other
SystemVerilog has two object lifetimes. The static world — modules, interfaces, nets — is built once during elaboration , before simulation time zero. Every instance has a fixed hierarchical path like tb_top.bif, known at compile time. The dynamic world — class objects — is created at runtime with new(), lives on the heap, has no hierarchical path, and can be created in any quantity at any time.
The problem: a driver class needs to wiggle DUT pins, but it cannot contain an interface instance (classes cannot instantiate static objects), and hard-coding a hierarchical path like tb_top.bif.addr inside the class would weld the class to one specific testbench — destroying reuse, which was the entire reason to use classes. The class needs a way to be pointed at some interface instance, decided at runtime, without naming it in source.
THE BINDING PROBLEM
STATIC WORLD DYNAMIC WORLD
(fixed at elaboration) (created at runtime)
────────────────────── ─────────────────────
tb_top env = new()
├─ bus_if bif0 (clk) ├─ drv = new()
├─ bus_if bif1 (clk) │ needs pins... WHICH pins?
└─ dut u_dut (bif0, bif1) └─ mon = new()
needs pins... WHICH pins?
│ ▲
│ virtual bus_if vif = bif0; │
└────────────── BINDING ──────────────┘
a runtime reference from the
dynamic world to one static instanceWhat a virtual interface actually stores
A virtual bus_if variable stores a reference (conceptually a pointer) to one elaborated interface instance. It does not copy the interface, does not create a new one, and weighs nothing — assigning it copies only the reference, exactly like copying a class handle. Two drivers holding the same vif see the same signals; reassigning a vif retargets the class to different pins without touching class source code. Before any assignment its value is null, and dereferencing it crashes — the subject of the next lesson.
interface bus_if (input logic clk);
logic [31:0] addr;
logic [31:0] data;
logic valid;
endinterface
class driver;
virtual bus_if vif; // a REFERENCE — null until assigned
task run();
forever begin
@(posedge vif.clk); // dereference: follows the reference
vif.addr <= $urandom; // drives the REAL tb_top signal
vif.valid <= 1'b1;
end
endtask
endclassReference semantics in action
module tb_top;
logic clk;
bus_if bif0 (clk);
bus_if bif1 (clk);
initial begin
driver d = new();
d.vif = bif0; // d now drives bif0's signals
// ... later, same object, different pins:
d.vif = bif1; // retargeted — zero class changes
// also legal: two objects sharing one instance
// monitor m = new(); m.vif = bif0;
end
endmoduleWhy this design and not alternatives
What the language could have done instead
Hierarchical paths in classes (tb_top.bif.addr) — compiles, but the class is unusable in any other testbench or for a second interface instance. Reuse dies.
Passing hundreds of individual signals as task arguments — unmanageable, and loses interface-level grouping, modports, and clocking blocks.
Instantiating the interface inside the class — illegal; static instances need elaboration-time positions, classes do not have one.
The vif gets the best of both: the interface keeps its static-world privileges (synthesizable connection to the DUT, clocking blocks, assertions inside it), while class code gets a late-bound, swappable reference. Interview angle: 'why can't a class just contain an interface?' is the classic phrasing — the answer is the lifetime mismatch above, and the follow-up 'so what does the vif store?' expects the word reference .
Key takeaways
Static world is fixed at elaboration; class objects are created at runtime — a vif bridges them.
A vif stores a reference to one elaborated interface instance, never a copy.
Reassigning a vif retargets a class to different pins with zero source changes — that is the reuse win.
An unassigned vif is null; every pin access through it is a dereference.
Common pitfalls
Hard-coding hierarchical paths inside classes — works once, kills reuse forever.
Thinking the vif copies the interface — two objects with the same vif share the same signals.
Trying to instantiate an interface inside a class — illegal; lifetimes are incompatible.
Comparing vifs across different parameterizations — differently parameterized interfaces are different vif types (covered in multi-instance lesson).