Fun with Type Handles
In SystemVerilog, like other compiled languages, data types are essentially a compile time concept and type information is not readily available at run time. However, occasionally you need to know something about the data type of an object at run time. The most obvious example is when you use $cast() (or dynamic_cast<> in C++) to convert from a base class handle to a derived class handle. Or, when you want to classify objects by type. In this article we will look more closely at run time type information needed to classify objects.
Probably many of you thought of type conversion as an occasion when type information must be known at run time. Type conversions can be handled at compile time. The compiler has to agree that it's possible to convert the object from its original type to some other, and then it will generate the code to do so. There is no run time knowledge of types necessary.
The SystemVerilog LRM defines something called a type reference. Like type handles, it is an object whose value uniquely identifies a type. The language also defines an operator called 'type' that takes a type name or an expression and returns a unique type reference. It can probably do most of what type handles do. The thing is, it's not uniformly implemented in SystemVerilog simulators across the industry.
So that brings us to type handles, a tool we can use for classifying objects by type at run time. A type handle is a handle to some sort of object that uniquely identifies a type. That is, each different value of a type handle represents a different type. SystemVerilog does not natively provide this kind of facility (beyond type references) but we can build something that serves the purpose quite well.
To start with we will define a type handle pattern, a small collection of classes with specific relationships that provide the functionality needed for type handles. Our pattern is based on a container which holds an object whose type is identified by the type handle. Here is the pattern:
virtual class base; pure virtual function base get_type(); endclass base; class container#(type T) extends base; typedef container#(T) this_type; static this_type type_handle = get_type(); static function this_type get_type(); if(type_handle == null) type_handle = new(); return type_handle; endfunction function base get_type_handle(); return get_type(); endfunction endclass
The essential elements of the pattern are a pure virtual base class (aka abstract base class), a polymorphic container parameterized with a type, a static type handle, and methods for getting and setting the type handle. The actual type handle is in the container. In fact, it's a static instance of the container itself.
The type handle pattern has some similarity to the singleton pattern. The get_type() function ensures that only one instance is ever created. Every instance of container#() will contain a static instance of type_handle. Because type_handle is static, and because of the way the (static) get_type() function is constructed, only one value of type_handle is in existence for every specialization of container#(T). This is why the type handle can serve as a proxy for the type.
At first glance the container class seems odd, having an instance of itself as a member. However, because the instance is static, and the get_type() function ensures that there is ever only one instance, there is no danger of any weird recursion. Further, we never ask anything of the member instance, we will only use its handle value.
The abstract base class provides the prototype of the get_type_handle() function. Note that the return type is base. The base handle of the type handle is returned In the container implementation of get_type_handle().
The essence of the patter is that it brings the compile-time notion of type into the run-time environment (but without the details).
Using Type Handles
Our type handle pattern gives us a way to find out what type of thing is in the container. This enables us to process the contents in a type-specific way. For example:
case(my_container.get_type_handle()) container#(int)::get_type() : // int containers container#(real)::get_type() : // real containers container#(string)::get_type() : // string containers default: $display("Uh oh! illegal container type"); endcase
Of course, you can compare type handles to see if they are equivalent:
if(some_container.get_type_handle() == other_container.get_type_handle()) do_something(); else do_something_else();
Notice that we use the static function container#()::get_type() to represent constants or hard-coded type values. The non-static function get_type_handle() is used to retrieve the base class handle of our static type handle. Get_type_handle() can be used to identify the type of an instance of the container, whereas get_type() is used to set the value of the type handle.
As an example, consider a protocol with different packet types. Each type of packet has different number of bytes, and the way each is mapped to the bus is different. We want to build a driver that can easily deal with the differences in packet types. The logical thing is to create a set of sequence items to represent each of the packet types. Now the problem is how to process each packet type differently.
The correct thing to do from an OOP perspective is to create a virtual function (or set of virtual functions) whose role is to map the data to the bus. Each subclass will have a different implementation that knows how the packet is organized and how it should be mapped to the bus. The problem is that this may require knowledge of the bus or the bus protocol that you do not want to code into the sequence item. Only the transactor has knowledge of the bus protocol details.
You can create a type handle in the sequence item that has a unique value for each packet type. The transactor can use a case statement (similar to the one above) to process the packets in a type-specific manner. A type handle may be preferable to an enum in this case, because both the upstream sequences and downstream transactor are not required to have access to the enum type.
class protocol_seq_base extends uvm_sequence_item; pure virtual function protocol_seq_base get_type_handle(); endclass class protocol_seq#(type T) extends seq_base; typedef protocol_seq#(T) this_type; static this_type type_handle = get_type(); ... endclass
The sequences items can be processed in the driver in a type-specific manner.
task run_phase(uvm_phase phase); forever begin seq_item = seq_item_port.get_next_item(); case (seq_item.get_type_handle()) protocol_seq#(big_pkt_t)::id() :; // process big pkt protocol_seq#(med_pkt_t):i:d() :; // process medium pkt protocol_deq#(sml_pkt_t)::id() :; // process small pkt endcase end endtask
We used the container-based pattern. Protocol_seq_base is the non-parameterized base class that contains the prototype for get_type_handle(). The parameterized protocol_seq#() class is the container that contains the static and non-static elements of our pattern.
Type Handles in UVM
The type handle pattern is used in several places in UVM. The factory uses type handles to store overrides by type. Though, in the factory type handles are called object wrappers (uvm_object_wrapper). The container in this case is uvm_object_registry#(T,Tname). The header looks like this:
class uvm_object_registry #(type T, string Tname) extends uvm_object_wrapper; endclass
The object wrapper, like the type handle base class in our pattern, is also an abstract base class. The method that is provided by the abstract interface is create(). The create() call is delegated to the object registry which calls new() based on the type T. The restriction is that T must be derived from uvm_object. Object wrappers, which are all unique by type, are used as keys in an associative array where the overriding object wrapper is stored.
There is a corresponding uvm_component_registry#() class for building components of different types. I don't want to get too deep into the factory here. Perhaps in the future I'll write an article on the factory where I can get into the nitty-gritty details. The point here is that the factory uses the type handle pattern to manage type overrides.
The resource database also uses the type handle pattern in a bit more recognizable fashion to store and retrieve resources by type. The non-parameterized base class for resource containers is uvm_resource_base. It contains the pure virtual function get_type_handle(), just like our pattern. The parameterized class uvm_resource#(T) contains the static function get_type() and the non-static function get_type_handle(), again just like our pattern. These methods are used in the same way as the pattern.
The third place type handles are used in UVM is in the TLM generic payload extensions. TLM extensions, as they are commonly referred, are used to pass user-defined data as part of the otherwise rigidly defined generic payload. IEEE-1666-2012, the current SystemC standard, upon which UVM TLM was built, requires that there can only be one extension of any one type attached to an instance of a generic payload (aka GP). To enforce that rule in UVM we need to know the type of each extension. Type handles are the mechanism employed to do that. The non-parameterized base class is uvm_tlm_extension_base, and the parameterized extension container is uvm_tlm_extension#(T). The static function that returns the singleton type handle is called ID() rather than get_type().
Generic Type Handles
Using a slight modification of our container-based pattern, we can create a generic type handle that is independent of any particular container type. Let's look at the code for a generic type handle first, and then we'll discuss the implementation.
virtual class type_handle; pure virtual function type_handle id(); pure virtual function string name(); endclass class type_handle_generator#(type T) extends type_handle; typedef type_handle_generator#(T) this_type; static this_type handle = get_type(); static function this_type get_type(); if(handle == null) handle = new(); return handle; endfunction function type_handle id(); return get_type(); endfunction function string name(); return $typename(T); endfunction endclass
The pattern is mostly identical to our container-based type handle, only some of the names have been changed to be more descriptive for this application. We've also added the function name() to our abstract base class as a convenience. Our container class has been replaced by type_handle_generator#() class. The abstract base class is called type_handle. To minimize typing and keep our code a little cleaner, we changed get_type_handle() to simply id(), borrowing from the economical naming style in the UVM TLM GP extension facility. Otherwise the organization and operation is the same.
Since our generic type handle pattern is not associated with any particular container we can use it with any container. E.g.
class some_container#(type T); const type_handle th = type_handle_generator#(T)::id(); ... endclass class some_other_container#(type T); const type_handle th = type_handle_generator#(T)::id(); ... endclass
Now in addition to the other things we can do with container based type handles, we can also compare type handles between container types. For example:
some_container#(int) c1; some_other_container#(real) c2; if(c1.th == c2.th) do_something();
A Few Nuances
- Type handles cannot be used to dynamically determine the type of an object. You could use type references for this -- if the feature were implemented in your favorite simulator.
- Type handles must be initialized statically. We are grabbing compile time information about types and making available at run time.
- When you create new type handles (e.g. using the type_handle_generator#()) the compiler will use the type matching rules to determine whether to create a new specialization of the generator or not. Consider the following example:
typedef bit[63:0] addr_t; type_handle h1 = type_handle_generator#(addr_t)::id(); type_handle h2 = type_handle_generator#(bit[63:0])::id();
Because of the typedef, addr_t is an alias for bit[63:0]. By the type matching rules defined in section 6.22.1 of the IEEE-1800-2012 LRM these types are identical. The compiler will not create a new type handle specialization for the second one. That is h1 == h2.
- While the compiler will not create new specializations of the type handle generator for matching types, it will create new specializations for types that are only equivalent or assignment compatible.
- The container style type handle requires that the container contain an instance of itself. The memory consumed is dependent on the size of the container. If T is a large class the memory usage could be large. The generic type handle, on the other hand, doesn't contain anything except an instance of the handle. Memory usage for the generic type handles is much more efficient.
Type handles are a very effective way of dealing with type information at run time. You can use type handles to identify the type of contents of a container. Or, you can use the generic type handle whose implementation is not tied to any particular container type. In any case, please experiment with type handles. Get creative -- find new ways to use them and perhaps make improvements to the pattern.
No matter how you use them, have fun!.