Skip to content
DenseChip
Learn
PracticeCode labInterview

DenseChip

Navigate

Menu

LearnPracticeCode labInterview

Tools

All toolsSV editorCode repoResume
DenseChip

Practice, courses, and chip-design tools in one focused workspace.

LearnPracticeCode labInterviewToolsLearning paths

© 2026 DenseChip

AboutContactPrivacyTerms
Skip to lesson

Course contents

  • Start Here6
  • SystemVerilog Language Foundations28
  • Object-Oriented SystemVerilog for Verification30
    • Classes, Objects & Handles
    • Inheritance and Polymorphism
    • Static, Encapsulation & Parameterized Classes
    • Virtual Interfaces: the Class-to-Pins Bridge
      • Why Virtual Interfaces Exist
      • Declaring & Binding a vif
      • Driving & Sampling via Clocking Blocks
      • vif Configuration Patterns
      • Multi-Instance & Parameterized vifs
    • Events, Semaphores, Mailboxes and Processes
    • OOP Patterns for Testbenches
  • Constraint Randomization — Interview-Critical Deep Dive36
  • SystemVerilog Assertions — Interview-Critical Deep Dive40
  • Functional Coverage in SystemVerilog22
  • Testbench Architecture in SystemVerilog23
  • Advanced Language & Integration25
  • Senior & Interview Preparation21
  • Principal-Level Verification28
Loading lesson

‹Virtual Interfaces: the Class-to-Pins Bridge
  1. Topics
  2. SystemVerilog
  3. Object-Oriented SystemVerilog for Verification
  4. Virtual Interfaces: the Class-to-Pins Bridge
  5. Declaring & Binding a vif

Part 2 · OOP for Verification · Intermediate

Declaring & Binding a vif

Declare a vif, bind it top-down in constructors, and guard every binding against null.

