Interface Classes in SystemVerilog
In SystemVerilog, an interface class declares a number of method prototypes, data types and parameters which together specify how the classes that need those features can interact. The methods are declared as pure virtual functions - an interface class does not provide an implementation for the prototypes - this is done in a non-interface class (virtual or 'concrete') that implements one or more interface classes. In other words, an interface class has neither state nor implementation.
An interface class can be thought of as bringing together a number of qualities that may be needed for some aspect of a class' behaviour.
- An interface class establishes a protocol for how objects can use a certain behavioural feature and therefore interact.
- The methods can be implemented differently in a non-interface class which can implement one or more interfaces according to its requirements. This allows the developer to emulate multiple inheritance - getting base class behaviour from multiple sources.
- As a result of separate implementation, it is possible to develop a leaner and flexible class hierarchy which models objects more accurately and without the overhead of unecessary declarations resulting from single inheritance.
- Using interface classes is potentially more efficient than (for example) using abstract classes as there is no performance cost of method look up associated with virtual classes.
- Interface classes can have type parameters.
In the following example, a channel provides connectivity and messaging APIs for components and the prototypes for the APIs are declared in separate interface classes.
The Messaging API deals with passing messages (transactions) between connected components and consist of both blocking and non-blocking methods:
interface class Messaging #(type T = logic); pure virtual task put(T t); pure virtual task get(output T t); pure virtual task peek(output T t); pure virtual function bit try_peek(output T t); pure virtual function bit try_put(T t); pure virtual function bit try_get(output T t); endclass
The Connecting interface declares methods to connect and disconnect components, as well as to introspect and debug the connection (not shown here).
interface class Connecting #(type P = logic); pure virtual function void connect (P provider); pure virtual function int connected_to(); pure virtual function void disconnect(P other); endclass
The concept of a channel to pass transactions between components and implement a basic TLM protocol would need to implement these two interface classes. Here, we have a base virtual class to declare the root of the channel based class hierarchy. We have added the notion that the channel will connect objects derived from a base Component type and will pass messages which will also be derived from a base Transaction type
virtual class TLM_channel_base #(type Tr = Transaction, C = Component) implements Messaging #(Tr), Connecting #(C); protected event e_get, e_put; protected int bound; pure virtual protected function T pop(); pure virtual protected function void push(T t); endclass : TLM_channel_base
By using interface classes to specify the API, we have not achieved any functionality that couldn't be done with single class inheritance alone, but what we have done is thought a bit more about how a certain API can be contained to provide a distinct feature. In this example, there is no need to provide a class hierarchical relationship between Messaging and Connecting, which might otherwise be necessary.
The channel root class, above, commits to providing an implementation of the APIs (delegated to a non-virtual channel). It also adds some channel specific (as opposed to messaging related) methods, pop and push. In the channel implementation considered here, these are the methods that actually do the insertion and retrieval of messages in the channel. The TLM channel declaration, below, allows the user to reuse the same TLM channel base class and API, but with different message and component types.
virtual class TLM_Channel extends TLM_channel_base #(.Tr(Transaction), .C(Component)); local Tr fifo[$]; local C providers[$]; local int connected; // Messaging functionality extern function new(int _bound = 0); extern task put(Tr t); extern function bit try_put(Tr t); extern task get(output Tr t); extern function bit try_get(output Tr t); extern task peek(output Tr t); extern function bit try_peek(output Tr t); extern function Tr pop(); extern function void push(Tr t); extern local function bit empty(); extern local function bit full(); extern local task await_not_full(); extern local task await_not_empty(); // Connection functionality // Connect a provider extern virtual function void connect (C provider); extern virtual function int connected_to(); extern virtual function void disconnect(C other); endclass
So now we have a more specialised specification for the channel which can be used to pass transactions between components. The user then extends from this base class to declare a non-virtual channel for their environments.
class APB_component extends Component; ... endclass typedef TLM_Channel #(APB_transaction, APB_component) APB_channel;
A working example of an interface class can be found on EDA Playground.
When to Use Interfaces, Inheritance or Aggregationa
An extended class has an is-a relationship with the super class and can-do many things as well as has qualities represented by other classes.
- is-a implies inheritance
- can-do implies interface
- has implies aggregation
- Interface vs Abstract Classes
- "SystemVerilog Interface Classes – More Useful Than You Thought", Stan Sokorac, ARM
- IEEE Std 1800-2012, section 8.26, p157
- Universal Verification Methodology(UVM) 1.2 Class Reference