An interface can be instantiated inside a module, but never inside a class. That's a real problem the moment you start building an object-oriented testbench: a class-based driver, monitor, or scoreboard still needs to drive and sample real DUT signals somehow. A virtual interface is the handle that closes that gap — a class-compatible reference that points at an actual interface instance created elsewhere, letting class code reach out into the physical world without ever instantiating an interface itself.
7 min read | Intermediate Level
What You'll Learn
- Why a class can't directly instantiate an interface, and what problem virtual interfaces solve
- The syntax for declaring a virtual interface handle, with or without a modport
- How to pass a virtual interface into a class through its constructor
- How virtual interfaces work together with modports and clocking blocks
- The most common pitfall: using an uninitialized (null) virtual interface handle
Why Do We Need Virtual Interfaces?
SystemVerilog Interface covers how an interface bundles signals together, and SystemVerilog Modport covers restricting access to those signals per module. Both of those are still module-side constructs, though — a class cannot declare an interface instance as one of its own properties. Since class-based components (drivers, monitors, scoreboards, and similar) are exactly where most of a modern testbench's logic lives, there has to be some way for that class code to reach the actual interface signals living out in a module.
A virtual interface is that bridge. It's a handle — conceptually similar to a class handle — that can be declared as a class property, and later assigned to point at a real interface instance. Nothing about declaring one creates any new signals; it's just a reference, exactly like an unassigned class handle is just a reference until you call new() on it.
Basic Syntax
virtual interface_name variable_name;
// Restricted to a specific modport's view, same as a module port would be
virtual interface_name.modport_name variable_name;
Both forms declare a handle, not a new interface instance. The second form, with a modport, restricts what the class can access through that handle to exactly what the named modport allows — the same restriction a module would get by declaring its port as interface_name.modport_name instead of the whole interface.
A Complete Example
The interface below reuses the request-grant pattern from earlier in this chapter, with a clocking block and a TB modport that references it:
interface req_gnt_if (input logic clk);
logic req;
logic gnt;
logic rstn;
clocking cb @(posedge clk);
default input #1step output #2ns;
output req;
input gnt;
endclocking
modport TB (clocking cb, output rstn);
endinterface
A simple Driver class stores a virtual interface handle, typed to the TB modport, and takes it in as a constructor argument:
class Driver;
virtual req_gnt_if.TB vif; // handle only - no signals of its own
function new (virtual req_gnt_if.TB vif);
this.vif = vif;
endfunction
task drive_req (bit value);
vif.cb.req <= value;
endtask
endclass
The design and testbench modules stay exactly as they'd look without any class involved — the only new piece is passing vif into Driver's constructor:
module des (input logic clk, req, rstn, output logic gnt);
always @ (posedge clk or negedge rstn)
if (!rstn) gnt <= 0;
else gnt <= req;
endmodule
module tb;
logic clk;
always #10 clk = ~clk;
req_gnt_if vif (.clk(clk));
des dut (.clk(clk), .req(vif.req), .rstn(vif.rstn), .gnt(vif.gnt));
Driver drv;
initial begin
vif.rstn <= 0;
repeat (3) @(posedge clk);
vif.rstn <= 1;
// Pass the physical interface instance into the class through its constructor -
// SystemVerilog automatically converts it to a virtual interface handle here
drv = new (vif);
repeat (2) @(posedge clk);
drv.drive_req(1);
repeat (5) @(posedge clk);
$finish;
end
endmodule
vif is declared in the module as a plain interface instance, exactly as in every other example in this chapter. Passing it into new(vif) is where the conversion happens: SystemVerilog automatically treats an actual interface instance as a valid value for a virtual interface parameter, so no explicit cast is needed. From that point on, drv.vif behaves like any other class handle — it can be reassigned, passed to other methods, or stored in an array of handles.
Quick Check
Q: Does declaring virtual req_gnt_if.TB vif; inside the Driver class create a new set of req/gnt/rstn signals?
Show Answer
No. It only declares a handle — a reference that can later point at an already-existing interface instance. No signals exist until the handle is assigned to point at a real interface, the same way a class handle doesn't create an object until new() is called on it.
The Most Common Pitfall: An Uninitialized Virtual Interface
A virtual interface handle that's declared but never assigned defaults to null, exactly like an unassigned class handle. Using it before assignment produces a run-time error — not a compile-time one — because the compiler has no way of knowing at compile time whether the handle will actually be assigned before it's used. Suppose Driver had no constructor argument at all, and nothing else in the testbench ever assigned vif:
class Driver;
virtual req_gnt_if.TB vif; // starts out null - nothing assigned yet
task drive_req (bit value);
vif.cb.req <= value; // fails at run time if vif was never assigned
endtask
endclass
module tb;
Driver drv;
initial begin
drv = new(); // vif was never passed in or assigned anywhere!
drv.drive_req(1); // run-time error: dereferencing a null handle
end
endmodule
This is one of the most common real-world virtual interface bugs, especially in UVM-style environments where the virtual interface is often set from the top-level testbench and retrieved deep inside the class hierarchy through a configuration database. If that set/get pairing is missed or mistyped anywhere along the way, the handle stays null, and the failure only shows up much later, at run time, the first time the class actually tries to use it.
Where Are Virtual Interfaces Used?
Virtual interfaces are the standard mechanism by which class-based driver, monitor, and scoreboard components in an object-oriented testbench reach the DUT at all — in UVM specifically, a virtual interface is typically set once from the top-level testbench and retrieved by name wherever a component needs it, rather than being passed through every constructor by hand.
Virtual Interfaces with Modports and Clocking Blocks
A virtual interface handle typed to a specific modport, like virtual req_gnt_if.TB above, restricts the class to that modport's view — exactly as if a module had declared that same modport as its port type. Accessing signals through the interface's clocking block works identically through a virtual handle as it does through a plain interface instance: vif.cb.req applies the clocking block's output skew whether vif is a module's local interface instance or a class's virtual interface handle pointing at one.
Common Beginner Mistakes
Mistake #1: Using a virtual interface handle before it's assigned
As shown above, a virtual interface handle defaults to null until something assigns it to point at a real interface instance. Why? Any attempt to access a signal through a null handle fails at run time, and since this compiles fine, the bug only surfaces the first time that code path actually executes — which can be well into a test run.
Mistake #2: Declaring the virtual interface without a modport
A plain virtual req_gnt_if vif; gives the class unrestricted access to every signal in the interface, including ones it was never meant to touch. Why? Typing the handle to a specific modport, like virtual req_gnt_if.TB, restricts the class to exactly the signals and directions that modport allows — the same access control a module gets from using a modport on its own port.
Mistake #3: Expecting a virtual interface declaration to create new signals
Declaring a virtual interface property inside a class doesn't instantiate anything. Why? It's a handle, not a new interface — the actual signals only exist because some module somewhere instantiated the real interface; the virtual interface just gives class code a way to reach them.
Practice Exercise
Challenge: Write a Monitor Class
Using the req_gnt_if interface from earlier, write a Monitor class that stores a virtual interface handle (typed to the TB modport) and has a task sample_gnt(gnt_val) that waits for one clock edge and captures the current value of gnt into its output argument.
Hint
Follow the same pattern as Driver: a virtual interface property, a constructor that assigns it, and a task that accesses the signal through vif.cb. Since the task needs to wait on a clock edge, it has to stay a task, not a function — so use an output argument to hand back the sampled value instead of a return type.
Solution
class Monitor;
virtual req_gnt_if.TB vif;
function new (virtual req_gnt_if.TB vif);
this.vif = vif;
endfunction
task sample_gnt (output bit gnt_val);
@(vif.cb);
gnt_val = vif.cb.gnt;
endtask
endclass
module tb;
// ... clk, req_gnt_if vif, and des dut instantiated as before
Monitor mon;
bit g;
initial begin
mon = new (vif);
mon.sample_gnt(g);
$display("gnt = %0b", g);
end
endmodule
sample_gnt has to be a task, not a function, because @(vif.cb) consumes simulation time — functions can't contain timing controls. That's also why the sampled value comes back through an output argument instead of a function return value.
Summary
In this tutorial, you learned:
- A class can never instantiate an interface directly — a virtual interface is the handle that lets class code reach an interface instance created elsewhere
- Declaring a virtual interface only creates a reference, not new signals; it defaults to
nulluntil assigned - An actual interface instance is automatically usable wherever a virtual interface of a matching type is expected, including as a constructor argument
- Typing a virtual interface to a specific modport restricts class access to exactly that modport's signals and directions
- Accessing a clocking block through a virtual interface handle applies the same input/output skew as accessing it through a plain interface instance
- Using an unassigned (null) virtual interface handle is one of the most common real-world virtual interface bugs, and only fails at run time