On this page

    Why this matters

    The simulation died at time 0 with a null handle inside the driver. Its run() had started before anyone gave it a vif.

    After this lesson you can: declare a vif, bind it top-down in a constructor or connect step, and order bind, guard and start so a missing binding fails clearly.

    Binding flows top-down

    Declaring a vif takes one line: virtual bus_if vif. Binding fills it from the static world, so the reference always travels from tb_top into the classes.

    Constructor binding is the strong default. An object that exists already holds a valid vif, and the null check lives in exactly one place.

    The standard guard compares the handle with null in the constructor:

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    // snippet: the constructor guard; bus_if is declared in the listing below
    class driver;
      virtual bus_if vif;
      function new(virtual bus_if vif);
        if (vif == null)
          $fatal(1, "driver::new called with null vif");
        this.vif = vif;   // bind at construction: never null afterward
      endfunction
    endclass

    Not every simulator builds a comparison of a virtual interface with null yet, so this course's runnable listings record the binding in a flag instead and check the flag. The idea is the same: no pin access until the binding is proven.

    • Never half-built: an object that exists already holds a valid vif.

    • One guard site: the null check sits in new, not before every pin access.

    • Visible dependency: the signature shows reviewers that a driver requires a bus_if.

    The null-vif crash

    A vif defaults to null. The first pin access before binding fatals, usually at time zero from a loop that started too early.

    The root cause is always ordering. Bind first, guard second, and start time-consuming processes last.

    The null-vif crash is an ordering bug: bind, guard, then start.

    Two places to guard in a hierarchy, shown as a fragment:

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    // snippet: guard patterns; bus_if and driver are the ones above
    class monitor;
      virtual bus_if vif;
    
      // Pattern 1: guard in new() when binding via constructor
      function new(virtual bus_if vif);
        if (vif == null) $fatal(1, "monitor: null vif at construction");
        this.vif = vif;
      endfunction
    endclass
    
    class env;
      driver  drv;
      monitor mon;
    
      // Pattern 2: late binding via a connect step -- guard there,
      // and again at start-of-run as a final safety net
      function void connect(virtual bus_if bif);
        drv = new(bif);   // construct here: assigning drv.vif on a null drv crashes
        mon = new(bif);
      endfunction
    
      task run();
        if (drv == null || drv.vif == null)
          $fatal(1, "env::run: drv never bound -- check connect order");
        fork drv.run(); join_none
      endtask
    endclass

    connect constructs every child before returning. The run guard checks the child handle first, then its vif, so neither dereference can fire.

    Common mistakes

    Common pitfall

    Forking run() before assigning the vif is the canonical time-zero null-handle fatal. Binding one component but forgetting another gives a subtler variant: the driver works while the monitor crashes mid-run.

    Interview

    “Your sim dies with a null handle at time 0 in the driver — walk me through it.” Name the vif, say run() started before binding, and propose guards at construction or connect plus correct start ordering.

    Check yourself connect assigns drv.vif directly instead of constructing drv, and drv was never built. What happens?

    The assignment itself dereferences a null drv. Construct each child in connect (drv = new(bif)), then guard drv == null before drv.vif.

    Check yourself The guard if (drv.vif == null) in run() itself crashes with a null handle. Why?

    It dereferenced a possibly-null drv to reach the vif. Guard the child handle first: if (drv == null || drv.vif == null).

    Testbench

    Predict first In the bench, e.run() is called after e.connect(bif). What would happen if the two calls were swapped?

    run() would find drv still null and stop the simulation with its own clear message, instead of crashing later on a null vif.

    A real vif bound in connect, guarded in run, and only then started: four beats on the bus.

    bus_if.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    interface bus_if (input logic clk);
      logic [31:0] addr;
      logic        valid;
    endinterface
    
    class driver;
      virtual bus_if vif;
      local bit bound;                 // set only when a real instance is handed over
    
      function void connect(virtual bus_if v);
        vif = v;
        bound = 1;
      endfunction
    
      function bit is_bound();
        return bound;
      endfunction
    
      task run(int n);                 // drive n beats, then stop
        repeat (n) begin
          @(posedge vif.clk);
          vif.valid <= 1'b1;
          vif.addr  <= vif.addr + 4;
        end
        @(posedge vif.clk);
        vif.valid <= 1'b0;
      endtask
    endclass
    
    class env;
      driver drv;
    
      function void connect(virtual bus_if bif);
        drv = new();                   // construct the child first
        drv.connect(bif);              // BIND
      endfunction
    
      task run();
        if (drv == null || !drv.is_bound())   // GUARD the child, then its binding
          $fatal(1, "env::run: driver never bound; check connect order");
        drv.run(4);                    // START
      endtask
    endclass
    
    module tb_bind;
      logic clk = 0;
      always #5 clk = ~clk;
      bus_if bif (clk);
      int beats = 0;
      always @(negedge clk) if (bif.valid) beats++;
      initial begin
        env e;
        bif.valid = 0; bif.addr = 0;
        e = new();
        if (e.drv != null) $fatal(1, "FAIL: driver exists before connect");
        e.connect(bif);
        e.run();
        @(negedge clk);
        if (beats == 4 && bif.addr == 32'h10) $display("PASS: bind, guard, start: the driver drove 4 beats and the address reached 10");
        else $fatal(1, "FAIL: %0d beats, addr %h", beats, bif.addr);
        $finish;
      end
    endmodule
    Terminal
    PASS: bind, guard, start: the driver drove 4 beats and the address reached 10

    Remember

    • A vif is null until the top assigns it.

    • Bind, then guard, then start the time-consuming tasks.

    • Guard the child handle before the child's vif.

    Rule of thumb: no pin access until the binding is proven.

    How this was checked

    Run with Verilator 5.048 (--binary --timing) on 2026-10-18; every bench printed the PASS line shown, with no warnings. These class benches were run on Verilator only.

    Verilator 5.048 does not build a comparison of a virtual interface with null (a C++ compile error), so the runnable bench records the binding in a flag; the null-compare guard is shown as a fragment.

    Related topics

    • Next: Driving & Sampling via Clocking Blocks

    Previous

    50 / 259

    Next: Driving & Sampling via Clocking Blocks

    Up next

    Up nextDriving & Sampling via Clocking Blocks

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • vif Configuration Patterns
    • Multi-Instance & Parameterized vifs