Interfaces SystemVerilog

SystemVerilog Interfaces vs. Interface Classes: Wiring, Contracts, and Polymorphism

Cover image for SystemVerilog Interfaces vs. Interface Classes: Wiring, Contracts, and Polymorphism

“Just pass the interface into the class.”

That sentence means two completely different things depending on who says it. One engineer means a virtual handle to an AXI or custom protocol interface. Another means an object handle typed by an interface-class contract. Both said interface. Only one of them gets access to DUT signals.

The correct OOP term is interface class, not “class interface.” An interface class is not an advanced signal interface, and it is not another spelling of interface. These constructs solve different engineering problems.

A language interface organizes connectivity and communication in the elaborated hierarchy. A virtual interface lets a dynamically constructed class refer to one of those interface instances. An interface class classifies objects by the behavior they promise to provide.

The distinction worth internalizing is topology versus capability. Use a language interface when representing a communication boundary. Use an interface class when unrelated or differently inherited objects must expose the same behavioral API.

One word, two abstractions

A SystemVerilog interface connects or coordinates structural participants such as a DUT, assertions, and testbench code. An interface class defines an object-oriented contract that ordinary classes fulfill using implements.

Construct Domain Refers to Primary purpose
interface Static, elaborated hierarchy An interface instance Connectivity and communication
virtual interface Dynamic class code pointing into the static hierarchy An interface instance Let class-based code access signals and interface operations
interface class Dynamic OOP type system An implementing class object Behavioral polymorphism

A useful analogy is a wiring harness versus a compliance specification. A language interface gathers a communication boundary into one reusable connector. An interface class specifies operations that conforming objects must provide. A virtual interface is the typed access path to one installed connector.

The analogy has limits. A language interface can contain signals, methods, modports, clocking blocks, assertions, and protocol logic. An interface class participates in nominal typing with declared method contracts; it is not informal duck typing or runtime capability discovery. A virtual interface is a typed handle, not a copied signal bundle.

Both constructs can expose tasks and functions, which is where the terminology becomes misleading. A task declared in a language interface executes in the context of an elaborated interface instance and may operate on its signals. A method declared by an interface class describes behavior that an implementing object must provide. Implementing that contract grants no DUT connectivity.

The declaration and closing keywords make the distinction explicit:

interface link_if(input logic clk);
    // Static communication construct
endinterface

interface class Resettable;
    // Dynamic OOP contract
endclass

An interface instance is a concrete part of the elaborated hierarchy, such as link in link_if link(clk, reset_n);. A virtual-interface handle is a class variable that can refer to such an instance. An interface-class handle is an object handle typed by a behavioral contract.

Side by side, the handle categories are clear:

virtual link_if.producer link_vif;
Resettable              reset_target;

Neither declaration creates its target. link_vif must receive a reference to an existing interface instance with a compatible view. reset_target must receive an existing object whose class implements Resettable. Both handles initially have null values, but they point into different parts of the SystemVerilog model.


What a language interface solves

A language interface packages communication-related declarations shared by structural participants. At its simplest, it groups signals. Production interfaces often add modports, clocking blocks, protocol tasks, assertions, and coverage tied to the communication boundary.

The following complete example defines producer and consumer roles, gives the testbench producer a clocking block, connects the DUT through a modport, and shows exactly where the static interface instance is passed into a class.

interface link_if (
    input logic clk,
    input logic reset_n
);
    logic        valid;
    logic        ready;
    logic [31:0] data;

    clocking producer_cb @(posedge clk);
        default input #1step output #0;
        input  reset_n;
        input  ready;
        output valid;
        output data;
    endclocking

    modport producer (
        clocking producer_cb
    );

    modport consumer (
        input  clk,
        input  reset_n,
        input  valid,
        input  data,
        output ready
    );
endinterface

class LinkDriver;
    virtual link_if.producer vif;

    function new(virtual link_if.producer vif);
        this.vif = vif;
    endfunction

    task drive_word(logic [31:0] value);
        vif.producer_cb.valid <= 1'b0;
        vif.producer_cb.data  <= '0;

        do @(vif.producer_cb);
        while (!vif.producer_cb.reset_n);

        vif.producer_cb.data  <= value;
        vif.producer_cb.valid <= 1'b1;

        do @(vif.producer_cb);
        while (!vif.producer_cb.ready);

        vif.producer_cb.valid <= 1'b0;
    endtask
endclass

module LinkDut(link_if.consumer link);
    always_ff @(posedge link.clk or negedge link.reset_n) begin
        if (!link.reset_n)
            link.ready <= 1'b0;
        else
            link.ready <= link.valid;
    end
endmodule

