Class and Object-Oriented Programming
One of the biggest features introduced in SystemVerilog is the ability to write functional model in an object-oriented manner, i.e. using the class
construct. Although class
construct is not synthesizable unfortunately, class
provides a more software-oriented approach to model hardware behavior for verification. SystemVerilog supports the following object-oriented programming (OOP) practice: - Inheritance - Polymorphism
In addition to the normal OOP, SystemVerilog also allows type parametrization for the class (similar to templates in C++)
5.1 Class Definition and Syntax
The syntax of class is similar to that of C++. The constructor is called new
and unfortunately there is no function overloading. However, similar to Python, users can provide default arguments to allow different usage. Here is a simple example of class
:
Notice that like Java/Python, each instance of the object in SystemVerilog is a “reference” and there is no pointer type. Keyword null
is used to denoted null reference. You will get null exceptions when you try to access members of a null object. We can enhance the Node
constructor with default argument in such way that users can also pass in the next reference while constructing the object:
By default, every member declared in the class is public, meaning any code can modify the attributes or call the functions regardless of the scope. To add private access modifier, we can use local
keyword. For instance, we can have:
class Node;
local Node next;
function new(Node next_node=null);
next = next_node;
endfunction
// local function
local function foo();
endfunction
endclass
To instantiate the object, we can use new
keyword, similar to Java/C#. To access member attributes or function, we can either use their identifiers directly or use this
keyword to avoid naming conflicts, similar to that of C++. Here is a complete example of single-linked-list that has proper access modifier:
class Node;
local Node next;
function new(Node next_node=null);
next = next_node;
endfunction
// local function
local function foo();
endfunction
endclass
class List;
local Node head_;
function new();
head_ = null;
endfunction
function Node head();
return head_;
endfunction
function void add_node();
automatic Node new_node;
new_node = new(head_);
// use "this" to access member attributes
this.head_ = new_node;
endfunction
endclass
Object in SystemVerilog is managed by the simulator runtime and thus garbage-collected, similar to Java/C#. As a result of that, we may also see performance downgrade when garbage collection hurts our simulation flow. As premature optimization is the root of all evil, we should be careful about optimization on memory allocation during verification.
You can also have static methods in the class, using the keyword static
, e.g. static function string name();
. static
function follows the same semantics as normal function, e.g. their scope and lifetime. As a result, automatic
keyword is required in some cases.
5.1.1 Out-of-Block Declarations
Similar to other OOP languages, SystemVerilog allows you to move the method implementation our of the body of the class declaration. Todo so, the method inside of the class declaration has to be marked as extern
. Then, in the same scope, we can add the implementation following the class declaration, as shown below:
class List;
local Node head_;
extern function void add_node();
endclass
function void List::add_node();
// actual implementation of the function
endfunction
Notice that in the out-of-block declaration, we have to specify the full namespace of the function, i.e. prefixing it with class name and scope resolution operator ::
. Similar to C++, we don’t need to specify function modifier such as extern
or virtual
in the out-of-block declaration.
Although the language allows default argument in the out-of-block declaration, it is confusing and difficult to maintain. We recommend default argument in C++ style, where default arguments are only specified in the function prototype, as shown below:
class ClassA;
extern function int func(int a = 0, int b = 1);
endfunction
endclass
function int ClassA::func(int a, int b);
endfunction
5.1.2 Forward Declaration
Similar to C++, if the class declaration needs to refer to another class declared later or else where, we can use keyword typedef
for forward declaration as shown below:
5.2 Class Inheritance
SystemVerilog allows class inheritance and polymorphism via inheritance. Although the semantics are similar to other software programming languages, there are some nuance difference that creates somme gotcha moments and thus is one of the popular source of bugs.
To inherit from the base class, we can use the keyword extend
and we can override any methods marked as virtual
, as shown in the example below:
class Vehicle;
function int num_wheels();
return 4;
endfunction
virtual function int num_seats();
return 2;
endfunction
endclass
class Car extends Vehicle;
function int num_seats();
return 4;
endfunction
endclass
module top;
initial begin
Car car;
car = new();
// we will get
// num of seats: 4
$display("num of seats: %0d", car.num_seats());
end
endmodule
In the code example, we first declare the base class Vehicle
, and then inherit it with Car
class. Notice that num_seats
is marked as virtual
, allowing it to be overridden by child classes.
If the base class has a constructor function, the child class must call the base class constructor using the keyword super
:
We can also specify constant values that cannot be modified. Any attempt to modify constant values will result in compiler error:
Abstract class and abstract methods are also supported in SystemVerilog and their semantics is identical to most OOP languages: 1. Users cannot directly instantiate an abstract class - a compiler error will be issued. 2. Users cannot inherit an abstract class without fully implement abstract methods.
To declare the class as an abstract class, we can declare the class as virtual class
using the keyword virtual
. To indicate an abstract method, we can use a new keyword pure
in addition to virtual
, e.g. pure virtual function
. Here is an example reusing the vehicle class.
virtual class Vehicle;
// abstract class can still offer implementations
function int num_wheels();
return 4;
endfunction
pure virtual function int num_seats();
endclass
class Car extends Vehicle;
function int num_seats();
return 4;
endfunction
endclass
module top;
initial begin
Vehicle v;
Car car;
// the line before will trigger an error
// v = new();
car = new();
// we will get
// num of seats: 4
$display("num of seats: %0d", car.num_seats());
end
endmodule
To allow child class access parent attributes and function, we can use the keyword modifier protected
. Similar to C++, attributes and functions marked as protected
are private outside the class scope, but are visible to child class. Users shall use protected
properly for best OOP practice.
5.3 Polymorphism
SystemVerilog supports Polymorphism via inheritance. Similar to other OOP languages, parent class variable can hold references to child class instances, as shown below:
class BaseClass;
virtual function string name();
return "BaseClass";
endfunction
endclass
class ClassA extends BaseClass;
function string name();
return "ClassA";
endfunction
endclass
class ClassB extends BaseClass;
function string name();
return "ClassB";
endfunction
endclass
module top;
initial begin
BaseClass c[2];
ClassA c_a;
ClassB c_b;
// initialize child class instances
c_a = new();
c_b = new();
// polymorphism
// we get
// name is ClassA
// name is ClassB
c[0] = c_a;
c[1] = c_b;
for (int i = 0; i < 2; i++) begin
$display("name is %s", c[i].name());
end
end
endmodule
One common gotcha is that if the base class method is not marked as virtual
yet is still overridden in the child class, the base class’s method will be called when used as base class, as shown below:
class Vehicle;
function int num_wheels();
return 4;
endfunction
endclass
class Bicycle extends Vehicle;
function int num_wheels();
return 2;
endfunction
endclass
module top;
initial begin
Bicycle bike;
Vehicle v;
bike = new();
v = bike;
// we will get
// num of wheels: 4
$display("num of wheels: %0d", v.num_wheels());
end
endmodule
Advanced linters may catch such errors, but most simulators will not issue a warning. Readers should always make sure adding proper modifiers such as virtual
.
5.4 Class Parametrization
Like module
or interface
, you can parametrize class types in a similar way to templates in C++. Unfortunately there is not much compiler elaboration supports so you cannot do much meta-programming in SystemVerilog. However, with typedef
keyword, we can get most of the things done with class parametrization.
5.4.1 Value Parametrization
The simplest way to parametrize a class is to parametrize the values. One usage for that is to parametrize the logic width or pre-allocated size of class variables, as shown below:
To instantiate the class, we can do something similar to module instantiate with parameters:
Value parametrization is useful in cases where the width/size of the variable or constant values that can only be set at compile time. For other values we recommend to use class constructor instead.
5.4.2 Type Parametrization
SystemVerilog allows type parametrization for classes in a similar manner as generics in Java/C#. Below is an example of how to use class with type parametrization.
class Node #(type T = int);
local Node #(T) next;
T value;
function new(Node #(T) next_node=null);
next = next_node;
endfunction
endclass
In the example, we define a Node
class that can be instantiated with variables types, e.g. int
or even other classes. The type for Node
class that has a parametrized type specified can be referred as Node #(T)
, where T
can be any type. To avoid duplicated code, we can use typedef
to redefine the type as follows:
class Node #(type T = int);
typedef Node #(T) NodeT;
T value;
function new(NodeT next_node=null);
next = next_node;
endfunction
endclass
We can use the same syntax as class with parametrized values to instantiate a class with parametrized types:
Type checking for class with parametrized types happens at compile time and designers can use the error information from the compiler to fix their code.
5.5 Interface Class
Interface class is an OOP concept borrowed from languages such as Java and C#. It defines public functions and other public information about the class. However, one major distinction between interface class and other class is that typically interface class does not have any implementation details (some recent change in some OOP languages may change this fact).
To declare an interface class, we can use the following syntax:
Notice that every functions in defined in the interface class should be pure virtual
. Interface class can inherit from another interface class using the keyword extends
, as shown below:
For a class to implement the interface class, we can use the keyword implements
, as shown below. A class can implement as many interface class as the designers see fit.
You can also put other non-implementation related information in the interface class, such as enum definition, as shown below:
interface class IEnum;
typedef enum int {RED, GREEN, BLUE} color_t;
pure virtual function color_t color();
endinterface
As with normal class, interface class can also have parametrized types. However, there are some syntax restriction on how interface are used:
An class cannot not implements/extends an interface class that has been forward declared. The following code describe an illegal usage
Interface class cannot be used as a type parameter.