See how SystemVerilog supports templates for generic code writing using parameterized classes, and some of the design patterns in the code that make up the UVM base class library.
SystemVerilog supports templates for generic code writing using parameterized classes. Here we’re going to describe some of the design patterns in the code that make up the UVM base class library. Users writing testbenches with the SystemVerilog Universal Verification Methodology (UVM) or any kind of class-based methodology can learn from these techniques.
Design patterns are optimized, reusable solutions to commonly occurring programming problems. They are more than just class definitions or a package of routines—they are language-independent templates for writing code.
The concept of design patterns specifically for SystemVerilog object oriented programming (OOP) languages was popularized in 1994 by the book “Design Patterns: Elements of Reusable Object-Oriented Software.” OOP enables writing reusable code. OOP design patterns take reuse another step.
There are many kinds of design patterns. This article covers two important categories:
Singleton patterns – Restrict instantiation of a class to one object.
Factory patterns – Provide an interface for creating families of related or dependent objects and specify a policy for creating them.
Before explaining these in more detail, we need to understand how SystemVerilog supports templates for writing generic code using parameterized classes.
A parameter is a type of constant that represents a value or a data type. You can’t change a parameter once simulation starts. The compiler evaluates parameter expressions as part of its elaboration and code-generation phases before simulation starts. So you can use a parameter as part of the declaration of another type or use a parameter’s value in, for example, the range of an array declaration.
SystemVerilog uses a pound sign (#) with a list of parameter names in the class header to define a generic class. When you reference the generic class, you also use the pound sign to provide a list of parameter assignments or overrides.
In this example, we must provide a type parameter, T, because it has no default; the value parameter, W, is optional because we provided a default value of 5.
A generic class referenced with its actual parameter values is called a specialization. When referencing a generic parameterized class in the declaration of a class variable or in the declaration of another type, we fix the parameterization of the generic class. Each combination of a generic class together with its final parameter overrides becomes a unique, class specialization: a new class type.
Static class properties get allocated and initialized before time 0. But that no longer holds true as soon as you declare a generic class with parameters. The distinction between a parameterized class as a generic template versus the specialization of that class becomes very important. Static class properties do not get allocated unless their enclosing class gets specialized. And there is a unique instance and initialization of its static properties for each specialization.
Figure 2 Parameterized classes with static properties
SystemVerilog allocates a static property of a generic class once there is a reference to a specialization of that class. There is no need to construct an object of that class. The two typedefs in Figure 2 create two instances of the static counter variable. Declaration and construction of the two class variables S1 and S2 all share the same static count variable with the typedef count_byte.
Just like with unparameterized classes, we can use the class scope operator to reference static properties and methods. We use a unique specialization to reference the unique member we want. Just like when defining a new specialization, you must provide a parameter override for each parameter that does not have a default, otherwise it won’t compile.
The singleton design pattern is used to guarantee that only one instance of a class type ever gets constructed. Suppose we are trying to build a tree structure and we want to define the structure so there is only one root object in the tree. To control how many objects are constructed, we need to make the class constructor a local method. This means that no one outside this class can call it, and no one can construct an extended class because the extended class would need to call super.new().
Figure 3 Singleton pattern with local qualifiers
We also create a local static property m_root that holds a handle to our singleton object. Since we cannot call the constructor directly, we define a static method get() that enforces our singleton pattern. Calling get() checks m_root and only calls new() if it’s null. Since no one else can access m_root, there is no way new() could ever get called more than once. The get() method demonstrates the pattern of “create on first use.”
When many people first think about singletons, they imagine defining a “const” static variable that can only be written to once by calling new() at initialization before time 0 and never written to again. The problem with this is that as long as new() does not have a local qualifier, anyone can define another root variable and still construct multiple root objects.
Another problem is what’s known as “the static variable initialization order fiasco.” All static variables in a design get initialized before time 0. But there’s no way to know in what order the static variables are initialized since there is no procedural flow of statements yet. These static variable initializations might be scattered all over the code.
To avoid the initialization order fiasco, we use an access method to get the value of a local static variable that initializes it at the first reference, as opposed to referencing the static variable directly. Now it no longer matters which variable gets initialized first.
The factory pattern makes object construction polymorphic. This allows you to delegate construction to another object that can decide what kind of object to create. The factory also provides a policy for determining what type of object the factory returns. If we use the factory pattern when creating our classes, we can decouple the class type constructing an object from the actual class type we want constructed.
How does this work? Suppose we have a class A that constructs a class B object, and a class B that constructs a class C object. At some point later on, we might want to extend class C into D. But then we need to either modify or extend class B to construct a D object instead of a C object. If we extend B, then we also need to change or extend A. But we don’t want to modify A or B. The factory pattern comes to the rescue by letting us replace the call to new() in Class B that constructs a C object with a method that would normally return a C object, but could instead return an object of any type derived from C. So class A or B no longer has to be modified when we want to change what it constructs.
The factory pattern involves a number of different concepts working together. There are many kinds of factories that produce different kinds of things.
For example, a factory can be used to produce a single object derived from an abstract base. The factory uses another kind of abstract class, known as a proxy class, which has a virtual method that returns a handle to an object of the class type you want. For each class we want to create with the factory, we need to extend the proxy class and implement a create object method that constructs an object of the requested type. We then construct the extended object proxy class and get a handle to a proxy object that can be used to construct the requested object.
A proxy object is therefore a lightweight stand-in for the full requested object with all of its properties and methods. We can pass the proxy object handle around or store a number of different proxy object handles in a database, such as in the associative array, factory, in Figure 5. Then we can call the virtual create object method which chooses an implementation based on the type of handle stored in the array.
There are a few more things we can do to automate the factory registration process. The factory pattern can take advantage of static property initialization with a specialized class to do the factory registration for us. We can also make the specialized proxy object a singleton pattern, me, which allows us to change the factory’s associative array index from a string type to the proxy base class type.