module tb_top;
    logic clk     = 1'b0;
    logic reset_n = 1'b0;

    always #5 clk = ~clk;

    link_if link(clk, reset_n);

    LinkDut dut(.link(link));

    initial begin
        repeat (2) @(posedge clk);
        reset_n <= 1'b1;
    end

    initial begin
        LinkDriver driver;

        driver = new(link);
        driver.drive_word(32'h1234_5678);

        repeat (2) @(posedge clk);
        $finish;
    end
endmodule

link is the elaborated interface instance. It is created as part of tb_top; nobody calls new() on it. The DUT consumes the entire communication boundary through link_if.consumer rather than expanding it back into individual module ports.

LinkDriver, by contrast, is a dynamically allocated class object. Its constructor receives a modport-qualified virtual-interface handle referring to tb_top.link. The class does not contain a private copy of valid, ready, or data.

The modports establish participant views. The consumer reads valid and data and drives ready. The producer uses producer_cb, whose directions and timing are defined by the clocking block. These direction and access rules are part of the language semantics. A tool that fails to enforce a prohibited access, or reports it poorly, has a diagnostic or conformance limitation; the semantics are not implementation-defined.

The clocking block also makes the testbench timing policy visible. Its input skew samples values before the clock event, while its outputs are driven through the clocking-block mechanism rather than by unrestricted direct signal access. A production driver still needs a documented definition of handshake completion, cancellation, timeout behavior, and reset interaction, but the example avoids leaving race prevention entirely to convention.

Physical reset has a separate owner here: the top-level reset process drives reset_n, while LinkDriver owns the ordinary producer outputs. The driver observes reset but does not assert it. That ownership split matters once several agents share a reset domain.

The keyword virtual describes the interface handle category. It does not make link_if an interface class and has nothing to do with implements.


What an interface class solves

An interface class defines an OOP contract. A class implementing that contract promises to provide methods with compatible signatures. Consumers can then depend on what an object can do without depending on its concrete class or normal inheritance path.

Capability-oriented contracts benefit from capability-oriented names:

interface class Resettable;
    pure virtual task reset();
endclass

interface class Checkable;
    pure virtual function bit check();
endclass

interface class Loggable;
    pure virtual function string log_message();
endclass

These examples use pure virtual methods because that is the central architectural use of interface classes. The applicable IEEE 1800 revision remains the normative source for all permitted declarations, method matching rules, and inheritance interactions.

Method signatures are necessary, but they are not a complete behavioral specification. Resettable::reset() does not say whether the call may consume simulation time, whether concurrent calls are legal, whether it clears owned subobjects, or whether it controls a physical reset signal. Checkable::check() does not define reporting policy or diagnostic detail.

That warning is especially important in verification code: method signatures do not capture timing or ownership semantics. Put those semantics in the contract documentation and enforce them in the surrounding architecture.


Implementing multiple contracts

A class uses extends for its single normal base-class path and implements for interface-class contracts.

class ComponentBase;
    protected string name;

    function new(string name);
        this.name = name;
    endfunction

    function string get_name();
        return name;
    endfunction
endclass

class ProtocolTracker extends ComponentBase
                      implements Resettable, Checkable;
    int outstanding;
    int errors;

    function new(string name);
        super.new(name);
    endfunction

    virtual task reset();
        outstanding = 0;
        errors      = 0;
    endtask

    virtual function bit check();
        return (outstanding == 0) && (errors == 0);
    endfunction
endclass

module contract_example;
    initial begin
        ProtocolTracker tracker;
        Resettable      reset_h;
        Checkable       check_h;

        tracker = new("tracker");

        reset_h = tracker;
        check_h = tracker;

        reset_h.reset();

        if (!check_h.check())
            $error("%s failed its health check", tracker.get_name());
    end
endmodule

Only one ProtocolTracker object is constructed. reset_h and check_h refer to that same object through different static contract types. Reset-oriented code can call only the Resettable API through reset_h; checking code sees only the Checkable API through check_h.

ComponentBase supplies shared state and implementation. The interface classes add type conformance without displacing that base class. This is multiple contract implementation, not C++-style multiple implementation inheritance. The concrete class does not receive fields, constructors, or reusable method bodies from Resettable or Checkable.

A concrete implementing class must provide compatible implementations for all required methods. A task does not satisfy a function declaration, and incompatible return types, argument types, directions, or qualifiers do not become compatible merely because the method names match. Diagnostic wording varies among tools, but the language requirements come first.

An interface class itself has no concrete implementation to construct:

// Invalid: Resettable is a contract, not a concrete class.
Resettable reset_h = new();

Construct an implementing class, then assign that object handle to the interface-class handle.


Keeping DUT reset separate from object reset

Language interfaces and interface classes can coexist, but they should retain distinct ownership. Physical reset sequencing is a connectivity and timing responsibility. Resetting dynamic model state is an object behavior.

A coordinator can bridge those responsibilities without asking every agent to drive both reset and protocol outputs:

interface reset_if(input logic clk);
    logic reset_n = 1'b1;

    clocking controller_cb @(posedge clk);
        default input #1step output #0;
        output reset_n;
    endclocking

    modport controller(clocking controller_cb);
endinterface

interface class Resettable;
    pure virtual task reset();
endclass

class PacketModel implements Resettable;
    logic [31:0] expected_data;
    bit          pending;

    virtual task reset();
        expected_data = '0;
        pending       = 1'b0;
    endtask
endclass

class ResetCoordinator;
    virtual reset_if.controller reset_vif;
    Resettable                  models[$];

    function new(virtual reset_if.controller reset_vif);
        this.reset_vif = reset_vif;
    endfunction

    function void add_model(Resettable model);
        models.push_back(model);
    endfunction

    task reset_all(int unsigned cycles = 2);
        @(reset_vif.controller_cb);
        reset_vif.controller_cb.reset_n <= 1'b0;

        repeat (cycles) @(reset_vif.controller_cb);

        reset_vif.controller_cb.reset_n <= 1'b1;

        foreach (models[i])
            models[i].reset();
    endtask
endclass

module reset_example;
    logic clk = 1'b0;

    always #5 clk = ~clk;

    reset_if reset_bus(clk);

    initial begin
        PacketModel      model;
        ResetCoordinator coordinator;

        model       = new();
        coordinator = new(reset_bus);

        coordinator.add_model(model);
        coordinator.reset_all(3);

        $finish;
    end
endmodule

The virtual reset_if.controller handle reaches a static reset interface instance and owns DUT-facing reset sequencing. The Resettable queue contains object handles used to clear dynamic model state. Calling PacketModel::reset() never asserts the physical reset signal.

Projects should define whether model reset occurs before, during, or after DUT reset, and whether a model reset may block. The code above chooses to clear models after deassertion; that is an architectural policy, not a consequence of the method signature.


Why interface classes reduce inheritance pressure

A virtual or abstract base class can carry shared state, implemented methods, constructors, protected helpers, lifecycle policy, and common identity. Those are good reasons to use ordinary inheritance.

The pressure appears because SystemVerilog gives a class one normal base-class path. If checking, resetting, serialization, or logging are all modeled as separate implementation base classes, a component that already extends a framework base cannot inherit from those additional classes.

Interface classes relieve that pressure when the actual requirement is substitutability through a behavioral contract. A scoreboard, reference model, transaction record, and protocol tracker can all be Checkable without pretending that they share one representation or lifecycle.

That does not make interface classes the default choice. If every implementation needs the same state and method bodies, a base class or composed helper is clearer. If the behavior is local to one component, a direct method is usually enough. If the goal is reuse rather than polymorphic substitution, composition and delegation generally fit better.

A bare bit result may also be too weak for a production checking API. Real contracts often return a structured result containing severity and context, or accept a diagnostic collector. Keep the example’s bit return as a minimal demonstration, not a universal checking design.


Handle comparison and construct selection

The central comparison is still this:

virtual link_if.producer link_vif;
Checkable               item_to_check;

link_vif refers to a specific elaborated interface instance through its producer view. Expressions through its clocking block access signals in that instance.

item_to_check refers to a dynamically allocated object whose class implements Checkable. Calling item_to_check.check() performs object-method dispatch. It does not expose an interface instance or grant hierarchical signal access.

Choose the construct according to the boundary being modeled:

  • Use a language interface for protocol connectivity, role-specific views, and timing access.
  • Use a virtual interface when a class needs access to an existing interface instance.
  • Use an interface class when unrelated objects must provide the same behavioral capability.
  • Use a base class for shared identity, state, and implementation.
  • Use composition when behavior should be reused without inheritance.

A modport is not the structural equivalent of an interface class. A modport defines a participant’s view of a language-interface instance. An interface class defines an OOP type contract. One addresses topology and access; the other addresses capability.


Portability and sources

IEEE 1800 is normative for interface-class syntax and semantics. Doulos tutorials are useful explanatory material, while Accellera proposals are historical documents that help explain the feature’s motivation but do not replace the adopted standard. Interface classes were added in IEEE 1800-2012, so older simulators, parsers, linters, IDE front ends, and compatibility modes may have tool limitations even when ordinary interfaces work correctly.

Use a compact feature probe in every supported simulator, linter, CI flow, and selected language mode. Compile a class implementing multiple contracts, assign one object to each contract-typed handle, and dispatch through every handle. This tests more than parser recognition and exposes incomplete implementation support early.


Final perspective

The shared word does not make these mechanisms interchangeable.

A language interface organizes communication in the elaborated hierarchy. A virtual interface gives dynamic class code a typed path to one interface instance. An interface class organizes object polymorphism around a behavioral contract.

When reviewing a verification architecture, classify each dependency as topology, access to topology, capability, or implementation reuse. Keep DUT-facing communication behind language interfaces and role-specific modports. Pass interface instances into classes through virtual-interface handles. Use interface classes only where narrow behavioral contracts genuinely cross otherwise unrelated class hierarchies.

That topology-versus-capability model is simple enough to remember and precise enough to prevent the most expensive category mistakes.


References


Publication metadata

SEO title: SystemVerilog Interfaces vs. Interface Classes: A Practical Guide

SEO description: Learn how SystemVerilog interfaces, virtual interfaces, and interface classes differ, how they work together, and when to use each in verification code.

Excerpt: SystemVerilog uses interfaces for static communication topology and interface classes for object-oriented behavioral contracts. This practical guide explains the distinction, shows how virtual interfaces bridge class code to elaborated interface instances, and demonstrates safe ownership, modport, clocking-block, reset, and polymorphism patterns.


Discussion

0 comments

No comments yet — start the conversation.

Leave a comment

Comments are moderated and appear here after review.

Name
Email
Won't be published.
Comment